VulpesOS/gaia/apps/email/js/ext/drafts/composer.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

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