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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

188
gaia/apps/email/js/ext/a64.js Executable file
View file

@ -0,0 +1,188 @@
// asuth.
/**
* ASCII-encoding tricks, particularly ordered-base64 encoding for
* lexicographically ordered things like IndexedDB or 64-bit number support that
* we can't use JS numbers for.
*
* The math logic is by me (asuth); hopefully it's not too embarassing.
**/
define(
[
'exports'
],
function(
exports
) {
/**
* A lexicographically ordered base64 encoding. Our two extra characters are {
* and } because they are at the top of the ordering space and have a clear (to
* JS coders) ordering which makes it tractable to eyeball an encoded value and
* not be completely confused/misled.
*/
var ORDERED_ARBITRARY_BASE64_CHARS = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd',
'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
'y', 'z', '{', '}'
];
/**
* Zero padding to get us up to the maximum encoding length of a 64-bit value in
* our encoding (11) or for decimal re-conversion (16).
*/
var ZERO_PADDING = '0000000000000000';
/**
* Encode a JS int in our base64 encoding.
*/
function encodeInt(v, padTo) {
var sbits = [];
do {
// note: bitwise ops are 32-bit only.
// so, this is fine:
sbits.push(ORDERED_ARBITRARY_BASE64_CHARS[v & 0x3f]);
// but this can't be >>> 6 and has to be a divide.
v = Math.floor(v / 64);
} while (v > 0);
sbits.reverse();
var estr = sbits.join('');
if (padTo && estr.length < padTo)
return ZERO_PADDING.substring(0, padTo - estr.length) + estr;
return estr;
}
exports.encodeInt = encodeInt;
/**
* 10^14 >> 14 so that its 'lowest' binary 1 ends up in the one's place. It
* is encoded in 33 bits itself.
*/
var E10_14_RSH_14 = Math.pow(10, 14) / Math.pow(2, 14),
P2_14 = Math.pow(2, 14),
P2_22 = Math.pow(2, 22),
P2_32 = Math.pow(2, 32),
P2_36 = Math.pow(2, 36),
MASK32 = 0xffffffff;
/**
* Convert a decimal uint64 string to a compact string representation that can
* be compared using our helper method `cmpUI64`. We could do direct straight
* string comparison if we were willing to pad all strings out to 11 characters,
* but that's a lot of overhead considering that we expect a lot of our values
* to be muuuuch smaller. (Appropriate padding can be requested for cases
* where the ordering is explicitly desired, like IndexedDB keys. Just only
* request as many bits as you really need!)
*
* JS can handle up to 2^53 reliably which means that for numbers larger than
* that we will have to do a second parse. For that to work (easily), we need
* to pick a power of 10 to cut at where the smallest '1' in its binary encoding
* is at least in the 14th bit so we can pre-shift off 13 bits so when we
* multiply by 10 we don't go floating point, as it were. (We also need to add
* in the relevant bits from the lower parse appropriately shifted.)
*/
exports.parseUI64 = function p(s, padTo) {
// 2^53 is 16 digits long, so any string shorter than that can be handled
// by the built-in logic.
if (s.length < 16) {
return encodeInt(parseInt(s, 10));
}
var lowParse = parseInt(s.substring(s.length - 14), 10),
highParse = parseInt(s.substring(0, s.length - 14), 10),
// multiply the high parse by our scaled power of 10
rawHighBits = highParse * E10_14_RSH_14;
// Now lowParse's low 14 bits are valid, but everything above that needs to
// be mixed (by addition) with rawHighBits. We'll mix in 22 bits from
// rawHighBits to get lowBits to 36 useful bits. The main thing is to lop off
// the higher bits in rawHighBits that we don't want so they don't go float.
// We do want the 37rd bit if there was addition overflow to carry to the
// upper calculation.
var lowBitsAdded = (((rawHighBits % P2_36) * P2_14) % P2_36 +
lowParse % P2_36),
lowBits = lowBitsAdded % P2_36,
overflow = Math.floor(lowBitsAdded / P2_36) % 2;
// We can lop off the low 22-bits of the high bits (since lowBits is taking
// care of that) and combine that with the bits of low above 36.
var highBits = Math.floor(rawHighBits / P2_22) +
Math.floor(lowParse / P2_36) + overflow;
var outStr = encodeInt(highBits) + encodeInt(lowBits, 6);
if (padTo && outStr.length < padTo)
return ZERO_PADDING.substring(0, padTo - outStr.length) + outStr;
return outStr;
};
exports.cmpUI64 = function(a, b) {
// longer equals bigger!
var c = a.length - b.length;
if (c !== 0)
return c;
if (a < b)
return -1;
else if (a > b)
return 1;
return 0;
};
/**
* Convert the output of `parseUI64` back into a decimal string.
*/
exports.decodeUI64 = function d(es) {
var iNonZero = 0;
for (;es.charCodeAt(iNonZero) === 48; iNonZero++) {
}
if (iNonZero)
es = es.substring(iNonZero);
var v, i;
// 8 characters is 48 bits, JS can do that internally.
if (es.length <= 8) {
v = 0;
for (i = 0; i < es.length; i++) {
v = v * 64 + ORDERED_ARBITRARY_BASE64_CHARS.indexOf(es[i]);
}
return v.toString(10);
}
// upper-string gets 28 bits (that could hold 30), lower-string gets 36 bits.
// This is how we did things in encoding is why.
var ues = es.substring(0, es.length - 6), uv = 0,
les = es.substring(es.length - 6), lv = 0;
for (i = 0; i < ues.length; i++) {
uv = uv * 64 + ORDERED_ARBITRARY_BASE64_CHARS.indexOf(ues[i]);
}
for (i = 0; i < les.length; i++) {
lv = lv * 64 + ORDERED_ARBITRARY_BASE64_CHARS.indexOf(les[i]);
}
// Do the division to figure out the "high" string from our encoding (a whole
// number.) Then subtract that whole number off our effective number, leaving
// us dealing with <53 bits so we can just hand it off to the JS engine.
var rsh14val = (uv * P2_22 + Math.floor(lv / P2_14)),
uraw = rsh14val / E10_14_RSH_14,
udv = Math.floor(uraw),
uds = udv.toString();
var rsh14Leftover = rsh14val - udv * E10_14_RSH_14,
lowBitsRemoved = rsh14Leftover * P2_14 + lv % P2_14;
var lds = lowBitsRemoved.toString();
if (lds.length < 14)
lds = ZERO_PADDING.substring(0, 14 - lds.length) + lds;
return uds + lds;
};
//d(p('10000000000000000'));
//d(p('18014398509481984'));
//d(p('1171221845949812801'));
}); // end define

View file

@ -0,0 +1,867 @@
/**
* Common code for creating and working with various account types.
**/
define(
[
'./a64',
'logic',
'./allback',
'require',
'module',
'exports'
],
function(
$a64,
logic,
allback,
require,
$module,
exports
) {
var latchedWithRejections = allback.latchedWithRejections;
// The number of milliseconds to wait for various (non-ActiveSync) XHRs to
// complete during the autoconfiguration process. This value is intentionally
// fairly large so that we don't abort an XHR just because the network is
// spotty.
var AUTOCONFIG_TIMEOUT_MS = 30 * 1000;
var ISPDB_AUTOCONFIG_ROOT =
'https://live.mozillamessaging.com/autoconfig/v1.1/';
function requireConfigurator(type, fn) {
if (type === 'activesync') {
require(['activesync/configurator'], fn);
} else if (type === 'pop3+smtp' || type === 'imap+smtp') {
require(['composite/configurator'], fn);
}
}
function accountTypeToClass(type, callback) {
requireConfigurator(type, function(mod) {
callback(mod.account.Account);
});
}
exports.accountTypeToClass = accountTypeToClass;
// Simple hard-coded autoconfiguration by domain...
var autoconfigByDomain = exports._autoconfigByDomain = {
'localhost': {
type: 'imap+smtp',
incoming: {
hostname: 'localhost',
port: 143,
socketType: 'plain',
username: '%EMAILLOCALPART%',
},
outgoing: {
hostname: 'localhost',
port: 25,
socketType: 'plain',
username: '%EMAILLOCALPART%',
},
},
'fakeimaphost': {
type: 'imap+smtp',
incoming: {
hostname: 'localhost',
port: 0,
socketType: 'plain',
username: '%EMAILLOCALPART%',
},
outgoing: {
hostname: 'localhost',
port: 0,
socketType: 'plain',
username: '%EMAILLOCALPART%',
},
},
'fakepop3host': {
type: 'pop3+smtp',
incoming: {
hostname: 'localhost',
port: 0,
socketType: 'plain',
username: '%EMAILLOCALPART%',
},
outgoing: {
hostname: 'localhost',
port: 0,
socketType: 'plain',
username: '%EMAILLOCALPART%',
},
},
'slocalhost': {
type: 'imap+smtp',
incoming: {
hostname: 'localhost',
port: 993,
socketType: 'SSL',
username: '%EMAILLOCALPART%',
},
outgoing: {
hostname: 'localhost',
port: 465,
socketType: 'SSL',
username: '%EMAILLOCALPART%',
},
},
'fakeashost': {
type: 'activesync',
displayName: 'Test',
incoming: {
// This string will be clobbered with the correct port number when running
// as a unit test.
server: 'http://localhost:8880',
username: '%EMAILADDRESS%',
},
},
// like slocalhost, really just exists to generate a test failure
'saslocalhost': {
type: 'activesync',
displayName: 'Test',
incoming: {
server: 'https://localhost:443',
username: '%EMAILADDRESS%',
},
},
// Mapping for a nonexistent domain for testing a bad domain without it being
// detected ahead of time by the autoconfiguration logic or otherwise.
'nonesuch.nonesuch': {
type: 'imap+smtp',
imapHost: 'nonesuch.nonesuch',
imapPort: 993,
imapCrypto: true,
smtpHost: 'nonesuch.nonesuch',
smtpPort: 465,
smtpCrypto: true,
usernameIsFullEmail: false,
},
};
/**
* Recreate the array of identities for a given account.
*
* @param universe the MailUniverse
* @param accountId the ID for this account
* @param oldIdentities an array of the old identities
* @return the new identities
*/
function recreateIdentities(universe, accountId, oldIdentities) {
var identities = [];
for (var iter in Iterator(oldIdentities)) {
var oldIdentity = iter[1];
identities.push({
id: accountId + '/' + $a64.encodeInt(universe.config.nextIdentityNum++),
name: oldIdentity.name,
address: oldIdentity.address,
replyTo: oldIdentity.replyTo,
signature: oldIdentity.signature,
signatureEnabled: oldIdentity.signatureEnabled
});
}
return identities;
}
exports.recreateIdentities = recreateIdentities;
function fillConfigPlaceholders(userDetails, sourceConfigInfo) {
// Return a mutated copy, don't mutate the original.
var configInfo = JSON.parse(JSON.stringify(sourceConfigInfo));
var details = userDetails.emailAddress.split('@');
var emailLocalPart = details[0], emailDomainPart = details[1];
var domain = emailDomainPart.toLowerCase();
var placeholderFields = {
incoming: ['username', 'hostname', 'server'],
outgoing: ['username', 'hostname'],
};
function fillPlaceholder(value) {
return value.replace('%EMAILADDRESS%', userDetails.emailAddress)
.replace('%EMAILLOCALPART%', emailLocalPart)
.replace('%EMAILDOMAIN%', emailDomainPart)
.replace('%REALNAME%', userDetails.displayName);
}
for (var serverType in placeholderFields) {
var fields = placeholderFields[serverType];
var server = configInfo[serverType];
if (!server) {
continue;
}
for (var iField = 0; iField < fields.length; iField++) {
var field = fields[iField];
if (server.hasOwnProperty(field)) {
server[field] = fillPlaceholder(server[field]);
}
}
}
return configInfo;
}
exports.fillConfigPlaceholders = fillConfigPlaceholders;
/**
* The Autoconfigurator tries to automatically determine account settings, in
* large part by taking advantage of Thunderbird's prior work on autoconfig:
* <https://developer.mozilla.org/en-US/docs/Thunderbird/Autoconfiguration>.
* There are some important differences, however, since we support ActiveSync
* whereas Thunderbird does not.
*
* The v2 process is as follows. All of this is done without a password (since
* it might turn out we don't need a password in the case of OAuth2-based auth.)
*
* 1) Get the domain from the user's email address
* 2) Check hardcoded-into-GELAM account settings for the domain (useful for
* unit tests)
* 3) Check locally stored XML config files in Gaia for the domain at
* `/autoconfig/<domain>`
* 4) In parallel:
* - Do server-hosted autoconfig checks at URLs, passing the user's email
* address in the query string (as `emailaddress`)
* - `https://autoconfig.<domain>/mail/config-v1.1.xml` and
* - `https://<domain>/.well-known/autoconfig/mail/config-v1.1.xml`,
* - Check the Mozilla ISPDB for the domain:
* - `https://live.mozillamessaging.com/autoconfig/v1.1/<domain>`
* - Having the Mozilla ISPDB do an MX lookup.
* 5) If we didn't reach a conclusion in step 4, check the MX lookup result.
* If it differed from the domain, then re-lookup the locally stored XML
* config and failing that, check the ISPDB for that domain.
* 6) If that didn't net us anything, look for evidence of ActiveSync
* AutoDiscover endpoints at the following locations in parallel:
* `https://<domain>/autodiscover/autodiscover.xml`
* `https://autodiscover.<domain>/autodiscover/autodiscover.xml`
*
* This differs from the v1 process in that:
* - v1 did everything in serial not parallel
* - v1 used http, not httpS, to look for self-hosted autoconfig servers.
* - v1 ran autodiscover before checking with the ISPDB but after local and
* server-hosted ISPDB autoconfig files.
* - v1 actually ran autodiscover. Now we just look for evidence of
* autodiscover. AutoDiscover requires being able to authenticate the user
* which implies having the password. Since we're not sure if we need a
* password or not, we can't do that yet. We leave AutoDiscover up to the
* ActiveSync configurator to perform.
* - v1 wanted the user's password
*
* These changes were informed by the following needs and observations:
* - http was a bad idea security-wise, but was done for consistency with
* Thunderbird. The Thunderbird rationale involved DNS also being insecure,
* so an attacker could already win with local network control. However,
* Thunderbird also allowed autoconfig to return settings that didn't
* use SSL/TLS, whereas we do not. (We ignore them).
*
* Thunderbird has recently come around to the use of https instead of http
* for this purpose. It's also worth noting that as far as we know, almost
* no one actually hosts their own autoconfig servers.
*
* - AutoDiscover can be very slow, especially if we're waiting for our requests
* to timeout.
*
* - It's become common to see servers that implement ActiveSync (which we
* strongly dislike) as well as IMAP (which we strongly prefer). By letting
* ActiveSync derail the decision-making process we rob ourselves of the
* ability to have the ISPDB indicate the IMAP is an option.
*
* - If the user's server supports OAuth2, there's no need to make them type in
* their password; it might even be confusing to them.
*
* Our ConfigInfo structures look like the following:
*
* {
* type: 'imap+smtp',
* incoming: {
* hostname: <imap hostname>,
* port: <imap port number>,
* socketType: <one of 'plain', 'SSL', 'STARTTLS'>,
* username: <imap username>,
* authentication: <one of 'password-cleartext', 'xoauth2'>
* },
* outgoing: {
* hostname: <smtp hostname>,
* port: <smtp port>,
* socketType: <one of 'plain', 'SSL', 'STARTTLS'>,
* username: <smtp username>,
* authentication: <one of 'password-cleartext', 'xoauth2'>
* },
* oauth2Settings: null or {
* secretGroup: <group identify which app secrets should be used>,
* authEndpoint: <auth url of the page to show to the user>,
* tokenEndpoint: <url for getting/refreshing tokens (no user ui)>,
* scope: <space-delimited scopes to request>
* }
* }
*
* POP3 is similar to IMAP/SMTP but it's 'pop3+smtp'. ActiveSync looks
* like:
*
* {
* type: 'activesync',
* displayName: <display name>, (optional)
* incoming: {
* server: 'https://<activesync hostname>'
* },
* }
*/
function Autoconfigurator() {
this.timeout = AUTOCONFIG_TIMEOUT_MS;
logic.defineScope(this, 'Autoconfigurator');
}
exports.Autoconfigurator = Autoconfigurator;
Autoconfigurator.prototype = {
/**
* The list of fatal error codes.
*
* What's fatal and why:
* - bad-user-or-pass: We found a server, it told us the credentials were
* bogus. There is no point going on.
* - not-authorized: We found a server, it told us the credentials are fine
* but the access rights are insufficient. There is no point going on.
*
* Non-fatal and why:
* - unknown: If something failed we should keep checking other info sources.
* - no-config-info: The specific source had no details; we should keep
* checking other sources.
*/
_fatalErrors: ['bad-user-or-pass', 'not-authorized'],
/**
* Check the supplied error and return true if it's really a "success" or if
* it's a fatal error we can't recover from.
*
* @param error the error code
* @return true if the error is a "success" or if it's a fatal error
*/
_isSuccessOrFatal: function(error) {
return !error || this._fatalErrors.indexOf(error) !== -1;
},
/**
* Get an XML config file from the supplied url. The format is defined at
* <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat>.
*
* @param url the URL to fetch the config file from
* @param callback a callback taking an error string (if any) and the config
* info, formatted as JSON
*/
_getXmlConfig: function getXmlConfig(url) {
return new Promise(function(resolve, reject) {
var scope = logic.subscope(this, { method: 'GET', url: url });
logic(scope, 'xhr:start');
var xhr = new XMLHttpRequest({mozSystem: true});
xhr.open('GET', url, true);
xhr.timeout = this.timeout;
xhr.onload = function() {
logic(scope, 'xhr:end', { status: xhr.status });
if (xhr.status < 200 || xhr.status >= 300) {
reject('status' + xhr.status);
return;
}
// XXX: For reasons which are currently unclear (possibly a platform
// issue), trying to use responseXML results in a SecurityError when
// running XPath queries. So let's just do an end-run around the
// "security".
self.postMessage({
uid: 0,
type: 'configparser',
cmd: 'accountcommon',
args: [xhr.responseText]
});
self.addEventListener('message', function onworkerresponse(evt) {
var data = evt.data;
if (data.type != 'configparser' || data.cmd != 'accountcommon') {
return;
}
self.removeEventListener(evt.type, onworkerresponse);
var args = data.args;
var config = args[0], status = args[1];
resolve(config);
});
};
// Caution: don't overwrite ".onerror" twice here. Just be careful
// to only assign that once until <http://bugzil.la/949722> is fixed.
xhr.ontimeout = function() {
logic(scope, 'xhr:end', { status: 'timeout' });
reject('timeout');
};
xhr.onerror = function() {
logic(scope, 'xhr:end', { status: 'error' });
reject('error');
};
// At least in the past, Gecko might synchronously throw when we call
// send for a locally-hosted file, so we're sticking with this until the
// end of time.
try {
xhr.send();
}
catch(e) {
logic(scope, 'xhr:end', { status: 'sync-error' });
reject('status404');
}
}.bind(this));
},
/**
* Attempt to get an XML config file locally.
*
* @param domain the domain part of the user's email address
* @param callback a callback taking an error string (if any) and the config
* info, formatted as JSON
*/
_getConfigFromLocalFile: function getConfigFromLocalFile(domain) {
return this._getXmlConfig('/autoconfig/' + encodeURIComponent(domain));
},
/**
* Check whether it looks like there's an AutoDiscover endpoint at the given
* URL. AutoDiscover wants us to be authenticated, so we treat a 401 status
* as success and anything else as failure.
*
* For maximum realism we perform a POST.
*/
_checkAutodiscoverUrl: function(url) {
return new Promise(function(resolve, reject) {
var scope = logic.subscope(this, { method: 'POST', url: url });
logic(scope, 'autodiscoverProbe:start');
var xhr = new XMLHttpRequest({mozSystem: true});
xhr.open('POST', url, true);
xhr.timeout = this.timeout;
var victory = function() {
resolve({
type: 'activesync',
incoming: {
autodiscoverEndpoint: url
}
});
}.bind(this);
xhr.onload = function() {
logic(scope, 'autodiscoverProbe:end', { status: xhr.status });
if (xhr.status === 401) {
victory();
return;
}
reject('status' + xhr.status);
};
xhr.ontimeout = function() {
logic(scope, 'autodiscoverProbe:end', { status: 'timeout' });
reject('timeout');
};
xhr.onerror = function() {
logic(scope, 'autodiscoverProbe:end', { status: 'error' });
reject('error');
};
try {
xhr.send(null);
}
catch(e) {
logic(scope, 'autodiscoverProbe:end', { status: 'sync-error' });
reject('status404');
}
}.bind(this));
},
/**
* Look for AutoDiscover endpoints for the given domain. If we find one, we
* return what amounts to pseudo-config information. We don't actually know
* enough information to do a full autodiscover at this point, so we need to
* return enough for our ActiveSync account's tryToCreateAccount method to
* handle things from there.
*/
_probeForAutodiscover: function(domain) {
var subdirUrl = 'https://' + domain + '/autodiscover/autodiscover.xml';
var domainUrl = 'https://autodiscover.' + domain +
'/autodiscover/autodiscover.xml';
return latchedWithRejections({
subdir: this._checkAutodiscoverUrl(subdirUrl),
domain: this._checkAutodiscoverUrl(domainUrl)
}).then(function(results) {
// Favor the subdirectory discovery point.
if (results.subdir.resolved && results.subdir.value) {
return results.subdir.value;
}
if (results.domain.resolved && results.domain.value) {
return results.domain.value;
}
// Yeah, no AutoDiscover possible.
return null;
}.bind(this));
},
/**
* Attempt to get an XML config file from the Mozilla ISPDB.
*
* @param domain the domain part of the user's email address
*/
_getConfigFromISPDB: function(domain) {
return this._getXmlConfig(ISPDB_AUTOCONFIG_ROOT +
encodeURIComponent(domain));
},
/**
* Look up the DNS MX record for a domain. This currently uses a web service
* instead of querying it directly.
*
* @param domain the domain part of the user's email address
*
* @return {Promise}
* If we locate a MX domain and that domain differs from our own domain, we
* will resolve the promise with that String. If there is no MX domain or
* it is the same as our existing domain, we will resolve with a null value.
* In the event of a problem contacting the ISPDB server, we will reject
* the promise.
*/
_getMX: function getMX(domain) {
return new Promise(function(resolve, reject) {
var scope = logic.subscope(this, { domain: domain });
logic(scope, 'mxLookup:begin');
var xhr = new XMLHttpRequest({mozSystem: true});
xhr.open('GET', 'https://live.mozillamessaging.com/dns/mx/' +
encodeURIComponent(domain), true);
xhr.timeout = this.timeout;
xhr.onload = function() {
var reportDomain = null;
if (xhr.status === 200) {
var normStr = xhr.responseText.split('\n')[0];
if (normStr) {
normStr = normStr.toLowerCase();
// XXX: We need to normalize the domain here to get the base domain,
// but that's complicated because people like putting dots in
// TLDs. For now, let's just pretend no one would do such a horrible
// thing.
var mxDomain = normStr.split('.').slice(-2).join('.');
if (mxDomain !== domain) {
reportDomain = mxDomain;
}
}
}
logic(scope, 'mxLookup:end',
{ 'raw': normStr, normalized: mxDomain, reporting: reportDomain });
resolve(reportDomain);
};
xhr.ontimeout = function() {
logic(scope, 'mxLookup:end', { status: 'timeout' });
reject('timeout');
};
xhr.onerror = function() {
logic(scope, 'mxLookup:end', { status: 'error' });
reject('error');
};
xhr.send();
}.bind(this));
},
_getHostedAndISPDBConfigs: function(domain, emailAddress) {
var commonAutoconfigSuffix = '/mail/config-v1.1.xml?emailaddress=' +
encodeURIComponent(emailAddress);
// subdomain autoconfig URL
var subdomainAutoconfigUrl =
'https://autoconfig.' + domain + commonAutoconfigSuffix;
// .well-known autoconfig URL
var wellKnownAutoconfigUrl =
'https://' + domain + '/.well-known/autoconfig' +
commonAutoconfigSuffix;
return latchedWithRejections({
autoconfigSubdomain: this._getXmlConfig(subdomainAutoconfigUrl),
autoconfigWellKnown: this._getXmlConfig(wellKnownAutoconfigUrl),
ispdb: this._getConfigFromISPDB(domain),
mxDomain: this._getMX(domain)
}).then(function(results) {
// Favor the autoconfig subdomain for historical reasons
if (results.autoconfigSubdomain.resolved &&
results.autoconfigSubdomain.value) {
return { type: 'config', source: 'autoconfig-subdomain',
config: results.autoconfigSubdomain.value };
}
// Then the well-known
if (results.autoconfigWellKnown.resolved &&
results.autoconfigWellKnown.value) {
return { type: 'config', source: 'autoconfig-wellknown',
config: results.autoconfigWellKnown.value };
}
// Then the ISPDB
if (results.ispdb.resolved &&
results.ispdb.value) {
return { type: 'config', source: 'ispdb',
config: results.ispdb.value };
}
if (results.mxDomain.resolved &&
results.mxDomain.value &&
results.mxDomain.value !== domain) {
return { type: 'mx', domain: results.mxDomain.value };
}
return { type: null };
}.bind(this));
},
/**
* Attempt to get an XML config file by checking the DNS MX record and
* querying the Mozilla ISPDB.
*
* @param domain the domain part of the user's email address
* @param callback a callback taking an error string (if any) and the config
* info, formatted as JSON
*/
_getConfigFromMX: function getConfigFromMX(domain, callback) {
var self = this;
this._getMX(domain, function(error, mxDomain, errorDetails) {
if (error)
return callback(error, null, errorDetails);
console.log(' Found MX for', mxDomain);
if (domain === mxDomain)
return callback('no-config-info', null, { status: 'mxsame' });
// If we found a different domain after MX lookup, we should look in our
// local file store (mostly to support Google Apps domains) and, if that
// doesn't work, the Mozilla ISPDB.
console.log(' Looking in local file store');
self._getConfigFromLocalFile(mxDomain, function(error, config,
errorDetails) {
// (Local XML lookup should not have any fatal errors)
if (!error) {
callback(error, config, errorDetails);
return;
}
console.log(' Looking in the Mozilla ISPDB');
self._getConfigFromDB(mxDomain, callback);
});
});
},
_checkGelamConfig: function(domain) {
if (autoconfigByDomain.hasOwnProperty(domain)) {
return autoconfigByDomain[domain];
}
return null;
},
/**
* See the MailAPI.learnAboutAccount documentation for usage information.
*
* Internals:
*
*
*
* @return {Promise}
*/
learnAboutAccount: function(details) {
return new Promise(function(resolve, reject) {
var emailAddress = details.emailAddress;
var emailParts = emailAddress.split('@');
var emailLocalPart = emailParts[0], emailDomainPart = emailParts[1];
var domain = emailDomainPart.toLowerCase();
var scope = logic.subscope(this, { domain: domain });
logic(scope, 'autoconfig:begin');
// Call this when we find a usable config setting to perform appropriate
// normalization, logging, and promise resolution.
var victory = function(sourceConfigInfo, source) {
var configInfo = null, result;
if (sourceConfigInfo) {
configInfo = fillConfigPlaceholders(details, sourceConfigInfo);
if (configInfo.incoming &&
configInfo.incoming.authentication === 'xoauth2') {
result = 'need-oauth2';
} else {
result = 'need-password';
}
} else {
result = 'no-config-info';
}
logic(scope, 'autoconfig:end', {
result: result,
source: source,
configInfo: configInfo
});
resolve({ result: result, source: source, configInfo: configInfo });
}.bind(this);
// Call this if we can't find a configuration.
var failsafeFailure = function(err) {
logic(this, 'autoconfig:end', { err: {
message: err && err.message,
stack: err && err.stack
}});
resolve({ result: 'no-config-info', configInfo: null });
}.bind(this);
// Helper that turns a rejection into a null and outputs a log entry.
var coerceRejectionToNull = function(err) {
logic(scope, 'autoconfig:coerceRejection', { err: err });
return null;
}.bind(this);
// -- Synchronous logic
// - Group 0: hardcoded in GELAM (testing only)
var hardcodedConfig = this._checkGelamConfig(domain);
if (hardcodedConfig) {
victory(hardcodedConfig, 'hardcoded');
return;
}
// -- Asynchronous setup
// This all wants to be a generator. It doesn't make a lot of sense to
// structure this as a chain of then's since we do want an early return.
// This is a good candidate for 'koa' or something like it in the near
// future.
// - Group 1: local config
var localConfigHandler = function(info) {
if (info) {
victory(info, 'local');
return null;
}
// We don't need to coerce because there will be no rejections.
return this._getHostedAndISPDBConfigs(domain, emailAddress)
.then(selfHostedAndISPDBHandler);
}.bind(this);
// - Group 2: self-hosted autoconfig, ISPDB first checks
var mxDomain;
var selfHostedAndISPDBHandler = function(typedResult) {
if (typedResult.type === 'config') {
victory(typedResult.config, typedResult.source);
return null;
}
// Did we get a different MX result?
if (typedResult.type === 'mx') {
mxDomain = typedResult.domain;
return this._getConfigFromLocalFile(mxDomain)
.catch(coerceRejectionToNull)
.then(mxLocalHandler);
}
// No MX result, probe autodiscover.
return this._probeForAutodiscover(domain)
.then(autodiscoverHandler);
}.bind(this);
// - Group 3: MX-derived lookups
var mxLocalHandler = function(info) {
if (info) {
victory(info, 'mx local');
return null;
}
// We didn't know about it locally, ask the ISPDB
return this._getConfigFromISPDB(mxDomain)
.catch(coerceRejectionToNull)
.then(mxISPDBHandler);
}.bind(this);
var mxISPDBHandler = function(info) {
if (info) {
victory(info, 'mx ispdb');
return null;
}
// The ISPDB didn't know, probe for autodiscovery. No coercion needed.
return this._probeForAutodiscover(domain)
.then(autodiscoverHandler);
}.bind(this);
// - Group 4: Autodiscover probing
var autodiscoverHandler = function(info) {
// This is either success or we're simply done.
victory(info, info ? 'autodiscover' : null);
return null;
}.bind(this);
// -- Kick it off.
this._getConfigFromLocalFile(domain)
// Coerce the rejection for our then handler's purpose.
.catch(coerceRejectionToNull)
.then(localConfigHandler)
// Register a catch-handler against localConfigHandler and all of the
// follow-on handlers we associate. Technically we should never call
// this, but better safe than sorry.
.catch(failsafeFailure);
}.bind(this));
},
/**
* Try to create an account for the user's email address by running through
* autoconfigure and, if successful, delegating to the appropriate account
* type.
*
* @param universe the MailUniverse object
* @param userDetails an object containing `emailAddress` and `password`
* attributes
* @param callback a callback taking an error string (if any) and the config
* info, formatted as JSON
*/
tryToCreateAccount: function(universe, userDetails, callback) {
this.learnAboutAccount(userDetails).then(
function success(results) {
// If we found a config and we just need a password, then we're good
// to go.
if (results.result === 'need-password') {
var config = results.configInfo;
requireConfigurator(config.type, function (mod) {
mod.configurator.tryToCreateAccount(universe, userDetails, config,
callback);
});
return;
}
logic(this, 'legacyCreateFail', { result: results.result });
// need-oauth2 is not supported via this code-path; coerce to a config
// failure...
callback('no-config-info');
}.bind(this),
function failure(err) {
callback(err, null, null);
}.bind(this));
},
};
/**
* Recreate an existing account, e.g. after a database upgrade.
*
* @param universe the MailUniverse
* @param oldVersion the old database version, to help with migration
* @param accountInfo the old account info
* @param callback a callback to fire when we've completed recreating the
* account
*/
function recreateAccount(universe, oldVersion, accountInfo, callback) {
requireConfigurator(accountInfo.def.type, function (mod) {
mod.configurator.recreateAccount(universe, oldVersion,
accountInfo, callback);
});
}
exports.recreateAccount = recreateAccount;
function tryToManuallyCreateAccount(universe, userDetails, domainInfo,
callback) {
requireConfigurator(domainInfo.type, function (mod) {
mod.configurator.tryToCreateAccount(universe, userDetails, domainInfo,
callback);
});
}
exports.tryToManuallyCreateAccount = tryToManuallyCreateAccount;
}); // end define

View file

@ -0,0 +1,305 @@
define(function(require, exports) {
var DisasterRecovery = require('./disaster-recovery');
var logic = require('logic');
/**
* The no-op operation for job operations that are not implemented.
* Returns successs in a future turn of the event loop.
*/
function unimplementedJobOperation(op, callback) {
window.setZeroTimeout(function() {
callback(null, null);
});
}
/**
* Account Mixins:
*
* This mixin function is executed from the constructor of the
* CompositeAccount and ActiveSyncAccount, with 'this' being bound to
* the main account instance. If the account has separate receive/send
* parts, they are passed as arguments. (ActiveSync's receive and send
* pieces merely reference the root account.)
*/
exports.accountConstructorMixin = function(receivePiece, sendPiece) {
// The following flags are set on the receivePiece, because the
// receiving side is what manages the job operations (and sending
// messages from the outbox is a job).
// On startup, we need to ignore any stale sendStatus information
// from messages in the outbox. See `sendOutboxMessages` in
// jobmixins.js.
receivePiece.outboxNeedsFreshSync = true;
// This is a runtime flag, used to temporarily prevent
// `sendOutboxMessages` from executing, such as when the user is
// actively trying to edit the list of messages in the Outbox.
receivePiece.outboxSyncEnabled = true;
};
/**
* @args[
* @param[op MailOp]
* @param[mode @oneof[
* @case['local_do']{
* Apply the mutation locally to our database rep.
* }
* @case['check']{
* Check if the manipulation has been performed on the server. There
* is no need to perform a local check because there is no way our
* database can be inconsistent in its view of this.
* }
* @case['do']{
* Perform the manipulation on the server.
* }
* @case['local_undo']{
* Undo the mutation locally.
* }
* @case['undo']{
* Undo the mutation on the server.
* }
* ]]
* @param[callback @func[
* @args[
* @param[error @oneof[String null]]
* ]
* ]]
* }
* ]
*/
exports.runOp = function runOp(op, mode, callback) {
console.log('runOp(' + mode + ': ' + JSON.stringify(op).substring(0, 160) +
')');
var methodName = mode + '_' + op.type;
// If the job driver doesn't support the operation, assume that it
// is a moot operation that will succeed. Assign it a no-op callback
// that completes in the next tick, so as to maintain job ordering.
var method = this._jobDriver[methodName];
if (!method) {
console.warn('Unsupported op:', op.type, 'mode:', mode);
method = unimplementedJobOperation;
}
var alreadyCompleted = false;
var jobCompletedCallback = function(error, resultIfAny, accountSaveSuggested) {
// If DisasterRecovery already called the completion callback due
// to an unforeseen error, ensure that we don't try to
// double-resolve this job.
if (alreadyCompleted) {
console.warn('Job already completed, ignoring secondary completion:',
mode, JSON.stringify(op).substring(0, 160), error, resultIfAny);
return;
}
alreadyCompleted = true;
DisasterRecovery.clearCurrentAccountOp(this);
this._jobDriver.postJobCleanup(error);
// We used to log the runOp_end here, but this is too early because the
// book-keeping for the op actually happens in the following callback. So
// we leave it to _localOpCompleted and _serverOpCompleted to log this.
// defer the callback to the next tick to avoid deep recursion
window.setZeroTimeout(function() {
callback(error, resultIfAny, accountSaveSuggested);
});
}.bind(this);
DisasterRecovery.setCurrentAccountOp(this, op, jobCompletedCallback);
// Legacy tests:
logic(this, 'runOp_begin', { mode: mode, type: op.type, op: op });
// New-style tests:
Object.defineProperty(op, '_logicAsyncEvent', {
configurable: true,
enumerable: false, // So that we don't try to JSONify it.
value: logic.startAsync(this, 'runOp', {
mode: mode, type: op.type, op: op
})
});
try {
method.call(this._jobDriver, op, jobCompletedCallback);
}
catch (ex) {
DisasterRecovery.clearCurrentAccountOp(this);
logic(this, 'opError', { mode: mode, type: op.type, ex: ex });
}
};
/**
* Return the folder metadata for the first folder with the given type, or null
* if no such folder exists.
*/
exports.getFirstFolderWithType = function(type) {
var folders = this.folders;
for (var iFolder = 0; iFolder < folders.length; iFolder++) {
if (folders[iFolder].type === type)
return folders[iFolder];
}
return null;
};
exports.getFolderByPath = function(folderPath) {
var folders = this.folders;
for (var iFolder = 0; iFolder < folders.length; iFolder++) {
if (folders[iFolder].path === folderPath)
return folders[iFolder];
}
return null;
};
/**
* Ensure that local-only folders live in a reasonable place in the
* folder hierarchy by moving them if necessary.
*
* We proactively create local-only folders at the root level before
* we synchronize with the server; if possible, we want these
* folders to reside as siblings to other system-level folders on
* the account. This is called at the end of syncFolderList, after
* we have learned about all existing server folders.
*/
exports.normalizeFolderHierarchy = function() {
// Find a folder for which we'd like to become a sibling.
var sibling =
this.getFirstFolderWithType('drafts') ||
this.getFirstFolderWithType('sent');
// If for some reason we can't find those folders yet, that's
// okay, we will try this again after the next folder sync.
if (!sibling) {
return;
}
var parent = this.getFolderMetaForFolderId(sibling.parentId);
// NOTE: `parent` may be null if `sibling` is a top-level folder.
var foldersToMove = [this.getFirstFolderWithType('localdrafts'),
this.getFirstFolderWithType('outbox')];
foldersToMove.forEach(function(folder) {
// These folders should always exist, but we double-check here
// for safety. Also, if the folder is already in the right
// place, we're done.
if (!folder || folder.parentId === sibling.parentId) {
return;
}
console.log('Moving folder', folder.name,
'underneath', parent && parent.name || '(root)');
this.universe.__notifyRemovedFolder(this, folder);
// On `delim`: We previously attempted to discover a
// server-specific root delimiter. ActiveSync hard-codes "/". POP3
// doesn't even go that far. An empty delimiter would be
// incorrect, as it could cause folder paths to smush into one
// another. In the case where our folder doesn't specify a
// delimiter, fall back to the standard-ish '/'.
if (parent) {
folder.path = parent.path + (parent.delim || '/') + folder.name;
folder.delim = parent.delim || '/';
folder.parentId = parent.id;
folder.depth = parent.depth + 1;
} else {
folder.path = folder.name;
folder.delim = '/';
folder.parentId = null;
folder.depth = 0;
}
this.universe.__notifyAddedFolder(this, folder);
}, this);
};
/**
* Save the state of this account to the database. This entails updating all
* of our highly-volatile state (folderInfos which contains counters, accuracy
* structures, and our block info structures) as well as any dirty blocks.
*
* This should be entirely coherent because the structured clone should occur
* synchronously during this call, but it's important to keep in mind that if
* that ever ends up not being the case that we need to cause mutating
* operations to defer until after that snapshot has occurred.
*/
exports.saveAccountState = function(reuseTrans, callback, reason) {
if (!this._alive) {
logic(this, 'accountDeleted', { reason: 'saveAccountState' });
return null;
}
logic(this, 'saveAccountState_begin', { reason: reason,
folderSaveCount: null });
// Indicate save is active, in case something, like
// signaling the end of a sync, needs to run after
// a save, via runAfterSaves.
this._saveAccountStateActive = true;
if (!this._deferredSaveAccountCalls) {
this._deferredSaveAccountCalls = [];
}
if (callback)
this.runAfterSaves(callback);
var perFolderStuff = [], self = this;
for (var iFolder = 0; iFolder < this.folders.length; iFolder++) {
var folderPub = this.folders[iFolder],
folderStorage = this._folderStorages[folderPub.id],
folderStuff = folderStorage.generatePersistenceInfo();
if (folderStuff)
perFolderStuff.push(folderStuff);
}
var folderSaveCount = perFolderStuff.length;
var trans = this._db.saveAccountFolderStates(
this.id, this._folderInfos, perFolderStuff,
this._deadFolderIds,
function stateSaved() {
this._saveAccountStateActive = false;
logic(this, 'saveAccountState_end', { reason: reason,
folderSaveCount: folderSaveCount });
// NB: we used to log when the save completed, but it ended up being
// annoying to the unit tests since we don't block our actions on
// the completion of the save at this time.
var callbacks = this._deferredSaveAccountCalls;
this._deferredSaveAccountCalls = [];
callbacks.forEach(function(callback) {
callback();
});
}.bind(this),
reuseTrans);
// Reduce the length of time perFolderStuff and its contents are kept alive.
perFolderStuff = null;
this._deadFolderIds = null;
return trans;
};
exports.runAfterSaves = function(callback) {
if (this._saveAccountStateActive || this._saveAccountIsImminent) {
this._deferredSaveAccountCalls.push(callback);
} else {
callback();
}
};
/**
* This function goes through each folder storage object in
* an account and performs the necessary upgrade steps if
* there is a new version. See upgradeIfNeeded in mailslice.js.
* Note: This function schedules a job for each folderStorage
* object in the account.
*/
exports.upgradeFolderStoragesIfNeeded = function() {
for (var key in this._folderStorages) {
var storage = this._folderStorages[key];
storage.upgradeIfNeeded();
}
}
}); // end define

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,322 @@
/**
* Configurator for activesync
**/
define(
[
'logic',
'../accountcommon',
'../a64',
'./account',
'../date',
'tcp-socket',
'require',
'exports'
],
function(
logic,
$accountcommon,
$a64,
$asacct,
$date,
tcpSocket,
require,
exports
) {
function checkServerCertificate(url, callback) {
var match = /^https:\/\/([^:/]+)(?::(\d+))?/.exec(url);
// probably unit test http case?
if (!match) {
callback(null);
return;
}
var port = match[2] ? parseInt(match[2], 10) : 443,
host = match[1];
console.log('checking', host, port, 'for security problem');
var sock = tcpSocket.open(host, port);
function reportAndClose(err) {
if (sock) {
var wasSock = sock;
sock = null;
try {
wasSock.close();
}
catch (ex) {
}
callback(err);
}
}
// this is a little dumb, but since we don't actually get an event right now
// that tells us when our secure connection is established, and connect always
// happens, we write data when we connect to help trigger an error or have us
// receive data to indicate we successfully connected.
// so, the deal is that connect is going to happen.
sock.onopen = function() {
sock.send(
new TextEncoder('utf-8').encode('GET /images/logo.png HTTP/1.1\n\n'));
};
sock.onerror = function(err) {
var reportErr = null;
if (err && typeof(err) === 'object' &&
/^Security/.test(err.name))
reportErr = 'bad-security';
reportAndClose(reportErr);
};
sock.ondata = function(data) {
reportAndClose(null);
};
}
var scope = logic.scope('ActivesyncConfigurator');
exports.account = $asacct;
exports.configurator = {
timeout: 30 * 1000,
_getFullDetailsFromAutodiscover: function($asproto, userDetails, url,
callback) {
logic(scope, 'autodiscover:begin', { url: url });
$asproto.raw_autodiscover(
url, userDetails.emailAddress, userDetails.password, self.timeout,
/* redirects are okay */ false,
function(error, config) {
if (error) {
var failureType = 'no-config-info',
failureDetails = {};
if (error instanceof $asproto.HttpError) {
if (error.status === 401)
failureType = 'bad-user-or-pass';
else if (error.status === 403)
failureType = 'not-authorized';
else
failureDetails.status = error.status;
}
else if (error instanceof $asproto.AutodiscoverDomainError) {
logic(scope, 'autodiscover.error', { message: error.message });
}
logic(scope, 'autodiscover:end', { url: url, err: failureType });
callback(failureType, null, failureDetails);
return;
}
logic(scope, 'autodiscover:end',
{ url: url, server: config.mobileSyncServer.url });
var autoconfig = {
type: 'activesync',
displayName: config.user.name,
incoming: {
server: config.mobileSyncServer.url,
username: config.user.email
},
};
callback(null, autoconfig, null);
});
},
/**
* There are 2 scenarios we can get invoked with:
* - Direct creation. We already know the ActiveSync endpoint. This happens
* from a hardcoded (for testing) or local (hotmail.com/outlook.com)
* autoconfig entry OR from a user typing that stuff in manually.
*
* - Indirection creation. We just know an AutoDiscover endpoing and need
* to run AutoDiscover. If our autoconfig process probed and found some
* AutoDiscover looking endpoints, that's how we end up here. It's also
* conceivable that in the future the manual config mode could use this
* path.
*/
tryToCreateAccount: function(universe, userDetails, domainInfo, callback) {
require(['activesync/protocol'], function ($asproto) {
if (domainInfo.incoming.autodiscoverEndpoint) {
this._getFullDetailsFromAutodiscover(
$asproto, userDetails, domainInfo.incoming.autodiscoverEndpoint,
function(err, fullConfigInfo, errDetails) {
// If we got an error, pass it directly back.
if (err) {
callback(err, fullConfigInfo, errDetails);
return;
}
// Otherwise we have a config and should continue the creation
// process.
this._createAccountUsingFullInfo(
universe, userDetails, fullConfigInfo, callback, $asproto);
}.bind(this));
return;
}
// We should have full config info then. Just call direct in.
this._createAccountUsingFullInfo(universe, userDetails, domainInfo,
callback, $asproto);
}.bind(this));
},
_createAccountUsingFullInfo: function(universe, userDetails, domainInfo,
callback, $asproto) {
logic(scope, 'create:begin', { server: domainInfo.incoming.server });
var credentials = {
username: domainInfo.incoming.username,
password: userDetails.password,
};
var deviceId = $asacct.makeUniqueDeviceId();
var self = this;
var conn = new $asproto.Connection(deviceId);
conn.open(domainInfo.incoming.server, credentials.username,
credentials.password);
conn.timeout = $asacct.DEFAULT_TIMEOUT_MS;
conn.connect(function(error, options) {
if (error) {
// This error is basically an indication of whether we were able to
// call getOptions or not. If the XHR request completed, we get an
// HttpError. If we timed out or an XHR error occurred, we get a
// general Error.
var failureType,
failureDetails = { server: domainInfo.incoming.server };
if (error instanceof $asproto.HttpError) {
if (error.status === 401) {
failureType = 'bad-user-or-pass';
}
else if (error.status === 403) {
failureType = 'not-authorized';
}
// Treat any other errors where we talked to the server as a problem
// with the server.
else {
failureType = 'server-problem';
failureDetails.status = error.status;
}
}
else {
// We didn't talk to the server, so it's either an unresponsive
// server or a server with a bad certificate. (We require https
// outside of unit tests so there's no need to branch here.)
checkServerCertificate(
domainInfo.incoming.server,
function(securityError) {
var failureType;
if (securityError)
failureType = 'bad-security';
else
failureType = 'unresponsive-server';
callback(failureType, null, failureDetails);
});
return;
}
logic(scope, 'create:end', {
server: domainInfo.incoming.server,
err: failureType
});
callback(failureType, null, failureDetails);
return;
}
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
var accountDef = {
id: accountId,
name: userDetails.accountName || userDetails.emailAddress,
defaultPriority: $date.NOW(),
type: 'activesync',
syncRange: 'auto',
syncInterval: userDetails.syncInterval || 0,
notifyOnNew: userDetails.hasOwnProperty('notifyOnNew') ?
userDetails.notifyOnNew : true,
playSoundOnSend: userDetails.hasOwnProperty('playSoundOnSend') ?
userDetails.playSoundOnSend : true,
credentials: credentials,
connInfo: {
server: domainInfo.incoming.server,
deviceId: deviceId
},
identities: [
{
id: accountId + '/' +
$a64.encodeInt(universe.config.nextIdentityNum++),
name: userDetails.displayName || domainInfo.displayName,
address: userDetails.emailAddress,
replyTo: null,
signature: null
},
]
};
logic(scope, 'create:end', {
server: domainInfo.incoming.server,
id: accountId
});
self._loadAccount(universe, accountDef, conn, function (account) {
callback(null, account, null);
});
});
},
recreateAccount: function cfg_as_ra(universe, oldVersion, oldAccountInfo,
callback) {
var oldAccountDef = oldAccountInfo.def;
var credentials = {
username: oldAccountDef.credentials.username,
password: oldAccountDef.credentials.password,
};
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
var accountDef = {
id: accountId,
name: oldAccountDef.name,
type: 'activesync',
syncRange: oldAccountDef.syncRange,
syncInterval: oldAccountDef.syncInterval || 0,
notifyOnNew: oldAccountDef.hasOwnProperty('notifyOnNew') ?
oldAccountDef.notifyOnNew : true,
playSoundOnSend: oldAccountDef.hasOwnProperty('playSoundOnSend') ?
oldAccountDef.playSoundOnSend : true,
credentials: credentials,
connInfo: {
server: oldAccountDef.connInfo.server,
deviceId: oldAccountDef.connInfo.deviceId ||
$asacct.makeUniqueDeviceId()
},
identities: $accountcommon.recreateIdentities(universe, accountId,
oldAccountDef.identities)
};
this._loadAccount(universe, accountDef, null, function (account) {
callback(null, account, null);
});
},
/**
* Save the account def and folder info for our new (or recreated) account and
* then load it.
*/
_loadAccount: function cfg_as__loadAccount(universe, accountDef,
protoConn, callback) {
// XXX: Just reload the old folders when applicable instead of syncing the
// folder list again, which is slow.
var folderInfo = {
$meta: {
nextFolderNum: 0,
nextMutationNum: 0,
lastFolderSyncAt: 0,
syncKey: '0',
},
$mutations: [],
$mutationState: {},
};
universe.saveAccountDef(accountDef, folderInfo);
universe._loadAccount(accountDef, folderInfo, protoConn, callback);
},
};
}); // end define

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,472 @@
define(
[
'logic',
'mix',
'../jobmixins',
'../drafts/jobs',
'activesync/codepages/AirSync',
'activesync/codepages/Email',
'activesync/codepages/Move',
'module',
'require',
'exports'
],
function(
logic,
mix,
$jobmixins,
draftsJobs,
$AirSync,
$Email,
$Move,
$module,
require,
exports
) {
'use strict';
var $wbxml;
function lazyConnection(cbIndex, fn, failString) {
return function lazyRun() {
var args = Array.slice(arguments),
errback = args[cbIndex],
self = this;
require(['wbxml'], function (wbxml) {
if (!$wbxml) {
$wbxml = wbxml;
}
self.account.withConnection(errback, function () {
fn.apply(self, args);
}, failString);
});
};
}
function ActiveSyncJobDriver(account, state) {
this.account = account;
// XXX for simplicity for now, let's assume that ActiveSync GUID's are
// maintained across folder moves.
this.resilientServerIds = true;
this._heldMutexReleasers = [];
logic.defineScope(this, 'ActiveSyncJobDriver',
{ accountId: this.account.id });
this._state = state;
// (we only need to use one as a proxy for initialization)
if (!state.hasOwnProperty('suidToServerId')) {
state.suidToServerId = {};
state.moveMap = {};
}
this._stateDelta = {
serverIdMap: null,
moveMap: null,
};
}
exports.ActiveSyncJobDriver = ActiveSyncJobDriver;
ActiveSyncJobDriver.prototype = {
//////////////////////////////////////////////////////////////////////////////
// helpers
postJobCleanup: $jobmixins.postJobCleanup,
allJobsDone: $jobmixins.allJobsDone,
_accessFolderForMutation: function(folderId, needConn, callback, deathback,
label) {
var storage = this.account.getFolderStorageForFolderId(folderId),
self = this;
storage.runMutexed(label, function(releaseMutex) {
self._heldMutexReleasers.push(releaseMutex);
var syncer = storage.folderSyncer;
if (needConn) {
self.account.withConnection(callback, function () {
try {
callback(syncer.folderConn, storage);
}
catch (ex) {
logic(self, 'callbackErr', { ex: ex });
}
});
} else {
try {
callback(syncer.folderConn, storage);
}
catch (ex) {
logic(self, 'callbackErr', { ex: ex });
}
}
});
},
_partitionAndAccessFoldersSequentially:
$jobmixins._partitionAndAccessFoldersSequentially,
//////////////////////////////////////////////////////////////////////////////
// modtags
local_do_modtags: $jobmixins.local_do_modtags,
do_modtags: lazyConnection(1, function(op, jobDoneCallback, undo) {
// Note: this method is derived from the IMAP implementation.
var addTags = undo ? op.removeTags : op.addTags,
removeTags = undo ? op.addTags : op.removeTags;
function getMark(tag) {
if (addTags && addTags.indexOf(tag) !== -1)
return true;
if (removeTags && removeTags.indexOf(tag) !== -1)
return false;
return undefined;
}
var markRead = getMark('\\Seen');
var markFlagged = getMark('\\Flagged');
var as = $AirSync.Tags;
var em = $Email.Tags;
var aggrErr = null;
this._partitionAndAccessFoldersSequentially(
op.messages, true,
function perFolder(folderConn, storage, serverIds, namers, callWhenDone) {
var modsToGo = 0;
function tagsModded(err) {
if (err) {
console.error('failure modifying tags', err);
aggrErr = 'unknown';
return;
}
op.progress += (undo ? -serverIds.length : serverIds.length);
if (--modsToGo === 0)
callWhenDone();
}
// Filter out any offline headers, since the server naturally can't do
// anything for them. If this means we have no headers at all, just bail
// out.
serverIds = serverIds.filter(function(srvid) { return !!srvid; });
if (!serverIds.length) {
callWhenDone();
return;
}
folderConn.performMutation(
function withWriter(w) {
for (var i = 0; i < serverIds.length; i++) {
w.stag(as.Change)
.tag(as.ServerId, serverIds[i])
.stag(as.ApplicationData);
if (markRead !== undefined)
w.tag(em.Read, markRead ? '1' : '0');
if (markFlagged !== undefined)
w.stag(em.Flag)
.tag(em.Status, markFlagged ? '2' : '0')
.etag();
w.etag(as.ApplicationData)
.etag(as.Change);
}
},
function mutationPerformed(err) {
if (err)
aggrErr = err;
callWhenDone();
});
},
function allDone() {
jobDoneCallback(aggrErr);
},
function deadConn() {
aggrErr = 'aborted-retry';
},
/* reverse if we're undoing */ undo,
'modtags');
}),
check_modtags: function(op, callback) {
callback(null, 'idempotent');
},
local_undo_modtags: $jobmixins.local_undo_modtags,
undo_modtags: function(op, callback) {
this.do_modtags(op, callback, true);
},
//////////////////////////////////////////////////////////////////////////////
// move
local_do_move: $jobmixins.local_do_move,
do_move: lazyConnection(1, function(op, jobDoneCallback) {
/*
* The ActiveSync command for this does not produce or consume SyncKeys.
* As such, we don't need to acquire mutexes for the source folders for
* synchronization correctness, although it is helpful for ordering
* purposes and reducing confusion.
*
* For the target folder a similar logic exists as long as the server-issued
* GUID's are resilient against folder moves. However, we do require in
* all cases that before synchronizing the target folder that we make sure
* all move operations to the folder have completed so we message doesn't
* disappear and then show up again. XXX we are not currently enforcing this
* yet.
*/
var aggrErr = null, account = this.account,
targetFolderStorage = this.account.getFolderStorageForFolderId(
op.targetFolder);
var mo = $Move.Tags;
this._partitionAndAccessFoldersSequentially(
op.messages, true,
function perFolder(folderConn, storage, serverIds, namers, callWhenDone) {
// Filter out any offline headers, since the server naturally can't do
// anything for them. If this means we have no headers at all, just bail
// out.
serverIds = serverIds.filter(function(srvid) { return !!srvid; });
if (!serverIds.length) {
callWhenDone();
return;
}
// Filter out any offline headers, since the server naturally can't do
// anything for them. If this means we have no headers at all, just bail
// out.
serverIds = serverIds.filter(function(srvid) { return !!srvid; });
if (!serverIds.length) {
callWhenDone();
return;
}
var w = new $wbxml.Writer('1.3', 1, 'UTF-8');
w.stag(mo.MoveItems);
for (var i = 0; i < serverIds.length; i++) {
w.stag(mo.Move)
.tag(mo.SrcMsgId, serverIds[i])
.tag(mo.SrcFldId, storage.folderMeta.serverId)
.tag(mo.DstFldId, targetFolderStorage.folderMeta.serverId)
.etag(mo.Move);
}
w.etag(mo.MoveItems);
account.conn.postCommand(w, function(err, response) {
if (err) {
aggrErr = err;
console.error('failure moving messages:', err);
}
callWhenDone();
});
},
function allDone() {
jobDoneCallback(aggrErr, null, true);
},
function deadConn() {
aggrErr = 'aborted-retry';
},
false,
'move');
}),
check_move: function(op, jobDoneCallback) {
},
local_undo_move: $jobmixins.local_undo_move,
undo_move: function(op, jobDoneCallback) {
},
//////////////////////////////////////////////////////////////////////////////
// delete
local_do_delete: $jobmixins.local_do_delete,
do_delete: lazyConnection(1, function(op, jobDoneCallback) {
var aggrErr = null;
var as = $AirSync.Tags;
var em = $Email.Tags;
this._partitionAndAccessFoldersSequentially(
op.messages, true,
function perFolder(folderConn, storage, serverIds, namers, callWhenDone) {
// Filter out any offline headers, since the server naturally can't do
// anything for them. If this means we have no headers at all, just bail
// out.
serverIds = serverIds.filter(function(srvid) { return !!srvid; });
if (!serverIds.length) {
callWhenDone();
return;
}
folderConn.performMutation(
function withWriter(w) {
for (var i = 0; i < serverIds.length; i++) {
w.stag(as.Delete)
.tag(as.ServerId, serverIds[i])
.etag(as.Delete);
}
},
function mutationPerformed(err) {
if (err) {
aggrErr = err;
console.error('failure deleting messages:', err);
}
callWhenDone();
});
},
function allDone() {
jobDoneCallback(aggrErr, null, true);
},
function deadConn() {
aggrErr = 'aborted-retry';
},
false,
'delete');
}),
check_delete: function(op, callback) {
callback(null, 'idempotent');
},
local_undo_delete: $jobmixins.local_undo_delete,
// TODO implement
undo_delete: function(op, callback) {
callback('moot');
},
//////////////////////////////////////////////////////////////////////////////
// syncFolderList
//
// Synchronize our folder list. This should always be an idempotent operation
// that makes no sense to undo/redo/etc.
local_do_syncFolderList: function(op, doneCallback) {
doneCallback(null);
},
do_syncFolderList: lazyConnection(1, function(op, doneCallback) {
var account = this.account, self = this;
// The inbox needs to be resynchronized if there was no server id and we
// have active slices displaying the contents of the folder. (No server id
// means the sync will not happen.)
var inboxFolder = account.getFirstFolderWithType('inbox'),
inboxStorage;
if (inboxFolder && inboxFolder.serverId === null)
inboxStorage = account.getFolderStorageForFolderId(inboxFolder.id);
account.syncFolderList(function(err) {
if (!err)
account.meta.lastFolderSyncAt = Date.now();
// save if it worked
doneCallback(err ? 'aborted-retry' : null, null, !err);
if (inboxStorage && inboxStorage.hasActiveSlices) {
if (!err) {
console.log("Refreshing fake inbox");
inboxStorage.resetAndRefreshActiveSlices();
}
// XXX: If we do have an error here, we should probably report
// syncfailed on the slices to let the user retry. However, what needs
// retrying is syncFolderList, not syncing the messages in a folder.
// Since that's complicated to handle, and syncFolderList will retry
// automatically, we can ignore that case for now.
}
});
}, 'aborted-retry'),
check_syncFolderList: function(op, doneCallback) {
doneCallback(null, 'coherent-notyet');
},
local_undo_syncFolderList: function(op, doneCallback) {
doneCallback('moot');
},
undo_syncFolderList: function(op, doneCallback) {
doneCallback('moot');
},
//////////////////////////////////////////////////////////////////////////////
// downloadBodies: Download the bodies from a list of messages
local_do_downloadBodies: $jobmixins.local_do_downloadBodies,
do_downloadBodies: $jobmixins.do_downloadBodies,
check_downloadBodies: $jobmixins.check_downloadBodies,
//////////////////////////////////////////////////////////////////////////////
// downloadBodyReps: Download the bodies from a single message
local_do_downloadBodyReps: $jobmixins.local_do_downloadBodyReps,
do_downloadBodyReps: $jobmixins.do_downloadBodyReps,
check_downloadBodyReps: $jobmixins.check_downloadBodyReps,
//////////////////////////////////////////////////////////////////////////////
// download: Download one or more attachments from a single message
local_do_download: $jobmixins.local_do_download,
do_download: $jobmixins.do_download,
check_download: $jobmixins.check_download,
local_undo_download: $jobmixins.local_undo_download,
undo_download: $jobmixins.undo_download,
//////////////////////////////////////////////////////////////////////////////
local_do_sendOutboxMessages: $jobmixins.local_do_sendOutboxMessages,
do_sendOutboxMessages: $jobmixins.do_sendOutboxMessages,
check_sendOutboxMessages: $jobmixins.check_sendOutboxMessages,
local_undo_sendOutboxMessages: $jobmixins.local_undo_sendOutboxMessages,
undo_sendOutboxMessages: $jobmixins.undo_sendOutboxMessages,
local_do_setOutboxSyncEnabled: $jobmixins.local_do_setOutboxSyncEnabled,
// upgrade: perfom necessary upgrades when the db version changes
local_do_upgradeDB: $jobmixins.local_do_upgradeDB,
//////////////////////////////////////////////////////////////////////////////
// purgeExcessMessages is a NOP for activesync
local_do_purgeExcessMessages: function(op, doneCallback) {
doneCallback(null);
},
do_purgeExcessMessages: function(op, doneCallback) {
doneCallback(null);
},
check_purgeExcessMessages: function(op, doneCallback) {
return 'idempotent';
},
local_undo_purgeExcessMessages: function(op, doneCallback) {
doneCallback(null);
},
undo_purgeExcessMessages: function(op, doneCallback) {
doneCallback(null);
},
//////////////////////////////////////////////////////////////////////////////
};
mix(ActiveSyncJobDriver.prototype, draftsJobs.draftsMixins);
}); // end define

200
gaia/apps/email/js/ext/allback.js Executable file
View file

@ -0,0 +1,200 @@
/**
* Simple coordination logic that might be better handled by promises, although
* we probably have the edge in comprehensibility for now.
**/
define(['exports'], function(exports) {
/**
* Create multiple named callbacks whose results are aggregated and a single
* callback invoked once all the callbacks have returned their result. This
* is intended to provide similar benefit to $Q.all in our non-promise world
* while also possibly being more useful.
*
* Example:
* @js{
* var callbacks = allbackMaker(['foo', 'bar'], function(aggrData) {
* console.log("Foo's result was", aggrData.foo);
* console.log("Bar's result was", aggrData.bar);
* });
* asyncFooFunc(callbacks.foo);
* asyncBarFunc(callbacks.bar);
* }
*
* Protection against a callback being invoked multiple times is provided as
* an anti-foot-shooting measure. Timeout logic and other protection against
* potential memory leaks is not currently provided, but could be.
*/
exports.allbackMaker = function allbackMaker(names, allDoneCallback) {
var aggrData = Object.create(null), callbacks = {},
waitingFor = names.concat();
names.forEach(function(name) {
// (build a consistent shape for aggrData regardless of callback ordering)
aggrData[name] = undefined;
callbacks[name] = function anAllback(callbackResult) {
var i = waitingFor.indexOf(name);
if (i === -1) {
console.error("Callback '" + name + "' fired multiple times!");
throw new Error("Callback '" + name + "' fired multiple times!");
}
waitingFor.splice(i, 1);
if (arguments.length > 1)
aggrData[name] = arguments;
else
aggrData[name] = callbackResult;
if (waitingFor.length === 0 && allDoneCallback)
allDoneCallback(aggrData);
};
});
return callbacks;
};
/**
* A lightweight deferred 'run-all'-like construct for waiting for
* multiple callbacks to finish executing, with a final completion
* callback at the end. Neither promises nor Q provide a construct
* quite like this; Q.all and Promise.all tend to either require all
* promises to be created up front, or they return when the first
* error occurs. This is designed to allow you to wait for an unknown
* number of callbacks, with the knowledge that they're going to
* execute anyway -- no sense trying to abort early.
*
* Results passed to each callback can be passed along to the final
* result by adding a `name` parameter when calling latch.defer().
*
* Example usage:
*
* var latch = allback.latch();
* setTimeout(latch.defer('timeout1'), 200);
* var cb = latch.defer('timeout2');
* cb('foo');
* latch.then(function(results) {
* console.log(results.timeout2[0]); // => 'foo'
* });
*
* The returned latch is an A+ Promises-compatible thennable, so you
* can chain multiple callbacks to the latch.
*
* The promise will never fail; it will always succeed. Each
* `.defer()` call can be passed a `name`; if a name is provided, that
* callback's arguments will be made available as a key on the result
* object.
*
* NOTE: The latch will not actually fire completion until you've
* attached a callback handler. This way, you can create the latch
* before you know how many callbacks you'll need; when you've called
* .defer() as many times as necessary, you can call `then()` to
* actually fire the completion function (when they have all
* completed).
*/
exports.latch = function() {
var ready = false;
var deferred = {};
// Avoid Object.prototype and any for-enumerations getting tripped up
var results = Object.create(null);
var count = 0;
deferred.promise = new Promise(function(resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});
function defer(name) {
count++;
var resolved = false;
return function resolve() {
if (resolved) {
var err = new Error("You have already resolved this deferred!");
// Exceptions aren't always readily visible, but this is a
// serious error and needs to be addressed.
console.error(err + '\n' + err.stack);
throw err;
}
resolved = true;
// 'name' might be the integer zero (among other integers) if
// the callee is doing array processing, so we pass anything not
// equalling null and undefined, even the poor falsey zero.
if (name != null) {
results[name] = Array.slice(arguments);
}
if (--count === 0) {
setZeroTimeout(function() {
deferred.resolve(results);
});
}
};
}
var unlatch = defer();
return {
defer: defer,
then: function () {
var ret = deferred.promise.then.apply(deferred.promise, arguments);
if (!ready) {
ready = true;
unlatch();
}
return ret;
}
};
};
/**
* Given the results object from an allback.latch() where named callbacks were
* used (or else we won't save the result!) and the callbacks use the form of
* callback(errIfAny, ...), find and return the first error object, or return
* null if none was found.
*
* Important notes:
* - Use this for callback-based idioms in the node style
* - You MUST use latch.defer(name), not latch.defer()!
* - Because of JS object property ordering, we actually will return the result
* of the first callback that fired with an error value, but you probably
* do not want to be depending on this too much.
*/
exports.extractErrFromCallbackArgs = function(results) {
// If there are any errors, find and propagate.
var anyErr = null;
for (var key in results) {
var args = results[key];
var errIfAny = args[0];
if (errIfAny) {
anyErr = errIfAny;
break;
}
}
return anyErr;
};
exports.latchedWithRejections = function(namedPromises) {
return new Promise(function(resolve, reject) {
// Avoid Object.prototype
var results = Object.create(null);
var pending = 0;
Object.keys(namedPromises).forEach(function(name) {
pending++;
var promise = namedPromises[name];
promise.then(
function(result) {
results[name] = { resolved: true, value: result };
if (--pending === 0) {
resolve(results);
}
},
function(err) {
results[name] = { resolved: false, value: err };
if (--pending === 0) {
resolve(results);
}
});
});
// If we didn't end up scheduling anything
if (!pending) {
resolve(results);
}
});
};
}); // end define

View file

@ -0,0 +1,52 @@
define(
[
'exports'
],
function(
exports
) {
/**
* Asynchronously fetch the contents of a Blob, returning a Uint8Array.
* Exists because there is no FileReader in Gecko workers and this totally
* works. In discussion, it sounds like :sicking wants to deprecate the
* FileReader API anyways.
*
* Our consumer in this case is our specialized base64 encode that wants a
* Uint8Array since that is more compactly represented than a binary string
* would be.
*
* @param blob {Blob}
* @param callback {Function(err, Uint8Array)}
*/
function asyncFetchBlobAsUint8Array(blob, callback) {
var blobUrl = URL.createObjectURL(blob);
var xhr = new XMLHttpRequest();
xhr.open('GET', blobUrl, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function() {
// blobs currently result in a status of 0 since there is no server.
if (xhr.status !== 0 && (xhr.status < 200 || xhr.status >= 300)) {
callback(xhr.status);
return;
}
callback(null, new Uint8Array(xhr.response));
};
xhr.onerror = function() {
callback('error');
};
try {
xhr.send();
}
catch(ex) {
console.error('XHR send() failure on blob');
callback('error');
}
URL.revokeObjectURL(blobUrl);
}
return {
asyncFetchBlobAsUint8Array: asyncFetchBlobAsUint8Array
};
}); // end define

View file

@ -0,0 +1,273 @@
/**
* Configurator for fake
**/
define(
[
'logic',
'../accountcommon',
'../a64',
'../accountmixins',
'../imap/account',
'../pop3/account',
'../smtp/account',
'../allback',
'exports'
],
function(
logic,
$accountcommon,
$a64,
$acctmixins,
$imapacct,
$pop3acct,
$smtpacct,
allback,
exports
) {
var PIECE_ACCOUNT_TYPE_TO_CLASS = {
'imap': $imapacct.ImapAccount,
'pop3': $pop3acct.Pop3Account,
'smtp': $smtpacct.SmtpAccount,
};
/**
* Composite account type to expose account piece types with individual
* implementations (ex: imap, smtp) together as a single account. This is
* intended to be a very thin layer that shields consuming code from the
* fact that IMAP and SMTP are not actually bundled tightly together.
*/
function CompositeAccount(universe, accountDef, folderInfo, dbConn,
receiveProtoConn) {
this.universe = universe;
this.id = accountDef.id;
this.accountDef = accountDef;
logic.defineScope(this, 'Account', { accountId: this.id });
// Currently we don't persist the disabled state of an account because it's
// easier for the UI to be edge-triggered right now and ensure that the
// triggering occurs once each session.
this._enabled = true;
this.problems = [];
// For oauth2, hold on to a "last renew attempt" timestamp. However, since it
// uses performance.now() that can be reset depending on clock time and
// environment (shared worker always resets to 0 for instance), always reset
// the value here to 0. It is just a transient timestamp that is useful
// during the lifetime of the app.
if (accountDef.credentials && accountDef.credentials.oauth2) {
accountDef.credentials.oauth2._transientLastRenew = 0;
}
this.identities = accountDef.identities;
if (!PIECE_ACCOUNT_TYPE_TO_CLASS.hasOwnProperty(accountDef.receiveType)) {
logic(this, 'badAccountType', { type: accountDef.receiveType });
}
if (!PIECE_ACCOUNT_TYPE_TO_CLASS.hasOwnProperty(accountDef.sendType)) {
logic(this, 'badAccountType', { type: accountDef.sendType });
}
this._receivePiece =
new PIECE_ACCOUNT_TYPE_TO_CLASS[accountDef.receiveType](
universe, this,
accountDef.id, accountDef.credentials, accountDef.receiveConnInfo,
folderInfo, dbConn, receiveProtoConn);
this._sendPiece =
new PIECE_ACCOUNT_TYPE_TO_CLASS[accountDef.sendType](
universe, this,
accountDef.id, accountDef.credentials,
accountDef.sendConnInfo, dbConn);
// expose public lists that are always manipulated in place.
this.folders = this._receivePiece.folders;
this.meta = this._receivePiece.meta;
this.mutations = this._receivePiece.mutations;
// Mix in any fields common to all accounts.
$acctmixins.accountConstructorMixin.call(
this, this._receivePiece, this._sendPiece);
}
exports.Account = exports.CompositeAccount = CompositeAccount;
CompositeAccount.prototype = {
toString: function() {
return '[CompositeAccount: ' + this.id + ']';
},
get supportsServerFolders() {
return this._receivePiece.supportsServerFolders;
},
toBridgeWire: function() {
return {
id: this.accountDef.id,
name: this.accountDef.name,
type: this.accountDef.type,
defaultPriority: this.accountDef.defaultPriority,
enabled: this.enabled,
problems: this.problems,
syncRange: this.accountDef.syncRange,
syncInterval: this.accountDef.syncInterval,
notifyOnNew: this.accountDef.notifyOnNew,
playSoundOnSend: this.accountDef.playSoundOnSend,
identities: this.identities,
credentials: {
username: this.accountDef.credentials.username,
outgoingUsername: this.accountDef.credentials.outgoingUsername,
// no need to send the password to the UI.
// send all the oauth2 stuff we've got, though.
oauth2: this.accountDef.credentials.oauth2
},
servers: [
{
type: this.accountDef.receiveType,
connInfo: this.accountDef.receiveConnInfo,
activeConns: this._receivePiece.numActiveConns || 0,
},
{
type: this.accountDef.sendType,
connInfo: this.accountDef.sendConnInfo,
activeConns: this._sendPiece.numActiveConns || 0,
}
],
};
},
toBridgeFolder: function() {
return {
id: this.accountDef.id,
name: this.accountDef.name,
path: this.accountDef.name,
type: 'account',
};
},
get enabled() {
return this._enabled;
},
set enabled(val) {
this._enabled = this._receivePiece.enabled = val;
},
saveAccountState: function(reuseTrans, callback, reason) {
return this._receivePiece.saveAccountState(reuseTrans, callback, reason);
},
get _saveAccountIsImminent() {
return this.__saveAccountIsImminent;
},
set _saveAccountIsImminent(val) {
this.___saveAccountIsImminent =
this._receivePiece._saveAccountIsImminent = val;
},
runAfterSaves: function(callback) {
return this._receivePiece.runAfterSaves(callback);
},
allOperationsCompleted: function() {
if (this._receivePiece.allOperationsCompleted) {
this._receivePiece.allOperationsCompleted();
}
},
/**
* Check that the account is healthy in that we can login at all.
* We'll check both the incoming server and the SMTP server; for
* simplicity, the errors are returned as follows:
*
* callback(incomingErr, outgoingErr);
*
* If you don't want to check both pieces, you should just call
* checkAccount on the receivePiece or sendPiece as appropriate.
*/
checkAccount: function(callback) {
var latch = allback.latch();
this._receivePiece.checkAccount(latch.defer('incoming'));
this._sendPiece.checkAccount(latch.defer('outgoing'));
latch.then(function(results) {
callback(results.incoming[0], results.outgoing[0]);
});
},
/**
* Shutdown the account; see `MailUniverse.shutdown` for semantics.
*/
shutdown: function(callback) {
this._sendPiece.shutdown();
this._receivePiece.shutdown(callback);
},
accountDeleted: function() {
this._sendPiece.accountDeleted();
this._receivePiece.accountDeleted();
},
deleteFolder: function(folderId, callback) {
return this._receivePiece.deleteFolder(folderId, callback);
},
sliceFolderMessages: function(folderId, bridgeProxy) {
return this._receivePiece.sliceFolderMessages(folderId, bridgeProxy);
},
searchFolderMessages: function(folderId, bridgeHandle, phrase, whatToSearch) {
return this._receivePiece.searchFolderMessages(
folderId, bridgeHandle, phrase, whatToSearch);
},
syncFolderList: function(callback) {
return this._receivePiece.syncFolderList(callback);
},
sendMessage: function(composer, callback) {
return this._sendPiece.sendMessage(
composer,
function(err, errDetails) {
if (!err) {
// The saving is done asynchronously as a best-effort.
this._receivePiece.saveSentMessage(composer);
}
callback(err, errDetails, null);
}.bind(this));
},
getFolderStorageForFolderId: function(folderId) {
return this._receivePiece.getFolderStorageForFolderId(folderId);
},
getFolderMetaForFolderId: function(folderId) {
return this._receivePiece.getFolderMetaForFolderId(folderId);
},
runOp: function(op, mode, callback) {
return this._receivePiece.runOp(op, mode, callback);
},
/**
* Kick off jobs to create required folders, both locally and on the
* server. See imap/account.js and activesync/account.js for documentation.
*
* @param {function} callback
* Called when all jobs have run.
*/
ensureEssentialOnlineFolders: function(callback) {
return this._receivePiece.ensureEssentialOnlineFolders(callback);
},
getFirstFolderWithType: $acctmixins.getFirstFolderWithType,
upgradeFolderStoragesIfNeeded: function() {
for (var key in this._receivePiece._folderStorages) {
var storage = this._receivePiece._folderStorages[key];
storage.upgradeIfNeeded();
}
}
};
}); // end define

View file

@ -0,0 +1,343 @@
/**
* Configurator for imap+smtp and pop3+smtp.
**/
define(
[
'logic',
'../accountcommon',
'../a64',
'../allback',
'./account',
'../date',
'require',
'exports'
],
function(
logic,
$accountcommon,
$a64,
$allback,
$account,
$date,
require,
exports
) {
var allbackMaker = $allback.allbackMaker;
exports.account = $account;
exports.configurator = {
tryToCreateAccount: function(universe, userDetails, domainInfo,
callback) {
var credentials, incomingInfo, smtpConnInfo, incomingType;
if (domainInfo) {
incomingType = (domainInfo.type === 'imap+smtp' ? 'imap' : 'pop3');
var password = null;
// If the account has an outgoingPassword, use that; otherwise
// use the main password. We must take care to treat null values
// as potentially valid in the future, if we allow password-free
// account configurations.
if (userDetails.outgoingPassword !== undefined) {
password = userDetails.outgoingPassword;
} else {
password = userDetails.password;
}
credentials = {
username: domainInfo.incoming.username,
password: userDetails.password,
outgoingUsername: domainInfo.outgoing.username,
outgoingPassword: password,
};
if (domainInfo.oauth2Tokens) {
// We need to save off all the information so:
// - the front-end can reauthorize exclusively from this info.
// - the back-end can refresh its token
// - on upgrades so we can know if our scope isn't good enough. (Note
// that we're not saving off the secret group; upgrades would need to
// factor in the auth or token endpoints.)
credentials.oauth2 = {
authEndpoint: domainInfo.oauth2Settings.authEndpoint,
tokenEndpoint: domainInfo.oauth2Settings.tokenEndpoint,
scope: domainInfo.oauth2Settings.scope,
clientId: domainInfo.oauth2Secrets.clientId,
clientSecret: domainInfo.oauth2Secrets.clientSecret,
refreshToken: domainInfo.oauth2Tokens.refreshToken,
accessToken: domainInfo.oauth2Tokens.accessToken,
expireTimeMS: domainInfo.oauth2Tokens.expireTimeMS,
// Treat the access token like it was recently retrieved; although we
// generally expect the XOAUTH2 case should go through without
// failure, in the event something is wrong, immediately re-fetching
// a new accessToken is not going to be useful for us.
_transientLastRenew: $date.PERFNOW()
};
}
incomingInfo = {
hostname: domainInfo.incoming.hostname,
port: domainInfo.incoming.port,
crypto: (typeof domainInfo.incoming.socketType === 'string' ?
domainInfo.incoming.socketType.toLowerCase() :
domainInfo.incoming.socketType),
};
if (incomingType === 'pop3') {
incomingInfo.preferredAuthMethod = null;
}
smtpConnInfo = {
emailAddress: userDetails.emailAddress, // used for probing
hostname: domainInfo.outgoing.hostname,
port: domainInfo.outgoing.port,
crypto: (typeof domainInfo.outgoing.socketType === 'string' ?
domainInfo.outgoing.socketType.toLowerCase() :
domainInfo.outgoing.socketType),
};
}
// Note: For OAUTH accounts, the credentials may be updated
// in-place if a new access token was required. We don't need to
// explicitly save those changes here because we define the
// account with the same object below.
var incomingPromise = new Promise(function(resolve, reject) {
if (incomingType === 'imap') {
require(['../imap/probe'], function(probe) {
probe.probeAccount(credentials, incomingInfo).then(resolve, reject);
});
} else {
require(['../pop3/probe'], function(probe) {
probe.probeAccount(credentials, incomingInfo).then(resolve, reject);
});
}
});
var outgoingPromise = new Promise(function(resolve, reject) {
require(['../smtp/probe'], function(probe) {
probe.probeAccount(credentials, smtpConnInfo).then(resolve, reject);
});
});
// Note: Promise.all() will fire the catch handler as soon as one
// of the promises is rejected. While this means we will only see
// the first error that returns, it actually works well for our
// semantics, as we only notify the user about one side's problems
// at a time.
Promise.all([incomingPromise, outgoingPromise])
.then(function(results) {
var incomingConn = results[0].conn;
var defineAccount;
if (incomingType === 'imap') {
defineAccount = this._defineImapAccount;
} else if (incomingType === 'pop3') {
incomingInfo.preferredAuthMethod = incomingConn.authMethod;
defineAccount = this._definePop3Account;
}
defineAccount.call(this,
universe, userDetails, credentials,
incomingInfo, smtpConnInfo, incomingConn,
callback);
}.bind(this))
.catch(function(ambiguousErr) {
// One of the account sides failed. Normally we leave the
// IMAP/POP3 side open for reuse, but if the SMTP
// configuration falied we must close the incoming connection.
// (If the incoming side failed as well, we won't receive the
// `.then` callback.)
incomingPromise.then(function incomingOkButOutgoingFailed(result) {
result.conn.close();
// the error is no longer ambiguous; it was SMTP
callback(ambiguousErr, /* conn: */ null,
{ server: smtpConnInfo.hostname });
}).catch(function incomingFailed(incomingErr) {
callback(incomingErr, /* conn: */ null,
{ server: incomingInfo.hostname });
});
});
},
recreateAccount: function(universe, oldVersion, oldAccountInfo, callback) {
var oldAccountDef = oldAccountInfo.def;
var credentials = {
username: oldAccountDef.credentials.username,
password: oldAccountDef.credentials.password,
// (if these two keys are null, keep them that way:)
outgoingUsername: oldAccountDef.credentials.outgoingUsername,
outgoingPassword: oldAccountDef.credentials.outgoingPassword,
authMechanism: oldAccountDef.credentials.authMechanism,
oauth2: oldAccountDef.credentials.oauth2
};
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
var oldType = oldAccountDef.type || 'imap+smtp';
var accountDef = {
id: accountId,
name: oldAccountDef.name,
type: oldType,
receiveType: oldType.split('+')[0],
sendType: 'smtp',
syncRange: oldAccountDef.syncRange,
syncInterval: oldAccountDef.syncInterval || 0,
notifyOnNew: oldAccountDef.hasOwnProperty('notifyOnNew') ?
oldAccountDef.notifyOnNew : true,
playSoundOnSend: oldAccountDef.hasOwnProperty('playSoundOnSend') ?
oldAccountDef.playSoundOnSend : true,
credentials: credentials,
receiveConnInfo: {
hostname: oldAccountDef.receiveConnInfo.hostname,
port: oldAccountDef.receiveConnInfo.port,
crypto: oldAccountDef.receiveConnInfo.crypto,
preferredAuthMethod:
oldAccountDef.receiveConnInfo.preferredAuthMethod || null,
},
sendConnInfo: {
hostname: oldAccountDef.sendConnInfo.hostname,
port: oldAccountDef.sendConnInfo.port,
crypto: oldAccountDef.sendConnInfo.crypto,
},
identities: $accountcommon.recreateIdentities(universe, accountId,
oldAccountDef.identities)
};
this._loadAccount(universe, accountDef,
oldAccountInfo.folderInfo, null, function(account) {
callback(null, account, null);
});
},
/**
* Define an account now that we have verified the credentials are good and
* the server meets our minimal functionality standards. We are also
* provided with the protocol connection that was used to perform the check
* so we can immediately put it to work.
*/
_defineImapAccount: function(universe, userDetails, credentials,
incomingInfo, smtpConnInfo, imapProtoConn,
callback) {
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
var accountDef = {
id: accountId,
name: userDetails.accountName || userDetails.emailAddress,
defaultPriority: $date.NOW(),
type: 'imap+smtp',
receiveType: 'imap',
sendType: 'smtp',
syncRange: 'auto',
syncInterval: userDetails.syncInterval || 0,
notifyOnNew: userDetails.hasOwnProperty('notifyOnNew') ?
userDetails.notifyOnNew : true,
playSoundOnSend: userDetails.hasOwnProperty('playSoundOnSend') ?
userDetails.playSoundOnSend : true,
credentials: credentials,
receiveConnInfo: incomingInfo,
sendConnInfo: smtpConnInfo,
identities: [
{
id: accountId + '/' +
$a64.encodeInt(universe.config.nextIdentityNum++),
name: userDetails.displayName,
address: userDetails.emailAddress,
replyTo: null,
signature: null,
signatureEnabled: false
},
]
};
this._loadAccount(universe, accountDef, null,
imapProtoConn, function(account) {
callback(null, account, null);
});
},
/**
* Define an account now that we have verified the credentials are good and
* the server meets our minimal functionality standards. We are also
* provided with the protocol connection that was used to perform the check
* so we can immediately put it to work.
*/
_definePop3Account: function(universe, userDetails, credentials,
incomingInfo, smtpConnInfo, pop3ProtoConn,
callback) {
var accountId = $a64.encodeInt(universe.config.nextAccountNum++);
var accountDef = {
id: accountId,
name: userDetails.accountName || userDetails.emailAddress,
defaultPriority: $date.NOW(),
type: 'pop3+smtp',
receiveType: 'pop3',
sendType: 'smtp',
syncRange: 'auto',
syncInterval: userDetails.syncInterval || 0,
notifyOnNew: userDetails.hasOwnProperty('notifyOnNew') ?
userDetails.notifyOnNew : true,
playSoundOnSend: userDetails.hasOwnProperty('playSoundOnSend') ?
userDetails.playSoundOnSend : true,
credentials: credentials,
receiveConnInfo: incomingInfo,
sendConnInfo: smtpConnInfo,
identities: [
{
id: accountId + '/' +
$a64.encodeInt(universe.config.nextIdentityNum++),
name: userDetails.displayName,
address: userDetails.emailAddress,
replyTo: null,
signature: null,
signatureEnabled: false
},
],
};
this._loadAccount(universe, accountDef, null,
pop3ProtoConn, function(account) {
callback(null, account, null);
});
},
/**
* Save the account def and folder info for our new (or recreated) account and
* then load it.
*/
_loadAccount: function(universe, accountDef, oldFolderInfo, protoConn,
callback) {
var folderInfo;
if (accountDef.receiveType === 'imap') {
folderInfo = {
$meta: {
nextFolderNum: 0,
nextMutationNum: 0,
lastFolderSyncAt: 0,
capability: (oldFolderInfo && oldFolderInfo.$meta.capability) ||
protoConn.capability
},
$mutations: [],
$mutationState: {},
};
} else { // POP3
folderInfo = {
$meta: {
nextFolderNum: 0,
nextMutationNum: 0,
lastFolderSyncAt: 0,
},
$mutations: [],
$mutationState: {},
};
}
universe.saveAccountDef(accountDef, folderInfo);
universe._loadAccount(accountDef, folderInfo, protoConn, callback);
},
};
}); // end define

View file

@ -0,0 +1,376 @@
define([
'logic', '../a64', '../accountmixins', '../mailslice',
'../searchfilter', '../util', '../db/folder_info_rep', 'require', 'exports'],
function(logic, $a64, $acctmixins, $mailslice,
$searchfilter, $util, $folder_info, require, exports) {
var bsearchForInsert = $util.bsearchForInsert;
function cmpFolderPubPath(a, b) {
return a.path.localeCompare(b.path);
}
/**
* A base class for IMAP and POP accounts.
*
* A lot of the functionality related to handling folders,
* orchestrating jobs, etc., is common to both IMAP and POP accounts.
* This class factors out the common functionality, allowing the
* ImapAccount and Pop3Account classes to only provide
* protocol-specific code.
*
* @param {Class} FolderSyncer The class to instantiate for folder sync.
*
* The rest of the parameters match those passed to Pop3Account and
* ImapAccount.
*/
function CompositeIncomingAccount(
FolderSyncer,
universe, compositeAccount, accountId, credentials,
connInfo, folderInfos, dbConn, existingProtoConn) {
this.universe = universe;
this.compositeAccount = compositeAccount;
this.id = accountId;
this.accountDef = compositeAccount.accountDef;
this.enabled = true;
this._alive = true;
this._credentials = credentials;
this._connInfo = connInfo;
this._db = dbConn;
// Yes, the pluralization is suspect, but unambiguous.
/** @dictof[@key[FolderId] @value[FolderStorage] */
var folderStorages = this._folderStorages = {};
/** @dictof[@key[FolderId] @value[FolderMeta] */
var folderPubs = this.folders = [];
// This is a class, not an instance, hence the camel case.
this.FolderSyncer = FolderSyncer;
/**
* The list of dead folder id's that we need to nuke the storage for when
* we next save our account status to the database.
*/
this._deadFolderIds = null;
/**
* The canonical folderInfo object we persist to the database.
*/
this._folderInfos = folderInfos;
/**
* @dict[
* @param[nextFolderNum Number]{
* The next numeric folder number to be allocated.
* }
* @param[nextMutationNum Number]{
* The next mutation id to be allocated.
* }
* @param[lastFolderSyncAt DateMS]{
* When was the last time we ran `syncFolderList`?
* }
* @param[capability @listof[String]]{
* The post-login capabilities from the server.
* }
* @param[overflowMap @dictof[
* @key[uidl String]
* @value[@dict[
* @key[size Number]
* ]]
* ]]{
* The list of messages that will NOT be downloaded by a sync
* automatically, but instead need to be fetched with a "Download
* more messages..." operation. (POP3 only.)
* }
* @param[uidlMap @dictof[
* @key[uidl String]
* @value[headerID String]
* ]]{
* A mapping of UIDLs to message header IDs. (POP3 only.)
* }
* ]{
* Meta-information about the account derived from probing the account.
* This information gets flushed on database upgrades.
* }
*/
this.meta = this._folderInfos.$meta;
/**
* @listof[SerializedMutation]{
* The list of recently issued mutations against us. Mutations are added
* as soon as they are requested and remain until evicted based on a hard
* numeric limit. The limit is driven by our unit tests rather than our
* UI which currently only allows a maximum of 1 (high-level) undo. The
* status of whether the mutation has been run is tracked on the mutation
* but does not affect its presence or position in the list.
*
* Right now, the `MailUniverse` is in charge of this and we just are a
* convenient place to stash the data.
* }
*/
this.mutations = this._folderInfos.$mutations;
for (var folderId in folderInfos) {
if (folderId[0] === '$')
continue;
var folderInfo = folderInfos[folderId];
folderStorages[folderId] =
new $mailslice.FolderStorage(this, folderId, folderInfo, this._db,
FolderSyncer);
folderPubs.push(folderInfo.$meta);
}
this.folders.sort(function(a, b) {
return a.path.localeCompare(b.path);
});
// Ensure we have an inbox. This is a folder that must exist with a standard
// name, so we can create it without talking to the server.
var inboxFolder = this.getFirstFolderWithType('inbox');
if (!inboxFolder) {
this._learnAboutFolder('INBOX', 'INBOX', null, 'inbox', '/', 0, true);
}
}
exports.CompositeIncomingAccount = CompositeIncomingAccount;
CompositeIncomingAccount.prototype = {
////////////////////////////////////////////////////////////////
// ACCOUNT OVERRIDES
runOp: $acctmixins.runOp,
getFirstFolderWithType: $acctmixins.getFirstFolderWithType,
getFolderByPath: $acctmixins.getFolderByPath,
saveAccountState: $acctmixins.saveAccountState,
runAfterSaves: $acctmixins.runAfterSaves,
/**
* Make a given folder known to us, creating state tracking instances, etc.
*
* @param {Boolean} suppressNotification
* Don't report this folder to subscribed slices. This is used in cases
* where the account has not been made visible to the front-end yet and/or
* syncFolderList hasn't yet run, but something subscribed to the "all
* accounts" unified folder slice could end up seeing something before it
* should. This is a ret-con'ed comment, so maybe do some auditing before
* adding new call-sites that use this, especially if it's not used for
* offline-only folders at account creation/app startup.
*/
_learnAboutFolder: function(name, path, parentId, type, delim, depth,
suppressNotification) {
var folderId = this.id + '/' + $a64.encodeInt(this.meta.nextFolderNum++);
var folderInfo = this._folderInfos[folderId] = {
$meta: $folder_info.makeFolderMeta({
id: folderId,
name: name,
type: type,
path: path,
parentId: parentId,
delim: delim,
depth: depth,
lastSyncedAt: 0,
version: $mailslice.FOLDER_DB_VERSION
}),
$impl: {
nextId: 0,
nextHeaderBlock: 0,
nextBodyBlock: 0,
},
accuracy: [],
headerBlocks: [],
bodyBlocks: [],
serverIdHeaderBlockMapping: null, // IMAP/POP3 does not need the mapping
};
this._folderStorages[folderId] =
new $mailslice.FolderStorage(this, folderId, folderInfo, this._db,
this.FolderSyncer);
var folderMeta = folderInfo.$meta;
var idx = bsearchForInsert(this.folders, folderMeta, cmpFolderPubPath);
this.folders.splice(idx, 0, folderMeta);
if (!suppressNotification)
this.universe.__notifyAddedFolder(this, folderMeta);
return folderMeta;
},
_forgetFolder: function(folderId, suppressNotification) {
var folderInfo = this._folderInfos[folderId],
folderMeta = folderInfo.$meta;
delete this._folderInfos[folderId];
var folderStorage = this._folderStorages[folderId];
delete this._folderStorages[folderId];
var idx = this.folders.indexOf(folderMeta);
this.folders.splice(idx, 1);
if (this._deadFolderIds === null)
this._deadFolderIds = [];
this._deadFolderIds.push(folderId);
folderStorage.youAreDeadCleanupAfterYourself();
if (!suppressNotification)
this.universe.__notifyRemovedFolder(this, folderMeta);
},
/**
* 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) {
logic(this, 'recreateFolder', { folderId: folderId });
var folderInfo = this._folderInfos[folderId];
folderInfo.$impl = {
nextId: 0,
nextHeaderBlock: 0,
nextBodyBlock: 0,
};
folderInfo.accuracy = [];
folderInfo.headerBlocks = [];
folderInfo.bodyBlocks = [];
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,
self.FolderSyncer);
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.
*/
__checkpointSyncCompleted: function(callback, betterReason) {
this.saveAccountState(null, callback, betterReason || 'checkpointSync');
},
/**
* Delete an existing folder WITHOUT ANY ABILITY TO UNDO IT. Current
* UX does not desire this, but the unit tests do.
*
* XXX: This is not quite right for POP3; address when we expose
* deleting folders to the UI when we need to create jobs too.
*
* Callback is like the createFolder one, why not.
*/
deleteFolder: function(folderId, callback) {
if (!this._folderInfos.hasOwnProperty(folderId))
throw new Error("No such folder: " + folderId);
if (!this.universe.online) {
if (callback)
callback('offline');
return;
}
var folderMeta = this._folderInfos[folderId].$meta;
var rawConn = null, self = this;
function gotConn(conn) {
rawConn = conn;
rawConn.delBox(folderMeta.path, deletionCallback);
}
function deletionCallback(err) {
if (err)
done('unknown');
else
done(null);
}
function done(errString) {
if (rawConn) {
self.__folderDoneWithConnection(rawConn, false, false);
rawConn = null;
}
if (!errString) {
logic(self, 'deleteFolder', { path: folderMeta.path });
self._forgetFolder(folderId);
}
if (callback)
callback(errString, folderMeta);
}
this.__folderDemandsConnection(null, 'deleteFolder', gotConn);
},
getFolderStorageForFolderId: function(folderId) {
if (this._folderStorages.hasOwnProperty(folderId))
return this._folderStorages[folderId];
throw new Error('No folder with id: ' + folderId);
},
getFolderStorageForMessageSuid: function(messageSuid) {
var folderId = messageSuid.substring(0, messageSuid.lastIndexOf('/'));
if (this._folderStorages.hasOwnProperty(folderId))
return this._folderStorages[folderId];
throw new Error('No folder with id: ' + folderId);
},
getFolderMetaForFolderId: function(folderId) {
if (this._folderInfos.hasOwnProperty(folderId))
return this._folderInfos[folderId].$meta;
return null;
},
/**
* Create a view slice on the messages in a folder, starting from the most
* recent messages and synchronizing further as needed.
*/
sliceFolderMessages: function(folderId, bridgeHandle) {
var storage = this._folderStorages[folderId],
slice = new $mailslice.MailSlice(bridgeHandle, storage);
storage.sliceOpenMostRecent(slice);
},
searchFolderMessages: function(folderId, bridgeHandle, phrase, whatToSearch) {
var storage = this._folderStorages[folderId],
slice = new $searchfilter.SearchSlice(bridgeHandle, storage, phrase,
whatToSearch);
storage.sliceOpenSearch(slice);
return slice;
},
shutdownFolders: function() {
// - kill all folder storages (for their loggers)
for (var iFolder = 0; iFolder < this.folders.length; iFolder++) {
var folderPub = this.folders[iFolder],
folderStorage = this._folderStorages[folderPub.id];
folderStorage.shutdown();
}
},
scheduleMessagePurge: function(folderId, callback) {
this.universe.purgeExcessMessages(this.compositeAccount, folderId,
callback);
},
/**
* We receive this notification from our _backoffEndpoint.
*/
onEndpointStateChange: function(state) {
switch (state) {
case 'healthy':
this.universe.__removeAccountProblem(this.compositeAccount,
'connection', 'incoming');
break;
case 'unreachable':
case 'broken':
this.universe.__reportAccountProblem(this.compositeAccount,
'connection', 'incoming');
break;
}
},
};
}); // end define

View file

@ -0,0 +1,419 @@
/*global define, console */
/**
* Drives periodic synchronization, covering the scheduling, deciding what
* folders to sync, and generating notifications to relay to the UI. More
* specifically, we have two goals:
*
* 1) Generate notifications about new messages.
*
* 2) Cause the device to synchronize its offline store periodically with the
* server for general responsiveness and so the user can use the device
* offline.
*
* We use mozAlarm to schedule ourselves to wake up when our next
* synchronization should occur.
*
* All synchronization occurs in parallel because we want the interval that we
* force the device's radio into higher power modes to be as short as possible.
*
* This logic is part of the back-end, not the front-end. We want to notify
* the front-end of new messages, but we want the front-end to be the one that
* displays and services them to the user.
**/
define(function(require, exports) {
'use strict';
var logic = require('logic'),
router = require('./worker-router'),
mailslice = require('./mailslice'),
syncbase = require('./syncbase'),
allback = require( './allback');
function debug(str) {
console.log('cronsync: ' + str + '\n');
}
var SliceBridgeProxy = require('./slice_bridge_proxy').SliceBridgeProxy;
/**
* Create a specialized sync slice via clobbering that accumulates a list of
* new headers and invokes a callback when the sync has fully completed.
*
* Fully completed includes:
* - The account update has been fully saved to disk.
*
* New header semantics are:
* - Header is as new or newer than the newest header we previously knew about.
* Specifically we're using SINCE which is >=, so a message that arrived at
* the same second as the other message still counts as new. Because the new
* message will inherently have a higher id than the other message, this
* meets with our other ordering semantics, although I'm thinking it wasn't
* totally intentional.
* - Header is unread. (AKA Not \Seen)
*
* "Clobbering" in this case means this is a little hacky. What we do is:
* - Take a normal slice and hook it up to a normal SliceBridgeProxy, but
* give the proxy a fake bridge that never sends any data anywhere. This
* is reasonably future-proof/safe.
*
* - Put an onNewHeader method on the slice to accumulate the new headers.
* The new headers are all we care about. The rest of the headers loaded/etc.
* are boring to us and do not matter. However, there's relatively little
* memory or CPU overhead to letting that stuff get populated/retained since
* it's in memory already anyways and at worst we're only delaying the GC by
* a little bit. (This is not to say there aren't pathological situations
* possible, but they'd be largely the same if the user triggered the sync.
* The main difference cronsync currently will definitely not shrink the
* slice.
*
* - Clobber proy.sendStatus to know when the sync has completed via the
* same signal the front-end uses to know when the sync is over.
*
* You as the caller need to:
* - Make sure to kill the slice in a timely fashion after we invoke the
* callback. Since killing the slice can result in the connection immediately
* being closed, you want to make sure that if you're doing anything like
* scheduling snippet downloads that you do that first.
*/
function makeHackedUpSlice(storage, callback) {
var fakeBridgeThatEatsStuff = {
__sendMessage: function() {}
},
proxy = new SliceBridgeProxy(fakeBridgeThatEatsStuff, 'cron'),
slice = new mailslice.MailSlice(proxy, storage),
oldStatusMethod = proxy.sendStatus,
newHeaders = [];
slice.onNewHeader = function(header) {
console.log('onNewHeader: ' + header);
newHeaders.push(header);
};
proxy.sendStatus = function(status, requested, moreExpected,
progress, newEmailCount) {
// (maintain normal behaviour)
oldStatusMethod.apply(this, arguments);
// We do not want to declare victory until the sync process has fully
// completed which (significantly!) includes waiting for the save to have
// completed.
// (Only fire completion once.)
if (callback) {
switch (status) {
// normal success and failure
case 'synced':
case 'syncfailed':
// ActiveSync specific edge-case where syncFolderList has not yet
// completed. If the slice is still alive when syncFolderList completes
// the slice will auto-refresh itself. We don't want or need this,
// which is fine since we kill the slice in the callback.
case 'syncblocked':
try {
callback(newHeaders);
}
catch (ex) {
console.error('cronsync callback error:', ex, '\n', ex.stack);
callback = null;
throw ex;
}
callback = null;
break;
}
}
};
return slice;
}
/**
* The brains behind periodic account synchronization; only created by the
* universe once it has loaded its configuration and accounts.
*/
function CronSync(universe) {
this._universe = universe;
logic.defineScope(this, 'CronSync');
this._ensureSyncResolve = null;
this.sendCronSync = router.registerSimple('cronsync', (data) => {
var args = data.args;
switch (data.cmd) {
case 'alarm':
debug('received an alarm via a message handler');
this.onAlarm.apply(this, args);
break;
case 'syncEnsured':
debug('received an syncEnsured via a message handler');
this.onSyncEnsured.apply(this, args);
break;
}
});
this.sendCronSync('hello');
this.ensureSync();
}
exports.CronSync = CronSync;
CronSync.prototype = {
/**
* Makes sure there is a sync timer set up for all accounts.
*/
ensureSync: function() {
// Only execute ensureSync if it is not already in progress. Otherwise, due
// to async timing of mozAlarm setting, could end up with two sync tasks for
// the same ID.
if (this._ensureSyncResolve) {
return;
}
logic(this, 'ensureSync_begin');
this._ensureSyncPromise = new Promise((resolve) => {
// No error pathway for the bridge hop, so just tracking resolve.
this._ensureSyncResolve = resolve;
});
debug('ensureSync called');
var accounts = this._universe.accounts,
syncData = {};
accounts.forEach(function(account) {
// Store data by interval, use a more obvious string key instead of just
// stringifying a number, which could be confused with an array construct.
var interval = account.accountDef.syncInterval,
intervalKey = 'interval' + interval;
if (!syncData.hasOwnProperty(intervalKey)) {
syncData[intervalKey] = [];
}
syncData[intervalKey].push(account.id);
});
this.sendCronSync('ensureSync', [syncData]);
},
/**
* Called from cronsync-main once ensureSync as set any alarms needed. Need to
* wait for it before signaling sync is done because otherwise the app could
* get closed down before the alarm additions succeed.
*/
onSyncEnsured: function() {
this._ensureSyncResolve();
this._ensureSyncResolve = null;
logic(this, 'ensureSync_end');
},
/**
* Synchronize the given account. This fetches new messages for the inbox, and
* attempts to send pending outbox messages (if applicable). The callback
* occurs after both of those operations have completed.
*/
syncAccount: function(account) {
return new Promise((resolve) => {
var scope = logic.subscope(this, { accountId: account.id });
// - Skip syncing if we are offline or the account is disabled
if (!this._universe.online || !account.enabled) {
debug('syncAccount early exit: online: ' +
this._universe.online + ', enabled: ' + account.enabled);
logic(scope, 'syncSkipped');
resolve();
return;
}
var latch = allback.latch();
var inboxDone = latch.defer('inbox');
var inboxFolder = account.getFirstFolderWithType('inbox');
var storage = account.getFolderStorageForFolderId(inboxFolder.id);
// XXX check when the folder was most recently synchronized and skip this
// sync if it is sufficiently recent.
// - Initiate a sync of the folder covering the desired time range.
logic(scope, 'syncAccount_begin');
logic(scope, 'syncAccountHeaders_begin');
var slice = makeHackedUpSlice(storage, (newHeaders) => {
logic(scope, 'syncAccountHeaders_end', { headers: newHeaders });
// Reduce headers to the minimum number and data set needed for
// notifications.
var notifyHeaders = [];
newHeaders.some(function(header, i) {
notifyHeaders.push({
date: header.date,
from: header.author.name || header.author.address,
subject: header.subject,
accountId: account.id,
messageSuid: header.suid
});
if (i === syncbase.CRONSYNC_MAX_MESSAGES_TO_REPORT_PER_ACCOUNT - 1) {
return true;
}
});
if (newHeaders.length) {
debug('Asking for snippets for ' + notifyHeaders.length + ' headers');
// POP3 downloads snippets as part of the sync process, there is no
// need to call downloadBodies.
if (account.accountDef.type === 'pop3+smtp') {
logic(scope, 'syncAccount_end');
inboxDone([newHeaders.length, notifyHeaders]);
} else if (this._universe.online) {
logic(scope, 'syncAccountSnippets_begin');
this._universe.downloadBodies(
newHeaders.slice(
0, syncbase.CRONSYNC_MAX_SNIPPETS_TO_FETCH_PER_ACCOUNT),
{
maximumBytesToFetch: syncbase.MAX_SNIPPET_BYTES
},
() => {
debug('Notifying for ' + newHeaders.length + ' headers');
logic(scope, 'syncAccountSnippets_end');
logic(scope, 'syncAccount_end');
inboxDone([newHeaders.length, notifyHeaders]);
});
} else {
logic(scope, 'syncAccount_end');
debug('UNIVERSE OFFLINE. Notifying for ' + newHeaders.length +
' headers');
inboxDone([newHeaders.length, notifyHeaders]);
}
} else {
logic(scope, 'syncAccount_end');
inboxDone();
}
// Kill the slice. This will release the connection and result in its
// death if we didn't schedule snippet downloads above.
slice.die();
});
// Pass true to force contacting the server.
storage.sliceOpenMostRecent(slice, true);
// Check the outbox; if it has pending messages, attempt to send them.
var outboxFolder = account.getFirstFolderWithType('outbox');
if (outboxFolder) {
var outboxStorage = account
.getFolderStorageForFolderId(outboxFolder.id);
if (outboxStorage.getKnownMessageCount() > 0) {
var outboxDone = latch.defer('outbox');
logic(scope, 'sendOutbox_begin');
this._universe.sendOutboxMessages(
account,
{
reason: 'syncAccount'
},
() => {
logic(scope, 'sendOutbox_end');
outboxDone();
});
}
}
// After both inbox and outbox syncing are algorithmically done, wait for
// any ongoing job operations to complete so that the app is not killed in
// the middle of a sync.
latch.then((latchResults) => {
// Right now, we ignore the outbox sync's results; we only care about
// the inbox.
var inboxResult = latchResults.inbox[0];
this._universe.waitForAccountOps(account, function() {
// Also wait for any account save to finish. Most likely failure will
// be new message headers not getting saved if the callback is not
// fired until after account saves.
account.runAfterSaves(function() {
resolve(inboxResult);
});
});
});
});
},
onAlarm: function(accountIds) {
logic(this, 'alarmFired', { accountIds: accountIds });
if (!accountIds) {
return;
}
var accounts = this._universe.accounts,
targetAccounts = [],
ids = [];
logic(this, 'cronSync_begin');
this._universe.__notifyStartedCronSync(accountIds);
// Make sure the acount IDs are still valid. This is to protect against an
// account deletion that did not clean up any alarms correctly.
accountIds.forEach(function(id) {
accounts.some(function(account) {
if (account.id === id) {
targetAccounts.push(account);
ids.push(id);
return true;
}
});
});
// Make sure next alarm is set up. In the case of a cold start background
// sync, this is a bit redundant in that the startup of the mailuniverse
// would trigger this work. However, if the app is already running, need to
// be sure next alarm is set up, so ensure the next sync is set up here. Do
// it here instead of after a sync in case an error in sync would prevent
// the next sync from getting scheduled.
this.ensureSync();
var syncResults = [];
var accountsResults = {
accountIds: accountIds
};
var done = () => {
// Make sure the ensure work is done before wrapping up.
this._ensureSyncPromise.then(() => {
if (syncResults.length) {
accountsResults.updates = syncResults;
}
this._universe.__notifyStoppedCronSync(accountsResults);
logic(this, 'syncAccounts_end', { accountsResults: accountsResults });
logic(this, 'cronSync_end');
});
};
// Nothing new to sync, probably old accounts. Just return and indicate that
// syncing is done.
if (!ids.length) {
done();
return;
}
logic(this, 'syncAccounts_begin');
Promise.all(targetAccounts.map((account) => {
return this.syncAccount(account).then((result) => {
if (result) {
syncResults.push({
id: account.id,
address: account.identities[0].address,
count: result[0],
latestMessageInfos: result[1]
});
}
});
}))
.then(done);
},
shutdown: function() {
router.unregister('cronsync');
}
};
}); // end define

240
gaia/apps/email/js/ext/date.js Executable file
View file

@ -0,0 +1,240 @@
define(
[
'module',
'exports'
],
function(
$module,
exports
) {
////////////////////////////////////////////////////////////////////////////////
// Time
//
// == JS Dates
//
// We primarily deal in UTC timestamps. When we need to talk dates with IMAP
// (see next section), we need these timestamps to line up with midnight for
// a given day. We do not need to line up with weeks, months, or years,
// saving us a lot of complexity.
//
// Day algebra is straightforward because JS Date objects have no concept of
// leap seconds. We don't need to worry that a leap second will cause adding
// a day to be less than or more than a day. Hooray!
//
// == IMAP and Time
//
// The stock IMAP SEARCH command's SINCE and BEFORE predicates only operate on
// whole-dates (and ignore the non-date time parts). Additionally, SINCE is
// inclusive and BEFORE is exclusive.
//
// We use JS millisecond timestamp values throughout, and it's important to us
// that our date logic is consistent with IMAP's time logic where relevant.
// All of our IMAP-exposed time-interval related logic operates on day
// granularities. Our timestamp/date values are always normalized to midnight
// which happily works out with intuitive range operations.
//
// Observe the pretty ASCII art where as you move to the right you are moving
// forward in time.
//
// ________________________________________
// BEFORE)| midnight (0 millis) ... 11:59:59:999 |
// ON_OR_BEFORE]
// [SINCE......................................
// (AFTER.....................................
//
// Our date range comparisons (noting that larger timestamps are 'younger') are:
// SINCE analog: (testDate >= comparisonDate)
// testDate is as-recent-as or more-recent-than the comparisonDate.
// BEFORE analog: (testDate < comparisonDate)
// testDate is less-recent-than the comparisonDate
//
// Because "who is the test date and who is the range value under discussion"
// can be unclear and the numerical direction of time is not always intuitive,
// I'm introducing simple BEFORE and SINCE helper functions to try and make
// the comparison logic ridiculously explicit as well as calling out where we
// are being consistent with IMAP.
//
// Not all of our time logic is consistent with IMAP! Specifically, use of
// exclusive time bounds without secondary comparison keys means that ranges
// defined in this way cannot spread messages with the same timestamp over
// multiple ranges. This allows for pathological data structure situations
// where there's too much data in a data block, etc.
// Our date ranges are defined by 'startTS' and 'endTS'. Using math syntax, our
// IMAP-consistent time ranges end up as: [startTS, endTS). It is always true
// that BEFORE(startTS, endTS) and SINCE(endTS, startTS) in these cases.
//
// As such, I've also created an ON_OR_BEFORE helper that allows equivalence and
// STRICTLY_AFTER that does not check equivalence to round out all possibilities
// while still being rather explicit.
/**
* IMAP-consistent date comparison; read this as "Is `testDate` BEFORE
* `comparisonDate`"?
*
* !BEFORE(a, b) === SINCE(a, b)
*/
var BEFORE = exports.BEFORE =
function BEFORE(testDate, comparisonDate) {
// testDate is numerically less than comparisonDate, so it is chronologically
// before it.
return testDate < comparisonDate;
};
var ON_OR_BEFORE = exports.ON_OR_BEFORE =
function ON_OR_BEFORE(testDate, comparisonDate) {
return testDate <= comparisonDate;
};
/**
* IMAP-consistent date comparison; read this as "Is `testDate` SINCE
* `comparisonDate`"?
*
* !SINCE(a, b) === BEFORE(a, b)
*/
var SINCE = exports.SINCE =
function SINCE(testDate, comparisonDate) {
// testDate is numerically greater-than-or-equal-to comparisonDate, so it
// chronologically after/since it.
return testDate >= comparisonDate;
};
var STRICTLY_AFTER = exports.STRICTLY_AFTER =
function STRICTLY_AFTER(testDate, comparisonDate) {
return testDate > comparisonDate;
};
var IN_BS_DATE_RANGE = exports.IN_BS_DATE_RANGE =
function IN_BS_DATE_RANGE(testDate, startTS, endTS) {
return testDate >= startTS && testDate < endTS;
};
var PASTWARDS = 1, FUTUREWARDS = -1;
/**
* Check if `testDate` is "beyond" the comparison date given the `dir`. If
* the direction is pastwards, we will return true if testDate happened
* chronologically before comparisonDate. If the direction is futurewards,
* we will return true if testDate happened chronologically after
* comparisonDate.
*/
var TIME_DIR_AT_OR_BEYOND = exports.TIME_DIR_AT_OR_BEYOND =
function TIME_DIR_AT_OR_BEYOND(dir, testDate, comparisonDate) {
if (dir === PASTWARDS)
return testDate <= comparisonDate;
// we use null as a sentinel value for 'the future'/'now'
else if (comparisonDate === null)
return testDate >= NOW();
else // FUTUREWARDS
return testDate >= comparisonDate;
};
/**
* Compute the delta of the `testDate` relative to the `comparisonDate` where
* a positive value indicates `testDate` is beyond the `comparisonDate` in
* the given direction and a negative value indicates it is before it.
*/
var TIME_DIR_DELTA = exports.TIME_DIR_DELTA =
function TIME_DIR_DELTA(dir, testDate, comparisonDate) {
if (dir === PASTWARDS)
return testDate - comparisonDate;
else // FUTUREWARDS
return comparisonDate - testDate;
};
/**
* Add `time` to the `baseDate` in the given direction. So if the direction
* is `PASTWARDS`, then we add the date, otherwise we subtract it.
*/
var TIME_DIR_ADD = exports.TIME_DIR_ADD =
function TIME_DIR_ADD(dir, baseDate, time) {
if (dir === PASTWARDS)
return baseDate + time;
else // FUTUREWARDS
return baseDate - time;
};
//function DATE_RANGES_OVERLAP(A_startTS, A_endTS, B_startTS, B_endTS) {
//}
var HOUR_MILLIS = exports.HOUR_MILLIS = 60 * 60 * 1000;
var DAY_MILLIS = exports.DAY_MILLIS = 24 * 60 * 60 * 1000;
/**
* Testing override that when present replaces use of Date.now().
*/
var TIME_WARPED_NOW = null;
/**
* Pretend that 'now' is actually a fixed point in time for the benefit of
* unit tests using canned message stores.
*/
exports.TEST_LetsDoTheTimewarpAgain = function(fakeNow) {
if (fakeNow === null) {
TIME_WARPED_NOW = null;
return;
}
if (typeof(fakeNow) !== 'number')
fakeNow = fakeNow.valueOf();
TIME_WARPED_NOW = fakeNow;
};
var NOW = exports.NOW =
function NOW() {
return TIME_WARPED_NOW || Date.now();
};
/**
* Like NOW, but uses performance.now instead. This means it can be reset or
* changed to a value different than the last window or worker lifetime value.
* Allows TIME_WARP overrides and falls back to Date.now. So while all the
* values are in milliseconds, if performance.now() is used, can have a decimal
* value indicating up to one thousandth of a millisecond.
*/
var perfObj = typeof performance !== 'undefined' ? performance : Date;
exports.PERFNOW = function PERFNOW() {
return TIME_WARPED_NOW || perfObj.now();
};
/**
* Make a timestamp some number of days in the past, quantized to midnight of
* that day. This results in rounding up; if it's noon right now and you
* ask for 2 days ago, you really get 2.5 days worth of time.
*/
var makeDaysAgo = exports.makeDaysAgo =
function makeDaysAgo(numDays) {
var past = quantizeDate(TIME_WARPED_NOW || Date.now()) -
numDays * DAY_MILLIS;
return past;
};
var makeDaysBefore = exports.makeDaysBefore =
function makeDaysBefore(date, numDaysBefore) {
if (date === null)
return makeDaysAgo(numDaysBefore - 1);
return quantizeDate(date) - numDaysBefore * DAY_MILLIS;
};
/**
* Quantize a date to midnight on that day.
*/
var quantizeDate = exports.quantizeDate =
function quantizeDate(date) {
if (date === null)
return null;
if (typeof(date) === 'number')
date = new Date(date);
return date.setUTCHours(0, 0, 0, 0).valueOf();
};
/**
* If a date is already lined up with midnight of its day, then return that,
* otherwise round up to the midnight of the next day.
*/
var quantizeDateUp = exports.quantizeDateUp =
function quantizeDateUp(date) {
if (typeof(date) === 'number')
date = new Date(date);
var truncated = date.setUTCHours(0, 0, 0, 0).valueOf();
if (date.valueOf() === truncated)
return truncated;
return truncated + DAY_MILLIS;
};
}); // end define

View file

@ -0,0 +1,64 @@
define(function(require) {
/**
*
* @typedef {Object} FolderMeta
*
* @property {string} id - ID assigned to the folder by the backend.
*
* @property {string} serverId - Optional. For ActiveSync folders, the
* server-issued id for the folder that we use to reference the folder.
*
* @property {string} name - The human-readable name of the folder with all utf-7
* decoding/etc performed. This is intended to be shown to the user,
* the path should not be. Folder names should be considered
* private/personal data and if logged should be marked to be
* sanitized unless the user has explicitly enabled super-verbose
* privacy-entraining logs.
*
* @property {string} type - The type of the folder, i.e. 'inbox' or 'drafts'.
* Refer to mailapi.js for a list of acceptable values.
*
* @property {string} path - The fully qualified path of the folder.
* For IMAP servers, this is the raw path including utf-7 encoded parts.
* For ActiveSync and POP3 this is just for super-verbose private-data-entraining
* debugging and testing.
* This should be considered private/personal data like the folder name.
*
* @property {number} depth - The depth of the folder in the folder tree.
* This is useful since the folders are stored as a flattened list, so
* attempts to display the folder hierarchy would otherwise have to compute
* this themsevles.
*
* @property {DateMS} lastSyncedAt - The last time the folder was synced.
*
* @property {number} unreadCount - The total number of locally stored unread
* messages in the folder.
*
* @property {string} syncKey - ActiveSync-only per-folder synchronization key.
*/
function makeFolderMeta(raw) {
return {
id: raw.id || null,
serverId: raw.serverId || null,
name: raw.name || null,
type: raw.type || null,
path: raw.path || null,
parentId: raw.parentId || null,
depth: raw.depth || 0,
lastSyncedAt: raw.lastSyncedAt || 0,
unreadCount: raw.unreadCount || 0,
syncKey: raw.syncKey || null,
version: raw.version || null
}
};
return {
makeFolderMeta: makeFolderMeta
}
}); // end define

View file

@ -0,0 +1,283 @@
/**
* Centralize the creation of our header and body object representations.
*
* We provide constructor functions which take input objects that should
* basically look like the output object, but the function enforces
* consistency and provides the ability to assert about the state of the
* representation at the call-site. We discussed making sure to check
* representations when we are inserting records into our database, but we
* might also want to opt to do it at creation time too so we can explode
* slightly closer to the source of the problem.
*
* This module will also provide representation checking functions to make
* sure all the data structures are well-formed/have no obvious problems.
*
* @module mailapi/db/mail_rep
**/
define(function() {
/*
* @typedef[HeaderInfo @dict[
* @key[id]{
* An id allocated by the back-end that names the message within the folder.
* We use this instead of the server-issued UID because if we used the UID
* for this purpose then we would still need to issue our own temporary
* speculative id's for offline operations and would need to implement
* renaming and it all gets complicated.
* }
* @key[srvid]{
* The server-issued UID for the folder, or 0 if the folder is an offline
* header.
* }
* @key[suid]{
* Basically "account id/folder id/message id", although technically the
* folder id includes the account id.
* }
* @key[guid String]{
* This is the message-id header value of the message.
* }
* @key[author NameAddressPair]
* @key[to #:optional @listof[NameAddressPair]]
* @key[cc #:optional @listof[NameAddressPair]]
* @key[bcc #:optional @listof[NameAddressPair]]
* @key[replyTo #:optional String]{
* The contents of the reply-to header.
* }
* @key[date DateMS]
* @key[flags @listof[String]]
* @key[hasAttachments Boolean]
* @key[subject @oneof [String null]]
* @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.
* }
* ]]
* ]]
*/
function makeHeaderInfo(raw) {
// All messages absolutely need the following; the caller needs to make up
// values if they're missing.
if (!raw.author)
throw new Error('No author?!');
if (!raw.date)
throw new Error('No date?!');
// We also want/require a valid id, but we check that at persistence time
// since POP3 assigns the id/suid slightly later on. We check the suid at
// that point too. (Checked in FolderStorage.addMessageHeader.)
return {
id: raw.id,
srvid: raw.srvid || null,
suid: raw.suid || null,
guid: raw.guid || null,
author: raw.author,
to: raw.to || null,
cc: raw.cc || null,
bcc: raw.bcc || null,
replyTo: raw.replyTo || null,
date: raw.date,
flags: raw.flags || [],
hasAttachments: raw.hasAttachments || false,
// These can be empty strings which are falsey, so no ||
subject: (raw.subject != null) ? raw.subject : null,
snippet: (raw.snippet != null) ? raw.snippet : null,
imapMissingInSyncRange: raw.imapMissingInSyncRange || null
};
}
/*
* @typedef[BodyInfo @dict[
* @key[date DateMS]{
* Redundantly stored date info for block splitting purposes. We pretty
* much need this no matter what because our ordering is on the tuples of
* dates and UIDs, so we could have trouble efficiently locating our header
* from the body without this.
* }
* @key[size Number]
* @key[attaching #:optional AttachmentInfo]{
* Because of memory limitations, we need to encode and attach attachments
* in small pieces. An attachment in the process of being attached is
* stored here until fully processed. Its 'file' field contains a list of
* Blobs.
* }
* @key[attachments @listof[AttachmentInfo]]{
* Proper attachments for explicit downloading.
* }
* @key[relatedParts @oneof[null @listof[AttachmentInfo]]]{
* Attachments for inline display in the contents of the (hopefully)
* multipart/related message.
* }
* @key[references @oneof[null @listof[String]]]{
* The contents of the references header as a list of de-quoted ('<' and
* '>' removed) message-id's. If there was no header, this is null.
* }
* @key[bodyReps @listof[BodyPartInfo]]
* ]]{
* Information on the message body that is only for full message display.
* The to/cc/bcc information may get moved up to the header in the future,
* but our driving UI doesn't need it right now.
* }
*/
function makeBodyInfo(raw) {
if (!raw.date)
throw new Error('No date?!');
if (!raw.attachments || !raw.bodyReps)
throw new Error('No attachments / bodyReps?!');
return {
date: raw.date,
size: raw.size || 0,
attachments: raw.attachments,
relatedParts: raw.relatedParts || null,
references: raw.references || null,
bodyReps: raw.bodyReps
};
}
/*
* @typedef[BodyPartInfo @dict[
* @key[type @oneof['plain' 'html']]{
* The type of body; this is actually the MIME sub-type.
* }
* @key[part String]{
* IMAP part number.
* }
* @key[sizeEstimate Number]
* @key[amountDownloaded Number]
* @key[isDownloaded Boolean]
* @key[_partInfo #:optional RawIMAPPartInfo]
* @key[content]{
* The representation for 'plain' values is a `quotechew.js` processed
* body representation which is pair-wise list where the first item in each
* pair is a packed integer identifier and the second is a string containing
* the text for that block.
*
* The body representation for 'html' values is an already sanitized and
* already quote-normalized String representation that could be directly
* fed into innerHTML safely if you were so inclined. See `htmlchew.js`
* for more on that process.
* }
* ]]
*/
function makeBodyPart(raw) {
// We don't persist body types to our representation that we don't understand.
if (raw.type !== 'plain' &&
raw.type !== 'html')
throw new Error('Bad body type: ' + raw.type);
// 0 is an okay body size, but not giving us a guess is not!
if (raw.sizeEstimate === undefined)
throw new Error('Need size estimate!');
return {
type: raw.type,
part: raw.part || null,
sizeEstimate: raw.sizeEstimate,
amountDownloaded: raw.amountDownloaded || 0,
isDownloaded: raw.isDownloaded || false,
_partInfo: raw._partInfo || null,
content: raw.content || ''
};
}
/*
* @typedef[AttachmentInfo @dict[
* @key[name String]{
* The filename of the attachment, if any.
* }
* @key[contentId String]{
* The content-id of the attachment if this is a related part for inline
* display.
* }
* @key[type String]{
* The (full) mime-type of the attachment.
* }
* @key[part String]{
* The IMAP part number for fetching the attachment.
* }
* @key[encoding String]{
* The encoding of the attachment so we know how to decode it. For
* ActiveSync, the server takes care of this for us so there is no encoding
* from our perspective. (Although the attachment may get base64 encoded
* for transport in the inline case, but that's a protocol thing and has
* nothing to do with the message itself.)
* }
* @key[sizeEstimate Number]{
* Estimated file size in bytes. Gets updated to be the correct size on
* attachment download.
* }
* @key[file @oneof[
* @case[null]{
* The attachment has not been downloaded, the file size is an estimate.
* }
* @case[@list["device storage type" "file path"]{
* The DeviceStorage type (ex: pictures) and the path to the file within
* device storage.
* }
* @case[HTMLBlob]{
* The Blob that contains the attachment. It can be thought of as a
* handle/name to access the attachment. IndexedDB in Gecko stores the
* blobs as (quota-tracked) files on the file-system rather than inline
* with the record, so the attachments don't need to count against our
* block size since they are not part of the direct I/O burden for the
* block.
* }
* @case[@listof[HTMLBlob]]{
* For draft messages, a list of one or more pre-base64-encoded attachment
* pieces that were sliced up in chunks due to Gecko's inability to stream
* Blobs to disk off the main thread.
* }
* ]]
* @key[charset @oneof[undefined String]]{
* The character set, for example "ISO-8859-1". If not specified, as is
* likely for binary attachments, this should be null.
* }
* @key[textFormat @oneof[undefined String]]{
* The text format, for example, "flowed" for format=flowed. If not
* specified, as is likely for binary attachments, this should be null.
* }
* ]]
*/
function makeAttachmentPart(raw) {
// Something is very wrong if there is no size estimate.
if (raw.sizeEstimate === undefined)
throw new Error('Need size estimate!');
return {
// XXX ActiveSync may leave this null, although it's conceivable the
// server might do normalization to save us. This needs a better treatment.
// IMAP generates a made-up name for us if there isn't one.
name: (raw.name != null) ? raw.name : null,
contentId: raw.contentId || null,
type: raw.type || 'application/octet-stream',
part: raw.part || null,
encoding: raw.encoding || null,
sizeEstimate: raw.sizeEstimate,
file: raw.file || null,
charset: raw.charset || null,
textFormat: raw.textFormat || null
};
}
return {
makeHeaderInfo: makeHeaderInfo,
makeBodyInfo: makeBodyInfo,
makeBodyPart: makeBodyPart,
makeAttachmentPart: makeAttachmentPart
};
}); // end define

View file

@ -0,0 +1,122 @@
define(function(require) {
/**
* DisasterRecovery: A universal error-handling helper. When
* unexpected and unhandled exceptions occur in the world, do the
* best we can to recover as safely as possible from exceptions
* thrown in strange places. This entails finishing any job ops,
* closing any socket (if applicable) to ensure it doesn't get left
* in a weird state, and logging an error as best we can.
*
* Besides connections, the only resources job-ops can acquire are
* folder mutexes. Currently, these are tracked
*
* In order to do this, we must track the following:
*
* - Which account a socket is attached to (in case it throws)
* - Whether or not an account is running a job operation
*
* Then, the error handling is just a process of cleaning up any
* resources we can.
*/
var logic = require('./logic');
var socketToAccountMap = new WeakMap();
var accountToOperationMap = new WeakMap();
var scope = logic.scope('DisasterRecovery');
var DisasterRecovery = {
setCurrentAccountOp: function(account, op, jobCompletedCallback) {
accountToOperationMap.set(account, {
op: op,
callback: jobCompletedCallback
});
},
clearCurrentAccountOp: function(account) {
accountToOperationMap.delete(account);
},
// Track which account maps to each socket.
associateSocketWithAccount: function(socket, account) {
socketToAccountMap.set(socket, account);
},
/**
* Wrap calls to external socket handlers in this function; if
* they throw an exception, we'll try to mitigate it.
*/
catchSocketExceptions: function(socket, fn) {
try {
fn();
} catch(e) {
var account = socketToAccountMap.get(socket);
// Attempt to close the socket so that we're less likely to
// get stuck in a completely broken state, either by sending
// bogus data or barfing on further received data.
try {
socket.close();
} catch(socketEx) {
console.error('Error attempting to close socket:', socketEx);
}
this.handleDisastrousError(e, account);
}
},
/**
* Something horrible has happened, and we must clean up the mess.
* Perhaps a socket ondata handler threw an exception, or
* something of that nature. If we're running a job, we should
* abort and maybe reschedule. If we have the mutex, we must
* release it.
*
* @param {Error} exception
* @param {MailAccount|null} account
*/
handleDisastrousError: function(e, account) {
var op, jobDoneCallback;
if (account) {
var opInfo = accountToOperationMap.get(account);
if (opInfo) {
op = opInfo.op;
jobDoneCallback = opInfo.callback;
}
}
logic(scope, 'exception', {
accountId: account && account.id,
op: op,
error: e,
errorName: e && e.name,
errorMessage: e && e.message,
stack: e.stack
});
console.error('*** Disastrous Error for email accountId',
account && account.id,
'-- attempting to recover...');
// See if we can recover in any way.
if (account) {
if (op) {
logic(scope, 'finished-job', { error: e });
console.warn('Force-completing in-progress op:', op);
jobDoneCallback('disastrous-error');
} else {
console.warn('No job operation was currently running.');
}
} else {
console.warn('No account associated with this error; nothing to abort.');
}
}
};
return DisasterRecovery;
});

View file

@ -0,0 +1,287 @@
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

View file

@ -0,0 +1,192 @@
/**
* Back-end draft abstraction.
*
* Drafts are saved to folder storage and look almost exactly like received
* messages. The primary difference is that attachments that are in the
* process of being attached are stored in an `attaching` field on the
* `BodyInfo` instance and that they are discarded on load if still present
* (indicating a crash/something like a crash during the save process).
*
**/
define(function(require) {
var mailRep = require('../db/mail_rep');
/**
* Create a new header and body for a draft by extracting any useful state
* from the previous draft's persisted header/body and the revised draft.
*
* @method mergeDraftStates
* @param oldHeader {HeaderInfo}
* @param oldBody {BodyInfo}
* @param newDraftRep {DraftRep}
* @param newDraftInfo {Object}
* @param newDraftInfo.id {Number}
* @param newDraftInfo.suid {SUID}
* @param newDraftInfo.date {Number}
*/
function mergeDraftStates(oldHeader, oldBody,
newDraftRep, newDraftInfo,
universe) {
var identity = universe.getIdentityForSenderIdentityId(newDraftRep.senderId);
// -- convert from compose rep to header/body rep
var newHeader = mailRep.makeHeaderInfo({
id: newDraftInfo.id,
srvid: null, // stays null
suid: newDraftInfo.suid, // filled in by the job
// we currently don't generate a message-id for drafts, but we'll need to
// do this when we start appending to the server.
guid: oldHeader ? oldHeader.guid : null,
author: { name: identity.name, address: identity.address},
to: newDraftRep.to,
cc: newDraftRep.cc,
bcc: newDraftRep.bcc,
replyTo: identity.replyTo,
date: newDraftInfo.date,
flags: [],
hasAttachments: oldBody ? oldBody.attachments.length > 0 : false,
subject: newDraftRep.subject,
snippet: newDraftRep.body.text.substring(0, 100),
});
var newBody = mailRep.makeBodyInfo({
date: newDraftInfo.date,
size: 0,
attachments: oldBody ? oldBody.attachments.concat() : [],
relatedParts: oldBody ? oldBody.relatedParts.concat() : [],
references: newDraftRep.referencesStr,
bodyReps: []
});
newBody.bodyReps.push(mailRep.makeBodyPart({
type: 'plain',
part: null,
sizeEstimate: newDraftRep.body.text.length,
amountDownloaded: newDraftRep.body.text.length,
isDownloaded: true,
_partInfo: {},
content: [0x1, newDraftRep.body.text]
}));
if (newDraftRep.body.html) {
newBody.bodyReps.push(mailRep.makeBodyPart({
type: 'html',
part: null,
sizeEstimate: newDraftRep.body.html.length,
amountDownloaded: newDraftRep.body.html.length,
isDownloaded: true,
_partInfo: {},
content: newDraftRep.body.html
}));
}
return {
header: newHeader,
body: newBody
};
}
function convertHeaderAndBodyToDraftRep(account, header, body) {
var composeBody = {
text: '',
html: null,
};
// Body structure should be guaranteed, but add some checks.
if (body.bodyReps.length >= 1 &&
body.bodyReps[0].type === 'plain' &&
body.bodyReps[0].content.length === 2 &&
body.bodyReps[0].content[0] === 0x1) {
composeBody.text = body.bodyReps[0].content[1];
}
// HTML is optional, but if present, should satisfy our guard
if (body.bodyReps.length == 2 &&
body.bodyReps[1].type === 'html') {
composeBody.html = body.bodyReps[1].content;
}
var attachments = [];
body.attachments.forEach(function(att) {
attachments.push({
name: att.name,
blob: att.file
});
});
var draftRep = {
identity: account.identities[0],
subject: header.subject,
body: composeBody,
to: header.to,
cc: header.cc,
bcc: header.bcc,
referencesStr: body.references,
attachments: attachments
};
}
/**
* Given the HeaderInfo and BodyInfo for a draft, create a new header and body
* suitable for saving to the sent folder for a POP3 account. Specifically:
* - mark the message as read
* - make sure body.attaching does not make it through
* - strip the Blob references so we don't accidentally keep the base64
* encoded attachment parts around forever and clog up the disk.
* - avoid accidental use of the same instances between the drafts folder and
* the sent folder
*
* @param header {HeaderInfo}
* @param body {BodyInfo}
* @param newInfo
* @param newInfo.id
* @param newInfo.suid {SUID}
* @return { header, body }
*/
function cloneDraftMessageForSentFolderWithoutAttachments(header, body,
newInfo) {
// clone the header (drops excess fields)
var newHeader = mailRep.makeHeaderInfo(header);
// clobber the id/suid
newHeader.id = newInfo.id;
newHeader.suid = newInfo.suid;
// Mark the message as read. We are clobbering other flags, but we don't
// currently support a way for them to exist.
newHeader.flags = ['\\Seen'];
// clone the body, dropping excess fields like "attaching".
var newBody = mailRep.makeBodyInfo(body);
// transform attachments
if (newBody.attachments) {
newBody.attachments = newBody.attachments.map(function(oldAtt) {
var newAtt = mailRep.makeAttachmentPart(oldAtt);
// mark the attachment as non-downloadable
newAtt.type = 'application/x-gelam-no-download';
// get rid of the blobs!
newAtt.file = null;
// we will keep around the sizeEstimate so they know how much they sent
// and we keep around encoding/charset/textFormat because we don't care
return newAtt;
});
}
// We currently can't generate related parts, but just in case.
if (newBody.relatedParts) {
newBody.relatedParts = [];
}
// Body parts can be transferred verbatim.
newBody.bodyReps = newBody.bodyReps.map(function(oldRep) {
return mailRep.makeBodyPart(oldRep);
});
return { header: newHeader, body: newBody };
}
return {
mergeDraftStates: mergeDraftStates,
convertHeaderAndBodyToDraftRep: convertHeaderAndBodyToDraftRep,
cloneDraftMessageForSentFolderWithoutAttachments:
cloneDraftMessageForSentFolderWithoutAttachments
};
}); // end define

View file

@ -0,0 +1,379 @@
/**
* Draft jobs: save/delete drafts, attach/remove attachments. These gets mixed
* into the specific JobDriver implementations.
**/
define(function(require, exports) {
var mailRep = require('../db/mail_rep');
var draftRep = require('../drafts/draft_rep');
var base64 = require('safe-base64');
var asyncFetchBlobAsUint8Array =
require('../async_blob_fetcher').asyncFetchBlobAsUint8Array;
var draftsMixins = exports.draftsMixins = {};
////////////////////////////////////////////////////////////////////////////////
// attachBlobToDraft
/**
* How big a chunk of an attachment should we encode in a single read? Because
* we want our base64-encoded lines to be 76 bytes long (before newlines) and
* there's a 4/3 expansion factor, we want to read a multiple of 57 bytes.
*
* I initially chose the largest value just under 1MiB. This appeared too
* chunky on the ZTE open, so I'm halving to just under 512KiB. Calculated via
* Math.floor(512 * 1024 / 57) = 9198. The encoded size of this ends up to be
* 9198 * 78 which is ~700 KiB. So together that's ~1.2 megs if we don't
* generate a ton of garbage by creating a lot of intermediary strings.
*
* This seems reasonable given goals of not requiring the GC to run after every
* block and not having us tie up the CPU too long during our encoding.
*/
draftsMixins.BLOB_BASE64_BATCH_CONVERT_SIZE = 9198 * 57;
/**
* Incrementally convert an attachment into its base64 encoded attachment form
* which we save in chunks to IndexedDB to avoid using too much memory now or
* during the sending process.
*
* - Retrieve the body the draft is persisted to,
* - Repeat until the attachment is fully attached:
* - take a chunk of the source attachment
* - base64 encode it into a Blob by creating a Uint8Array and manually
* encoding into that. (We need to put a \r\n after every 76 bytes, and
* doing that using window.btoa is going to create a lot of garbage. And
* addressing that is no longer premature optimization.)
* - update the body with that Blob
* - trigger a save of the account so that IndexedDB writes the account to
* disk.
* - force the body block to be discarded from the cache and then re-get the
* body. We won't be saving any memory until the Blob has been written to
* disk and we have forgotten all references to the in-memory Blob we wrote
* to the database. (The Blob does not magically get turned into a
* reference to the database.)
* - Be done. Note that we leave the "small" Blobs independent; we do not
* create a super Blob.
*
* ## Logging ##
*
* We log at:
* - The start of the process.
* - For each block.
* - The end of the process.
*/
draftsMixins.local_do_attachBlobToDraft = function(op, callback) {
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
if (!localDraftsFolder) {
callback('moot');
return;
}
var self = this;
this._accessFolderForMutation(
localDraftsFolder.id, /* needConn*/ false,
function(nullFolderConn, folderStorage) {
var wholeBlob = op.attachmentDef.blob;
// - Retrieve the message
var header, body;
console.log('attachBlobToDraft: retrieving message');
folderStorage.getMessage(
op.existingNamer.suid, op.existingNamer.date, {}, gotMessage);
function gotMessage(records) {
header = records.header;
body = records.body;
if (!header || !body) {
// No header/body suggests either some major invariant is busted or
// one or more UIs issued attach commands after the draft was mooted.
callback('failure-give-up');
return;
}
body.attaching = mailRep.makeAttachmentPart({
name: op.attachmentDef.name,
type: wholeBlob.type,
sizeEstimate: wholeBlob.size,
// this is where we put the Blob segments...
file: [],
});
convertNextChunk(body);
}
var blobOffset = 0;
function convertNextChunk(refreshedBody) {
body = refreshedBody;
var nextOffset =
Math.min(wholeBlob.size,
blobOffset + self.BLOB_BASE64_BATCH_CONVERT_SIZE);
console.log('attachBlobToDraft: fetching', blobOffset, 'to',
nextOffset, 'of', wholeBlob.size);
var slicedBlob = wholeBlob.slice(blobOffset, nextOffset);
blobOffset = nextOffset;
asyncFetchBlobAsUint8Array(slicedBlob, gotChunk);
}
function gotChunk(err, binaryDataU8) {
console.log('attachBlobToDraft: fetched');
// The Blob really should not be disappear out from under us, but it
// could happen.
if (err) {
callback('failure-give-up');
return;
}
var lastChunk = (blobOffset >= wholeBlob.size);
var encodedU8 = base64.mimeStyleBase64Encode(binaryDataU8);
body.attaching.file.push(new Blob([encodedU8],
{ type: wholeBlob.type }));
var eventDetails;
if (lastChunk) {
var attachmentIndex = body.attachments.length;
body.attachments.push(body.attaching);
delete body.attaching; // bad news for shapes, but drafts are rare.
eventDetails = {
changeDetails: {
attachments: [attachmentIndex]
}
};
}
else {
// Do not generate an event for intermediary states; there is nothing
// to observe.
eventDetails = null;
}
console.log('attachBlobToDraft: flushing');
folderStorage.updateMessageBody(
header, body, { flushBecause: 'blobs' }, eventDetails,
lastChunk ? bodyUpdatedAllDone : convertNextChunk);
body = null;
}
function bodyUpdatedAllDone(newBodyInfo) {
console.log('attachBlobToDraft: blob fully attached');
callback(null);
}
},
/* no conn => no deathback required */ null,
'attachBlobToDraft');
};
draftsMixins.do_attachBlobToDraft = function(op, callback) {
// there is no server component for this
callback(null);
};
draftsMixins.check_attachBlobToDraft = function(op, callback) {
callback(null, 'moot');
};
draftsMixins.local_undo_attachBlobToDraft = function(op, callback) {
callback(null);
};
draftsMixins.undo_attachBlobToDraft = function(op, callback) {
callback(null);
};
////////////////////////////////////////////////////////////////////////////////
// detachAttachmentFromDraft
draftsMixins.local_do_detachAttachmentFromDraft = function(op, callback) {
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
if (!localDraftsFolder) {
callback('moot');
return;
}
var self = this;
this._accessFolderForMutation(
localDraftsFolder.id, /* needConn*/ false,
function(nullFolderConn, folderStorage) {
// - Retrieve the message
var header, body;
console.log('detachAttachmentFromDraft: retrieving message');
folderStorage.getMessage(
op.existingNamer.suid, op.existingNamer.date, {}, gotMessage);
function gotMessage(records) {
header = records.header;
body = records.body;
if (!header || !body) {
// No header/body suggests either some major invariant is busted or
// one or more UIs issued attach commands after the draft was mooted.
callback('failure-give-up');
return;
}
// Just forget about the attachment. Splice handles insane indices.
body.attachments.splice(op.attachmentIndex, 1);
console.log('detachAttachmentFromDraft: flushing');
folderStorage.updateMessageBody(
header, body,
{ flushBecause: 'blobs' },
{
changeDetails: {
detachedAttachments: [op.attachmentIndex]
}
},
bodyUpdatedAllDone);
}
function bodyUpdatedAllDone(newBodyInfo) {
console.log('detachAttachmentFromDraft: blob fully detached');
callback(null);
}
},
/* no conn => no deathback required */ null,
'detachAttachmentFromDraft');
};
draftsMixins.do_detachAttachmentFromDraft = function(op, callback) {
// there is no server component for this at this time.
callback(null);
};
draftsMixins.check_detachAttachmentFromDraft = function(op, callback) {
callback(null);
};
draftsMixins.local_undo_detachAttachmentFromDraft = function(op, callback) {
callback(null);
};
draftsMixins.undo_detachAttachmentFromDraft = function(op, callback) {
callback(null);
};
////////////////////////////////////////////////////////////////////////////////
// saveDraft
/**
* Save a draft; if there already was a draft, it gets replaced. The new
* draft gets a new date and id/SUID so it is logically distinct. However,
* we will propagate attachment and on-server information between drafts.
*/
draftsMixins.local_do_saveDraft = function(op, callback) {
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
if (!localDraftsFolder) {
callback('moot');
return;
}
var self = this;
this._accessFolderForMutation(
localDraftsFolder.id, /* needConn*/ false,
function(nullFolderConn, folderStorage) {
// there's always a header add and a body add
var waitingForDbMods = 2;
function gotMessage(oldRecords) {
var newRecords = draftRep.mergeDraftStates(
oldRecords.header, oldRecords.body,
op.draftRep,
op.newDraftInfo,
self.account.universe);
// If there already was a draft saved, delete it.
// Note that ordering of the removal and the addition doesn't really
// matter here because of our use of transactions.
if (op.existingNamer) {
waitingForDbMods++;
folderStorage.deleteMessageHeaderAndBody(
op.existingNamer.suid, op.existingNamer.date, dbModCompleted);
}
folderStorage.addMessageHeader(newRecords.header, newRecords.body,
dbModCompleted);
folderStorage.addMessageBody(newRecords.header, newRecords.body,
dbModCompleted);
function dbModCompleted() {
if (--waitingForDbMods === 0) {
callback(
/* no error */ null,
/* result */ newRecords,
/* save account */ true);
}
}
}
if (op.existingNamer) {
folderStorage.getMessage(
op.existingNamer.suid, op.existingNamer.date, null, gotMessage);
}
else {
gotMessage({ header: null, body: null });
}
},
/* no conn => no deathback required */ null,
'saveDraft');
};
/**
* FUTURE WORK: Save a draft to the server; this is inherently IMAP only.
* Tracked on: https://bugzilla.mozilla.org/show_bug.cgi?id=799822
*
* It is very possible that we will save local drafts faster / more frequently
* than we can update our server state. It only makes sense to upload the
* latest draft state to the server. Because we delete our old local drafts,
* it's obvious when we should skip out on updating the server draft for
* something.
*
* Because IMAP drafts have to replace the prior drafts, we use our old 'srvid'
* to know what message to delete as well as what message to pull attachments
* from when we're in a mode where we upload attachments to drafts and CATENATE
* is available.
*/
draftsMixins.do_saveDraft = function(op, callback) {
callback(null);
};
draftsMixins.check_saveDraft = function(op, callback) {
callback(null, 'moot');
};
draftsMixins.local_undo_saveDraft = function(op, callback) {
callback(null);
};
draftsMixins.undo_saveDraft = function(op, callback) {
callback(null);
};
////////////////////////////////////////////////////////////////////////////////
// deleteDraft
draftsMixins.local_do_deleteDraft = function(op, callback) {
var localDraftsFolder = this.account.getFirstFolderWithType('localdrafts');
if (!localDraftsFolder) {
callback('moot');
return;
}
var self = this;
this._accessFolderForMutation(
localDraftsFolder.id, /* needConn*/ false,
function(nullFolderConn, folderStorage) {
folderStorage.deleteMessageHeaderAndBody(
op.messageNamer.suid, op.messageNamer.date,
function() {
callback(null, null, /* save account */ true);
});
},
/* no conn => no deathback required */ null,
'deleteDraft');
};
draftsMixins.do_deleteDraft = function(op, callback) {
// there is no server component for this
callback(null);
};
draftsMixins.check_deleteDraft = function(op, callback) {
callback(null, 'moot');
};
draftsMixins.local_undo_deleteDraft = function(op, callback) {
callback(null);
};
draftsMixins.undo_deleteDraft = function(op, callback) {
callback(null);
};
////////////////////////////////////////////////////////////////////////////////
}); // end define

View file

@ -0,0 +1,226 @@
/**
* Error-handling/backoff logic.
*
* - All existing-account network-accessing functionality uses this module to
* track the state of accounts and resources within accounts that are may
* experience some type of time-varying failures.
* - Account autoconfiguration probing logic does not use this module; it just
* checks whether there is a network connection.
*
* - Accounts define 'endpoints' with us when they are instantiated for each
* server connection type they have. For IMAP, this means an SMTP endpoint
* and an IMAP endpoint.
* - These 'endpoints' may have internal 'resources' which may manifest failures
* of their own if-and-only-if it is expected that there could be transient
* failures within the endpoint. For IMAP, it is possible for IMAP servers to
* not let us into certain folders because there are other active connections
* inside them. If something can't fail, there is no need to name it as a
* resource.
*
* - All endpoints have exactly one status: 'healthy', 'unreachable', or
* 'broken'. Unreachable implies we are having trouble talking with the
* endpoint because of network issues. Broken implies that although we can
* talk to the endpoint, it doesn't want to talk to us for reasons of being
* offline for maintenance or account migration or something like that.
* - Endpoint resources can only be 'broken' and are only tracked if they are
* broken.
*
* - If we encounter a network error for an otherwise healthy endpoint then we
* try again once right away, as a lot of network errors only become evident
* once we have a new, good network.
* - On subsequent network errors for the previously healthy endpoint where we
* have already retried, we try after a ~1 second delay and then a ~5 second
* delay. Then we give up and put the endpoint in the unreachable or broken
* state, as appropriate. These choice of delays are entirely arbitrary.
*
* - We only try once to connect to endpoints that are in a degraded state. We
* do not retry because that would be wasteful.
*
* - Once we put an endpoint in a degraded (unreachable or broken) state, this
* module never does anything to try and probe for the endpoint coming back
* on its own. We rely on the existing periodic synchronization logic or
* user actions to trigger a new attempt. WE MAY NEED TO CHANGE THIS AT
* SOME POINT since it's possible that the user may have queued an email for
* sending that they want delivered sooner than the cron logic triggers, but
* that's way down the road.
**/
define(
[
'./date',
'logic',
'module',
'exports'
],
function(
$date,
logic,
$module,
exports
) {
var BACKOFF_DURATIONS = exports.BACKOFF_DURATIONS = [
{ fixedMS: 0, randomMS: 0 },
{ fixedMS: 800, randomMS: 400 },
{ fixedMS: 4500, randomMS: 1000 },
];
var BAD_RESOURCE_RETRY_DELAYS_MS = [
1000,
60 * 1000,
2 * 60 * 1000,
];
var setTimeoutFunc = window.setTimeout.bind(window);
exports.TEST_useTimeoutFunc = function(func) {
setTimeoutFunc = func;
for (var i = 0; i < BACKOFF_DURATIONS.length; i++) {
BACKOFF_DURATIONS[i].randomMS = 0;
}
};
/**
* @args[
* @param[listener @dict[
* @key[onEndpointStateChange @func[
* @args[state]
* ]]
* ]]
* ]
*/
function BackoffEndpoint(name, listener) {
/** @oneof[
* @case['healthy']
* @case['unreachable']
* @case['broken']
* @case['shutdown']{
* We are shutting down; ignore any/all errors and avoid performing
* activities that would result in new network traffic, etc.
* }
* ]
*/
this.state = 'healthy';
this._iNextBackoff = 0;
logic.defineScope(this, 'BackoffEndpoint', { name: name });
logic(this, 'state', { state: this.state });
this._badResources = {};
this.listener = listener;
}
BackoffEndpoint.prototype = {
_setState: function(newState) {
if (this.state === newState)
return;
this.state = newState;
logic(this, 'state', { state: newState });
if (this.listener)
this.listener.onEndpointStateChange(newState);
},
noteConnectSuccess: function() {
this._setState('healthy');
this._iNextBackoff = 0;
},
/**
* Logs a connection failure and returns true if a retry attempt should be
* made.
*
* @args[
* @param[reachable Boolean]{
* If true, we were able to connect to the endpoint, but failed to login
* for some reason.
* }
* ]
* @return[shouldRetry Boolean]{
* Returns true if we should retry creating the connection, false if we
* should give up.
* }
*/
noteConnectFailureMaybeRetry: function(reachable) {
logic(this, 'connectFailure', { reachable: reachable });
if (this.state === 'shutdown')
return false;
if (reachable) {
this._setState('broken');
return false;
}
if (this._iNextBackoff > 0)
this._setState(reachable ? 'broken' : 'unreachable');
// (Once this saturates, we never perform retries until the connection is
// healthy again. We do attempt re-connections when triggered by user
// activity or synchronization logic; they just won't get retries.)
if (this._iNextBackoff >= BACKOFF_DURATIONS.length)
return false;
return true;
},
/**
* Logs a connection problem where we can talk to the server but we are
* confident there is no reason retrying. In some cases, like bad
* credentials, this is part of what you want to do, but you will still also
* want to put the kibosh on additional requests at a higher level since
* servers can lock people out if they make repeated bad authentication
* requests.
*/
noteBrokenConnection: function() {
logic(this, 'connectFailure', { reachable: true });
this._setState('broken');
this._iNextBackoff = BACKOFF_DURATIONS.length;
},
scheduleConnectAttempt: function(connectFunc) {
if (this.state === 'shutdown')
return;
// If we have already saturated our retries then there won't be any
// automatic retries and this request is assumed to want us to try and
// create a connection right now.
if (this._iNextBackoff >= BACKOFF_DURATIONS.length) {
connectFunc();
return;
}
var backoff = BACKOFF_DURATIONS[this._iNextBackoff++],
delay = backoff.fixedMS +
Math.floor(Math.random() * backoff.randomMS);
setTimeoutFunc(connectFunc, delay);
},
noteBadResource: function(resourceId) {
var now = $date.NOW();
if (!this._badResources.hasOwnProperty(resourceId)) {
this._badResources[resourceId] = { count: 1, last: now };
}
else {
var info = this._badResources[resourceId];
info.count++;
info.last = now;
}
},
resourceIsOkayToUse: function(resourceId) {
if (!this._badResources.hasOwnProperty(resourceId))
return true;
var info = this._badResources[resourceId], now = $date.NOW();
},
shutdown: function() {
this._setState('shutdown');
},
};
exports.createEndpoint = function(name, listener) {
return new BackoffEndpoint(name, listener);
};
}); // end define

View file

@ -0,0 +1,111 @@
define(['exports'], function(exports) {
/**
* Helpers to deal with our normalized error strings. Provides
* metadata such as whether we should retry or report errors given
* an error string.
*
* For a full description of each of these errors and why they might
* be raised, see `mailapi.js`.
*/
var ALL_ERRORS = {
// Transient Errors
'offline': { reachable: false, retry: true, report: false },
'server-maintenance': { reachable: true, retry: true, report: false },
'unresponsive-server': { reachable: false, retry: true, report: false },
'port-not-listening': { reachable: false, retry: true, report: false },
// Permanent Account Errors (user intervention required)
'bad-user-or-pass': { reachable: true, retry: false, report: true },
'needs-oauth-reauth': { reachable: true, retry: false, report: true },
'imap-disabled': { reachable: true, retry: false, report: true },
'pop3-disabled': { reachable: true, retry: false, report: true },
'not-authorized': { reachable: true, retry: false, report: true },
// Account Configuration Errors
'bad-security': { reachable: true, retry: false, report: true },
'no-config-info': { reachable: false, retry: false, report: false },
'user-account-exists': { reachable: false, retry: false, report: false },
'pop-server-not-great': { reachable: true, retry: false, report: false },
'no-dns-entry': { reachable: false, retry: false, report: false },
// Action-Specific Errors
'bad-address': { reachable: true, retry: false, report: false },
'server-problem': { reachable: true, retry: false, report: false },
'unknown': { reachable: false, retry: false, report: false }
};
/**
* Should we surface the error to the user so that they can take
* action? Permanent connection problems and credential errors
* require user intervention.
*
* @param {String} err
* A normalized error string.
* @return {Boolean}
*/
exports.shouldReportProblem = function(err) {
return (ALL_ERRORS[err] || ALL_ERRORS['unknown']).report;
};
/**
* Should we retry the operation using backoff? Only for errors that
* will likely resolve themselves automatically, such as network
* problems and server maintenance.
*
* @param {String} err
* A normalized error string.
* @return {Boolean}
*/
exports.shouldRetry = function(err) {
return (ALL_ERRORS[err] || ALL_ERRORS['unknown']).retry;
};
/**
* Did this error occur when the server was reachable?
*/
exports.wasErrorFromReachableState = function(err) {
return (ALL_ERRORS[err] || ALL_ERRORS['unknown']).reachable;
};
/**
* Analyze an error object (specifically, an exception or an
* exception-like object) and attempt to find a matching normalized
* string error code. Returns null if none matched, for easy
* cascading with more specific error types.
*
* @param {String} err
* A normalized error string.
* @return {String|null}
*/
exports.analyzeException = function(err) {
// XXX: Fault-injecting-socket returns the string "Connection
// refused" for certian socket errors. (Does mozTCPSocket raise
// that error verbatim?) Convert that to an Error-like object.
if (err === 'Connection refused') {
err = { name: 'ConnectionRefusedError' };
}
// Otherwise, assume a plain-old string is already normalized.
else if (typeof err === 'string') {
return err;
}
if (!err.name) {
return null;
}
if (/^Security/.test(err.name)) {
return 'bad-security';
}
else if (/^ConnectionRefused/i.test(err.name)) {
return 'unresponsive-server';
}
else {
return null;
}
}
});

View file

@ -0,0 +1,122 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object') {
define = function(deps, factory) {
deps = deps.map.forEach(function(id) {
return require(id);
});
module.exports = factory(deps);
};
define.amd = {};
}
if (typeof define === 'function' && define.amd) {
define([
'wbxml',
'./codepages/Common',
'./codepages/AirSync',
'./codepages/Contacts',
'./codepages/Email',
'./codepages/Calendar',
'./codepages/Move',
'./codepages/ItemEstimate',
'./codepages/FolderHierarchy',
'./codepages/MeetingResponse',
'./codepages/Tasks',
'./codepages/ResolveRecipients',
'./codepages/ValidateCert',
'./codepages/Contacts2',
'./codepages/Ping',
'./codepages/Provision',
'./codepages/Search',
'./codepages/GAL',
'./codepages/AirSyncBase',
'./codepages/Settings',
'./codepages/DocumentLibrary',
'./codepages/ItemOperations',
'./codepages/ComposeMail',
'./codepages/Email2',
'./codepages/Notes',
'./codepages/RightsManagement'
], factory);
} else {
root.ActiveSyncCodepages = factory(WBXML,
ASCPCommon,
ASCPAirSync,
ASCPContacts,
ASCPEmail,
ASCPCalendar,
ASCPMove,
ASCPItemEstimate,
ASCPHierarchy,
ASCPMeetingResponse,
ASCPTasks,
ASCPResolveRecipients,
ASCPValidateCert,
ASCPContacts2,
ASCPPing,
ASCPProvision,
ASCPSearch,
ASCPGAL,
ASCPAirSyncBase,
ASCPSettings,
ASCPDocumentLibrary,
ASCPItemOperations,
ASCPComposeMail,
ASCPEmail2,
ASCPNotes,
ASCPRightsManagement);
}
}(this, function(WBXML, Common, AirSync, Contacts, Email, Calendar, Move,
ItemEstimate, FolderHierarchy, MeetingResponse, Tasks,
ResolveRecipients, ValidateCert, Contacts2, Ping, Provision,
Search, GAL, AirSyncBase, Settings, DocumentLibrary,
ItemOperations, ComposeMail, Email2, Notes, RightsManagement) {
'use strict';
var codepages = {
Common: Common,
AirSync: AirSync,
Contacts: Contacts,
Email: Email,
Calendar: Calendar,
Move: Move,
ItemEstimate: ItemEstimate,
FolderHierarchy: FolderHierarchy,
MeetingResponse: MeetingResponse,
Tasks: Tasks,
ResolveRecipients: ResolveRecipients,
ValidateCert: ValidateCert,
Contacts2: Contacts2,
Ping: Ping,
Provision: Provision,
Search: Search,
GAL: GAL,
AirSyncBase: AirSyncBase,
Settings: Settings,
DocumentLibrary: DocumentLibrary,
ItemOperations: ItemOperations,
ComposeMail: ComposeMail,
Email2: Email2,
Notes: Notes,
RightsManagement: RightsManagement
};
WBXML.CompileCodepages(codepages);
return codepages;
}));

View file

@ -0,0 +1,115 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPAirSync = factory();
}(this, function() {
'use strict';
return {
Tags: {
Sync: 0x0005,
Responses: 0x0006,
Add: 0x0007,
Change: 0x0008,
Delete: 0x0009,
Fetch: 0x000A,
SyncKey: 0x000B,
ClientId: 0x000C,
ServerId: 0x000D,
Status: 0x000E,
Collection: 0x000F,
Class: 0x0010,
Version: 0x0011,
CollectionId: 0x0012,
GetChanges: 0x0013,
MoreAvailable: 0x0014,
WindowSize: 0x0015,
Commands: 0x0016,
Options: 0x0017,
FilterType: 0x0018,
Truncation: 0x0019,
RtfTruncation: 0x001A,
Conflict: 0x001B,
Collections: 0x001C,
ApplicationData: 0x001D,
DeletesAsMoves: 0x001E,
NotifyGUID: 0x001F,
Supported: 0x0020,
SoftDelete: 0x0021,
MIMESupport: 0x0022,
MIMETruncation: 0x0023,
Wait: 0x0024,
Limit: 0x0025,
Partial: 0x0026,
ConversationMode: 0x0027,
MaxItems: 0x0028,
HeartbeatInterval: 0x0029,
},
Enums: {
Status: {
Success: '1',
InvalidSyncKey: '3',
ProtocolError: '4',
ServerError: '5',
ConversionError: '6',
MatchingConflict: '7',
ObjectNotFound: '8',
OutOfSpace: '9',
HierarchyChanged: '12',
IncompleteRequest: '13',
InvalidInterval: '14',
InvalidRequest: '15',
Retry: '16',
},
FilterType: {
NoFilter: '0',
OneDayBack: '1',
ThreeDaysBack: '2',
OneWeekBack: '3',
TwoWeeksBack: '4',
OneMonthBack: '5',
ThreeMonthsBack: '6',
SixMonthsBack: '7',
IncompleteTasks: '8',
},
Conflict: {
ClientReplacesServer: '0',
ServerReplacesClient: '1',
},
MIMESupport: {
Never: '0',
SMIMEOnly: '1',
Always: '2',
},
MIMETruncation: {
TruncateAll: '0',
Truncate4K: '1',
Truncate5K: '2',
Truncate7K: '3',
Truncate10K: '4',
Truncate20K: '5',
Truncate50K: '6',
Truncate100K: '7',
NoTruncate: '8',
},
},
};
}));

View file

@ -0,0 +1,74 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPAirSyncBase = factory();
}(this, function() {
'use strict';
return {
Tags: {
BodyPreference: 0x1105,
Type: 0x1106,
TruncationSize: 0x1107,
AllOrNone: 0x1108,
Reserved: 0x1109,
Body: 0x110A,
Data: 0x110B,
EstimatedDataSize: 0x110C,
Truncated: 0x110D,
Attachments: 0x110E,
Attachment: 0x110F,
DisplayName: 0x1110,
FileReference: 0x1111,
Method: 0x1112,
ContentId: 0x1113,
ContentLocation: 0x1114,
IsInline: 0x1115,
NativeBodyType: 0x1116,
ContentType: 0x1117,
Preview: 0x1118,
BodyPartPreference: 0x1119,
BodyPart: 0x111A,
Status: 0x111B,
},
Enums: {
Type: {
PlainText: '1',
HTML: '2',
RTF: '3',
MIME: '4',
},
Method: {
Normal: '1',
EmbeddedMessage: '5',
AttachOLE: '6',
},
NativeBodyType: {
PlainText: '1',
HTML: '2',
RTF: '3',
},
Status: {
Success: '1',
}
}
};
}));

View file

@ -0,0 +1,85 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPCalendar = factory();
}(this, function() {
'use strict';
return {
Tags: {
TimeZone: 0x0405,
AllDayEvent: 0x0406,
Attendees: 0x0407,
Attendee: 0x0408,
Email: 0x0409,
Name: 0x040A,
Body: 0x040B,
BodyTruncated: 0x040C,
BusyStatus: 0x040D,
Categories: 0x040E,
Category: 0x040F,
CompressedRTF: 0x0410,
DtStamp: 0x0411,
EndTime: 0x0412,
Exception: 0x0413,
Exceptions: 0x0414,
Deleted: 0x0415,
ExceptionStartTime: 0x0416,
Location: 0x0417,
MeetingStatus: 0x0418,
OrganizerEmail: 0x0419,
OrganizerName: 0x041A,
Recurrence: 0x041B,
Type: 0x041C,
Until: 0x041D,
Occurrences: 0x041E,
Interval: 0x041F,
DayOfWeek: 0x0420,
DayOfMonth: 0x0421,
WeekOfMonth: 0x0422,
MonthOfYear: 0x0423,
Reminder: 0x0424,
Sensitivity: 0x0425,
Subject: 0x0426,
StartTime: 0x0427,
UID: 0x0428,
AttendeeStatus: 0x0429,
AttendeeType: 0x042A,
Attachment: 0x042B,
Attachments: 0x042C,
AttName: 0x042D,
AttSize: 0x042E,
AttOid: 0x042F,
AttMethod: 0x0430,
AttRemoved: 0x0431,
DisplayName: 0x0432,
DisallowNewTimeProposal: 0x0433,
ResponseRequested: 0x0434,
AppointmentReplyTime: 0x0435,
ResponseType: 0x0436,
CalendarType: 0x0437,
IsLeapMonth: 0x0438,
FirstDayOfWeek: 0x0439,
OnlineMeetingConfLink: 0x043A,
OnlineMeetingExternalLink: 0x043B,
},
};
}));

View file

@ -0,0 +1,106 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPCommon = factory();
}(this, function() {
'use strict';
return {
Enums: {
Status: {
InvalidContent: '101',
InvalidWBXML: '102',
InvalidXML: '103',
InvalidDateTime: '104',
InvalidCombinationOfIDs: '105',
InvalidIDs: '106',
InvalidMIME: '107',
DeviceIdMissingOrInvalid: '108',
DeviceTypeMissingOrInvalid: '109',
ServerError: '110',
ServerErrorRetryLater: '111',
ActiveDirectoryAccessDenied: '112',
MailboxQuotaExceeded: '113',
MailboxServerOffline: '114',
SendQuotaExceeded: '115',
MessageRecipientUnresolved: '116',
MessageReplyNotAllowed: '117',
MessagePreviouslySent: '118',
MessageHasNoRecipient: '119',
MailSubmissionFailed: '120',
MessageReplyFailed: '121',
AttachmentIsTooLarge: '122',
UserHasNoMailbox: '123',
UserCannotBeAnonymous: '124',
UserPrincipalCouldNotBeFound: '125',
UserDisabledForSync: '126',
UserOnNewMailboxCannotSync: '127',
UserOnLegacyMailboxCannotSync: '128',
DeviceIsBlockedForThisUser: '129',
AccessDenied: '130',
AccountDisabled: '131',
SyncStateNotFound: '132',
SyncStateLocked: '133',
SyncStateCorrupt: '134',
SyncStateAlreadyExists: '135',
SyncStateVersionInvalid: '136',
CommandNotSupported: '137',
VersionNotSupported: '138',
DeviceNotFullyProvisionable: '139',
RemoteWipeRequested: '140',
LegacyDeviceOnStrictPolicy: '141',
DeviceNotProvisioned: '142',
PolicyRefresh: '143',
InvalidPolicyKey: '144',
ExternallyManagedDevicesNotAllowed: '145',
NoRecurrenceInCalendar: '146',
UnexpectedItemClass: '147',
RemoteServerHasNoSSL: '148',
InvalidStoredRequest: '149',
ItemNotFound: '150',
TooManyFolders: '151',
NoFoldersFounds: '152',
ItemsLostAfterMove: '153',
FailureInMoveOperation: '154',
MoveCommandDisallowedForNonPersistentMoveAction: '155',
MoveCommandInvalidDestinationFolder: '156',
AvailabilityTooManyRecipients: '160',
AvailabilityDLLimitReached: '161',
AvailabilityTransientFailure: '162',
AvailabilityFailure: '163',
BodyPartPreferenceTypeNotSupported: '164',
DeviceInformationRequired: '165',
InvalidAccountId: '166',
AccountSendDisabled: '167',
IRM_FeatureDisabled: '168',
IRM_TransientError: '169',
IRM_PermanentError: '170',
IRM_InvalidTemplateID: '171',
IRM_OperationNotPermitted: '172',
NoPicture: '173',
PictureTooLarge: '174',
PictureLimitReached: '175',
BodyPart_ConversationTooLarge: '176',
MaximumDevicesReached: '177',
},
},
};
}));

View file

@ -0,0 +1,45 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPComposeMail = factory();
}(this, function() {
'use strict';
return {
Tags: {
SendMail: 0x1505,
SmartForward: 0x1506,
SmartReply: 0x1507,
SaveInSentItems: 0x1508,
ReplaceMime: 0x1509,
/* Missing tag value 0x150A */
Source: 0x150B,
FolderId: 0x150C,
ItemId: 0x150D,
LongId: 0x150E,
InstanceId: 0x150F,
Mime: 0x1510,
ClientId: 0x1511,
Status: 0x1512,
AccountId: 0x1513,
}
};
}));

View file

@ -0,0 +1,88 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPContacts = factory();
}(this, function() {
'use strict';
return {
Tags: {
Anniversary: 0x0105,
AssistantName: 0x0106,
AssistantPhoneNumber: 0x0107,
Birthday: 0x0108,
Body: 0x0109,
BodySize: 0x010A,
BodyTruncated: 0x010B,
Business2PhoneNumber: 0x010C,
BusinessAddressCity: 0x010D,
BusinessAddressCountry: 0x010E,
BusinessAddressPostalCode: 0x010F,
BusinessAddressState: 0x0110,
BusinessAddressStreet: 0x0111,
BusinessFaxNumber: 0x0112,
BusinessPhoneNumber: 0x0113,
CarPhoneNumber: 0x0114,
Categories: 0x0115,
Category: 0x0116,
Children: 0x0117,
Child: 0x0118,
CompanyName: 0x0119,
Department: 0x011A,
Email1Address: 0x011B,
Email2Address: 0x011C,
Email3Address: 0x011D,
FileAs: 0x011E,
FirstName: 0x011F,
Home2PhoneNumber: 0x0120,
HomeAddressCity: 0x0121,
HomeAddressCountry: 0x0122,
HomeAddressPostalCode: 0x0123,
HomeAddressState: 0x0124,
HomeAddressStreet: 0x0125,
HomeFaxNumber: 0x0126,
HomePhoneNumber: 0x0127,
JobTitle: 0x0128,
LastName: 0x0129,
MiddleName: 0x012A,
MobilePhoneNumber: 0x012B,
OfficeLocation: 0x012C,
OtherAddressCity: 0x012D,
OtherAddressCountry: 0x012E,
OtherAddressPostalCode: 0x012F,
OtherAddressState: 0x0130,
OtherAddressStreet: 0x0131,
PagerNumber: 0x0132,
RadioPhoneNumber: 0x0133,
Spouse: 0x0134,
Suffix: 0x0135,
Title: 0x0136,
WebPage: 0x0137,
YomiCompanyName: 0x0138,
YomiFirstName: 0x0139,
YomiLastName: 0x013A,
CompressedRTF: 0x013B,
Picture: 0x013C,
Alias: 0x013D,
WeightedRank: 0x013E,
},
};
}));

View file

@ -0,0 +1,40 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPContacts2 = factory();
}(this, function() {
'use strict';
return {
Tags: {
CustomerId: 0x0C05,
GovernmentId: 0x0C06,
IMAddress: 0x0C07,
IMAddress2: 0x0C08,
IMAddress3: 0x0C09,
ManagerName: 0x0C0A,
CompanyMainPhone: 0x0C0B,
AccountName: 0x0C0C,
NickName: 0x0C0D,
MMS: 0x0C0E,
}
};
}));

View file

@ -0,0 +1,38 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPDocumentLibrary = factory();
}(this, function() {
'use strict';
return {
Tags: {
LinkId: 0x1305,
DisplayName: 0x1306,
IsFolder: 0x1307,
CreationDate: 0x1308,
LastModifiedDate: 0x1309,
IsHidden: 0x130A,
ContentLength: 0x130B,
ContentType: 0x130C,
},
};
}));

View file

@ -0,0 +1,128 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPEmail = factory();
}(this, function() {
'use strict';
return {
Tags: {
Attachment: 0x0205,
Attachments: 0x0206,
AttName: 0x0207,
AttSize: 0x0208,
Att0Id: 0x0209,
AttMethod: 0x020A,
AttRemoved: 0x020B,
Body: 0x020C,
BodySize: 0x020D,
BodyTruncated: 0x020E,
DateReceived: 0x020F,
DisplayName: 0x0210,
DisplayTo: 0x0211,
Importance: 0x0212,
MessageClass: 0x0213,
Subject: 0x0214,
Read: 0x0215,
To: 0x0216,
Cc: 0x0217,
From: 0x0218,
ReplyTo: 0x0219,
AllDayEvent: 0x021A,
Categories: 0x021B,
Category: 0x021C,
DTStamp: 0x021D,
EndTime: 0x021E,
InstanceType: 0x021F,
BusyStatus: 0x0220,
Location: 0x0221,
MeetingRequest: 0x0222,
Organizer: 0x0223,
RecurrenceId: 0x0224,
Reminder: 0x0225,
ResponseRequested: 0x0226,
Recurrences: 0x0227,
Recurrence: 0x0228,
Recurrence_Type: 0x0229,
Recurrence_Until: 0x022A,
Recurrence_Occurrences: 0x022B,
Recurrence_Interval: 0x022C,
Recurrence_DayOfWeek: 0x022D,
Recurrence_DayOfMonth: 0x022E,
Recurrence_WeekOfMonth: 0x022F,
Recurrence_MonthOfYear: 0x0230,
StartTime: 0x0231,
Sensitivity: 0x0232,
TimeZone: 0x0233,
GlobalObjId: 0x0234,
ThreadTopic: 0x0235,
MIMEData: 0x0236,
MIMETruncated: 0x0237,
MIMESize: 0x0238,
InternetCPID: 0x0239,
Flag: 0x023A,
Status: 0x023B,
ContentClass: 0x023C,
FlagType: 0x023D,
CompleteTime: 0x023E,
DisallowNewTimeProposal: 0x023F,
},
Enums: {
Importance: {
Low: '0',
Normal: '1',
High: '2',
},
InstanceType: {
Single: '0',
RecurringMaster: '1',
RecurringInstance: '2',
RecurringException: '3',
},
BusyStatus: {
Free: '0',
Tentative: '1',
Busy: '2',
Oof: '3',
},
Recurrence_Type: {
Daily: '0',
Weekly: '1',
MonthlyNthDay: '2',
Monthly: '3',
YearlyNthDay: '5',
YearlyNthDayOfWeek: '6',
},
/* XXX: missing Recurrence_DayOfWeek */
Sensitivity: {
Normal: '0',
Personal: '1',
Private: '2',
Confidential: '3',
},
Status: {
Cleared: '0',
Complete: '1',
Active: '2',
},
},
};
}));

View file

@ -0,0 +1,89 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPEmail2 = factory();
}(this, function() {
'use strict';
return {
Tags: {
UmCallerID: 0x1605,
UmUserNotes: 0x1606,
UmAttDuration: 0x1607,
UmAttOrder: 0x1608,
ConversationId: 0x1609,
ConversationIndex: 0x160A,
LastVerbExecuted: 0x160B,
LastVerbExecutionTime: 0x160C,
ReceivedAsBcc: 0x160D,
Sender: 0x160E,
CalendarType: 0x160F,
IsLeapMonth: 0x1610,
AccountId: 0x1611,
FirstDayOfWeek: 0x1612,
MeetingMessageType: 0x1613,
},
Enums: {
LastVerbExecuted: {
Unknown: '0',
ReplyToSender: '1',
ReplyToAll: '2',
Forward: '3',
},
CalendarType: {
Default: '0',
Gregorian: '1',
GregorianUS: '2',
Japan: '3',
Taiwan: '4',
Korea: '5',
Hijri: '6',
Thai: '7',
Hebrew: '8',
GregorianMeFrench: '9',
GregorianArabic: '10',
GregorianTranslatedEnglish: '11',
GregorianTranslatedFrench: '12',
JapaneseLunar: '14',
ChineseLunar: '15',
KoreanLunar: '20',
},
FirstDayOfWeek: {
Sunday: '0',
Monday: '1',
Tuesday: '2',
Wednesday: '3',
Thursday: '4',
Friday: '5',
Saturday: '6',
},
MeetingMessageType: {
Unspecified: '0',
InitialRequest: '1',
FullUpdate: '2',
InformationalUpdate: '3',
Outdated: '4',
DelegatorsCopy: '5',
Delegated: '6',
}
}
};
}));

View file

@ -0,0 +1,84 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPHierarchy = factory();
}(this, function() {
'use strict';
return {
Tags: {
Folders: 0x0705,
Folder: 0x0706,
DisplayName: 0x0707,
ServerId: 0x0708,
ParentId: 0x0709,
Type: 0x070A,
Response: 0x070B,
Status: 0x070C,
ContentClass: 0x070D,
Changes: 0x070E,
Add: 0x070F,
Delete: 0x0710,
Update: 0x0711,
SyncKey: 0x0712,
FolderCreate: 0x0713,
FolderDelete: 0x0714,
FolderUpdate: 0x0715,
FolderSync: 0x0716,
Count: 0x0717,
},
Enums: {
Type: {
Generic: '1',
DefaultInbox: '2',
DefaultDrafts: '3',
DefaultDeleted: '4',
DefaultSent: '5',
DefaultOutbox: '6',
DefaultTasks: '7',
DefaultCalendar: '8',
DefaultContacts: '9',
DefaultNotes: '10',
DefaultJournal: '11',
Mail: '12',
Calendar: '13',
Contacts: '14',
Tasks: '15',
Journal: '16',
Notes: '17',
Unknown: '18',
RecipientCache: '19',
},
Status: {
Success: '1',
FolderExists: '2',
SystemFolder: '3',
FolderNotFound: '4',
ParentFolderNotFound: '5',
ServerError: '6',
InvalidSyncKey: '9',
MalformedRequest: '10',
UnknownError: '11',
CodeUnknown: '12',
}
}
};
}));

View file

@ -0,0 +1,44 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPGAL = factory();
}(this, function() {
'use strict';
return {
Tags: {
DisplayName: 0x1005,
Phone: 0x1006,
Office: 0x1007,
Title: 0x1008,
Company: 0x1009,
Alias: 0x100A,
FirstName: 0x100B,
LastName: 0x100C,
HomePhone: 0x100D,
MobilePhone: 0x100E,
EmailAddress: 0x100F,
Picture: 0x1010,
Status: 0x1011,
Data: 0x1012,
}
};
}));

View file

@ -0,0 +1,48 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPItemEstimate = factory();
}(this, function() {
'use strict';
return {
Tags: {
GetItemEstimate: 0x0605,
Version: 0x0606,
Collections: 0x0607,
Collection: 0x0608,
Class: 0x0609,
CollectionId: 0x060A,
DateTime: 0x060B,
Estimate: 0x060C,
Response: 0x060D,
Status: 0x060E,
},
Enums: {
Status: {
Success: '1',
InvalidCollection: '2',
NoSyncState: '3',
InvalidSyncKey: '4',
},
},
};
}));

View file

@ -0,0 +1,70 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPItemOperations = factory();
}(this, function() {
'use strict';
return {
Tags: {
ItemOperations: 0x1405,
Fetch: 0x1406,
Store: 0x1407,
Options: 0x1408,
Range: 0x1409,
Total: 0x140A,
Properties: 0x140B,
Data: 0x140C,
Status: 0x140D,
Response: 0x140E,
Version: 0x140F,
Schema: 0x1410,
Part: 0x1411,
EmptyFolderContents: 0x1412,
DeleteSubFolders: 0x1413,
UserName: 0x1414,
Password: 0x1415,
Move: 0x1416,
DstFldId: 0x1417,
ConversationId: 0x1418,
MoveAlways: 0x1419,
},
Enums: {
Status: {
Success: '1',
ProtocolError: '2',
ServerError: '3',
BadURI: '4',
AccessDenied: '5',
ObjectNotFound: '6',
ConnectionFailure: '7',
InvalidByteRange: '8',
UnknownStore: '9',
EmptyFile: '10',
DataTooLarge: '11',
IOFailure: '12',
ConversionFailure: '14',
InvalidAttachment: '15',
ResourceAccessDenied: '16',
},
},
};
}));

View file

@ -0,0 +1,52 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPMeetingResponse = factory();
}(this, function() {
'use strict';
return {
Tags: {
CalendarId: 0x0805,
CollectionId: 0x0806,
MeetingResponse: 0x0807,
RequestId: 0x0808,
Request: 0x0809,
Result: 0x080A,
Status: 0x080B,
UserResponse: 0x080C,
InstanceId: 0x080E,
},
Enums: {
Status: {
Success: '1',
InvalidRequest: '2',
MailboxError: '3',
ServerError: '4',
},
UserResponse: {
Accepted: '1',
Tentative: '2',
Declined: '3',
},
},
};
}));

View file

@ -0,0 +1,48 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPMove = factory();
}(this, function() {
'use strict';
return {
Tags: {
MoveItems: 0x0505,
Move: 0x0506,
SrcMsgId: 0x0507,
SrcFldId: 0x0508,
DstFldId: 0x0509,
Response: 0x050A,
Status: 0x050B,
DstMsgId: 0x050C,
},
Enums: {
Status: {
InvalidSourceID: '1',
InvalidDestID: '2',
Success: '3',
SourceIsDest: '4',
MoveFailure: '5',
ItemLocked: '7',
},
},
};
}));

View file

@ -0,0 +1,35 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPNotes = factory();
}(this, function() {
'use strict';
return {
Tags: {
Subject: 0x1705,
MessageClass: 0x1706,
LastModifiedDate: 0x1707,
Categories: 0x1708,
Category: 0x1709,
}
};
}));

View file

@ -0,0 +1,51 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPPing = factory();
}(this, function() {
'use strict';
return {
Tags: {
Ping: 0x0D05,
AutdState: 0x0D06,
Status: 0x0D07,
HeartbeatInterval: 0x0D08,
Folders: 0x0D09,
Folder: 0x0D0A,
Id: 0x0D0B,
Class: 0x0D0C,
MaxFolders: 0x0D0D,
},
Enums: {
Status: {
Expired: '1',
Changed: '2',
MissingParameters: '3',
SyntaxError: '4',
InvalidInterval: '5',
TooManyFolders: '6',
SyncFolders: '7',
ServerError: '8',
},
},
};
}));

View file

@ -0,0 +1,84 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPProvision = factory();
}(this, function() {
'use strict';
return {
Tags: {
Provision: 0x0E05,
Policies: 0x0E06,
Policy: 0x0E07,
PolicyType: 0x0E08,
PolicyKey: 0x0E09,
Data: 0x0E0A,
Status: 0x0E0B,
RemoteWipe: 0x0E0C,
EASProvisionDoc: 0x0E0D,
DevicePasswordEnabled: 0x0E0E,
AlphanumericDevicePasswordRequired: 0x0E0F,
DeviceEncryptionEnabled: 0x0E10,
RequireStorageCardEncryption: 0x0E10,
PasswordRecoveryEnabled: 0x0E11,
AttachmentsEnabled: 0x0E13,
MinDevicePasswordLength: 0x0E14,
MaxInactivityTimeDeviceLock: 0x0E15,
MaxDevicePasswordFailedAttempts: 0x0E16,
MaxAttachmentSize: 0x0E17,
AllowSimpleDevicePassword: 0x0E18,
DevicePasswordExpiration: 0x0E19,
DevicePasswordHistory: 0x0E1A,
AllowStorageCard: 0x0E1B,
AllowCamera: 0x0E1C,
RequireDeviceEncryption: 0x0E1D,
AllowUnsignedApplications: 0x0E1E,
AllowUnsignedInstallationPackages: 0x0E1F,
MinDevicePasswordComplexCharacters: 0x0E20,
AllowWiFi: 0x0E21,
AllowTextMessaging: 0x0E22,
AllowPOPIMAPEmail: 0x0E23,
AllowBluetooth: 0x0E24,
AllowIrDA: 0x0E25,
RequireManualSyncWhenRoaming: 0x0E26,
AllowDesktopSync: 0x0E27,
MaxCalendarAgeFilter: 0x0E28,
AllowHTMLEmail: 0x0E29,
MaxEmailAgeFilter: 0x0E2A,
MaxEmailBodyTruncationSize: 0x0E2B,
MaxEmailHTMLBodyTruncationSize: 0x0E2C,
RequireSignedSMIMEMessages: 0x0E2D,
RequireEncryptedSMIMEMessages: 0x0E2E,
RequireSignedSMIMEAlgorithm: 0x0E2F,
RequireEncryptionSMIMEAlgorithm: 0x0E30,
AllowSMIMEEncryptionAlgorithmNegotiation: 0x0E31,
AllowSMIMESoftCerts: 0x0E32,
AllowBrowser: 0x0E33,
AllowConsumerEmail: 0x0E34,
AllowRemoteDesktop: 0x0E35,
AllowInternetSharing: 0x0E36,
UnapprovedInROMApplicationList: 0x0E37,
ApplicationName: 0x0E38,
ApprovedApplicationList: 0x0E39,
Hash: 0x0E3A,
}
};
}));

View file

@ -0,0 +1,79 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPResolveRecipients = factory();
}(this, function() {
'use strict';
return {
Tags: {
ResolveRecipients: 0x0A05,
Response: 0x0A06,
Status: 0x0A07,
Type: 0x0A08,
Recipient: 0x0A09,
DisplayName: 0x0A0A,
EmailAddress: 0x0A0B,
Certificates: 0x0A0C,
Certificate: 0x0A0D,
MiniCertificate: 0x0A0E,
Options: 0x0A0F,
To: 0x0A10,
CertificateRetrieval: 0x0A11,
RecipientCount: 0x0A12,
MaxCertificates: 0x0A13,
MaxAmbiguousRecipients: 0x0A14,
CertificateCount: 0x0A15,
Availability: 0x0A16,
StartTime: 0x0A17,
EndTime: 0x0A18,
MergedFreeBusy: 0x0A19,
Picture: 0x0A1A,
MaxSize: 0x0A1B,
Data: 0x0A1C,
MaxPictures: 0x0A1D,
},
Enums: {
Status: {
Success: '1',
AmbiguousRecipientFull: '2',
AmbiguousRecipientPartial: '3',
RecipientNotFound: '4',
ProtocolError: '5',
ServerError: '6',
InvalidSMIMECert: '7',
CertLimitReached: '8',
},
CertificateRetrieval: {
None: '1',
Full: '2',
Mini: '3',
},
MergedFreeBusy: {
Free: '0',
Tentative: '1',
Busy: '2',
Oof: '3',
NoData: '4',
},
},
};
}));

View file

@ -0,0 +1,50 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPRightsManagement = factory();
}(this, function() {
'use strict';
return {
Tags: {
RightsManagementSupport: 0x1805,
RightsManagementTemplates: 0x1806,
RightsManagementTemplate: 0x1807,
RightsManagementLicense: 0x1808,
EditAllowed: 0x1809,
ReplyAllowed: 0x180A,
ReplyAllAllowed: 0x180B,
ForwardAllowed: 0x180C,
ModifyRecipientsAllowed: 0x180D,
ExtractAllowed: 0x180E,
PrintAllowed: 0x180F,
ExportAllowed: 0x1810,
ProgrammaticAccessAllowed: 0x1811,
Owner: 0x1812,
ContentExpiryDate: 0x1813,
TemplateID: 0x1814,
TemplateName: 0x1815,
TemplateDescription: 0x1816,
ContentOwner: 0x1817,
RemoveRightsManagementDistribution: 0x1818,
}
};
}));

View file

@ -0,0 +1,77 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPSearch = factory();
}(this, function() {
'use strict';
return {
Tags: {
Search: 0x0F05,
Stores: 0x0F06,
Store: 0x0F07,
Name: 0x0F08,
Query: 0x0F09,
Options: 0x0F0A,
Range: 0x0F0B,
Status: 0x0F0C,
Response: 0x0F0D,
Result: 0x0F0E,
Properties: 0x0F0F,
Total: 0x0F10,
EqualTo: 0x0F11,
Value: 0x0F12,
And: 0x0F13,
Or: 0x0F14,
FreeText: 0x0F15,
DeepTraversal: 0x0F17,
LongId: 0x0F18,
RebuildResults: 0x0F19,
LessThan: 0x0F1A,
GreaterThan: 0x0F1B,
Schema: 0x0F1C,
Supported: 0x0F1D,
UserName: 0x0F1E,
Password: 0x0F1F,
ConversationId: 0x0F20,
Picture: 0x0F21,
MaxSize: 0x0F22,
MaxPictures: 0x0F23,
},
Enums: {
Status: {
Success: '1',
InvalidRequest: '2',
ServerError: '3',
BadLink: '4',
AccessDenied: '5',
NotFound: '6',
ConnectionFailure: '7',
TooComplex: '8',
Timeout: '10',
SyncFolders: '11',
EndOfRange: '12',
AccessBlocked: '13',
CredentialsRequired: '14',
}
}
};
}));

View file

@ -0,0 +1,85 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPSettings = factory();
}(this, function() {
'use strict';
return {
Tags: {
Settings: 0x1205,
Status: 0x1206,
Get: 0x1207,
Set: 0x1208,
Oof: 0x1209,
OofState: 0x120A,
StartTime: 0x120B,
EndTime: 0x120C,
OofMessage: 0x120D,
AppliesToInternal: 0x120E,
AppliesToExternalKnown: 0x120F,
AppliesToExternalUnknown: 0x1210,
Enabled: 0x1211,
ReplyMessage: 0x1212,
BodyType: 0x1213,
DevicePassword: 0x1214,
Password: 0x1215,
DeviceInformation: 0x1216,
Model: 0x1217,
IMEI: 0x1218,
FriendlyName: 0x1219,
OS: 0x121A,
OSLanguage: 0x121B,
PhoneNumber: 0x121C,
UserInformation: 0x121D,
EmailAddresses: 0x121E,
SmtpAddress: 0x121F,
UserAgent: 0x1220,
EnableOutboundSMS: 0x1221,
MobileOperator: 0x1222,
PrimarySmtpAddress: 0x1223,
Accounts: 0x1224,
Account: 0x1225,
AccountId: 0x1226,
AccountName: 0x1227,
UserDisplayName: 0x1228,
SendDisabled: 0x1229,
/* Missing tag value 0x122A */
RightsManagementInformation: 0x122B,
},
Enums: {
Status: {
Success: '1',
ProtocolError: '2',
AccessDenied: '3',
ServerError: '4',
InvalidArguments: '5',
ConflictingArguments: '6',
DeniedByPolicy: '7',
},
OofState: {
Disabled: '0',
Global: '1',
TimeBased: '2',
}
}
};
}));

View file

@ -0,0 +1,64 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPTasks = factory();
}(this, function() {
'use strict';
return {
Tags: {
Body: 0x0905,
BodySize: 0x0906,
BodyTruncated: 0x0907,
Categories: 0x0908,
Category: 0x0909,
Complete: 0x090A,
DateCompleted: 0x090B,
DueDate: 0x090C,
UtcDueDate: 0x090D,
Importance: 0x090E,
Recurrence: 0x090F,
Recurrence_Type: 0x0910,
Recurrence_Start: 0x0911,
Recurrence_Until: 0x0912,
Recurrence_Occurrences: 0x0913,
Recurrence_Interval: 0x0914,
Recurrence_DayOfMonth: 0x0915,
Recurrence_DayOfWeek: 0x0916,
Recurrence_WeekOfMonth: 0x0917,
Recurrence_MonthOfYear: 0x0918,
Recurrence_Regenerate: 0x0919,
Recurrence_DeadOccur: 0x091A,
ReminderSet: 0x091B,
ReminderTime: 0x091C,
Sensitivity: 0x091D,
StartDate: 0x091E,
UtcStartDate: 0x091F,
Subject: 0x0920,
CompressedRTF: 0x0921,
OrdinalDate: 0x0922,
SubOrdinalDate: 0x0923,
CalendarType: 0x0924,
IsLeapMonth: 0x0925,
FirstDayOfWeek: 0x0926,
}
};
}));

View file

@ -0,0 +1,57 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory();
else if (typeof define === 'function' && define.amd)
define([], factory);
else
root.ASCPValidateCert = factory();
}(this, function() {
'use strict';
return {
Tags: {
ValidateCert: 0x0B05,
Certificates: 0x0B06,
Certificate: 0x0B07,
CertificateChain: 0x0B08,
CheckCRL: 0x0B09,
Status: 0x0B0A,
},
Enums: {
Status: {
Success: '1',
ProtocolError: '2',
InvalidSignature: '3',
UntrustedSource: '4',
InvalidChain: '5',
NotForEmail: '6',
Expired: '7',
InconsistentTimes: '8',
IdMisused: '9',
MissingInformation: '10',
CAEndMismatch: '11',
EmailAddressMismatch: '12',
Revoked: '13',
ServerOffline: '14',
ChainRevoked: '15',
RevocationUnknown: '16',
UnknownError: '17',
},
},
};
}));

View file

@ -0,0 +1,698 @@
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(function (root, factory) {
if (typeof exports === 'object')
module.exports = factory(require('wbxml'), require('activesync/codepages'));
else if (typeof define === 'function' && define.amd)
define(['wbxml', 'activesync/codepages'], factory);
else
root.ActiveSyncProtocol = factory(WBXML, ActiveSyncCodepages);
}(this, function(WBXML, ASCP) {
'use strict';
var exports = {};
var USER_AGENT = 'JavaScript ActiveSync (jsas) Client';
function nullCallback() {}
/**
* Create a constructor for a custom error type that works like a built-in
* Error.
*
* @param name the string name of the error
* @param parent (optional) a parent class for the error, defaults to Error
* @param extraArgs an array of extra arguments that can be passed to the
* constructor of this error type
* @return the constructor for this error
*/
function makeError(name, parent, extraArgs) {
function CustomError() {
// Try to let users call this as CustomError(...) without the "new". This
// is imperfect, and if you call this function directly and give it a
// |this| that's a CustomError, things will break. Don't do it!
var self = this instanceof CustomError ?
this : Object.create(CustomError.prototype);
var tmp = Error();
var offset = 1;
self.stack = tmp.stack.substring(tmp.stack.indexOf('\n') + 1);
self.message = arguments[0] || tmp.message;
if (extraArgs) {
offset += extraArgs.length;
for (var i = 0; i < extraArgs.length; i++)
self[extraArgs[i]] = arguments[i+1];
}
var m = /@(.+):(.+)/.exec(self.stack);
self.fileName = arguments[offset] || (m && m[1]) || "";
self.lineNumber = arguments[offset + 1] || (m && m[2]) || 0;
return self;
}
CustomError.prototype = Object.create((parent || Error).prototype);
CustomError.prototype.name = name;
CustomError.prototype.constructor = CustomError;
return CustomError;
}
var AutodiscoverError = makeError('ActiveSync.AutodiscoverError');
exports.AutodiscoverError = AutodiscoverError;
var AutodiscoverDomainError = makeError('ActiveSync.AutodiscoverDomainError',
AutodiscoverError);
exports.AutodiscoverDomainError = AutodiscoverDomainError;
var HttpError = makeError('ActiveSync.HttpError', null, ['status']);
exports.HttpError = HttpError;
function nsResolver(prefix) {
var baseUrl = 'http://schemas.microsoft.com/exchange/autodiscover/';
var ns = {
rq: baseUrl + 'mobilesync/requestschema/2006',
ad: baseUrl + 'responseschema/2006',
ms: baseUrl + 'mobilesync/responseschema/2006',
};
return ns[prefix] || null;
}
function Version(str) {
var details = str.split('.').map(function(x) {
return parseInt(x);
});
this.major = details[0], this.minor = details[1];
}
exports.Version = Version;
Version.prototype = {
eq: function(other) {
if (!(other instanceof Version))
other = new Version(other);
return this.major === other.major && this.minor === other.minor;
},
ne: function(other) {
return !this.eq(other);
},
gt: function(other) {
if (!(other instanceof Version))
other = new Version(other);
return this.major > other.major ||
(this.major === other.major && this.minor > other.minor);
},
gte: function(other) {
if (!(other instanceof Version))
other = new Version(other);
return this.major >= other.major ||
(this.major === other.major && this.minor >= other.minor);
},
lt: function(other) {
return !this.gte(other);
},
lte: function(other) {
return !this.gt(other);
},
toString: function() {
return this.major + '.' + this.minor;
},
};
/**
* Set the Authorization header on an XMLHttpRequest.
*
* @param xhr the XMLHttpRequest
* @param username the username
* @param password the user's password
*/
function setAuthHeader(xhr, username, password) {
var authorization = 'Basic ' + btoa(username + ':' + password);
xhr.setRequestHeader('Authorization', authorization);
}
/**
* Perform autodiscovery for the server associated with this account.
*
* @param aEmailAddress the user's email address
* @param aPassword the user's password
* @param aTimeout a timeout (in milliseconds) for the request
* @param aCallback a callback taking an error status (if any) and the
* server's configuration
* @param aNoRedirect true if autodiscovery should *not* follow any
* specified redirects (typically used when autodiscover has already
* told us about a redirect)
*/
function autodiscover(aEmailAddress, aPassword, aTimeout, aCallback,
aNoRedirect) {
if (!aCallback) aCallback = nullCallback;
var domain = aEmailAddress.substring(aEmailAddress.indexOf('@') + 1);
// The first time we try autodiscovery, we should try to recover from
// AutodiscoverDomainErrors and HttpErrors. The second time, *all* errors
// should be reported to the callback.
do_autodiscover(domain, aEmailAddress, aPassword, aTimeout, aNoRedirect,
function(aError, aConfig) {
if (aError instanceof AutodiscoverDomainError ||
aError instanceof HttpError)
do_autodiscover('autodiscover.' + domain, aEmailAddress, aPassword,
aTimeout, aNoRedirect, aCallback);
else
aCallback(aError, aConfig);
});
}
exports.autodiscover = autodiscover;
/**
* Perform the actual autodiscovery process for a given URL.
*
* @param aHost the host name to attempt autodiscovery for
* @param aEmailAddress the user's email address
* @param aPassword the user's password
* @param aTimeout a timeout (in milliseconds) for the request
* @param aNoRedirect true if autodiscovery should *not* follow any
* specified redirects (typically used when autodiscover has already
* told us about a redirect)
* @param aCallback a callback taking an error status (if any) and the
* server's configuration
*/
function do_autodiscover(aHost, aEmailAddress, aPassword, aTimeout,
aNoRedirect, aCallback) {
var url = 'https://' + aHost + '/autodiscover/autodiscover.xml';
return raw_autodiscover(url, aEmailAddress, aPassword, aTimeout,
aNoRedirect, aCallback);
}
function raw_autodiscover(aUrl, aEmailAddress, aPassword, aTimeout,
aNoRedirect, aCallback) {
var xhr = new XMLHttpRequest({mozSystem: true, mozAnon: true});
xhr.open('POST', aUrl, true);
setAuthHeader(xhr, aEmailAddress, aPassword);
xhr.setRequestHeader('Content-Type', 'text/xml');
xhr.setRequestHeader('User-Agent', USER_AGENT);
xhr.timeout = aTimeout;
xhr.upload.onprogress = xhr.upload.onload = function() {
xhr.timeout = 0;
};
xhr.onload = function() {
if (xhr.status < 200 || xhr.status >= 300)
return aCallback(new HttpError(xhr.statusText, xhr.status));
var uid = Math.random();
self.postMessage({
uid: uid,
type: 'configparser',
cmd: 'accountactivesync',
args: [xhr.responseText, aNoRedirect]
});
self.addEventListener('message', function onworkerresponse(evt) {
var data = evt.data;
if (data.type != 'configparser' || data.cmd != 'accountactivesync' ||
data.uid != uid) {
return;
}
self.removeEventListener(evt.type, onworkerresponse);
var args = data.args;
var config = args[0], error = args[1], redirectedEmail = args[2];
if (error) {
aCallback(new AutodiscoverDomainError(error), config);
} else if (redirectedEmail) {
autodiscover(redirectedEmail, aPassword, aTimeout, aCallback, true);
} else {
aCallback(null, config);
}
});
};
xhr.ontimeout = xhr.onerror = function() {
// Something bad happened in the network layer, so treat this like an HTTP
// error.
aCallback(new HttpError('Error getting Autodiscover URL', null));
};
// TODO: use something like
// http://ejohn.org/blog/javascript-micro-templating/ here?
var postdata =
'<?xml version="1.0" encoding="utf-8"?>\n' +
'<Autodiscover xmlns="' + nsResolver('rq') + '">\n' +
' <Request>\n' +
' <EMailAddress>' + aEmailAddress + '</EMailAddress>\n' +
' <AcceptableResponseSchema>' + nsResolver('ms') +
'</AcceptableResponseSchema>\n' +
' </Request>\n' +
'</Autodiscover>';
xhr.send(postdata);
}
exports.raw_autodiscover = raw_autodiscover;
/**
* Create a new ActiveSync connection.
*
* ActiveSync connections use XMLHttpRequests to communicate with the
* server. These XHRs are created with mozSystem: true and mozAnon: true to,
* respectively, help with CORS, and to ignore the authentication cache. The
* latter is important because 1) it prevents the HTTP auth dialog from
* appearing if the user's credentials are wrong and 2) it allows us to
* connect to the same server as multiple users.
*
* @param aDeviceId (optional) a string identifying this device
* @param aDeviceType (optional) a string identifying the type of this device
*/
function Connection(aDeviceId, aDeviceType) {
this._deviceId = aDeviceId || 'v140Device';
this._deviceType = aDeviceType || 'SmartPhone';
this.timeout = 0;
this._connected = false;
this._waitingForConnection = false;
this._connectionError = null;
this._connectionCallbacks = [];
this.baseUrl = null;
this._username = null;
this._password = null;
this.versions = [];
this.supportedCommands = [];
this.currentVersion = null;
/**
* Debug support function that is called every time an XHR call completes.
* This is intended to be used for logging.
*
* The arguments to the function are:
*
* - type: 'options' if caused by a call to getOptions. 'post' if caused by
* a call to postCommand/postData.
*
* - special: 'timeout' if a timeout error occurred, 'redirect' if the
* status code was 451 and the call is being reissued, 'error' if some
* type of error occurred, or 'ok' on success. Check xhr.status for the
* specific http status code.
*
* - xhr: The XMLHttpRequest used. Use this to check the statusCode,
* statusText, or response headers.
*
* - params: The object dictionary of parameters encoded into the URL.
* Always present if type is 'post', not present for 'options'.
*
* - extraHeaders: Optional dictionary of extra request headers that were
* provided. These will not include the always-present request headers of
* MS-ASProtocolVersion and Content-Type.
*
* - sent data: If type is 'post', the ArrayBuffer provided to xhr.send().
*
* - response: In the case of a successful 'post', the WBXML Reader instance
* that will be passed to the callback for the method. If you use the
* reader, you are responsible for calling rewind() on it.
*/
this.onmessage = null;
}
exports.Connection = Connection;
Connection.prototype = {
/**
* Perform any callbacks added during the connection process.
*
* @param aError the error status (if any)
*/
_notifyConnected: function(aError) {
if (aError)
this.disconnect();
for (var iter in Iterator(this._connectionCallbacks)) {
var callback = iter[1];
callback.apply(callback, arguments);
}
this._connectionCallbacks = [];
},
/**
* Get the connection status.
*
* @return true iff we are fully connected to the server
*/
get connected() {
return this._connected;
},
/*
* Initialize the connection with a server and account credentials.
*
* @param aURL the ActiveSync URL to connect to
* @param aUsername the account's username
* @param aPassword the account's password
*/
open: function(aURL, aUsername, aPassword) {
// XXX: We add the default service path to the URL if it's not already
// there. This is a hack to work around the failings of Hotmail (and
// possibly other servers), which doesn't provide the service path in its
// URL. If it turns out this causes issues with other domains, remove it.
var servicePath = '/Microsoft-Server-ActiveSync';
this.baseUrl = aURL;
if (!this.baseUrl.endsWith(servicePath))
this.baseUrl += servicePath;
this._username = aUsername;
this._password = aPassword;
},
/**
* Connect to the server with this account by getting the OPTIONS from
* the server (and verifying the account's credentials).
*
* @param aCallback a callback taking an error status (if any) and the
* server's options.
*/
connect: function(aCallback) {
// If we're already connected, just run the callback and return.
if (this.connected) {
if (aCallback)
aCallback(null);
return;
}
// Otherwise, queue this callback up to fire when we do connect.
if (aCallback)
this._connectionCallbacks.push(aCallback);
// Don't do anything else if we're already trying to connect.
if (this._waitingForConnection)
return;
this._waitingForConnection = true;
this._connectionError = null;
this.getOptions((function(aError, aOptions) {
this._waitingForConnection = false;
this._connectionError = aError;
if (aError) {
console.error('Error connecting to ActiveSync:', aError);
return this._notifyConnected(aError, aOptions);
}
this._connected = true;
this.versions = aOptions.versions;
this.supportedCommands = aOptions.commands;
this.currentVersion = new Version(aOptions.versions.slice(-1)[0]);
return this._notifyConnected(null, aOptions);
}).bind(this));
},
/**
* Disconnect from the ActiveSync server, and reset the connection state.
* The server and credentials remain set however, so you can safely call
* connect() again immediately after.
*/
disconnect: function() {
if (this._waitingForConnection)
throw new Error("Can't disconnect while waiting for server response");
this._connected = false;
this.versions = [];
this.supportedCommands = [];
this.currentVersion = null;
},
/**
* Attempt to provision this account. XXX: Currently, this doesn't actually
* do anything, but it's useful as a test command for Gmail to ensure that
* the user entered their password correctly.
*
* @param aCallback a callback taking an error status (if any) and the
* WBXML response
*/
provision: function(aCallback) {
var pv = ASCP.Provision.Tags;
var w = new WBXML.Writer('1.3', 1, 'UTF-8');
w.stag(pv.Provision)
.etag();
this.postCommand(w, aCallback);
},
/**
* Get the options for the server associated with this account.
*
* @param aCallback a callback taking an error status (if any), and the
* resulting options.
*/
getOptions: function(aCallback) {
if (!aCallback) aCallback = nullCallback;
var conn = this;
var xhr = new XMLHttpRequest({mozSystem: true, mozAnon: true});
xhr.open('OPTIONS', this.baseUrl, true);
setAuthHeader(xhr, this._username, this._password);
xhr.setRequestHeader('User-Agent', USER_AGENT);
xhr.timeout = this.timeout;
xhr.upload.onprogress = xhr.upload.onload = function() {
xhr.timeout = 0;
};
xhr.onload = function() {
if (xhr.status < 200 || xhr.status >= 300) {
console.error('ActiveSync options request failed with response ' +
xhr.status);
if (conn.onmessage)
conn.onmessage('options', 'error', xhr, null, null, null, null);
aCallback(new HttpError(xhr.statusText, xhr.status));
return;
}
// These headers are comma-separated lists. Sometimes, people like to
// put spaces after the commas, so make sure we trim whitespace too.
var result = {
versions: xhr.getResponseHeader('MS-ASProtocolVersions')
.split(/\s*,\s*/),
commands: xhr.getResponseHeader('MS-ASProtocolCommands')
.split(/\s*,\s*/)
};
if (conn.onmessage)
conn.onmessage('options', 'ok', xhr, null, null, null, result);
aCallback(null, result);
};
xhr.ontimeout = xhr.onerror = function() {
var error = new Error('Error getting OPTIONS URL');
console.error(error);
if (conn.onmessage)
conn.onmessage('options', 'timeout', xhr, null, null, null, null);
aCallback(error);
};
// Set the response type to "text" so that we don't try to parse an empty
// body as XML.
xhr.responseType = 'text';
xhr.send();
},
/**
* Check if the server supports a particular command. Requires that we be
* connected to the server already.
*
* @param aCommand a string/tag representing the command type
* @return true iff the command is supported
*/
supportsCommand: function(aCommand) {
if (!this.connected)
throw new Error('Connection required to get command');
if (typeof aCommand === 'number')
aCommand = ASCP.__tagnames__[aCommand];
return this.supportedCommands.indexOf(aCommand) !== -1;
},
/**
* DEPRECATED. See postCommand() below.
*/
doCommand: function() {
console.warn('doCommand is deprecated. Use postCommand instead.');
this.postCommand.apply(this, arguments);
},
/**
* Send a WBXML command to the ActiveSync server and listen for the
* response.
*
* @param aCommand {WBXML.Writer|String|Number}
* The WBXML representing the command or a string/tag representing the
* command type for empty commands
* @param aCallback a callback to call when the server has responded; takes
* two arguments: an error status (if any) and the response as a
* WBXML reader. If the server returned an empty response, the
* response argument is null.
* @param aExtraParams (optional) an object containing any extra URL
* parameters that should be added to the end of the request URL
* @param aExtraHeaders (optional) an object containing any extra HTTP
* headers to send in the request
* @param aProgressCallback (optional) a callback to invoke with progress
* information, when available. Two arguments are provided: the
* number of bytes received so far, and the total number of bytes
* expected (when known, 0 if unknown).
*/
postCommand: function(aCommand, aCallback, aExtraParams, aExtraHeaders,
aProgressCallback) {
var contentType = 'application/vnd.ms-sync.wbxml';
if (typeof aCommand === 'string' || typeof aCommand === 'number') {
this.postData(aCommand, contentType, null, aCallback, aExtraParams,
aExtraHeaders);
}
// WBXML.Writer
else {
var commandName = ASCP.__tagnames__[aCommand.rootTag];
this.postData(
commandName, contentType,
aCommand.dataType === 'blob' ? aCommand.blob : aCommand.buffer,
aCallback, aExtraParams, aExtraHeaders, aProgressCallback);
}
},
/**
* Send arbitrary data to the ActiveSync server and listen for the response.
*
* @param aCommand a string (or WBXML tag) representing the command type
* @param aContentType the content type of the post data
* @param aData {ArrayBuffer|Blob} the data to be posted
* @param aCallback a callback to call when the server has responded; takes
* two arguments: an error status (if any) and the response as a
* WBXML reader. If the server returned an empty response, the
* response argument is null.
* @param aExtraParams (optional) an object containing any extra URL
* parameters that should be added to the end of the request URL
* @param aExtraHeaders (optional) an object containing any extra HTTP
* headers to send in the request
* @param aProgressCallback (optional) a callback to invoke with progress
* information, when available. Two arguments are provided: the
* number of bytes received so far, and the total number of bytes
* expected (when known, 0 if unknown).
*/
postData: function(aCommand, aContentType, aData, aCallback, aExtraParams,
aExtraHeaders, aProgressCallback) {
// Make sure our command name is a string.
if (typeof aCommand === 'number')
aCommand = ASCP.__tagnames__[aCommand];
if (!this.supportsCommand(aCommand)) {
var error = new Error("This server doesn't support the command " +
aCommand);
console.error(error);
aCallback(error);
return;
}
// Build the URL parameters.
var params = [
['Cmd', aCommand],
['User', this._username],
['DeviceId', this._deviceId],
['DeviceType', this._deviceType]
];
if (aExtraParams) {
for (var iter in Iterator(params)) {
var param = iter[1];
if (param[0] in aExtraParams)
throw new TypeError('reserved URL parameter found');
}
for (var kv in Iterator(aExtraParams))
params.push(kv);
}
var paramsStr = params.map(function(i) {
return encodeURIComponent(i[0]) + '=' + encodeURIComponent(i[1]);
}).join('&');
// Now it's time to make our request!
var xhr = new XMLHttpRequest({mozSystem: true, mozAnon: true});
xhr.open('POST', this.baseUrl + '?' + paramsStr, true);
setAuthHeader(xhr, this._username, this._password);
xhr.setRequestHeader('MS-ASProtocolVersion', this.currentVersion);
xhr.setRequestHeader('Content-Type', aContentType);
xhr.setRequestHeader('User-Agent', USER_AGENT);
// Add extra headers if we have any.
if (aExtraHeaders) {
for (var iter in Iterator(aExtraHeaders)) {
var key = iter[0], key = iter[1];
xhr.setRequestHeader(key, value);
}
}
xhr.timeout = this.timeout;
xhr.upload.onprogress = xhr.upload.onload = function() {
xhr.timeout = 0;
};
xhr.onprogress = function(event) {
if (aProgressCallback)
aProgressCallback(event.loaded, event.total);
};
var conn = this;
var parentArgs = arguments;
xhr.onload = function() {
// This status code is a proprietary Microsoft extension used to
// indicate a redirect, not to be confused with the draft-standard
// "Unavailable For Legal Reasons" status. More info available here:
// <http://msdn.microsoft.com/en-us/library/gg651019.aspx>
if (xhr.status === 451) {
conn.baseUrl = xhr.getResponseHeader('X-MS-Location');
if (conn.onmessage)
conn.onmessage(aCommand, 'redirect', xhr, params, aExtraHeaders,
aData, null);
conn.postData.apply(conn, parentArgs);
return;
}
if (xhr.status < 200 || xhr.status >= 300) {
console.error('ActiveSync command ' + aCommand + ' failed with ' +
'response ' + xhr.status);
if (conn.onmessage)
conn.onmessage(aCommand, 'error', xhr, params, aExtraHeaders,
aData, null);
aCallback(new HttpError(xhr.statusText, xhr.status));
return;
}
var response = null;
if (xhr.response.byteLength > 0)
response = new WBXML.Reader(new Uint8Array(xhr.response), ASCP);
if (conn.onmessage)
conn.onmessage(aCommand, 'ok', xhr, params, aExtraHeaders,
aData, response);
aCallback(null, response);
};
xhr.ontimeout = xhr.onerror = function(evt) {
var error = new Error('Command URL ' + evt.type + ' for command ' +
aCommand + ' at baseUrl ' + this.baseUrl);
console.error(error);
if (conn.onmessage)
conn.onmessage(aCommand, evt.type, xhr, params, aExtraHeaders,
aData, null);
aCallback(error);
}.bind(this);
xhr.responseType = 'arraybuffer';
xhr.send(aData);
},
};
return exports;
}));

View file

@ -0,0 +1,101 @@
/**
* Simple Blob and FileReader shim that does enough of an impression of the W3C
* File API that our unit tests can pass.
*
* Key differences:
* - Blob does not snapshot mutable TypedArray or ArrayBuffer instances passed
* in.
* - Blob surfaces helper logic/properties with underscore-prefixed names.
* - FileReader is not fully implemented and its events are bare bones.
**/
function Blob(parts, properties) {
this._parts = parts;
var size = 0;
parts.forEach(function(part) {
if (part instanceof Blob)
size += part.size;
else if (part instanceof ArrayBuffer)
size += part.byteLength;
else
size += part.length;
});
this.size = size;
this.type = properties ? properties.type : null;
};
exports.Blob = Blob;
Blob.prototype = {
_asArrayBuffer: function() {
var buffer = new ArrayBuffer(this.size);
var u8 = new Uint8Array(buffer);
var offset = 0;
this._parts.forEach(function(part) {
var sub8;
if (part instanceof Blob) {
var subarr = part._asArrayBuffer();
sub8 = new Uint8Array(subarr);
u8.set(sub8, offset);
offset += sub8.length;
}
else if (typeof(part) === 'string') {
var encoder = new TextEncoder('utf-8');
sub8 = encoder.encode(part);
u8.set(sub8, offset);
offset += sub8.length;
}
else if (part instanceof ArrayBuffer) {
sub8 = new Uint8Array(part);
u8.set(sub8, offset);
offset += sub8.length;
}
else {
u8.set(part, offset);
offset += part.length;
}
});
return buffer;
}
};
function FileReader() {
this.error = null;
this.readyState = this.EMPTY;
this.result = null;
this.onabort = null;
this.onerror = null;
this.onload = null;
this.onloadend = null;
this.onloadstart = null;
this.onprogress = null;
}
exports.FileReader = FileReader;
FileReader.prototype = {
EMPTY: 0,
LOADING: 1,
DONE: 2,
readAsArrayBuffer: function(blob) {
process.nextTick(function() {
var event = { target: this };
if (this.onload)
this.onload(event);
if (this.onloadend)
this.onloadend(event);
}.bind(this));
this.result = blob._asArrayBuffer();
},
readAsBinaryString: function(blob) {
throw new Error('not implemented');
},
readAsDataURL: function(blob) {
throw new Error('not implemented');
},
readAsText: function(blob, encoding) {
throw new Error('not implemented');
},
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,325 @@
// Copyright (c) 2013 Andris Reinman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(root, factory) {
"use strict";
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.addressparser = factory();
}
}(this, function() {
"use strict";
/**
* Defines an object as a namespace for the parsing function
*/
var addressparser = {};
/**
* Parses structured e-mail addresses from an address field
*
* Example:
*
* "Name <address@domain>"
*
* will be converted to
*
* [{name: "Name", address: "address@domain"}]
*
* @param {String} str Address field
* @return {Array} An array of address objects
*/
addressparser.parse = function(str) {
var tokenizer = new addressparser.Tokenizer(str),
tokens = tokenizer.tokenize();
var addresses = [],
address = [],
parsedAddresses = [];
tokens.forEach(function(token) {
if (token.type === "operator" && (token.value === "," || token.value === ";")) {
if (address.length) {
addresses.push(address);
}
address = [];
} else {
address.push(token);
}
});
if (address.length) {
addresses.push(address);
}
addresses.forEach(function(address) {
address = addressparser._handleAddress(address);
if (address.length) {
parsedAddresses = parsedAddresses.concat(address);
}
});
return parsedAddresses;
};
/**
* Converts tokens for a single address into an address object
*
* @param {Array} tokens Tokens object
* @return {Object} Address object
*/
addressparser._handleAddress = function(tokens) {
var token,
isGroup = false,
state = "text",
address,
addresses = [],
data = {
address: [],
comment: [],
group: [],
text: []
},
i, len;
// Filter out <addresses>, (comments) and regular text
for (i = 0, len = tokens.length; i < len; i++) {
token = tokens[i];
if (token.type === "operator") {
switch (token.value) {
case "<":
state = "address";
break;
case "(":
state = "comment";
break;
case ":":
state = "group";
isGroup = true;
break;
default:
state = "text";
}
} else {
if (token.value) {
data[state].push(token.value);
}
}
}
// If there is no text but a comment, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
if (isGroup) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
data.text = data.text.join(" ");
addresses.push({
name: data.text || (address && address.name),
group: data.group.length ? addressparser.parse(data.group.join(",")) : []
});
} else {
// If no address was found, try to detect one from regular text
if (!data.address.length && data.text.length) {
for (i = data.text.length - 1; i >= 0; i--) {
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.address = data.text.splice(i, 1);
break;
}
}
var _regexHandler = function(address) {
if (!data.address.length) {
data.address = [address.trim()];
return " ";
} else {
return address;
}
};
// still no address
if (!data.address.length) {
for (i = data.text.length - 1; i >= 0; i--) {
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^@\s]+\b\s*/, _regexHandler).trim();
if (data.address.length) {
break;
}
}
}
}
// If there's still is no text but a comment exixts, replace the two
if (!data.text.length && data.comment.length) {
data.text = data.comment;
data.comment = [];
}
// Keep only the first address occurence, push others to regular text
if (data.address.length > 1) {
data.text = data.text.concat(data.address.splice(1));
}
// Join values with spaces
data.text = data.text.join(" ");
data.address = data.address.join(" ");
if (!data.address && isGroup) {
return [];
} else {
address = {
address: data.address || data.text || "",
name: data.text || data.address || ""
};
if (address.address === address.name) {
if ((address.address || "").match(/@/)) {
address.name = "";
} else {
address.address = "";
}
}
addresses.push(address);
}
}
return addresses;
};
/**
* Creates a Tokenizer object for tokenizing address field strings
*
* @constructor
* @param {String} str Address field string
*/
addressparser.Tokenizer = function(str) {
this.str = (str || "").toString();
this.operatorCurrent = "";
this.operatorExpecting = "";
this.node = null;
this.escaped = false;
this.list = [];
};
/**
* Operator tokens and which tokens are expected to end the sequence
*/
addressparser.Tokenizer.prototype.operators = {
"\"": "\"",
"(": ")",
"<": ">",
",": "",
// Groups are ended by semicolons
":": ";",
// Semicolons are not a legal delimiter per the RFC2822 grammar other
// than for terminating a group, but they are also not valid for any
// other use in this context. Given that some mail clients have
// historically allowed the semicolon as a delimiter equivalent to the
// comma in their UI, it makes sense to treat them the same as a comma
// when used outside of a group.
";": ""
};
/**
* Tokenizes the original input string
*
* @return {Array} An array of operator|text tokens
*/
addressparser.Tokenizer.prototype.tokenize = function() {
var chr, list = [];
for (var i = 0, len = this.str.length; i < len; i++) {
chr = this.str.charAt(i);
this.checkChar(chr);
}
this.list.forEach(function(node) {
node.value = (node.value || "").toString().trim();
if (node.value) {
list.push(node);
}
});
return list;
};
/**
* Checks if a character is an operator or text and acts accordingly
*
* @param {String} chr Character from the address field
*/
addressparser.Tokenizer.prototype.checkChar = function(chr) {
if ((chr in this.operators || chr === "\\") && this.escaped) {
this.escaped = false;
} else if (this.operatorExpecting && chr === this.operatorExpecting) {
this.node = {
type: "operator",
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = "";
this.escaped = false;
return;
} else if (!this.operatorExpecting && chr in this.operators) {
this.node = {
type: "operator",
value: chr
};
this.list.push(this.node);
this.node = null;
this.operatorExpecting = this.operators[chr];
this.escaped = false;
return;
}
if (!this.escaped && chr === "\\") {
this.escaped = true;
return;
}
if (!this.node) {
this.node = {
type: "text",
value: ""
};
this.list.push(this.node);
}
if (this.escaped && chr !== "\\") {
this.node.value += "\\";
}
this.node.value += chr;
this.escaped = false;
};
return addressparser;
}));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,13 @@
/**
* Shim for 'axe-logger' as required by the email.js libs.
*/
define(function() {
// For now, silence debug/log, and forward warn/error to the
// console just in case.
return {
debug: function() { /* shh */ },
log: function() { /* shh */ },
warn: console.warn.bind(console),
error: console.error.bind(console)
};
});

View file

@ -0,0 +1,8 @@
/**
* Alternate-named shim for 'axe-logger', referenced in third-party
* dependencies. This could alternately just be included within our
* RequireJS configuration.
*/
define(['axe-logger'], function(axe) {
return axe;
});

View file

@ -0,0 +1,36 @@
/**
* Customized shim for browserbox to use 'slog' with configurable logging level
* that can be cranked up.
*/
define(function(require) {
var logic = require('logic');
var scope = logic.scope('BrowserBox');
return {
/**
* Provide a .debug for things that are *only* logged when
* sensitive logging is enabled. This exists right now mainly for
* the benefit of the email.js libs. We're tying "debug" to
* logSensitiveData both because we haven't audited the use of
* debug and also because it is indeed a bit chatty.
*
* TODO: Address the logging detail level as a separate issue,
* ideally while working with whiteout.io to fancify the email.js
* logging slightly.
*/
debug: function(ignoredTag, msg) {
if (!logic.isCensored) {
logic(scope, 'debug', { msg: msg });
}
},
log: function(ignoredTag, msg) {
logic(scope, 'log', { msg: msg });
},
warn: function(ignoredTag, msg) {
logic(scope, 'warn', { msg: msg });
},
error: function(ignoredTag, msg) {
logic(scope, 'error', { msg: msg });
}
};
});

View file

@ -0,0 +1,26 @@
/**
* Customized shim for browserbox to use 'slog' with configurable logging level
* that can be cranked up.
*/
define(function(require) {
var logic = require('logic');
var scope = logic.scope('SmtpClient');
return {
// see axeshim-browserbox's comment about '.debug'
debug: function(ignoredTag, msg) {
if (!logic.isCensored) {
logic(scope, 'debug', { msg: msg });
}
},
log: function(ignoredTag, msg) {
logic(scope, 'log', { msg: msg });
},
warn: function(ignoredTag, msg) {
logic(scope, 'warn', { msg: msg });
},
error: function(ignoredTag, msg) {
logic(scope, 'error', { msg: msg });
}
};
});

View file

@ -0,0 +1,10 @@
// for node, require jsdom bindings, and pass it in as the document constructor
var jsdom = require('jsdom');
module.exports = require('./lib/bleach.js');
module.exports.documentConstructor = jsdom.jsdom;
module.exports._preCleanNodeHack = function(node, html) {
// workaround for jsdom comment bug
if (node.innerHTML === '' && html.match(/<!--/)) {
node.innerHTML = (html + '-->');
}
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,380 @@
(function (root, factory) {
// Universal Module Definition (UMD) to support AMD, CommonJS/Node.js,
// Rhino, and plain browser loading.
if (typeof define === 'function' && define.amd) {
define(['require', 'exports'], factory);
} else if (typeof exports !== 'undefined') {
factory(require, exports);
} else {
factory(root);
}
}(this, function (require, exports) {
var tokenizer = require('./tokenizer');
function parse(tokens, initialMode) {
var mode = initialMode || 'top-level';
var i = -1;
var token;
var stylesheet;
switch (mode) {
case 'top-level':
stylesheet = new Stylesheet;
break;
// (Used for style attributes; start out parsing declarations which
// means that our container must be a StyleRule.)
case 'declaration':
stylesheet = new StyleRule;
break;
}
stylesheet.startTok = tokens[0];
//console.log(' initial tok', JSON.stringify(tokens[0]), JSON.stringify(stylesheet));
var stack = [stylesheet];
var rule = stack[0];
var consume = function(advance) {
if(advance === undefined) advance = 1;
i += advance;
if(i < tokens.length)
token = tokens[i];
else
token = new EOFToken;
return true;
};
var reprocess = function() {
i--;
return true;
}
var next = function() {
return tokens[i+1];
};
var switchto = function(newmode) {
if(newmode === undefined) {
if(rule.fillType !== '')
mode = rule.fillType;
else if(rule.type == 'STYLESHEET')
mode = 'top-level'
// mozmod: disable console.log
else { /* console.log("Unknown rule-type while switching to current rule's content mode: ",rule); mode = ''; */ }
} else {
mode = newmode;
}
return true;
}
var push = function(newRule) {
rule = newRule;
rule.startTok = token;
//console.log(' startTok', JSON.stringify(token), JSON.stringify(rule));
stack.push(rule);
return true;
}
var parseerror = function(msg) {
// mozmod: disable console.log
//console.log("Parse error at token " + i + ": " + token + ".\n" + msg);
return true;
}
var pop = function() {
var oldrule = stack.pop();
oldrule.endTok = token;
//console.log(' endTok', JSON.stringify(token), JSON.stringify(oldrule));
rule = stack[stack.length - 1];
rule.append(oldrule);
return true;
}
var discard = function() {
stack.pop();
rule = stack[stack.length - 1];
return true;
}
var finish = function() {
while(stack.length > 1) {
pop();
}
rule.endTok = token;
//console.log(' endTok', JSON.stringify(token), JSON.stringify(rule));
}
for(;;) {
consume();
switch(mode) {
case "top-level":
switch(token.tokenType) {
case "CDO":
case "CDC":
case "WHITESPACE": break;
case "AT-KEYWORD": push(new AtRule(token.value)) && switchto('at-rule'); break;
case "{": parseerror("Attempt to open a curly-block at top-level.") && consumeAPrimitive(); break;
case "EOF": finish(); return stylesheet;
default: push(new StyleRule) && switchto('selector') && reprocess();
}
break;
case "at-rule":
switch(token.tokenType) {
case ";": pop() && switchto(); break;
case "{":
if(rule.fillType !== '') switchto(rule.fillType);
else parseerror("Attempt to open a curly-block in a statement-type at-rule.") && discard() && switchto('next-block') && reprocess();
break;
case "EOF": finish(); return stylesheet;
default: rule.appendPrelude(consumeAPrimitive());
}
break;
case "rule":
switch(token.tokenType) {
case "WHITESPACE": break;
case "}": pop() && switchto(); break;
case "AT-KEYWORD": push(new AtRule(token.value)) && switchto('at-rule'); break;
case "EOF": finish(); return stylesheet;
default: push(new StyleRule) && switchto('selector') && reprocess();
}
break;
case "selector":
switch(token.tokenType) {
case "{": switchto('declaration'); break;
case "EOF": discard() && finish(); return stylesheet;
default: rule.appendSelector(consumeAPrimitive());
}
break;
case "declaration":
switch(token.tokenType) {
case "WHITESPACE":
case ";": break;
case "}": pop() && switchto(); break;
case "AT-RULE": push(new AtRule(token.value)) && switchto('at-rule'); break;
case "IDENT": push(new Declaration(token.value)) && switchto('after-declaration-name'); break;
case "EOF": finish(); return stylesheet;
default: parseerror() && discard() && switchto('next-declaration');
}
break;
case "after-declaration-name":
switch(token.tokenType) {
case "WHITESPACE": break;
case ":": switchto('declaration-value'); break;
case ";": parseerror("Incomplete declaration - semicolon after property name.") && discard() && switchto(); break;
case "EOF": discard() && finish(); return stylesheet;
default: parseerror("Invalid declaration - additional token after property name") && discard() && switchto('next-declaration');
}
break;
case "declaration-value":
switch(token.tokenType) {
case "DELIM":
if(token.value == "!" && next().tokenType == 'IDENTIFIER' && next().value.toLowerCase() == "important") {
consume();
rule.important = true;
switchto('declaration-end');
} else {
rule.append(token);
}
break;
case ";": pop() && switchto(); break;
case "}": pop() && pop() && switchto(); break;
case "EOF": finish(); return stylesheet;
default: rule.append(consumeAPrimitive());
}
break;
case "declaration-end":
switch(token.tokenType) {
case "WHITESPACE": break;
case ";": pop() && switchto(); break;
case "}": pop() && pop() && switchto(); break;
case "EOF": finish(); return stylesheet;
default: parseerror("Invalid declaration - additional token after !important.") && discard() && switchto('next-declaration');
}
break;
case "next-block":
switch(token.tokenType) {
case "{": consumeAPrimitive() && switchto(); break;
case "EOF": finish(); return stylesheet;
default: consumeAPrimitive(); break;
}
break;
case "next-declaration":
switch(token.tokenType) {
case ";": switchto('declaration'); break;
case "}": switchto('declaration') && reprocess(); break;
case "EOF": finish(); return stylesheet;
default: consumeAPrimitive(); break;
}
break;
default:
// If you hit this, it's because one of the switchto() calls is typo'd.
// mozmod: disable console.log
//console.log('Unknown parsing mode: ' + mode);
return;
}
}
function consumeAPrimitive() {
switch(token.tokenType) {
case "(":
case "[":
case "{": return consumeASimpleBlock();
case "FUNCTION": return consumeAFunc();
default: return token;
}
}
function consumeASimpleBlock() {
var endingTokenType = {"(":")", "[":"]", "{":"}"}[token.tokenType];
var block = new SimpleBlock(token.tokenType);
for(;;) {
consume();
switch(token.tokenType) {
case "EOF":
case endingTokenType: return block;
default: block.append(consumeAPrimitive());
}
}
}
function consumeAFunc() {
var func = new Func(token.value);
var arg = new FuncArg();
for(;;) {
consume();
switch(token.tokenType) {
case "EOF":
case ")": func.append(arg); return func;
case "DELIM":
if(token.value == ",") {
func.append(arg);
arg = new FuncArg();
} else {
arg.append(token);
}
break;
default: arg.append(consumeAPrimitive());
}
}
}
}
function CSSParserRule() { return this; }
CSSParserRule.prototype.fillType = '';
CSSParserRule.prototype.toString = function(indent) {
return JSON.stringify(this.toJSON(),null,indent);
}
CSSParserRule.prototype.append = function(val) {
this.value.push(val);
return this;
}
function Stylesheet() {
this.value = [];
return this;
}
Stylesheet.prototype = new CSSParserRule;
Stylesheet.prototype.type = "STYLESHEET";
Stylesheet.prototype.toJSON = function() {
return {type:'stylesheet', value: this.value.map(function(e){return e.toJSON();})};
}
function AtRule(name) {
this.name = name;
this.prelude = [];
this.value = [];
if(name in AtRule.registry)
this.fillType = AtRule.registry[name];
return this;
}
AtRule.prototype = new CSSParserRule;
AtRule.prototype.type = "AT-RULE";
AtRule.prototype.appendPrelude = function(val) {
this.prelude.push(val);
return this;
}
AtRule.prototype.toJSON = function() {
return {type:'at', name:this.name, prelude:this.prelude.map(function(e){return e.toJSON();}), value:this.value.map(function(e){return e.toJSON();})};
}
AtRule.registry = {
'import': '',
'media': 'rule',
'font-face': 'declaration',
'page': 'declaration',
'keyframes': 'rule',
'namespace': '',
'counter-style': 'declaration',
'supports': 'rule',
'document': 'rule',
'font-feature-values': 'declaration',
'viewport': '',
'region-style': 'rule'
};
function StyleRule() {
this.selector = [];
this.value = [];
return this;
}
StyleRule.prototype = new CSSParserRule;
StyleRule.prototype.type = "STYLE-RULE";
StyleRule.prototype.fillType = 'declaration';
StyleRule.prototype.appendSelector = function(val) {
this.selector.push(val);
return this;
}
StyleRule.prototype.toJSON = function() {
return {type:'selector', selector:this.selector.map(function(e){return e.toJSON();}), value:this.value.map(function(e){return e.toJSON();})};
}
function Declaration(name) {
this.name = name;
this.value = [];
return this;
}
Declaration.prototype = new CSSParserRule;
Declaration.prototype.type = "DECLARATION";
Declaration.prototype.toJSON = function() {
return {type:'declaration', name:this.name, value:this.value.map(function(e){return e.toJSON();})};
}
function SimpleBlock(type) {
this.name = type;
this.value = [];
return this;
}
SimpleBlock.prototype = new CSSParserRule;
SimpleBlock.prototype.type = "BLOCK";
SimpleBlock.prototype.toJSON = function() {
return {type:'block', name:this.name, value:this.value.map(function(e){return e.toJSON();})};
}
function Func(name) {
this.name = name;
this.value = [];
return this;
}
Func.prototype = new CSSParserRule;
Func.prototype.type = "FUNCTION";
Func.prototype.toJSON = function() {
return {type:'func', name:this.name, value:this.value.map(function(e){return e.toJSON();})};
}
function FuncArg() {
this.value = [];
return this;
}
FuncArg.prototype = new CSSParserRule;
FuncArg.prototype.type = "FUNCTION-ARG";
FuncArg.prototype.toJSON = function() {
return this.value.map(function(e){return e.toJSON();});
}
// Exportation.
// TODO: also export the various rule objects?
exports.parse = parse;
}));

View file

@ -0,0 +1,739 @@
(function (root, factory) {
// Universal Module Definition (UMD) to support AMD, CommonJS/Node.js,
// Rhino, and plain browser loading.
if (typeof define === 'function' && define.amd) {
define(['exports'], factory);
} else if (typeof exports !== 'undefined') {
factory(exports);
} else {
factory(root);
}
}(this, function (exports) {
var between = function (num, first, last) { return num >= first && num <= last; }
function digit(code) { return between(code, 0x30,0x39); }
function hexdigit(code) { return digit(code) || between(code, 0x41,0x46) || between(code, 0x61,0x66); }
function uppercaseletter(code) { return between(code, 0x41,0x5a); }
function lowercaseletter(code) { return between(code, 0x61,0x7a); }
function letter(code) { return uppercaseletter(code) || lowercaseletter(code); }
function nonascii(code) { return code >= 0xa0; }
function namestartchar(code) { return letter(code) || nonascii(code) || code == 0x5f; }
function namechar(code) { return namestartchar(code) || digit(code) || code == 0x2d; }
function nonprintable(code) { return between(code, 0,8) || between(code, 0xe,0x1f) || between(code, 0x7f,0x9f); }
function newline(code) { return code == 0xa || code == 0xc; }
function whitespace(code) { return newline(code) || code == 9 || code == 0x20; }
function badescape(code) { return newline(code) || isNaN(code); }
// Note: I'm not yet acting smart enough to actually handle astral characters.
var maximumallowedcodepoint = 0x10ffff;
function tokenize(str, options) {
if(options == undefined) options = {transformFunctionWhitespace:false, scientificNotation:false};
var i = -1;
var tokens = [];
var state = "data";
var code;
var currtoken;
// Line number information.
var line = 0;
var column = 0;
// The only use of lastLineLength is in reconsume().
var lastLineLength = 0;
var incrLineno = function() {
line += 1;
lastLineLength = column;
column = 0;
};
var locStart = {line:line, column:column};
var next = function(num) { if(num === undefined) num = 1; return str.charCodeAt(i+num); };
var consume = function(num) {
if(num === undefined)
num = 1;
i += num;
code = str.charCodeAt(i);
if (newline(code)) incrLineno();
else column += num;
//console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16));
return true;
};
var reconsume = function() {
i -= 1;
if (newline(code)) {
line -= 1;
column = lastLineLength;
} else {
column -= 1;
}
locStart.line = line;
locStart.column = column;
return true;
};
var eof = function() { return i >= str.length; };
var donothing = function() {};
var emit = function(token) {
if(token) {
token.finish();
} else {
token = currtoken.finish();
}
if (options.loc === true) {
token.loc = {};
token.loc.start = {line:locStart.line, column:locStart.column, idx: locStart.idx};
locStart = {line: line, column: column, idx: i};
token.loc.end = locStart;
}
tokens.push(token);
//console.log('Emitting ' + token);
currtoken = undefined;
return true;
};
var create = function(token) { currtoken = token; return true; };
// mozmod: disable console.log
var parseerror = function() { /* console.log("Parse error at index " + i + ", processing codepoint 0x" + code.toString(16) + " in state " + state + "."); */ return true; };
// mozmod: disable console.log
var catchfire = function(msg) { /* console.log("MAJOR SPEC ERROR: " + msg); */ return true;}
var switchto = function(newstate) {
state = newstate;
//console.log('Switching to ' + state);
return true;
};
var consumeEscape = function() {
// Assume the the current character is the \
consume();
if(hexdigit(code)) {
// Consume 1-6 hex digits
var digits = [];
for(var total = 0; total < 6; total++) {
if(hexdigit(code)) {
digits.push(code);
consume();
} else { break; }
}
var value = parseInt(digits.map(String.fromCharCode).join(''), 16);
if( value > maximumallowedcodepoint ) value = 0xfffd;
// If the current char is whitespace, cool, we'll just eat it.
// Otherwise, put it back.
if(!whitespace(code)) reconsume();
return value;
} else {
return code;
}
};
for(;;) {
if(i > str.length*2) return "I'm infinite-looping!";
consume();
switch(state) {
case "data":
if(whitespace(code)) {
emit(new WhitespaceToken);
while(whitespace(next())) consume();
}
else if(code == 0x22) switchto("double-quote-string");
else if(code == 0x23) switchto("hash");
else if(code == 0x27) switchto("single-quote-string");
else if(code == 0x28) emit(new OpenParenToken);
else if(code == 0x29) emit(new CloseParenToken);
else if(code == 0x2b) {
if(digit(next()) || (next() == 0x2e && digit(next(2)))) switchto("number") && reconsume();
else emit(new DelimToken(code));
}
else if(code == 0x2d) {
if(next(1) == 0x2d && next(2) == 0x3e) consume(2) && emit(new CDCToken);
else if(digit(next()) || (next(1) == 0x2e && digit(next(2)))) switchto("number") && reconsume();
else switchto('ident') && reconsume();
}
else if(code == 0x2e) {
if(digit(next())) switchto("number") && reconsume();
else emit(new DelimToken(code));
}
else if(code == 0x2f) {
if(next() == 0x2a) consume() && switchto("comment");
else emit(new DelimToken(code));
}
else if(code == 0x3a) emit(new ColonToken);
else if(code == 0x3b) emit(new SemicolonToken);
else if(code == 0x3c) {
if(next(1) == 0x21 && next(2) == 0x2d && next(3) == 0x2d) consume(3) && emit(new CDOToken);
else emit(new DelimToken(code));
}
else if(code == 0x40) switchto("at-keyword");
else if(code == 0x5b) emit(new OpenSquareToken);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new DelimToken(code));
else switchto('ident') && reconsume();
}
else if(code == 0x5d) emit(new CloseSquareToken);
else if(code == 0x7b) emit(new OpenCurlyToken);
else if(code == 0x7d) emit(new CloseCurlyToken);
else if(digit(code)) switchto("number") && reconsume();
else if(code == 0x55 || code == 0x75) {
if(next(1) == 0x2b && hexdigit(next(2))) consume() && switchto("unicode-range");
else switchto('ident') && reconsume();
}
else if(namestartchar(code)) switchto('ident') && reconsume();
else if(eof()) { emit(new EOFToken); return tokens; }
else emit(new DelimToken(code));
break;
case "double-quote-string":
if(currtoken == undefined) create(new StringToken);
if(code == 0x22) emit() && switchto("data");
else if(eof()) parseerror() && emit() && switchto("data") && reconsume();
else if(newline(code)) parseerror() && emit(new BadStringToken) && switchto("data") && reconsume();
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new BadStringToken) && switchto("data");
else if(newline(next())) consume();
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "single-quote-string":
if(currtoken == undefined) create(new StringToken);
if(code == 0x27) emit() && switchto("data");
else if(eof()) parseerror() && emit() && switchto("data");
else if(newline(code)) parseerror() && emit(new BadStringToken) && switchto("data") && reconsume();
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new BadStringToken) && switchto("data");
else if(newline(next())) consume();
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "hash":
if(namechar(code)) create(new HashToken(code)) && switchto("hash-rest");
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume();
else create(new HashToken(consumeEscape())) && switchto('hash-rest');
}
else emit(new DelimToken(0x23)) && switchto('data') && reconsume();
break;
case "hash-rest":
if(namechar(code)) currtoken.append(code);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit() && switchto("data") && reconsume();
else currtoken.append(consumeEscape());
}
else emit() && switchto('data') && reconsume();
break;
case "comment":
if(code == 0x2a) {
if(next() == 0x2f) consume() && switchto('data');
else donothing();
}
else if(eof()) parseerror() && switchto('data') && reconsume();
else donothing();
break;
case "at-keyword":
if(code == 0x2d) {
if(namestartchar(next())) create(new AtKeywordToken(0x2d)) && switchto('at-keyword-rest');
else if(next(1) == 0x5c && !badescape(next(2))) create(new AtKeywordtoken(0x2d)) && switchto('at-keyword-rest');
else parseerror() && emit(new DelimToken(0x40)) && switchto('data') && reconsume();
}
else if(namestartchar(code)) create(new AtKeywordToken(code)) && switchto('at-keyword-rest');
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit(new DelimToken(0x23)) && switchto("data") && reconsume();
else create(new AtKeywordToken(consumeEscape())) && switchto('at-keyword-rest');
}
else emit(new DelimToken(0x40)) && switchto('data') && reconsume();
break;
case "at-keyword-rest":
if(namechar(code)) currtoken.append(code);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit() && switchto("data") && reconsume();
else currtoken.append(consumeEscape());
}
else emit() && switchto('data') && reconsume();
break;
case "ident":
if(code == 0x2d) {
if(namestartchar(next())) create(new IdentifierToken(code)) && switchto('ident-rest');
else if(next(1) == 0x5c && !badescape(next(2))) create(new IdentifierToken(code)) && switchto('ident-rest');
else emit(new DelimToken(0x2d)) && switchto('data');
}
else if(namestartchar(code)) create(new IdentifierToken(code)) && switchto('ident-rest');
else if(code == 0x5c) {
if(badescape(next())) parseerror() && switchto("data") && reconsume();
else create(new IdentifierToken(consumeEscape())) && switchto('ident-rest');
}
else catchfire("Hit the generic 'else' clause in ident state.") && switchto('data') && reconsume();
break;
case "ident-rest":
if(namechar(code)) currtoken.append(code);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit() && switchto("data") && reconsume();
else currtoken.append(consumeEscape());
}
else if(code == 0x28) {
if(currtoken.ASCIImatch('url')) switchto('url');
else emit(new FunctionToken(currtoken)) && switchto('data');
}
else if(whitespace(code) && options.transformFunctionWhitespace) switchto('transform-function-whitespace') && reconsume();
else emit() && switchto('data') && reconsume();
break;
case "transform-function-whitespace":
if(whitespace(next())) donothing();
else if(code == 0x28) emit(new FunctionToken(currtoken)) && switchto('data');
else emit() && switchto('data') && reconsume();
break;
case "number":
create(new NumberToken());
if(code == 0x2d) {
if(digit(next())) consume() && currtoken.append([0x2d,code]) && switchto('number-rest');
else if(next(1) == 0x2e && digit(next(2))) consume(2) && currtoken.append([0x2d,0x2e,code]) && switchto('number-fraction');
else switchto('data') && reconsume();
}
else if(code == 0x2b) {
if(digit(next())) consume() && currtoken.append([0x2b,code]) && switchto('number-rest');
else if(next(1) == 0x2e && digit(next(2))) consume(2) && currtoken.append([0x2b,0x2e,code]) && switchto('number-fraction');
else switchto('data') && reconsume();
}
else if(digit(code)) currtoken.append(code) && switchto('number-rest');
else if(code == 0x2e) {
if(digit(next())) consume() && currtoken.append([0x2e,code]) && switchto('number-fraction');
else switchto('data') && reconsume();
}
else switchto('data') && reconsume();
break;
case "number-rest":
if(digit(code)) currtoken.append(code);
else if(code == 0x2e) {
if(digit(next())) consume() && currtoken.append([0x2e,code]) && switchto('number-fraction');
else emit() && switchto('data') && reconsume();
}
else if(code == 0x25) emit(new PercentageToken(currtoken)) && switchto('data');
else if(code == 0x45 || code == 0x65) {
if(digit(next())) consume() && currtoken.append([0x25,code]) && switchto('sci-notation');
else if((next(1) == 0x2b || next(1) == 0x2d) && digit(next(2))) currtoken.append([0x25,next(1),next(2)]) && consume(2) && switchto('sci-notation');
else create(new DimensionToken(currtoken,code)) && switchto('dimension');
}
else if(code == 0x2d) {
if(namestartchar(next())) consume() && create(new DimensionToken(currtoken,[0x2d,code])) && switchto('dimension');
else if(next(1) == 0x5c && badescape(next(2))) parseerror() && emit() && switchto('data') && reconsume();
else if(next(1) == 0x5c) consume() && create(new DimensionToken(currtoken, [0x2d,consumeEscape()])) && switchto('dimension');
else emit() && switchto('data') && reconsume();
}
else if(namestartchar(code)) create(new DimensionToken(currtoken, code)) && switchto('dimension');
else if(code == 0x5c) {
if(badescape(next)) parseerror() && emit() && switchto('data') && reconsume();
else create(new DimensionToken(currtoken,consumeEscape)) && switchto('dimension');
}
else emit() && switchto('data') && reconsume();
break;
case "number-fraction":
currtoken.type = "number";
if(digit(code)) currtoken.append(code);
else if(code == 0x25) emit(new PercentageToken(currtoken)) && switchto('data');
else if(code == 0x45 || code == 0x65) {
if(digit(next())) consume() && currtoken.append([0x65,code]) && switchto('sci-notation');
else if((next(1) == 0x2b || next(1) == 0x2d) && digit(next(2))) currtoken.append([0x65,next(1),next(2)]) && consume(2) && switchto('sci-notation');
else create(new DimensionToken(currtoken,code)) && switchto('dimension');
}
else if(code == 0x2d) {
if(namestartchar(next())) consume() && create(new DimensionToken(currtoken,[0x2d,code])) && switchto('dimension');
else if(next(1) == 0x5c && badescape(next(2))) parseerror() && emit() && switchto('data') && reconsume();
else if(next(1) == 0x5c) consume() && create(new DimensionToken(currtoken, [0x2d,consumeEscape()])) && switchto('dimension');
else emit() && switchto('data') && reconsume();
}
else if(namestartchar(code)) create(new DimensionToken(currtoken, code)) && switchto('dimension');
else if(code == 0x5c) {
if(badescape(next)) parseerror() && emit() && switchto('data') && reconsume();
else create(new DimensionToken(currtoken,consumeEscape())) && switchto('dimension');
}
else emit() && switchto('data') && reconsume();
break;
case "dimension":
if(namechar(code)) currtoken.append(code);
else if(code == 0x5c) {
if(badescape(next())) parseerror() && emit() && switchto('data') && reconsume();
else currtoken.append(consumeEscape());
}
else emit() && switchto('data') && reconsume();
break;
case "sci-notation":
currtoken.type = "number";
if(digit(code)) currtoken.append(code);
else emit() && switchto('data') && reconsume();
break;
case "url":
if(eof()) parseerror() && emit(new BadURLToken) && switchto('data');
else if(code == 0x22) switchto('url-double-quote');
else if(code == 0x27) switchto('url-single-quote');
else if(code == 0x29) emit(new URLToken) && switchto('data');
else if(whitespace(code)) donothing();
else switchto('url-unquoted') && reconsume();
break;
case "url-double-quote":
if(! (currtoken instanceof URLToken)) create(new URLToken);
if(eof()) parseerror() && emit(new BadURLToken) && switchto('data');
else if(code == 0x22) switchto('url-end');
else if(newline(code)) parseerror() && switchto('bad-url');
else if(code == 0x5c) {
if(newline(next())) consume();
else if(badescape(next())) parseerror() && emit(new BadURLToken) && switchto('data') && reconsume();
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "url-single-quote":
if(! (currtoken instanceof URLToken)) create(new URLToken);
if(eof()) parseerror() && emit(new BadURLToken) && switchto('data');
else if(code == 0x27) switchto('url-end');
else if(newline(code)) parseerror() && switchto('bad-url');
else if(code == 0x5c) {
if(newline(next())) consume();
else if(badescape(next())) parseerror() && emit(new BadURLToken) && switchto('data') && reconsume();
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "url-end":
if(eof()) parseerror() && emit(new BadURLToken) && switchto('data');
else if(whitespace(code)) donothing();
else if(code == 0x29) emit() && switchto('data');
else parseerror() && switchto('bad-url') && reconsume();
break;
case "url-unquoted":
if(! (currtoken instanceof URLToken)) create(new URLToken);
if(eof()) parseerror() && emit(new BadURLToken) && switchto('data');
else if(whitespace(code)) switchto('url-end');
else if(code == 0x29) emit() && switchto('data');
else if(code == 0x22 || code == 0x27 || code == 0x28 || nonprintable(code)) parseerror() && switchto('bad-url');
else if(code == 0x5c) {
if(badescape(next())) parseerror() && switchto('bad-url');
else currtoken.append(consumeEscape());
}
else currtoken.append(code);
break;
case "bad-url":
if(eof()) parseerror() && emit(new BadURLToken) && switchto('data');
else if(code == 0x29) emit(new BadURLToken) && switchto('data');
else if(code == 0x5c) {
if(badescape(next())) donothing();
else consumeEscape();
}
else donothing();
break;
case "unicode-range":
// We already know that the current code is a hexdigit.
var start = [code], end = [code];
for(var total = 1; total < 6; total++) {
if(hexdigit(next())) {
consume();
start.push(code);
end.push(code);
}
else break;
}
if(next() == 0x3f) {
for(;total < 6; total++) {
if(next() == 0x3f) {
consume();
start.push("0".charCodeAt(0));
end.push("f".charCodeAt(0));
}
else break;
}
emit(new UnicodeRangeToken(start,end)) && switchto('data');
}
else if(next(1) == 0x2d && hexdigit(next(2))) {
consume();
consume();
end = [code];
for(var total = 1; total < 6; total++) {
if(hexdigit(next())) {
consume();
end.push(code);
}
else break;
}
emit(new UnicodeRangeToken(start,end)) && switchto('data');
}
else emit(new UnicodeRangeToken(start)) && switchto('data');
break;
default:
catchfire("Unknown state '" + state + "'");
}
}
}
function stringFromCodeArray(arr) {
return String.fromCharCode.apply(null,arr.filter(function(e){return e;}));
}
function CSSParserToken(options) { return this; }
CSSParserToken.prototype.finish = function() { return this; }
CSSParserToken.prototype.toString = function() { return this.tokenType; }
CSSParserToken.prototype.toJSON = function() { return this.toString(); }
function BadStringToken() { return this; }
BadStringToken.prototype = new CSSParserToken;
BadStringToken.prototype.tokenType = "BADSTRING";
function BadURLToken() { return this; }
BadURLToken.prototype = new CSSParserToken;
BadURLToken.prototype.tokenType = "BADURL";
function WhitespaceToken() { return this; }
WhitespaceToken.prototype = new CSSParserToken;
WhitespaceToken.prototype.tokenType = "WHITESPACE";
WhitespaceToken.prototype.toString = function() { return "WS"; }
function CDOToken() { return this; }
CDOToken.prototype = new CSSParserToken;
CDOToken.prototype.tokenType = "CDO";
function CDCToken() { return this; }
CDCToken.prototype = new CSSParserToken;
CDCToken.prototype.tokenType = "CDC";
function ColonToken() { return this; }
ColonToken.prototype = new CSSParserToken;
ColonToken.prototype.tokenType = ":";
function SemicolonToken() { return this; }
SemicolonToken.prototype = new CSSParserToken;
SemicolonToken.prototype.tokenType = ";";
function OpenCurlyToken() { return this; }
OpenCurlyToken.prototype = new CSSParserToken;
OpenCurlyToken.prototype.tokenType = "{";
function CloseCurlyToken() { return this; }
CloseCurlyToken.prototype = new CSSParserToken;
CloseCurlyToken.prototype.tokenType = "}";
function OpenSquareToken() { return this; }
OpenSquareToken.prototype = new CSSParserToken;
OpenSquareToken.prototype.tokenType = "[";
function CloseSquareToken() { return this; }
CloseSquareToken.prototype = new CSSParserToken;
CloseSquareToken.prototype.tokenType = "]";
function OpenParenToken() { return this; }
OpenParenToken.prototype = new CSSParserToken;
OpenParenToken.prototype.tokenType = "(";
function CloseParenToken() { return this; }
CloseParenToken.prototype = new CSSParserToken;
CloseParenToken.prototype.tokenType = ")";
function EOFToken() { return this; }
EOFToken.prototype = new CSSParserToken;
EOFToken.prototype.tokenType = "EOF";
function DelimToken(code) {
this.value = String.fromCharCode(code);
return this;
}
DelimToken.prototype = new CSSParserToken;
DelimToken.prototype.tokenType = "DELIM";
DelimToken.prototype.toString = function() { return "DELIM("+this.value+")"; }
function StringValuedToken() { return this; }
StringValuedToken.prototype = new CSSParserToken;
StringValuedToken.prototype.append = function(val) {
if(val instanceof Array) {
for(var i = 0; i < val.length; i++) {
this.value.push(val[i]);
}
} else {
this.value.push(val);
}
return true;
}
StringValuedToken.prototype.finish = function() {
this.value = this.valueAsString();
return this;
}
StringValuedToken.prototype.ASCIImatch = function(str) {
return this.valueAsString().toLowerCase() == str.toLowerCase();
}
StringValuedToken.prototype.valueAsString = function() {
if(typeof this.value == 'string') return this.value;
return stringFromCodeArray(this.value);
}
StringValuedToken.prototype.valueAsCodes = function() {
if(typeof this.value == 'string') {
var ret = [];
for(var i = 0; i < this.value.length; i++)
ret.push(this.value.charCodeAt(i));
return ret;
}
return this.value.filter(function(e){return e;});
}
function IdentifierToken(val) {
this.value = [];
this.append(val);
}
IdentifierToken.prototype = new StringValuedToken;
IdentifierToken.prototype.tokenType = "IDENT";
IdentifierToken.prototype.toString = function() { return "IDENT("+this.value+")"; }
function FunctionToken(val) {
// These are always constructed by passing an IdentifierToken
this.value = val.finish().value;
}
FunctionToken.prototype = new StringValuedToken;
FunctionToken.prototype.tokenType = "FUNCTION";
FunctionToken.prototype.toString = function() { return "FUNCTION("+this.value+")"; }
function AtKeywordToken(val) {
this.value = [];
this.append(val);
}
AtKeywordToken.prototype = new StringValuedToken;
AtKeywordToken.prototype.tokenType = "AT-KEYWORD";
AtKeywordToken.prototype.toString = function() { return "AT("+this.value+")"; }
function HashToken(val) {
this.value = [];
this.append(val);
}
HashToken.prototype = new StringValuedToken;
HashToken.prototype.tokenType = "HASH";
HashToken.prototype.toString = function() { return "HASH("+this.value+")"; }
function StringToken(val) {
this.value = [];
this.append(val);
}
StringToken.prototype = new StringValuedToken;
StringToken.prototype.tokenType = "STRING";
StringToken.prototype.toString = function() { return "\""+this.value+"\""; }
function URLToken(val) {
this.value = [];
this.append(val);
}
URLToken.prototype = new StringValuedToken;
URLToken.prototype.tokenType = "URL";
URLToken.prototype.toString = function() { return "URL("+this.value+")"; }
function NumberToken(val) {
this.value = [];
this.append(val);
this.type = "integer";
}
NumberToken.prototype = new StringValuedToken;
NumberToken.prototype.tokenType = "NUMBER";
NumberToken.prototype.toString = function() {
if(this.type == "integer")
return "INT("+this.value+")";
return "NUMBER("+this.value+")";
}
NumberToken.prototype.finish = function() {
this.repr = this.valueAsString();
this.value = this.repr * 1;
if(Math.abs(this.value) % 1 != 0) this.type = "number";
return this;
}
function PercentageToken(val) {
// These are always created by passing a NumberToken as val
val.finish();
this.value = val.value;
this.repr = val.repr;
}
PercentageToken.prototype = new CSSParserToken;
PercentageToken.prototype.tokenType = "PERCENTAGE";
PercentageToken.prototype.toString = function() { return "PERCENTAGE("+this.value+")"; }
function DimensionToken(val,unit) {
// These are always created by passing a NumberToken as the val
val.finish();
this.num = val.value;
this.unit = [];
this.repr = val.repr;
this.append(unit);
}
DimensionToken.prototype = new CSSParserToken;
DimensionToken.prototype.tokenType = "DIMENSION";
DimensionToken.prototype.toString = function() { return "DIM("+this.num+","+this.unit+")"; }
DimensionToken.prototype.append = function(val) {
if(val instanceof Array) {
for(var i = 0; i < val.length; i++) {
this.unit.push(val[i]);
}
} else {
this.unit.push(val);
}
return true;
}
DimensionToken.prototype.finish = function() {
this.unit = stringFromCodeArray(this.unit);
this.repr += this.unit;
return this;
}
function UnicodeRangeToken(start,end) {
// start and end are array of char codes, completely finished
start = parseInt(stringFromCodeArray(start),16);
if(end === undefined) end = start + 1;
else end = parseInt(stringFromCodeArray(end),16);
if(start > maximumallowedcodepoint) end = start;
if(end < start) end = start;
if(end > maximumallowedcodepoint) end = maximumallowedcodepoint;
this.start = start;
this.end = end;
return this;
}
UnicodeRangeToken.prototype = new CSSParserToken;
UnicodeRangeToken.prototype.tokenType = "UNICODE-RANGE";
UnicodeRangeToken.prototype.toString = function() {
if(this.start+1 == this.end)
return "UNICODE-RANGE("+this.start.toString(16).toUpperCase()+")";
if(this.start < this.end)
return "UNICODE-RANGE("+this.start.toString(16).toUpperCase()+"-"+this.end.toString(16).toUpperCase()+")";
return "UNICODE-RANGE()";
}
UnicodeRangeToken.prototype.contains = function(code) {
return code >= this.start && code < this.end;
}
// Exportation.
// TODO: also export the various tokens objects?
exports.tokenize = tokenize;
exports.EOFToken = EOFToken;
}));

View file

@ -0,0 +1,818 @@
// Copyright (c) 2014 Andris Reinman
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(['tcp-socket', 'imap-handler', 'mimefuncs', 'axe'], function(TCPSocket, imapHandler, mimefuncs, axe) {
return factory(TCPSocket, imapHandler, mimefuncs, axe);
});
} else if (typeof exports === 'object') {
module.exports = factory(require('tcp-socket'), require('wo-imap-handler'), require('mimefuncs'), require('axe-logger'));
} else {
root.BrowserboxImapClient = factory(navigator.TCPSocket, root.imapHandler, root.mimefuncs, root.axe);
}
}(this, function(TCPSocket, imapHandler, mimefuncs, axe) {
'use strict';
var DEBUG_TAG = 'browserbox IMAP';
/**
* Creates a connection object to an IMAP server. Call `connect` method to inititate
* the actual connection, the constructor only defines the properties but does not actually connect.
*
* @constructor
*
* @param {String} [host='localhost'] Hostname to conenct to
* @param {Number} [port=143] Port number to connect to
* @param {Object} [options] Optional options object
* @param {Boolean} [options.useSecureTransport] Set to true, to use encrypted connection
*/
function ImapClient(host, port, options) {
this._TCPSocket = TCPSocket;
this.options = options || {};
this.port = port || (this.options.useSecureTransport ? 993 : 143);
this.host = host || 'localhost';
/**
* If set to true, start an encrypted connection instead of the plaintext one
* (recommended if applicable). If useSecureTransport is not set but the port used is 993,
* then ecryption is used by default.
*/
this.options.useSecureTransport = 'useSecureTransport' in this.options ? !!this.options.useSecureTransport : this.port === 993;
/**
* Authentication object. If not set, authentication step will be skipped.
*/
this.options.auth = this.options.auth || false;
/**
* Downstream TCP socket to the IMAP server, created with TCPSocket
*/
this.socket = false;
/**
* Indicates if the connection has been closed and can't be used anymore
*
*/
this.destroyed = false;
/**
* Keeps track if the downstream socket is currently full and
* a drain event should be waited for or not
*/
this.waitDrain = false;
// Private properties
/**
* Does the connection use SSL/TLS
*/
this.secureMode = !!this.options.useSecureTransport;
/**
* Is the conection established and greeting is received from the server
*/
this._connectionReady = false;
/**
* As the server sends data in chunks, it needs to be split into
* separate lines. These variables help with parsing the input.
*/
this._remainder = '';
this._command = '';
this._literalRemaining = 0;
/**
* Is there something being processed
*/
this._processingServerData = false;
/**
* Queue of received commands
*/
this._serverQueue = [];
/**
* Is it OK to send something to the server
*/
this._canSend = false;
/**
* Queue of outgoing commands
*/
this._clientQueue = [];
/**
* Counter to allow uniqueue imap tags
*/
this._tagCounter = 0;
/**
* Current command that is waiting for response from the server
*/
this._currentCommand = false;
/**
* Global handlers for unrelated responses (EXPUNGE, EXISTS etc.)
*/
this._globalAcceptUntagged = {};
/**
* Timer waiting to enter idle
*/
this._idleTimer = false;
/**
* Timer waiting to declare the socket dead starting from the last write
*/
this._socketTimeoutTimer = false;
}
// Constants
/**
* How much time to wait since the last response until the connection is considered idling
*/
ImapClient.prototype.TIMEOUT_ENTER_IDLE = 1000;
/**
* Lower Bound for socket timeout to wait since the last data was written to a socket
*/
ImapClient.prototype.TIMEOUT_SOCKET_LOWER_BOUND = 10000;
/**
* Multiplier for socket timeout:
*
* We assume at least a GPRS connection with 115 kb/s = 14,375 kB/s tops, so 10 KB/s to be on
* the safe side. We can timeout after a lower bound of 10s + (n KB / 10 KB/s). A 1 MB message
* upload would be 110 seconds to wait for the timeout. 10 KB/s === 0.1 s/B
*/
ImapClient.prototype.TIMEOUT_SOCKET_MULTIPLIER = 0.1;
// PUBLIC EVENTS
// Event functions should be overriden, these are just placeholders
/**
* Will be run when an error occurs. Connection to the server will be closed automatically,
* so wait for an `onclose` event as well.
*
* @event
* @param {Error} err Error object
*/
ImapClient.prototype.onerror = function() {};
/**
* More data can be buffered in the socket. See `waitDrain` property or
* check if `send` method returns false to see if you should be waiting
* for the drain event.
*
* @event
*/
ImapClient.prototype.ondrain = function() {};
/**
* The connection to the server has been closed
*
* @event
*/
ImapClient.prototype.onclose = function() {};
/**
* The connection to the server has been established and greeting is received
*
* @event
*/
ImapClient.prototype.onready = function() {};
/**
* There are no more commands to process
*
* @event
*/
ImapClient.prototype.onidle = function() {};
// PUBLIC METHODS
/**
* Initiate a connection to the server. Wait for onready event
*/
ImapClient.prototype.connect = function() {
this.socket = this._TCPSocket.open(this.host, this.port, {
binaryType: 'arraybuffer',
useSecureTransport: this.secureMode,
ca: this.options.ca,
tlsWorkerPath: this.options.tlsWorkerPath
});
// allows certificate handling for platform w/o native tls support
// oncert is non standard so setting it might throw if the socket object is immutable
try {
this.socket.oncert = this.oncert;
} catch (E) {}
this.socket.onerror = this._onError.bind(this);
this.socket.onopen = this._onOpen.bind(this);
};
/**
* Closes the connection to the server
*/
ImapClient.prototype.close = function() {
if (this.socket && this.socket.readyState === 'open') {
this.socket.close();
} else {
this._destroy();
}
};
/**
* Closes the connection to the server
*/
ImapClient.prototype.upgrade = function(callback) {
if (this.secureMode) {
return callback(null, false);
}
this.secureMode = true;
this.socket.upgradeToSecure();
callback(null, true);
};
/**
* Schedules a command to be sent to the server. This method is chainable.
* See https://github.com/Kreata/imapHandler for request structure.
* Do not provide a tag property, it will be set byt the queue manager.
*
* To catch untagged responses use acceptUntagged property. For example, if
* the value for it is 'FETCH' then the reponse includes 'payload.FETCH' property
* that is an array including all listed * FETCH responses.
*
* Callback function provides 2 arguments, parsed response object and continue callback.
*
* function(response, next){
* console.log(response);
* next();
* }
*
* @param {Object} request Structured request object
* @param {Array} acceptUntagged a list of untagged responses that will be included in 'payload' property
* @param {Object} [options] Optional data for the command payload, eg. {onplustagged: function(response, next){next();}}
* @param {Function} callback Callback function to run once the command has been processed
*/
ImapClient.prototype.exec = function(request, acceptUntagged, options, callback) {
if (typeof request === 'string') {
request = {
command: request
};
}
this._addToClientQueue(request, acceptUntagged, options, callback);
return this;
};
/**
* Send data to the TCP socket
* Arms a timeout waiting for a response from the server.
*
* @param {String} str Payload
*/
ImapClient.prototype.send = function(str) {
var buffer = mimefuncs.toTypedArray(str).buffer,
timeout = this.TIMEOUT_SOCKET_LOWER_BOUND + Math.floor(buffer.byteLength * this.TIMEOUT_SOCKET_MULTIPLIER);
clearTimeout(this._socketTimeoutTimer); // clear pending timeouts
this._socketTimeoutTimer = setTimeout(this._onTimeout.bind(this), timeout); // arm the next timeout
this.waitDrain = this.socket.send(buffer);
};
/**
* Set a global handler for an untagged response. If currently processed command
* has not listed untagged command it is forwarded to the global handler. Useful
* with EXPUNGE, EXISTS etc.
*
* @param {String} command Untagged command name
* @param {Function} callback Callback function with response object and continue callback function
*/
ImapClient.prototype.setHandler = function(command, callback) {
this._globalAcceptUntagged[(command || '').toString().toUpperCase().trim()] = callback;
};
// INTERNAL EVENTS
/**
* Error handler for the socket
*
* @event
* @param {Event} evt Event object. See evt.data for the error
*/
ImapClient.prototype._onError = function(evt) {
if (this.isError(evt)) {
this.onerror(evt);
} else if (evt && this.isError(evt.data)) {
this.onerror(evt.data);
} else {
this.onerror(new Error(evt && evt.data && evt.data.message || evt.data || evt || 'Error'));
}
this.close();
};
/**
* Ensures that the connection is closed
*/
ImapClient.prototype._destroy = function() {
this._serverQueue = [];
this._clientQueue = [];
this._currentCommand = false;
clearTimeout(this._idleTimer);
clearTimeout(this._socketTimeoutTimer);
if (!this.destroyed) {
this.destroyed = true;
this.onclose();
}
};
/**
* Indicates that the socket has been closed
*
* @event
* @param {Event} evt Event object. Not used
*/
ImapClient.prototype._onClose = function() {
this._destroy();
};
/**
* Indicates that a socket timeout has occurred
*/
ImapClient.prototype._onTimeout = function() {
// inform about the timeout, _onError takes case of the rest
var error = new Error(this.options.sessionId + ' Socket timed out!');
axe.error(DEBUG_TAG, error);
this._onError(error);
};
/**
* More data can be buffered in the socket, `waitDrain` is reset to false
*
* @event
* @param {Event} evt Event object. Not used
*/
ImapClient.prototype._onDrain = function() {
this.waitDrain = false;
this.ondrain();
};
/**
* Handler for incoming data from the server. The data is sent in arbitrary
* chunks and can't be used directly so this function makes sure the data
* is split into complete lines before the data is passed to the command
* handler
*
* @param {Event} evt
*/
ImapClient.prototype._onData = function(evt) {
if (!evt || !evt.data) {
return;
}
clearTimeout(this._socketTimeoutTimer);
var match,
str = mimefuncs.fromTypedArray(evt.data);
if (this._literalRemaining) {
if (this._literalRemaining > str.length) {
this._literalRemaining -= str.length;
this._command += str;
return;
}
this._command += str.substr(0, this._literalRemaining);
str = str.substr(this._literalRemaining);
this._literalRemaining = 0;
}
this._remainder = str = this._remainder + str;
while ((match = str.match(/(\{(\d+)(\+)?\})?\r?\n/))) {
if (!match[2]) {
// Now we have a full command line, so lets do something with it
this._addToServerQueue(this._command + str.substr(0, match.index));
this._remainder = str = str.substr(match.index + match[0].length);
this._command = '';
continue;
}
this._remainder = '';
this._command += str.substr(0, match.index + match[0].length);
this._literalRemaining = Number(match[2]);
str = str.substr(match.index + match[0].length);
if (this._literalRemaining > str.length) {
this._command += str;
this._literalRemaining -= str.length;
return;
} else {
this._command += str.substr(0, this._literalRemaining);
this._remainder = str = str.substr(this._literalRemaining);
this._literalRemaining = 0;
}
}
};
/**
* Connection listener that is run when the connection to the server is opened.
* Sets up different event handlers for the opened socket
*
* @event
*/
ImapClient.prototype._onOpen = function() {
axe.debug(DEBUG_TAG, this.options.sessionId + ' tcp socket opened');
this.socket.ondata = this._onData.bind(this);
this.socket.onclose = this._onClose.bind(this);
this.socket.ondrain = this._onDrain.bind(this);
};
// PRIVATE METHODS
/**
* Pushes command line from the server to the server processing queue. If the
* processor is idle, start processing.
*
* @param {String} cmd Command line
*/
ImapClient.prototype._addToServerQueue = function(cmd) {
this._serverQueue.push(cmd);
if (this._processingServerData) {
return;
}
this._processingServerData = true;
this._processServerQueue();
};
/**
* Process a command from the queue. The command is parsed and feeded to a handler
*/
ImapClient.prototype._processServerQueue = function() {
if (!this._serverQueue.length) {
this._processingServerData = false;
return;
} else {
this._clearIdle();
}
var data = this._serverQueue.shift(),
response;
try {
// + tagged response is a special case, do not try to parse it
if (/^\+/.test(data)) {
response = {
tag: '+',
payload: data.substr(2) || ''
};
} else {
response = imapHandler.parser(data);
axe.debug(DEBUG_TAG, this.options.sessionId + ' S: ' + imapHandler.compiler(response, false, true));
}
} catch (e) {
axe.error(DEBUG_TAG, this.options.sessionId + ' error parsing imap response: ' + e + '\n' + e.stack + '\nraw:' + data);
return this._onError(e);
}
if (response.tag === '*' &&
/^\d+$/.test(response.command) &&
response.attributes && response.attributes.length && response.attributes[0].type === 'ATOM') {
response.nr = Number(response.command);
response.command = (response.attributes.shift().value || '').toString().toUpperCase().trim();
}
// feed the next chunk to the server if a + tagged response was received
if (response.tag === '+') {
if (this._currentCommand.data.length) {
data = this._currentCommand.data.shift();
this.send(data + (!this._currentCommand.data.length ? '\r\n' : ''));
} else if (typeof this._currentCommand.onplustagged === 'function') {
this._currentCommand.onplustagged(response, this._processServerQueue.bind(this));
return;
}
setTimeout(this._processServerQueue.bind(this), 0);
return;
}
this._processServerResponse(response, function(err) {
if (err) {
return this._onError(err);
}
// first response from the server, connection is now usable
if (!this._connectionReady) {
this._connectionReady = true;
this.onready();
this._canSend = true;
this._sendRequest();
} else if (response.tag !== '*') {
// allow sending next command after full response
this._canSend = true;
this._sendRequest();
}
setTimeout(this._processServerQueue.bind(this), 0);
}.bind(this));
};
/**
* Feeds a parsed response object to an appropriate handler
*
* @param {Object} response Parsed command object
* @param {Function} callback Continue callback function
*/
ImapClient.prototype._processServerResponse = function(response, callback) {
var command = (response && response.command || '').toUpperCase().trim();
this._processResponse(response);
if (!this._currentCommand) {
if (response.tag === '*' && command in this._globalAcceptUntagged) {
return this._globalAcceptUntagged[command](response, callback);
} else {
return callback();
}
}
if (this._currentCommand.payload && response.tag === '*' && command in this._currentCommand.payload) {
this._currentCommand.payload[command].push(response);
return callback();
} else if (response.tag === '*' && command in this._globalAcceptUntagged) {
this._globalAcceptUntagged[command](response, callback);
} else if (response.tag === this._currentCommand.tag) {
if (typeof this._currentCommand.callback === 'function') {
if (this._currentCommand.payload && Object.keys(this._currentCommand.payload).length) {
response.payload = this._currentCommand.payload;
}
return this._currentCommand.callback(response, callback);
} else {
return callback();
}
} else {
// Unexpected response
return callback();
}
};
/**
* Adds a request object to outgoing queue. And if data can be sent to the server,
* the command is executed
*
* @param {Object} request Structured request object
* @param {Array} [acceptUntagged] a list of untagged responses that will be included in 'payload' property
* @param {Object} [options] Optional data for the command payload, eg. {onplustagged: function(response, next){next();}}
* @param {Function} callback Callback function to run once the command has been processed
*/
ImapClient.prototype._addToClientQueue = function(request, acceptUntagged, options, callback) {
var tag = 'W' + (++this._tagCounter),
data;
if (!callback && typeof options === 'function') {
callback = options;
options = undefined;
}
if (!callback && typeof acceptUntagged === 'function') {
callback = acceptUntagged;
acceptUntagged = undefined;
}
acceptUntagged = [].concat(acceptUntagged || []).map(function(untagged) {
return (untagged || '').toString().toUpperCase().trim();
});
request.tag = tag;
data = {
tag: tag,
request: request,
payload: acceptUntagged.length ? {} : undefined,
callback: callback
};
// apply any additional options to the command
Object.keys(options || {}).forEach(function(key) {
data[key] = options[key];
});
acceptUntagged.forEach(function(command) {
data.payload[command] = [];
});
// if we're in priority mode (i.e. we ran commands in a precheck),
// queue any commands BEFORE the command that contianed the precheck,
// otherwise just queue command as usual
var index = data.ctx ? this._clientQueue.indexOf(data.ctx) : -1;
if (index >= 0) {
data.tag += '.p';
data.request.tag += '.p';
this._clientQueue.splice(index, 0, data);
} else {
this._clientQueue.push(data);
}
if (this._canSend) {
this._sendRequest();
}
};
/**
* Sends a command from client queue to the server.
*/
ImapClient.prototype._sendRequest = function() {
if (!this._clientQueue.length) {
return this._enterIdle();
}
this._clearIdle();
// an operation was made in the precheck, no need to restart the queue manually
this._restartQueue = false;
var command = this._clientQueue[0];
if (typeof command.precheck === 'function') {
// remember the context
var context = command;
var precheck = context.precheck;
delete context.precheck;
// we need to restart the queue handling if no operation was made in the precheck
this._restartQueue = true;
// invoke the precheck command with a callback to signal that you're
// done with precheck and ready to resume normal operation
precheck(context, function(err) {
// we're done with the precheck
if (!err) {
if (this._restartQueue) {
// we need to restart the queue handling
this._sendRequest();
}
return;
}
// precheck callback failed, so we remove the initial command
// from the queue, invoke its callback and resume normal operation
var cmd, index = this._clientQueue.indexOf(context);
if (index >= 0) {
cmd = this._clientQueue.splice(index, 1)[0];
}
if (cmd && cmd.callback) {
cmd.callback(err, function() {
this._canSend = true;
this._sendRequest();
setTimeout(this._processServerQueue.bind(this), 0);
}.bind(this));
}
}.bind(this));
return;
}
this._canSend = false;
this._currentCommand = this._clientQueue.shift();
var loggedCommand = false;
try {
this._currentCommand.data = imapHandler.compiler(this._currentCommand.request, true);
loggedCommand = imapHandler.compiler(this._currentCommand.request, false, true);
} catch (e) {
axe.error(DEBUG_TAG, this.options.sessionId + ' error compiling imap command: ' + e + '\nstack trace: ' + e.stack + '\nraw:' + this._currentCommand.request);
return this._onError(e);
}
axe.debug(DEBUG_TAG, this.options.sessionId + ' C: ' + loggedCommand);
var data = this._currentCommand.data.shift();
this.send(data + (!this._currentCommand.data.length ? '\r\n' : ''));
return this.waitDrain;
};
/**
* Emits onidle, noting to do currently
*/
ImapClient.prototype._enterIdle = function() {
clearTimeout(this._idleTimer);
this._idleTimer = setTimeout(function() {
this.onidle();
}.bind(this), this.TIMEOUT_ENTER_IDLE);
};
/**
* Cancel idle timer
*/
ImapClient.prototype._clearIdle = function() {
clearTimeout(this._idleTimer);
};
// HELPER FUNCTIONS
/**
* Method checks if a response includes optional response codes
* and copies these into separate properties. For example the
* following response includes a capability listing and a human
* readable message:
*
* * OK [CAPABILITY ID NAMESPACE] All ready
*
* This method adds a 'capability' property with an array value ['ID', 'NAMESPACE']
* to the response object. Additionally 'All ready' is added as 'humanReadable' property.
*
* See possiblem IMAP Response Codes at https://tools.ietf.org/html/rfc5530
*
* @param {Object} response Parsed response object
*/
ImapClient.prototype._processResponse = function(response) {
var command = (response && response.command || '').toString().toUpperCase().trim(),
option,
key;
if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].indexOf(command) >= 0) {
// Check if the response includes an optional response code
if (
(option = response && response.attributes &&
response.attributes.length && response.attributes[0].type === 'ATOM' &&
response.attributes[0].section && response.attributes[0].section.map(function(key) {
if (!key) {
return;
}
if (Array.isArray(key)) {
return key.map(function(key) {
return (key.value || '').toString().trim();
});
} else {
return (key.value || '').toString().toUpperCase().trim();
}
}))) {
key = option && option.shift();
response.code = key;
if (option.length) {
option = [].concat(option || []);
response[key.toLowerCase()] = option.length === 1 ? option[0] : option;
}
}
// If last element of the response is TEXT then this is for humans
if (response && response.attributes && response.attributes.length &&
response.attributes[response.attributes.length - 1].type === 'TEXT') {
response.humanReadable = response.attributes[response.attributes.length - 1].value;
}
}
};
/**
* Checks if a value is an Error object
*
* @param {Mixed} value Value to be checked
* @return {Boolean} returns true if the value is an Error
*/
ImapClient.prototype.isError = function(value) {
return !!Object.prototype.toString.call(value).match(/Error\]$/);
};
return ImapClient;
}));

File diff suppressed because it is too large Load diff

236
gaia/apps/email/js/ext/ext/co.js Executable file
View file

@ -0,0 +1,236 @@
define(function(require) {
/**
* slice() reference.
*/
var slice = Array.prototype.slice;
/**
* Expose `co`.
*/
co['default'] = co.co = co;
/**
* Wrap the given generator `fn` into a
* function that returns a promise.
* This is a separate function so that
* every `co()` call doesn't create a new,
* unnecessary closure.
*
* @param {GeneratorFunction} fn
* @return {Function}
* @api public
*/
co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn;
return createPromise;
function createPromise() {
return co.call(this, fn.apply(this, arguments));
}
};
/**
* Execute the generator function or a generator
* and return a promise.
*
* @param {Function} fn
* @return {Promise}
* @api public
*/
function co(gen) {
var ctx = this;
if (typeof gen === 'function') gen = gen.call(this);
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
onFulfilled();
/**
* @param {Mixed} res
* @return {Promise}
* @api private
*/
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* @param {Error} err
* @return {Promise}
* @api private
*/
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
*/
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
/**
* Convert a `yield`ed value into a promise.
*
* @param {Mixed} obj
* @return {Promise}
* @api private
*/
function toPromise(obj) {
if (!obj) return obj;
if (isPromise(obj)) return obj;
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
if ('function' == typeof obj) return thunkToPromise.call(this, obj);
if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
if (isObject(obj)) return objectToPromise.call(this, obj);
return obj;
}
/**
* Convert a thunk to a promise.
*
* @param {Function}
* @return {Promise}
* @api private
*/
function thunkToPromise(fn) {
var ctx = this;
return new Promise(function (resolve, reject) {
fn.call(ctx, function (err, res) {
if (err) return reject(err);
if (arguments.length > 2) res = slice.call(arguments, 1);
resolve(res);
});
});
}
/**
* Convert an array of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Array} obj
* @return {Promise}
* @api private
*/
function arrayToPromise(obj) {
return Promise.all(obj.map(toPromise, this));
}
/**
* Convert an object of "yieldables" to a promise.
* Uses `Promise.all()` internally.
*
* @param {Object} obj
* @return {Promise}
* @api private
*/
function objectToPromise(obj){
var results = new obj.constructor();
var keys = Object.keys(obj);
var promises = [];
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var promise = toPromise.call(this, obj[key]);
if (promise && isPromise(promise)) defer(promise, key);
else results[key] = obj[key];
}
return Promise.all(promises).then(function () {
return results;
});
function defer(promise, key) {
// predefine the key in the result
results[key] = undefined;
promises.push(promise.then(function (res) {
results[key] = res;
}));
}
}
/**
* Check if `obj` is a promise.
*
* @param {Object} obj
* @return {Boolean}
* @api private
*/
function isPromise(obj) {
return 'function' == typeof obj.then;
}
/**
* Check if `obj` is a generator.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGenerator(obj) {
return 'function' == typeof obj.next && 'function' == typeof obj.throw;
}
/**
* Check if `obj` is a generator function.
*
* @param {Mixed} obj
* @return {Boolean}
* @api private
*/
function isGeneratorFunction(obj) {
var constructor = obj.constructor;
if (!constructor) return false;
if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true;
return isGenerator(constructor.prototype);
}
/**
* Check for plain object.
*
* @param {Mixed} val
* @return {Boolean}
* @api private
*/
function isObject(val) {
return Object == val.constructor;
}
return co;
});

View file

@ -0,0 +1,120 @@
/**
* Sane equivalence checking, originally from loggest's rdcommon/log.js.
*/
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at:
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Mozilla Raindrop Code.
*
* The Initial Developer of the Original Code is
* The Mozilla Foundation
* Portions created by the Initial Developer are Copyright (C) 2011
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Andrew Sutherland <asutherland@asutherland.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
/**
* This module exports a single comparison function:
*
* equal(a, b) -> boolean
*
*/
define(function(require) {
/**
* Maximum comparison depth for argument equivalence in expectation checking.
* This value gets bumped every time I throw something at it that fails that
* still seems reasonable to me.
*/
var COMPARE_DEPTH = 6;
function boundedCmpObjs(a, b, depthLeft) {
var aAttrCount = 0, bAttrCount = 0, key, nextDepth = depthLeft - 1;
if ('toJSON' in a)
a = a.toJSON();
if ('toJSON' in b)
b = b.toJSON();
for (key in a) {
aAttrCount++;
if (!(key in b))
return false;
if (depthLeft) {
if (!equal(a[key], b[key], nextDepth))
return false;
}
else {
if (a[key] !== b[key])
return false;
}
}
// the theory is that if every key in a is in b and its value is equal, and
// there are the same number of keys in b, then they must be equal.
for (key in b) {
bAttrCount++;
}
if (aAttrCount !== bAttrCount)
return false;
return true;
}
/**
* @return[Boolean]{
* True when equivalent, false when not equivalent.
* }
*/
function equal(a, b, depthLeft) {
if (depthLeft === undefined) {
depthLeft = COMPARE_DEPTH;
}
var ta = typeof(a), tb = typeof(b);
if (ta !== 'object' || (tb !== ta) || (a == null) || (b == null))
return a === b;
// fast-path for identical objects
if (a === b)
return true;
if (Array.isArray(a)) {
if (!Array.isArray(b))
return false;
if (a.length !== b.length)
return false;
for (var iArr = 0; iArr < a.length; iArr++) {
if (!equal(a[iArr], b[iArr], depthLeft - 1))
return false;
}
return true;
}
return boundedCmpObjs(a, b, depthLeft);
}
return equal;
}); // end define

162
gaia/apps/email/js/ext/ext/evt.js Executable file
View file

@ -0,0 +1,162 @@
// COPIED FROM gaia/apps/emailjs/evt.js
/*global define, setTimeout */
/*
* Custom events lib. Notable features:
*
* - the module itself is an event emitter. Useful for "global" pub/sub.
* - evt.mix can be used to mix in an event emitter into existing object.
* - notification of listeners is done in a try/catch, so all listeners
* are notified even if one fails. Errors are thrown async via setTimeout
* so that all the listeners can be notified without escaping from the
* code via a throw within the listener group notification.
* - new evt.Emitter() can be used to create a new instance of an
* event emitter.
* - Uses "this" insternally, so always call object with the emitter args
*
*/
define(function() {
var evt,
slice = Array.prototype.slice,
props = ['_events', '_pendingEvents', 'on', 'once', 'latest',
'latestOnce', 'removeListener', 'emitWhenListener', 'emit'];
function Emitter() {
this._events = {};
this._pendingEvents = {};
}
Emitter.prototype = {
on: function(id, fn) {
var listeners = this._events[id],
pending = this._pendingEvents[id];
if (!listeners) {
listeners = this._events[id] = [];
}
listeners.push(fn);
if (pending) {
pending.forEach(function(args) {
fn.apply(null, args);
});
delete this._pendingEvents[id];
}
return this;
},
once: function(id, fn) {
var self = this,
fired = false;
function one() {
if (fired)
return;
fired = true;
fn.apply(null, arguments);
// Remove at a further turn so that the event
// forEach in emit does not get modified during
// this turn.
setTimeout(function() {
self.removeListener(id, one);
});
}
return this.on(id, one);
},
/**
* Waits for a property on the object that has the event interface
* to be available. That property MUST EVALUATE TO A TRUTHY VALUE.
* hasOwnProperty is not used because many objects are created with
* null placeholders to give a proper JS engine shape to them, and
* this method should not trigger the listener for those cases.
* If the property is already available, call the listener right
* away. If not available right away, listens for an event name that
* matches the property name.
* @param {String} id property name.
* @param {Function} fn listener.
*/
latest: function(id, fn) {
if (this[id] && !this._pendingEvents[id]) {
fn(this[id]);
}
this.on(id, fn);
},
/**
* Same as latest, but only calls the listener once.
* @param {String} id property name.
* @param {Function} fn listener.
*/
latestOnce: function(id, fn) {
if (this[id] && !this._pendingEvents[id])
fn(this[id]);
else
this.once(id, fn);
},
removeListener: function(id, fn) {
var i,
listeners = this._events[id];
if (listeners) {
i = listeners.indexOf(fn);
if (i !== -1) {
listeners.splice(i, 1);
}
if (listeners.length === 0)
delete this._events[id];
}
},
/**
* Like emit, but if no listeners yet, holds on
* to the value until there is one. Any other
* args after first one are passed to listeners.
* @param {String} id event ID.
*/
emitWhenListener: function(id) {
var listeners = this._events[id];
if (listeners) {
this.emit.apply(this, arguments);
} else {
if (!this._pendingEvents[id])
this._pendingEvents[id] = [];
this._pendingEvents[id].push(slice.call(arguments, 1));
}
},
emit: function(id) {
var args = slice.call(arguments, 1),
listeners = this._events[id];
if (listeners) {
listeners.forEach(function(fn) {
try {
fn.apply(null, args);
} catch (e) {
// Throw at later turn so that other listeners
// can complete. While this messes with the
// stack for the error, continued operation is
// valued more in this tradeoff.
setTimeout(function() {
throw e;
});
}
});
}
}
};
evt = new Emitter();
evt.Emitter = Emitter;
evt.mix = function(obj) {
var e = new Emitter();
props.forEach(function(prop) {
if (obj.hasOwnProperty(prop)) {
throw new Error('Object already has a property "' + prop + '"');
}
obj[prop] = e[prop];
});
return obj;
};
return evt;
});

View file

@ -0,0 +1,3 @@
define(['./imap-handler/src/imap-handler'], function(imapHandler) {
return imapHandler;
});

View file

@ -0,0 +1,144 @@
// Copyright (c) 2013 Andris Reinman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(['imap-formal-syntax'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('./imap-formal-syntax'));
} else {
root.imapCompiler = factory(root.imapFormalSyntax);
}
}(this, function(imapFormalSyntax) {
'use strict';
/**
* Compiles an input object into
*/
return function(response, asArray, isLogging) {
var respParts = [],
resp = (response.tag || '') + (response.command ? ' ' + response.command : ''),
val, lastType,
walk = function(node) {
if (lastType === 'LITERAL' || (['(', '<', '['].indexOf(resp.substr(-1)) < 0 && resp.length)) {
resp += ' ';
}
if (Array.isArray(node)) {
lastType = 'LIST';
resp += '(';
node.forEach(walk);
resp += ')';
return;
}
if (!node && typeof node !== 'string' && typeof node !== 'number') {
resp += 'NIL';
return;
}
if (typeof node === 'string') {
if (isLogging && node.length > 20) {
resp += '"(* ' + node.length + 'B string *)"';
} else {
resp += JSON.stringify(node);
}
return;
}
if (typeof node === 'number') {
resp += Math.round(node) || 0; // Only integers allowed
return;
}
lastType = node.type;
if (isLogging && node.sensitive) {
resp += '"(* value hidden *)"';
return;
}
switch (node.type.toUpperCase()) {
case 'LITERAL':
if (isLogging) {
resp += '"(* ' + node.value.length + 'B literal *)"';
} else {
if (!node.value) {
resp += '{0}\r\n';
} else {
resp += '{' + node.value.length + '}\r\n';
}
respParts.push(resp);
resp = node.value || '';
}
break;
case 'STRING':
if (isLogging && node.value.length > 20) {
resp += '"(* ' + node.value.length + 'B string *)"';
} else {
resp += JSON.stringify(node.value || '');
}
break;
case 'TEXT':
case 'SEQUENCE':
resp += node.value || '';
break;
case 'NUMBER':
resp += (node.value || 0);
break;
case 'ATOM':
case 'SECTION':
val = node.value || '';
if (imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
val = JSON.stringify(val);
}
resp += val;
if (node.section) {
resp += '[';
node.section.forEach(walk);
resp += ']';
}
if (node.partial) {
resp += '<' + node.partial.join('.') + '>';
}
break;
}
};
[].concat(response.attributes || []).forEach(walk);
if (resp.length) {
respParts.push(resp);
}
return asArray ? respParts : respParts.join('');
};
}));

View file

@ -0,0 +1,180 @@
// Copyright (c) 2013 Andris Reinman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.imapFormalSyntax = factory();
}
}(this, function() {
'use strict';
// IMAP Formal Syntax
// http://tools.ietf.org/html/rfc3501#section-9
function expandRange(start, end) {
var chars = [];
for (var i = start; i <= end; i++) {
chars.push(i);
}
return String.fromCharCode.apply(String, chars);
}
function excludeChars(source, exclude) {
var sourceArr = Array.prototype.slice.call(source);
for (var i = sourceArr.length - 1; i >= 0; i--) {
if (exclude.indexOf(sourceArr[i]) >= 0) {
sourceArr.splice(i, 1);
}
}
return sourceArr.join('');
}
return {
CHAR: function() {
var value = expandRange(0x01, 0x7F);
this.CHAR = function() {
return value;
};
return value;
},
CHAR8: function() {
var value = expandRange(0x01, 0xFF);
this.CHAR8 = function() {
return value;
};
return value;
},
SP: function() {
return ' ';
},
CTL: function() {
var value = expandRange(0x00, 0x1F) + '\x7F';
this.CTL = function() {
return value;
};
return value;
},
DQUOTE: function() {
return '"';
},
ALPHA: function() {
var value = expandRange(0x41, 0x5A) + expandRange(0x61, 0x7A);
this.ALPHA = function() {
return value;
};
return value;
},
DIGIT: function() {
var value = expandRange(0x30, 0x39) + expandRange(0x61, 0x7A);
this.DIGIT = function() {
return value;
};
return value;
},
'ATOM-CHAR': function() {
var value = excludeChars(this.CHAR(), this['atom-specials']());
this['ATOM-CHAR'] = function() {
return value;
};
return value;
},
'ASTRING-CHAR': function() {
var value = this['ATOM-CHAR']() + this['resp-specials']();
this['ASTRING-CHAR'] = function() {
return value;
};
return value;
},
'TEXT-CHAR': function() {
var value = excludeChars(this.CHAR(), '\r\n');
this['TEXT-CHAR'] = function() {
return value;
};
return value;
},
'atom-specials': function() {
var value = '(' + ')' + '{' + this.SP() + this.CTL() + this['list-wildcards']() +
this['quoted-specials']() + this['resp-specials']();
this['atom-specials'] = function() {
return value;
};
return value;
},
'list-wildcards': function() {
return '%' + '*';
},
'quoted-specials': function() {
var value = this.DQUOTE() + '\\';
this['quoted-specials'] = function() {
return value;
};
return value;
},
'resp-specials': function() {
return ']';
},
tag: function() {
var value = excludeChars(this['ASTRING-CHAR'](), '+');
this.tag = function() {
return value;
};
return value;
},
command: function() {
var value = this.ALPHA() + this.DIGIT();
this.command = function() {
return value;
};
return value;
},
verify: function(str, allowedChars) {
for (var i = 0, len = str.length; i < len; i++) {
if (allowedChars.indexOf(str.charAt(i)) < 0) {
return i;
}
}
return -1;
}
};
}));

View file

@ -0,0 +1,40 @@
// Copyright (c) 2013 Andris Reinman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// UMD shim, see: https://github.com/umdjs/umd/blob/master/returnExports.js
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(['./imap-parser', './imap-compiler'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('./imap-parser'), require('./imap-compiler'));
} else {
root.imapHandler = factory(root.imapParser, root.imapCompiler);
}
}(this, function(imapParser, imapCompiler) {
'use strict';
return {
parser: imapParser,
compiler: imapCompiler
};
}));

View file

@ -0,0 +1,656 @@
// Copyright (c) 2013 Andris Reinman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(['imap-formal-syntax'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('./imap-formal-syntax'));
} else {
root.imapParser = factory(root.imapFormalSyntax);
}
}(this, function(imapFormalSyntax) {
'use strict';
function ParserInstance(input, options) {
this.input = (input || '').toString();
this.options = options || {};
this.remainder = this.input;
this.pos = 0;
}
ParserInstance.prototype.getTag = function() {
if (!this.tag) {
this.tag = this.getElement(imapFormalSyntax.tag() + '*+', true);
}
return this.tag;
};
ParserInstance.prototype.getCommand = function() {
var responseCode;
if (!this.command) {
this.command = this.getElement(imapFormalSyntax.command());
}
switch ((this.command || '').toString().toUpperCase()) {
case 'OK':
case 'NO':
case 'BAD':
case 'PREAUTH':
case 'BYE':
responseCode = this.remainder.match(/^ \[(?:[^\]]*\])+/);
if (responseCode) {
this.humanReadable = this.remainder.substr(responseCode[0].length).trim();
this.remainder = responseCode[0];
} else {
this.humanReadable = this.remainder.trim();
this.remainder = '';
}
break;
}
return this.command;
};
ParserInstance.prototype.getElement = function(syntax) {
var match, element, errPos;
if (this.remainder.match(/^\s/)) {
throw new Error('Unexpected whitespace at position ' + this.pos);
}
if ((match = this.remainder.match(/^[^\s]+(?=\s|$)/))) {
element = match[0];
if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
throw new Error('Unexpected char at position ' + (this.pos + errPos));
}
} else {
throw new Error('Unexpected end of input at position ' + this.pos);
}
this.pos += match[0].length;
this.remainder = this.remainder.substr(match[0].length);
return element;
};
ParserInstance.prototype.getSpace = function() {
if (!this.remainder.length) {
throw new Error('Unexpected end of input at position ' + this.pos);
}
if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) {
throw new Error('Unexpected char at position ' + this.pos);
}
this.pos++;
this.remainder = this.remainder.substr(1);
};
ParserInstance.prototype.getAttributes = function() {
if (!this.remainder.length) {
throw new Error('Unexpected end of input at position ' + this.pos);
}
if (this.remainder.match(/^\s/)) {
throw new Error('Unexpected whitespace at position ' + this.pos);
}
return new TokenParser(this, this.pos, this.remainder, this.options).getAttributes();
};
function TokenParser(parent, startPos, str, options) {
this.str = (str || '').toString();
this.options = options || {};
this.parent = parent;
this.tree = this.currentNode = this.createNode();
this.pos = startPos || 0;
this.currentNode.type = 'TREE';
this.state = 'NORMAL';
this.processString();
}
TokenParser.prototype.getAttributes = function() {
var attributes = [],
branch = attributes;
var walk = function(node) {
var elm, curBranch = branch,
partial;
if (!node.closed && node.type === 'SEQUENCE' && node.value === '*') {
node.closed = true;
node.type = 'ATOM';
}
// If the node was never closed, throw it
if (!node.closed) {
throw new Error('Unexpected end of input at position ' + (this.pos + this.str.length - 1));
}
switch (node.type.toUpperCase()) {
case 'LITERAL':
case 'STRING':
case 'SEQUENCE':
elm = {
type: node.type.toUpperCase(),
value: node.value
};
branch.push(elm);
break;
case 'ATOM':
if (node.value.toUpperCase() === 'NIL') {
branch.push(null);
break;
}
elm = {
type: node.type.toUpperCase(),
value: node.value
};
branch.push(elm);
break;
case 'SECTION':
branch = branch[branch.length - 1].section = [];
break;
case 'LIST':
elm = [];
branch.push(elm);
branch = elm;
break;
case 'PARTIAL':
partial = node.value.split('.').map(Number);
if (partial.slice(-1)[0] < partial.slice(0, 1)[0]) {
throw new Error('Invalid partial value at position ' + node.startPos);
}
branch[branch.length - 1].partial = partial;
break;
}
node.childNodes.forEach(function(childNode) {
walk(childNode);
});
branch = curBranch;
}.bind(this);
walk(this.tree);
return attributes;
};
TokenParser.prototype.createNode = function(parentNode, startPos) {
var node = {
childNodes: [],
type: false,
value: '',
closed: true
};
if (parentNode) {
node.parentNode = parentNode;
}
if (typeof startPos === 'number') {
node.startPos = startPos;
}
if (parentNode) {
parentNode.childNodes.push(node);
}
return node;
};
TokenParser.prototype.processString = function() {
var chr, i, len,
checkSP = function() {
// jump to the next non whitespace pos
while (this.str.charAt(i + 1) === ' ') {
i++;
}
}.bind(this);
for (i = 0, len = this.str.length; i < len; i++) {
chr = this.str.charAt(i);
switch (this.state) {
case 'NORMAL':
switch (chr) {
// DQUOTE starts a new string
case '"':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'string';
this.state = 'STRING';
this.currentNode.closed = false;
break;
// ( starts a new list
case '(':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'LIST';
this.currentNode.closed = false;
break;
// ) closes a list
case ')':
if (this.currentNode.type !== 'LIST') {
throw new Error('Unexpected list terminator ) at position ' + (this.pos + i));
}
this.currentNode.closed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
checkSP();
break;
// ] closes section group
case ']':
if (this.currentNode.type !== 'SECTION') {
throw new Error('Unexpected section terminator ] at position ' + (this.pos + i));
}
this.currentNode.closed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
checkSP();
break;
// < starts a new partial
case '<':
if (this.str.charAt(i - 1) !== ']') {
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode.value = chr;
this.state = 'ATOM';
} else {
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'PARTIAL';
this.state = 'PARTIAL';
this.currentNode.closed = false;
}
break;
// { starts a new literal
case '{':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'LITERAL';
this.state = 'LITERAL';
this.currentNode.closed = false;
break;
// ( starts a new sequence
case '*':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'SEQUENCE';
this.currentNode.value = chr;
this.currentNode.closed = false;
this.state = 'SEQUENCE';
break;
// normally a space should never occur
case ' ':
// just ignore
break;
// [ starts section
case '[':
// If it is the *first* element after response command, then process as a response argument list
if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].indexOf(this.parent.command.toUpperCase()) >= 0 && this.currentNode === this.tree) {
this.currentNode.endPos = this.pos + i;
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'SECTION';
this.currentNode.closed = false;
this.state = 'NORMAL';
// RFC2221 defines a response code REFERRAL whose payload is an
// RFC2192/RFC5092 imapurl that we will try to parse as an ATOM but
// fail quite badly at parsing. Since the imapurl is such a unique
// (and crazy) term, we just specialize that case here.
if (this.str.substr(i + 1, 9).toUpperCase() === 'REFERRAL ') {
// create the REFERRAL atom
this.currentNode = this.createNode(this.currentNode, this.pos + i + 1);
this.currentNode.type = 'ATOM';
this.currentNode.endPos = this.pos + i + 8;
this.currentNode.value = 'REFERRAL';
this.currentNode = this.currentNode.parentNode;
// eat all the way through the ] to be the IMAPURL token.
this.currentNode = this.createNode(this.currentNode, this.pos + i + 10);
// just call this an ATOM, even though IMAPURL might be more correct
this.currentNode.type = 'ATOM';
// jump i to the ']'
i = this.str.indexOf(']', i + 10);
this.currentNode.endPos = this.pos + i - 1;
this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos,
this.currentNode.endPos - this.pos + 1);
this.currentNode = this.currentNode.parentNode;
// close out the SECTION
this.currentNode.closed = true;
this.currentNode = this.currentNode.parentNode;
checkSP();
}
break;
}
/* falls through */
default:
// Any ATOM supported char starts a new Atom sequence, otherwise throw an error
// Allow \ as the first char for atom to support system flags
// Allow % to support LIST '' %
if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%') {
throw new Error('Unexpected char at position ' + (this.pos + i));
}
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode.value = chr;
this.state = 'ATOM';
break;
}
break;
case 'ATOM':
// space finishes an atom
if (chr === ' ') {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.state = 'NORMAL';
break;
}
//
if (
this.currentNode.parentNode &&
(
(chr === ')' && this.currentNode.parentNode.type === 'LIST') ||
(chr === ']' && this.currentNode.parentNode.type === 'SECTION')
)
) {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.currentNode.closed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
this.state = 'NORMAL';
checkSP();
break;
}
if ((chr === ',' || chr === ':') && this.currentNode.value.match(/^\d+$/)) {
this.currentNode.type = 'SEQUENCE';
this.currentNode.closed = true;
this.state = 'SEQUENCE';
}
// [ starts a section group for this element
if (chr === '[') {
// allowed only for selected elements
if (['BODY', 'BODY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) < 0) {
throw new Error('Unexpected section start char [ at position ' + this.pos);
}
this.currentNode.endPos = this.pos + i;
this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
this.currentNode.type = 'SECTION';
this.currentNode.closed = false;
this.state = 'NORMAL';
break;
}
if (chr === '<') {
throw new Error('Unexpected start of partial at position ' + this.pos);
}
// if the char is not ATOM compatible, throw. Allow \* as an exception
if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== ']' && !(chr === '*' && this.currentNode.value === '\\')) {
throw new Error('Unexpected char at position ' + (this.pos + i));
} else if (this.currentNode.value === '\\*') {
throw new Error('Unexpected char at position ' + (this.pos + i));
}
this.currentNode.value += chr;
break;
case 'STRING':
// DQUOTE ends the string sequence
if (chr === '"') {
this.currentNode.endPos = this.pos + i;
this.currentNode.closed = true;
this.currentNode = this.currentNode.parentNode;
this.state = 'NORMAL';
checkSP();
break;
}
// \ Escapes the following char
if (chr === '\\') {
i++;
if (i >= len) {
throw new Error('Unexpected end of input at position ' + (this.pos + i));
}
chr = this.str.charAt(i);
}
/* // skip this check, otherwise the parser might explode on binary input
if (imapFormalSyntax['TEXT-CHAR']().indexOf(chr) < 0) {
throw new Error('Unexpected char at position ' + (this.pos + i));
}
*/
this.currentNode.value += chr;
break;
case 'PARTIAL':
if (chr === '>') {
if (this.currentNode.value.substr(-1) === '.') {
throw new Error('Unexpected end of partial at position ' + this.pos);
}
this.currentNode.endPos = this.pos + i;
this.currentNode.closed = true;
this.currentNode = this.currentNode.parentNode;
this.state = 'NORMAL';
checkSP();
break;
}
if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
throw new Error('Unexpected partial separator . at position ' + this.pos);
}
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
throw new Error('Unexpected char at position ' + (this.pos + i));
}
if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
throw new Error('Invalid partial at position ' + (this.pos + i));
}
this.currentNode.value += chr;
break;
case 'LITERAL':
if (this.currentNode.started) {
//if(imapFormalSyntax['CHAR8']().indexOf(chr) < 0){
if (chr === '\u0000') {
throw new Error('Unexpected \\x00 at position ' + (this.pos + i));
}
this.currentNode.value += chr;
if (this.currentNode.value.length >= this.currentNode.literalLength) {
this.currentNode.endPos = this.pos + i;
this.currentNode.closed = true;
this.currentNode = this.currentNode.parentNode;
this.state = 'NORMAL';
checkSP();
}
break;
}
if (chr === '+' && this.options.literalPlus) {
this.currentNode.literalPlus = true;
break;
}
if (chr === '}') {
if (!('literalLength' in this.currentNode)) {
throw new Error('Unexpected literal prefix end char } at position ' + (this.pos + i));
}
if (this.str.charAt(i + 1) === '\n') {
i++;
} else if (this.str.charAt(i + 1) === '\r' && this.str.charAt(i + 2) === '\n') {
i += 2;
} else {
throw new Error('Unexpected char at position ' + (this.pos + i));
}
this.currentNode.literalLength = Number(this.currentNode.literalLength);
this.currentNode.started = true;
if (!this.currentNode.literalLength) {
// special case where literal content length is 0
// close the node right away, do not wait for additional input
this.currentNode.endPos = this.pos + i;
this.currentNode.closed = true;
this.currentNode = this.currentNode.parentNode;
this.state = 'NORMAL';
checkSP();
}
break;
}
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
throw new Error('Unexpected char at position ' + (this.pos + i));
}
if (this.currentNode.literalLength === '0') {
throw new Error('Invalid literal at position ' + (this.pos + i));
}
this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
break;
case 'SEQUENCE':
// space finishes the sequence set
if (chr === ' ') {
if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
throw new Error('Unexpected whitespace at position ' + (this.pos + i));
}
if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
throw new Error('Unexpected whitespace at position ' + (this.pos + i));
}
this.currentNode.closed = true;
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.state = 'NORMAL';
break;
} else if (this.currentNode.parentNode &&
chr === ']' &&
this.currentNode.parentNode.type === 'SECTION') {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.currentNode.closed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
this.state = 'NORMAL';
checkSP();
break;
}
if (chr === ':') {
if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
throw new Error('Unexpected range separator : at position ' + (this.pos + i));
}
} else if (chr === '*') {
if ([',', ':'].indexOf(this.currentNode.value.substr(-1)) < 0) {
throw new Error('Unexpected range wildcard at position ' + (this.pos + i));
}
} else if (chr === ',') {
if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
}
if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
}
} else if (!chr.match(/\d/)) {
throw new Error('Unexpected char at position ' + (this.pos + i));
}
if (chr.match(/\d/) && this.currentNode.value.substr(-1) === '*') {
throw new Error('Unexpected number at position ' + (this.pos + i));
}
this.currentNode.value += chr;
break;
}
}
};
return function(command, options) {
var parser, response = {};
options = options || {};
parser = new ParserInstance(command, options);
response.tag = parser.getTag();
parser.getSpace();
response.command = parser.getCommand();
if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) {
parser.getSpace();
response.command += ' ' + parser.getElement(imapFormalSyntax.command());
}
if (parser.remainder.trim().length) {
parser.getSpace();
response.attributes = parser.getAttributes();
}
if (parser.humanReadable) {
response.attributes = (response.attributes || []).concat({
type: 'TEXT',
value: parser.humanReadable
});
}
return response;
};
}));

View file

@ -0,0 +1,730 @@
// Copyright (c) 2013 Andris Reinman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(['mimefuncs', 'mimetypes', 'punycode', 'addressparser'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('mimefuncs'), require('mimetypes'), require('punycode'), require('wo-addressparser'));
} else {
root.mailbuild = factory(mimefuncs, mimetypes, punycode, addressparser);
}
}(this, function(mimefuncs, mimetypes, punycode, addressparser) {
'use strict';
/**
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
* if it is a branch, anything else counts as leaf. If rootNode is missing from
* the options, assumes this is the root.
*
* @param {String} contentType Define the content type for the node. Can be left blank for attachments (derived from filename)
* @param {Object} [options] optional options
* @param {Object} [options.rootNode] root node for this tree
* @param {Object} [options.parentNode] immediate parent for this node
* @param {Object} [options.filename] filename for an attachment node
* @param {String} [options.baseBoundary] shared part of the unique multipart boundary
*/
function MimeNode(contentType, options) {
this.nodeCounter = 0;
options = options || {};
/**
* shared part of the unique multipart boundary
*/
this.baseBoundary = options.baseBoundary || Date.now().toString() + Math.random();
/**
* If date headers is missing and current node is the root, this value is used instead
*/
this.date = new Date();
/**
* Root node for current mime tree
*/
this.rootNode = options.rootNode || this;
/**
* If filename is specified but contentType is not (probably an attachment)
* detect the content type from filename extension
*/
if (options.filename) {
/**
* Filename for this node. Useful with attachments
*/
this.filename = options.filename;
if (!contentType) {
contentType = mimetypes.detectMimeType(this.filename.split('.').pop());
}
}
/**
* Immediate parent for this node (or undefined if not set)
*/
this.parentNode = options.parentNode;
/**
* Used for generating unique boundaries (prepended to the shared base)
*/
this._nodeId = ++this.rootNode.nodeCounter;
/**
* An array for possible child nodes
*/
this._childNodes = [];
/**
* A list of header values for this node in the form of [{key:'', value:''}]
*/
this._headers = [];
/**
* If content type is set (or derived from the filename) add it to headers
*/
if (contentType) {
this.setHeader('content-type', contentType);
}
}
/////// PUBLIC METHODS
/**
* Creates and appends a child node. Arguments provided are passed to MimeNode constructor
*
* @param {String} [contentType] Optional content type
* @param {Object} [options] Optional options object
* @return {Object} Created node object
*/
MimeNode.prototype.createChild = function(contentType, options) {
if (!options && typeof contentType === 'object') {
options = contentType;
contentType = undefined;
}
var node = new MimeNode(contentType, options);
this.appendChild(node);
return node;
};
/**
* Appends an existing node to the mime tree. Removes the node from an existing
* tree if needed
*
* @param {Object} childNode node to be appended
* @return {Object} Appended node object
*/
MimeNode.prototype.appendChild = function(childNode) {
if (childNode.rootNode !== this.rootNode) {
childNode.rootNode = this.rootNode;
childNode._nodeId = ++this.rootNode.nodeCounter;
}
childNode.parentNode = this;
this._childNodes.push(childNode);
return childNode;
};
/**
* Replaces current node with another node
*
* @param {Object} node Replacement node
* @return {Object} Replacement node
*/
MimeNode.prototype.replace = function(node) {
if (node === this) {
return this;
}
this.parentNode._childNodes.forEach(function(childNode, i) {
if (childNode === this) {
node.rootNode = this.rootNode;
node.parentNode = this.parentNode;
node._nodeId = this._nodeId;
this.rootNode = this;
this.parentNode = undefined;
node.parentNode._childNodes[i] = node;
}
}.bind(this));
return node;
};
/**
* Removes current node from the mime tree
*
* @return {Object} removed node
*/
MimeNode.prototype.remove = function() {
if (!this.parentNode) {
return this;
}
for (var i = this.parentNode._childNodes.length - 1; i >= 0; i--) {
if (this.parentNode._childNodes[i] === this) {
this.parentNode._childNodes.splice(i, 1);
this.parentNode = undefined;
this.rootNode = this;
return this;
}
}
};
/**
* Sets a header value. If the value for selected key exists, it is overwritten.
* You can set multiple values as well by using [{key:'', value:''}] or
* {key: 'value'} as the first argument.
*
* @param {String|Array|Object} key Header key or a list of key value pairs
* @param {String} value Header value
* @return {Object} current node
*/
MimeNode.prototype.setHeader = function(key, value) {
var added = false,
headerValue;
// Allow setting multiple headers at once
if (!value && key && typeof key === 'object') {
// allow {key:'content-type', value: 'text/plain'}
if (key.key && key.value) {
this.setHeader(key.key, key.value);
}
// allow [{key:'content-type', value: 'text/plain'}]
else if (Array.isArray(key)) {
key.forEach(function(i) {
this.setHeader(i.key, i.value);
}.bind(this));
}
// allow {'content-type': 'text/plain'}
else {
Object.keys(key).forEach(function(i) {
this.setHeader(i, key[i]);
}.bind(this));
}
return this;
}
key = this._normalizeHeaderKey(key);
headerValue = {
key: key,
value: value
};
// Check if the value exists and overwrite
for (var i = 0, len = this._headers.length; i < len; i++) {
if (this._headers[i].key === key) {
if (!added) {
// replace the first match
this._headers[i] = headerValue;
added = true;
} else {
// remove following matches
this._headers.splice(i, 1);
i--;
len--;
}
}
}
// match not found, append the value
if (!added) {
this._headers.push(headerValue);
}
return this;
};
/**
* Adds a header value. If the value for selected key exists, the value is appended
* as a new field and old one is not touched.
* You can set multiple values as well by using [{key:'', value:''}] or
* {key: 'value'} as the first argument.
*
* @param {String|Array|Object} key Header key or a list of key value pairs
* @param {String} value Header value
* @return {Object} current node
*/
MimeNode.prototype.addHeader = function(key, value) {
// Allow setting multiple headers at once
if (!value && key && typeof key === 'object') {
// allow {key:'content-type', value: 'text/plain'}
if (key.key && key.value) {
this.addHeader(key.key, key.value);
}
// allow [{key:'content-type', value: 'text/plain'}]
else if (Array.isArray(key)) {
key.forEach(function(i) {
this.addHeader(i.key, i.value);
}.bind(this));
}
// allow {'content-type': 'text/plain'}
else {
Object.keys(key).forEach(function(i) {
this.addHeader(i, key[i]);
}.bind(this));
}
return this;
}
this._headers.push({
key: this._normalizeHeaderKey(key),
value: value
});
return this;
};
/**
* Retrieves the first mathcing value of a selected key
*
* @param {String} key Key to search for
* @retun {String} Value for the key
*/
MimeNode.prototype.getHeader = function(key) {
key = this._normalizeHeaderKey(key);
for (var i = 0, len = this._headers.length; i < len; i++) {
if (this._headers[i].key === key) {
return this._headers[i].value;
}
}
};
/**
* Sets body content for current node. If the value is a string, charset is added automatically
* to Content-Type (if it is text/*). If the value is a Typed Array, you need to specify
* the charset yourself
*
* @param (String|Uint8Array) content Body content
* @return {Object} current node
*/
MimeNode.prototype.setContent = function(content) {
this.content = content;
return this;
};
/**
* Builds the rfc2822 message from the current node. If this is a root node,
* mandatory header fields are set if missing (Date, Message-Id, MIME-Version)
*
* @return {String} Compiled message
*/
MimeNode.prototype.build = function() {
var lines = [],
contentType = (this.getHeader('Content-Type') || '').toString().toLowerCase().trim(),
transferEncoding, flowed;
if (this.content) {
transferEncoding = (this.getHeader('Content-Transfer-Encoding') || '').toString().toLowerCase().trim();
if (!transferEncoding || ['base64', 'quoted-printable'].indexOf(transferEncoding) < 0) {
if (/^text\//i.test(contentType)) {
// If there are no special symbols, no need to modify the text
if (this._isPlainText(this.content)) {
// If there are lines longer than 76 symbols/bytes, make the text 'flowed'
if (/^.{77,}/m.test(this.content)) {
flowed = true;
}
transferEncoding = '7bit';
} else {
transferEncoding = 'quoted-printable';
}
} else if (!/^multipart\//i.test(contentType)) {
transferEncoding = transferEncoding || 'base64';
}
}
if (transferEncoding) {
this.setHeader('Content-Transfer-Encoding', transferEncoding);
}
}
if (this.filename && !this.getHeader('Content-Disposition')) {
this.setHeader('Content-Disposition', 'attachment');
}
this._headers.forEach(function(header) {
var key = header.key,
value = header.value,
structured;
switch (header.key) {
case 'Content-Disposition':
structured = mimefuncs.parseHeaderValue(value);
if (this.filename) {
structured.params.filename = this.filename;
}
value = this._buildHeaderValue(structured);
break;
case 'Content-Type':
structured = mimefuncs.parseHeaderValue(value);
this._handleContentType(structured);
if (flowed) {
structured.params.format = 'flowed';
}
if (String(structured.params.format).toLowerCase().trim() === 'flowed') {
flowed = true;
}
if (structured.value.match(/^text\//) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
structured.params.charset = 'utf-8';
}
value = this._buildHeaderValue(structured);
break;
case 'Bcc':
// skip BCC values
return;
}
// skip empty lines
value = this._encodeHeaderValue(key, value);
if (!(value || '').toString().trim()) {
return;
}
lines.push(mimefuncs.foldLines(key + ': ' + value, 76));
}.bind(this));
// Ensure mandatory header fields
if (this.rootNode === this) {
if (!this.getHeader('Date')) {
lines.push('Date: ' + this.date.toUTCString().replace(/GMT/, '+0000'));
}
// You really should define your own Message-Id field
if (!this.getHeader('Message-Id')) {
lines.push('Message-Id: <' +
// crux to generate random strings like this:
// "1401391905590-58aa8c32-d32a065c-c1a2aad2"
[0, 0, 0].reduce(function(prev) {
return prev + '-' + Math.floor((1 + Math.random()) * 0x100000000).
toString(16).
substring(1);
}, Date.now()) +
'@' +
// try to use the domain of the FROM address or fallback localhost
(this.getEnvelope().from || 'localhost').split('@').pop() +
'>');
}
if (!this.getHeader('MIME-Version')) {
lines.push('MIME-Version: 1.0');
}
}
lines.push('');
if (this.content) {
switch (transferEncoding) {
case 'quoted-printable':
lines.push(mimefuncs.quotedPrintableEncode(this.content));
break;
case 'base64':
lines.push(mimefuncs.base64Encode(this.content, typeof this.content === 'object' && 'binary' || false));
break;
default:
if (flowed) {
lines.push(mimefuncs.foldLines(this.content.replace(/\r?\n/g, '\r\n').
// space stuffing http://tools.ietf.org/html/rfc3676#section-4.2
replace(/^( |From|>)/igm, ' $1'),
76, true));
} else {
lines.push(this.content.replace(/\r?\n/g, '\r\n'));
}
}
if (this.multipart) {
lines.push('');
}
}
if (this.multipart) {
this._childNodes.forEach(function(node) {
lines.push('--' + this.boundary);
lines.push(node.build());
}.bind(this));
lines.push('--' + this.boundary + '--');
lines.push('');
}
return lines.join('\r\n');
};
/**
* Generates and returns SMTP envelope with the sender address and a list of recipients addresses
*
* @return {Object} SMTP envelope in the form of {from: 'from@example.com', to: ['to@example.com']}
*/
MimeNode.prototype.getEnvelope = function() {
var envelope = {
from: false,
to: []
};
this._headers.forEach(function(header) {
var list = [];
if (header.key === 'From' || (!envelope.from && ['Reply-To', 'Sender'].indexOf(header.key) >= 0)) {
this._convertAddresses(this._parseAddresses(header.value), list);
if (list.length && list[0]) {
envelope.from = list[0];
}
} else if (['To', 'Cc', 'Bcc'].indexOf(header.key) >= 0) {
this._convertAddresses(this._parseAddresses(header.value), envelope.to);
}
}.bind(this));
return envelope;
};
/////// PRIVATE METHODS
/**
* Parses addresses. Takes in a single address or an array or an
* array of address arrays (eg. To: [[first group], [second group],...])
*
* @param {Mixed} addresses Addresses to be parsed
* @return {Array} An array of address objects
*/
MimeNode.prototype._parseAddresses = function(addresses) {
return [].concat.apply([], [].concat(addresses).map(function(address) {
if (address && address.address) {
address = this._convertAddresses(address);
}
return addressparser.parse(address);
}.bind(this)));
};
/**
* Normalizes a header key, uses Camel-Case form, except for uppercase MIME-
*
* @param {String} key Key to be normalized
* @return {String} key in Camel-Case form
*/
MimeNode.prototype._normalizeHeaderKey = function(key) {
return (key || '').toString().
// no newlines in keys
replace(/\r?\n|\r/g, ' ').
trim().toLowerCase().
// use uppercase words, except MIME
replace(/^MIME\b|^[a-z]|\-[a-z]/ig, function(c) {
return c.toUpperCase();
});
};
/**
* Joins parsed header value together as 'value; param1=value1; param2=value2'
*
* @param {Object} structured Parsed header value
* @return {String} joined header value
*/
MimeNode.prototype._buildHeaderValue = function(structured) {
var paramsArray = [];
Object.keys(structured.params || {}).forEach(function(param) {
// filename might include unicode characters so it is a special case
if (param === 'filename') {
mimefuncs.continuationEncode(param, structured.params[param], 50).forEach(function(encodedParam) {
// continuation encoded strings are always escaped, so no need to use enclosing quotes
// in fact using quotes might end up with invalid filenames in some clients
paramsArray.push(encodedParam.key + '=' + encodedParam.value);
});
} else {
paramsArray.push(param + '=' + this._escapeHeaderArgument(structured.params[param]));
}
}.bind(this));
return structured.value + (paramsArray.length ? '; ' + paramsArray.join('; ') : '');
};
/**
* Escapes a header argument value (eg. boundary value for content type),
* adds surrounding quotes if needed
*
* @param {String} value Header argument value
* @return {String} escaped and quoted (if needed) argument value
*/
MimeNode.prototype._escapeHeaderArgument = function(value) {
if (value.match(/[\s'"\\;\/=]|^\-/g)) {
return '"' + value.replace(/(["\\])/g, "\\$1") + '"';
} else {
return value;
}
};
/**
* Checks if the content type is multipart and defines boundary if needed.
* Doesn't return anything, modifies object argument instead.
*
* @param {Object} structured Parsed header value for 'Content-Type' key
*/
MimeNode.prototype._handleContentType = function(structured) {
this.contentType = structured.value.trim().toLowerCase();
this.multipart = this.contentType.split('/').reduce(function(prev, value) {
return prev === 'multipart' ? value : false;
});
if (this.multipart) {
this.boundary = structured.params.boundary = structured.params.boundary || this.boundary || this._generateBoundary();
} else {
this.boundary = false;
}
};
/**
* Generates a multipart boundary value
*
* @return {String} boundary value
*/
MimeNode.prototype._generateBoundary = function() {
return '----sinikael-?=_' + this._nodeId + '-' + this.rootNode.baseBoundary;
};
/**
* Encodes a header value for use in the generated rfc2822 email.
*
* @param {String} key Header key
* @param {String} value Header value
*/
MimeNode.prototype._encodeHeaderValue = function(key, value) {
key = this._normalizeHeaderKey(key);
switch (key) {
case 'From':
case 'Sender':
case 'To':
case 'Cc':
case 'Bcc':
case 'Reply-To':
return this._convertAddresses(this._parseAddresses(value));
case 'Message-Id':
case 'In-Reply-To':
case 'Content-Id':
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
if (value.charAt(0) !== '<') {
value = '<' + value;
}
if (value.charAt(value.length - 1) !== '>') {
value = value + '>';
}
return value;
case 'References':
value = [].concat.apply([], [].concat(value || '').map(function(elm) {
elm = (elm || '').toString().replace(/\r?\n|\r/g, ' ').trim();
return elm.replace(/<[^>]*>/g, function(str) {
return str.replace(/\s/g, '');
}).split(/\s+/);
})).map(function(elm) {
if (elm.charAt(0) !== '<') {
elm = '<' + elm;
}
if (elm.charAt(elm.length - 1) !== '>') {
elm = elm + '>';
}
return elm;
});
return value.join(' ').trim();
default:
value = (value || '').toString().replace(/\r?\n|\r/g, ' ');
// mimeWordsEncode only encodes if needed, otherwise the original string is returned
return mimefuncs.mimeWordsEncode(value, 'Q', 52);
}
};
/**
* Rebuilds address object using punycode and other adjustments
*
* @param {Array} addresses An array of address objects
* @param {Array} [uniqueList] An array to be populated with addresses
* @return {String} address string
*/
MimeNode.prototype._convertAddresses = function(addresses, uniqueList) {
var values = [];
uniqueList = uniqueList || [];
[].concat(addresses || []).forEach(function(address) {
if (address.address) {
address.address = address.address.replace(/^.*?(?=\@)/, function(user) {
return mimefuncs.mimeWordsEncode(user, 'Q', 52);
}).replace(/@.+$/, function(domain) {
return '@' + punycode.toASCII(domain.substr(1));
});
if (!address.name) {
values.push(address.address);
} else if (address.name) {
values.push(this._encodeAddressName(address.name) + ' <' + address.address + '>');
}
if (uniqueList.indexOf(address.address) < 0) {
uniqueList.push(address.address);
}
} else if (address.group) {
values.push(this._encodeAddressName(address.name) + ':' + (address.group.length ? this._convertAddresses(address.group, uniqueList) : '').trim() + ';');
}
}.bind(this));
return values.join(', ');
};
/**
* If needed, mime encodes the name part
*
* @param {String} name Name part of an address
* @returns {String} Mime word encoded string if needed
*/
MimeNode.prototype._encodeAddressName = function(name) {
if (!/^[\w ']*$/.test(name)) {
if (/^[\x20-\x7e]*$/.test(name)) {
return '"' + name.replace(/([\\"])/g, '\\$1') + '"';
} else {
return mimefuncs.mimeWordEncode(name, 'Q', 52);
}
}
return name;
};
/**
* Checks if a value is plaintext string (uses only printable 7bit chars)
*
* @param {String} value String to be tested
* @returns {Boolean} true if it is a plaintext string
*/
MimeNode.prototype._isPlainText = function(value) {
if (typeof value !== 'string' || /[\x00-\x08\x0b\x0c\x0e-\x1f\u0080-\uFFFF]/.test(value)) {
return false;
} else {
return true;
}
};
return MimeNode;
}));

387
gaia/apps/email/js/ext/ext/md5.js Executable file
View file

@ -0,0 +1,387 @@
define(function(require, exports, module) {
module.exports = function md5(data) {
return hex_md5(data);
};
/*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
/*
* Configurable variables. You may need to tweak these to be compatible with
* the server-side, but the defaults work in most cases.
*/
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */
/*
* These are the functions you'll usually want to call
* They take string arguments and return either hex or base-64 encoded strings
*/
function hex_md5(s) { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
function b64_md5(s) { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
function hex_hmac_md5(k, d)
{ return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
function b64_hmac_md5(k, d)
{ return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
function any_hmac_md5(k, d, e)
{ return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }
/*
* Perform a simple self-test to see if the VM is working
*/
function md5_vm_test()
{
return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
}
/*
* Calculate the MD5 of a raw string
*/
function rstr_md5(s)
{
return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
}
/*
* Calculate the HMAC-MD5, of a key and some data (raw strings)
*/
function rstr_hmac_md5(key, data)
{
var bkey = rstr2binl(key);
if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);
var ipad = Array(16), opad = Array(16);
for(var i = 0; i < 16; i++)
{
ipad[i] = bkey[i] ^ 0x36363636;
opad[i] = bkey[i] ^ 0x5C5C5C5C;
}
var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
}
/*
* Convert a raw string to a hex string
*/
function rstr2hex(input)
{
try { hexcase } catch(e) { hexcase=0; }
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var output = "";
var x;
for(var i = 0; i < input.length; i++)
{
x = input.charCodeAt(i);
output += hex_tab.charAt((x >>> 4) & 0x0F)
+ hex_tab.charAt( x & 0x0F);
}
return output;
}
/*
* Convert a raw string to a base-64 string
*/
function rstr2b64(input)
{
try { b64pad } catch(e) { b64pad=''; }
var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var output = "";
var len = input.length;
for(var i = 0; i < len; i += 3)
{
var triplet = (input.charCodeAt(i) << 16)
| (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
| (i + 2 < len ? input.charCodeAt(i+2) : 0);
for(var j = 0; j < 4; j++)
{
if(i * 8 + j * 6 > input.length * 8) output += b64pad;
else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
}
}
return output;
}
/*
* Convert a raw string to an arbitrary string encoding
*/
function rstr2any(input, encoding)
{
var divisor = encoding.length;
var i, j, q, x, quotient;
/* Convert to an array of 16-bit big-endian values, forming the dividend */
var dividend = Array(Math.ceil(input.length / 2));
for(i = 0; i < dividend.length; i++)
{
dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
}
/*
* Repeatedly perform a long division. The binary array forms the dividend,
* the length of the encoding is the divisor. Once computed, the quotient
* forms the dividend for the next step. All remainders are stored for later
* use.
*/
var full_length = Math.ceil(input.length * 8 /
(Math.log(encoding.length) / Math.log(2)));
var remainders = Array(full_length);
for(j = 0; j < full_length; j++)
{
quotient = Array();
x = 0;
for(i = 0; i < dividend.length; i++)
{
x = (x << 16) + dividend[i];
q = Math.floor(x / divisor);
x -= q * divisor;
if(quotient.length > 0 || q > 0)
quotient[quotient.length] = q;
}
remainders[j] = x;
dividend = quotient;
}
/* Convert the remainders to the output string */
var output = "";
for(i = remainders.length - 1; i >= 0; i--)
output += encoding.charAt(remainders[i]);
return output;
}
/*
* Encode a string as utf-8.
* For efficiency, this assumes the input is valid utf-16.
*/
function str2rstr_utf8(input)
{
var output = "";
var i = -1;
var x, y;
while(++i < input.length)
{
/* Decode utf-16 surrogate pairs */
x = input.charCodeAt(i);
y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
{
x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
i++;
}
/* Encode output as utf-8 */
if(x <= 0x7F)
output += String.fromCharCode(x);
else if(x <= 0x7FF)
output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
0x80 | ( x & 0x3F));
else if(x <= 0xFFFF)
output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
0x80 | ((x >>> 6 ) & 0x3F),
0x80 | ( x & 0x3F));
else if(x <= 0x1FFFFF)
output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
0x80 | ((x >>> 12) & 0x3F),
0x80 | ((x >>> 6 ) & 0x3F),
0x80 | ( x & 0x3F));
}
return output;
}
/*
* Encode a string as utf-16
*/
function str2rstr_utf16le(input)
{
var output = "";
for(var i = 0; i < input.length; i++)
output += String.fromCharCode( input.charCodeAt(i) & 0xFF,
(input.charCodeAt(i) >>> 8) & 0xFF);
return output;
}
function str2rstr_utf16be(input)
{
var output = "";
for(var i = 0; i < input.length; i++)
output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
input.charCodeAt(i) & 0xFF);
return output;
}
/*
* Convert a raw string to an array of little-endian words
* Characters >255 have their high-byte silently ignored.
*/
function rstr2binl(input)
{
var output = Array(input.length >> 2);
for(var i = 0; i < output.length; i++)
output[i] = 0;
for(var i = 0; i < input.length * 8; i += 8)
output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
return output;
}
/*
* Convert an array of little-endian words to a string
*/
function binl2rstr(input)
{
var output = "";
for(var i = 0; i < input.length * 32; i += 8)
output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
return output;
}
/*
* Calculate the MD5 of an array of little-endian words, and a bit length.
*/
function binl_md5(x, len)
{
/* append padding */
x[len >> 5] |= 0x80 << ((len) % 32);
x[(((len + 64) >>> 9) << 4) + 14] = len;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
for(var i = 0; i < x.length; i += 16)
{
var olda = a;
var oldb = b;
var oldc = c;
var oldd = d;
a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
a = safe_add(a, olda);
b = safe_add(b, oldb);
c = safe_add(c, oldc);
d = safe_add(d, oldd);
}
return Array(a, b, c, d);
}
/*
* These functions implement the four basic operations the algorithm uses.
*/
function md5_cmn(q, a, b, x, s, t)
{
return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}
/*
* Add integers, wrapping at 2^32. This uses 16-bit operations internally
* to work around bugs in some JS interpreters.
*/
function safe_add(x, y)
{
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
/*
* Bitwise rotate a 32-bit number to the left.
*/
function bit_rol(num, cnt)
{
return (num << cnt) | (num >>> (32 - cnt));
}
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,193 @@
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.tzabbr = factory();
}
}(this, function() {
'use strict';
return {
"ACDT": "+1030",
"ACST": "+0930",
"ACT": "+0800",
"ADT": "-0300",
"AEDT": "+1100",
"AEST": "+1000",
"AFT": "+0430",
"AKDT": "-0800",
"AKST": "-0900",
"AMST": "-0300",
"AMT": "+0400",
"ART": "-0300",
"AST": "+0300",
"AWDT": "+0900",
"AWST": "+0800",
"AZOST": "-0100",
"AZT": "+0400",
"BDT": "+0800",
"BIOT": "+0600",
"BIT": "-1200",
"BOT": "-0400",
"BRT": "-0300",
"BST": "+0600",
"BTT": "+0600",
"CAT": "+0200",
"CCT": "+0630",
"CDT": "-0500",
"CEDT": "+0200",
"CEST": "+0200",
"CET": "+0100",
"CHADT": "+1345",
"CHAST": "+1245",
"CHOT": "+0800",
"CHST": "+1000",
"CHUT": "+1000",
"CIST": "-0800",
"CIT": "+0800",
"CKT": "-1000",
"CLST": "-0300",
"CLT": "-0400",
"COST": "-0400",
"COT": "-0500",
"CST": "-0600",
"CT": "+0800",
"CVT": "-0100",
"CWST": "+0845",
"CXT": "+0700",
"DAVT": "+0700",
"DDUT": "+1000",
"DFT": "+0100",
"EASST": "-0500",
"EAST": "-0600",
"EAT": "+0300",
"ECT": "-0500",
"EDT": "-0400",
"EEDT": "+0300",
"EEST": "+0300",
"EET": "+0200",
"EGST": "+0000",
"EGT": "-0100",
"EIT": "+0900",
"EST": "-0500",
"FET": "+0300",
"FJT": "+1200",
"FKST": "-0300",
"FKT": "-0400",
"FNT": "-0200",
"GALT": "-0600",
"GAMT": "-0900",
"GET": "+0400",
"GFT": "-0300",
"GILT": "+1200",
"GIT": "-0900",
"GMT": "+0000",
"GST": "+0400",
"GYT": "-0400",
"HADT": "-0900",
"HAEC": "+0200",
"HAST": "-1000",
"HKT": "+0800",
"HMT": "+0500",
"HOVT": "+0700",
"HST": "-1000",
"ICT": "+0700",
"IDT": "+0300",
"IOT": "+0300",
"IRDT": "+0430",
"IRKT": "+0900",
"IRST": "+0330",
"IST": "+0530",
"JST": "+0900",
"KGT": "+0600",
"KOST": "+1100",
"KRAT": "+0700",
"KST": "+0900",
"LHST": "+1030",
"LINT": "+1400",
"MAGT": "+1200",
"MART": "-0930",
"MAWT": "+0500",
"MDT": "-0600",
"MET": "+0100",
"MEST": "+0200",
"MHT": "+1200",
"MIST": "+1100",
"MIT": "-0930",
"MMT": "+0630",
"MSK": "+0400",
"MST": "-0700",
"MUT": "+0400",
"MVT": "+0500",
"MYT": "+0800",
"NCT": "+1100",
"NDT": "-0230",
"NFT": "+1130",
"NPT": "+0545",
"NST": "-0330",
"NT": "-0330",
"NUT": "-1100",
"NZDT": "+1300",
"NZST": "+1200",
"OMST": "+0700",
"ORAT": "+0500",
"PDT": "-0700",
"PET": "-0500",
"PETT": "+1200",
"PGT": "+1000",
"PHOT": "+1300",
"PHT": "+0800",
"PKT": "+0500",
"PMDT": "-0200",
"PMST": "-0300",
"PONT": "+1100",
"PST": "-0800",
"PYST": "-0300",
"PYT": "-0400",
"RET": "+0400",
"ROTT": "-0300",
"SAKT": "+1100",
"SAMT": "+0400",
"SAST": "+0200",
"SBT": "+1100",
"SCT": "+0400",
"SGT": "+0800",
"SLST": "+0530",
"SRT": "-0300",
"SST": "+0800",
"SYOT": "+0300",
"TAHT": "-1000",
"THA": "+0700",
"TFT": "+0500",
"TJT": "+0500",
"TKT": "+1300",
"TLT": "+0900",
"TMT": "+0500",
"TOT": "+1300",
"TVT": "+1200",
"UCT": "+0000",
"ULAT": "+0800",
"UTC": "+0000",
"UYST": "-0200",
"UYT": "-0300",
"UZT": "+0500",
"VET": "-0430",
"VLAT": "+1000",
"VOLT": "+0400",
"VOST": "+0600",
"VUT": "+1100",
"WAKT": "+1200",
"WAST": "+0200",
"WAT": "+0100",
"WEDT": "+0100",
"WEST": "+0100",
"WET": "+0000",
"WST": "+0800",
"YAKT": "+1000",
"YEKT": "+0600",
"Z": "+0000"
};
}));

View file

@ -0,0 +1,676 @@
// Copyright (c) 2013 Andris Reinman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(['mimefuncs', 'addressparser', 'mimeparser-tzabbr'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('mimefuncs'), require('wo-addressparser'), require('./mimeparser-tzabbr'));
} else {
root.MimeParser = factory(root.mimefuncs, root.addressparser, root.tzabbr);
}
}(this, function(mimefuncs, addressparser, tzabbr) {
'use strict';
/**
* Creates a parser for a mime stream
*
* @constructor
*/
function MimeParser() {
/**
* Returned to the write calls
*/
this.running = true;
/**
* Cache for parsed node objects
*/
this.nodes = {};
/**
* Root node object
*/
this.node = new MimeNode(null, this);
/**
* Data is written to nodes one line at the time. If entire line
* is not received yet, buffer it before passing on
*/
this._remainder = '';
}
/**
* Writes a chunk of data to the processing queue. Splits data to lines and feeds
* complete lines to the current node element
*
* @param {Uint8Array|String} chunk Chunk to be processed. Either an Uint8Array value or a 'binary' string
*/
MimeParser.prototype.write = function(chunk) {
if (!chunk || !chunk.length) {
return !this.running;
}
var lines = (this._remainder + (typeof chunk === 'object' ?
mimefuncs.fromTypedArray(chunk) : chunk)).split(/\r?\n/g);
this._remainder = lines.pop();
for (var i = 0, len = lines.length; i < len; i++) {
this.node.writeLine(lines[i]);
}
return !this.running;
};
/**
* Indicates that there is no more data coming
*
* @param {Uint8Array|String} [chunk] Final chunk to be processed
*/
MimeParser.prototype.end = function(chunk) {
if (chunk && chunk.length) {
this.write(chunk);
}
if (this.node._lineCount || this._remainder) {
this.node.writeLine(this._remainder);
this._remainder = '';
}
if (this.node) {
this.node.finalize();
}
this.onend();
};
/**
* Retrieves a mime part object for specified path
*
* parser.getNode('1.2.3')
*
* @param {String} path Path to the node
*/
MimeParser.prototype.getNode = function(path) {
path = path || '';
return this.nodes['node' + path] || null;
};
// PARSER EVENTS
/**
* Override this function.
* Called when the parsing is ended
* @event
*/
MimeParser.prototype.onend = function() {};
/**
* Override this function.
* Called when the parsing is ended
* @event
* @param {Object} node Current mime part. See node.header for header lines
*/
MimeParser.prototype.onheader = function() {};
/**
* Override this function.
* Called when a body chunk is emitted
* @event
* @param {Object} node Current mime part
* @param {Uint8Array} chunk Body chunk
*/
MimeParser.prototype.onbody = function() {};
// NODE PROCESSING
/**
* Creates an object that holds and manages one part of the multipart message
*
* @constructor
* @param {Object} parentNode Reference to the parent element. If not specified, then this is root node
* @param {Object} parser MimeParser object
*/
function MimeNode(parentNode, parser) {
// Public properties
/**
* An array of unfolded header lines
*/
this.header = [];
/**
* An object that holds header key=value pairs
*/
this.headers = {};
/**
* Path for this node
*/
this.path = parentNode ? parentNode.path.concat(parentNode._childNodes.length + 1) : [];
// Private properties
/**
* Reference to the 'master' parser object
*/
this._parser = parser;
/**
* Parent node for this specific node
*/
this._parentNode = parentNode;
/**
* Current state, always starts out with HEADER
*/
this._state = 'HEADER';
/**
* Body buffer
*/
this._bodyBuffer = '';
/**
* Line counter bor the body part
*/
this._lineCount = 0;
/**
* If this is a multipart or message/rfc822 mime part, the value
* will be converted to array and hold all child nodes for this node
*/
this._childNodes = false;
/**
* Active child node (if available)
*/
this._currentChild = false;
/**
* Remainder string when dealing with base64 and qp values
*/
this._lineRemainder = '';
/**
* Indicates if this is a multipart node
*/
this._isMultipart = false;
/**
* Stores boundary value for current multipart node
*/
this._multipartBoundary = false;
/**
* Indicates if this is a message/rfc822 node
*/
this._isRfc822 = false;
/**
* Stores the raw content of this node
*/
this.raw = '';
// Att this node to the path cache
this._parser.nodes['node' + this.path.join('.')] = this;
}
// Public methods
/**
* Processes an enitre input line
*
* @param {String} line Entire input line as 'binary' string
*/
MimeNode.prototype.writeLine = function(line) {
this.raw += (this.raw ? '\n' : '') + line;
if (this._state === 'HEADER') {
this._processHeaderLine(line);
} else if (this._state === 'BODY') {
this._processBodyLine(line);
}
};
/**
* Processes any remainders
*/
MimeNode.prototype.finalize = function() {
if (this._isRfc822) {
this._currentChild.finalize();
} else {
this._emitBody(true);
}
};
// Private methods
/**
* Processes a line in the HEADER state. It the line is empty, change state to BODY
*
* @param {String} line Entire input line as 'binary' string
*/
MimeNode.prototype._processHeaderLine = function(line) {
if (!line) {
this._parseHeaders();
this._parser.onheader(this);
this._state = 'BODY';
return;
}
if (line.match(/^\s/) && this.header.length) {
this.header[this.header.length - 1] += '\n' + line;
} else {
this.header.push(line);
}
};
/**
* Joins folded header lines and calls Content-Type and Transfer-Encoding processors
*/
MimeNode.prototype._parseHeaders = function() {
// Join header lines
var key, value, hasBinary;
for (var i = 0, len = this.header.length; i < len; i++) {
value = this.header[i].split(':');
key = (value.shift() || '').trim().toLowerCase();
value = (value.join(':') || '').replace(/\n/g, '').trim();
if (value.match(/[\u0080-\uFFFF]/)) {
if (!this.charset) {
hasBinary = true;
}
// use default charset at first and if the actual charset is resolved, the conversion is re-run
value = mimefuncs.charset.decode(mimefuncs.charset.convert(mimefuncs.toTypedArray(value), this.charset || 'iso-8859-1'));
}
if (!this.headers[key]) {
this.headers[key] = [this._parseHeaderValue(key, value)];
} else {
this.headers[key].push(this._parseHeaderValue(key, value));
}
if (!this.charset && key === 'content-type') {
this.charset = this.headers[key][this.headers[key].length - 1].params.charset;
}
if (hasBinary && this.charset) {
// reset values and start over once charset has been resolved and 8bit content has been found
hasBinary = false;
this.headers = {};
i = -1; // next iteration has i == 0
}
}
this._processContentType();
this._processContentTransferEncoding();
};
/**
* Parses single header value
* @param {String} key Header key
* @param {String} value Value for the key
* @return {Object} parsed header
*/
MimeNode.prototype._parseHeaderValue = function(key, value) {
var parsedValue, isAddress = false;
switch (key) {
case 'content-type':
case 'content-transfer-encoding':
case 'content-disposition':
case 'dkim-signature':
parsedValue = mimefuncs.parseHeaderValue(value);
break;
case 'from':
case 'sender':
case 'to':
case 'reply-to':
case 'cc':
case 'bcc':
case 'abuse-reports-to':
case 'errors-to':
case 'return-path':
case 'delivered-to':
isAddress = true;
parsedValue = {
value: [].concat(addressparser.parse(value) || [])
};
break;
case 'date':
parsedValue = {
value: this._parseDate(value)
};
break;
default:
parsedValue = {
value: value
};
}
parsedValue.initial = value;
this._decodeHeaderCharset(parsedValue, {
isAddress: isAddress
});
return parsedValue;
};
/**
* Checks if a date string can be parsed. Falls back replacing timezone
* abbrevations with timezone values
*
* @param {String} str Date header
* @returns {String} UTC date string if parsing succeeded, otherwise returns input value
*/
MimeNode.prototype._parseDate = function(str) {
str = (str || '').toString().trim();
var date = new Date(str);
if (this._isValidDate(date)) {
return date.toUTCString().replace(/GMT/, '+0000');
}
// Assume last alpha part is a timezone
// Ex: "Date: Thu, 15 May 2014 13:53:30 EEST"
str = str.replace(/\b[a-z]+$/i, function(tz) {
tz = tz.toUpperCase();
if (tzabbr.hasOwnProperty(tz)) {
return tzabbr[tz];
}
return tz;
});
date = new Date(str);
if (this._isValidDate(date)) {
return date.toUTCString().replace(/GMT/, '+0000');
} else {
return str;
}
};
/**
* Checks if a value is a Date object and it contains an actual date value
* @param {Date} date Date object to check
* @returns {Boolean} True if the value is a valid date
*/
MimeNode.prototype._isValidDate = function(date) {
return Object.prototype.toString.call(date) === '[object Date]' && date.toString() !== 'Invalid Date';
};
MimeNode.prototype._decodeHeaderCharset = function(parsed, options) {
options = options || {};
// decode default value
if (typeof parsed.value === 'string') {
parsed.value = mimefuncs.mimeWordsDecode(parsed.value);
}
// decode possible params
Object.keys(parsed.params || {}).forEach(function(key) {
if (typeof parsed.params[key] === 'string') {
parsed.params[key] = mimefuncs.mimeWordsDecode(parsed.params[key]);
}
});
// decode addresses
if (options.isAddress && Array.isArray(parsed.value)) {
parsed.value.forEach(function(addr) {
if (addr.name) {
addr.name = mimefuncs.mimeWordsDecode(addr.name);
if (Array.isArray(addr.group)) {
this._decodeHeaderCharset({
value: addr.group
}, {
isAddress: true
});
}
}
}.bind(this));
}
return parsed;
};
/**
* Parses Content-Type value and selects following actions.
*/
MimeNode.prototype._processContentType = function() {
var contentDisposition;
this.contentType = this.headers['content-type'] && this.headers['content-type'][0] ||
mimefuncs.parseHeaderValue('text/plain');
this.contentType.value = (this.contentType.value || '').toLowerCase().trim();
this.contentType.type = (this.contentType.value.split('/').shift() || 'text');
if (this.contentType.params && this.contentType.params.charset && !this.charset) {
this.charset = this.contentType.params.charset;
}
if (this.contentType.type === 'multipart' && this.contentType.params.boundary) {
this._childNodes = [];
this._isMultipart = (this.contentType.value.split('/').pop() || 'mixed');
this._multipartBoundary = this.contentType.params.boundary;
}
if (this.contentType.value === 'message/rfc822') {
/**
* Parse message/rfc822 only if the mime part is not marked with content-disposition: attachment,
* otherwise treat it like a regular attachment
*/
contentDisposition = this.headers['content-disposition'] && this.headers['content-disposition'][0] ||
mimefuncs.parseHeaderValue('');
if ((contentDisposition.value || '').toLowerCase().trim() !== 'attachment') {
this._childNodes = [];
this._currentChild = new MimeNode(this, this._parser);
this._childNodes.push(this._currentChild);
this._isRfc822 = true;
}
}
};
/**
* Parses Content-Trasnfer-Encoding value to see if the body needs to be converted
* before it can be emitted
*/
MimeNode.prototype._processContentTransferEncoding = function() {
this.contentTransferEncoding = this.headers['content-transfer-encoding'] && this.headers['content-transfer-encoding'][0] ||
mimefuncs.parseHeaderValue('7bit');
this.contentTransferEncoding.value = (this.contentTransferEncoding.value || '').toLowerCase().trim();
};
/**
* Processes a line in the BODY state. If this is a multipart or rfc822 node,
* passes line value to child nodes.
*
* @param {String} line Entire input line as 'binary' string
*/
MimeNode.prototype._processBodyLine = function(line) {
var curLine, match;
this._lineCount++;
if (this._isMultipart) {
if (line === '--' + this._multipartBoundary) {
if (this._currentChild) {
this._currentChild.finalize();
}
this._currentChild = new MimeNode(this, this._parser);
this._childNodes.push(this._currentChild);
} else if (line === '--' + this._multipartBoundary + '--') {
if (this._currentChild) {
this._currentChild.finalize();
}
this._currentChild = false;
} else if (this._currentChild) {
this._currentChild.writeLine(line);
} else {
// Ignore body for multipart
}
} else if (this._isRfc822) {
this._currentChild.writeLine(line);
} else {
switch (this.contentTransferEncoding.value) {
case 'base64':
curLine = this._lineRemainder + line.trim();
if (curLine.length % 4) {
this._lineRemainder = curLine.substr(-curLine.length % 4);
curLine = curLine.substr(0, curLine.length - this._lineRemainder.length);
} else {
this._lineRemainder = '';
}
if (curLine.length) {
this._bodyBuffer += mimefuncs.fromTypedArray(mimefuncs.base64.decode(curLine));
}
break;
case 'quoted-printable':
curLine = this._lineRemainder + (this._lineCount > 1 ? '\n' : '') + line;
if ((match = curLine.match(/=[a-f0-9]{0,1}$/i))) {
this._lineRemainder = match[0];
curLine = curLine.substr(0, curLine.length - this._lineRemainder.length);
} else {
this._lineRemainder = '';
}
this._bodyBuffer += curLine.replace(/\=(\r?\n|$)/g, '').replace(/=([a-f0-9]{2})/ig, function(m, code) {
return String.fromCharCode(parseInt(code, 16));
});
break;
// case '7bit':
// case '8bit':
default:
this._bodyBuffer += (this._lineCount > 1 ? '\n' : '') + line;
break;
}
}
};
/**
* Emits a chunk of the body
*
* @param {Boolean} forceEmit If set to true does not keep any remainders
*/
MimeNode.prototype._emitBody = function() {
var contentDisposition = this.headers['content-disposition'] && this.headers['content-disposition'][0] ||
mimefuncs.parseHeaderValue('');
var delSp;
if (this._isMultipart || !this._bodyBuffer) {
return;
}
// Process flowed text before emitting it
if (/^text\/(plain|html)$/i.test(this.contentType.value) &&
this.contentType.params && /^flowed$/i.test(this.contentType.params.format)) {
delSp = /^yes$/i.test(this.contentType.params.delsp);
this._bodyBuffer = this._bodyBuffer.
split('\n').
// remove soft linebreaks
// soft linebreaks are added after space symbols
reduce(function(previousValue, currentValue, index) {
var body = previousValue;
if (delSp) {
// delsp adds spaces to text to be able to fold it
// these spaces can be removed once the text is unfolded
body = body.replace(/[ ]+$/, '');
}
if (/ $/.test(previousValue) && !/(^|\n)\-\- $/.test(previousValue)) {
return body + currentValue;
} else {
return body + '\n' + currentValue;
}
}).
// remove whitespace stuffing
// http://tools.ietf.org/html/rfc3676#section-4.4
replace(/^ /gm, '');
}
this.content = mimefuncs.toTypedArray(this._bodyBuffer);
if (/^text\/(plain|html)$/i.test(this.contentType.value) && !/^attachment$/i.test(contentDisposition.value)) {
if (!this.charset && /^text\/html$/i.test(this.contentType.value)) {
this.charset = this._detectHTMLCharset(this._bodyBuffer);
}
// decode "binary" string to an unicode string
if (!/^utf[\-_]?8$/i.test(this.charset)) {
this.content = mimefuncs.charset.convert(mimefuncs.toTypedArray(this._bodyBuffer), this.charset || 'iso-8859-1');
}
// override charset for text nodes
this.charset = this.contentType.params.charset = 'utf-8';
}
this._bodyBuffer = '';
this._parser.onbody(this, this.content);
};
/**
* Detect charset from a html file
*
* @param {String} html Input HTML
* @returns {String} Charset if found or undefined
*/
MimeNode.prototype._detectHTMLCharset = function(html) {
var charset, input, meta;
if (typeof html !== 'string') {
html = html.toString('ascii');
}
html = html.replace(/\r?\n|\r/g, " ");
if ((meta = html.match(/<meta\s+http-equiv=["'\s]*content-type[^>]*?>/i))) {
input = meta[0];
}
if (input) {
charset = input.match(/charset\s?=\s?([a-zA-Z\-_:0-9]*);?/);
if (charset) {
charset = (charset[1] || '').trim().toLowerCase();
}
}
if (!charset && (meta = html.match(/<meta\s+charset=["'\s]*([^"'<>\/\s]+)/i))) {
charset = (meta[1] || '').trim().toLowerCase();
}
return charset;
};
return MimeParser;
}));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,19 @@
/*global define*/
define(function() {
/**
* Mixes properties from source into target.
* @param {Object} target target of the mix.
* @param {Object} source source object providing properties to mix in.
* @param {Boolean} override if target already has a the property,
* override it with the one from source.
* @return {Object} the target object, now with the new properties
* mixed in.
*/
return function mix(target, source, override) {
Object.keys(source).forEach(function(key) {
if (!target.hasOwnProperty(key) || override)
target[key] = source[key];
});
return target;
};
});

View file

@ -0,0 +1,507 @@
/*! http://mths.be/punycode v1.2.4 by @mathias */
;(function(root) {
/** Detect free variables */
var freeExports = typeof exports == 'object' && exports;
var freeModule = typeof module == 'object' && module &&
module.exports == freeExports && module;
var freeGlobal = typeof global == 'object' && global;
if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
root = freeGlobal;
}
/**
* The `punycode` object.
* @name punycode
* @type Object
*/
var punycode,
/** Highest positive signed 32-bit float value */
maxInt = 2147483647, // aka. 0x7FFFFFFF or 2^31-1
/** Bootstring parameters */
base = 36,
tMin = 1,
tMax = 26,
skew = 38,
damp = 700,
initialBias = 72,
initialN = 128, // 0x80
delimiter = '-', // '\x2D'
/** Regular expressions */
regexPunycode = /^xn--/,
regexNonASCII = /[^ -~]/, // unprintable ASCII chars + non-ASCII chars
regexSeparators = /\x2E|\u3002|\uFF0E|\uFF61/g, // RFC 3490 separators
/** Error messages */
errors = {
'overflow': 'Overflow: input needs wider integers to process',
'not-basic': 'Illegal input >= 0x80 (not a basic code point)',
'invalid-input': 'Invalid input'
},
/** Convenience shortcuts */
baseMinusTMin = base - tMin,
floor = Math.floor,
stringFromCharCode = String.fromCharCode,
/** Temporary variable */
key;
/*--------------------------------------------------------------------------*/
/**
* A generic error utility function.
* @private
* @param {String} type The error type.
* @returns {Error} Throws a `RangeError` with the applicable error message.
*/
function error(type) {
throw RangeError(errors[type]);
}
/**
* A generic `Array#map` utility function.
* @private
* @param {Array} array The array to iterate over.
* @param {Function} callback The function that gets called for every array
* item.
* @returns {Array} A new array of values returned by the callback function.
*/
function map(array, fn) {
var length = array.length;
while (length--) {
array[length] = fn(array[length]);
}
return array;
}
/**
* A simple `Array#map`-like wrapper to work with domain name strings.
* @private
* @param {String} domain The domain name.
* @param {Function} callback The function that gets called for every
* character.
* @returns {Array} A new string of characters returned by the callback
* function.
*/
function mapDomain(string, fn) {
return map(string.split(regexSeparators), fn).join('.');
}
/**
* Creates an array containing the numeric code points of each Unicode
* character in the string. While JavaScript uses UCS-2 internally,
* this function will convert a pair of surrogate halves (each of which
* UCS-2 exposes as separate characters) into a single code point,
* matching UTF-16.
* @see `punycode.ucs2.encode`
* @see <http://mathiasbynens.be/notes/javascript-encoding>
* @memberOf punycode.ucs2
* @name decode
* @param {String} string The Unicode input string (UCS-2).
* @returns {Array} The new array of code points.
*/
function ucs2decode(string) {
var output = [],
counter = 0,
length = string.length,
value,
extra;
while (counter < length) {
value = string.charCodeAt(counter++);
if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
// high surrogate, and there is a next character
extra = string.charCodeAt(counter++);
if ((extra & 0xFC00) == 0xDC00) { // low surrogate
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
} else {
// unmatched surrogate; only append this code unit, in case the next
// code unit is the high surrogate of a surrogate pair
output.push(value);
counter--;
}
} else {
output.push(value);
}
}
return output;
}
/**
* Creates a string based on an array of numeric code points.
* @see `punycode.ucs2.decode`
* @memberOf punycode.ucs2
* @name encode
* @param {Array} codePoints The array of numeric code points.
* @returns {String} The new Unicode string (UCS-2).
*/
function ucs2encode(array) {
return map(array, function(value) {
var output = '';
if (value > 0xFFFF) {
value -= 0x10000;
output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
value = 0xDC00 | value & 0x3FF;
}
output += stringFromCharCode(value);
return output;
}).join('');
}
/**
* Converts a basic code point into a digit/integer.
* @see `digitToBasic()`
* @private
* @param {Number} codePoint The basic numeric code point value.
* @returns {Number} The numeric value of a basic code point (for use in
* representing integers) in the range `0` to `base - 1`, or `base` if
* the code point does not represent a value.
*/
function basicToDigit(codePoint) {
if (codePoint - 48 < 10) {
return codePoint - 22;
}
if (codePoint - 65 < 26) {
return codePoint - 65;
}
if (codePoint - 97 < 26) {
return codePoint - 97;
}
return base;
}
/**
* Converts a digit/integer into a basic code point.
* @see `basicToDigit()`
* @private
* @param {Number} digit The numeric value of a basic code point.
* @returns {Number} The basic code point whose value (when used for
* representing integers) is `digit`, which needs to be in the range
* `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
* used; else, the lowercase form is used. The behavior is undefined
* if `flag` is non-zero and `digit` has no uppercase form.
*/
function digitToBasic(digit, flag) {
// 0..25 map to ASCII a..z or A..Z
// 26..35 map to ASCII 0..9
return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
}
/**
* Bias adaptation function as per section 3.4 of RFC 3492.
* http://tools.ietf.org/html/rfc3492#section-3.4
* @private
*/
function adapt(delta, numPoints, firstTime) {
var k = 0;
delta = firstTime ? floor(delta / damp) : delta >> 1;
delta += floor(delta / numPoints);
for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) {
delta = floor(delta / baseMinusTMin);
}
return floor(k + (baseMinusTMin + 1) * delta / (delta + skew));
}
/**
* Converts a Punycode string of ASCII-only symbols to a string of Unicode
* symbols.
* @memberOf punycode
* @param {String} input The Punycode string of ASCII-only symbols.
* @returns {String} The resulting string of Unicode symbols.
*/
function decode(input) {
// Don't use UCS-2
var output = [],
inputLength = input.length,
out,
i = 0,
n = initialN,
bias = initialBias,
basic,
j,
index,
oldi,
w,
k,
digit,
t,
/** Cached calculation results */
baseMinusT;
// Handle the basic code points: let `basic` be the number of input code
// points before the last delimiter, or `0` if there is none, then copy
// the first basic code points to the output.
basic = input.lastIndexOf(delimiter);
if (basic < 0) {
basic = 0;
}
for (j = 0; j < basic; ++j) {
// if it's not a basic code point
if (input.charCodeAt(j) >= 0x80) {
error('not-basic');
}
output.push(input.charCodeAt(j));
}
// Main decoding loop: start just after the last delimiter if any basic code
// points were copied; start at the beginning otherwise.
for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) {
// `index` is the index of the next character to be consumed.
// Decode a generalized variable-length integer into `delta`,
// which gets added to `i`. The overflow checking is easier
// if we increase `i` as we go, then subtract off its starting
// value at the end to obtain `delta`.
for (oldi = i, w = 1, k = base; /* no condition */; k += base) {
if (index >= inputLength) {
error('invalid-input');
}
digit = basicToDigit(input.charCodeAt(index++));
if (digit >= base || digit > floor((maxInt - i) / w)) {
error('overflow');
}
i += digit * w;
t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
if (digit < t) {
break;
}
baseMinusT = base - t;
if (w > floor(maxInt / baseMinusT)) {
error('overflow');
}
w *= baseMinusT;
}
out = output.length + 1;
bias = adapt(i - oldi, out, oldi == 0);
// `i` was supposed to wrap around from `out` to `0`,
// incrementing `n` each time, so we'll fix that now:
if (floor(i / out) > maxInt - n) {
error('overflow');
}
n += floor(i / out);
i %= out;
// Insert `n` at position `i` of the output
output.splice(i++, 0, n);
}
return ucs2encode(output);
}
/**
* Converts a string of Unicode symbols to a Punycode string of ASCII-only
* symbols.
* @memberOf punycode
* @param {String} input The string of Unicode symbols.
* @returns {String} The resulting Punycode string of ASCII-only symbols.
*/
function encode(input) {
var n,
delta,
handledCPCount,
basicLength,
bias,
j,
m,
q,
k,
t,
currentValue,
output = [],
/** `inputLength` will hold the number of code points in `input`. */
inputLength,
/** Cached calculation results */
handledCPCountPlusOne,
baseMinusT,
qMinusT;
// Convert the input in UCS-2 to Unicode
input = ucs2decode(input);
// Cache the length
inputLength = input.length;
// Initialize the state
n = initialN;
delta = 0;
bias = initialBias;
// Handle the basic code points
for (j = 0; j < inputLength; ++j) {
currentValue = input[j];
if (currentValue < 0x80) {
output.push(stringFromCharCode(currentValue));
}
}
handledCPCount = basicLength = output.length;
// `handledCPCount` is the number of code points that have been handled;
// `basicLength` is the number of basic code points.
// Finish the basic string - if it is not empty - with a delimiter
if (basicLength) {
output.push(delimiter);
}
// Main encoding loop:
while (handledCPCount < inputLength) {
// All non-basic code points < n have been handled already. Find the next
// larger one:
for (m = maxInt, j = 0; j < inputLength; ++j) {
currentValue = input[j];
if (currentValue >= n && currentValue < m) {
m = currentValue;
}
}
// Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
// but guard against overflow
handledCPCountPlusOne = handledCPCount + 1;
if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
error('overflow');
}
delta += (m - n) * handledCPCountPlusOne;
n = m;
for (j = 0; j < inputLength; ++j) {
currentValue = input[j];
if (currentValue < n && ++delta > maxInt) {
error('overflow');
}
if (currentValue == n) {
// Represent delta as a generalized variable-length integer
for (q = delta, k = base; /* no condition */; k += base) {
t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias);
if (q < t) {
break;
}
qMinusT = q - t;
baseMinusT = base - t;
output.push(
stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))
);
q = floor(qMinusT / baseMinusT);
}
output.push(stringFromCharCode(digitToBasic(q, 0)));
bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength);
delta = 0;
++handledCPCount;
}
}
++delta;
++n;
}
return output.join('');
}
/**
* Converts a Punycode string representing a domain name to Unicode. Only the
* Punycoded parts of the domain name will be converted, i.e. it doesn't
* matter if you call it on a string that has already been converted to
* Unicode.
* @memberOf punycode
* @param {String} domain The Punycode domain name to convert to Unicode.
* @returns {String} The Unicode representation of the given Punycode
* string.
*/
function toUnicode(domain) {
return mapDomain(domain, function(string) {
return regexPunycode.test(string)
? decode(string.slice(4).toLowerCase())
: string;
});
}
/**
* Converts a Unicode string representing a domain name to Punycode. Only the
* non-ASCII parts of the domain name will be converted, i.e. it doesn't
* matter if you call it with a domain that's already in ASCII.
* @memberOf punycode
* @param {String} domain The domain name to convert, as a Unicode string.
* @returns {String} The Punycode representation of the given domain name.
*/
function toASCII(domain) {
return mapDomain(domain, function(string) {
return regexNonASCII.test(string)
? 'xn--' + encode(string)
: string;
});
}
/*--------------------------------------------------------------------------*/
/** Define the public API */
punycode = {
/**
* A string representing the current Punycode.js version number.
* @memberOf punycode
* @type String
*/
'version': '1.2.4',
/**
* An object of methods to convert from JavaScript's internal character
* representation (UCS-2) to Unicode code points, and back.
* @see <http://mathiasbynens.be/notes/javascript-encoding>
* @memberOf punycode
* @type Object
*/
'ucs2': {
'decode': ucs2decode,
'encode': ucs2encode
},
'decode': decode,
'encode': encode,
'toASCII': toASCII,
'toUnicode': toUnicode
};
/** Expose `punycode` */
// Some AMD build optimizers, like r.js, check for specific condition patterns
// like the following:
if (
typeof define == 'function' &&
typeof define.amd == 'object' &&
define.amd
) {
define('punycode', function() {
return punycode;
});
} else if (freeExports && !freeExports.nodeType) {
if (freeModule) { // in Node.js or RingoJS v0.8.0+
freeModule.exports = punycode;
} else { // in Narwhal or RingoJS v0.7.0-
for (key in punycode) {
punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]);
}
}
} else { // in Rhino or a web browser
root.punycode = punycode;
}
}(this));

View file

@ -0,0 +1,169 @@
define(function(require, exports, module) {
/**
* Safe atob-variant that does not throw exceptions and just ignores
* characters that it does not know about. This is an attempt to
* mimic node's implementation so that we can parse base64 with
* newlines present as well as being tolerant of complete gibberish
* people throw at us. Since we are doing this by hand, we also take
* the opportunity to put the output directly in a typed array.
*
* In contrast, window.atob() throws Exceptions for all kinds of
* angry reasons.
*/
exports.decode = function(s) {
var bitsSoFar = 0, validBits = 0, iOut = 0,
arr = new Uint8Array(Math.ceil(s.length * 3 / 4));
for (var i = 0; i < s.length; i++) {
var c = s.charCodeAt(i), bits;
if (c >= 65 && c <= 90) // [A-Z]
bits = c - 65;
else if (c >= 97 && c <= 122) // [a-z]
bits = c - 97 + 26;
else if (c >= 48 && c <= 57) // [0-9]
bits = c - 48 + 52;
else if (c === 43) // +
bits = 62;
else if (c === 47) // /
bits = 63;
else if (c === 61) { // =
validBits = 0;
continue;
}
// ignore all other characters!
else
continue;
bitsSoFar = (bitsSoFar << 6) | bits;
validBits += 6;
if (validBits >= 8) {
validBits -= 8;
arr[iOut++] = bitsSoFar >> validBits;
if (validBits === 2)
bitsSoFar &= 0x3;
else if (validBits === 4)
bitsSoFar &= 0xf;
}
}
if (iOut < arr.length)
return arr.subarray(0, iOut);
return arr;
}
/**
* UInt8Array => base64 => UTF-8 String
*/
exports.encode = function(view) {
var sbits, i;
sbits = new Array(view.length);
for (i = 0; i < view.length; i++) {
sbits[i] = String.fromCharCode(view[i]);
}
// (btoa is binary JS string -> base64 ASCII string)
return window.btoa(sbits.join(''));
}
/**
* Base64 binary data from a Uint8array to a Uint8Array the way an
* RFC2822 MIME message likes it. Which is to say with a maximum of
* 76 bytes of base64 encoded data followed by a \r\n. If the last
* line has less than 76 bytes of encoded data we still put the \r\n
* on.
*
* This method came into existence because we were blowing out our
* memory limits which is how it justifies all this specialization.
* Use window.btoa if you don't need this exact logic/help.
*/
exports.mimeStyleBase64Encode = function(data) {
var wholeLines = Math.floor(data.length / 57);
var partialBytes = data.length - (wholeLines * 57);
var encodedLength = wholeLines * 78;
if (partialBytes) {
// The padding bytes mean we're always a multiple of 4 long. And then we
// still want a CRLF as part of our encoding contract.
encodedLength += Math.ceil(partialBytes / 3) * 4 + 2;
}
var encoded = new Uint8Array(encodedLength);
// A nibble is 4 bits.
function encode6Bits(nibbly) {
// [0, 25] => ['A', 'Z'], 'A'.charCodeAt(0) === 65
if (nibbly <= 25) {
encoded[iWrite++] = 65 + nibbly;
}
// [26, 51] => ['a', 'z'], 'a'.charCodeAt(0) === 97
else if (nibbly <= 51) {
encoded[iWrite++] = 97 - 26 + nibbly;
}
// [52, 61] => ['0', '9'], '0'.charCodeAt(0) === 48
else if (nibbly <= 61) {
encoded[iWrite++] = 48 - 52 + nibbly;
}
// 62 is '+', '+'.charCodeAt(0) === 43
else if (nibbly === 62) {
encoded[iWrite++] = 43;
}
// 63 is '/', '/'.charCodeAt(0) === 47
else {
encoded[iWrite++] = 47;
}
}
var iRead = 0, iWrite = 0, bytesToRead;
// Steady state
for (bytesToRead = data.length; bytesToRead >= 3; bytesToRead -= 3) {
var b1 = data[iRead++], b2 = data[iRead++], b3 = data[iRead++];
// U = Use, i = ignore
// UUUUUUii
encode6Bits(b1 >> 2);
// iiiiiiUU UUUUiiii
encode6Bits(((b1 & 0x3) << 4) | (b2 >> 4));
// iiiiUUUU UUiiiiii
encode6Bits(((b2 & 0xf) << 2) | (b3 >> 6));
// iiUUUUUU
encode6Bits(b3 & 0x3f);
// newlines; it's time to wrap every 57 bytes, or if it's our
// last full set
if ((iRead % 57) === 0 || bytesToRead === 3) {
encoded[iWrite++] = 13; // \r
encoded[iWrite++] = 10; // \n
}
}
// Leftovers (could be zero). If we ended on a full set in the
// prior loop, the newline is taken care of.
switch(bytesToRead) {
case 2:
b1 = data[iRead++];
b2 = data[iRead++];
encode6Bits(b1 >> 2);
encode6Bits(((b1 & 0x3) << 4) | (b2 >> 4));
encode6Bits(((b2 & 0xf) << 2) | 0);
encoded[iWrite++] = 61; // '='.charCodeAt(0) === 61
encoded[iWrite++] = 13; // \r
encoded[iWrite++] = 10; // \n
break;
case 1:
b1 = data[iRead++];
encode6Bits(b1 >> 2);
encode6Bits(((b1 & 0x3) << 4) | 0);
encoded[iWrite++] = 61; // '='.charCodeAt(0) === 61
encoded[iWrite++] = 61;
encoded[iWrite++] = 13; // \r
encoded[iWrite++] = 10; // \n
break;
}
// The code was used to help sanity check, but is inert. Left in for
// reviewers or those who suspect this code! :)
/*
if (iWrite !== encodedLength)
throw new Error('Badly written code! iWrite: ' + iWrite +
' encoded length: ' + encodedLength);
*/
return encoded;
}
}); // end define

View file

@ -0,0 +1,3 @@
define(['ext/smtpclient/src/smtpclient'], function(SmtpClient) {
return SmtpClient;
});

View file

@ -0,0 +1,183 @@
// Copyright (c) 2013 Andris Reinman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.SmtpClientResponseParser = factory();
}
}(this, function() {
'use strict';
/**
* Generates a parser object for data coming from a SMTP server
*
* @constructor
*/
var SmtpResponseParser = function() {
/**
* If the complete line is not received yet, contains the beginning of it
*/
this._remainder = '';
/**
* If the response is a list, contains previous not yet emitted lines
*/
this._block = {
data: [],
lines: [],
statusCode: null
};
/**
* If set to true, do not accept any more input
*/
this.destroyed = false;
};
// Event handlers
/**
* NB! Errors do not block, the parsing and data emitting continues despite of the errors
*/
SmtpResponseParser.prototype.onerror = function() {};
SmtpResponseParser.prototype.ondata = function() {};
SmtpResponseParser.prototype.onend = function() {};
// Public API
/**
* Queue some data from the server for parsing. Only allowed, if 'end' has not been called yet
*
* @param {String} chunk Chunk of data received from the server
*/
SmtpResponseParser.prototype.send = function(chunk) {
if (this.destroyed) {
return this.onerror(new Error('This parser has already been closed, "write" is prohibited'));
}
// Lines should always end with <CR><LF> but you never know, might be only <LF> as well
var lines = (this._remainder + (chunk || '')).split(/\r?\n/);
this._remainder = lines.pop(); // not sure if the line has completely arrived yet
for (var i = 0, len = lines.length; i < len; i++) {
this._processLine(lines[i]);
}
};
/**
* Indicate that all the data from the server has been received. Can be called only once.
*
* @param {String} [chunk] Chunk of data received from the server
*/
SmtpResponseParser.prototype.end = function(chunk) {
if (this.destroyed) {
return this.onerror(new Error('This parser has already been closed, "end" is prohibited'));
}
if (chunk) {
this.send(chunk);
}
if (this._remainder) {
this._processLine(this._remainder);
}
this.destroyed = true;
this.onend();
};
// Private API
/**
* Processes a single and complete line. If it is a continous one (slash after status code),
* queue it to this._block
*
* @param {String} line Complete line of data from the server
*/
SmtpResponseParser.prototype._processLine = function(line) {
var match, response;
// possible input strings for the regex:
// 250-MESSAGE
// 250 MESSAGE
// 250 1.2.3 MESSAGE
if (!line.trim()) {
// nothing to check, empty line
return;
}
this._block.lines.push(line);
if ((match = line.match(/^(\d{3})([\- ])(?:(\d+\.\d+\.\d+)(?: ))?(.*)/))) {
this._block.data.push(match[4]);
if (match[2] === '-') {
if (this._block.statusCode && this._block.statusCode !== Number(match[1])) {
this.onerror('Invalid status code ' + match[1] +
' for multi line response (' + this._block.statusCode + ' expected)');
} else if (!this._block.statusCode) {
this._block.statusCode = Number(match[1]);
}
return;
} else {
response = {
statusCode: Number(match[1]) || 0,
enhancedStatus: match[3] || null,
data: this._block.data.join('\n'),
line: this._block.lines.join('\n')
};
response.success = response.statusCode >= 200 && response.statusCode < 300;
this.ondata(response);
this._block = {
data: [],
lines: [],
statusCode: null
};
this._block.statusCode = null;
}
} else {
this.onerror(new Error('Invalid SMTP response "' + line + '"'));
this.ondata({
success: false,
statusCode: this._block.statusCode || null,
enhancedStatus: null,
data: [line].join('\n'),
line: this._block.lines.join('\n')
});
this._block = {
data: [],
lines: [],
statusCode: null
};
}
};
return SmtpResponseParser;
}));

View file

@ -0,0 +1,962 @@
// Copyright (c) 2013 Andris Reinman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(root, factory) {
'use strict';
var encoding;
if (typeof define === 'function' && define.amd) {
// AMD in browser environment
define(['tcp-socket', 'stringencoding', 'axe', './smtpclient-response-parser'], function(TCPSocket, encoding, axe, SmtpClientResponseParser) {
return factory(TCPSocket, encoding.TextEncoder, encoding.TextDecoder, axe, SmtpClientResponseParser, window.btoa);
});
} else if (typeof exports === 'object' && typeof navigator !== 'undefined') {
// common.js in browser environment
encoding = require('wo-stringencoding');
module.exports = factory(require('tcp-socket'), encoding.TextEncoder, encoding.TextDecoder, require('axe-logger'), require('./smtpclient-response-parser'), btoa);
} else if (typeof exports === 'object') {
// node.js
encoding = require('wo-stringencoding');
module.exports = factory(require('tcp-socket'), encoding.TextEncoder, encoding.TextDecoder, require('axe-logger'), require('./smtpclient-response-parser'), function(str) {
var NodeBuffer = require('buffer').Buffer;
return new NodeBuffer(str, 'binary').toString("base64");
});
} else {
// browser global
navigator.TCPSocket = navigator.TCPSocket || navigator.mozTCPSocket;
root.SmtpClient = factory(navigator.TCPSocket, root.TextEncoder, root.TextDecoder, root.axe, root.SmtpClientResponseParser, window.btoa);
}
}(this, function(TCPSocket, TextEncoder, TextDecoder, axe, SmtpClientResponseParser, btoa) {
'use strict';
var DEBUG_TAG = 'SMTP Client';
/**
* Creates a connection object to a SMTP server and allows to send mail through it.
* Call `connect` method to inititate the actual connection, the constructor only
* defines the properties but does not actually connect.
*
* NB! The parameter order (host, port) differs from node.js "way" (port, host)
*
* @constructor
*
* @param {String} [host="localhost"] Hostname to conenct to
* @param {Number} [port=25] Port number to connect to
* @param {Object} [options] Optional options object
* @param {Boolean} [options.useSecureTransport] Set to true, to use encrypted connection
* @param {String} [options.name] Client hostname for introducing itself to the server
* @param {Object} [options.auth] Authentication options. Depends on the preferred authentication method. Usually {user, pass}
* @param {String} [options.authMethod] Force specific authentication method
* @param {Boolean} [options.disableEscaping] If set to true, do not escape dots on the beginning of the lines
*/
function SmtpClient(host, port, options) {
this._TCPSocket = TCPSocket;
this.options = options || {};
this.port = port || (this.options.useSecureTransport ? 465 : 25);
this.host = host || 'localhost';
/**
* If set to true, start an encrypted connection instead of the plaintext one
* (recommended if applicable). If useSecureTransport is not set but the port used is 465,
* then ecryption is used by default.
*/
this.options.useSecureTransport = 'useSecureTransport' in this.options ? !!this.options.useSecureTransport : this.port === 465;
/**
* Authentication object. If not set, authentication step will be skipped.
*/
this.options.auth = this.options.auth || false;
/**
* Hostname of the client, this will be used for introducing to the server
*/
this.options.name = this.options.name || false;
/**
* Downstream TCP socket to the SMTP server, created with mozTCPSocket
*/
this.socket = false;
/**
* Indicates if the connection has been closed and can't be used anymore
*
*/
this.destroyed = false;
/**
* Informational value that indicates the maximum size (in bytes) for
* a message sent to the current server. Detected from SIZE info.
* Not available until connection has been established.
*/
this.maxAllowedSize = 0;
/**
* Keeps track if the downstream socket is currently full and
* a drain event should be waited for or not
*/
this.waitDrain = false;
// Private properties
/**
* SMTP response parser object. All data coming from the downstream server
* is feeded to this parser
*/
this._parser = new SmtpClientResponseParser();
/**
* If authenticated successfully, stores the username
*/
this._authenticatedAs = null;
/**
* A list of authentication mechanisms detected from the EHLO response
* and which are compatible with this library
*/
this._supportedAuth = [];
/**
* If true, accepts data from the upstream to be passed
* directly to the downstream socket. Used after the DATA command
*/
this._dataMode = false;
/**
* Keep track of the last bytes to see how the terminating dot should be placed
*/
this._lastDataBytes = '';
/**
* Envelope object for tracking who is sending mail to whom
*/
this._envelope = null;
/**
* Stores the function that should be run after a response has been received
* from the server
*/
this._currentAction = null;
/**
* Indicates if the connection is secured or plaintext
*/
this._secureMode = !!this.options.useSecureTransport;
}
//
// EVENTS
//
// Event functions should be overriden, these are just placeholders
/**
* Will be run when an error occurs. Connection to the server will be closed automatically,
* so wait for an `onclose` event as well.
*
* @param {Error} err Error object
*/
SmtpClient.prototype.onerror = function() {};
/**
* More data can be buffered in the socket. See `waitDrain` property or
* check if `send` method returns false to see if you should be waiting
* for the drain event. Before sending anything else.
*/
SmtpClient.prototype.ondrain = function() {};
/**
* The connection to the server has been closed
*/
SmtpClient.prototype.onclose = function() {};
/**
* The connection is established and idle, you can send mail now
*/
SmtpClient.prototype.onidle = function() {};
/**
* The connection is waiting for the mail body
*
* @param {Array} failedRecipients List of addresses that were not accepted as recipients
*/
SmtpClient.prototype.onready = function() {};
/**
* The mail has been sent.
* Wait for `onidle` next.
*
* @param {Boolean} success Indicates if the message was queued by the server or not
*/
SmtpClient.prototype.ondone = function() {};
//
// PUBLIC METHODS
//
// Connection related methods
/**
* Initiate a connection to the server
*/
SmtpClient.prototype.connect = function() {
if (!this.options.name && 'getHostname' in this._TCPSocket && typeof this._TCPSocket.getHostname === 'function') {
this._TCPSocket.getHostname(function(err, hostname) {
this.options.name = hostname || 'localhost';
this.connect();
}.bind(this));
return;
} else if (!this.options.name) {
this.options.name = 'localhost';
}
this.socket = this._TCPSocket.open(this.host, this.port, {
binaryType: 'arraybuffer',
useSecureTransport: this._secureMode,
ca: this.options.ca,
tlsWorkerPath: this.options.tlsWorkerPath
});
// allows certificate handling for platform w/o native tls support
// oncert is non standard so setting it might throw if the socket object is immutable
try {
this.socket.oncert = this.oncert;
} catch (E) {}
this.socket.onerror = this._onError.bind(this);
this.socket.onopen = this._onOpen.bind(this);
};
/**
* Pauses `data` events from the downstream SMTP server
*/
SmtpClient.prototype.suspend = function() {
if (this.socket && this.socket.readyState === 'open') {
this.socket.suspend();
}
};
/**
* Resumes `data` events from the downstream SMTP server. Be careful of not
* resuming something that is not suspended - an error is thrown in this case
*/
SmtpClient.prototype.resume = function() {
if (this.socket && this.socket.readyState === 'open') {
this.socket.resume();
}
};
/**
* Sends QUIT
*/
SmtpClient.prototype.quit = function() {
axe.debug(DEBUG_TAG, 'Sending QUIT...');
this._sendCommand('QUIT');
this._currentAction = this.close;
};
/**
* Reset authentication
*
* @param {Object} [auth] Use this if you want to authenticate as another user
*/
SmtpClient.prototype.reset = function(auth) {
this.options.auth = auth || this.options.auth;
axe.debug(DEBUG_TAG, 'Sending RSET...');
this._sendCommand('RSET');
this._currentAction = this._actionRSET;
};
/**
* Closes the connection to the server
*/
SmtpClient.prototype.close = function() {
axe.debug(DEBUG_TAG, 'Closing connection...');
if (this.socket && this.socket.readyState === 'open') {
this.socket.close();
} else {
this._destroy();
}
};
// Mail related methods
/**
* Initiates a new message by submitting envelope data, starting with
* `MAIL FROM:` command. Use after `onidle` event
*
* @param {Object} envelope Envelope object in the form of {from:"...", to:["..."]}
*/
SmtpClient.prototype.useEnvelope = function(envelope) {
this._envelope = envelope || {};
this._envelope.from = [].concat(this._envelope.from || ('anonymous@' + this.options.name))[0];
this._envelope.to = [].concat(this._envelope.to || []);
// clone the recipients array for latter manipulation
this._envelope.rcptQueue = [].concat(this._envelope.to);
this._envelope.rcptFailed = [];
this._envelope.responseQueue = [];
this._currentAction = this._actionMAIL;
axe.debug(DEBUG_TAG, 'Sending MAIL FROM...');
this._sendCommand('MAIL FROM:<' + (this._envelope.from) + '>');
};
/**
* Send ASCII data to the server. Works only in data mode (after `onready` event), ignored
* otherwise
*
* @param {String} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server
* @return {Boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more
*/
SmtpClient.prototype.send = function(chunk) {
// works only in data mode
if (!this._dataMode) {
// this line should never be reached but if it does,
// act like everything's normal.
return true;
}
// TODO: if the chunk is an arraybuffer, use a separate function to send the data
return this._sendString(chunk);
};
/**
* Indicates that a data stream for the socket is ended. Works only in data
* mode (after `onready` event), ignored otherwise. Use it when you are done
* with sending the mail. This method does not close the socket. Once the mail
* has been queued by the server, `ondone` and `onidle` are emitted.
*
* @param {Buffer} [chunk] Chunk of data to be sent to the server
*/
SmtpClient.prototype.end = function(chunk) {
// works only in data mode
if (!this._dataMode) {
// this line should never be reached but if it does,
// act like everything's normal.
return true;
}
if (chunk && chunk.length) {
this.send(chunk);
}
// redirect output from the server to _actionStream
this._currentAction = this._actionStream;
// indicate that the stream has ended by sending a single dot on its own line
// if the client already closed the data with \r\n no need to do it again
if (this._lastDataBytes === '\r\n') {
this.waitDrain = this.socket.send(new Uint8Array([0x2E, 0x0D, 0x0A]).buffer); // .\r\n
} else if (this._lastDataBytes.substr(-1) === '\r') {
this.waitDrain = this.socket.send(new Uint8Array([0x0A, 0x2E, 0x0D, 0x0A]).buffer); // \n.\r\n
} else {
this.waitDrain = this.socket.send(new Uint8Array([0x0D, 0x0A, 0x2E, 0x0D, 0x0A]).buffer); // \r\n.\r\n
}
// end data mode
this._dataMode = false;
return this.waitDrain;
};
// PRIVATE METHODS
// EVENT HANDLERS FOR THE SOCKET
/**
* Connection listener that is run when the connection to the server is opened.
* Sets up different event handlers for the opened socket
*
* @event
* @param {Event} evt Event object. Not used
*/
SmtpClient.prototype._onOpen = function() {
this.socket.ondata = this._onData.bind(this);
this.socket.onclose = this._onClose.bind(this);
this.socket.ondrain = this._onDrain.bind(this);
this._parser.ondata = this._onCommand.bind(this);
this._currentAction = this._actionGreeting;
};
/**
* Data listener for chunks of data emitted by the server
*
* @event
* @param {Event} evt Event object. See `evt.data` for the chunk received
*/
SmtpClient.prototype._onData = function(evt) {
var stringPayload = new TextDecoder('UTF-8').decode(new Uint8Array(evt.data));
axe.debug(DEBUG_TAG, 'SERVER: ' + stringPayload);
this._parser.send(stringPayload);
};
/**
* More data can be buffered in the socket, `waitDrain` is reset to false
*
* @event
* @param {Event} evt Event object. Not used
*/
SmtpClient.prototype._onDrain = function() {
this.waitDrain = false;
this.ondrain();
};
/**
* Error handler for the socket
*
* @event
* @param {Event} evt Event object. See evt.data for the error
*/
SmtpClient.prototype._onError = function(evt) {
if (evt instanceof Error && evt.message) {
axe.error(DEBUG_TAG, evt);
this.onerror(evt);
} else if (evt && evt.data instanceof Error) {
axe.error(DEBUG_TAG, evt.data);
this.onerror(evt.data);
} else {
axe.error(DEBUG_TAG, new Error(evt && evt.data && evt.data.message || evt.data || evt || 'Error'));
this.onerror(new Error(evt && evt.data && evt.data.message || evt.data || evt || 'Error'));
}
this.close();
};
/**
* Indicates that the socket has been closed
*
* @event
* @param {Event} evt Event object. Not used
*/
SmtpClient.prototype._onClose = function() {
axe.debug(DEBUG_TAG, 'Socket closed.');
this._destroy();
};
/**
* This is not a socket data handler but the handler for data emitted by the parser,
* so this data is safe to use as it is always complete (server might send partial chunks)
*
* @event
* @param {Object} command Parsed data
*/
SmtpClient.prototype._onCommand = function(command) {
if (typeof this._currentAction === 'function') {
this._currentAction.call(this, command);
}
};
/**
* Ensures that the connection is closed and such
*/
SmtpClient.prototype._destroy = function() {
if (!this.destroyed) {
this.destroyed = true;
this.onclose();
}
};
/**
* Sends a string to the socket.
*
* @param {String} chunk ASCII string (quoted-printable, base64 etc.) to be sent to the server
* @return {Boolean} If true, it is safe to send more data, if false, you *should* wait for the ondrain event before sending more
*/
SmtpClient.prototype._sendString = function(chunk) {
// escape dots
if (!this.options.disableEscaping) {
chunk = chunk.replace(/\n\./g, '\n..');
if ((this._lastDataBytes.substr(-1) === '\n' || !this._lastDataBytes) && chunk.charAt(0) === '.') {
chunk = '.' + chunk;
}
}
// Keeping eye on the last bytes sent, to see if there is a <CR><LF> sequence
// at the end which is needed to end the data stream
if (chunk.length > 2) {
this._lastDataBytes = chunk.substr(-2);
} else if (chunk.length === 1) {
this._lastDataBytes = this._lastDataBytes.substr(-1) + chunk;
}
axe.debug(DEBUG_TAG, 'Sending ' + chunk.length + ' bytes of payload');
// pass the chunk to the socket
this.waitDrain = this.socket.send(new TextEncoder('UTF-8').encode(chunk).buffer);
return this.waitDrain;
};
/**
* Send a string command to the server, also append \r\n if needed
*
* @param {String} str String to be sent to the server
*/
SmtpClient.prototype._sendCommand = function(str) {
this.waitDrain = this.socket.send(new TextEncoder('UTF-8').encode(str + (str.substr(-2) !== '\r\n' ? '\r\n' : '')).buffer);
};
/**
* Intitiate authentication sequence if needed
*/
SmtpClient.prototype._authenticateUser = function() {
if (!this.options.auth) {
// no need to authenticate, at least no data given
this._currentAction = this._actionIdle;
this.onidle(); // ready to take orders
return;
}
var auth;
if (!this.options.authMethod && this.options.auth.xoauth2) {
this.options.authMethod = 'XOAUTH2';
}
if (this.options.authMethod) {
auth = this.options.authMethod.toUpperCase().trim();
} else {
// use first supported
auth = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
}
switch (auth) {
case 'LOGIN':
// LOGIN is a 3 step authentication process
// C: AUTH LOGIN
// C: BASE64(USER)
// C: BASE64(PASS)
axe.debug(DEBUG_TAG, 'Authentication via AUTH LOGIN');
this._currentAction = this._actionAUTH_LOGIN_USER;
this._sendCommand('AUTH LOGIN');
return;
case 'PLAIN':
// AUTH PLAIN is a 1 step authentication process
// C: AUTH PLAIN BASE64(\0 USER \0 PASS)
axe.debug(DEBUG_TAG, 'Authentication via AUTH PLAIN');
this._currentAction = this._actionAUTHComplete;
this._sendCommand(
// convert to BASE64
'AUTH PLAIN ' +
btoa(unescape(encodeURIComponent(
//this.options.auth.user+'\u0000'+
'\u0000' + // skip authorization identity as it causes problems with some servers
this.options.auth.user + '\u0000' +
this.options.auth.pass))));
return;
case 'XOAUTH2':
// See https://developers.google.com/gmail/xoauth2_protocol#smtp_protocol_exchange
axe.debug(DEBUG_TAG, 'Authentication via AUTH XOAUTH2');
this._currentAction = this._actionAUTH_XOAUTH2;
this._sendCommand('AUTH XOAUTH2 ' + this._buildXOAuth2Token(this.options.auth.user, this.options.auth.xoauth2));
return;
}
this._onError(new Error('Unknown authentication method ' + auth));
};
// ACTIONS FOR RESPONSES FROM THE SMTP SERVER
/**
* Initial response from the server, must have a status 220
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionGreeting = function(command) {
if (command.statusCode !== 220) {
this._onError(new Error('Invalid greeting: ' + command.data));
return;
}
if (this.options.lmtp) {
axe.debug(DEBUG_TAG, 'Sending LHLO ' + this.options.name);
this._currentAction = this._actionLHLO;
this._sendCommand('LHLO ' + this.options.name);
} else {
axe.debug(DEBUG_TAG, 'Sending EHLO ' + this.options.name);
this._currentAction = this._actionEHLO;
this._sendCommand('EHLO ' + this.options.name);
}
};
/**
* Response to LHLO
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionLHLO = function(command) {
if (!command.success) {
axe.error(DEBUG_TAG, 'LHLO not successful');
this._onError(new Error(command.data));
return;
}
// Process as EHLO response
this._actionEHLO(command);
};
/**
* Response to EHLO. If the response is an error, try HELO instead
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionEHLO = function(command) {
var match;
if (!command.success) {
if (!this._secureMode && this.options.requireTLS) {
var errMsg = 'STARTTLS not supported without EHLO';
axe.error(DEBUG_TAG, errMsg);
this._onError(new Error(errMsg));
return;
}
// Try HELO instead
axe.warn(DEBUG_TAG, 'EHLO not successful, trying HELO ' + this.options.name);
this._currentAction = this._actionHELO;
this._sendCommand('HELO ' + this.options.name);
return;
}
// Detect if the server supports PLAIN auth
if (command.line.match(/AUTH(?:\s+[^\n]*\s+|\s+)PLAIN/i)) {
axe.debug(DEBUG_TAG, 'Server supports AUTH PLAIN');
this._supportedAuth.push('PLAIN');
}
// Detect if the server supports LOGIN auth
if (command.line.match(/AUTH(?:\s+[^\n]*\s+|\s+)LOGIN/i)) {
axe.debug(DEBUG_TAG, 'Server supports AUTH LOGIN');
this._supportedAuth.push('LOGIN');
}
// Detect if the server supports XOAUTH2 auth
if (command.line.match(/AUTH(?:\s+[^\n]*\s+|\s+)XOAUTH2/i)) {
axe.debug(DEBUG_TAG, 'Server supports AUTH XOAUTH2');
this._supportedAuth.push('XOAUTH2');
}
// Detect maximum allowed message size
if ((match = command.line.match(/SIZE (\d+)/i)) && Number(match[1])) {
this._maxAllowedSize = Number(match[1]);
axe.debug(DEBUG_TAG, 'Maximum allowd message size: ' + this._maxAllowedSize);
}
// Detect if the server supports STARTTLS
if (!this._secureMode) {
if (command.line.match(/[ \-]STARTTLS\s?$/mi) && !this.options.ignoreTLS || !!this.options.requireTLS) {
this._currentAction = this._actionSTARTTLS;
this._sendCommand('STARTTLS');
return;
}
}
this._authenticateUser.call(this);
};
/**
* Handles server response for STARTTLS command. If there's an error
* try HELO instead, otherwise initiate TLS upgrade. If the upgrade
* succeedes restart the EHLO
*
* @param {String} str Message from the server
*/
SmtpClient.prototype._actionSTARTTLS = function(command) {
if (!command.success) {
axe.error(DEBUG_TAG, 'STARTTLS not successful');
this._onError(new Error(command.data));
return;
}
this._secureMode = true;
this.socket.upgradeToSecure();
// restart protocol flow
this._currentAction = this._actionEHLO;
this._sendCommand('EHLO ' + this.options.name);
};
/**
* Response to HELO
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionHELO = function(command) {
if (!command.success) {
axe.error(DEBUG_TAG, 'HELO not successful');
this._onError(new Error(command.data));
return;
}
this._authenticateUser.call(this);
};
/**
* Response to AUTH LOGIN, if successful expects base64 encoded username
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionAUTH_LOGIN_USER = function(command) {
if (command.statusCode !== 334 || command.data !== 'VXNlcm5hbWU6') {
axe.error(DEBUG_TAG, 'AUTH LOGIN USER not successful: ' + command.data);
this._onError(new Error('Invalid login sequence while waiting for "334 VXNlcm5hbWU6 ": ' + command.data));
return;
}
axe.debug(DEBUG_TAG, 'AUTH LOGIN USER successful');
this._currentAction = this._actionAUTH_LOGIN_PASS;
this._sendCommand(btoa(unescape(encodeURIComponent(this.options.auth.user))));
};
/**
* Response to AUTH LOGIN username, if successful expects base64 encoded password
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionAUTH_LOGIN_PASS = function(command) {
if (command.statusCode !== 334 || command.data !== 'UGFzc3dvcmQ6') {
axe.error(DEBUG_TAG, 'AUTH LOGIN PASS not successful: ' + command.data);
this._onError(new Error('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6 ": ' + command.data));
return;
}
axe.debug(DEBUG_TAG, 'AUTH LOGIN PASS successful');
this._currentAction = this._actionAUTHComplete;
this._sendCommand(btoa(unescape(encodeURIComponent(this.options.auth.pass))));
};
/**
* Response to AUTH XOAUTH2 token, if error occurs send empty response
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionAUTH_XOAUTH2 = function(command) {
if (!command.success) {
axe.warn(DEBUG_TAG, 'Error during AUTH XOAUTH2, sending empty response');
this._sendCommand('');
this._currentAction = this._actionAUTHComplete;
} else {
this._actionAUTHComplete(command);
}
};
/**
* Checks if authentication succeeded or not. If successfully authenticated
* emit `idle` to indicate that an e-mail can be sent using this connection
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionAUTHComplete = function(command) {
if (!command.success) {
axe.debug(DEBUG_TAG, 'Authentication failed: ' + command.data);
this._onError(new Error(command.data));
return;
}
axe.debug(DEBUG_TAG, 'Authentication successful.');
this._authenticatedAs = this.options.auth.user;
this._currentAction = this._actionIdle;
this.onidle(); // ready to take orders
};
/**
* Used when the connection is idle and the server emits timeout
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionIdle = function(command) {
if (command.statusCode > 300) {
this._onError(new Error(command.line));
return;
}
this._onError(new Error(command.data));
};
/**
* Response to MAIL FROM command. Proceed to defining RCPT TO list if successful
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionMAIL = function(command) {
if (!command.success) {
axe.debug(DEBUG_TAG, 'MAIL FROM unsuccessful: ' + command.data);
this._onError(new Error(command.data));
return;
}
if (!this._envelope.rcptQueue.length) {
this._onError(new Error('Can\'t send mail - no recipients defined'));
} else {
axe.debug(DEBUG_TAG, 'MAIL FROM successful, proceeding with ' + this._envelope.rcptQueue.length + ' recipients');
axe.debug(DEBUG_TAG, 'Adding recipient...');
this._envelope.curRecipient = this._envelope.rcptQueue.shift();
this._currentAction = this._actionRCPT;
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>');
}
};
/**
* Response to a RCPT TO command. If the command is unsuccessful, try the next one,
* as this might be related only to the current recipient, not a global error, so
* the following recipients might still be valid
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionRCPT = function(command) {
if (!command.success) {
axe.warn(DEBUG_TAG, 'RCPT TO failed for: ' + this._envelope.curRecipient);
// this is a soft error
this._envelope.rcptFailed.push(this._envelope.curRecipient);
} else {
this._envelope.responseQueue.push(this._envelope.curRecipient);
}
if (!this._envelope.rcptQueue.length) {
if (this._envelope.rcptFailed.length < this._envelope.to.length) {
this._currentAction = this._actionDATA;
axe.debug(DEBUG_TAG, 'RCPT TO done, proceeding with payload');
this._sendCommand('DATA');
} else {
this._onError(new Error('Can\'t send mail - all recipients were rejected'));
this._currentAction = this._actionIdle;
return;
}
} else {
axe.debug(DEBUG_TAG, 'Adding recipient...');
this._envelope.curRecipient = this._envelope.rcptQueue.shift();
this._currentAction = this._actionRCPT;
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>');
}
};
/**
* Response to the RSET command. If successful, clear the current authentication
* information and reauthenticate.
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionRSET = function(command) {
if (!command.success) {
axe.error(DEBUG_TAG, 'RSET unsuccessful ' + command.data);
this._onError(new Error(command.data));
return;
}
this._authenticatedAs = null;
this._authenticateUser.call(this);
};
/**
* Response to the DATA command. Server is now waiting for a message, so emit `onready`
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionDATA = function(command) {
// response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
// some servers might use 250 instead
if ([250, 354].indexOf(command.statusCode) < 0) {
axe.error(DEBUG_TAG, 'DATA unsuccessful ' + command.data);
this._onError(new Error(command.data));
return;
}
this._dataMode = true;
this._currentAction = this._actionIdle;
this.onready(this._envelope.rcptFailed);
};
/**
* Response from the server, once the message stream has ended with <CR><LF>.<CR><LF>
* Emits `ondone`.
*
* @param {Object} command Parsed command from the server {statusCode, data, line}
*/
SmtpClient.prototype._actionStream = function(command) {
var rcpt;
if (this.options.lmtp) {
// LMTP returns a response code for *every* successfully set recipient
// For every recipient the message might succeed or fail individually
rcpt = this._envelope.responseQueue.shift();
if (!command.success) {
axe.error(DEBUG_TAG, 'Local delivery to ' + rcpt + ' failed.');
this._envelope.rcptFailed.push(rcpt);
} else {
axe.error(DEBUG_TAG, 'Local delivery to ' + rcpt + ' succeeded.');
}
if (this._envelope.responseQueue.length) {
this._currentAction = this._actionStream;
return;
}
this._currentAction = this._actionIdle;
this.ondone(true);
} else {
// For SMTP the message either fails or succeeds, there is no information
// about individual recipients
if (!command.success) {
axe.error(DEBUG_TAG, 'Message sending failed.');
} else {
axe.debug(DEBUG_TAG, 'Message sent successfully.');
}
this._currentAction = this._actionIdle;
this.ondone(!!command.success);
}
// If the client wanted to do something else (eg. to quit), do not force idle
if (this._currentAction === this._actionIdle) {
// Waiting for new connections
axe.debug(DEBUG_TAG, 'Idling while waiting for new connections...');
this.onidle();
}
};
/**
* Builds a login token for XOAUTH2 authentication command
*
* @param {String} user E-mail address of the user
* @param {String} token Valid access token for the user
* @return {String} Base64 formatted login token
*/
SmtpClient.prototype._buildXOAuth2Token = function(user, token) {
var authData = [
'user=' + (user || ''),
'auth=Bearer ' + token,
'',
''
];
// base64("user={User}\x00auth=Bearer {Token}\x00\x00")
return btoa(unescape(encodeURIComponent(authData.join('\x01'))));
};
return SmtpClient;
}));

View file

@ -0,0 +1,26 @@
'use strict';
/**
* This TextEncoder and TextDecoder are used by MimeParser. In
* addition to their standard behavior, we also support decoding utf-7.
*/
define(function(require) {
var utf7 = require('utf7');
return {
TextEncoder: function(encoding) {
var encoder = new TextEncoder(encoding);
this.encode = encoder.encode.bind(encoder);
},
TextDecoder: function(encoding) {
encoding = encoding && encoding.toLowerCase();
if (encoding === 'utf-7' || encoding === 'utf7') {
this.decode = function(buf) {
var mimefuncs = require('mimefuncs');
return utf7.decode(mimefuncs.fromTypedArray(buf));
};
} else {
var decoder = new TextDecoder(encoding);
this.decode = decoder.decode.bind(decoder)
}
}
};
});

View file

@ -0,0 +1,164 @@
'use strict';
/**
* Proxies navigator.mozTCPSocket from the worker thread to the main
* thread, as TCPSocket is not available on workers yet. This API is
* largely compatible with the real mozTCPSocket implementation,
* except for bufferedAmount and other buffering semantics (because we
* can't synchronously retrieve those values).
*
* See worker-support/net-main.js for the main-thread counterpart.
*
* NOTE: There is also a whiteout-io/tcp-socket repo, which provides a
* compatibility shim for TCPSocket on non-browser runtimes. This
* module performs the same type of wrapping (i.e. exporting TCPSocket
* directly) so that we don't have to modify browserbox/smtpclient to
* expect a different socket wrapper module.
*
* ## Sending lots of data: flow control, Blobs ##
*
* mozTCPSocket provides a flow-control mechanism (the return value to
* send indicates whether we've crossed a buffering boundary and
* 'ondrain' tells us when all buffered data has been sent), but does
* not yet support enqueueing Blobs for processing (which is part of
* the proposed standard at
* http://www.w3.org/2012/sysapps/raw-sockets/). Also, the raw-sockets
* spec calls for generating the 'drain' event once our buffered
* amount goes back under the internal buffer target rather than
* waiting for it to hit zero like mozTCPSocket.
*
* Our main desire right now for flow-control is to avoid using a lot
* of memory and getting killed by the OOM-killer. As such, flow
* control is not important to us if we're just sending something that
* we're already keeping in memory. The things that will kill us are
* giant things like attachments (or message bodies we are
* quoting/repeating, potentially) that we are keeping as Blobs.
*
* As such, rather than echoing the flow-control mechanisms over to
* this worker context, we just allow ourselves to write() a Blob and
* have the net-main.js side take care of streaming the Blobs over the
* network.
*
* Note that successfully sending a lot of data may entail holding a
* wake-lock to avoid having the network device we are using turned
* off in the middle of our sending. The network-connection
* abstraction is not currently directly involved with the wake-lock
* management, but I could see it needing to beef up its error
* inference in terms of timeouts/detecting disconnections so we can
* avoid grabbing a wi-fi wake-lock, having our connection quietly
* die, and then we keep holding the wi-fi wake-lock for much longer
* than we should.
*/
define(function(require, exports, module) {
var router = require('worker-router');
var routerMaker = router.registerInstanceType('netsocket');
var DisasterRecovery = require('disaster-recovery');
function TCPSocketProxy(host, port, options) {
options = options || {};
options.binaryType = 'arraybuffer';
// Supported TCPSocket attributes:
this.host = host;
this.port = port;
this.ssl = !!options.useSecureTransport;
this.binaryType = options.binaryType;
this.bufferedAmount = 0; // This is fake.
this.readyState = 'connecting';
// Event handlers:
var routerInfo = routerMaker.register(function(data) {
var eventHandlerName = data.cmd;
var internalHandler = this['_' + eventHandlerName];
var externalHandler = this[eventHandlerName];
// Allow this class to update internal state first:
internalHandler && internalHandler.call(this, data.args);
// Then, emulate the real TCP socket events (vaguely):
DisasterRecovery.catchSocketExceptions(this, function() {
externalHandler && externalHandler.call(this, { data: data.args });
});
}.bind(this));
this._sendMessage = routerInfo.sendMessage;
this._unregisterWithRouter = routerInfo.unregister;
this._sendMessage('open', [host, port, options]);
}
TCPSocketProxy.prototype = {
_onopen: function() {
this.readyState = 'open';
},
_onclose: function() {
this._unregisterWithRouter();
this.readyState = 'closed';
},
upgradeToSecure: function() {
this._sendMessage('upgradeToSecure', []);
},
suspend: function() {
throw new Error('tcp-socket.js does not support suspend().');
},
resume: function() {
throw new Error('tcp-socket.js does not support resume().');
},
close: function() {
if (this.readyState !== 'closed') {
this._sendMessage('close');
}
},
// We do not use transferrables by default; historically we
// wrapped a NodeJS-style API whose semantics did not take
// ownership. However, there is an optimization we want to perform
// related to Uint8Array.subarray().
//
// All the subarray does is create a view on the underlying
// buffer. This is important and notable because the structured
// clone implementation for typed arrays and array buffers is
// *not* clever; it just serializes the entire underlying buffer
// and the typed array as a view on that. (This does have the
// upside that you can transfer a whole bunch of typed arrays and
// only one copy of the buffer.) The good news is that
// ArrayBuffer.slice() does create an entirely new copy of the
// buffer, so that works with our semantics and we can use that to
// transfer only what needs to be transferred.
send: function(u8array) {
if (u8array instanceof Blob) {
// We always send blobs in their entirety; you should slice the blob and
// give us that if that's what you want.
this._sendMessage('write', [u8array]);
}
else if (u8array instanceof ArrayBuffer) {
this._sendMessage('write', [u8array, 0, u8array.byteLength]);
}
// Slice the underlying buffer and transfer it if the array is a subarray
else if (u8array.byteOffset !== 0 ||
u8array.length !== u8array.buffer.byteLength) {
var buf = u8array.buffer.slice(u8array.byteOffset,
u8array.byteOffset + u8array.length);
this._sendMessage('write',
[buf, 0, buf.byteLength],
[buf]);
}
else {
this._sendMessage('write',
[u8array.buffer, u8array.byteOffset, u8array.length]);
}
return true;
}
};
return {
open: function(host, port, options) {
return new TCPSocketProxy(host, port, options);
}
};
});

View file

@ -0,0 +1,208 @@
// Copyright (c) 2010-2011 Konstantin Käfer
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
(function(root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.utf7 = factory();
}
}(this, function() {
'use strict';
function encode(str) {
var b = new Uint8Array(str.length * 2),
octets = '',
i, bi, len, c, encoded;
for (i = 0, bi = 0, len = str.length; i < len; i++) {
// Note that we can't simply convert a UTF-8 string to Base64 because
// UTF-8 uses a different encoding. In modified UTF-7, all characters
// are represented by their two byte Unicode ID.
c = str.charCodeAt(i);
// Upper 8 bits shifted into lower 8 bits so that they fit into 1 byte.
b[bi++] = c >> 8;
// Lower 8 bits. Cut off the upper 8 bits so that they fit into 1 byte.
b[bi++] = c & 0xFF;
}
// Convert b:Uint8Array to a binary string
for (i = 0, len = b.length; i < len; i++) {
octets += String.fromCharCode(b[i]);
}
// Modified Base64 uses , instead of / and omits trailing =.
encoded = '';
if (typeof window !== 'undefined' && btoa) {
encoded = btoa(octets);
} else {
encoded = (new Buffer(octets, "binary")).toString("base64");
}
return encoded.replace(/=+$/, '');
}
/**
* Safe base64 decoding. Does not throw on unexpected input.
*
* Implementation from the MDN docs:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding
* (MDN code samples are MIT licensed)
*
* @param {String} base64Str Base64 encoded string
* @returns {Uint8Array} Decoded binary blob
*/
function base64toTypedArray(base64Str) {
var bitsSoFar = 0;
var validBits = 0;
var iOut = 0;
var arr = new Uint8Array(Math.ceil(base64Str.length * 3 / 4));
var c;
var bits;
for (var i = 0, len = base64Str.length; i < len; i++) {
c = base64Str.charCodeAt(i);
if (c >= 0x41 && c <= 0x5a) { // [A-Z]
bits = c - 0x41;
} else if (c >= 0x61 && c <= 0x7a) { // [a-z]
bits = c - 0x61 + 0x1a;
} else if (c >= 0x30 && c <= 0x39) { // [0-9]
bits = c - 0x30 + 0x34;
} else if (c === 0x2b) { // +
bits = 0x3e;
} else if (c === 0x2f) { // /
bits = 0x3f;
} else if (c === 0x3d) { // =
validBits = 0;
continue;
} else {
// ignore all other characters!
continue;
}
bitsSoFar = (bitsSoFar << 6) | bits;
validBits += 6;
if (validBits >= 8) {
validBits -= 8;
arr[iOut++] = bitsSoFar >> validBits;
if (validBits === 2) {
bitsSoFar &= 0x03;
} else if (validBits === 4) {
bitsSoFar &= 0x0f;
}
}
}
if (iOut < arr.length) {
return arr.subarray(0, iOut);
}
return arr;
}
function decode(str) {
var octets = base64toTypedArray(str),
r = [];
for (var i = 0, len = octets.length; i < len;) {
// Calculate charcode from two adjacent bytes.
r.push(String.fromCharCode(octets[i++] << 8 | octets[i++]));
}
return r.join('');
}
// Escape RegEx from http://simonwillison.net/2006/Jan/20/escape/
function escape(chars) {
return chars.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
// Character classes defined by RFC 2152.
var setD = 'A-Za-z0-9' + escape('\'(),-./:?'),
setO = escape('!"#$%&*;<=>@[]^_\'{|}'),
setW = escape(' \r\n\t'),
// Stores compiled regexes for various replacement pattern.
regexes = {},
regexAll = new RegExp('[^' + setW + setD + setO + ']+', 'g');
return {
// RFC 2152 UTF-7 encoding.
encode: function(str, mask) {
// Generate a RegExp object from the string of mask characters.
if (!mask) {
mask = '';
}
if (!regexes[mask]) {
regexes[mask] = new RegExp('[^' + setD + escape(mask) + ']+', 'g');
}
// We replace subsequent disallowed chars with their escape sequence.
return str.replace(regexes[mask], function(chunk) {
// + is represented by an empty sequence +-, otherwise call encode().
return '+' + (chunk === '+' ? '' : encode(chunk)) + '-';
});
},
// RFC 2152 UTF-7 encoding with all optionals.
encodeAll: function(str) {
// We replace subsequent disallowed chars with their escape sequence.
return str.replace(regexAll, function(chunk) {
// + is represented by an empty sequence +-, otherwise call encode().
return '+' + (chunk === '+' ? '' : encode(chunk)) + '-';
});
},
// RFC 2152 UTF-7 decoding.
decode: function(str) {
return str.replace(/\+([A-Za-z0-9\/]*)-?/gi, function(_, chunk) {
// &- represents &.
if (chunk === '') {
return '+';
}
return decode(chunk);
});
},
imap: {
// RFC 3501, section 5.1.3 UTF-7 encoding.
encode: function(str) {
// All printable ASCII chars except for & must be represented by themselves.
// We replace subsequent non-representable chars with their escape sequence.
return str.replace(/&/g, '&-').replace(/[^\x20-\x7e]+/g, function(chunk) {
// & is represented by an empty sequence &-, otherwise call encode().
chunk = (chunk === '&' ? '' : encode(chunk)).replace(/\//g, ',');
return '&' + chunk + '-';
});
},
// RFC 3501, section 5.1.3 UTF-7 decoding.
decode: function(str) {
return str.replace(/&([^-]*)-/g, function(_, chunk) {
// &- represents &.
if (chunk === '') {
return '&';
}
return decode(chunk.replace(/,/g, '/'));
});
}
}
};
}));

View file

@ -0,0 +1,4 @@
// Alias for 'utf7', could alternately be in our RequireJS config.
define(['./utf7'], function(utf7) {
return utf7;
});

View file

@ -0,0 +1,93 @@
/**
* This file implements a function which performs a
* a streaming search of a folder to determine the count of
* headers which match a particular filter.
*/
define(
[
'module',
'exports'
],
function(
$module,
exports) {
exports.countHeaders = function(storage, filter, options, callback) {
var fetchClobber = null;
if (typeof options === "function") {
callback = options;
} else {
fetchClobber = options.fetchSize;
}
var matched = 0;
// Relatively arbitrary value, but makes sure we don't use too much
// memory while streaming
var fetchSize = fetchClobber || 100;
var startTS = null;
var startUID = null;
function gotMessages(dir, callback, headers, moreMessagesComing) {
// conditionally indent messages that are non-notable callbacks since we
// have more messages coming. sanity measure for asuth for now.
var logPrefix = moreMessagesComing ? 'sf: ' : 'sf:';
console.log(logPrefix, 'gotMessages', headers.length, 'more coming?',
moreMessagesComing);
// update the range of what we have seen and searched
if (headers.length) {
var lastHeader = headers[headers.length - 1];
startTS = lastHeader.date;
startUID = lastHeader.id;
}
var checkHandle = function checkHandle(headers) {
// Update the matched count
for (var i = 0; i < headers.length; i++) {
var header = headers[i];
var isMatch = filter(header);
if (isMatch) {
matched++;
}
}
var atBottom = storage.headerIsOldestKnown(
startTS, startUID);
var canGetMore = !atBottom,
wantMore = !moreMessagesComing && canGetMore;
if (wantMore) {
console.log(logPrefix, 'requesting more because want more');
getNewMessages(dir, false, true, callback);
} else if (!moreMessagesComing) {
callback(matched);
}
// (otherwise we need to wait for the additional messages to show before
// doing anything conclusive)
};
checkHandle(headers);
}
function getNewMessages (dirMagnitude, userRequestsGrowth, autoDoNotDesireMore,
callback) {
storage.flushExcessCachedBlocks('countHeaders');
storage.getMessagesBeforeMessage(startTS, startUID,
fetchSize, gotMessages.bind(null, 1, callback));
}
storage.getMessagesInImapDateRange(
0, null, fetchSize, fetchSize,
gotMessages.bind(null, 1, callback));
};
}); // end define

View file

@ -0,0 +1,637 @@
/**
* Process text/html for message body purposes. Specifically:
*
* - sanitize HTML (using bleach.js): discard illegal markup entirely, render
* legal but 'regulated' markup inert (ex: links to external content).
* - TODO: perform normalization of quote markup from different clients into
* blockquotes, like how Thunderbird conversations does it.
* - snippet generation: Try and generate a usable snippet string from something
* that is not a quote.
*
* We may eventually try and perform more detailed analysis like `quotechew.js`
* does with structured markup, potentially by calling out to quotechew, but
* that's a tall order to get right, so it's mightily postponed.
**/
define(
[
'exports',
'bleach'
],
function(
exports,
$bleach
) {
/**
* Whitelisted HTML tags list. Currently from nsTreeSanitizer.cpp which credits
* Mark Pilgrim and Sam Ruby for its own initial whitelist.
*
* IMPORTANT THUNDERBIRD NOTE: Thunderbird only engages its sanitization logic
* when processing mailto URIs, when the non-default
* "view | message body as | simple html" setting is selected, or when
* displaying spam messages. Accordingly, the settings are pretty strict
* and not particularly thought-out. Non-CSS presentation is stripped, which
* is pretty much the lingua franca of e-mail. (Thunderbird itself generates
* font tags, for example.)
*
* Some things are just not in the list at all:
* - SVG: Thunderbird nukes these itself because it forces
* SanitizerCidEmbedsOnly which causes flattening of everything in the SVG
* namespace.
*
* Tags that we are opting not to include will be commented with a reason tag:
* - annoying: This thing is ruled out currently because it only allows annoying
* things to happen *given our current capabilities*.
* - scripty: This thing requires scripting to make anything happen, and we do
* not allow scripting.
* - forms: We have no UI to expose the target of a form right now, so it's
* not safe. Thunderbird displays a scam warning, which isn't realy a big
* help, but it's something. Because forms are largely unsupported or just
* broken in many places, they are rarely used, so we are turning them off
* entirely.
* - non-body: previously killed as part of the parse process because we were
* assigning to innerHTML rather than creating a document with the string in
* it. We could change this up in a future bug now.
* - dangerous: The semantics of the tag are intentionally at odds with our
* goals and/or are extensible. (ex: link tag.) Our callbacks could be
* used to only let through okay things.
* - interactive-ui: A cross between scripty and forms, things like (HTML5)
* menu and command imply some type of mutation that requires scripting.
* They also are frequently very attribute-heavy.
* - svg: it's SVG, we don't support it yet!
*/
var LEGAL_TAGS = [
'a', 'abbr', 'acronym', 'area', 'article', 'aside',
// annoying: 'audio',
'b',
'bdi', 'bdo', // (bidirectional markup stuff)
'big', 'blockquote',
// implicitly-nuked: 'body'
'br',
// forms: 'button',
// scripty: canvas
'caption',
'center',
'cite', 'code', 'col', 'colgroup',
// interactive-ui: 'command',
// forms: 'datalist',
'dd', 'del', 'details', 'dfn', 'dir', 'div', 'dl', 'dt',
'em',
// forms: 'fieldset' (but allowed by nsTreeSanitizer)
'figcaption', 'figure',
'font',
'footer',
// forms: 'form',
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
// non-body: 'head'
'header', 'hgroup', 'hr',
// non-body: 'html'
'i', 'img',
// forms: 'input',
'ins', // ("represents a range of text that has been inserted to a document")
'kbd', // ("The kbd element represents user input")
'label', 'legend', 'li',
// dangerous: link (for CSS styles
/* link supports many types, none of which we want, some of which are
* risky: http://dev.w3.org/html5/spec/links.html#linkTypes. Specifics:
* - "stylesheet": This would be okay for cid links, but there's no clear
* advantage over inline styles, so we forbid it, especially as supporting
* it might encourage other implementations to dangerously support link.
* - "prefetch": Its whole point is de facto information leakage.
*/
'listing', // (deprecated, like "pre")
'map', 'mark',
// interactive-ui: 'menu', 'meta', 'meter',
'nav',
'nobr', // (deprecated "white-space:nowrap" equivalent)
'noscript',
'ol',
// forms: 'optgroup',
// forms: 'option',
'output', // (HTML5 draft: "result of a calculation in a form")
'p', 'pre',
// interactive-ui: 'progress',
'q',
/* http://www.w3.org/TR/ruby/ is a pronounciation markup that is not directly
* supported by gecko at this time (although there is a Firefox extension).
* All of 'rp', 'rt', and 'ruby' are ruby tags. The spec also defines 'rb'
* and 'rbc' tags that nsTreeSanitizer does not whitelist, however.
*/
'rp', 'rt', 'ruby',
's', 'samp', 'section',
// forms: 'select',
'small',
// annoying?: 'source',
'span', 'strike', 'strong',
'style',
'sub', 'summary', 'sup',
// svg: 'svg', NB: this lives in its own namespace
'table', 'tbody', 'td',
// forms: 'textarea',
'tfoot', 'th', 'thead', 'time',
'title', // XXX does this mean anything outside head?
'tr',
// annoying?: 'track'
'tt',
'u', 'ul', 'var',
// annoying: 'video',
'wbr' // (HTML5 draft: line break opportunity)
];
/**
* Tags whose children should be removed along with the tag itself, rather than
* splicing the children into the position originally occupied by the parent.
*
* We do this for:
* - forms; see `LEGAL_TAGS` for the rationale. Note that we don't bother
* including children that should already be nuked by PRUNE_TAGS. For
* example, 'option' and 'optgroup' only make sense under 'select' or
* 'datalist', so we need not include them. This means that if the tags
* are used in nonsensical positions, they will have their contents
* merged into the document text, but that's not a major concern.
* - non-body: don't have stuff from the header show up like it's part of the
* body! For now we do want <style> tags to fall out, but we want <title>
* to not show up, etc.
* - 'script': no one wants to read the ignored JS code!
* Note that bleach.js now is aware of the special nature of 'script' and
* 'style' tags, so putting them in prune is not strictly required.
*/
var PRUNE_TAGS = [
'button', // (forms)
'datalist', // (forms)
'script', // (script)
'select', // (forms)
'svg', // (svg)
'title', // (non-body)
];
/**
* What attributes to allow globally and on specific tags.
*
* Forbidden marker names:
* - URL-like: The attribute can contain URL's and we don't care enough to
* sanitize the contents right now.
* - sanitized: We manually do something with the attribute in our processing
* logic.
* - specific: The attribute is explicitly named on the relevant element types.
* - unsupported: Gecko ignores the attribute and there is no chance of
* standardization, so just strip it.
* - microformat: we can't do anything with microformats right now, save some
* space.
* - awkward: It's not dangerous, but it's not clear how it could have useful
* semantics.
*/
var LEGAL_ATTR_MAP = {
'*': [
'abbr', // (tables: removed from HTML5)
// forms: 'accept', 'accept-charset',
// interactive-ui: 'accesskey',
// forms: 'action',
'align', // (pres)
'alt', // (fallback content)
// forms: 'autocomplete', 'autofocus',
// annoying: 'autoplay',
'axis', // (tables: removed from HTML5)
// URL-like: 'background',
'bgcolor', 'border', // (pres)
'cellpadding', 'cellspacing', // (pres)
// unsupported: 'char',
'charoff', // (tables)
// specific: 'charset'
// forms, interactive-ui: 'checked',
// URL-like: 'cite'
'class', 'clear', 'color', // (pres)
'cols', 'colspan', // (tables)
'compact', // (pres)
// dangerous: 'content', (meta content refresh is bad.)
// interactive-ui: 'contenteditable', (we already use this ourselves!)
// interactive-ui: 'contextmenu',
// annoying: 'controls', (media)
'coords', // (area image map)
'datetime', // (ins, del, time semantic markups)
// forms: 'disabled',
'dir', // (rtl)
// interactive-ui: 'draggable',
// forms: 'enctype',
'face', // (pres)
// forms: 'for',
'frame', // (tables)
'headers', // (tables)
'height', // (layout)
// interactive-ui: 'hidden', 'high',
// sanitized: 'href',
// specific: 'hreflang',
'hspace', // (pres)
// dangerous: 'http-equiv' (meta refresh, maybe other trickiness)
// interactive-ui: 'icon',
'id', // (pres; white-listed for style targets)
// specific: 'ismap', (area image map)
// microformat: 'itemid', 'itemprop', 'itemref', 'itemscope', 'itemtype',
// annoying: 'kind', (media)
// annoying, forms, interactive-ui: 'label',
'lang', // (language support)
// forms: 'list',
// dangerous: 'longdesc', (link to a long description, html5 removed)
// annoying: 'loop',
// interactive-ui: 'low',
// forms, interactive-ui: 'max',
// forms: 'maxlength',
'media', // (media-query for linky things; safe if links are safe)
// forms: 'method',
// forms, interactive-ui: 'min',
// unsupported: 'moz-do-not-send', (thunderbird internal composition)
// forms: 'multiple',
// annoying: 'muted',
// forms, interactive-ui: 'name', (although pretty safe)
'nohref', // (image maps)
// forms: 'novalidate',
'noshade', // (pres)
'nowrap', // (tables)
'open', // (for "details" element)
// interactive-ui: 'optimum',
// forms: 'pattern', 'placeholder',
// annoying: 'playbackrate',
'pointsize', // (pres)
// annoying: 'poster', 'preload',
// forms: 'prompt',
'pubdate', // ("time" element)
// forms: 'radiogroup', 'readonly',
// dangerous: 'rel', (link rel, a rel, area rel)
// forms: 'required',
// awkward: 'rev' (reverse link; you can't really link to emails)
'reversed', // (pres? "ol" reverse numbering)
// interactive-ui: 'role', We don't want a screen reader making the user
// think that part of the e-mail is part of the UI. (WAI-ARIA defines
// "accessible rich internet applications", not content markup.)
'rows', 'rowspan', 'rules', // (tables)
// sanitized: 'src',
'size', // (pres)
'scope', // (tables)
'scoped', // (pres; on "style" elem)
// forms: 'selected',
'shape', // (image maps)
'span', // (tables)
// interactive-ui: 'spellcheck',
// sanitized, dangerous: 'src'
// annoying: 'srclang',
'start', // (pres? "ol" numbering)
'summary', // (tables accessibility)
'style', // (pres)
// interactive-ui: 'tabindex',
// dangerous: 'target', (specifies a browsing context, but our semantics
// are extremely clear and don't need help.)
'title', // (advisory)
// specific, dangerous: type (various, but mime-type for links is not the
// type of thing we would ever want to propagate or potentially deceive
// the user with.)
'valign', // (pres)
'value', // (pres? "li" override for "ol"; various form uses)
'vspace', // (pres)
'width', // (layout)
// forms: 'wrap',
],
'a': ['ext-href', 'hreflang'],
'area': ['ext-href', 'hreflang'],
// these are used by our quoting and Thunderbird's quoting
'blockquote': ['cite', 'type'],
'img': ['cid-src', 'ext-src', 'ismap', 'usemap'],
// This may only end up being used as a debugging thing, but let's let charset
// through for now.
'meta': ['charset'],
'ol': ['type'], // (pres)
'style': ['type'],
};
/**
* CSS Style rules to support.
*
* nsTreeSanitizer is super lazy about style binding and does not help us out.
* What it does is nuke all rule types except NAMESPACE (@namespace), FONT_FACE
* (@font-face), and STYLE rules (actual styling). This means nuking CHARSET
* (@charset to specify the encoding of the stylesheet if the server did not
* provide it), IMPORT (@import to reference other stylesheet files), MEDIA
* (@media media queries), PAGE (@page page box info for paged media),
* MOZ_KEYFRAMES, MOZ_KEYFRAME, SUPPORTS (@supports provides support for rules
* conditioned on browser support, but is at risk.) The only style directive it
* nukes is "-moz-binding" which is the XBL magic and considered dangerous.
*
* Risks: Anything that takes a url() is dangerous insofar as we need to
* sanitize the url. XXX for now we just avoid any style that could potentially
* hold a URI.
*
* Good news: We always cram things into an iframe, so we don't need to worry
* about clever styling escaping out into our UI.
*
* New reasons not to allow:
* - animation: We don't want or need animated wackiness.
* - slow: Doing the thing is slow!
*/
var LEGAL_STYLES = [
// animation: animation*
// URI-like: background, background-image
'background-color',
// NB: border-image is not set by the 'border' aliases
'border',
'border-bottom', 'border-bottom-color', 'border-bottom-left-radius',
'border-bottom-right-radius', 'border-bottom-style', 'border-bottom-width',
'border-color',
// URI-like: border-image*
'border-left', 'border-left-color', 'border-left-style', 'border-left-width',
'border-radius',
'border-right', 'border-right-color', 'border-right-style',
'border-right-width',
'border-style',
'border-top', 'border-top-color', 'border-top-left-radius',
'border-top-right-radius', 'border-top-style', 'border-top-width',
'border-width',
// slow: box-shadow
'clear',
'color',
'display',
'float',
'font-family',
'font-size',
'font-style',
'font-weight',
'height',
'line-height',
// URI-like: list-style, list-style-image
'list-style-position',
'list-style-type',
'margin', 'margin-bottom', 'margin-left', 'margin-right', 'margin-top',
'padding', 'padding-bottom', 'padding-left', 'padding-right', 'padding-top',
'text-align', 'text-align-last',
'text-decoration', 'text-decoration-color', 'text-decoration-line',
'text-decoration-style', 'text-indent',
'vertical-align',
'white-space',
'width',
'word-break', 'word-spacing', 'word-wrap',
];
/**
* The regular expression to detect nodes that should be passed to stashLinks.
*
* ignore-case is not required; the value is checked against the lower-cased tag.
*/
var RE_NODE_NEEDS_TRANSFORM = /^(?:a|area|img)$/;
var RE_CID_URL = /^cid:/i;
var RE_HTTP_URL = /^http(?:s)?/i;
var RE_MAILTO_URL = /^mailto:/i;
var RE_IMG_TAG = /^img$/;
function getAttributeFromList(attrs, name) {
var len = attrs.length;
for (var i = 0; i < len; i++) {
var attr = attrs[i];
if (attr.name.toLowerCase() === name) {
return attr;
}
}
return null;
}
/**
* Transforms src tags, ensure that links are http and transform them too so
* that they don't actually navigate when clicked on but we can hook them. (The
* HTML display iframe is not intended to navigate; we just want to trigger the
* browser.
*/
function stashLinks(lowerTag, attrs) {
var classAttr;
// - img: src
if (RE_IMG_TAG.test(lowerTag)) {
// filter out things we might write to, also find the 'class attr'
attrs = attrs.filter(function(attr) {
switch (attr.name.toLowerCase()) {
case 'cid-src':
case 'ext-src':
return false;
case 'class':
classAttr = attr;
default:
return true;
}
});
var srcAttr = getAttributeFromList(attrs, 'src');
if (srcAttr) {
if (RE_CID_URL.test(srcAttr.escaped)) {
srcAttr.name = 'cid-src';
if (classAttr)
classAttr.escaped += ' moz-embedded-image';
else
attrs.push({ name: 'class', escaped: 'moz-embedded-image' });
// strip the cid: bit, it is necessarily there and therefore redundant.
srcAttr.escaped = srcAttr.escaped.substring(4);
}
else if (RE_HTTP_URL.test(srcAttr.escaped)) {
srcAttr.name = 'ext-src';
if (classAttr)
classAttr.escaped += ' moz-external-image';
else
attrs.push({ name: 'class', escaped: 'moz-external-image' });
}
}
}
// - a, area: href
else {
// filter out things we might write to, also find the 'class attr'
attrs = attrs.filter(function(attr) {
switch (attr.name.toLowerCase()) {
case 'cid-src':
case 'ext-src':
return false;
case 'class':
classAttr = attr;
default:
return true;
}
});
var linkAttr = getAttributeFromList(attrs, 'href');
if (linkAttr) {
var link = linkAttr.escaped;
if (RE_HTTP_URL.test(link) ||
RE_MAILTO_URL.test(link)) {
linkAttr.name = 'ext-href';
if (classAttr)
classAttr.escaped += ' moz-external-link';
else
attrs.push({ name: 'class', escaped: 'moz-external-link' });
}
else {
// paranoia; no known benefit if this got through
attrs.splice(attrs.indexOf(linkAttr), 1);
}
}
}
return attrs;
}
var BLEACH_SETTINGS = {
tags: LEGAL_TAGS,
strip: true,
stripComments: true,
prune: PRUNE_TAGS,
attributes: LEGAL_ATTR_MAP,
styles: LEGAL_STYLES,
asNode: true,
callbackRegexp: RE_NODE_NEEDS_TRANSFORM,
callback: stashLinks
};
var BLEACH_SNIPPET_SETTINGS = {
tags: [],
strip: true,
stripComments: true,
prune: [
'style',
'button', // (forms)
'datalist', // (forms)
'script', // (script)
'select', // (forms)
'svg', // (svg)
'title' // (non-body)
],
asNode: true,
maxLength: 100
};
/**
* @args[
* @param[htmlString String]{
* An unsanitized HTML string. The HTML content can be a fully valid HTML
* document with 'html' and 'body' tags and such, but most of that extra
* structure will currently be discarded.
*
* In the future we may try and process the body and such correctly, but for
* now we don't. This is consistent with many webmail clients who ignore
* style tags in the head, etc.
* }
* ]
* @return[HtmlString]{
* The sanitized HTML string wrapped into a div container.
* }
*/
exports.sanitizeAndNormalizeHtml = function sanitizeAndNormalize(htmlString) {
return $bleach.clean(htmlString, BLEACH_SETTINGS);
};
/**
* Derive snippet text from the an HTML string. It will also sanitize it.
* Note that it unescapes HTML enttities, so best to only use this output
* in textContent cases.
*/
exports.generateSnippet = function generateSnippet(htmlString) {
return $bleach.unescapeHTMLEntities($bleach.clean(htmlString,
BLEACH_SNIPPET_SETTINGS));
};
var BLEACH_SEARCHABLE_TEXT_WITH_QUOTES_SETTINGS = {
tags: [],
strip: true,
stripComments: true,
prune: [
'style',
'button', // (forms)
'datalist', // (forms)
'script', // (script)
'select', // (forms)
'svg', // (svg)
'title' // (non-body)
],
asNode: true,
};
var BLEACH_SEARCHABLE_TEXT_WITHOUT_QUOTES_SETTINGS = {
tags: [],
strip: true,
stripComments: true,
prune: [
'style',
'button', // (forms)
'datalist', // (forms)
'script', // (script)
'select', // (forms)
'svg', // (svg)
'title', // (non-body),
// specific to getting rid of quotes:
'blockquote'
],
asNode: true,
};
/**
* Produce a textual version of the body of the e-mail suitable for search
* purposes. This is basically the same thing as generateSnippet but without a
* length limit applied and with the ability to either include quoted text or
* not include quoted text. We do process the entire document in a go and
* return the entire results, so this could be fairly inefficient from a
* memory/time perspective.
*
* The following potential enhancements could be fairly good ideas:
* - Avoid processing the entire HTML document by passing the search string in
* or using a generator-type implementation to yield incremental string hunks.
* - Generate a semantic representation similar/identical to the one used by
* quotechew (at least for this searchable text mode.)
*/
exports.generateSearchableTextVersion = function(htmlString, includeQuotes) {
var settings;
if (includeQuotes) {
settings = BLEACH_SEARCHABLE_TEXT_WITH_QUOTES_SETTINGS;
}
else {
settings = BLEACH_SEARCHABLE_TEXT_WITHOUT_QUOTES_SETTINGS;
}
var cleaned = $bleach.clean(htmlString, settings);
return $bleach.unescapeHTMLEntities(cleaned);
};
/**
* Wrap text/plain content into a serialized HTML string safe for insertion
* via innerHTML.
*
* By default we wrap everything in a 'div' tag with 'br' indicating newlines.
* Alternately, we could use 'white-space: pre-wrap' if we were more confident
* about recipients having sufficient CSS support and our own desire to have
* things resemble text/plain.
*
*/
exports.wrapTextIntoSafeHTMLString = function(text, wrapTag,
transformNewlines, attrs) {
if (transformNewlines === undefined) {
transformNewlines = true;
}
wrapTag = wrapTag || 'div';
text = $bleach.escapePlaintextIntoElementContext(text);
text = transformNewlines ? text.replace(/\n/g, '<br/>') : text;
var attributes = '';
if (attrs) {
var len = attrs.length;
for (var i = 0; i < len; i += 2) {
attributes += ' ' + attrs[i] + '="' +
$bleach.escapePlaintextIntoAttribute(attrs[i + 1]) + '"';
}
}
return '<' + wrapTag + attributes + '>' + text + '</' + wrapTag + '>';
};
var RE_QUOTE_CHAR = /"/g;
/**
* Make an HTML attribute value safe.
*/
exports.escapeAttrValue = function(s) {
return s.replace(RE_QUOTE_CHAR, '&quot;');
};
}); // end define

View file

@ -0,0 +1,926 @@
define(
[
'logic',
'../a64',
'../accountmixins',
'../allback',
'../errbackoff',
'../mailslice',
'../searchfilter',
'../syncbase',
'../util',
'../composite/incoming',
'./folder',
'./jobs',
'./client',
'../errorutils',
'../disaster-recovery',
'module',
'require',
'exports'
],
function(
logic,
$a64,
$acctmixins,
$allback,
$errbackoff,
$mailslice,
$searchfilter,
$syncbase,
$util,
incoming,
$imapfolder,
$imapjobs,
$imapclient,
errorutils,
DisasterRecovery,
$module,
require,
exports
) {
var bsearchForInsert = $util.bsearchForInsert;
var allbackMaker = $allback.allbackMaker;
var CompositeIncomingAccount = incoming.CompositeIncomingAccount;
function cmpFolderPubPath(a, b) {
return a.path.localeCompare(b.path);
}
/**
* Account object, root of all interaction with servers.
*
* Passwords are currently held in cleartext with the rest of the data. Ideally
* we would like them to be stored in some type of keyring coupled to the TCP
* API in such a way that we never know the API. Se a vida e.
*
*/
function ImapAccount(universe, compositeAccount, accountId, credentials,
connInfo, folderInfos,
dbConn, existingProtoConn) {
// Using the generic 'Account' here, as current tests don't
// distinguish between events on ImapAccount vs. CompositeAccount.
logic.defineScope(this, 'Account', { accountId: accountId,
accountType: 'imap' });
CompositeIncomingAccount.apply(
this, [$imapfolder.ImapFolderSyncer].concat(Array.slice(arguments)));
/**
* The maximum number of connections we are allowed to have alive at once. We
* want to limit this both because we generally aren't sophisticated enough
* to need to use many connections at once (unless we have bugs), and because
* servers may enforce a per-account connection limit which can affect both
* us and other clients on other devices.
*
* Thunderbird's default for this is 5.
*
* gmail currently claims to have a limit of 15 connections per account:
* http://support.google.com/mail/bin/answer.py?hl=en&answer=97150
*
* I am picking 3 right now because it should cover the "I just sent a
* messages from the folder I was in and then switched to another folder",
* where we could have stuff to do in the old folder, new folder, and sent
* mail folder. I have also seem claims of connection limits of 3 for some
* accounts out there, so this avoids us needing logic to infer a need to
* lower our connection limit.
*/
this._maxConnsAllowed = 3;
/**
* The `ImapConnection` we are attempting to open, if any. We only try to
* open one connection at a time.
*/
this._pendingConn = null;
this._ownedConns = [];
/**
* @listof[@dict[
* @key[folderId]
* @key[callback]
* ]]{
* The list of requested connections that have not yet been serviced. An
* }
*/
this._demandedConns = [];
this._backoffEndpoint = $errbackoff.createEndpoint('imap:' + this.id, this);
if (existingProtoConn)
this._reuseConnection(existingProtoConn);
this._jobDriver = new $imapjobs.ImapJobDriver(
this, this._folderInfos.$mutationState);
/**
* Flag to allow us to avoid calling closeBox to close a folder. This avoids
* expunging deleted messages.
*/
this._TEST_doNotCloseFolder = false;
// Immediately ensure that we have any required local-only folders,
// as those can be created even while offline.
this.ensureEssentialOfflineFolders();
}
exports.Account = exports.ImapAccount = ImapAccount;
ImapAccount.prototype = Object.create(CompositeIncomingAccount.prototype);
var properties = {
type: 'imap',
supportsServerFolders: true,
toString: function() {
return '[ImapAccount: ' + this.id + ']';
},
//////////////////////////////////////////////////////////////////////////////
// Server type indicators for quirks and heuristics like sent mail
/**
* Is this server gmail? Not something that just looks like gmail, but IS
* gmail.
*
* Gmail self-identifies via the nonstandard but documented X-GM-EXT-1
* capability. Documentation is at
* https://developers.google.com/gmail/imap_extensions
*/
get isGmail() {
return this.meta.capability.indexOf('X-GM-EXT-1') !== -1;
},
/**
* Is this a CoreMail server, as used by 126.com/163.com/others?
*
* CoreMail servers self-identify via the apparently cargo-culted
* X-CM-EXT-1 capability.
*/
get isCoreMailServer() {
return this.meta.capability.indexOf('X-CM-EXT-1') !== -1;
},
/**
* Do messages sent via the corresponding SMTP account automatically show up
* in the sent folder? Both Gmail and CoreMail do this. (It's a good thing
* to do, it just sucks that there's no explicit IMAP capability, etc. to
* indicate this without us having to infer from the server type. Although
* we could probe this if we wanted...)
*/
get sentMessagesAutomaticallyAppearInSentFolder() {
return this.isGmail || this.isCoreMailServer;
},
//////////////////////////////////////////////////////////////////////////////
// Connection Pool-ish stuff
get numActiveConns() {
return this._ownedConns.length;
},
/**
* Mechanism for an `ImapFolderConn` to request an IMAP protocol connection.
* This is to potentially support some type of (bounded) connection pooling
* like Thunderbird uses. The rationale is that many servers cap the number
* of connections we are allowed to maintain, plus it's hard to justify
* locally tying up those resources. (Thunderbird has more need of watching
* multiple folders than ourselves, but we may still want to synchronize a
* bunch of folders in parallel for latency reasons.)
*
* The provided connection will *not* be in the requested folder; it's up to
* the folder connection to enter the folder.
*
* @args[
* @param[folderId #:optional FolderId]{
* The folder id of the folder that will be using the connection. If
* it's not a folder but some task, then pass null (and ideally provide
* a useful `label`).
* }
* @param[label #:optional String]{
* A human readable explanation of the activity for debugging purposes.
* }
* @param[callback @func[@args[@param[conn]]]]{
* The callback to invoke once the connection has been established. If
* there is a connection present in the reuse pool, this may be invoked
* immediately.
* }
* @param[deathback Function]{
* A callback to invoke if the connection dies or we feel compelled to
* reclaim it.
* }
* @param[dieOnConnectFailure #:optional Boolean]{
* Should we invoke the deathback for this request if we fail to establish
* a connection in a timely manner? This will be immediately invoked if
* we are offline or if we exhaust our retries for establishing
* connections with the server.
* }
* ]
*/
__folderDemandsConnection: function(folderId, label, callback, deathback,
dieOnConnectFailure) {
// If we are offline, invoke the deathback soon and don't bother trying to
// get a connection.
if (dieOnConnectFailure && !this.universe.online) {
window.setZeroTimeout(deathback);
return;
}
var demand = {
folderId: folderId,
label: label,
callback: callback,
deathback: deathback,
dieOnConnectFailure: Boolean(dieOnConnectFailure)
};
this._demandedConns.push(demand);
// No line-cutting; bail if there was someone ahead of us.
if (this._demandedConns.length > 1)
return;
// - try and reuse an existing connection
if (this._allocateExistingConnection())
return;
// - we need to wait for a new conn or one to free up
this._makeConnectionIfPossible();
return;
},
/**
* Trigger the deathbacks for all connection demands where dieOnConnectFailure
* is true.
*/
_killDieOnConnectFailureDemands: function() {
for (var i = 0; i < this._demandedConns.length; i++) {
var demand = this._demandedConns[i];
if (demand.dieOnConnectFailure) {
demand.deathback.call(null);
this._demandedConns.splice(i--, 1);
}
}
},
/**
* Try and find an available connection and assign it to the first connection
* demand.
*
* @return[Boolean]{
* True if we allocated a demand to a conncetion, false if we did not.
* }
*/
_allocateExistingConnection: function() {
if (!this._demandedConns.length)
return false;
var demandInfo = this._demandedConns[0];
var reusableConnInfo = null;
for (var i = 0; i < this._ownedConns.length; i++) {
var connInfo = this._ownedConns[i];
// It's concerning if the folder already has a connection...
if (demandInfo.folderId && connInfo.folderId === demandInfo.folderId)
logic(this, 'folderAlreadyHasConn', { folderId: demandInfo.folderId });
if (connInfo.inUseBy)
continue;
connInfo.inUseBy = demandInfo;
this._demandedConns.shift();
logic(this, 'reuseConnection',
{ folderId: demandInfo.folderId, label: demandInfo.label });
demandInfo.callback(connInfo.conn);
return true;
}
return false;
},
/**
* All our operations completed; let's think about closing any connections
* they may have established that we don't need anymore.
*/
allOperationsCompleted: function() {
this.maybeCloseUnusedConnections();
},
/**
* Using great wisdom, potentially close some/all connections.
*/
maybeCloseUnusedConnections: function() {
// XXX: We are closing unused connections in an effort to stem
// problems associated with unreliable cell connections; they
// tend to be dropped unceremoniously when left idle for a
// long time, particularly on cell networks. NB: This will
// close the connection we just used, unless someone else is
// waiting for a connection.
if ($syncbase.KILL_CONNECTIONS_WHEN_JOBLESS &&
!this._demandedConns.length &&
!this.universe.areServerJobsWaiting(this)) {
this.closeUnusedConnections();
}
},
/**
* Close all connections that aren't currently in use.
*/
closeUnusedConnections: function() {
for (var i = this._ownedConns.length - 1; i >= 0; i--) {
var connInfo = this._ownedConns[i];
if (connInfo.inUseBy)
continue;
console.log('Killing unused IMAP connection.');
// this eats all future notifications, so we need to splice...
this._ownedConns.splice(i, 1);
connInfo.conn.client.close();
logic(this, 'deadConnection', { reason: 'unused' });
}
},
_makeConnectionIfPossible: function() {
if (this._ownedConns.length >= this._maxConnsAllowed) {
logic(this, 'maximumConnsNoNew');
return;
}
if (this._pendingConn) {
return;
}
this._pendingConn = true;
var boundMakeConnection = this._makeConnection.bind(this);
this._backoffEndpoint.scheduleConnectAttempt(boundMakeConnection);
},
_makeConnection: function(callback, whyFolderId, whyLabel) {
// Mark a pending connection synchronously; the require call will not return
// until at least the next turn of the event loop.
this._pendingConn = true;
// Dynamically load the probe/imap code to speed up startup.
require(['./client'], function ($imapclient) {
logic(this, 'createConnection', {
folderId: whyFolderId,
label: whyLabel
});
$imapclient.createImapConnection(
this._credentials,
this._connInfo,
function onCredentialsUpdated() {
return new Promise(function(resolve) {
// Note: Since we update the credentials object in-place,
// there's no need to explicitly assign the changes here;
// just save the account information.
this.universe.saveAccountDef(
this.compositeAccount.accountDef,
/* folderInfo: */ null,
/* callback: */ resolve);
}.bind(this));
}.bind(this)
).then(function(conn) {
DisasterRecovery.associateSocketWithAccount(conn.client.socket, this);
this._pendingConn = null;
this._bindConnectionDeathHandlers(conn);
this._backoffEndpoint.noteConnectSuccess();
this._ownedConns.push({
conn: conn,
inUseBy: null
});
this._allocateExistingConnection();
// If more connections are needed, keep connecting.
if (this._demandedConns.length) {
this._makeConnectionIfPossible();
}
callback && callback(null);
}.bind(this))
.catch(function(err) {
logic(this, 'deadConnection', {
reason: 'connect-error',
folderId: whyFolderId
});
if (errorutils.shouldReportProblem(err)) {
this.universe.__reportAccountProblem(
this.compositeAccount,
err,
'incoming');
}
this._pendingConn = null;
callback && callback(err);
// Track this failure for backoff purposes.
if (errorutils.shouldRetry(err)) {
if (this._backoffEndpoint.noteConnectFailureMaybeRetry(
errorutils.wasErrorFromReachableState(err))) {
this._makeConnectionIfPossible();
} else {
this._killDieOnConnectFailureDemands();
}
} else {
this._backoffEndpoint.noteBrokenConnection();
this._killDieOnConnectFailureDemands();
}
}.bind(this));
}.bind(this));
},
/**
* Treat a connection that came from the IMAP prober as a connection we
* created ourselves.
*/
_reuseConnection: function(existingProtoConn) {
DisasterRecovery.associateSocketWithAccount(
existingProtoConn.client.socket, this);
this._ownedConns.push({
conn: existingProtoConn,
inUseBy: null
});
this._bindConnectionDeathHandlers(existingProtoConn);
},
_bindConnectionDeathHandlers: function(conn) {
conn.breakIdle(function() {
conn.client.TIMEOUT_ENTER_IDLE = $syncbase.STALE_CONNECTION_TIMEOUT_MS;
conn.client.onidle = function() {
console.warn('Killing stale IMAP connection.');
conn.client.close();
};
// Reenter the IDLE state here so that we properly time out if
// we never send any further requests (which would normally
// cause _enterIdle to be called when the request queue has been
// emptied).
conn.client._enterIdle();
});
conn.onclose = function() {
for (var i = 0; i < this._ownedConns.length; i++) {
var connInfo = this._ownedConns[i];
if (connInfo.conn === conn) {
logic(this, 'deadConnection', {
reason: 'closed',
folderId: connInfo.inUseBy &&
connInfo.inUseBy.folderId
});
if (connInfo.inUseBy && connInfo.inUseBy.deathback)
connInfo.inUseBy.deathback(conn);
connInfo.inUseBy = null;
this._ownedConns.splice(i, 1);
return;
}
}
}.bind(this);
conn.onerror = function(err) {
err = $imapclient.normalizeImapError(conn, err);
logic(this, 'connectionError', { error: err });
console.error('imap:onerror', JSON.stringify({
error: err,
host: this._connInfo.hostname,
port: this._connInfo.port
}));
}.bind(this);
},
__folderDoneWithConnection: function(conn, closeFolder, resourceProblem) {
for (var i = 0; i < this._ownedConns.length; i++) {
var connInfo = this._ownedConns[i];
if (connInfo.conn === conn) {
if (resourceProblem)
this._backoffEndpoint(connInfo.inUseBy.folderId);
logic(this, 'releaseConnection', {
folderId: connInfo.inUseBy.folderId,
label: connInfo.inUseBy.label
});
connInfo.inUseBy = null;
// We just freed up a connection, it may be appropriate to close it.
this.maybeCloseUnusedConnections();
return;
}
}
logic(this, 'connectionMismatch');
},
//////////////////////////////////////////////////////////////////////////////
// Folder synchronization
/**
* Helper in conjunction with `_syncFolderComputeDeltas` for use by the
* syncFolderList operation/job. The op is on the hook for the connection's
* lifecycle.
*/
_syncFolderList: function(conn, callback) {
conn.listMailboxes(
this._syncFolderComputeDeltas.bind(this, conn, callback));
},
_determineFolderType: function(box, path) {
var attribs = (box.flags || []).map(function(flag) {
return flag.substr(1).toUpperCase(); // Map "\\Noselect" => "NOSELECT"
});
var type = null;
// NoSelect trumps everything.
if (attribs.indexOf('NOSELECT') !== -1) {
type = 'nomail';
}
else {
// Standards-ish:
// - special-use: http://tools.ietf.org/html/rfc6154
// IANA registrations:
// http://www.iana.org/assignments/imap4-list-extended
// - xlist:
// https://developers.google.com/google-apps/gmail/imap_extensions
// Process the attribs for goodness.
for (var i = 0; i < attribs.length; i++) {
switch (attribs[i]) {
// TODO: split the 'all' cases into their own type!
case 'ALL': // special-use
case 'ALLMAIL': // xlist
case 'ARCHIVE': // special-use
type = 'archive';
break;
case 'DRAFTS': // special-use xlist
type = 'drafts';
break;
case 'FLAGGED': // special-use
type = 'starred';
break;
case 'IMPORTANT': // (undocumented) xlist
type = 'important';
break;
case 'INBOX': // xlist
type = 'inbox';
break;
case 'JUNK': // special-use
type = 'junk';
break;
case 'SENT': // special-use xlist
type = 'sent';
break;
case 'SPAM': // xlist
type = 'junk';
break;
case 'STARRED': // xlist
type = 'starred';
break;
case 'TRASH': // special-use xlist
type = 'trash';
break;
case 'HASCHILDREN': // 3348
case 'HASNOCHILDREN': // 3348
// - standard bits we don't care about
case 'MARKED': // 3501
case 'UNMARKED': // 3501
case 'NOINFERIORS': // 3501
// XXX use noinferiors to prohibit folder creation under it.
// NOSELECT
default:
}
}
// heuristic based type assignment based on the name
if (!type) {
// ensure that we treat folders at the root, see bug 854128
var prefix = this._namespaces.personal[0] &&
this._namespaces.personal[0].prefix;
var isAtNamespaceRoot = path === (prefix + box.name);
// If our name is our path, we are at the absolute root of the tree.
// This will be the case for INBOX even if there is a namespace.
if (isAtNamespaceRoot || path === box.name) {
switch (box.name.toUpperCase()) {
case 'DRAFT':
case 'DRAFTS':
type = 'drafts';
break;
case 'INBOX':
// Inbox is special; the path needs to case-insensitively match.
if (path.toUpperCase() === 'INBOX')
type = 'inbox';
break;
// Yahoo provides "Bulk Mail" for yahoo.fr.
case 'BULK MAIL':
case 'JUNK':
case 'SPAM':
type = 'junk';
break;
case 'SENT':
type = 'sent';
break;
case 'TRASH':
type = 'trash';
break;
// This currently only exists for consistency with Thunderbird, but
// may become useful in the future when we need an outbox.
case 'UNSENT MESSAGES':
type = 'queue';
break;
}
}
}
if (!type)
type = 'normal';
}
return type;
},
/**
* A map of namespaces: { personal: { prefix: '', delimiter: '' }, ... }.
* Populated in _syncFolderComputeDeltas.
*/
_namespaces: {
personal: { prefix: '', delimiter: '/' },
provisional: true
},
_syncFolderComputeDeltas: function(conn, callback, err, boxesRoot) {
var self = this;
if (err) {
callback(err);
return;
}
// Before we walk the boxes, get namespace information.
// In the failure case, assume no relevant namespaces.
if (self._namespaces.provisional) {
conn.listNamespaces(function(err, namespaces) {
if (!err && namespaces) {
self._namespaces = namespaces;
}
self._namespaces.provisional = false;
logic(self, 'list-namespaces', {
namespaces: namespaces
});
self._syncFolderComputeDeltas(conn, callback, err, boxesRoot);
});
return;
}
// - build a map of known existing folders
var folderPubsByPath = {};
var folderPub;
for (var iFolder = 0; iFolder < this.folders.length; iFolder++) {
folderPub = this.folders[iFolder];
folderPubsByPath[folderPub.path] = folderPub;
}
var syncScope = logic.scope('ImapFolderSync');
// - walk the boxes
function walkBoxes(boxLevel, pathDepth, parentId) {
boxLevel.forEach(function(box) {
var boxName = box.name, meta,
folderId;
var delim = box.delimiter || '/';
if (box.path.indexOf(delim) === 0) {
box.path = box.path.slice(delim.length);
}
var path = box.path;
// - normalize jerk-moves
var type = self._determineFolderType(box, path);
// gmail finds it amusing to give us the localized name/path of its
// inbox, but still expects us to ask for it as INBOX.
if (type === 'inbox')
path = 'INBOX';
// - already known folder
if (folderPubsByPath.hasOwnProperty(path)) {
// Because we speculatively create the Inbox, both its display name
// and delimiter may be incorrect and need to be updated.
meta = folderPubsByPath[path];
meta.name = box.name;
meta.delim = delim;
logic(syncScope, 'folder-sync:existing', {
type: type,
name: box.name,
path: path,
delim: delim
});
// mark it with true to show that we've seen it.
folderPubsByPath[path] = true;
}
// - new to us!
else {
logic(syncScope, 'folder-sync:add', {
type: type,
name: box.name,
path: path,
delim: delim
});
meta = self._learnAboutFolder(box.name, path, parentId, type,
delim, pathDepth);
}
if (box.children)
walkBoxes(box.children, pathDepth + 1, meta.id);
});
}
walkBoxes(boxesRoot.children, 0, null);
// - detect deleted folders
// track dead folder id's so we can issue a
var deadFolderIds = [];
for (var folderPath in folderPubsByPath) {
folderPub = folderPubsByPath[folderPath];
// (skip those we found above)
if (folderPub === true)
continue;
// Never delete our localdrafts or outbox folder.
if ($mailslice.FolderStorage.isTypeLocalOnly(folderPub.type))
continue;
logic(syncScope, 'delete-dead-folder', {
folderType: folderPub.type,
folderId: folderPub.id
});
// It must have gotten deleted!
this._forgetFolder(folderPub.id);
}
// Once we've synchonized the folder list, kick off another job to
// check that we have all essential online folders. Once that
// completes, we'll check to make sure our offline-only folders
// (localdrafts, outbox) are in the right place according to where
// this server stores other built-in folders.
this.ensureEssentialOnlineFolders();
this.normalizeFolderHierarchy();
callback(null);
},
/**
* Ensure that local-only folders exist. This runs synchronously
* before we sync the folder list with the server. Ideally, these
* folders should reside in a proper place in the folder hierarchy,
* which may differ between servers depending on whether the
* account's other folders live underneath the inbox or as
* top-level-folders. But since moving folders is easy and doesn't
* really affect the backend, we'll just ensure they exist here, and
* fix up their hierarchical location when syncing the folder list.
*/
ensureEssentialOfflineFolders: function() {
[ 'outbox', 'localdrafts' ].forEach(function(folderType) {
if (!this.getFirstFolderWithType(folderType)) {
this._learnAboutFolder(
/* name: */ folderType,
/* path: */ folderType,
/* parentId: */ null,
/* type: */ folderType,
/* delim: */ '',
/* depth: */ 0,
/* suppressNotification: */ true);
}
}, this);
},
/**
* Kick off jobs to create essential folders (sent, trash) if
* necessary. These folders should be created on both the client and
* the server; contrast with `ensureEssentialOfflineFolders`.
*
* TODO: Support localizing all automatically named e-mail folders
* regardless of the origin locale.
* Relevant bugs: <https://bugzil.la/905869>, <https://bugzil.la/905878>.
*
* @param {function} callback
* Called when all ops have run.
*/
ensureEssentialOnlineFolders: function(callback) {
var essentialFolders = { 'trash': 'Trash', 'sent': 'Sent' };
var latch = $allback.latch();
for (var type in essentialFolders) {
if (!this.getFirstFolderWithType(type)) {
this.universe.createFolder(
this.id, null, essentialFolders[type], type, false, latch.defer());
}
}
latch.then(callback);
},
/**
* Ensure that local-only folders live in a reasonable place in the
* folder hierarchy by moving them if necessary.
*
* We proactively create local-only folders at the root level before
* we synchronize with the server; if possible, we want these
* folders to reside as siblings to other system-level folders on
* the account. This is called at the end of syncFolderList, after
* we have learned about all existing server folders.
*/
normalizeFolderHierarchy: $acctmixins.normalizeFolderHierarchy,
/**
* Asynchronously save the sent message to the sent folder, if applicable.
* This should only be called once the SMTP send has completed.
*
* If non-gmail, append a bcc-including version of the message into the sent
* folder. For gmail, the SMTP server automatically copies the message into
* the sent folder so we don't need to do this.
*
* There are several notable limitations with the current implementation:
* - We do not write a copy of the message into the sent folder locally, so
* the message must be downloaded/synchronized for the user to see it.
* - The operation to append the message does not get persisted to disk, so
* in the event the app crashes or is closed, a copy of the message will
* not end up in the sent folder. This has always been the emergent
* phenomenon for IMAP, except previously we would persist the operation
* and then mark it moot at 'check' time. Our new technique of not saving
* the operation is preferable for disk space reasons. (NB: We could
* persist it, but the composite Blob we build would be flattened which
* could generate an I/O storm, cause temporary double-storage use, etc.)
*/
saveSentMessage: function(composer) {
if (this.sentMessagesAutomaticallyAppearInSentFolder) {
return;
}
composer.withMessageBlob({ includeBcc: true }, function(blob) {
var message = {
messageText: blob,
// do not specify date; let the server use its own timestamping
// since we want the approximate value of 'now' anyways.
flags: ['\\Seen'],
};
var sentFolder = this.getFirstFolderWithType('sent');
if (sentFolder) {
this.universe.appendMessages(sentFolder.id,
[message]);
}
}.bind(this));
},
shutdown: function(callback) {
CompositeIncomingAccount.prototype.shutdownFolders.call(this);
this._backoffEndpoint.shutdown();
// - close all connections
var liveConns = this._ownedConns.length;
function connDead() {
if (--liveConns === 0)
callback();
}
for (var i = 0; i < this._ownedConns.length; i++) {
var connInfo = this._ownedConns[i];
if (callback) {
connInfo.inUseBy = { deathback: connDead };
try {
connInfo.conn.client.close();
}
catch (ex) {
liveConns--;
}
}
else {
connInfo.conn.client.close();
}
}
if (!liveConns && callback)
callback();
},
checkAccount: function(listener) {
logic(this, 'checkAccount_begin');
this._makeConnection(function(err) {
logic(this, 'checkAccount_end', { error: err });
listener(err);
}.bind(this), null, 'check');
},
accountDeleted: function() {
this._alive = false;
this.shutdown();
},
//////////////////////////////////////////////////////////////////////////////
};
// XXX: Use mix.js when it lands in the streaming patch.
for (var k in properties) {
Object.defineProperty(ImapAccount.prototype, k,
Object.getOwnPropertyDescriptor(properties, k));
}
}); // end define

View file

@ -0,0 +1,298 @@
/**
* imap/client.js: Wraps IMAP connection creation, to avoid redundancy
* between connection-related setup in imap/account.js and
* imap/probe.js.
*/
define(function(require, exports) {
var BrowserBox = require('browserbox');
var ImapClient = require('browserbox-imap');
var imapHandler = require('imap-handler');
var logic = require('logic');
var syncbase = require('../syncbase');
var errorutils = require('../errorutils');
var oauth = require('../oauth');
var setTimeout = window.setTimeout;
var clearTimeout = window.clearTimeout;
exports.setTimeoutFunctions = function(setFn, clearFn) {
setTimeout = setFn;
clearTimeout = clearFn;
};
function noop() {
// nothing
}
var scope = logic.scope('ImapClient');
/**
* Open a connection to an IMAP server.
*
* @param {object} credentials
* keys: username, password, [oauth2]
* @param {object} connInfo
* keys: hostname, port, crypto
* @param {function(credentials)} credsUpdatedCallback
* Callback, called if the credentials have been updated and
* should be stored to disk. Not called if the credentials are
* already up-to-date.
* @return {Promise}
* resolve => {BrowserBox} conn
* reject => {String} normalized String error
*/
exports.createImapConnection = function(credentials, connInfo,
credsUpdatedCallback) {
var conn;
return oauth.ensureUpdatedCredentials(credentials, credsUpdatedCallback)
.then(function() {
return new Promise(function(resolve, reject) {
conn = new BrowserBox(
connInfo.hostname,
connInfo.port, {
auth: {
user: credentials.username,
pass: credentials.password,
xoauth2: credentials.oauth2 ?
credentials.oauth2.accessToken : null
},
id: {
vendor: 'Mozilla',
name: 'GaiaMail',
version: '0.2',
'support-url': 'http://mzl.la/file-gaia-email-bug'
},
useSecureTransport: (connInfo.crypto === 'ssl' ||
connInfo.crypto === true),
requireTLS: connInfo.crypto === 'starttls',
// In the case no encryption is explicitly requested (either for
// testing or because a user regrettably chose to disable it via
// manual config), we want to avoid opportunistic encryption
// since in the latter case the user may have done this because
// the server's certificates are invalid.
ignoreTLS: connInfo.crypto === 'plain'
});
var connectTimeout = setTimeout(function() {
conn.onerror('unresponsive-server');
conn.close();
}, syncbase.CONNECT_TIMEOUT_MS);
conn.onauth = function() {
clearTimeout(connectTimeout);
logic(scope, 'connected', { connInfo: connInfo });
conn.onauth = conn.onerror = noop;
resolve(conn);
};
conn.onerror = function(err) {
clearTimeout(connectTimeout);
// XXX: if error is just expired access token, try to refresh one time
reject(err);
};
conn.connect();
});
}).catch(function(errorObject) {
var errorString = normalizeImapError(conn, errorObject);
if (conn) {
conn.close();
}
// Could hit an oauth reauth case due to date skews, so give a token
// review a shot before really bailing.
if (errorString === 'needs-oauth-reauth' &&
oauth.isRenewPossible(credentials)) {
return oauth.ensureUpdatedCredentials(credentials,
credsUpdatedCallback, true)
.then(function() {
return exports.createImapConnection(credentials, connInfo,
credsUpdatedCallback);
});
} else {
logic(scope, 'connect-error', {
error: errorString
});
throw errorString;
}
});
};
//****************************************************************
// UNFORTUNATE IMAP WORKAROUNDS & SHIMS BEGIN HERE
//----------------------------------------------------------------
// ImapClient (from BrowserBox) doesn't pass along any useful error
// information other than a human-readable string, and even then,
// not always reliably. When we receive an IMAP protocol error
// response form the server, all of the data we care about is in the
// most recent 'NO' or 'BAD' response line. Until we can add
// improved error handling upstream, cache the most recent error
// response from the IMAP server so that we can extract detailed
// error codes when we handle error events from the IMAP library.
var processResponse = ImapClient.prototype._processResponse;
ImapClient.prototype._processResponse = function(response) {
processResponse.apply(this, arguments);
var cmd = (response && response.command || '').toString()
.toUpperCase().trim();
if (['NO', 'BAD'].indexOf(cmd) !== -1) {
logic(scope, 'protocol-error', {
humanReadable: response.humanReadable,
responseCode: response.code,
// Include the command structure
commandData: this._currentCommand && this._currentCommand.request &&
imapHandler.compiler(this._currentCommand.request)
});
this._lastImapError = {
// To most accurately report STARTTLS issues, latch the active command
// at the time of the failure rather than just the response. (An evil
// attacker could say "NO SUCKER" instead of "NO STARTTLS" or
// something.)
command: this._currentCommand,
response: response
};
}
}
// ImapClient passes data directly into the `new Error()`
// constructor, which causes err.message to equal "[Object object]"
// rather than the actual error object with details. This is just a
// copy of that function, with the `new Error` constructor stripped
// out so that the error details pass through to onerror.
ImapClient.prototype._onError = function(evt) {
if (this.isError(evt)) {
this.onerror(evt);
} else if (evt && this.isError(evt.data)) {
this.onerror(evt.data);
} else {
this.onerror(evt && evt.data && evt.data.message ||
evt.data || evt || 'Error');
}
this.close();
};
/**
* Given an error possibly generated by the IMAP client, analyze it
* and convert it to a normalized string if possible. Otherwise,
* return null.
*/
function analyzeLastImapError(lastErrInfo, conn) {
// Make sure it's an error we know how to analyze:
if (!lastErrInfo || !lastErrInfo.response) {
return null;
}
// If the most recent command was to initiate STARTTLS, then this is a
// security error.
if (lastErrInfo.command && lastErrInfo.command.request &&
lastErrInfo.command.request.command === 'STARTTLS') {
return 'bad-security';
}
var wasOauth = conn && !!conn.options.auth.xoauth2;
// Structure of an IMAP error response:
// { "tag":"W2",
// "command": "NO",
// "code": "AUTHENTICATIONFAILED",
// "attributes": [
// {"type":"TEXT","value":"invalid password"}
// ],
// "humanReadable": "invalid password" }
// Dovecot says after a delay and does not terminate the connection:
// NO [AUTHENTICATIONFAILED] Authentication failed.
// Zimbra 7.2.x says after a delay and DOES terminate the connection:
// NO LOGIN failed
// * BYE Zimbra IMAP server terminating connection
// Yahoo says after a delay and does not terminate the connection:
// NO [AUTHENTICATIONFAILED] Incorrect username or password.
var err = lastErrInfo.response;
var str = (err.code || '') + (err.humanReadable || '');
if (/Your account is not enabled for IMAP use/.test(str) ||
/IMAP access is disabled for your domain/.test(str)) {
return 'imap-disabled';
} else if (/UNAVAILABLE/.test(str)) {
return 'server-maintenance';
// If login failed and LOGINDISABLED was claimed, then it's
// server-maintenance. We used to be more aggressive about
// LOGINDISABLED and would just give up if we saw it, but we had a
// bad regression, so the balance has tipped here. Additionally, it
// makes a lot of sense to only report the error if we actually failed
// to login!
} else if (conn.capability.indexOf('LOGINDISABLED') != -1 &&
!conn.authenticated) {
return 'server-maintenance';
// The invalid-credentials case goes last, because we
// optimistically assume that any other not-authenticated failure
// is caused by the user's invalid credentials.
} else if (/AUTHENTICATIONFAILED/.test(str) ||
/Invalid credentials/i.test(str) || // Gmail bad access token
/login failed/i.test(str) ||
/password/.test(str) ||
// We can't trust state since it gets updated on close, but
// authenticated latches to true and stays that way.
!conn.authenticated) {
// If we got a protocol-level error but we weren't authenticated
// yet, it's likely an authentication problem, as authenticating
// is the first thing we do. Any other socket-level connection
// problems (including STARTTLS, since we pass that along as an
// exception) will be surfaced before hitting this conditional.
if (wasOauth) {
// Gmail returns "NO [ALERT] Invalid credentials (Failure)" in
// the case of a failed OAUTH password.
return 'needs-oauth-reauth';
} else {
return 'bad-user-or-pass';
}
} else {
return null;
}
}
/**
* Here's where the cascades come in: This function accepts an error
* of potentially unknown origin. Maybe it's an exception; maybe
* it's an IMAP library error with details; maybe it has no useful
* details, in which case we can see if the IMAP connection reported
* any error responses. No matter what, we take an IMAP-related
* error, analyze it, and convert it to our normalized string error
* representation.
*
* @param {BrowserBox} conn
* The IMAP connection; necessary if you expect us to analyze
* protocol errors
* @param {object} err
* The exception or error that started all the trouble.
*/
var normalizeImapError = exports.normalizeImapError = function(conn, err) {
var socketLevelError = errorutils.analyzeException(err);
var protocolLevelError =
conn && analyzeLastImapError(conn.client._lastImapError, conn);
var reportAs = (socketLevelError ||
protocolLevelError ||
'unknown');
logic(scope, 'normalized-error', {
error: err,
errorName: err && err.name,
errorMessage: err && err.message,
errorStack: err && err.stack,
socketLevelError: socketLevelError,
protocolLevelError: protocolLevelError,
reportAs: reportAs
});
return reportAs;
};
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,610 @@
/**
*
**/
define(
[
'mimefuncs',
'../db/mail_rep',
'../mailchew',
'mimeparser',
'exports'
],
function(
mimefuncs,
mailRep,
$mailchew,
MimeParser,
exports
) {
function parseRfc2231CharsetEncoding(s) {
// charset'lang'url-encoded-ish
var match = /^([^']*)'([^']*)'(.+)$/.exec(s);
if (match) {
// we can convert the dumb encoding into quoted printable.
return mimefuncs.mimeWordsDecode(
'=?' + (match[1] || 'us-ascii') + '?Q?' +
match[3].replace(/%/g, '=') + '?=');
}
return null;
}
// Given an array or string, strip any surrounding angle brackets.
function stripArrows(s) {
if (Array.isArray(s)) {
return s.map(stripArrows);
} else if (s && s[0] === '<') {
return s.slice(1, -1);
} else {
return s;
}
}
function firstHeader(msg, headerName) {
return msg.headers[headerName] && msg.headers[headerName][0] || null;
}
/**
* Process the headers and bodystructure of a message to build preliminary state
* and determine what body parts to fetch. The list of body parts will be used
* to issue another fetch request, and those results will be passed to
* `chewBodyParts`.
*
* For now, our stop-gap heuristics for content bodies are:
* - pick text/plain in multipart/alternative
* - recurse into other multipart types looking for an alterntive that has
* text.
* - do not recurse into message/rfc822
* - ignore/fail-out messages that lack a text part, skipping to the next
* task. (This should not happen once we support HTML, as there are cases
* where there are attachments without any body part.)
* - Append text body parts together; there is no benefit in separating a
* mailing list footer from its content.
*
* For attachments, our heuristics are:
* - only like them if they have filenames. We will find this as "name" on
* the "content-type" or "filename" on the "content-disposition", quite
* possibly on both even.
* - ignore crypto signatures, even though they are named. S/MIME gives us
* "smime.p7s" as an application/pkcs7-signature under a multipart/signed
* (that the server tells us is "signed"). PGP in MIME mode gives us
* application/pgp-signature "signature.asc" under a multipart/signed.
*
* The next step in the plan is to get an HTML sanitizer exposed so we can
* support text/html. That will also imply grabbing multipart/related
* attachments.
*
* @typedef[ChewRep @dict[
* @key[bodyReps @listof[ImapJsPart]]
* @key[attachments @listof[AttachmentInfo]]
* @key[relatedParts @listof[RelatedPartInfo]]
* ]]
* @return[ChewRep]
*/
function chewStructure(msg) {
var attachments = [], bodyReps = [], unnamedPartCounter = 0,
relatedParts = [];
/**
* Sizes are the size of the encoded string, not the decoded value.
*/
function estimatePartSizeInBytes(partInfo) {
var encoding = partInfo.encoding.toLowerCase();
// Base64 encodes 3 bytes in 4 characters with padding that always
// causes the encoding to take 4 characters. The max encoded line length
// (ignoring CRLF) is 76 bytes, with 72 bytes also fairly common.
// As such, a 78=19*4+2 character line encodes 57=19*3 payload bytes and
// we can use that as a rough estimate.
if (encoding === 'base64') {
return Math.floor(partInfo.size * 57 / 78);
}
// Quoted printable is hard to predict since only certain things need
// to be encoded. It could be perfectly efficient if the source text
// has a bunch of newlines built-in.
else if (encoding === 'quoted-printable') {
// Let's just provide an upper-bound of perfectly efficient.
return partInfo.size;
}
// No clue; upper bound.
return partInfo.size;
}
function chewNode(partInfo, parentMultipartSubtype) {
var i, filename, disposition;
var type = partInfo.type.split('/')[0];
var subtype = partInfo.type.split('/')[1];
if (type === 'multipart') {
switch (subtype) {
// For alternative, scan from the back to find the first part we like.
// XXX I believe in Thunderbird we observed some ridiculous misuse of
// alternative that we'll probably want to handle.
case 'alternative':
for (i = partInfo.childNodes.length - 1; i >= 0; i--) {
var subPartInfo = partInfo.childNodes[i];
var childType = subPartInfo.type.split('/')[0];
var childSubtype = subPartInfo.type.split('/')[1];
switch(childType) {
case 'text':
// fall out for subtype checking
break;
case 'multipart':
// this is probably HTML with attachments, let's give it a try
if (chewNode(subPartInfo)) {
return true;
}
break;
default:
// no good, keep going
continue;
}
switch (childSubtype) {
case 'html':
case 'plain':
// (returns true if successfully handled)
if (chewNode(subPartInfo), subtype) {
return true;
}
}
}
// (If we are here, we failed to find a valid choice.)
return false;
// multipart that we should recurse into
case 'mixed':
case 'signed':
case 'related':
for (i = 0; i < partInfo.childNodes.length; i++) {
chewNode(partInfo.childNodes[i], subtype);
}
return true;
default:
console.warn('Ignoring multipart type:', subtype);
return false;
}
}
// Otherwise, this is a leaf node:
else {
// Detect named parts; they could be attachments.
// filename via content-type 'name' parameter
if (partInfo.parameters && partInfo.parameters.name) {
filename = mimefuncs.mimeWordsDecode(partInfo.parameters.name);
}
// filename via content-type 'name' with charset/lang info
else if (partInfo.parameters && partInfo.parameters['name*']) {
filename = parseRfc2231CharsetEncoding(partInfo.parameters['name*']);
}
// rfc 2231 stuff:
// filename via content-disposition filename without charset/lang info
else if (partInfo.dispositionParameters &&
partInfo.dispositionParameters.filename) {
filename = mimefuncs.mimeWordsDecode(
partInfo.dispositionParameters.filename);
}
// filename via content-disposition filename with charset/lang info
else if (partInfo.dispositionParameters &&
partInfo.dispositionParameters['filename*']) {
filename = parseRfc2231CharsetEncoding(
partInfo.dispositionParameters['filename*']);
}
else {
filename = null;
}
// Determining disposition:
// First, check whether an explict one exists
if (partInfo.disposition) {
// If it exists, keep it the same, except in the case of inline
// disposition without a content id.
if (partInfo.disposition.toLowerCase() == 'inline') {
// Displaying text-parts inline is not a problem for us, but we need a
// content id for other embedded content. (Currently only images are
// supported, but that is enforced in a subsequent check.)
if (type === 'text' || partInfo.id) {
disposition = 'inline';
} else {
disposition = 'attachment';
}
}
else if (partInfo.disposition.toLowerCase() == 'attachment') {
disposition = 'attachment';
}
// This case should never trigger, but it's here for safety's sake
else {
disposition = 'inline';
}
// Inline image attachments that belong to a multipart/related
// may lack a disposition but have a content-id.
// XXX Ensure 100% correctness in the future by fixing up
// mis-guesses during sanitization as part of
// https://bugzil.la/1024685
} else if (parentMultipartSubtype === 'related' && partInfo.id &&
type === 'image') {
disposition = "inline";
} else if (filename || type !== 'text') {
disposition = 'attachment';
} else {
disposition = 'inline';
}
// Some clients want us to display things inline that we simply can't
// display (historically and currently, PDF) or that our usage profile
// does not want to automatically download (in the future, PDF, because
// they can get big.)
if (type !== 'text' && type !== 'image') {
disposition = 'attachment';
}
// - But we don't care if they are signatures...
if ((type === 'application') &&
(subtype === 'pgp-signature' || subtype === 'pkcs7-signature')) {
return true;
}
var makePart = function(partInfo, filename) {
return mailRep.makeAttachmentPart({
name: filename || 'unnamed-' + (++unnamedPartCounter),
contentId: partInfo.id ? stripArrows(partInfo.id) : null,
type: partInfo.type.toLowerCase(),
part: partInfo.part,
encoding: partInfo.encoding && partInfo.encoding.toLowerCase(),
sizeEstimate: estimatePartSizeInBytes(partInfo),
file: null
});
}
var makeTextPart = function(partInfo) {
return mailRep.makeBodyPart({
type: subtype,
part: partInfo.part || '1',
sizeEstimate: partInfo.size,
amountDownloaded: 0,
// its important to know that sizeEstimate and amountDownloaded
// do _not_ determine if the bodyRep is fully downloaded; the
// estimated amount is not reliable
// 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.size ? {
partID: partInfo.part,
type: type,
subtype: subtype,
params: valuesOnly(partInfo.parameters),
encoding: partInfo.encoding && partInfo.encoding.toLowerCase()
} : null,
content: ''
});
}
if (disposition === 'attachment') {
attachments.push(makePart(partInfo, filename));
return true;
}
// - We must be an inline part or structure
switch (type) {
// - related image
case 'image':
relatedParts.push(makePart(partInfo, filename));
return true;
break;
// - content
case 'text':
if (subtype === 'plain' || subtype === 'html') {
bodyReps.push(makeTextPart(partInfo));
return true;
}
break;
}
return false;
}
}
chewNode(msg.bodystructure);
return {
bodyReps: bodyReps,
attachments: attachments,
relatedParts: relatedParts
};
};
/**
* Transform a browserbox representation of an item that has a value
* (i.e. { value: foo }) into a pure value, recursively.
*
* [{ value: 1 } ] -> [1]
* { value: 1 } -> 1
* undefined -> null
*/
function valuesOnly(item) {
if (Array.isArray(item)) {
return item.map(valuesOnly);
} else if (item && typeof item === 'object') {
if ('value' in item) {
return item.value;
} else {
var result = {};
for (var key in item) {
result[key] = valuesOnly(item[key]);
}
return result;
}
} else if (item && typeof item === 'object') {
return item;
} else if (item !== undefined) {
return item;
} else {
return null;
}
}
exports.chewHeaderAndBodyStructure = function(msg, folderId, newMsgId) {
// begin by splitting up the raw imap message
var parts = chewStructure(msg);
msg.date = msg.internaldate && parseImapDateTime(msg.internaldate);
msg.headers = {};
for (var key in msg) {
// We test the key using a regex here because the key name isn't
// normalized to a form we can rely on. The browserbox docs in
// particular indicate that the full key name may be dependent on
// the ordering of the fields as returned by the mail server (i.e.
// the key name includes every header requested). One thing we can
// rely on instead: grabbing the right key based upon just this
// regex.
if (/header\.fields/.test(key)) {
var headerParser = new MimeParser();
headerParser.write(msg[key] + '\r\n');
headerParser.end();
msg.headers = headerParser.node.headers;
break;
}
}
var fromArray = valuesOnly(firstHeader(msg, 'from'));
var references = valuesOnly(firstHeader(msg, 'references'));
return {
header: mailRep.makeHeaderInfo({
// the FolderStorage issued id for this message (which differs from the
// IMAP-server-issued UID so we can do speculative offline operations like
// moves).
id: newMsgId,
srvid: msg.uid,
// The sufficiently unique id is a concatenation of the UID onto the
// folder id.
suid: folderId + '/' + newMsgId,
// The message-id header value; as GUID as get for now; on gmail we can
// use their unique value, or if we could convince dovecot to tell us.
guid: stripArrows(valuesOnly(firstHeader(msg, 'message-id'))),
// mimeparser models from as an array; we do not.
author: fromArray && fromArray[0] ||
// we require a sender e-mail; let's choose an illegal default as
// a stopgap so we don't die.
{ address: 'missing-address@example.com' },
to: valuesOnly(firstHeader(msg, 'to')),
cc: valuesOnly(firstHeader(msg, 'cc')),
bcc: valuesOnly(firstHeader(msg, 'bcc')),
replyTo: valuesOnly(firstHeader(msg, 'reply-to')),
date: msg.date,
flags: msg.flags || [],
hasAttachments: parts.attachments.length > 0,
subject: valuesOnly(firstHeader(msg, 'subject')),
// we lazily fetch the snippet later on
snippet: null
}),
bodyInfo: mailRep.makeBodyInfo({
date: msg.date,
size: 0,
attachments: parts.attachments,
relatedParts: parts.relatedParts,
references: references ? stripArrows(references.split(/\s+/)) : null,
bodyReps: parts.bodyReps
})
};
};
/**
* Fill a given body rep with the content from fetching
* part or the entire body of the message...
*
* var body = ...;
* var header = ...;
* var content = (some fetched content)..
*
* $imapchew.updateMessageWithFetch(
* header,
* bodyInfo,
* {
* bodyRepIndex: 0,
* text: '',
* buffer: Uint8Array|Null,
* bytesFetched: n,
* bytesRequested: n
* }
* );
*
* // what just happend?
* // 1. the body.bodyReps[n].content is now the value of content.
* //
* // 2. we update .amountDownloaded with the second argument
* // (number of bytes downloaded).
* //
* // 3. if snippet has not bee set on the header we create the snippet
* // and set its value.
*
*/
exports.updateMessageWithFetch = function(header, body, req, res) {
var bodyRep = body.bodyReps[req.bodyRepIndex];
// check if the request was unbounded or we got back less bytes then we
// requested in which case the download of this bodyRep is complete.
if (!req.bytes || res.bytesFetched < req.bytes[1]) {
bodyRep.isDownloaded = true;
// clear private space for maintaining parser state.
bodyRep._partInfo = null;
}
if (!bodyRep.isDownloaded && res.buffer) {
bodyRep._partInfo.pendingBuffer = res.buffer;
}
bodyRep.amountDownloaded += res.bytesFetched;
var data = $mailchew.processMessageContent(
res.text, bodyRep.type, bodyRep.isDownloaded, req.createSnippet);
if (req.createSnippet) {
header.snippet = data.snippet;
}
if (bodyRep.isDownloaded)
bodyRep.content = data.content;
};
/**
* Selects a desirable snippet body rep if the given header has no snippet.
*/
exports.selectSnippetBodyRep = function(header, body) {
if (header.snippet)
return -1;
var bodyReps = body.bodyReps;
var len = bodyReps.length;
for (var i = 0; i < len; i++) {
if (exports.canBodyRepFillSnippet(bodyReps[i])) {
return i;
}
}
return -1;
};
/**
* Determines if a given body rep can be converted into a snippet. Useful for
* determining which body rep to use when downloading partial bodies.
*
*
* var bodyInfo;
* $imapchew.canBodyRepFillSnippet(bodyInfo.bodyReps[0]) // true/false
*
*/
exports.canBodyRepFillSnippet = function(bodyRep) {
return (
bodyRep &&
bodyRep.type === 'plain' ||
bodyRep.type === 'html'
);
};
/**
* Calculates and returns the correct estimate for the number of
* bytes to download before we can display the body. For IMAP, that
* includes the bodyReps and related parts. (POP3 is different.)
*/
exports.calculateBytesToDownloadForImapBodyDisplay = function(body) {
var bytesLeft = 0;
body.bodyReps.forEach(function(rep) {
if (!rep.isDownloaded) {
bytesLeft += rep.sizeEstimate - rep.amountDownloaded;
}
});
body.relatedParts.forEach(function(part) {
if (!part.file) {
bytesLeft += part.sizeEstimate;
}
});
return bytesLeft;
}
// parseImapDateTime and formatImapDateTime functions from node-imap;
// MIT licensed, (c) Brian White.
// ( ?\d|\d{2}) = day number; technically it's either "SP DIGIT" or "2DIGIT"
// but there's no harm in us accepting a single digit without whitespace;
// it's conceivable the caller might have trimmed whitespace.
//
// The timezone can, as unfortunately demonstrated by net-c.com/netc.fr, be
// omitted. So we allow it to be optional and assume its value was zero if
// omitted.
var reDateTime =
/^( ?\d|\d{2})-(.{3})-(\d{4}) (\d{2}):(\d{2}):(\d{2})(?: ([+-]\d{4}))?$/;
var HOUR_MILLIS = 60 * 60 * 1000;
var MINUTE_MILLIS = 60 * 1000;
var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec'];
/**
* Parses IMAP "date-time" instances into UTC timestamps whose quotes have
* already been stripped.
*
* http://tools.ietf.org/html/rfc3501#page-84
*
* date-day = 1*2DIGIT
* ; Day of month
* date-day-fixed = (SP DIGIT) / 2DIGIT
* ; Fixed-format version of date-day
* date-month = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" /
* "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
* date-year = 4DIGIT
* time = 2DIGIT ":" 2DIGIT ":" 2DIGIT
* ; Hours minutes seconds
* zone = ("+" / "-") 4DIGIT
* date-time = DQUOTE date-day-fixed "-" date-month "-" date-year
* SP time SP zone DQUOTE
*/
var parseImapDateTime = exports.parseImapDateTime = function(dstr) {
var match = reDateTime.exec(dstr);
if (!match)
throw new Error('Not a good IMAP date-time: ' + dstr);
var day = parseInt(match[1], 10),
zeroMonth = MONTHS.indexOf(match[2]),
year = parseInt(match[3], 10),
hours = parseInt(match[4], 10),
minutes = parseInt(match[5], 10),
seconds = parseInt(match[6], 10),
// figure the timestamp before the zone stuff. We don't
timestamp = Date.UTC(year, zeroMonth, day, hours, minutes, seconds),
// to reduce string garbage creation, we use one string. (we have to
// play math games no matter what, anyways.)
zoneDelta = match[7] ? parseInt(match[7], 10) : 0,
zoneHourDelta = Math.floor(zoneDelta / 100),
// (the negative sign sticks around through the mod operation)
zoneMinuteDelta = zoneDelta % 100;
// ex: GMT-0700 means 7 hours behind, so we need to add 7 hours, aka
// subtract negative 7 hours.
timestamp -= zoneHourDelta * HOUR_MILLIS + zoneMinuteDelta * MINUTE_MILLIS;
return timestamp;
};
exports.formatImapDateTime = function(date) {
var s;
s = ((date.getDate() < 10) ? ' ' : '') + date.getDate() + '-' +
MONTHS[date.getMonth()] + '-' +
date.getFullYear() + ' ' +
('0'+date.getHours()).slice(-2) + ':' +
('0'+date.getMinutes()).slice(-2) + ':' +
('0'+date.getSeconds()).slice(-2) +
((date.getTimezoneOffset() > 0) ? ' -' : ' +' ) +
('0'+(Math.abs(date.getTimezoneOffset()) / 60)).slice(-2) +
('0'+(Math.abs(date.getTimezoneOffset()) % 60)).slice(-2);
return s;
};
}); // end define

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,60 @@
/**
* Validates connection information for an account and verifies that
* the server on the other end is something we are capable of
* sustaining an account with.
*/
define([
'browserbox',
'logic',
'./client',
'../syncbase',
'exports'
], function(BrowserBox, logic, imapclient, syncbase, exports) {
/**
* Log in to test credentials, passing the established connection
* onward if successful for later reuse.
*
* @param {object} credentials
* keys: hostname, port, crypto
* @param {object} connInfo
* keys: username, password, xoauth2 (if OAUTH)
* @return {Promise}
* resolve => { conn: conn }
* reject => String (normalized)
*/
exports.probeAccount = function(credentials, connInfo) {
var scope = logic.scope('ImapProber');
logic(scope, 'connecting', { connInfo: connInfo });
var conn;
return imapclient.createImapConnection(
credentials,
connInfo,
function onCredentialsUpdated() {
// Normally we shouldn't see a request to update credentials
// here, as the caller should have already passed a valid
// accessToken during account setup. This might indicate a
// problem with our OAUTH handling, so log it just in case.
logic(scope, 'credentials-updated');
}
).then(function(newConn) {
conn = newConn;
logic(scope, 'success');
return { conn: conn };
})
.catch(function(err) {
// Normalize the error before passing it on.
err = imapclient.normalizeImapError(conn, err);
logic(scope, 'error', { error: err });
if (conn) {
conn.close();
}
throw err;
});
}
}); // end define

View file

@ -0,0 +1,86 @@
define(function() {
/**
* Convenience class and wrapper around building multiple fetching operations or
* the aggregation of many fetching operations into a single unit of
* operation...
*
*
* var fetcher = new $bodyfetcher.BodyFetcher(
* connection,
* BodyParser (or any other kind of parser),
* [
* { uid: X, partInfo: {}, bytes: [A, B] }
* ]
* );
*
* // in all examples item is a single element in the
* // array (third argument).
*
* fetcher.onparsed = function(parsed, item) {}
* fetcher.onend = function(err) {}
*
*/
function BodyFetcher(connection, parserClass, list) {
this.connection = connection;
this.parserClass = parserClass;
this.list = list;
this.pending = list.length;
this.onparsed = null;
this.onend = null;
list.forEach(this._fetch, this);
}
BodyFetcher.prototype = {
_fetch: function(request) {
this.connection.listMessages(
request.uid,
[
'BODY.PEEK[' + (request.partInfo.partID || '1') + ']' +
(request.bytes ?
'<' + request.bytes[0] + '.' + request.bytes[1] + '>' :
'')
],
{ byUid: true },
function (err, messages) {
if (err) {
this._resolve(err, request, null);
} else {
var parser = new this.parserClass(request.partInfo);
var msg = messages[0];
var body = null;
for (var key in msg) {
if (/^body/.test(key)) {
body = msg[key];
break;
}
}
if (!body) {
this.resolve('no body', request);
} else {
parser.parse(body);
this._resolve(null, request, parser.complete());
}
}
}.bind(this));
},
_resolve: function(err, req, result) {
if (this.onparsed) {
this.onparsed(err, req, result);
}
if (!--this.pending && this.onend) {
this.onend();
}
}
};
return {
BodyFetcher: BodyFetcher
};
});

View file

@ -0,0 +1,45 @@
define(['./textparser'], function($textparser) {
var TextParser = $textparser.TextParser;
function bufferAppend(buffer1, buffer2) {
var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
};
/**
* Wrapper around the textparser, accumulates buffer content and returns it as
* part of the .complete step.
*/
function SnippetParser(partDef) {
TextParser.apply(this, arguments);
}
SnippetParser.prototype = {
parse: function(buffer) {
if (!this._buffer) {
this._buffer = buffer;
} else {
this._buffer = bufferAppend(this._buffer, buffer);
}
// do some magic parsing
TextParser.prototype.parse.apply(this, arguments);
},
complete: function() {
var content =
TextParser.prototype.complete.apply(this, arguments);
content.buffer = this._buffer;
return content;
}
};
return {
SnippetParser: SnippetParser
};
});

View file

@ -0,0 +1,280 @@
define(
[
'../imapchew',
'../../allback',
'exports'
],
function(
$imapchew,
allback,
exports
) {
/**
* Fetch parameters to get the headers / bodystructure; exists to reuse the
* object since every fetch is the same.
*
* We are intentionally not using ENVELOPE because Thunderbird intentionally
* defaults to to not using ENVELOPE. Per bienvenu in
* https://bugzilla.mozilla.org/show_bug.cgi?id=402594#c33 "We stopped using it
* by default because servers often had issues with it so it was more trouble
* than it was worth."
*/
var INITIAL_FETCH_PARAMS = [
'BODYSTRUCTURE',
'INTERNALDATE',
'FLAGS',
'BODY.PEEK[HEADER.FIELDS (FROM TO CC BCC SUBJECT REPLY-TO MESSAGE-ID REFERENCES)]',
];
/**
* Fetch parameters to just get the flags.
*/
var FLAG_FETCH_PARAMS = ['FLAGS'];
// Number of bytes to fetch for snippet.
var SNIPPET_BYTES = 256;
// See the `ImapFolderConn` block comment for rationale.
var KNOWN_HEADERS_AGGR_COST = 20,
KNOWN_HEADERS_PER_COST = 1,
NEW_HEADERS_AGGR_COST = 20,
NEW_HEADERS_PER_COST = 5,
NEW_BODIES_PER_COST = 30;
/**
* Given a list of new-to-us UIDs and known-to-us UIDs and their corresponding
* headers, synchronize the flags for the known UIDs' headers and fetch and
* create the header and body objects for the new UIDS.
*
* First we fetch the headers/bodystructures for the new UIDs all in one go;
* all of these headers are going to end up in-memory at the same time, so
* batching won't let us reduce the overhead right now. We process them
* to determine the body parts we should fetch as the results come in. Once
* we have them all, we sort them by date, endTS-to-startTS for the third
* step and start issuing/pipelining the requests.
*
* Second, we issue the flag update requests for the known-to-us UIDs. This
* is done second so it can help avoid wasting the latency of the round-trip
* that would otherwise result between steps one and three. (Although we
* could also mitigate that by issuing some step three requests even as
* the step one requests are coming in; our sorting doesn't have to be
* perfect and may already be reasonably well ordered if UIDs correlate
* with internal date well.)
*
* Third, we fetch the body parts in our newest-to-startTS order, adding
* finalized headers and bodies as we go.
*
* == Usage
*
* var sync = new ImapUidSync({
* initialProgress: 0.25,
* connection: (ImapConnection),
*
* storage: (FolderStorage),
*
* knownHeaders: [],
*
* knownUIDs: [],
* newUIDs: []
* });
*
*/
function Sync(options) {
// storage and connections
this.storage = options.storage;
this.connection = options.connection;
this.knownHeaders = options.knownHeaders || [];
this.knownUIDs = options.knownUIDs || [];
this.newUIDs = options.newUIDs || [];
this._progress = options.initialProgress || 0.25;
this._progressCost =
(this.knownUIDs.length ? KNOWN_HEADERS_AGGR_COST : 0) +
KNOWN_HEADERS_PER_COST * this.knownUIDs.length +
(this.newUIDs.length ? NEW_HEADERS_AGGR_COST : 0) +
NEW_HEADERS_PER_COST * this.newUIDs.length;
// events
this.onprogress = null;
this.oncomplete = null;
this._beginSync();
}
Sync.prototype = {
_updateProgress: function(newProgress) {
this._progress += newProgress;
if (this.onprogress) {
this.onprogress(
0.25 + 0.75 * (this._progress / this._progressCost)
);
}
},
_beginSync: function() {
var latch = allback.latch();
if (this.newUIDs.length) {
this._handleNewUids(latch.defer('new'));
}
if (this.knownUIDs.length) {
this._handleKnownUids(latch.defer('known'));
}
latch.then(function() {
if (!this.oncomplete) {
return;
}
this.oncomplete(this.newUIDs.length, this.knownUIDs.length);
}.bind(this));
},
_handleNewUids: function(callback) {
var pendingSnippets = [];
var self = this;
this.connection.listMessages(
this.newUIDs,
INITIAL_FETCH_PARAMS,
{ byUid: true },
function (err, messages) {
if (err) {
// XXX the UID might have disappeared already? we might need to have
// our initiating command re-do whatever it's up to. Alternatively,
// we could drop back from a bulk fetch to a one-by-one fetch.
console.warn('New UIDs fetch error, ideally harmless:', err);
callback();
return;
}
var latch = allback.latch();
messages.forEach(function(msg) {
// Filter out the \Recent flag; our old imap library didn't
// pass it along (complaining about it being useless), so we
// don't either.
var recentIdx = msg.flags.indexOf('\\Recent');
if (recentIdx !== -1) {
msg.flags.splice(recentIdx, 1);
}
try {
// at this point we can build the first batch of header/body
// information.
var chewRep = $imapchew.chewHeaderAndBodyStructure(
msg,
self.storage.folderId,
self.storage._issueNewHeaderId()
);
chewRep.header.bytesToDownloadForBodyDisplay =
$imapchew.calculateBytesToDownloadForImapBodyDisplay(
chewRep.bodyInfo);
pendingSnippets.push(chewRep);
// flush our body/header information ? should we do some sorting,
// etc.. here or just let the UI update ASAP?
self.storage.addMessageHeader(
chewRep.header, chewRep.bodyInfo, latch.defer());
self.storage.addMessageBody(
chewRep.header, chewRep.bodyInfo, latch.defer());
}
catch (ex) {
// it's fine for us to not add bad messages to the database
// XXX do plumb the logger through here eventually
console.warn('message problem, skipping message', ex, '\n',
ex.stack);
}
}.bind(this));
latch.then(callback); // our caller doesn't care about its args
}.bind(this));
},
_handleKnownUids: function(callback) {
var self = this;
this.connection.listMessages(
self.knownUIDs,
FLAG_FETCH_PARAMS,
{ byUid: true },
function(err, messages) {
if (err) {
console.warn('Known UIDs fetch error, ideally harmless:', err);
callback();
return;
}
var latch = allback.latch();
messages.forEach(function(msg, i) {
console.log('FETCHED', i, 'known id', self.knownHeaders[i].id,
'known srvid', self.knownHeaders[i].srvid,
'actual id', msg.uid);
// Filter out the \Recent flag; our old imap library didn't
// pass it along (complaining about it being useless), so we
// don't either.
var recentIdx = msg.flags.indexOf('\\Recent');
if (recentIdx !== -1) {
msg.flags.splice(recentIdx, 1);
}
// RFC 3501 doesn't require that we get results in the order we
// request them, so use indexOf if things don't line up. (In fact,
// dovecot sorts them, so we might just want to sort ours too.)
if (self.knownHeaders[i].srvid !== msg.uid) {
i = self.knownUIDs.indexOf(msg.uid);
// If it's telling us about a message we don't know about, run away.
if (i === -1) {
console.warn("Server fetch reports unexpected message:", msg.uid);
return;
}
}
var header = self.knownHeaders[i];
// msg.flags is not sorted in browserbox; sort here before comparing.
var sortedExistingFlags = header.flags.slice();
sortedExistingFlags.sort();
msg.flags.sort();
// Update number of unread messages
if (header.flags.indexOf('\\Seen') === -1 &&
msg.flags.indexOf('\\Seen') !== -1) {
self.storage.folderMeta.unreadCount--;
} else if (header.flags.indexOf('\\Seen') !== -1 &&
msg.flags.indexOf('\\Seen') === -1) {
self.storage.folderMeta.unreadCount++;
}
if (header.flags.toString() !== msg.flags.toString()) {
console.warn(' FLAGS: "' + header.flags.toString() + '" VS "' +
msg.flags.toString() + '"');
header.flags = msg.flags;
self.storage.updateMessageHeader(
header.date, header.id, true, header, /* body hint */ null,
latch.defer());
}
else {
self.storage.unchangedMessageHeader(header);
}
});
// the fetch results will be bursty, so just update all at once
self._updateProgress(KNOWN_HEADERS_AGGR_COST +
KNOWN_HEADERS_PER_COST * self.knownUIDs.length);
latch.then(callback); // our caller doesn't care about its args
}.bind(this));
}
};
exports.Sync = Sync;
});

View file

@ -0,0 +1,79 @@
define(
[
'mimeparser',
'mimefuncs',
'exports'
],
function(
MimeParser,
mimefuncs,
exports
) {
/**
* Simple wrapper around mimeparser hacks allows us to reuse data from the
* BODYSTRUCT request that contained the mime type, etc....
*
* var parser = $textparser.TextParser(
* bodyInfo.bodyReps[n]
* );
*
* // msg is some stream thing from fetcher
*
* msg.on('data', parser.parse.bind(parser));
* msg.on('end', function() {
* var content = parser.complete();
* });
*
*/
function TextParser(partDef) {
this._partDef = partDef;
var parser = this._parser = new MimeParser();
this._totalBytes = 0;
// TODO: escape?
var charsetPart = '', formatPart = '';
if (partDef.params && partDef.params.charset) {
charsetPart = '; charset="' + partDef.params.charset.toLowerCase() + '"';
}
if (partDef.params && partDef.params.format) {
formatPart = '; format="' + partDef.params.format.toLowerCase() + '"';
}
parser.write('Content-Type: ' + partDef.type.toLowerCase() +
'/' + partDef.subtype.toLowerCase() + charsetPart +
formatPart + '\r\n');
if (partDef.encoding) {
parser.write('Content-Transfer-Encoding: ' + partDef.encoding + '\r\n');
}
parser.write('\r\n'); // Finish headers.
if (partDef.pendingBuffer) {
this.parse(partDef.pendingBuffer);
}
}
TextParser.prototype = {
parse: function(buffer) {
this._totalBytes += buffer.length;
this._parser.write(buffer);
},
complete: function() {
this._parser.end();
var str = mimefuncs.charset.decode(this._parser.node.content, 'utf-8');
return {
bytesFetched: this._totalBytes,
text: str
};
}
};
exports.TextParser = TextParser;
});

Some files were not shown because too many files have changed in this diff Show more