Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
287 lines
10 KiB
JavaScript
Executable file
287 lines
10 KiB
JavaScript
Executable file
define(
|
|
[
|
|
'mailbuild',
|
|
'../mailchew',
|
|
'../util'
|
|
],
|
|
function(
|
|
MimeNode,
|
|
$mailchew,
|
|
$imaputil
|
|
) {
|
|
|
|
var formatAddresses = $imaputil.formatAddresses;
|
|
|
|
// MimeNode doesn't have a `removeHeader` method, but it's so helpful.
|
|
// Upstream this when possible.
|
|
MimeNode.prototype.removeHeader = function(key) {
|
|
for (var i = 0, len = this._headers.length; i < len; i++) {
|
|
if (this._headers[i].key === key) {
|
|
this._headers.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Ensure that all newlines are of the form \r\n. Our database representation
|
|
* for composed messages uses just \n at the current time.
|
|
*
|
|
* Test coverage is currently provided by end-to-end tests like test_compose
|
|
* since the SMTP fake server knows to generate a 451 if it sees incorrect
|
|
* newlines. (Thanks to qmail!)
|
|
*/
|
|
function normalizeNewlines(body) {
|
|
// If regexps supported look-behinds we could avoid the wasted identity
|
|
// transform on \r\n but that's the only way to find an \n not preceded by
|
|
// an \r. We don't really need the lone \r but if we're normalizing why
|
|
// not normalize 100%?
|
|
return body.replace(/\r?\n|\r/g, '\r\n');
|
|
}
|
|
|
|
/**
|
|
* Abstraction around the mailbuild helper library and our efforts to avoid
|
|
* filling up the device's memory and crashing when we send large messages. We
|
|
* produce a Blob that is made up of some combination of strings and Blobs.
|
|
*
|
|
* ## Composer Endpoints ##
|
|
*
|
|
* There are 3 main endpoints for our data:
|
|
*
|
|
* 1) SMTP transmission. SMTP does not require us to know the size of what we
|
|
* are sending up front. It usually does NOT want the BCC header included in
|
|
* the message.
|
|
*
|
|
* 2) IMAP APPEND. APPEND needs to know the size of the message we are
|
|
* appending up front. It DOES want the BCC header included for archival
|
|
* purposes, so this needs to be a different body.
|
|
*
|
|
* 3) ActiveSync sending. There are actually two paths here; ActiveSync <= 12.x
|
|
* does a more traditional POST of just the MIME data. ActiveSync >= 14.0
|
|
* generates a WBXML command, but it's basically just a wrapper around the MIME
|
|
* data. Because we use XHR's to communicate with the server, the size
|
|
* effectively needs to be known ahead of time.
|
|
*
|
|
* ## The Data ##
|
|
*
|
|
* The actual data that goes into our message is either finite-ish (headers),
|
|
* variable in size but arguably manageable (the contents of the message), or
|
|
* potentially very large in size (attachments). Besides being potentially
|
|
* huge, attachments are also notable because they are immutable once attached.
|
|
* Also, they arguably need to be redundantly stored once attached. That is,
|
|
* if a user attaches an image from DeviceStorage and then later deletes it
|
|
* before the message is sent, you can make a convincing case that the
|
|
* attachment should still be sent with the message. Previously, this would
|
|
* happen as a side-effect of IndexedDB's need to duplicate the contents of
|
|
* Blobs passed into it so it could persist them and manage its life-cycle.
|
|
*
|
|
* @param newRecords
|
|
* The HeaderInfo and BodyInfo for the most recent saved copy of the draft.
|
|
* Used to construct the outgoing message.
|
|
* @param account
|
|
* @param identity
|
|
*/
|
|
function Composer(newRecords, account, identity) {
|
|
var header = this.header = newRecords.header;
|
|
var body = this.body = newRecords.body;
|
|
this.account = account;
|
|
this.identity = identity;
|
|
this.sentDate = new Date(this.header.date);
|
|
this._smartWakeLock = null;
|
|
|
|
// Snapshot data we create for consistency.
|
|
// We're copying nodemailer here; we might want to include some more.
|
|
this.messageId =
|
|
'<' + Date.now() + Math.random().toString(16).substr(1) + '@mozgaia>';
|
|
|
|
var messageNode;
|
|
|
|
// text/plain and text/html
|
|
var textContent = body.bodyReps[0].content[1];
|
|
if (body.bodyReps.length === 2) {
|
|
var htmlContent = body.bodyReps[1].content;
|
|
messageNode = new MimeNode('text/html');
|
|
messageNode.setContent(
|
|
normalizeNewlines(
|
|
$mailchew.mergeUserTextWithHTML(textContent, htmlContent)));
|
|
} else {
|
|
messageNode = new MimeNode('text/plain');
|
|
messageNode.setContent(normalizeNewlines(textContent));
|
|
}
|
|
|
|
var root;
|
|
if (body.attachments.length) {
|
|
root = this._rootNode = new MimeNode('multipart/mixed');
|
|
root.appendChild(messageNode);
|
|
} else {
|
|
root = this._rootNode = messageNode;
|
|
}
|
|
|
|
root.setHeader('From', formatAddresses([this.identity]));
|
|
root.setHeader('Subject', header.subject);
|
|
|
|
if (this.identity.replyTo) {
|
|
root.setHeader('Reply-To', this.identity.replyTo);
|
|
}
|
|
if (header.to && header.to.length) {
|
|
root.setHeader('To', formatAddresses(header.to));
|
|
}
|
|
if (header.cc && header.cc.length) {
|
|
root.setHeader('Cc', formatAddresses(header.cc));
|
|
}
|
|
// Note: We include the BCC header here, even though we also do some
|
|
// BCC trickery below, so that the getEnvelope() function includes
|
|
// the proper "to" addresses in the envelope.
|
|
if (header.bcc && header.bcc.length) {
|
|
root.setHeader('Bcc', formatAddresses(header.bcc));
|
|
}
|
|
|
|
root.setHeader('User-Agent', 'GaiaMail/0.2');
|
|
root.setHeader('Date', this.sentDate.toUTCString());
|
|
root.setHeader('Message-Id', this.messageId);
|
|
if (body.references) {
|
|
root.setHeader('References', body.references);
|
|
}
|
|
|
|
// Set the transfer-encoding to quoted-printable so that mailbuild
|
|
// doesn't attempt to insert linebreaks in HTML mail.
|
|
root.setHeader('Content-Transfer-Encoding', 'quoted-printable');
|
|
|
|
// Mailbuild doesn't currently support blobs. As a workaround,
|
|
// insert a unique placeholder separator, which we will replace with
|
|
// the real contents of the blobs during the sending process.
|
|
this._blobReplacements = [];
|
|
this._uniqueBlobBoundary = '{{blob!' + Math.random() + Date.now() + '}}';
|
|
|
|
body.attachments.forEach(function(attachment) {
|
|
try {
|
|
var attachmentNode = new MimeNode(
|
|
attachment.type,
|
|
{
|
|
// This implies Content-Disposition: attachment
|
|
filename: attachment.name
|
|
});
|
|
// Explicitly indicate that the attachment is base64 encoded. mailbuild
|
|
// only picks base64 for non-text/* MIME parts, but our attachment logic
|
|
// encodes *all* attachments in base64, so base64 is the only correct
|
|
// answer. (Also, failure to base64 encode our _uniqueBlobBoundary breaks
|
|
// the replace logic in withMessageBlob. So base64 all the things!)
|
|
attachmentNode.setHeader('Content-Transfer-Encoding', 'base64');
|
|
attachmentNode.setContent(this._uniqueBlobBoundary);
|
|
root.appendChild(attachmentNode);
|
|
this._blobReplacements.push(new Blob(attachment.file));
|
|
} catch (ex) {
|
|
console.error('Problem attaching attachment:', ex, '\n', ex.stack);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
Composer.prototype = {
|
|
|
|
/**
|
|
* Return the currently-built envelope, in a format compatible with
|
|
* the SMTP library.
|
|
*
|
|
* @return {Object}
|
|
* { from: "me@example.com", to: [] }
|
|
*/
|
|
getEnvelope: function() {
|
|
return this._rootNode.getEnvelope();
|
|
},
|
|
|
|
/**
|
|
* Request that a body be produced as a single Blob with the given options.
|
|
* Multiple calls to this method may overlap concurrently.
|
|
*
|
|
* @param {Object} opts
|
|
* @param {Boolean} [opts.includeBcc=true]
|
|
* @param {Boolean} [opts.smtp=false]
|
|
* Is this for an SMTP server? Matters for dot-stuffing. Our use of
|
|
* Blobs currently has the side-effect of making it impossible for
|
|
* smtpclient's dot-stuffing to work, which is somewhat of a problem.
|
|
* @param {function(blob)} callback
|
|
*/
|
|
withMessageBlob: function(opts, callback) {
|
|
|
|
// Another horrible workaround: Mailbuild _never_ includes the
|
|
// 'Bcc' header in the generated output, and there isn't a simple
|
|
// monkeypatch that could address that behavior. Instead, insert a
|
|
// temporary header in place of BCC, and rename that header in the
|
|
// final output. Alternately, we could maintain a forked version
|
|
// of mailbuild, but we would need to be very careful not to
|
|
// inadvertently clobber our changes during updates.
|
|
var TEMP_BCC = 'Bcc-Temp';
|
|
var TEMP_BCC_REGEX = /^Bcc-Temp: /m;
|
|
|
|
var hasBcc = opts.includeBcc && this.header.bcc && this.header.bcc.length;
|
|
if (hasBcc) {
|
|
this._rootNode.setHeader(TEMP_BCC, formatAddresses(this.header.bcc));
|
|
} else {
|
|
this._rootNode.removeHeader(TEMP_BCC);
|
|
}
|
|
|
|
var str = this._rootNode.build();
|
|
// smtpclient knows how to do dot-stuffing, but we bypass its dot-stuffing
|
|
// logic because smtpclient doesn't understand Blobs.
|
|
if (opts.smtp) {
|
|
str = str.replace(/\n\./g, '\n..');
|
|
}
|
|
|
|
if (hasBcc) {
|
|
str = str.replace(TEMP_BCC_REGEX, 'Bcc: ');
|
|
}
|
|
|
|
// Ensure that the message always ends with a trailing CRLF for
|
|
// SMTP transmission (currently, our SMTP sending logic assumes
|
|
// that this will always be the case):
|
|
if (str.slice(-2) !== '\r\n') {
|
|
str += '\r\n';
|
|
}
|
|
|
|
// Split the message into an array, delimited by our unique blob
|
|
// boundary, interleaving the attachment blobs. Note that we must
|
|
// search for the base64-encoded blob boundary, as at this point
|
|
// it has been encoded for transport.
|
|
var splits = str.split(btoa(this._uniqueBlobBoundary) + '\r\n');
|
|
this._blobReplacements.forEach(function(blob, i) {
|
|
// blob 0 => index 1
|
|
// blob 1 => index 3
|
|
// blob 2 => index 5 ...
|
|
splits.splice((i * 2) + 1, 0, blob);
|
|
});
|
|
|
|
// Build a super-blob from any subparts.
|
|
callback(new Blob(splits, {
|
|
type: this._rootNode.getHeader('content-type')
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Call setSmartWakeLock to assign a SmartWakeLock instance.
|
|
* Accounts will call renewSmartWakeLock() during the sending
|
|
* process, so that the wake lock can continually be renewed during
|
|
* the sending process. This prevents the lock from expiring.
|
|
*
|
|
* @param {SmartWakeLock} wakeLock
|
|
*/
|
|
setSmartWakeLock: function(wakeLock) {
|
|
this._smartWakeLock = wakeLock;
|
|
},
|
|
|
|
/**
|
|
* Renew the SmartWakeLock associated with this composer, if one has
|
|
* been set. Provide an optional reason, which will be logged.
|
|
*
|
|
* @param {String} reason
|
|
*/
|
|
renewSmartWakeLock: function(reason) {
|
|
if (this._smartWakeLock) {
|
|
this._smartWakeLock.renew(reason);
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
Composer: Composer
|
|
};
|
|
|
|
}); // end define
|