Skip to content

Instantly share code, notes, and snippets.

@jrburke
Created June 14, 2013 18:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jrburke/85a05a65314b03e07768 to your computer and use it in GitHub Desktop.
Save jrburke/85a05a65314b03e07768 to your computer and use it in GitHub Desktop.
Gaia diff for the app/email folder between: b161f42c5647b37e35ba5a20f394ba40bf674d9c 1aed138dfef6279a00475f51ce0c491f32799ab6 Related to https://bugzilla.mozilla.org/show_bug.cgi?id=871826
diff --git a/apps/email/index.html b/apps/email/index.html
index 550ed1f..65ba063 100755
--- a/apps/email/index.html
+++ b/apps/email/index.html
@@ -10,6 +10,7 @@
<link rel="stylesheet" type="text/css" href="style/mail.css">
<link rel="stylesheet" type="text/css" href="style/message-cards.css">
<link rel="stylesheet" type="text/css" href="style/folder-cards.css">
+ <link rel="stylesheet" type="text/css" href="style/marquee.css">
<!-- Include shared building blocks -->
<link rel="stylesheet" type="text/css" href="shared/style/headers.css"/>
<link rel="stylesheet" type="text/css" href="shared/style/switches.css"/>
@@ -32,8 +33,10 @@
<script type="text/javascript" defer src="js/input_areas.js"></script>
<script type="text/javascript" defer src="js/console-hook.js"></script>
<script type="text/javascript" defer src="js/mail-common.js"></script>
+ <script type="text/javascript" defer src="js/message_list_topbar.js"></script>
<script type="text/javascript" defer src="js/message-cards.js"></script>
<script type="text/javascript" defer src="js/folder-cards.js"></script>
+ <script type="text/javascript" defer src="js/marquee.js"></script>
<!-- This one is an optimized vendor library containing our mail backend. -->
<!-- START BACKEND INJECT - do not modify -->
<script type="text/javascript" defer src="js/ext/mailapi/main-frame-setup.js"></script>
@@ -219,6 +222,8 @@
<div class="bottom-toolbar-spacer"></div>
</div>
</div>
+ <!-- New email notification bar -->
+ <div class="msg-list-topbar collapsed"></div>
<!-- Conveys background send, plus undo-able recent actions -->
<div class="msg-activity-infobar hidden">
</div>
@@ -779,13 +784,8 @@
<span data-l10n-id="settings-default-account" class="list-text">
DefaulT AccounT</span>
<em class="aside end">
- <label>
- <!-- When the input is focused, a resize event will be fired
- and will trigger _showCard() in mail-common.js
- the UI state will become wrong after _showCard() is called
- so disable the input first to prevent the wrong state
- -->
- <input disabled type="checkbox" checked="checked">
+ <label class="tng-default-label">
+ <input class="tng-default-input" type="checkbox">
<span></span>
</label>
</em>
@@ -1005,6 +1005,15 @@
</button>
   </menu>
 </form>
+ <form role="dialog" class="msg-attachment-disabled-confirm" data-type="confirm">
+ <section>
+ <p><span data-l10n-id="message-send-attachment-disabled-confirm"></span></p>
+ </section>
+ <menu>
+ <button id="msg-attachment-disabled-cancel" data-l10n-id="message-multiedit-cancel">CanceL</button>
+ <button id="msg-attachment-disabled-ok" data-l10n-id="dialog-button-ok">OK</button>
+ </menu>
+ </form>
<form role="dialog" class="msg-delete-confirm" data-type="confirm">
<section>
<h1 data-l10n-id="confirm-dialog-title">ConfirmatioN</h1>
@@ -1025,6 +1034,15 @@
<button id="msg-browse-ok" class="recommend" data-l10n-id="dialog-button-ok">OK</button>
</menu>
</form>
+ <form role="dialog" class="msg-attach-confirm" data-type="confirm">
+ <section>
+ <h1></h1>
+ <p></p>
+ </section>
+ <menu>
+ <button id="msg-attach-ok" class="full" data-l10n-id="dialog-button-ok">OK</button>
+ </menu>
+ </form>
</div>
<!-- Widget nodes for the compose cards; styles use the cmp prefix -->
<div id="templ-cmp">
diff --git a/apps/email/js/compose-cards.js b/apps/email/js/compose-cards.js
index 641931d..65d78ec 100644
--- a/apps/email/js/compose-cards.js
+++ b/apps/email/js/compose-cards.js
@@ -5,6 +5,11 @@
**/
/**
+ * Max composer attachment size is defined as 5120000 bytes.
+ */
+var MAX_ATTACHMENT_SIZE = 5120000;
+
+/**
* To make it easier to focus input boxes, we have clicks on their owning
* container cause a focus event to occur on the input. This method helps us
* also position the cursor based on the location of the click so the cursor
@@ -359,8 +364,13 @@ ComposeCard.prototype = {
if (target.classList.contains('cmp-peep-bubble')) {
var contents = cmpNodes['contact-menu'].cloneNode(true);
var email = target.querySelector('.cmp-peep-address').textContent;
- contents.getElementsByTagName('header')[0].textContent = email;
+ var headerNode = contents.getElementsByTagName('header')[0];
+ // Setup the marquee structure
+ Marquee.setup(email, headerNode);
+ // Activate marquee once the contents DOM are added to document
document.body.appendChild(contents);
+ Marquee.activate('alternate', 'ease');
+
var formSubmit = (function(evt) {
document.body.removeChild(contents);
switch (evt.explicitOriginalTarget.className) {
@@ -411,8 +421,41 @@ ComposeCard.prototype = {
attTemplate.getElementsByClassName('cmp-attachment-filename')[0],
filesizeTemplate =
attTemplate.getElementsByClassName('cmp-attachment-filesize')[0];
+ var totalSize = 0;
for (var i = 0; i < this.composer.attachments.length; i++) {
var attachment = this.composer.attachments[i];
+ //check for attachment max size
+ if ((totalSize + attachment.blob.size) > MAX_ATTACHMENT_SIZE) {
+
+ /*Remove all the remaining attachments from composer*/
+ while (this.composer.attachments.length > i) {
+ this.composer.removeAttachment(this.composer.attachments[i]);
+ }
+ var dialog = msgNodes['attach-confirm'].cloneNode(true);
+ var title = dialog.getElementsByTagName('h1')[0];
+ var content = dialog.getElementsByTagName('p')[0];
+
+ if (this.composer.attachments.length > 0) {
+ title.textContent = mozL10n.get('composer-attachments-large');
+ content.textContent =
+ mozL10n.get('compose-attchments-size-exceeded');
+ } else {
+ title.textContent = mozL10n.get('composer-attachment-large');
+ content.textContent =
+ mozL10n.get('compose-attchment-size-exceeded');
+ }
+ ConfirmDialog.show(dialog,
+ {
+ // ok
+ id: 'msg-attach-ok',
+ handler: function() {
+ this.updateAttachmentsSize();
+ }.bind(this)
+ }
+ );
+ return;
+ }
+ totalSize = totalSize + attachment.blob.size;
filenameTemplate.textContent = attachment.name;
filesizeTemplate.textContent = prettyFileSize(attachment.blob.size);
var attachmentNode = attTemplate.cloneNode(true);
diff --git a/apps/email/js/contacts.js b/apps/email/js/contacts.js
deleted file mode 100644
index 7a0c1a7..0000000
--- a/apps/email/js/contacts.js
+++ /dev/null
@@ -1,28 +0,0 @@
-/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
-/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
-
-'use strict';
-
-var ContactDataManager = {
- contactData: {},
-
- searchContactData: function cm_searchContactData(string, callback) {
- var options = {
- filterBy: ['email'],
- filterOp: 'contains',
- filterValue: string
- };
-
- var self = this;
- var req = window.navigator.mozContacts.find(options);
- req.onsuccess = function onsuccess() {
- callback(req.result);
- };
-
- req.onerror = function onerror() {
- var msg = 'Contact finding error. Error: ' + req.errorCode;
- console.log(msg);
- callback(null);
- };
- }
-};
diff --git a/apps/email/js/ext/mailapi/activesync/configurator.js b/apps/email/js/ext/mailapi/activesync/configurator.js
index 7442295..46c4d18 100644
--- a/apps/email/js/ext/mailapi/activesync/configurator.js
+++ b/apps/email/js/ext/mailapi/activesync/configurator.js
@@ -1386,6 +1386,10 @@ ActiveSyncFolderConn.prototype = {
/**
* Download the bodies for a set of headers.
+ *
+ * XXX This method is a slightly modified version of
+ * ImapFolderConn._lazyDownloadBodies; we should attempt to remove the
+ * duplication.
*/
downloadBodies: function(headers, options, callback) {
if (this._account.conn.currentVersion.lt('12.0'))
@@ -1393,6 +1397,7 @@ ActiveSyncFolderConn.prototype = {
var anyErr,
pending = 1,
+ downloadsNeeded = 0,
folderConn = this;
function next(err) {
@@ -1401,16 +1406,26 @@ ActiveSyncFolderConn.prototype = {
if (!--pending) {
folderConn._storage.runAfterDeferredCalls(function() {
- callback(anyErr);
+ callback(anyErr, /* number downloaded */ downloadsNeeded - pending);
});
}
}
for (var i = 0; i < headers.length; i++) {
- if (!headers[i] || headers[i].snippet)
+ // We obviously can't do anything with null header references.
+ // To avoid redundant work, we also don't want to do any fetching if we
+ // already have a snippet. This could happen because of the extreme
+ // potential for a caller to spam multiple requests at us before we
+ // service any of them. (Callers should only have one or two outstanding
+ // jobs of this and do their own suppression tracking, but bugs happen.)
+ if (!headers[i] || headers[i].snippet !== null) {
continue;
+ }
pending++;
+ // This isn't absolutely guaranteed to be 100% correct, but is good enough
+ // for indicating to the caller that we did some work.
+ downloadsNeeded++;
this.downloadBodyReps(headers[i], options, next);
}
@@ -1684,14 +1699,19 @@ ActiveSyncFolderConn.prototype = {
if (!moreAvailable) {
var messagesSeen = addedMessages + changedMessages + deletedMessages;
- // Note: For the second argument here, we report the number of messages
- // we saw that *changed*. This differs from IMAP, which reports the
- // number of messages it *saw*.
- folderConn._LOG.sync_end(addedMessages, changedMessages,
- deletedMessages);
- storage.markSyncRange($sync.OLDEST_SYNC_DATE, accuracyStamp, 'XXX',
- accuracyStamp);
- doneCallback(null, null, messagesSeen);
+ // Do not report completion of sync until all of our operations have
+ // been persisted to our in-memory database. (We do not wait for
+ // things to hit the disk.)
+ storage.runAfterDeferredCalls(function() {
+ // Note: For the second argument here, we report the number of
+ // messages we saw that *changed*. This differs from IMAP, which
+ // reports the number of messages it *saw*.
+ folderConn._LOG.sync_end(addedMessages, changedMessages,
+ deletedMessages);
+ storage.markSyncRange($sync.OLDEST_SYNC_DATE, accuracyStamp, 'XXX',
+ accuracyStamp);
+ doneCallback(null, null, messagesSeen);
+ });
}
},
progressCallback);
@@ -2450,6 +2470,8 @@ ActiveSyncJobDriver.prototype = {
do_downloadBodies: $jobmixins.do_downloadBodies,
+ check_downloadBodies: $jobmixins.check_downloadBodies,
+
//////////////////////////////////////////////////////////////////////////////
// downloadBodyReps: Download the bodies from a single message
@@ -2457,6 +2479,8 @@ ActiveSyncJobDriver.prototype = {
do_downloadBodyReps: $jobmixins.do_downloadBodyReps,
+ check_downloadBodyReps: $jobmixins.check_downloadBodyReps,
+
//////////////////////////////////////////////////////////////////////////////
// download: Download one or more attachments from a single message
@@ -2716,6 +2740,8 @@ ActiveSyncAccount.prototype = {
path: this.accountDef.name,
type: this.accountDef.type,
+ defaultPriority: this.accountDef.defaultPriority,
+
enabled: this.enabled,
problems: this.problems,
@@ -3054,7 +3080,10 @@ ActiveSyncAccount.prototype = {
/**
* Recreate the folder storage for a particular folder; useful when we end up
- * desyncing with the server and need to start fresh.
+ * desyncing with the server and need to start fresh. No notification is
+ * generated, although slices are repopulated.
+ *
+ * FYI: There is a nearly identical method in IMAP's account implementation.
*
* @param {string} folderId the local ID of the folder
* @param {function} callback a function to be called when the operation is
@@ -3373,6 +3402,7 @@ define('mailapi/activesync/configurator',
'../accountcommon',
'../a64',
'./account',
+ '../date',
'require',
'exports'
],
@@ -3381,6 +3411,7 @@ define('mailapi/activesync/configurator',
$accountcommon,
$a64,
$asacct,
+ $date,
require,
exports
) {
@@ -3495,6 +3526,7 @@ exports.configurator = {
var accountDef = {
id: accountId,
name: userDetails.accountName || userDetails.emailAddress,
+ defaultPriority: $date.NOW(),
type: 'activesync',
syncRange: 'auto',
diff --git a/apps/email/js/ext/mailapi/chewlayer.js b/apps/email/js/ext/mailapi/chewlayer.js
index 9a2829c..6437317 100644
--- a/apps/email/js/ext/mailapi/chewlayer.js
+++ b/apps/email/js/ext/mailapi/chewlayer.js
@@ -2193,37 +2193,33 @@ var HTMLParser = (function(){
//
// The spec defines attributes by what they must not include, which is:
// [\0\s"'>/=] plus also no control characters, or non-unicode characters.
- // But we currently use the same regexp as we use for tags because that's what
- // the code was using already.
+ //
+ // The (inherited) code used to have the regular expression effectively
+ // validate the attribute syntax by including their grammer in the regexp.
+ // The problem with this is that it can make the regexp fail to match tags
+ // that are clearly tags. When we encountered (quoted) attributes without
+ // whitespace between them, we would escape the entire tag. Attempted
+ // trivial fixes resulted in regex back-tracking, which begged the issue of
+ // why the regex would do this in the first place. So we stopped doing that.
//
// CDATA *is not a thing* in the HTML namespace. <![CDATA[ just gets treated
// as a "bogus comment". See:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#markup-declaration-open-state
- // NOTE: tag and attr regexps changed to ignore name spaces prefixes! via
+ // NOTE: tag and attr regexps changed to ignore name spaces prefixes!
+ //
+ // CHANGE: "we" previously required there to be white-space between attributes.
+ // Unfortunately, the world does not agree with this, so we now require
+ // whitespace only after the tag name prior to the first attribute and make
+ // the whole attribute clause optional.
+ //
// - Regular Expressions for parsing tags and attributes
// ^< anchored tag open character
// (?:[-A-Za-z0-9_]+:)? eat the namespace
// ([-A-Za-z0-9_]+) the tag name
- // ( repeated attributes:
- // (?:
- // \s+ Mandatory whitespace between attribute names
- // (?:[-A-Za-z0-9_]+:)? optional attribute prefix
- // [-A-Za-z0-9_]+ attribute name
- // (?: The attribute doesn't need a value
- // \s*=\s* whitespace, = to indicate value, whitespace
- // (?: attribute values:
- // (?:"[^"]*")| double-quoted
- // (?:'[^']*')| single-quoted
- // [^>\s]+ unquoted
- // )
- // )? (the attribute does't need a value)
- // )* (there can be multiple attributes)
- // ) (capture the list of attributes)
- // \s* optional whitespace before the tag closer
- // (\/?) optional self-closing character
+ // ([^>]*) capture attributes and/or closing '/' if present
// > tag close character
- var startTag = /^<(?:[-A-Za-z0-9_]+:)?([-A-Za-z0-9_]+)((?:\s+(?:[-A-Za-z0-9_]+:)?[-A-Za-z0-9_]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
+ var startTag = /^<(?:[-A-Za-z0-9_]+:)?([-A-Za-z0-9_]+)([^>]*)>/,
// ^<\/ close tag lead-in
// (?:[-A-Za-z0-9_]+:)? optional tag prefix
// ([-A-Za-z0-9_]+) tag name
@@ -2379,7 +2375,7 @@ var HTMLParser = (function(){
// Clean up any remaining tags
parseEndTag();
- function parseStartTag( tag, tagName, rest, unary ) {
+ function parseStartTag( tag, tagName, rest ) {
tagName = tagName.toLowerCase();
if ( block[ tagName ] ) {
while ( stack.last() && inline[ stack.last() ] ) {
@@ -2391,7 +2387,13 @@ var HTMLParser = (function(){
parseEndTag( "", tagName );
}
- unary = empty[ tagName ] || !!unary;
+ var unary = empty[ tagName ];
+ // to simplify the regexp, the 'rest capture group now absorbs the /, so
+ // we need to strip it off if it's there.
+ if (rest.length && rest[rest.length - 1] === '/') {
+ unary = true;
+ rest = rest.slice(0, -1);
+ }
if ( !unary )
stack.push( tagName );
@@ -3921,13 +3923,16 @@ function chewStructure(msg) {
sizeEstimate: partInfo.size,
amountDownloaded: 0,
// its important to know that sizeEstimate and amountDownloaded
- // do _not_ determine if the bodyRep is fully downloaded the
+ // do _not_ determine if the bodyRep is fully downloaded; the
// estimated amount is not reliable
- isDownloaded: false,
+ // Zero-byte bodies are assumed to be accurate and we treat the file
+ // as already downloaded.
+ isDownloaded: partInfo.size === 0,
// full internal IMAP representation
// it would also be entirely appropriate to move
// the information on the bodyRep directly?
- _partInfo: partInfo
+ _partInfo: partInfo.size ? partInfo : null,
+ content: ''
};
}
@@ -4114,7 +4119,7 @@ exports.updateMessageWithFetch = function(header, body, req, res, _LOG) {
bodyRep.isDownloaded = true;
// clear private space for maintaining parser state.
- delete bodyRep._partInfo;
+ bodyRep._partInfo = null;
}
if (!bodyRep.isDownloaded && res.buffer) {
diff --git a/apps/email/js/ext/mailapi/composite/configurator.js b/apps/email/js/ext/mailapi/composite/configurator.js
index 6992434..1bf6fd8 100644
--- a/apps/email/js/ext/mailapi/composite/configurator.js
+++ b/apps/email/js/ext/mailapi/composite/configurator.js
@@ -269,6 +269,7 @@ define('mailapi/imap/folder',
var $imaptextparser = null;
var $imapsnippetparser = null;
var $imapbodyfetcher = null;
+var $imapchew = null;
var $imapsync = null;
var allbackMaker = $allback.allbackMaker,
@@ -448,12 +449,14 @@ ImapFolderConn.prototype = {
/**
* If no connection, acquires one and also sets up
* deathback if connection is lost.
+ *
+ * See `acquireConn` for argument docs.
*/
- withConnection: function (callback, deathback, label) {
+ withConnection: function (callback, deathback, label, dieOnConnectFailure) {
if (!this._conn) {
this.acquireConn(function () {
this.withConnection(callback, deathback, label);
- }.bind(this), deathback, label);
+ }.bind(this), deathback, label, dieOnConnectFailure);
return;
}
@@ -813,6 +816,14 @@ console.log('BISECT CASE', serverUIDs.length, 'curDaysDelta', curDaysDelta);
overallMaximumBytes -= rep.sizeEstimate;
}
+ // For a byte-serve request, we need to request at least 1 byte, so
+ // request some bytes. This is a logic simplification that should not
+ // need to be used because imapchew.js should declare 0-byte files
+ // fully downloaded when their parts are created, but better a wasteful
+ // network request than breaking here.
+ if (bytesToFetch <= 0)
+ bytesToFetch = 64;
+
// we may only need a subset of the total number of bytes.
if (overallMaximumBytes !== undefined || rep.amountDownloaded) {
// request the remainder
@@ -927,7 +938,7 @@ console.log('BISECT CASE', serverUIDs.length, 'curDaysDelta', curDaysDelta);
* Download snippets for a set of headers.
*/
_lazyDownloadBodies: function(headers, options, callback) {
- var pending = 1;
+ var pending = 1, downloadsNeeded = 0;
var self = this;
var anyErr;
@@ -937,17 +948,26 @@ console.log('BISECT CASE', serverUIDs.length, 'curDaysDelta', curDaysDelta);
if (!--pending) {
self._storage.runAfterDeferredCalls(function() {
- callback(anyErr);
+ callback(anyErr, /* number downloaded */ downloadsNeeded - pending);
});
}
}
for (var i = 0; i < headers.length; i++) {
- if (!headers[i] || headers[i].snippet) {
+ // We obviously can't do anything with null header references.
+ // To avoid redundant work, we also don't want to do any fetching if we
+ // already have a snippet. This could happen because of the extreme
+ // potential for a caller to spam multiple requests at us before we
+ // service any of them. (Callers should only have one or two outstanding
+ // jobs of this and do their own suppression tracking, but bugs happen.)
+ if (!headers[i] || headers[i].snippet !== null) {
continue;
}
pending++;
+ // This isn't absolutely guaranteed to be 100% correct, but is good enough
+ // for indicating to the caller that we did some work.
+ downloadsNeeded++;
this.downloadBodyReps(headers[i], options, next);
}
@@ -1160,14 +1180,34 @@ ImapFolderSyncer.prototype = {
initialSync: function(slice, initialDays, syncCallback,
doneCallback, progressCallback) {
syncCallback('sync', false);
- this._startSync(
- slice, PASTWARDS, // sync into the past
- 'grow',
- null, // start syncing from the (unconstrained) future
- $sync.OLDEST_SYNC_DATE, // sync no further back than this constant
- null,
- initialDays,
- doneCallback, progressCallback);
+ // We want to enter the folder and get the box info so we can know if we
+ // should trigger our SYNC_WHOLE_FOLDER_AT_N_MESSAGES logic.
+ // _timelySyncSearch is what will get called next either way, and it will
+ // just reuse the connection and will correctly update the deathback so
+ // that our deathback is no longer active.
+ this.folderConn.withConnection(
+ function(folderConn, storage) {
+ // Flag to sync the whole range if we
+ var syncWholeTimeRange = false;
+ if (folderConn && folderConn.box &&
+ folderConn.box.messages.total <
+ $sync.SYNC_WHOLE_FOLDER_AT_N_MESSAGES) {
+ syncWholeTimeRange = true;
+ }
+
+ this._startSync(
+ slice, PASTWARDS, // sync into the past
+ 'grow',
+ null, // start syncing from the (unconstrained) future
+ $sync.OLDEST_SYNC_DATE, // sync no further back than this constant
+ null,
+ syncWholeTimeRange ? null : initialDays,
+ doneCallback, progressCallback);
+ }.bind(this),
+ function died() {
+ doneCallback('aborted');
+ },
+ 'initialSync', true);
},
/**
@@ -1491,27 +1531,27 @@ console.log("folder message count", folderMessageCount,
daysToSearch = Math.ceil(this._curSyncDayStep *
$sync.TIME_SCALE_FACTOR_ON_NO_MESSAGES);
+ // These values used to be more conservative, but the importance of these
+ // guards was reduced when we switched to only syncing headers.
+ // At current constants (sync=3, scale=2), our doubling in the face of
+ // clamping is: 3, 6, 12, 24, 45, ... 90,
if (lastSyncDaysInPast < 180) {
- if (daysToSearch > 14)
- daysToSearch = 14;
+ if (daysToSearch > 45)
+ daysToSearch = 45;
}
- else if (lastSyncDaysInPast < 365) {
- if (daysToSearch > 30)
- daysToSearch = 30;
- }
- else if (lastSyncDaysInPast < 730) {
- if (daysToSearch > 60)
- daysToSearch = 60;
- }
- else if (lastSyncDaysInPast < 1095) {
+ else if (lastSyncDaysInPast < 365) { // 1 year
if (daysToSearch > 90)
daysToSearch = 90;
}
- else if (lastSyncDaysInPast < 1825) { // 5 years
+ else if (lastSyncDaysInPast < 730) { // 2 years
if (daysToSearch > 120)
daysToSearch = 120;
}
- else if (lastSyncDaysInPast < 3650) {
+ else if (lastSyncDaysInPast < 1825) { // 5 years
+ if (daysToSearch > 180)
+ daysToSearch = 180;
+ }
+ else if (lastSyncDaysInPast < 3650) { // 10 years
if (daysToSearch > 365)
daysToSearch = 365;
}
@@ -1850,7 +1890,11 @@ ImapJobDriver.prototype = {
});
action();
- }, deathback, label);
+ },
+ // Always pass true for dieOnConnectFailure; we don't want any of our
+ // operations hanging out waiting for retry backoffs. The ops want to
+ // only run when we believe we are online with a good connection.
+ deathback, label, true);
} else {
action();
}
@@ -1900,6 +1944,8 @@ ImapJobDriver.prototype = {
do_downloadBodies: $jobmixins.do_downloadBodies,
+ check_downloadBodies: $jobmixins.check_downloadBodies,
+
//////////////////////////////////////////////////////////////////////////////
// downloadBodyReps: Download the bodies from a single message
@@ -1907,6 +1953,8 @@ ImapJobDriver.prototype = {
do_downloadBodyReps: $jobmixins.do_downloadBodyReps,
+ check_downloadBodyReps: $jobmixins.check_downloadBodyReps,
+
//////////////////////////////////////////////////////////////////////////////
// download: Download one or more attachments from a single message
@@ -2972,6 +3020,50 @@ ImapAccount.prototype = {
},
/**
+ * Completely reset the state of a folder. For use by unit tests and in the
+ * case of UID validity rolls. No notification is generated, although slices
+ * are repopulated.
+ *
+ * FYI: There is a nearly identical method in ActiveSync's account
+ * implementation.
+ */
+ _recreateFolder: function(folderId, callback) {
+ this._LOG.recreateFolder(folderId);
+ var folderInfo = this._folderInfos[folderId];
+ folderInfo.$impl = {
+ nextId: 0,
+ nextHeaderBlock: 0,
+ nextBodyBlock: 0,
+ };
+ folderInfo.accuracy = [];
+ folderInfo.headerBlocks = [];
+ folderInfo.bodyBlocks = [];
+ // IMAP does not use serverIdHeaderBlockMapping
+
+ if (this._deadFolderIds === null)
+ this._deadFolderIds = [];
+ this._deadFolderIds.push(folderId);
+
+ var self = this;
+ this.saveAccountState(null, function() {
+ var newStorage =
+ new $mailslice.FolderStorage(self, folderId, folderInfo, self._db,
+ $imapfolder.ImapFolderSyncer,
+ self._LOG);
+ for (var iter in Iterator(self._folderStorages[folderId]._slices)) {
+ var slice = iter[1];
+ slice._storage = newStorage;
+ slice.reset();
+ newStorage.sliceOpenMostRecent(slice);
+ }
+ self._folderStorages[folderId]._slices = [];
+ self._folderStorages[folderId] = newStorage;
+
+ callback(newStorage);
+ }, 'recreateFolder');
+ },
+
+ /**
* We are being told that a synchronization pass completed, and that we may
* want to consider persisting our state.
*/
@@ -2989,10 +3081,10 @@ ImapAccount.prototype = {
* that ever ends up not being the case that we need to cause mutating
* operations to defer until after that snapshot has occurred.
*/
- saveAccountState: function(reuseTrans, callback) {
+ saveAccountState: function(reuseTrans, callback, reason) {
if (!this._alive) {
this._LOG.accountDeleted('saveAccountState');
- return;
+ return null;
}
var perFolderStuff = [], self = this;
@@ -3003,7 +3095,7 @@ ImapAccount.prototype = {
if (folderStuff)
perFolderStuff.push(folderStuff);
}
- this._LOG.saveAccountState();
+ this._LOG.saveAccountState(reason);
var trans = this._db.saveAccountFolderStates(
this.id, this._folderInfos, perFolderStuff,
this._deadFolderIds,
@@ -3733,6 +3825,7 @@ var LOGFAB = exports.LOGFAB = $log.register($module, {
events: {
createFolder: {},
deleteFolder: {},
+ recreateFolder: { id: false },
createConnection: {},
reuseConnection: {},
@@ -3741,7 +3834,7 @@ var LOGFAB = exports.LOGFAB = $log.register($module, {
unknownDeadConnection: {},
connectionMismatch: {},
- saveAccountState: {},
+ saveAccountState: { reason: false },
/**
* XXX: this is really an error/warning, but to make the logging less
* confusing, treat it as an event.
@@ -4114,6 +4207,8 @@ CompositeAccount.prototype = {
name: this.accountDef.name,
type: this.accountDef.type,
+ defaultPriority: this.accountDef.defaultPriority,
+
enabled: this.enabled,
problems: this.problems,
@@ -4258,6 +4353,7 @@ define('mailapi/composite/configurator',
'../a64',
'../allback',
'./account',
+ '../date',
'require',
'exports'
],
@@ -4267,6 +4363,7 @@ define('mailapi/composite/configurator',
$a64,
$allback,
$account,
+ $date,
require,
exports
) {
@@ -4393,6 +4490,7 @@ exports.configurator = {
var accountDef = {
id: accountId,
name: userDetails.accountName || userDetails.emailAddress,
+ defaultPriority: $date.NOW(),
type: 'imap+smtp',
receiveType: 'imap',
diff --git a/apps/email/js/ext/mailapi/main-frame-setup.js b/apps/email/js/ext/mailapi/main-frame-setup.js
index d9d4f46..b10d558 100644
--- a/apps/email/js/ext/mailapi/main-frame-setup.js
+++ b/apps/email/js/ext/mailapi/main-frame-setup.js
@@ -142,13 +142,16 @@ var HEADER_CACHE_LIMIT = 8;
/**
*
*/
-function MailAccount(api, wireRep) {
+function MailAccount(api, wireRep, acctsSlice) {
this._api = api;
this.id = wireRep.id;
// Hold on to wireRep for caching
this._wireRep = wireRep;
+ // Hold on to acctsSlice for use in determining default account.
+ this.acctsSlice = acctsSlice;
+
this.type = wireRep.type;
this.name = wireRep.name;
this.syncRange = wireRep.syncRange;
@@ -209,6 +212,11 @@ MailAccount.prototype = {
__update: function(wireRep) {
this.enabled = wireRep.enabled;
this.problems = wireRep.problems;
+ this._wireRep.defaultPriority = wireRep.defaultPriority;
+ },
+
+ __die: function() {
+ // currently, nothing to clean up
},
/**
@@ -224,7 +232,11 @@ MailAccount.prototype = {
* @param[mods @dict[
* @key[password String]
* ]]
- * ]
+ * ]{
+ * In addition to regular account property settings,
+ * "setAsDefault": true can be passed to set this
+ * account as the default acccount.
+ * }
*/
modifyAccount: function(mods) {
this._api._modifyAccount(this, mods);
@@ -238,6 +250,17 @@ MailAccount.prototype = {
deleteAccount: function() {
this._api._deleteAccount(this);
},
+
+ /**
+ * Returns true if this account is the default account, by looking at
+ * all accounts in the acctsSlice.
+ */
+ get isDefault() {
+ if (!this.acctsSlice)
+ throw new Error('No account slice available');
+
+ return this.acctsSlice.defaultAccount === this;
+ },
};
/**
@@ -268,6 +291,10 @@ MailSenderIdentity.prototype = {
toJSON: function() {
return { type: 'MailSenderIdentity' };
},
+
+ __die: function() {
+ // nothing to clean up currently
+ },
};
function MailFolder(api, wireRep) {
@@ -351,6 +378,10 @@ MailFolder.prototype = {
this.lastSyncedAt = wireRep.lastSyncedAt ? new Date(wireRep.lastSyncedAt)
: null;
},
+
+ __die: function() {
+ // currently nothing to clean up
+ }
};
function filterOutBuiltinFlags(flags) {
@@ -381,18 +412,44 @@ function serializeMessageName(x) {
}
/**
- * Caches contact lookups, both hits and misses.
+ * Caches contact lookups, both hits and misses, as well as updating the
+ * MailPeep instances returned by resolve calls.
+ *
+ * We maintain strong maps from both contact id and e-mail address to MailPeep
+ * instances. We hold a strong reference because BridgedViewSlices already
+ * require explicit lifecycle maintenance (aka call die() when done with them).
+ * We need the contact id and e-mail address because when a contact is changed,
+ * an e-mail address may be changed, and we don't get to see the old
+ * representation. So if the e-mail address was deleted, we need the contact id
+ * mapping. And if the e-mail address was added, we need the e-mail address
+ * mapping.
+ *
+ * If the mozContacts API is not available, we just create inert MailPeep
+ * instances that do not get tracked or updated.
+ *
+ * Domain notes:
+ *
+ * The contacts API does not enforce any constraints on the number of contacts
+ * who can use an e-mail address, but the e-mail app only allows one contact
+ * to correspond to an e-mail address at a time.
*/
-var ContactCache = {
+var ContactCache = exports.ContactCache = {
/**
* Maps e-mail addresses to the mozContact rep for the object, or null if
* there was a miss.
+ *
+ * We explicitly do not want to choose an arbitrary MailPeep instance to
+ * (re)use because it could lead to GC memory leaks if data/element/an expando
+ * were set on the MailPeep and we did not zero it out when the owning slice
+ * was destroyed. We could, however, use the live set of peeps as a fallback
+ * if we don't have a contact cached.
*/
- _cache: {},
+ _contactCache: Object.create(null),
/** The number of entries in the cache. */
_cacheHitEntries: 0,
/** The number of stored misses in the cache. */
_cacheEmptyEntries: 0,
+
/**
* Maximum number of hit entries in the cache before we should clear the
* cache.
@@ -400,6 +457,189 @@ var ContactCache = {
MAX_CACHE_HITS: 256,
/** Maximum number of empty entries to store in the cache before clearing. */
MAX_CACHE_EMPTY: 1024,
+
+ /** Maps contact id to lists of MailPeep instances. */
+ _livePeepsById: Object.create(null),
+ /** Maps e-mail addresses to lists of MailPeep instances */
+ _livePeepsByEmail: Object.create(null),
+
+ pendingLookupCount: 0,
+
+ callbacks: [],
+
+ init: function() {
+ var contactsAPI = navigator.mozContacts;
+ if (!contactsAPI)
+ return;
+
+ contactsAPI.oncontactchange = this._onContactChange.bind(this);
+ },
+
+ _resetCache: function() {
+ this._contactCache = Object.create(null);
+ this._cacheHitEntries = 0;
+ this._cacheEmptyEntries = 0;
+ },
+
+ shutdown: function() {
+ var contactsAPI = navigator.mozContacts;
+ if (!contactsAPI)
+ return;
+ contactsAPI.oncontactchange = null;
+ },
+
+ /**
+ * Currently we process the updates in real-time as we get them. There's an
+ * inherent trade-off between chewing CPU when we're in the background and
+ * minimizing latency when we are displayed. We're biased towards minimizing
+ * latency right now.
+ *
+ * All contact changes flush our contact cache rather than try and be fancy.
+ * We are already fancy with the set of live peeps and our lookups could just
+ * leverage that. (The contact cache is just intended as a steady-state
+ * high-throughput thing like when displaying messages in the UI. We don't
+ * expect a lot of contact changes to happen during that time.)
+ *
+ * For info on the events/triggers, see:
+ * https://developer.mozilla.org/en-US/docs/DOM/ContactManager.oncontactchange
+ */
+ _onContactChange: function(event) {
+ var contactsAPI = navigator.mozContacts;
+ var livePeepsById = this._livePeepsById,
+ livePeepsByEmail = this._livePeepsByEmail;
+
+ // clear the cache if it has anything in it (per the above doc block)
+ if (this._cacheHitEntries || this._cacheEmptyEntries)
+ this._resetCache();
+
+ // -- Contact removed OR all contacts removed!
+ if (event.reason === 'remove') {
+ function cleanOutPeeps(livePeeps) {
+ for (var iPeep = 0; iPeep < livePeeps.length; iPeep++) {
+ var peep = livePeeps[iPeep];
+ peep.contactId = null;
+ if (peep.onchange) {
+ try {
+ peep.onchange(peep);
+ }
+ catch (ex) {
+ reportClientCodeError('peep.onchange error', ex, '\n',
+ ex.stack);
+ }
+ }
+ }
+ }
+
+ // - all contacts removed! (clear() called)
+ var livePeeps;
+ if (!event.contactID) {
+ for (var contactId in livePeepsById) {
+ livePeeps = livePeepsById[contactId];
+ cleanOutPeeps(livePeeps);
+ this._livePeepsById = Object.create(null);
+ }
+ }
+ // - just one contact removed
+ else {
+ livePeeps = livePeepsById[event.contactID];
+ if (livePeeps) {
+ cleanOutPeeps(livePeeps);
+ delete livePeepsById[event.contactID];
+ }
+ }
+ }
+ // -- Created or updated; we need to fetch the contact to investigate
+ else {
+ var req = contactsAPI.find({
+ filterBy: ['id'],
+ filterOp: 'equals',
+ filterValue: event.contactID
+ });
+ req.onsuccess = function() {
+ // If the contact disappeared we will hear a 'remove' event and so don't
+ // need to process this.
+ if (!req.result.length)
+ return;
+ var contact = req.result[0], livePeeps, iPeep, peep;
+
+ // - process update with apparent e-mail address removal
+ if (event.reason === 'update') {
+ livePeeps = livePeepsById[contact.id];
+ if (livePeeps) {
+ var contactEmails = contact.email ?
+ contact.email.map(function(e) { return e.value; }) :
+ [];
+ for (iPeep = 0; iPeep < livePeeps.length; iPeep++) {
+ peep = livePeeps[iPeep];
+ if (contactEmails.indexOf(peep.address) === -1) {
+ // Need to fix-up iPeep because of the splice; reverse iteration
+ // reorders our notifications and we don't want that, hence
+ // this.
+ livePeeps.splice(iPeep--, 1);
+ peep.contactId = null;
+ if (peep.onchange) {
+ try {
+ peep.onchange(peep);
+ }
+ catch (ex) {
+ reportClientCodeError('peep.onchange error', ex, '\n',
+ ex.stack);
+ }
+ }
+ }
+ }
+ if (livePeeps.length === 0)
+ delete livePeepsById[contact.id];
+ }
+ }
+ // - process create/update causing new coverage
+ if (!contact.email)
+ return;
+ for (var iEmail = 0; iEmail < contact.email.length; iEmail++) {
+ var email = contact.email[iEmail].value;
+ livePeeps = livePeepsByEmail[email];
+ // nothing to do if there are no peeps that use that email address
+ if (!livePeeps)
+ continue;
+
+ for (iPeep = 0; iPeep < livePeeps.length; iPeep++) {
+ peep = livePeeps[iPeep];
+ // If the peep is not yet associated with this contact or any other
+ // contact, then associate it.
+ if (!peep.contactId) {
+ peep.contactId = contact.id;
+ var idLivePeeps = livePeepsById[peep.contactId];
+ if (idLivePeeps === undefined)
+ idLivePeeps = livePeepsById[peep.contactId] = [];
+ idLivePeeps.push(peep);
+ }
+ // However, if it's associated with a different contact, then just
+ // skip the peep.
+ else if (peep.contactId !== contact.id) {
+ continue;
+ }
+ // (The peep must be associated with this contact, so update and
+ // fire)
+
+ if (contact.name && contact.name.length)
+ peep.name = contact.name[0];
+ if (peep.onchange) {
+ try {
+ peep.onchange(peep);
+ }
+ catch (ex) {
+ reportClientCodeError('peep.onchange error', ex, '\n',
+ ex.stack);
+ }
+ }
+ }
+ }
+ };
+ // We don't need to do anything about onerror; the 'remove' event will
+ // probably have fired in this case, making us correct.
+ }
+ },
+
resolvePeeps: function(addressPairs) {
if (addressPairs === null)
return null;
@@ -409,76 +649,106 @@ var ContactCache = {
}
return resolved;
},
+ /**
+ * Create a MailPeep instance with the best information available and return
+ * it. Information from the (moz)Contacts API always trumps the passed-in
+ * information. If we have a cache hit (which covers both positive and
+ * negative evidence), we are done/all resolved immediately. Otherwise, we
+ * need to issue an async request. In that case, you want to check
+ * ContactCache.pendingLookupCount and push yourself onto
+ * ContactCache.callbacks if you want to be notified when the current set of
+ * lookups gets resolved.
+ *
+ * This is a slightly odd API, but it's based on the knowledge that for a
+ * single e-mail we will potentially need to perform multiple lookups and that
+ * e-mail addresses are also likely to come in batches so there's no need to
+ * generate N callbacks when 1 will do.
+ */
resolvePeep: function(addressPair) {
var emailAddress = addressPair.address;
- var entry = this._cache[emailAddress], contact;
+ var entry = this._contactCache[emailAddress], contact, peep;
+ var contactsAPI = navigator.mozContacts;
// known miss; create miss peep
- if (entry === null) {
- return new MailPeep(addressPair.name || '', emailAddress, false, null);
+ // no contacts API, always a miss, skip out before extra logic happens
+ if (entry === null || !contactsAPI) {
+ peep = new MailPeep(addressPair.name || '', emailAddress, null, null);
+ if (!contactsAPI)
+ return peep;
}
// known contact; unpack contact info
else if (entry !== undefined) {
- return new MailPeep(entry.name || addressPair.name || '', emailAddress,
- true,
+ peep = new MailPeep(entry.name || addressPair.name || '', emailAddress,
+ entry.id,
(entry.photo && entry.photo.length) ?
entry.photo[0] : null);
}
// not yet looked-up; assume it's a miss and we'll fix-up if it's a hit
else {
- var peep = new MailPeep(addressPair.name || '', emailAddress, false,
- null),
- pendingLookups = this.pendingLookups;
-
- var idxPendingLookup = pendingLookups.indexOf(emailAddress),
- peepsToFixup;
- if (idxPendingLookup !== -1) {
- peepsToFixup = pendingLookups[idxPendingLookup + 1];
- peepsToFixup.push(peep);
- return peep;
- }
+ peep = new MailPeep(addressPair.name || '',
+ emailAddress, null, null);
- var contactsAPI = navigator.mozContacts;
- if (!contactsAPI)
- return peep;
+ // Place a speculative miss in the contact cache so that additional
+ // requests take that path. They will get fixed up when our lookup
+ // returns (or if a change event happens to come in before our lookup
+ // returns.) Note that we do not do any hit/miss counting right now; we
+ // wait for the result to come back.
+ this._contactCache[emailAddress] = null;
+ this.pendingLookupCount++;
var req = contactsAPI.find({
filterBy: ['email'],
- filterOp: 'contains',
+ filterOp: 'equals',
filterValue: emailAddress
});
- pendingLookups.push(emailAddress);
- pendingLookups.push(peepsToFixup = [peep]);
- var handleResult = function handleResult() {
- var idxPendingLookup = pendingLookups.indexOf(emailAddress), i;
+ var self = this, handleResult = function() {
if (req.result && req.result.length) {
var contact = req.result[0];
- ContactCache._cache[emailAddress] = contact;
- if (++ContactCache._cacheHitEntries > ContactCache.MAX_CACHE_HITS) {
- ContactCache._cacheHitEntries = 0;
- ContactCache._cacheEmptyEntries = 0;
- ContactCache._cache = {};
- }
+ ContactCache._contactCache[emailAddress] = contact;
+ if (++ContactCache._cacheHitEntries > ContactCache.MAX_CACHE_HITS)
+ self._resetCache();
- for (i = 0; i < peepsToFixup.length; i++) {
+ var peepsToFixup = self._livePeepsByEmail[emailAddress];
+ // there might no longer be any MailPeeps alive to care; leave
+ if (!peepsToFixup)
+ return;
+ for (var i = 0; i < peepsToFixup.length; i++) {
var peep = peepsToFixup[i];
- peep.isContact = true;
+ if (!peep.contactId) {
+ peep.contactId = contact.id;
+ var livePeeps = self._livePeepsById[peep.contactId];
+ if (livePeeps === undefined)
+ livePeeps = self._livePeepsById[peep.contactId] = [];
+ livePeeps.push(peep);
+ }
+
if (contact.name && contact.name.length)
peep.name = contact.name[0];
if (contact.photo && contact.photo.length)
peep._thumbnailBlob = contact.photo[0];
+
+ // If no one is waiting for our/any request to complete, generate an
+ // onchange notification.
+ if (!self.callbacks.length) {
+ if (peep.onchange) {
+ try {
+ peep.onchange(peep);
+ }
+ catch (ex) {
+ reportClientCodeError('peep.onchange error', ex, '\n',
+ ex.stack);
+ }
+ }
+ }
}
}
else {
- ContactCache._cache[emailAddress] = null;
- if (++ContactCache._cacheEmptyEntries > ContactCache.MAX_CACHE_EMPTY) {
- ContactCache._cacheHitEntries = 0;
- ContactCache._cacheEmptyEntries = 0;
- ContactCache._cache = {};
- }
+ ContactCache._contactCache[emailAddress] = null;
+ if (++ContactCache._cacheEmptyEntries > ContactCache.MAX_CACHE_EMPTY)
+ self._resetCache();
}
- pendingLookups.splice(idxPendingLookup, 2);
- if (!pendingLookups.length) {
+ // Only notify callbacks if all outstanding lookups have completed
+ if (--self.pendingLookupCount === 0) {
for (i = 0; i < ContactCache.callbacks.length; i++) {
ContactCache.callbacks[i]();
}
@@ -487,12 +757,57 @@ var ContactCache = {
};
req.onsuccess = handleResult;
req.onerror = handleResult;
+ }
- return peep;
+ // - track the peep in our lists of live peeps
+ var livePeeps;
+ livePeeps = this._livePeepsByEmail[emailAddress];
+ if (livePeeps === undefined)
+ livePeeps = this._livePeepsByEmail[emailAddress] = [];
+ livePeeps.push(peep);
+
+ if (peep.contactId) {
+ livePeeps = this._livePeepsById[peep.contactId];
+ if (livePeeps === undefined)
+ livePeeps = this._livePeepsById[peep.contactId] = [];
+ livePeeps.push(peep);
+ }
+
+ return peep;
+ },
+
+ forgetPeepInstances: function() {
+ var livePeepsById = this._livePeepsById,
+ livePeepsByEmail = this._livePeepsByEmail;
+ for (var iArg = 0; iArg < arguments.length; iArg++) {
+ var peeps = arguments[iArg];
+ if (!peeps)
+ continue;
+ for (var iPeep = 0; iPeep < peeps.length; iPeep++) {
+ var peep = peeps[iPeep], livePeeps, idx;
+ if (peep.contactId) {
+ livePeeps = livePeepsById[peep.contactId];
+ if (livePeeps) {
+ idx = livePeeps.indexOf(peep);
+ if (idx !== -1) {
+ livePeeps.splice(idx, 1);
+ if (livePeeps.length === 0)
+ delete livePeepsById[peep.contactId];
+ }
+ }
+ }
+ livePeeps = livePeepsByEmail[peep.address];
+ if (livePeeps) {
+ idx = livePeeps.indexOf(peep);
+ if (idx !== -1) {
+ livePeeps.splice(idx, 1);
+ if (livePeeps.length === 0)
+ delete livePeepsByEmail[peep.address];
+ }
+ }
+ }
}
},
- pendingLookups: [],
- callbacks: [],
};
function revokeImageSrc() {
@@ -515,13 +830,24 @@ function showBlobInImg(imgNode, blob) {
imgNode.addEventListener('load', revokeImageSrc);
}
-function MailPeep(name, address, isContact, thumbnailBlob) {
- this.isContact = isContact;
+function MailPeep(name, address, contactId, thumbnailBlob) {
this.name = name;
this.address = address;
+ this.contactId = contactId;
this._thumbnailBlob = thumbnailBlob;
+
+ this.element = null;
+ this.data = null;
+ // peeps are usually one of: from, to, cc, bcc
+ this.type = null;
+
+ this.onchange = null;
}
MailPeep.prototype = {
+ get isContact() {
+ return this.contactId !== null;
+ },
+
toString: function() {
return '[MailPeep: ' + this.address + ']';
},
@@ -529,6 +855,13 @@ MailPeep.prototype = {
return {
name: this.name,
address: this.address,
+ contactId: this.contactId
+ };
+ },
+ toWireRep: function() {
+ return {
+ name: this.name,
+ address: this.address
};
},
@@ -596,6 +929,27 @@ MailHeader.prototype = {
};
},
+ /**
+ * The use-case is the message list providing the message reader with a
+ * header. The header really wants to get update notifications from the
+ * backend and therefore not be inert, but that's a little complicated and out
+ * of scope for the current bug.
+ *
+ * We clone at all because our MailPeep.onchange and MailPeep.element values
+ * were getting clobbered. All the instances are currently intended to map
+ * 1:1 to a single UI widget, so cloning seems like the right thing to do.
+ *
+ * A deeper issue is whether the message reader will want to have its own
+ * slice since the reader will soon allow forward/backward navigation. I
+ * assume we'll want the message list to track that movement, which suggests
+ * that it really doesn't want to do that. This suggests we'll either want
+ * non-inert clones or to just use a list-of-handlers model with us using
+ * closures and being careful about removing event handlers.
+ */
+ makeCopy: function() {
+ return new MailHeader(this._slice, this._wireRep);
+ },
+
__update: function(wireRep) {
if (wireRep.snippet !== null)
this.snippet = wireRep.snippet;
@@ -609,6 +963,14 @@ MailHeader.prototype = {
},
/**
+ * Release subscriptions associated with the header; currently this just means
+ * tell the ContactCache we no longer care about the `MailPeep` instances.
+ */
+ __die: function() {
+ ContactCache.forgetPeepInstances([this.author], this.to, this.cc, this.bcc);
+ },
+
+ /**
* Delete this message
*/
deleteMessage: function() {
@@ -746,6 +1108,10 @@ MailMatchedHeader.prototype = {
id: this.header.id
};
},
+
+ __die: function() {
+ this.header.__die();
+ },
};
/**
@@ -1217,9 +1583,34 @@ BridgedViewSlice.prototype = {
type: 'killSlice',
handle: this._handle
});
+
+ for (var i = 0; i < this.items.length; i++) {
+ var item = this.items[i];
+ item.__die();
+ }
},
};
+function AccountsViewSlice(api, handle) {
+ BridgedViewSlice.call(this, api, 'accounts', handle);
+}
+AccountsViewSlice.prototype = Object.create(BridgedViewSlice.prototype);
+
+Object.defineProperty(AccountsViewSlice.prototype, 'defaultAccount', {
+ get: function () {
+ var defaultAccount = this.items[0];
+ for (var i = 1; i < this.items.length; i++) {
+ // For UI upgrades, the defaultPriority may not be set, so default to
+ // zero for comparisons
+ if ((this.items[i]._wireRep.defaultPriority || 0) >
+ (defaultAccount._wireRep.defaultPriority || 0))
+ defaultAccount = this.items[i];
+ }
+
+ return defaultAccount;
+ }
+});
+
function FoldersViewSlice(api, handle) {
BridgedViewSlice.call(this, api, 'folders', handle);
}
@@ -1256,6 +1647,7 @@ function HeadersViewSlice(api, handle, ns) {
this._bodiesRequest = {};
}
HeadersViewSlice.prototype = Object.create(BridgedViewSlice.prototype);
+
/**
* Request a re-sync of the time interval covering the effective time
* range. If the most recently displayed message is the most recent message
@@ -1731,6 +2123,8 @@ function MailAPI() {
}
this._setHasAccounts();
+
+ ContactCache.init();
}
exports.MailAPI = MailAPI;
MailAPI.prototype = {
@@ -1850,7 +2244,7 @@ MailAPI.prototype = {
_recv_badLogin: function ma__recv_badLogin(msg) {
if (this.onbadlogin)
- this.onbadlogin(new MailAccount(this, msg.account), msg.problem);
+ this.onbadlogin(new MailAccount(this, msg.account, null), msg.problem);
return true;
},
@@ -1874,7 +2268,7 @@ MailAPI.prototype = {
// call to mozContacts. In this case, we don't want to surface the data to
// the UI until the contacts are fully resolved in order to avoid the UI
// flickering or just triggering reflows that could otherwise be avoided.
- if (ContactCache.pendingLookups.length) {
+ if (ContactCache.pendingLookupCount) {
ContactCache.callbacks.push(function contactsResolved() {
this._fire_sliceSplice(msg, slice, transformedItems, fake);
this._doneProcessingMessage(msg);
@@ -1892,7 +2286,7 @@ MailAPI.prototype = {
switch (slice._ns) {
case 'accounts':
for (i = 0; i < addItems.length; i++) {
- transformedItems.push(new MailAccount(this, addItems[i]));
+ transformedItems.push(new MailAccount(this, addItems[i], slice));
}
break;
@@ -2015,6 +2409,8 @@ MailAPI.prototype = {
slice.onremove(item, i);
if (item.onremove)
item.onremove(item, i);
+ // the item needs a chance to clean up after itself.
+ item.__die();
}
}
catch (ex) {
@@ -2050,7 +2446,7 @@ MailAPI.prototype = {
// reset before calling in case it wants to chain.
slice.oncomplete = null;
try {
- completeFunc();
+ completeFunc(msg.newEmailCount);
}
catch (ex) {
reportClientCodeError('oncomplete notification error', ex,
@@ -2067,19 +2463,20 @@ MailAPI.prototype = {
switch (slice._ns) {
case 'accounts':
- // Cache the first / default account.
- var firstItem = slice.items[0] && slice.items[0]._wireRep;
+ // Cache default account.
+ var defaultItem = slice.defaultAccount;
// Clear cache if no accounts or the first account has changed.
- if (!slice.items.length || this._recvCache.accountId !== firstItem.id)
+ if (!slice.items.length ||
+ this._recvCache.accountId !== defaultItem.id)
this._resetCache();
tempMsg = objCopy(msg);
tempMsg.howMany = 0;
tempMsg.index = 0;
- if (firstItem) {
- tempMsg.addItems = [firstItem];
- this._recvCache.accountId = firstItem.id;
+ if (defaultItem) {
+ tempMsg.addItems = [defaultItem._wireRep];
+ this._recvCache.accountId = defaultItem.id;
}
this._recvCache.accounts = tempMsg;
this._setHasAccounts();
@@ -2448,6 +2845,12 @@ MailAPI.prototype = {
},
_modifyAccount: function ma__modifyAccount(account, mods) {
+ if (mods.hasOwnProperty('setAsDefault')) {
+ // The order of accounts changed. The cache is now invalid.
+ this._resetCache();
+ this._saveCache();
+ }
+
this.__bridgeSend({
type: 'modifyAccount',
accountId: account.id,
@@ -2477,7 +2880,7 @@ MailAPI.prototype = {
*/
viewAccounts: function ma_viewAccounts(realAccountsOnly) {
var handle = this._nextHandle++,
- slice = new BridgedViewSlice(this, 'accounts', handle);
+ slice = new AccountsViewSlice(this, handle);
this._slices[handle] = slice;
this.__bridgeSend({
@@ -2749,7 +3152,7 @@ MailAPI.prototype = {
resolveEmailAddressToPeep: function(emailAddress, callback) {
var peep = ContactCache.resolvePeep({ name: null, address: emailAddress });
- if (ContactCache.pendingLookups.length)
+ if (ContactCache.pendingLookupCount)
ContactCache.callbacks.push(callback.bind(null, peep));
else
callback(peep);
@@ -2822,7 +3225,7 @@ MailAPI.prototype = {
msg.refSuid = options.replyTo.id;
msg.refDate = options.replyTo.date.valueOf();
msg.refGuid = options.replyTo.guid;
- msg.refAuthor = options.replyTo.author.toJSON();
+ msg.refAuthor = options.replyTo.author.toWireRep();
msg.refSubject = options.replyTo.subject;
}
else if (options.hasOwnProperty('forwardOf') && options.forwardOf) {
@@ -2831,7 +3234,7 @@ MailAPI.prototype = {
msg.refSuid = options.forwardOf.id;
msg.refDate = options.forwardOf.date.valueOf();
msg.refGuid = options.forwardOf.guid;
- msg.refAuthor = options.forwardOf.author.toJSON();
+ msg.refAuthor = options.forwardOf.author.toWireRep();
msg.refSubject = options.forwardOf.subject;
}
else {
@@ -3397,6 +3800,7 @@ define('mailapi/worker-support/devicestorage-main',[],function() {
dump('DeviceStorage: ' + str + '\n');
}
+
function save(uid, cmd, storage, blob, filename) {
var deviceStorage = navigator.getDeviceStorage(storage);
var req = deviceStorage.addNamed(blob, filename);
@@ -3405,8 +3809,15 @@ define('mailapi/worker-support/devicestorage-main',[],function() {
self.sendMessage(uid, cmd, [false, req.error.name]);
};
- req.onsuccess = function() {
- self.sendMessage(uid, cmd, [true]);
+ req.onsuccess = function(e) {
+ var prefix = '';
+
+ if (typeof window.IS_GELAM_TEST !== 'undefined') {
+ prefix = 'TEST_PREFIX/';
+ }
+
+ // Bool success, String err, String filename
+ self.sendMessage(uid, cmd, [true, null, prefix + e.target.result]);
};
}
@@ -3651,6 +4062,8 @@ function MailDB(testOptions, successCb, errorCb, upgradeCb) {
};
var dbVersion = CUR_VERSION;
+ if (testOptions && testOptions.dbDelta)
+ dbVersion += testOptions.dbDelta;
if (testOptions && testOptions.dbVersion)
dbVersion = testOptions.dbVersion;
var openRequest = IndexedDB.open('b2g-email', dbVersion), self = this;
diff --git a/apps/email/js/ext/mailapi/worker-bootstrap.js b/apps/email/js/ext/mailapi/worker-bootstrap.js
index dd57083..bf03630 100644
--- a/apps/email/js/ext/mailapi/worker-bootstrap.js
+++ b/apps/email/js/ext/mailapi/worker-bootstrap.js
@@ -1819,6 +1819,15 @@ define('event-queue',['require'],function (require) {
});
define('microtime',['require'],function (require) {
+ // workers won't have this, of course...
+ if (window && window.performance && window.performance.now) {
+ return {
+ now: function () {
+ return window.performance.now() * 1000;
+ }
+ };
+ }
+
return {
now: function () {
return Date.now() * 1000;
@@ -3151,7 +3160,7 @@ LoggestClassMaker.prototype = {
throw new Error("Attempt to add expectations when already resolved!");
var exp = [name];
- for (var iArg = 0; iArg < numArgs; iArg++) {
+ for (var iArg = 0; iArg < arguments.length; iArg++) {
if (useArgs[iArg] && useArgs[iArg] !== EXCEPTION) {
exp.push(arguments[iArg]);
}
@@ -3284,7 +3293,7 @@ LoggestClassMaker.prototype = {
throw new Error("Attempt to add expectations when already resolved!");
var exp = [name_begin];
- for (var iArg = 0; iArg < numArgs; iArg++) {
+ for (var iArg = 0; iArg < arguments.length; iArg++) {
if (useArgs[iArg] && useArgs[iArg] !== EXCEPTION)
exp.push(arguments[iArg]);
}
@@ -3301,7 +3310,7 @@ LoggestClassMaker.prototype = {
throw new Error("Attempt to add expectations when already resolved!");
var exp = [name_end];
- for (var iArg = 0; iArg < numArgs; iArg++) {
+ for (var iArg = 0; iArg < arguments.length; iArg++) {
if (useArgs[iArg] && useArgs[iArg] !== EXCEPTION)
exp.push(arguments[iArg]);
}
@@ -3319,6 +3328,10 @@ LoggestClassMaker.prototype = {
return true;
};
},
+ /**
+ * Call like: loggedCall(logArg1, ..., logArgN, useAsThis, func,
+ * callArg1, ... callArgN);
+ */
addCall: function(name, logArgs, testOnlyLogArgs) {
this._define(name, 'call');
@@ -3514,7 +3527,7 @@ LoggestClassMaker.prototype = {
throw new Error("Attempt to add expectations when already resolved!");
var exp = [name];
- for (var iArg = 0; iArg < numArgs; iArg++) {
+ for (var iArg = 0; iArg < arguments.length; iArg++) {
if (useArgs[iArg] && useArgs[iArg] !== EXCEPTION)
exp.push(arguments[iArg]);
}
@@ -4605,12 +4618,17 @@ exports.INITIAL_SYNC_GROWTH_DAYS = 3;
/**
* What should be multiple the current number of sync days by when we perform
* a sync and don't find any messages? There are upper bounds in
- * `FolderStorage.onSyncCompleted` that cap this and there's more comments
- * there.
+ * `ImapFolderSyncer.onSyncCompleted` that cap this and there's more comments
+ * there. Note that we keep moving our window back as we go.
+ *
+ * This was 1.6 for a while, but it was proving to be a bit slow when the first
+ * messages start a ways back. Also, once we moved to just syncing headers
+ * without bodies, the cost of fetching more than strictly required went way
+ * down.
*
* IMAP only.
*/
-exports.TIME_SCALE_FACTOR_ON_NO_MESSAGES = 1.6;
+exports.TIME_SCALE_FACTOR_ON_NO_MESSAGES = 2;
/**
* What is the furthest back in time we are willing to go? This is an
@@ -4625,6 +4643,23 @@ exports.TIME_SCALE_FACTOR_ON_NO_MESSAGES = 1.6;
exports.OLDEST_SYNC_DATE = Date.UTC(1990, 0, 1);
/**
+ * Don't bother with iterative deepening if a folder has less than this many
+ * messages; just sync the whole thing. The trade-offs here are:
+ *
+ * - Not wanting to fetch more messages than we need.
+ * - Because header envelope fetches are done in a batch and IMAP servers like
+ * to sort UIDs from low-to-high, we will get the oldest messages first.
+ * This can be mitigated by having our sync logic use request windowing to
+ * offset this.
+ * - The time required to fetch the headers versus the time required to
+ * perform deepening. Because of network and disk I/O, deepening can take
+ * a very long time
+ *
+ * IMAP only.
+ */
+exports.SYNC_WHOLE_FOLDER_AT_N_MESSAGES = 40;
+
+/**
* If we issued a search for a date range and we are getting told about more
* than the following number of messages, we will try and reduce the date
* range proportionately (assuming a linear distribution) so that we sync
@@ -4634,7 +4669,7 @@ exports.OLDEST_SYNC_DATE = Date.UTC(1990, 0, 1);
*
* IMAP only.
*/
-exports.BISECT_DATE_AT_N_MESSAGES = 50;
+exports.BISECT_DATE_AT_N_MESSAGES = 60;
/**
* What's the maximum number of messages we should ever handle in a go and
@@ -4762,6 +4797,10 @@ exports.SYNC_RANGE_ENUMS_TO_MS = {
* Testing support to adjust the value we use for the number of initial sync
* days. The tests are written with a value in mind (7), but 7 turns out to
* be too high an initial value for actual use, but is fine for tests.
+ *
+ * This started by taking human-friendly strings, but I changed to just using
+ * the constant names when I realized that consistency for grepping purposes
+ * would be a good thing.
*/
exports.TEST_adjustSyncValues = function TEST_adjustSyncValues(syncValues) {
if (syncValues.hasOwnProperty('fillSize'))
@@ -4771,6 +4810,9 @@ exports.TEST_adjustSyncValues = function TEST_adjustSyncValues(syncValues) {
if (syncValues.hasOwnProperty('growDays'))
exports.INITIAL_SYNC_GROWTH_DAYS = syncValues.growDays;
+ if (syncValues.hasOwnProperty('SYNC_WHOLE_FOLDER_AT_N_MESSAGES'))
+ exports.SYNC_WHOLE_FOLDER_AT_N_MESSAGES =
+ syncValues.SYNC_WHOLE_FOLDER_AT_N_MESSAGES;
if (syncValues.hasOwnProperty('bisectThresh'))
exports.BISECT_DATE_AT_N_MESSAGES = syncValues.bisectThresh;
if (syncValues.hasOwnProperty('tooMany'))
@@ -5158,7 +5200,7 @@ MailSlice.prototype = {
},
setStatus: function(status, requested, moreExpected, flushAccumulated,
- progress) {
+ progress, newEmailCount) {
if (!this._bridgeHandle)
return;
@@ -5181,14 +5223,16 @@ MailSlice.prototype = {
// XXX remove concat() once our bridge sending makes rep sharing
// impossible by dint of actual postMessage or JSON roundtripping.
this._bridgeHandle.sendSplice(0, 0, this.headers.concat(),
- requested, moreExpected);
+ requested, moreExpected,
+ newEmailCount);
// If we're no longer synchronizing, we want to update desiredHeaders
// to avoid accumulating extra 'desire'.
if (status !== 'synchronizing')
this.desiredHeaders = this.headers.length;
}
else {
- this._bridgeHandle.sendStatus(status, requested, moreExpected, progress);
+ this._bridgeHandle.sendStatus(status, requested, moreExpected, progress,
+ newEmailCount);
}
},
@@ -5570,7 +5614,24 @@ MailSlice.prototype = {
* @key[flags @listof[String]]
* @key[hasAttachments Boolean]
* @key[subject String]
- * @key[snippet String]
+ * @key[snippet @oneof[
+ * @case[null]{
+ * We haven't tried to generate a snippet yet.
+ * }
+ * @case['']{
+ * We tried to generate a snippet, but got nothing useful. Note that we
+ * may try and generate a snippet from a partial body fetch; this does not
+ * indicate that we should avoid computing a better snippet. Whenever the
+ * snippet is falsey and we have retrieved more body data, we should
+ * always try and derive a snippet.
+ * }
+ * @case[String]{
+ * A non-empty string means we managed to produce some snippet data. It
+ * is still appropriate to regenerate the snippet if more body data is
+ * fetched since our snippet may be a fallback where we chose quoted text
+ * instead of authored text, etc.
+ * }
+ * ]]
* ]]
* @typedef[HeaderBlock @dict[
* @key[ids @listof[ID]]{
@@ -5848,6 +5909,13 @@ FolderStorage.prototype = {
},
/**
+ * Function that we call with header whenever addMessageHeader gets called.
+ * @type {Function}
+ * @private
+ */
+ _onAddingHeader: null,
+
+ /**
* Reset all active slices.
*/
resetAndRefreshActiveSlices: function() {
@@ -6374,7 +6442,7 @@ FolderStorage.prototype = {
// These variables let us detect if the deletion happened fully
// synchronously and thereby avoid blowing up the stack.
callActive = false, deleteTriggered = false;
- var deleteNextHeader = function deleteNextHeader() {
+ var deleteNextHeader = function() {
// if things are happening synchronously, bail out
if (callActive) {
deleteTriggered = true;
@@ -7032,16 +7100,19 @@ FolderStorage.prototype = {
// other synchronizations already in progress.
this._slices.push(slice);
- var doneCallback = function doneSyncCallback(err, reportSyncStatusAs) {
+ var doneCallback = function doneSyncCallback(err, reportSyncStatusAs,
+ moreExpected) {
if (!reportSyncStatusAs) {
if (err)
reportSyncStatusAs = 'syncfailed';
else
reportSyncStatusAs = 'synced';
}
+ if (moreExpected === undefined)
+ moreExpected = false;
slice.waitingOnData = false;
- slice.setStatus(reportSyncStatusAs, true, false, true);
+ slice.setStatus(reportSyncStatusAs, true, moreExpected, true);
this._curSyncSlice = null;
releaseMutex();
@@ -7087,7 +7158,7 @@ FolderStorage.prototype = {
// blocked. We'll update it soon enough.
if (!this.folderSyncer.syncable) {
console.log('Synchronization is currently blocked; waiting...');
- doneCallback(null, 'syncblocked');
+ doneCallback(null, 'syncblocked', true);
return;
}
@@ -7104,6 +7175,7 @@ FolderStorage.prototype = {
}
this._curSyncSlice = slice;
}.bind(this);
+
this.folderSyncer.initialSync(
slice, $sync.INITIAL_SYNC_DAYS,
syncCallback, doneCallback, progressCallback);
@@ -7346,7 +7418,8 @@ FolderStorage.prototype = {
if (!this._account.universe.online ||
!this.folderSyncer.canGrowSync ||
!userRequestsGrowth) {
- slice.sendEmptyCompletion();
+ if (this.folderSyncer.syncable)
+ slice.sendEmptyCompletion();
releaseMutex();
return;
}
@@ -7430,13 +7503,33 @@ FolderStorage.prototype = {
// In the event we grow the startTS to the dawn of time, then we want
// to also provide the original startTS so that the bisection does not
// need to scan through years of empty space.
- origStartTS = null;
+ origStartTS = null,
+ // If we are refreshing through 'now', we will count the new messages we
+ // hear about and update this.newEmailCount once the sync completes. If
+ // we are performing any othe sync, the value will not be updated.
+ newEmailCount = null;
// - Grow endTS
// If the endTS lines up with the most recent known message for the folder,
// then remove the timestamp constraint so it goes all the way to now.
// OR if we just have no known messages
if (this.headerIsYoungestKnown(endTS, slice.endUID)) {
+ var prevTS = endTS;
+ newEmailCount = 0;
+
+ /**
+ * Increment our new email count if the following conditions are met:
+ * 1. This header is younger than the youngest one before sync
+ * 2. and this hasn't already been seen.
+ * @param {HeaderInfo} header The header being added.
+ */
+ this._onAddingHeader = function(header) {
+ if (SINCE(header.date, prevTS) &&
+ (!header.flags || header.flags.indexOf('\\Seen') === -1)) {
+ newEmailCount += 1;
+ }
+ }.bind(this);
+
endTS = null;
}
else {
@@ -7467,6 +7560,8 @@ FolderStorage.prototype = {
var doneCallback = function refreshDoneCallback(err, bisectInfo,
numMessages) {
+ this._onAddingHeader = null;
+
var reportSyncStatusAs = 'synced';
switch (err) {
case 'aborted':
@@ -7477,7 +7572,8 @@ FolderStorage.prototype = {
releaseMutex();
slice.waitingOnData = false;
- slice.setStatus(reportSyncStatusAs, true, false);
+ slice.setStatus(reportSyncStatusAs, true, false, false, null,
+ newEmailCount);
return undefined;
}.bind(this);
@@ -8133,7 +8229,9 @@ FolderStorage.prototype = {
aranges.splice.apply(aranges, [newInfo[0], delCount].concat(insertions));
- this.folderMeta.lastSyncedAt = endTS;
+ /*lastSyncedAt depends on current timestamp of the client device
+ should not be added timezone offset*/
+ this.folderMeta.lastSyncedAt = NOW();
if (this._account.universe)
this._account.universe.__notifyModifiedFolder(this._account,
this.folderMeta);
@@ -8433,6 +8531,9 @@ FolderStorage.prototype = {
slice.desiredHeaders++;
}
+ if (this._onAddingHeader !== null) {
+ this._onAddingHeader(header);
+ }
slice.onHeaderAdded(header, false, true);
}
}
@@ -9374,9 +9475,9 @@ BodyFilter.prototype = {
matchQuotes = this.matchQuotes,
idx;
- for (var iBodyRep = 0; iBodyRep < body.bodyReps.length; iBodyRep += 2) {
- var bodyType = body.bodyReps[iBodyRep],
- bodyRep = body.bodyReps[iBodyRep + 1];
+ for (var iBodyRep = 0; iBodyRep < body.bodyReps.length; iBodyRep++) {
+ var bodyType = body.bodyReps[iBodyRep].type,
+ bodyRep = body.bodyReps[iBodyRep].content;
if (bodyType === 'plain') {
for (var iRep = 0; iRep < bodyRep.length && matches.length < stopAfter;
@@ -9946,7 +10047,7 @@ exports.local_do_modtags = function(op, doneCallback, undo) {
function() {
doneCallback(null, null, true);
},
- null,
+ null, // connection loss does not happen for local-only ops
undo,
'modtags');
};
@@ -10058,7 +10159,7 @@ exports.local_do_move = function(op, doneCallback, targetFolderId) {
function() {
doneCallback(null, null, true);
},
- null,
+ null, // connection loss does not happen for local-only ops
false,
'local move source');
};
@@ -10143,11 +10244,11 @@ exports.do_download = function(op, callback) {
function saveToStorage(blob, storage, filename, partInfo, isRetry) {
pendingStorageWrites++;
- var callback = function(success, error) {
+ var callback = function(success, error, savedFilename) {
if (success) {
self._LOG.savedAttachment(storage, blob.type, blob.size);
- console.log('saved attachment to', storage, filename, 'type:', blob.type);
- partInfo.file = [storage, filename];
+ console.log('saved attachment to', storage, savedFilename, 'type:', blob.type);
+ partInfo.file = [storage, savedFilename];
if (--pendingStorageWrites === 0)
done();
} else {
@@ -10215,8 +10316,8 @@ exports.local_do_download = function(op, callback) {
};
exports.check_download = function(op, callback) {
- // If we had download the file and persisted it successfully, this job would
- // be marked done because of the atomicity guarantee on our commits.
+ // If we downloaded the file and persisted it successfully, this job would be
+ // marked done because of the atomicity guarantee on our commits.
callback(null, 'coherent-notyet');
};
exports.local_undo_download = function(op, callback) {
@@ -10232,12 +10333,13 @@ exports.local_do_downloadBodies = function(op, callback) {
};
exports.do_downloadBodies = function(op, callback) {
- var aggrErr;
+ var aggrErr, totalDownloaded = 0;
this._partitionAndAccessFoldersSequentially(
op.messages,
true,
function perFolder(folderConn, storage, headers, namers, callWhenDone) {
- folderConn.downloadBodies(headers, op.options, function(err) {
+ folderConn.downloadBodies(headers, op.options, function(err, numDownloaded) {
+ totalDownloaded += numDownloaded;
if (err && !aggrErr) {
aggrErr = err;
}
@@ -10245,7 +10347,9 @@ exports.do_downloadBodies = function(op, callback) {
});
},
function allDone() {
- callback(aggrErr, null, true);
+ callback(aggrErr, null,
+ // save if we might have done work.
+ totalDownloaded > 0);
},
function deadConn() {
aggrErr = 'aborted-retry';
@@ -10256,6 +10360,21 @@ exports.do_downloadBodies = function(op, callback) {
);
};
+exports.check_downloadBodies = function(op, callback) {
+ // If we had downloaded the bodies and persisted them successfully, this job
+ // would be marked done because of the atomicity guarantee on our commits. It
+ // is possible this request might only be partially serviced, in which case we
+ // will avoid redundant body fetches, but redundant folder selection is
+ // possible if this request spans multiple folders.
+ callback(null, 'coherent-notyet');
+};
+
+exports.check_downloadBodyReps = function(op, callback) {
+ // If we downloaded all of the body parts and persisted them successfully,
+ // this job would be marked done because of the atomicity guarantee on our
+ // commits. But it's not, so there's more to do.
+ callback(null, 'coherent-notyet');
+};
exports.do_downloadBodyReps = function(op, callback) {
var self = this;
@@ -10276,8 +10395,10 @@ exports.do_downloadBodyReps = function(op, callback) {
var gotHeader = function gotHeader(header) {
// header may have been deleted by the time we get here...
- if (!header)
- return callback();
+ if (!header) {
+ callback();
+ return;
+ }
folderConn.downloadBodyReps(header, onDownloadReps);
};
@@ -10286,7 +10407,8 @@ exports.do_downloadBodyReps = function(op, callback) {
if (err) {
console.error('Error downloading reps', err);
// fail we cannot download for some reason?
- return callback('unknown');
+ callback('unknown');
+ return;
}
// success
@@ -10441,6 +10563,11 @@ exports.allJobsDone = function() {
* Its possible that entire folders will be skipped if no headers requested are
* now present.
*
+ * Connection loss by default causes this method to stop trying to traverse
+ * folders, calling callOnConnLoss and callWhenDone in that order. If you want
+ * to do something more clever, extend this method so that you can return a
+ * sentinel value or promise or something and do your clever thing.
+ *
* @args[
* @param[messageNamers @listof[MessageNamer]]
* @param[needConn Boolean]{
@@ -10461,8 +10588,19 @@ exports.allJobsDone = function() {
* @param[callWhenDoneWithFolder Function]
* ]
* ]]
- * @param[callWhenDone Function]
- * @param[callOnConnLoss Function]
+ * @param[callWhenDone @func[
+ * @args[err @oneof[null 'connection-list']]
+ * ]]{
+ * The function to invoke when all of the folders have been processed or the
+ * connection has been lost and we're giving up. This will be invoked after
+ * `callOnConnLoss` in the event of a conncetion loss.
+ * }
+ * @param[callOnConnLoss Function]{
+ * This function we invoke when we lose a connection. Traditionally, you would
+ * use this to flag an error in your function that you would then return when
+ * we invoke `callWhenDone`. Then your check function will be invoked and you
+ * can laboriously check what actually happened on the server, etc.
+ * }
* @param[reverse #:optional Boolean]{
* Should we walk the partitions in reverse order?
* }
@@ -10486,13 +10624,20 @@ exports._partitionAndAccessFoldersSequentially = function(
var partitions = $util.partitionMessagesByFolderId(allMessageNamers);
var folderConn, storage, self = this,
folderId = null, folderMessageNamers = null, serverIds = null,
- iNextPartition = 0, curPartition = null, modsToGo = 0;
+ iNextPartition = 0, curPartition = null, modsToGo = 0,
+ // Set to true immediately before calling callWhenDone; causes us to
+ // immediately bail out of any of our callbacks in order to avoid
+ // continuing beyond the point when we should have stopped.
+ terminated = false;
if (reverse)
partitions.reverse();
var openNextFolder = function openNextFolder() {
+ if (terminated)
+ return;
if (iNextPartition >= partitions.length) {
+ terminated = true;
callWhenDone(null);
return;
}
@@ -10514,10 +10659,26 @@ exports._partitionAndAccessFoldersSequentially = function(
if (curPartition.folderId !== folderId) {
folderId = curPartition.folderId;
self._accessFolderForMutation(folderId, needConn, gotFolderConn,
- callOnConnLoss, label);
+ connDied, label);
}
};
+ var connDied = function connDied() {
+ if (terminated)
+ return;
+ if (callOnConnLoss) {
+ try {
+ callOnConnLoss();
+ }
+ catch (ex) {
+ self._LOG.callbackErr(ex);
+ }
+ }
+ terminated = true;
+ callWhenDone('connection-lost');
+ };
var gotFolderConn = function gotFolderConn(_folderConn, _storage) {
+ if (terminated)
+ return;
folderConn = _folderConn;
storage = _storage;
// - Get headers or resolve current server id from name map
@@ -10555,6 +10716,8 @@ exports._partitionAndAccessFoldersSequentially = function(
}
};
var gotNeededHeaders = function gotNeededHeaders(headers) {
+ if (terminated)
+ return;
var iNextServerId = serverIds.indexOf(null);
for (var i = 0; i < headers.length; i++) {
var header = headers[i];
@@ -10578,7 +10741,8 @@ exports._partitionAndAccessFoldersSequentially = function(
// skip entering this folder as the job cannot do anything with an empty
// header.
if (!serverIds.length) {
- return openNextFolder();
+ openNextFolder();
+ return;
}
try {
@@ -10590,10 +10754,13 @@ exports._partitionAndAccessFoldersSequentially = function(
}
};
var gotHeaders = function gotHeaders(headers) {
+ if (terminated)
+ return;
// its unlikely but entirely possible that all pending headers have been
// removed somehow between when the job was queued and now.
if (!headers.length) {
- return openNextFolder();
+ openNextFolder();
+ return;
}
// Sort the headers in ascending-by-date order so that slices hear about
@@ -11524,6 +11691,7 @@ define('mailapi/mailbridge',
'rdcommon/log',
'./util',
'./mailchew-strings',
+ './date',
'require',
'module',
'exports'
@@ -11532,6 +11700,7 @@ define('mailapi/mailbridge',
$log,
$imaputil,
$mailchewStrings,
+ $date,
require,
$module,
exports
@@ -11776,6 +11945,14 @@ MailBridge.prototype = {
case 'syncRange':
accountDef.syncRange = val;
break;
+
+ case 'setAsDefault':
+ // Weird things can happen if the device's clock goes back in time,
+ // but this way, at least the user can change their default if they
+ // cycle through their accounts.
+ if (val)
+ accountDef.defaultPriority = $date.NOW();
+ break;
}
}
this.universe.saveAccountDef(accountDef, null);
@@ -12654,9 +12831,11 @@ function SliceBridgeProxy(bridge, ns, handle) {
SliceBridgeProxy.prototype = {
/**
* Issue a splice to add and remove items.
+ * @param {number} newEmailCount Number of new emails synced during this
+ * slice request.
*/
sendSplice: function sbp_sendSplice(index, howMany, addItems, requested,
- moreExpected) {
+ moreExpected, newEmailCount) {
this._bridge.__sendMessage({
type: 'sliceSplice',
handle: this._handle,
@@ -12671,6 +12850,7 @@ SliceBridgeProxy.prototype = {
atBottom: this.atBottom,
userCanGrowUpwards: this.userCanGrowUpwards,
userCanGrowDownwards: this.userCanGrowDownwards,
+ newEmailCount: newEmailCount,
});
},
@@ -12685,12 +12865,16 @@ SliceBridgeProxy.prototype = {
});
},
+ /**
+ * @param {number} newEmailCount Number of new emails synced during this
+ * slice request.
+ */
sendStatus: function sbp_sendStatus(status, requested, moreExpected,
- progress) {
+ progress, newEmailCount) {
this.status = status;
if (progress != null)
this.progress = progress;
- this.sendSplice(0, 0, [], requested, moreExpected);
+ this.sendSplice(0, 0, [], requested, moreExpected, newEmailCount);
},
sendSyncProgress: function(progress) {
@@ -14457,9 +14641,9 @@ MailUniverse.prototype = {
endings: 'transparent'
});
var filename = 'gem-log-' + Date.now() + '.json';
- sendMessage('save', ['sdcard', blob, filename], function(success) {
+ sendMessage('save', ['sdcard', blob, filename], function(success, err, savedFile) {
if (success)
- console.log('saved log to "sdcard" devicestorage:', filename);
+ console.log('saved log to "sdcard" devicestorage:', savedFile);
else
console.error('failed to save log to', filename);
@@ -14703,6 +14887,12 @@ MailUniverse.prototype = {
saveAccountDef: function(accountDef, folderInfo) {
this._db.saveAccountDef(this.config, accountDef, folderInfo);
+ var account = this.getAccountForAccountId(accountDef.id);
+
+ // If account exists, notify of modification. However on first
+ // save, the account does not exist yet.
+ if (account)
+ this.__notifyModifiedAccount(account);
},
/**
@@ -15492,7 +15682,7 @@ MailUniverse.prototype = {
type: 'downloadBodies',
longtermId: 'session', // don't persist this job.
lifecycle: 'do',
- localStatus: null,
+ localStatus: 'done',
serverStatus: null,
tryCount: 0,
humanOp: 'downloadBodies',
diff --git a/apps/email/js/folder-cards.js b/apps/email/js/folder-cards.js
index 494f3d0..81a7270 100644
--- a/apps/email/js/folder-cards.js
+++ b/apps/email/js/folder-cards.js
@@ -136,8 +136,8 @@ FolderPickerCard.prototype = {
self.updateFolderDom(folder, true);
foldersContainer.insertBefore(folderNode, insertBuddy);
- if (this.mostRecentSyncTimestamp < folder.lastSyncedAt) {
- this.mostRecentSyncTimestamp = folder.lastSyncedAt;
+ if (self.mostRecentSyncTimestamp < folder.lastSyncedAt) {
+ self.mostRecentSyncTimestamp = folder.lastSyncedAt;
dirtySyncTime = true;
}
});
diff --git a/apps/email/js/mail-app.js b/apps/email/js/mail-app.js
index 25f1f7a..b3ca1b8 100644
--- a/apps/email/js/mail-app.js
+++ b/apps/email/js/mail-app.js
@@ -95,7 +95,7 @@ var App = {
// XXX: Because we don't have unified account now, we should switch to
// the latest account which user just added.
var account = showLatest ? acctsSlice.items.slice(-1)[0] :
- acctsSlice.items[0];
+ acctsSlice.defaultAccount;
var foldersSlice = MailAPI.viewFolders('account', account);
foldersSlice.oncomplete = function() {
diff --git a/apps/email/js/marquee.js b/apps/email/js/marquee.js
new file mode 100644
index 0000000..c4f714f
--- /dev/null
+++ b/apps/email/js/marquee.js
@@ -0,0 +1,125 @@
+'use strict';
+
+/**
+ * HTML Marquee in JavaScript/CSS
+ * - slow scrolling of text depending on `behavior' and `timingFunction'
+ * parameters provided to the `activate' method
+ * - start aligned left with delay (see marquee.css for details on classes)
+ *
+ * Creates a HTML element of the form:
+ * <headerNode>
+ * <div id="marquee-h-wrapper">
+ * <div id="marquee-h-text" class="marquee">
+ * Marqueed text content
+ * </div>
+ * </div>
+ * </headerNode>
+ * where 'headerNode' is the node that will containt the text that needs a
+ * marquee (i.e. <header>, <h1>, etc.).
+ */
+var Marquee = {
+ /**
+ * List of supported timing functions
+ */
+ timingFunction: ['linear', 'ease'],
+
+ /**
+ * Setup the marquee DOM structure
+ * @param {string} text the string of text that requires a marquee.
+ * @param {element} headerNode the DOM element parent of the text.
+ */
+ setup: function marquee_setup(text, headerNode) {
+ this._headerNode = headerNode;
+
+ this._headerWrapper = document.getElementById('marquee-h-wrapper');
+ if (!this._headerWrapper) {
+ this._headerWrapper = document.createElement('div');
+ this._headerWrapper.id = 'marquee-h-wrapper';
+ this._headerNode.appendChild(this._headerWrapper);
+ }
+
+ var headerText = document.getElementById('marquee-h-text');
+ if (!headerText) {
+ headerText = document.createElement('div');
+ headerText.id = 'marquee-h-text';
+ this._headerWrapper.appendChild(headerText);
+ }
+
+ headerText.textContent = text;
+ },
+
+ /**
+ * Activate the marquee
+ * NOTE: This should only be called once the DOM structure is updated with
+ * Marquee.setup() and all created DOM elements are appended to the
+ * document, otherwise the text overflow check will not work properly.
+ * @param {string} behavior the way the marquee behaves: 'scroll' (default)
+ * for continuous right-to-left (rtl) scrolling or
+ * 'alternate' for alternating right-to-left and
+ * left-to-right scrolling.
+ * @param {string} timingFun the animation timing function: 'linear' (default)
+ * for linear animation speed, or 'ease' for slow
+ * start of the animation.
+ */
+ activate: function marquee_activate(behavior, timingFun) {
+ if (!this._headerNode || !this._headerWrapper)
+ return;
+
+ // Set defaults for arguments
+ var mode = behavior || 'scroll';
+ var tf = timingFun || null;
+ var timing = (Marquee.timingFunction.indexOf(tf) >= 0) ? tf : 'linear';
+ var marqueeCssClass = 'marquee';
+
+ var titleText = document.getElementById('marquee-h-text');
+
+ // Check if the title text overflows, and if so, add the marquee class
+ // NOTE: this can only be checked it the DOM structure is updated
+ // through Marquee.setup()
+ if (this._headerWrapper.clientWidth < this._headerWrapper.scrollWidth) {
+ // Track the CSS classes added to the text
+ this._marqueeCssClassList = [];
+ switch (mode) {
+ case 'scroll':
+ var cssClass = marqueeCssClass + '-rtl';
+ // Set the width of the marquee to match the text contents length
+ var width = this._headerWrapper.scrollWidth;
+ titleText.style.width = width + 'px';
+ // Start the marquee animation (aligned left with delay)
+ titleText.classList.add(cssClass + '-start-' + timing);
+ this._marqueeCssClassList.push(cssClass + '-start-' + timing);
+
+ var self = this;
+ titleText.addEventListener('animationend', function() {
+ titleText.classList.remove(cssClass + '-start-' + timing);
+ this._marqueeCssClassList.pop();
+ // Correctly calculate the width of the marquee
+ var visibleWidth = self._headerWrapper.clientWidth + 'px';
+ titleText.style.transform = 'translateX(' + visibleWidth + ')';
+ // Enable the continuous marquee
+ titleText.classList.add(cssClass);
+ this._marqueeCssClassList.push(cssClass);
+ });
+ break;
+ case 'alternate':
+ var cssClass = marqueeCssClass + '-alt-';
+ // Set the width of the marquee to match the text contents length
+ var width =
+ this._headerWrapper.scrollWidth - this._headerWrapper.clientWidth;
+ titleText.style.width = width + 'px';
+
+ // Start the marquee animation (aligned left with delay)
+ titleText.classList.add(cssClass + timing);
+ break;
+ }
+ } else {
+ if (!this._marqueeCssClassList)
+ return;
+ // Remove the active marquee CSS classes
+ for (var cssClass in this._marqueeCssClassList)
+ titleText.classList.remove(cssClass);
+
+ titleText.style.transform = '';
+ }
+ }
+};
diff --git a/apps/email/js/message-cards.js b/apps/email/js/message-cards.js
index 7b4a237..db44b92 100755
--- a/apps/email/js/message-cards.js
+++ b/apps/email/js/message-cards.js
@@ -213,6 +213,12 @@ MessageListCard.prototype = {
*/
PROGRESS_CANDYBAR_TIMEOUT_MS: 2000,
+ /**
+ * @type {MessageListTopbar}
+ * @private
+ */
+ _topbar: null,
+
postInsert: function() {
this._hideSearchBoxByScrolling();
@@ -538,7 +544,11 @@ MessageListCard.prototype = {
this.toolbar.searchBtn.classList.remove('disabled');
},
- onSliceRequestComplete: function() {
+
+ /**
+ * @param {number=} newEmailCount Optional number of new messages.
+ */
+ onSliceRequestComplete: function(newEmailCount) {
// We always want our logic to fire, but complete auto-clears before firing.
this.messagesSlice.oncomplete = this._boundSliceRequestComplete;
@@ -552,12 +562,31 @@ MessageListCard.prototype = {
if (this.messagesSlice.items.length === 0 && !this.messagesSlice._fake) {
this.showEmptyLayout();
}
+
+ if (newEmailCount && newEmailCount !== NaN && newEmailCount !== 0) {
+ // Decorate or update the little notification bar that tells the user
+ // how many new emails they've received after a sync.
+ if (this._topbar && this._topbar.getElement() !== null) {
+ // Update the existing status bar.
+ this._topbar.updateNewEmailCount(newEmailCount);
+ } else {
+ this._topbar = new MessageListTopbar(
+ this.scrollContainer, newEmailCount);
+
+ var el =
+ document.getElementsByClassName(MessageListTopbar.CLASS_NAME)[0];
+ this._topbar.decorate(el);
+ this._topbar.render();
+ }
+ }
+
// Consider requesting more data or discarding data based on scrolling that
// has happened since we issued the request. (While requests were pending,
// onScroll ignored scroll events.)
this._onScroll(null);
},
+
onScroll: function(evt) {
if (this._pendingScrollEvent) {
return;
@@ -829,6 +858,10 @@ MessageListCard.prototype = {
}
},
+ _updatePeepDom: function(peep) {
+ peep.element.textContent = peep.name || peep.address;
+ },
+
updateMessageDom: function(firstTime, message) {
var msgNode = message.element;
@@ -850,8 +883,10 @@ MessageListCard.prototype = {
listPerson = message.author;
// author
- msgNode.getElementsByClassName('msg-header-author')[0]
- .textContent = listPerson.name || listPerson.address;
+ listPerson.element =
+ msgNode.getElementsByClassName('msg-header-author')[0];
+ listPerson.onchange = this._updatePeepDom;
+ listPerson.onchange(listPerson);
// date
dateNode.dataset.time = message.date.valueOf();
dateNode.textContent = prettyDate(message.date);
@@ -898,10 +933,15 @@ MessageListCard.prototype = {
if (firstTime) {
// author
var authorNode = msgNode.getElementsByClassName('msg-header-author')[0];
- if (matches.author)
+ if (matches.author) {
appendMatchItemTo(matches.author, authorNode);
- else
- authorNode.textContent = message.author.name || message.author.address;
+ }
+ else {
+ // we can only update the name if it wasn't matched on.
+ message.author.element = authorNode;
+ message.author.onchange = this._updatePeepDom;
+ message.author.onchange(message.author);
+ }
// date
dateNode.dataset.time = message.date.valueOf();
@@ -1111,7 +1151,7 @@ var MAX_QUOTE_CLASS_NAME = 'msg-body-qmax';
function MessageReaderCard(domNode, mode, args) {
this.domNode = domNode;
- this.header = args.header;
+ this.header = args.header.makeCopy();
// The body elements for the (potentially multiple) iframes we created to hold
// HTML email content.
this.htmlBodyNodes = [];
@@ -1227,11 +1267,40 @@ MessageReaderCard.prototype = {
},
onForward: function(event) {
- Cards.eatEventsUntilNextCard();
- var composer = this.header.forwardMessage('inline', function() {
- Cards.pushCard('compose', 'default', 'animate',
- { composer: composer });
- });
+ // If we don't have a body yet, we can't forward the message. In the future
+ // we should visibly disable the button until the body has been retrieved.
+ if (!this.body)
+ return;
+
+ var needToPrompt = this.header.hasAttachments ||
+ this.body.embeddedImageCount > 0;
+
+ var forwardMessage = function() {
+ Cards.eatEventsUntilNextCard();
+ var composer = this.header.forwardMessage('inline', function() {
+ Cards.pushCard('compose', 'default', 'animate',
+ { composer: composer });
+ });
+ }.bind(this);
+
+ if (needToPrompt) {
+ var dialog = msgNodes['attachment-disabled-confirm'].cloneNode(true);
+ ConfirmDialog.show(dialog,
+ {
+ id: 'msg-attachment-disabled-ok',
+ handler: function() {
+ forwardMessage();
+ }
+ },
+ {
+ id: 'msg-attachment-disabled-cancel',
+ handler: null
+ }
+ );
+ } else {
+ forwardMessage();
+ }
+
},
onDelete: function() {
@@ -1285,51 +1354,17 @@ MessageReaderCard.prototype = {
onPeepClick: function(target) {
var contents = msgNodes['contact-menu'].cloneNode(true);
- var email = target.dataset.address;
- var contact = null;
- contents.getElementsByTagName('header')[0].textContent = email;
- document.body.appendChild(contents);
-
- /*
- * Show menu items based on the options which consists of values of
- * the type "_contextMenuType".
- */
- var showContextMenuItems = (function(options) {
- if (options & this._contextMenuType.VIEW_CONTACT)
- contents.querySelector('.msg-contact-menu-view')
- .classList.remove('collapsed');
- if (options & this._contextMenuType.CREATE_CONTACT)
- contents.querySelector('.msg-contact-menu-create-contact')
- .classList.remove('collapsed');
- if (options & this._contextMenuType.ADD_TO_CONTACT)
- contents.querySelector('.msg-contact-menu-add-to-existing-contact')
- .classList.remove('collapsed');
- if (options & this._contextMenuType.REPLY)
- contents.querySelector('.msg-contact-menu-reply')
- .classList.remove('collapsed');
- if (options & this._contextMenuType.NEW_MESSAGE)
- contents.querySelector('.msg-contact-menu-new')
- .classList.remove('collapsed');
- }).bind(this);
-
- var updateName = (function(targetMail, name) {
- if (!name || name === '')
- return;
+ var peep = target.peep;
+ var headerNode = contents.getElementsByTagName('header')[0];
+ // Setup the marquee structure
+ Marquee.setup(peep.address, headerNode);
- // update UI
- var selector = '.msg-peep-bubble[data-address="' +
- targetMail + '"]';
- var nodes = Array.prototype.slice
- .call(this.domNode.querySelectorAll(selector));
-
- for (var i = 0; i < nodes.length; i++) {
- var node = nodes[i];
- var content = node.querySelector('.msg-peep-content');
- node.dataset.name = name;
- content.textContent = name;
- }
- }).bind(this);
+ // Activate marquee once the contents DOM are added to document
+ document.body.appendChild(contents);
+ // XXX Remove 'ease' if linear animation is wanted
+ Marquee.activate('alternate', 'ease');
+ // -- context menu selection handling
var formSubmit = (function(evt) {
document.body.removeChild(contents);
switch (evt.explicitOriginalTarget.className) {
@@ -1339,34 +1374,35 @@ MessageReaderCard.prototype = {
var composer =
MailAPI.beginMessageComposition(this.header, null, null,
function composerReady() {
+ // XXX future work to just let us pass peeps directly; will
+ // require normalization when sending things over the wire to the
+ // backend.
composer.to = [{
- address: target.dataset.address,
- name: target.dataset.name
+ address: peep.address,
+ name: peep.name
}];
Cards.pushCard('compose', 'default', 'animate',
{ composer: composer });
});
break;
case 'msg-contact-menu-view':
- if (contact) {
- var activity = new MozActivity({
- name: 'open',
- data: {
- type: 'webcontacts/contact',
- params: {
- 'id': contact.id
- }
+ var activity = new MozActivity({
+ name: 'open',
+ data: {
+ type: 'webcontacts/contact',
+ params: {
+ 'id': peep.contactId
}
- });
- }
+ }
+ });
break;
case 'msg-contact-menu-create-contact':
var params = {
- 'email': email
+ 'email': peep.address
};
- if (name)
- params['givenName'] = target.dataset.name;
+ if (peep.name)
+ params['givenName'] = peep.name;
var activity = new MozActivity({
name: 'new',
@@ -1375,12 +1411,8 @@ MessageReaderCard.prototype = {
params: params
}
});
-
- activity.onsuccess = function() {
- var contact = activity.result.contact;
- if (contact)
- updateName(email, contact.name);
- };
+ // since we already have contact change listeners that are hooked up
+ // to the UI, we leave it up to them to update the UI for us.
break;
case 'msg-contact-menu-add-to-existing-contact':
var activity = new MozActivity({
@@ -1388,16 +1420,12 @@ MessageReaderCard.prototype = {
data: {
type: 'webcontacts/contact',
params: {
- 'email': email
+ 'email': peep.address
}
}
});
-
- activity.onsuccess = function() {
- var contact = activity.result.contact;
- if (contact)
- updateName(email, contact.name);
- };
+ // since we already have contact change listeners that are hooked up
+ // to the UI, we leave it up to them to update the UI for us.
break;
case 'msg-contact-menu-reply':
//TODO: We need to enter compose view with specific email address.
@@ -1411,23 +1439,36 @@ MessageReaderCard.prototype = {
}).bind(this);
contents.addEventListener('submit', formSubmit);
- ContactDataManager.searchContactData(email, function(contacts) {
- var contextMenuOptions = this._contextMenuType.NEW_MESSAGE;
- var messageType = target.dataset.type;
- if (messageType === 'from')
- contextMenuOptions |= this._contextMenuType.REPLY;
+ // -- populate context menu
+ var contextMenuOptions = this._contextMenuType.NEW_MESSAGE;
+ var messageType = peep.type;
- if (contacts && contacts.length > 0) {
- contact = contacts[0];
- contextMenuOptions |= this._contextMenuType.VIEW_CONTACT;
- } else {
- contact = null;
- contextMenuOptions |= this._contextMenuType.CREATE_CONTACT;
- contextMenuOptions |= this._contextMenuType.ADD_TO_CONTACT;
- }
- showContextMenuItems(contextMenuOptions);
- }.bind(this));
+ if (messageType === 'from')
+ contextMenuOptions |= this._contextMenuType.REPLY;
+
+ if (peep.isContact) {
+ contextMenuOptions |= this._contextMenuType.VIEW_CONTACT;
+ } else {
+ contextMenuOptions |= this._contextMenuType.CREATE_CONTACT;
+ contextMenuOptions |= this._contextMenuType.ADD_TO_CONTACT;
+ }
+
+ if (contextMenuOptions & this._contextMenuType.VIEW_CONTACT)
+ contents.querySelector('.msg-contact-menu-view')
+ .classList.remove('collapsed');
+ if (contextMenuOptions & this._contextMenuType.CREATE_CONTACT)
+ contents.querySelector('.msg-contact-menu-create-contact')
+ .classList.remove('collapsed');
+ if (contextMenuOptions & this._contextMenuType.ADD_TO_CONTACT)
+ contents.querySelector('.msg-contact-menu-add-to-existing-contact')
+ .classList.remove('collapsed');
+ if (contextMenuOptions & this._contextMenuType.REPLY)
+ contents.querySelector('.msg-contact-menu-reply')
+ .classList.remove('collapsed');
+ if (contextMenuOptions & this._contextMenuType.NEW_MESSAGE)
+ contents.querySelector('.msg-contact-menu-new')
+ .classList.remove('collapsed');
},
onLoadBarClick: function(event) {
@@ -1599,6 +1640,28 @@ MessageReaderCard.prototype = {
var header = this.header, body = this.body;
// -- Header
+ function updatePeep(peep) {
+ var nameNode = peep.element.getElementsByClassName('msg-peep-content')[0];
+
+ if (peep.type === 'from') {
+ // We display the sender of the message's name in the header and the
+ // address in the bubble.
+ domNode.getElementsByClassName('msg-reader-header-label')[0]
+ .textContent = peep.name || peep.address;
+
+ nameNode.textContent = peep.address;
+ nameNode.classList.add('msg-peep-address');
+ }
+ else {
+ nameNode.textContent = peep.name || peep.address;
+ if (!peep.name && peep.address) {
+ nameNode.classList.add('msg-peep-address');
+ } else {
+ nameNode.classList.remove('msg-peep-address');
+ }
+ }
+ }
+
function addHeaderEmails(type, peeps) {
var lineClass = 'msg-envelope-' + type + '-line';
var lineNode = domNode.getElementsByClassName(lineClass)[0];
@@ -1610,40 +1673,16 @@ MessageReaderCard.prototype = {
// Because we can avoid having to do multiple selector lookups, we just
// mutate the template in-place...
- var peepTemplate = msgNodes['peep-bubble'],
- contentTemplate =
- peepTemplate.getElementsByClassName('msg-peep-content')[0];
-
- // If the address field is "From", We only show the address and display
- // name in the message header.
- if (lineClass == 'msg-envelope-from-line') {
- var peep = peeps[0];
- // TODO: Display peep name if the address is not exist.
- // Do we nee to deal with that scenario?
- contentTemplate.textContent = peep.name || peep.address;
- peepTemplate.dataset.address = peep.address;
- peepTemplate.dataset.name = peep.name;
- peepTemplate.dataset.type = type;
- if (peep.address) {
- contentTemplate.classList.add('msg-peep-address');
- }
- lineNode.appendChild(peepTemplate.cloneNode(true));
- domNode.getElementsByClassName('msg-reader-header-label')[0]
- .textContent = peep.name || peep.address;
- return;
- }
+ var peepTemplate = msgNodes['peep-bubble'];
+
for (var i = 0; i < peeps.length; i++) {
var peep = peeps[i];
- contentTemplate.textContent = peep.name || peep.address;
- peepTemplate.dataset.address = peep.address;
- peepTemplate.dataset.name = peep.name;
- peepTemplate.dataset.type = type;
- if (!peep.name && peep.address) {
- contentTemplate.classList.add('msg-peep-address');
- } else {
- contentTemplate.classList.remove('msg-peep-address');
- }
- lineNode.appendChild(peepTemplate.cloneNode(true));
+ peep.type = type;
+ peep.element = peepTemplate.cloneNode(true);
+ peep.element.peep = peep;
+ peep.onchange = updatePeep;
+ updatePeep(peep);
+ lineNode.appendChild(peep.element);
}
}
@@ -1776,6 +1815,12 @@ MessageReaderCard.prototype = {
},
die: function() {
+ // Our header was makeCopy()d from the message-list and so needs to be
+ // explicitly removed since it is not part of a slice.
+ if (this.header) {
+ this.header.__die();
+ this.header = null;
+ }
if (this.body) {
this.body.die();
this.body = null;
diff --git a/apps/email/js/message_list_topbar.js b/apps/email/js/message_list_topbar.js
new file mode 100644
index 0000000..34c2a23
--- /dev/null
+++ b/apps/email/js/message_list_topbar.js
@@ -0,0 +1,164 @@
+
+/**
+ * @fileoverview This file provides a MessageListTopbar which is
+ * a little notification bar that tells the user
+ * how many new emails they've received after a sync.
+ */
+
+
+/**
+ * @constructor
+ * @param {Element} scrollContainer Element containing folder messages.
+ * @param {number} newEmailCount The number of new messages we received.
+ */
+function MessageListTopbar(scrollContainer, newEmailCount) {
+ this._scrollContainer = scrollContainer;
+ this._newEmailCount = newEmailCount;
+}
+
+
+/**
+ * @const {string}
+ */
+MessageListTopbar.CLASS_NAME = 'msg-list-topbar';
+
+
+/**
+ * Number of milliseconds after which the status bar should disappear.
+ * @const {number}
+ */
+MessageListTopbar.DISAPPEARS_AFTER_MILLIS = 5000;
+
+
+MessageListTopbar.prototype = {
+ /**
+ * @type {Element}
+ * @private
+ */
+ _el: null,
+
+
+ /**
+ * @type {Element}
+ * @private
+ */
+ _scrollContainer: null,
+
+
+ /**
+ * @type {number}
+ * @private
+ */
+ _newEmailCount: 0,
+
+
+ /**
+ * Update the div with the correct new email count and
+ * listen to it for mouse clicks.
+ * @param {Element} el The div we'll decorate.
+ */
+ decorate: function(el) {
+ el.addEventListener('click', this._onclick.bind(this));
+ this._el = el;
+ this.updateNewEmailCount();
+ return this._el;
+ },
+
+
+ /**
+ * Show our element and set a timer to destroy it after
+ * DISAPPEARS_AFTER_MILLIS.
+ */
+ render: function() {
+ this._el.classList.remove('collapsed');
+ setTimeout(
+ this.destroy.bind(this),
+ MessageListTopbar.DISAPPEARS_AFTER_MILLIS
+ );
+ },
+
+
+ /**
+ * Release the element and any event listeners and cleanup our data.
+ */
+ destroy: function() {
+ if (this._el) {
+ this._el.removeEventListener('click', this._onclick.bind(this));
+ this._el.classList.add('collapsed');
+ this._el.textContent = '';
+ this._el = null;
+ }
+ },
+
+
+ /**
+ * @return {Element} Our underlying element.
+ */
+ getElement: function() {
+ return this._el;
+ },
+
+
+ /**
+ * @param {number} newEmailCount Optional number of new messages we received.
+ */
+ updateNewEmailCount: function(newEmailCount) {
+ if (newEmailCount !== undefined) {
+ this._newEmailCount += newEmailCount;
+ }
+
+ if (this._el !== null) {
+ this._el.textContent =
+ navigator.mozL10n.get('new-emails', { n: this._newEmailCount });
+ }
+ },
+
+
+ /**
+ * @type {Event} evt Some mouseclick event.
+ */
+ _onclick: function(evt) {
+ this.destroy();
+
+ var scrollTop = this._scrollContainer.scrollTop;
+ var dest = this._getScrollDestination();
+ if (scrollTop <= dest) {
+ return;
+ }
+
+ this._scrollUp(this._scrollContainer, dest, 0, 50);
+ },
+
+
+ /**
+ * Move the element up to the specified position over time by increments.
+ * @param {Element} el Some element to scroll.
+ * @param {number} dest The eventual scrollTop value.
+ * @param {number} timeout How long to wait between each time.
+ * @param {number} inc How far to scroll each time.
+ * @private
+ */
+ _scrollUp: function(el, dest, timeout, inc) {
+ var next = el.scrollTop - inc;
+ if (dest >= next) {
+ // This is the last scroll.
+ el.scrollTop = dest;
+ return;
+ }
+
+ el.scrollTop = next;
+ setTimeout(this._scrollUp.bind(this, el, dest, timeout, inc), timeout);
+ },
+
+
+ /**
+ * @return {number} The point where we should scroll to calculated from
+ * the search box height.
+ * @private
+ */
+ _getScrollDestination: function() {
+ var searchBar =
+ document.getElementsByClassName('msg-search-tease-bar')[0];
+ return searchBar.offsetHeight;
+ }
+};
diff --git a/apps/email/js/setup-cards.js b/apps/email/js/setup-cards.js
index 7c94ea8..4b08712 100644
--- a/apps/email/js/setup-cards.js
+++ b/apps/email/js/setup-cards.js
@@ -648,6 +648,28 @@ function SettingsAccountCard(domNode, mode, args) {
domNode.getElementsByClassName('tng-account-delete')[0]
.addEventListener('click', this.onDelete.bind(this), false);
+ // Handle default account checkbox. If already a default, then the checkbox
+ // cannot be unchecked. The default is changed by going to an account that
+ // is not the default and checking that checkbox.
+ var defaultLabelNode = domNode.getElementsByClassName('tng-default-label')[0];
+ var defaultInputNode = domNode.getElementsByClassName('tng-default-input')[0];
+ if (this.account.isDefault) {
+ defaultInputNode.disabled = true;
+ defaultInputNode.checked = true;
+ } else {
+
+ defaultLabelNode.addEventListener('click', function(evt) {
+ evt.stopPropagation();
+ evt.preventBubble();
+
+ if (!defaultInputNode.disabled) {
+ defaultInputNode.disabled = true;
+ defaultInputNode.checked = true;
+ this.account.modifyAccount({ setAsDefault: true });
+ }
+ }.bind(this), false);
+ }
+
// ActiveSync, IMAP and SMTP are protocol names, no need to be localized
domNode.getElementsByClassName('tng-account-type')[0].textContent =
(this.account.type === 'activesync') ? 'ActiveSync' : 'IMAP+SMTP';
diff --git a/apps/email/locales/email.en-US.properties b/apps/email/locales/email.en-US.properties
index 72956fa..14c4aaa 100644
--- a/apps/email/locales/email.en-US.properties
+++ b/apps/email/locales/email.en-US.properties
@@ -187,7 +187,10 @@ compose-discard-message=Discard email?
compose-discard-confirm=Discard
compose-sending-message=Sending email
compose-send-message-failed=Sending email failed
-
+composer-attachment-large=Attachment too large
+composer-attachments-large=Attachments too large
+compose-attchment-size-exceeded=The selected attachment is too large to send with this message.
+compose-attchments-size-exceeded=The selected attachments are too large to send with this message. Try selecting fewer files.
dialog-button-ok=OK
attachment-size-kib={{kilobytes}}K
@@ -204,6 +207,8 @@ message-edit-menu-move=Move
message-edit-menu-delete=Delete
message-edit-delete-confirm=Delete email message?
+message-send-attachment-disabled-confirm=Attachments can not be forwarded.
+
message-multiedit-header={[ plural(n) ]}
message-multiedit-header[zero] = Edit
message-multiedit-header[one] = {{ n }} selected
@@ -329,3 +334,13 @@ toaster-retryable-syncfailed=Unable to connect to server
form-clear-input=Remove text
confirm-dialog-title=Confirmation
+
+# new-emails refers to the text shown on a notification that users
+# get when they receive new, unread email. The ui component that utilizes
+# this lives in apps/email/js/message_list_topbar.js.
+new-emails={[ plural(n) ]}
+new-emails[one]=1 New Email
+new-emails[two]={{ n }} New Emails
+new-emails[few]={{ n }} New Emails
+new-emails[many]={{ n }} New Emails
+new-emails[other]={{ n }} New Emails
diff --git a/apps/email/style/marquee.css b/apps/email/style/marquee.css
new file mode 100644
index 0000000..8d2abc7
--- /dev/null
+++ b/apps/email/style/marquee.css
@@ -0,0 +1,62 @@
+#marquee-h-wrapper {
+ position: absolute;
+ overflow: hidden;
+ white-space: nowrap;
+ line-height: normal;
+ /* Subtract padding-left and amount of padding desired on the right side */
+ width: calc(100% - 3rem - 2rem);
+}
+
+/* --- Right to left (RTL) marquee styles --- */
+.marquee-rtl-start-linear {
+ transform: translateX(0);
+ /* 12s duration of animation: 0% -> -100%;
+ * 3s start delay */
+ animation: left-marquee 12s linear 3s 1;
+}
+
+.marquee-rtl-start-ease {
+ transform: translateX(0);
+ /* 12s duration of animation: 0% -> -100%;
+ * 3s start delay */
+ animation: left-marquee 12s ease-in 3s 1;
+}
+
+.marquee-rtl {
+ /* 20s duration of animation: +100% -> -100%; */
+ animation: left-marquee 20s linear 0s infinite;
+}
+
+@keyframes left-marquee {
+ 100% {
+ transform: translateX(-100%);
+ }
+}
+
+/* --- Alternate (alt: RTL <-> LTR) marquee styles --- */
+.marquee-alt-linear {
+ transform: translateX(0);
+ animation: alt-marquee 15s linear 0s infinite;
+}
+
+.marquee-alt-ease {
+ transform: translateX(0);
+ animation: alt-marquee 15s ease-in-out 0s infinite;
+}
+
+@keyframes alt-marquee {
+ /* 3s delay in start (1.5s = 10%) */
+ 20% {
+ transform: translateX(0);
+ }
+ 50% {
+ transform: translateX(-100%);
+ }
+ /* 3s delay when aligned right */
+ 70% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ transform: translateX(0);
+ }
+}
diff --git a/apps/email/style/message-cards.css b/apps/email/style/message-cards.css
index 88428f0..91faf7d 100644
--- a/apps/email/style/message-cards.css
+++ b/apps/email/style/message-cards.css
@@ -517,14 +517,15 @@ input.msg-search-text-tease {
margin: 0 3rem;
}
-/* Filters selected state */
-ul[role="tablist"].filter li[aria-selected="true"] {
- background: #fff;
- border: solid 1px #999;
- border-top: none;
- color: #000;
-}
-
-ul[role="tablist"].filter li[aria-selected="true"] a {
- color: #000;
+.msg-list-topbar {
+ background-color: #00aacc;
+ color: white;
+ font-size: 1.2rem;
+ left: 0;
+ line-height: 2.2rem;
+ padding: 0.4rem 3rem 0.4rem 3rem;
+ position: absolute;
+ top: 5rem;
+ width: 100%;
+ z-index: 10;
}
diff --git a/apps/email/style/setup-cards.css b/apps/email/style/setup-cards.css
index d49f309..b28e7ea 100644
--- a/apps/email/style/setup-cards.css
+++ b/apps/email/style/setup-cards.css
@@ -47,7 +47,9 @@ input:focus:invalid {
color: black;
font-size: 1.4rem;
line-height: 1.2rem;
- margin: 1.5rem 3rem;
+ padding: 1.5rem;
+ margin-left: 1.5rem;
+ background: none; /* Overrides UA :active background-color */
}
.sup-form-link {
diff --git a/apps/email/test/unit/message_list_topbar_test.js b/apps/email/test/unit/message_list_topbar_test.js
new file mode 100644
index 0000000..9795373
--- /dev/null
+++ b/apps/email/test/unit/message_list_topbar_test.js
@@ -0,0 +1,135 @@
+require('/shared/js/l10n.js');
+
+requireApp('email/js/message_list_topbar.js');
+requireApp('email/test/unit/mock_l10n.js');
+
+suite('MessageListTopbar', function() {
+ var subject, el, scrollContainer, newEmailCount, nativeMozL10n;
+
+ setup(function() {
+ nativeMozL10n = navigator.mozL10n;
+ navigator.mozL10n = MockL10n;
+ // Mock the HTML
+ el = document.createElement('div');
+ el.classList.add(MessageListTopbar.CLASS_NAME);
+ el.classList.add('collapsed');
+
+ scrollContainer = {};
+ newEmailCount = 5;
+ subject = new MessageListTopbar(scrollContainer, newEmailCount);
+ });
+
+ teardown(function() {
+ navigator.mozL10n = nativeMozL10n;
+ });
+
+ suite('#constructor', function() {
+ test('should have been initialized appropriately', function() {
+ assert.deepEqual(subject._scrollContainer, scrollContainer);
+ assert.deepEqual(subject._newEmailCount, newEmailCount);
+ });
+ });
+
+ suite('#decorate', function() {
+ var spy;
+
+ setup(function() {
+ spy = sinon.spy(subject, 'updateNewEmailCount');
+ });
+
+ teardown(function() {
+ subject.updateNewEmailCount.restore();
+ });
+
+ test('should set _el appropriately', function() {
+ subject.decorate(el);
+ assert.equal(subject._el, el);
+ });
+
+ test('should update its new email count appropriately', function() {
+ subject.decorate(el);
+ sinon.assert.calledOnce(spy);
+ });
+
+ test('should set a click listener on the element', function() {
+ sinon.stub(subject, '_onclick');
+ subject.decorate(el);
+ el.click();
+ sinon.assert.calledOnce(subject._onclick);
+ subject._onclick.restore();
+ });
+ });
+
+ suite('#render', function() {
+ setup(function() {
+ subject.decorate(el);
+ });
+
+ test('should show _el', function() {
+ assert.equal(el.classList.contains('collapsed'), true);
+ subject.render();
+ assert.equal(el.classList.contains('collapsed'), false);
+ });
+
+ test('should call destroy after DISAPPEARS_AFTER_MILLIS', function(done) {
+ MessageListTopbar.DISAPPEARS_AFTER_MILLIS = 0;
+ sinon.stub(subject, 'destroy', function() {
+ sinon.assert.calledOnce(subject.destroy);
+ subject.destroy.restore();
+ done();
+ });
+
+ subject.render();
+ });
+ });
+
+ suite('#destroy', function() {
+ setup(function() {
+ subject.decorate(el);
+ subject.render();
+ });
+
+ test('should hide _el', function() {
+ assert.equal(el.classList.contains('collapsed'), false);
+ subject.destroy();
+ assert.equal(el.classList.contains('collapsed'), true);
+ });
+
+ test('should empty the element', function() {
+ assert.notEqual(el.textContent, '');
+ subject.destroy();
+ assert.equal(el.textContent, '');
+ });
+
+ test('should release its internal data', function() {
+ assert.notEqual(subject._el, undefined);
+ subject.destroy();
+ assert.equal(subject._el, undefined);
+ });
+
+ test('should remove the click listener on _el', function() {
+ sinon.stub(subject, '_onclick');
+ subject.destroy();
+ el.click();
+ sinon.assert.notCalled(subject._onclick);
+ });
+ });
+
+ suite('#updateNewEmailCount', function() {
+ test('should update email count', function() {
+ assert.equal(subject._newEmailCount, 5);
+ subject.updateNewEmailCount(10);
+ assert.equal(subject._newEmailCount, 15);
+ });
+
+ test('should update textContent', function() {
+ subject._el = el;
+ assert.equal(subject._el.textContent, '');
+ subject.updateNewEmailCount();
+ assert.equal(
+ subject._el.textContent,
+ navigator.mozL10n.get('new-emails', { n: 5 })
+ );
+ });
+ });
+});
diff --git a/apps/email/test/unit/mock_l10n.js b/apps/email/test/unit/mock_l10n.js
new file mode 100644
index 0000000..6c582aa
--- /dev/null
+++ b/apps/email/test/unit/mock_l10n.js
@@ -0,0 +1,25 @@
+(function(exports) {
+ 'use strict';
+
+ function DateTimeFormat() {
+ this.mInitialized = true;
+ }
+ DateTimeFormat.prototype = {
+ localeFormat: function mockLocaleFormat(time, strFormat) {
+ return '' + time;
+ }
+ };
+
+ var MockL10n = {
+ get: function get(key, params) {
+ if (params) {
+ return key + JSON.stringify(params);
+ }
+ return key;
+ },
+ DateTimeFormat: DateTimeFormat
+ };
+
+ exports.MockL10n = MockL10n;
+
+}(this));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment