mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-24 17:37:30 +09:00
907 lines
33 KiB
JavaScript
907 lines
33 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
this.EXPORTED_SYMBOLS = ['GlodaFundAttr'];
|
|
|
|
var Cc = Components.classes;
|
|
var Ci = Components.interfaces;
|
|
var Cr = Components.results;
|
|
var Cu = Components.utils;
|
|
|
|
Cu.import("resource:///modules/gloda/log4moz.js");
|
|
Cu.import("resource:///modules/StringBundle.js");
|
|
|
|
Cu.import("resource:///modules/gloda/utils.js");
|
|
Cu.import("resource:///modules/gloda/gloda.js");
|
|
Cu.import("resource:///modules/gloda/datastore.js");
|
|
Cu.import("resource:///modules/gloda/datamodel.js"); // for GlodaAttachment
|
|
|
|
Cu.import("resource:///modules/gloda/noun_mimetype.js");
|
|
Cu.import("resource:///modules/gloda/connotent.js");
|
|
|
|
/**
|
|
* @namespace The Gloda Fundamental Attribute provider is a special attribute
|
|
* provider; it provides attributes that the rest of the providers should be
|
|
* able to assume exist. Also, it may end up accessing things at a lower level
|
|
* than most extension providers should do. In summary, don't mimic this code
|
|
* unless you won't complain when your code breaks.
|
|
*/
|
|
var GlodaFundAttr = {
|
|
providerName: "gloda.fundattr",
|
|
strings: new StringBundle("chrome://messenger/locale/gloda.properties"),
|
|
_log: null,
|
|
|
|
init: function gloda_explattr_init() {
|
|
this._log = Log4Moz.repository.getLogger("gloda.fundattr");
|
|
|
|
try {
|
|
this.defineAttributes();
|
|
}
|
|
catch (ex) {
|
|
this._log.error("Error in init: " + ex);
|
|
throw ex;
|
|
}
|
|
},
|
|
|
|
POPULARITY_FROM_ME_TO: 10,
|
|
POPULARITY_FROM_ME_CC: 4,
|
|
POPULARITY_FROM_ME_BCC: 3,
|
|
POPULARITY_TO_ME: 5,
|
|
POPULARITY_CC_ME: 1,
|
|
POPULARITY_BCC_ME: 1,
|
|
|
|
/** Boost for messages 'I' sent */
|
|
NOTABILITY_FROM_ME: 10,
|
|
/** Boost for messages involving 'me'. */
|
|
NOTABILITY_INVOLVING_ME: 1,
|
|
/** Boost for message from someone in 'my' address book. */
|
|
NOTABILITY_FROM_IN_ADDR_BOOK: 10,
|
|
/** Boost for the first person involved in my address book. */
|
|
NOTABILITY_INVOLVING_ADDR_BOOK_FIRST: 8,
|
|
/** Boost for each additional person involved in my address book. */
|
|
NOTABILITY_INVOLVING_ADDR_BOOK_ADDL: 2,
|
|
|
|
defineAttributes: function gloda_fundattr_defineAttributes() {
|
|
/* ***** Conversations ***** */
|
|
// conversation: subjectMatches
|
|
this._attrConvSubject = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrDerived,
|
|
attributeName: "subjectMatches",
|
|
singular: true,
|
|
special: Gloda.kSpecialFulltext,
|
|
specialColumnName: "subject",
|
|
subjectNouns: [Gloda.NOUN_CONVERSATION],
|
|
objectNoun: Gloda.NOUN_FULLTEXT,
|
|
});
|
|
|
|
/* ***** Messages ***** */
|
|
// folder
|
|
this._attrFolder = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "folder",
|
|
singular: true,
|
|
facet: true,
|
|
special: Gloda.kSpecialColumn,
|
|
specialColumnName: "folderID",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_FOLDER,
|
|
}); // tested-by: test_attributes_fundamental
|
|
this._attrAccount = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrDerived,
|
|
attributeName: "account",
|
|
canQuery: "memory",
|
|
singular: true,
|
|
facet: true,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_ACCOUNT
|
|
});
|
|
this._attrMessageKey = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "messageKey",
|
|
singular: true,
|
|
special: Gloda.kSpecialColumn,
|
|
specialColumnName: "messageKey",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_NUMBER,
|
|
canQuery: true,
|
|
}); // tested-by: test_attributes_fundamental
|
|
|
|
// We need to surface the deleted attribute for querying, but there is no
|
|
// reason for user code, so let's call it "_deleted" rather than deleted.
|
|
// (In fact, our validity constraints require a special query formulation
|
|
// that user code should have no clue exists. That's right user code,
|
|
// that's a dare.)
|
|
Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "_deleted",
|
|
singular: true,
|
|
special: Gloda.kSpecialColumn,
|
|
specialColumnName: "deleted",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_NUMBER,
|
|
});
|
|
|
|
|
|
// -- fulltext search helpers
|
|
// fulltextMatches. Match over message subject, body, and attachments
|
|
// @testpoint gloda.noun.message.attr.fulltextMatches
|
|
this._attrFulltext = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrDerived,
|
|
attributeName: "fulltextMatches",
|
|
singular: true,
|
|
special: Gloda.kSpecialFulltext,
|
|
specialColumnName: "messagesText",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_FULLTEXT,
|
|
});
|
|
|
|
// subjectMatches. Fulltext match on subject
|
|
// @testpoint gloda.noun.message.attr.subjectMatches
|
|
this._attrSubjectText = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrDerived,
|
|
attributeName: "subjectMatches",
|
|
singular: true,
|
|
special: Gloda.kSpecialFulltext,
|
|
specialColumnName: "subject",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_FULLTEXT,
|
|
});
|
|
|
|
// bodyMatches. super-synthetic full-text matching...
|
|
// @testpoint gloda.noun.message.attr.bodyMatches
|
|
this._attrBody = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrDerived,
|
|
attributeName: "bodyMatches",
|
|
singular: true,
|
|
special: Gloda.kSpecialFulltext,
|
|
specialColumnName: "body",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_FULLTEXT,
|
|
});
|
|
|
|
// attachmentNamesMatch
|
|
// @testpoint gloda.noun.message.attr.attachmentNamesMatch
|
|
this._attrAttachmentNames = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrDerived,
|
|
attributeName: "attachmentNamesMatch",
|
|
singular: true,
|
|
special: Gloda.kSpecialFulltext,
|
|
specialColumnName: "attachmentNames",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_FULLTEXT,
|
|
});
|
|
|
|
// @testpoint gloda.noun.message.attr.authorMatches
|
|
this._attrAuthorFulltext = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrDerived,
|
|
attributeName: "authorMatches",
|
|
singular: true,
|
|
special: Gloda.kSpecialFulltext,
|
|
specialColumnName: "author",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_FULLTEXT,
|
|
});
|
|
|
|
// @testpoint gloda.noun.message.attr.recipientsMatch
|
|
this._attrRecipientsFulltext = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrDerived,
|
|
attributeName: "recipientsMatch",
|
|
singular: true,
|
|
special: Gloda.kSpecialFulltext,
|
|
specialColumnName: "recipients",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_FULLTEXT,
|
|
});
|
|
|
|
// --- synthetic stuff for some reason
|
|
// conversation
|
|
// @testpoint gloda.noun.message.attr.conversation
|
|
this._attrConversation = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "conversation",
|
|
singular: true,
|
|
special: Gloda.kSpecialColumnParent,
|
|
specialColumnName: "conversationID",
|
|
idStorageAttributeName: "_conversationID",
|
|
valueStorageAttributeName: "_conversation",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_CONVERSATION,
|
|
canQuery: true,
|
|
});
|
|
|
|
// --- Fundamental
|
|
// From
|
|
this._attrFrom = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "from",
|
|
singular: true,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_IDENTITY,
|
|
}); // tested-by: test_attributes_fundamental
|
|
// To
|
|
this._attrTo = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "to",
|
|
singular: false,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_IDENTITY,
|
|
}); // tested-by: test_attributes_fundamental
|
|
// Cc
|
|
this._attrCc = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "cc",
|
|
singular: false,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_IDENTITY,
|
|
}); // not-tested
|
|
/**
|
|
* Bcc'ed recipients; only makes sense for sent messages.
|
|
*/
|
|
this._attrBcc = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "bcc",
|
|
singular: false,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_IDENTITY,
|
|
}); // not-tested
|
|
|
|
// Date. now lives on the row.
|
|
this._attrDate = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "date",
|
|
singular: true,
|
|
facet: {
|
|
type: "date",
|
|
},
|
|
special: Gloda.kSpecialColumn,
|
|
specialColumnName: "date",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_DATE,
|
|
}); // tested-by: test_attributes_fundamental
|
|
|
|
// Header message ID.
|
|
this._attrHeaderMessageID = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "headerMessageID",
|
|
singular: true,
|
|
special: Gloda.kSpecialString,
|
|
specialColumnName: "headerMessageID",
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_STRING,
|
|
canQuery: true,
|
|
}); // tested-by: test_attributes_fundamental
|
|
|
|
// Attachment MIME Types
|
|
this._attrAttachmentTypes = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "attachmentTypes",
|
|
singular: false,
|
|
emptySetIsSignificant: true,
|
|
facet: {
|
|
type: "default",
|
|
// This will group the MIME types by their category.
|
|
groupIdAttr: "category",
|
|
queryHelper: "Category",
|
|
},
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_MIME_TYPE,
|
|
});
|
|
|
|
// Attachment infos
|
|
this._attrIsEncrypted = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "isEncrypted",
|
|
singular: true,
|
|
emptySetIsSignificant: false,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_NUMBER,
|
|
});
|
|
|
|
// Attachment infos
|
|
this._attrAttachmentInfos = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "attachmentInfos",
|
|
singular: false,
|
|
emptySetIsSignificant: false,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_ATTACHMENT,
|
|
});
|
|
|
|
// --- Optimization
|
|
/**
|
|
* Involves means any of from/to/cc/bcc. The queries get ugly enough
|
|
* without this that it seems to justify the cost, especially given the
|
|
* frequent use case. (In fact, post-filtering for the specific from/to/cc
|
|
* is probably justifiable rather than losing this attribute...)
|
|
*/
|
|
this._attrInvolves = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrOptimization,
|
|
attributeName: "involves",
|
|
singular: false,
|
|
facet: {
|
|
type: "default",
|
|
/**
|
|
* Filter out 'me', as we have other facets that deal with that, and the
|
|
* 'me' identities are so likely that they distort things.
|
|
*
|
|
* @return true if the identity is not one of my identities, false if it
|
|
* is.
|
|
*/
|
|
filter: function gloda_explattr_involves_filter(aItem) {
|
|
return (!(aItem.id in Gloda.myIdentities));
|
|
}
|
|
},
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_IDENTITY,
|
|
}); // not-tested
|
|
|
|
/**
|
|
* Any of to/cc/bcc.
|
|
*/
|
|
this._attrRecipients = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrOptimization,
|
|
attributeName: "recipients",
|
|
singular: false,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_IDENTITY,
|
|
}); // not-tested
|
|
|
|
// From Me (To/Cc/Bcc)
|
|
this._attrFromMe = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrOptimization,
|
|
attributeName: "fromMe",
|
|
singular: false,
|
|
// The interesting thing to a facet is whether the message is from me.
|
|
facet: {
|
|
type: "nonempty?"
|
|
},
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_PARAM_IDENTITY,
|
|
}); // not-tested
|
|
// To/Cc/Bcc Me
|
|
this._attrToMe = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "toMe",
|
|
// The interesting thing to a facet is whether the message is to me.
|
|
facet: {
|
|
type: "nonempty?"
|
|
},
|
|
singular: false,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_PARAM_IDENTITY,
|
|
}); // not-tested
|
|
|
|
|
|
// -- Mailing List
|
|
// Non-singular, but a hard call. Namely, it is obvious that a message can
|
|
// be addressed to multiple mailing lists. However, I don't see how you
|
|
// could receive a message with more than one set of List-* headers,
|
|
// since each list-serve would each send you a copy. Based on our current
|
|
// decision to treat each physical message as separate, it almost seems
|
|
// right to limit the list attribute to the copy that originated at the
|
|
// list. That may sound entirely wrong, but keep in mind that until we
|
|
// have seen a message from the list with the List headers, we can't
|
|
// definitely know it's a mailing list (although heuristics could take us
|
|
// pretty far). As such, the quasi-singular thing is appealing.
|
|
// Of course, the reality is that we really want to know if a message was
|
|
// sent to multiple mailing lists and be able to query on that.
|
|
// Additionally, our implicit-to logic needs to work on messages that
|
|
// weren't relayed by the list-serve, especially messages sent to the list
|
|
// by the user.
|
|
this._attrList = Gloda.defineAttribute({
|
|
provider: this,
|
|
extensionName: Gloda.BUILT_IN,
|
|
attributeType: Gloda.kAttrFundamental,
|
|
attributeName: "mailing-list",
|
|
bindName: "mailingLists",
|
|
singular: false,
|
|
emptySetIsSignificant: true,
|
|
facet: true,
|
|
subjectNouns: [Gloda.NOUN_MESSAGE],
|
|
objectNoun: Gloda.NOUN_IDENTITY,
|
|
}); // not-tested, not-implemented
|
|
},
|
|
|
|
RE_LIST_POST: /<mailto:([^>]+)>/,
|
|
|
|
/**
|
|
*
|
|
* Specializations:
|
|
* - Mailing Lists. Replies to a message on a mailing list frequently only
|
|
* have the list-serve as the 'to', so we try to generate a synthetic 'to'
|
|
* based on the author of the parent message when possible. (The 'possible'
|
|
* part is that we may not have a copy of the parent message at the time of
|
|
* processing.)
|
|
* - Newsgroups. Same deal as mailing lists.
|
|
*/
|
|
process: function* gloda_fundattr_process(aGlodaMessage, aRawReps,
|
|
aIsNew, aCallbackHandle) {
|
|
let aMsgHdr = aRawReps.header;
|
|
let aMimeMsg = aRawReps.mime;
|
|
|
|
// -- From
|
|
// Let's use replyTo if available.
|
|
// er, since we are just dealing with mailing lists for now, forget the
|
|
// reply-to...
|
|
// TODO: deal with default charset issues
|
|
let author = null;
|
|
/*
|
|
try {
|
|
author = aMsgHdr.getStringProperty("replyTo");
|
|
}
|
|
catch (ex) {
|
|
}
|
|
*/
|
|
if (author == null || author == "")
|
|
author = aMsgHdr.author;
|
|
|
|
let normalizedListPost = "";
|
|
if (aMimeMsg && aMimeMsg.has("list-post")) {
|
|
let match = this.RE_LIST_POST.exec(aMimeMsg.get("list-post"));
|
|
if (match)
|
|
normalizedListPost = "<" + match[1] + ">";
|
|
}
|
|
|
|
// Do not use the MIME decoded variants of any of the email addresses
|
|
// because if name is encoded and has a comma in it, it will break the
|
|
// address parser (which already knows how to do the decoding anyways).
|
|
let [authorIdentities, toIdentities, ccIdentities, bccIdentities,
|
|
listIdentities] =
|
|
yield aCallbackHandle.pushAndGo(
|
|
Gloda.getOrCreateMailIdentities(aCallbackHandle,
|
|
author, aMsgHdr.recipients,
|
|
aMsgHdr.ccList, aMsgHdr.bccList,
|
|
normalizedListPost));
|
|
|
|
if (authorIdentities.length != 1) {
|
|
throw new Gloda.BadItemContentsError(
|
|
"Message with subject '" + aMsgHdr.mime2DecodedSubject +
|
|
"' somehow lacks a valid author. Bailing.");
|
|
}
|
|
let authorIdentity = authorIdentities[0];
|
|
aGlodaMessage.from = authorIdentity;
|
|
|
|
// -- To, Cc, Bcc
|
|
aGlodaMessage.to = toIdentities;
|
|
aGlodaMessage.cc = ccIdentities;
|
|
aGlodaMessage.bcc = bccIdentities;
|
|
|
|
// -- Mailing List
|
|
if (listIdentities.length)
|
|
aGlodaMessage.mailingLists = listIdentities;
|
|
|
|
let findIsEncrypted = x =>
|
|
x.isEncrypted || (x.parts ? x.parts.some(findIsEncrypted) : false);
|
|
|
|
// -- Encryption
|
|
aGlodaMessage.isEncrypted = false;
|
|
if (aMimeMsg) {
|
|
aGlodaMessage.isEncrypted = findIsEncrypted(aMimeMsg);
|
|
}
|
|
|
|
// -- Attachments
|
|
if (aMimeMsg) {
|
|
// nsParseMailbox.cpp puts the attachment flag on msgHdrs as soon as it
|
|
// finds a multipart/mixed part. This is a good heuristic, but if it turns
|
|
// out the part has no filename, then we don't treat it as an attachment.
|
|
// We just streamed the message, and we have all the information to figure
|
|
// that out, so now is a good place to clear the flag if needed.
|
|
let foundRealAttachment = false;
|
|
let attachmentTypes = [];
|
|
for (let attachment of aMimeMsg.allAttachments) {
|
|
// We don't care about would-be attachments that are not user-intended
|
|
// attachments but rather artifacts of the message content.
|
|
// We also want to avoid dealing with obviously bogus mime types.
|
|
// (If you don't have a "/", you are probably bogus.)
|
|
if (attachment.isRealAttachment &&
|
|
attachment.contentType.includes("/")) {
|
|
attachmentTypes.push(MimeTypeNoun.getMimeType(attachment.contentType));
|
|
}
|
|
if (attachment.isRealAttachment)
|
|
foundRealAttachment = true;
|
|
}
|
|
if (attachmentTypes.length) {
|
|
aGlodaMessage.attachmentTypes = attachmentTypes;
|
|
}
|
|
|
|
let aMsgHdr = aRawReps.header;
|
|
let wasStreamed = aMsgHdr &&
|
|
!aGlodaMessage.isEncrypted &&
|
|
((aMsgHdr.flags & Ci.nsMsgMessageFlags.Offline) ||
|
|
(aMsgHdr.folder instanceof Ci.nsIMsgLocalMailFolder));
|
|
|
|
// Clear the flag if it turns out there's no attachment after all and we
|
|
// streamed completely the message (if we didn't, then we have no
|
|
// knowledge of attachments, unless bug 673370 is fixed).
|
|
if (!foundRealAttachment && wasStreamed)
|
|
aMsgHdr.markHasAttachments(false);
|
|
|
|
// This is not the same kind of attachments as above. Now, we want to
|
|
// provide convenience attributes to Gloda consumers, so that they can run
|
|
// through the list of attachments of a given message, to possibly build a
|
|
// visualization on top of it. We still reject bogus mime types, which
|
|
// means yencode won't be supported. Oh, I feel really bad.
|
|
let attachmentInfos = [];
|
|
for (let att of aMimeMsg.allUserAttachments) {
|
|
attachmentInfos.push(this.glodaAttFromMimeAtt(aRawReps.trueGlodaRep,
|
|
att));
|
|
}
|
|
aGlodaMessage.attachmentInfos = attachmentInfos;
|
|
}
|
|
|
|
// TODO: deal with mailing lists, including implicit-to. this will require
|
|
// convincing the indexer to pass us in the previous message if it is
|
|
// available. (which we'll simply pass to everyone... it can help body
|
|
// logic for quoting purposes, etc. too.)
|
|
|
|
yield Gloda.kWorkDone;
|
|
},
|
|
|
|
glodaAttFromMimeAtt:
|
|
function gloda_fundattr_glodaAttFromMimeAtt(aGlodaMessage, aAtt) {
|
|
// So we don't want to store the URL because it can change over time if
|
|
// the message is moved. What we do is store the full URL if it's a
|
|
// detached attachment, otherwise just keep the part information, and
|
|
// rebuild the URL according to where the message is sitting.
|
|
let part, externalUrl;
|
|
if (aAtt.isExternal) {
|
|
externalUrl = aAtt.url;
|
|
} else {
|
|
let matches = aAtt.url.match(GlodaUtils.PART_RE);
|
|
if (matches && matches.length)
|
|
part = matches[1];
|
|
else
|
|
this._log.error("Error processing attachment: " + aAtt.url);
|
|
}
|
|
return new GlodaAttachment(aGlodaMessage,
|
|
aAtt.name,
|
|
aAtt.contentType,
|
|
aAtt.size,
|
|
part,
|
|
externalUrl,
|
|
aAtt.isExternal);
|
|
},
|
|
|
|
optimize: function* gloda_fundattr_optimize(aGlodaMessage, aRawReps,
|
|
aIsNew, aCallbackHandle) {
|
|
|
|
let aMsgHdr = aRawReps.header;
|
|
|
|
// for simplicity this is used for both involves and recipients
|
|
let involvesIdentities = {};
|
|
let involves = aGlodaMessage.involves || [];
|
|
let recipients = aGlodaMessage.recipients || [];
|
|
|
|
// 'me' specialization optimizations
|
|
let toMe = aGlodaMessage.toMe || [];
|
|
let fromMe = aGlodaMessage.fromMe || [];
|
|
|
|
let myIdentities = Gloda.myIdentities; // needless optimization?
|
|
let authorIdentity = aGlodaMessage.from;
|
|
let isFromMe = authorIdentity.id in myIdentities;
|
|
|
|
// The fulltext search column for the author. We want to have in here:
|
|
// - The e-mail address and display name as enclosed on the message.
|
|
// - The name per the address book card for this e-mail address, if we have
|
|
// one.
|
|
aGlodaMessage._indexAuthor = aMsgHdr.mime2DecodedAuthor;
|
|
// The fulltext search column for the recipients. (same deal)
|
|
aGlodaMessage._indexRecipients = aMsgHdr.mime2DecodedRecipients;
|
|
|
|
if (isFromMe)
|
|
aGlodaMessage.notability += this.NOTABILITY_FROM_ME;
|
|
else {
|
|
let authorCard = authorIdentity.abCard;
|
|
if (authorCard) {
|
|
aGlodaMessage.notability += this.NOTABILITY_FROM_IN_ADDR_BOOK;
|
|
// @testpoint gloda.noun.message.attr.authorMatches
|
|
aGlodaMessage._indexAuthor += ' ' + authorCard.displayName;
|
|
}
|
|
}
|
|
|
|
involves.push(authorIdentity);
|
|
involvesIdentities[authorIdentity.id] = true;
|
|
|
|
let involvedAddrBookCount = 0;
|
|
|
|
for (let toIdentity of aGlodaMessage.to) {
|
|
if (!(toIdentity.id in involvesIdentities)) {
|
|
involves.push(toIdentity);
|
|
recipients.push(toIdentity);
|
|
involvesIdentities[toIdentity.id] = true;
|
|
let toCard = toIdentity.abCard;
|
|
if (toCard) {
|
|
involvedAddrBookCount++;
|
|
// @testpoint gloda.noun.message.attr.recipientsMatch
|
|
aGlodaMessage._indexRecipients += ' ' + toCard.displayName;
|
|
}
|
|
}
|
|
|
|
// optimization attribute to-me ('I' am the parameter)
|
|
if (toIdentity.id in myIdentities) {
|
|
toMe.push([toIdentity, authorIdentity]);
|
|
if (aIsNew)
|
|
authorIdentity.contact.popularity += this.POPULARITY_TO_ME;
|
|
}
|
|
// optimization attribute from-me-to ('I' am the parameter)
|
|
if (isFromMe) {
|
|
fromMe.push([authorIdentity, toIdentity]);
|
|
// also, popularity
|
|
if (aIsNew)
|
|
toIdentity.contact.popularity += this.POPULARITY_FROM_ME_TO;
|
|
}
|
|
}
|
|
for (let ccIdentity of aGlodaMessage.cc) {
|
|
if (!(ccIdentity.id in involvesIdentities)) {
|
|
involves.push(ccIdentity);
|
|
recipients.push(ccIdentity);
|
|
involvesIdentities[ccIdentity.id] = true;
|
|
let ccCard = ccIdentity.abCard;
|
|
if (ccCard) {
|
|
involvedAddrBookCount++;
|
|
// @testpoint gloda.noun.message.attr.recipientsMatch
|
|
aGlodaMessage._indexRecipients += ' ' + ccCard.displayName;
|
|
}
|
|
}
|
|
// optimization attribute cc-me ('I' am the parameter)
|
|
if (ccIdentity.id in myIdentities) {
|
|
toMe.push([ccIdentity, authorIdentity]);
|
|
if (aIsNew)
|
|
authorIdentity.contact.popularity += this.POPULARITY_CC_ME;
|
|
}
|
|
// optimization attribute from-me-to ('I' am the parameter)
|
|
if (isFromMe) {
|
|
fromMe.push([authorIdentity, ccIdentity]);
|
|
// also, popularity
|
|
if (aIsNew)
|
|
ccIdentity.contact.popularity += this.POPULARITY_FROM_ME_CC;
|
|
}
|
|
}
|
|
// just treat bcc like cc; the intent is the same although the exact
|
|
// semantics differ.
|
|
for (let bccIdentity of aGlodaMessage.bcc) {
|
|
if (!(bccIdentity.id in involvesIdentities)) {
|
|
involves.push(bccIdentity);
|
|
recipients.push(bccIdentity);
|
|
involvesIdentities[bccIdentity.id] = true;
|
|
let bccCard = bccIdentity.abCard;
|
|
if (bccCard) {
|
|
involvedAddrBookCount++;
|
|
// @testpoint gloda.noun.message.attr.recipientsMatch
|
|
aGlodaMessage._indexRecipients += ' ' + bccCard.displayName;
|
|
}
|
|
}
|
|
// optimization attribute cc-me ('I' am the parameter)
|
|
if (bccIdentity.id in myIdentities) {
|
|
toMe.push([bccIdentity, authorIdentity]);
|
|
if (aIsNew)
|
|
authorIdentity.contact.popularity += this.POPULARITY_BCC_ME;
|
|
}
|
|
// optimization attribute from-me-to ('I' am the parameter)
|
|
if (isFromMe) {
|
|
fromMe.push([authorIdentity, bccIdentity]);
|
|
// also, popularity
|
|
if (aIsNew)
|
|
bccIdentity.contact.popularity += this.POPULARITY_FROM_ME_BCC;
|
|
}
|
|
}
|
|
|
|
if (involvedAddrBookCount)
|
|
aGlodaMessage.notability += this.NOTABILITY_INVOLVING_ADDR_BOOK_FIRST +
|
|
(involvedAddrBookCount - 1) * this.NOTABILITY_INVOLVING_ADDR_BOOK_ADDL;
|
|
|
|
aGlodaMessage.involves = involves;
|
|
aGlodaMessage.recipients = recipients;
|
|
if (toMe.length) {
|
|
aGlodaMessage.toMe = toMe;
|
|
aGlodaMessage.notability += this.NOTABILITY_INVOLVING_ME;
|
|
}
|
|
if (fromMe.length)
|
|
aGlodaMessage.fromMe = fromMe;
|
|
|
|
// Content
|
|
if (aRawReps.bodyLines) {
|
|
aGlodaMessage._content = aRawReps.content = new GlodaContent();
|
|
if (this.contentWhittle({}, aRawReps.bodyLines, aGlodaMessage._content)) {
|
|
// we were going to do something here?
|
|
}
|
|
}
|
|
else {
|
|
aRawReps.content = null;
|
|
}
|
|
|
|
yield Gloda.kWorkDone;
|
|
},
|
|
|
|
/**
|
|
* Duplicates the notability logic from optimize(). Arguably optimize should
|
|
* be factored to call us, grokNounItem should be factored to call us, or we
|
|
* should get sufficiently fancy that our code wildly diverges.
|
|
*/
|
|
score: function gloda_fundattr_score(aMessage, aContext) {
|
|
let score = 0;
|
|
|
|
let authorIdentity = aMessage.from;
|
|
if (authorIdentity.id in Gloda.myIdentities)
|
|
score += this.NOTABILITY_FROM_ME;
|
|
else if (authorIdentity.inAddressBook)
|
|
score += this.NOTABILITY_FROM_IN_ADDR_BOOK;
|
|
if (aMessage.toMe)
|
|
score += this.NOTABILITY_INVOLVING_ME;
|
|
|
|
let involvedAddrBookCount = 0;
|
|
for (let [, identity] in Iterator(aMessage.to))
|
|
if (identity.inAddressBook)
|
|
involvedAddrBookCount++;
|
|
for (let [, identity] in Iterator(aMessage.cc))
|
|
if (identity.inAddressBook)
|
|
involvedAddrBookCount++;
|
|
if (involvedAddrBookCount)
|
|
score += this.NOTABILITY_INVOLVING_ADDR_BOOK_FIRST +
|
|
(involvedAddrBookCount - 1) * this.NOTABILITY_INVOLVING_ADDR_BOOK_ADDL;
|
|
return score;
|
|
},
|
|
|
|
_countQuoteDepthAndNormalize:
|
|
function gloda_fundattr__countQuoteDepthAndNormalize(aLine) {
|
|
let count = 0;
|
|
let lastStartOffset = 0;
|
|
|
|
for (let i = 0; i < aLine.length; i++) {
|
|
let c = aLine[i];
|
|
if (c == ">") {
|
|
count++;
|
|
lastStartOffset = i+1;
|
|
}
|
|
else if (c == " ") {
|
|
}
|
|
else {
|
|
return [count,
|
|
lastStartOffset ? aLine.substring(lastStartOffset) : aLine];
|
|
}
|
|
}
|
|
|
|
return [count, lastStartOffset ? aLine.substring(lastStartOffset) : aLine];
|
|
},
|
|
|
|
/**
|
|
* Attempt to understand simple quoting constructs that use ">" with
|
|
* obvious phrases to enter the quoting block. No support for other types
|
|
* of quoting at this time. Also no support for piercing the wrapper of
|
|
* forwarded messages to actually be the content of the forwarded message.
|
|
*/
|
|
contentWhittle: function gloda_fundattr_contentWhittle(aMeta,
|
|
aBodyLines, aContent) {
|
|
if (!aContent.volunteerContent(aContent.kPriorityBase))
|
|
return false;
|
|
|
|
// duplicate the list; we mutate somewhat...
|
|
let bodyLines = aBodyLines.concat();
|
|
|
|
// lastNonBlankLine originally was just for detecting quoting idioms where
|
|
// the "wrote" line was separated from the quoted block by a blank line.
|
|
// Now we also use it for whitespace suppression at the boundaries of
|
|
// quoted and un-quoted text. (We keep blank lines within the same
|
|
// 'block' of quoted or non-quoted text.)
|
|
// Because we now have two goals for it, and we still want to suppress blank
|
|
// lines when there is a 'wrote' line involved, we introduce...
|
|
// prevLastNonBlankLine! This arguably suggests refactoring should be the
|
|
// next step, but things work for now.
|
|
let rangeStart = 0, lastNonBlankLine = null, prevLastNonBlankLine = null;
|
|
let inQuoteDepth = 0;
|
|
for (let [iLine, line] of bodyLines.entries()) {
|
|
if (!line || (line == "\xa0")) /* unicode non breaking space */
|
|
continue;
|
|
|
|
if (line.startsWith(">")) {
|
|
if (!inQuoteDepth) {
|
|
let rangeEnd = iLine - 1;
|
|
let quoteRangeStart = iLine;
|
|
// see if the last non-blank-line was a lead-in...
|
|
if (lastNonBlankLine != null) {
|
|
// TODO: localize quote range start detection
|
|
if (aBodyLines[lastNonBlankLine].includes("wrote")) {
|
|
quoteRangeStart = lastNonBlankLine;
|
|
rangeEnd = lastNonBlankLine - 1;
|
|
// we 'used up' lastNonBlankLine, let's promote the prev guy to
|
|
// be the new lastNonBlankLine for the next logic block
|
|
lastNonBlankLine = prevLastNonBlankLine;
|
|
}
|
|
// eat the trailing whitespace...
|
|
if (lastNonBlankLine != null)
|
|
rangeEnd = Math.min(rangeEnd, lastNonBlankLine);
|
|
}
|
|
if (rangeEnd >= rangeStart)
|
|
aContent.content(aBodyLines.slice(rangeStart, rangeEnd+1));
|
|
|
|
[inQuoteDepth, line] = this._countQuoteDepthAndNormalize(line);
|
|
bodyLines[iLine] = line;
|
|
rangeStart = quoteRangeStart;
|
|
}
|
|
else {
|
|
let curQuoteDepth;
|
|
[curQuoteDepth, line] = this._countQuoteDepthAndNormalize(line);
|
|
bodyLines[iLine] = line;
|
|
|
|
if (curQuoteDepth != inQuoteDepth) {
|
|
// we could do some "wrote" compensation here, but it's not really
|
|
// as important. let's wait for a more clever algorithm.
|
|
aContent.quoted(aBodyLines.slice(rangeStart, iLine), inQuoteDepth);
|
|
inQuoteDepth = curQuoteDepth;
|
|
rangeStart = iLine;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (inQuoteDepth) {
|
|
aContent.quoted(aBodyLines.slice(rangeStart, iLine), inQuoteDepth);
|
|
inQuoteDepth = 0;
|
|
rangeStart = iLine;
|
|
}
|
|
}
|
|
|
|
prevLastNonBlankLine = lastNonBlankLine;
|
|
lastNonBlankLine = iLine;
|
|
}
|
|
|
|
if (inQuoteDepth) {
|
|
aContent.quoted(aBodyLines.slice(rangeStart), inQuoteDepth);
|
|
}
|
|
else {
|
|
aContent.content(aBodyLines.slice(rangeStart, lastNonBlankLine+1));
|
|
}
|
|
|
|
return true;
|
|
},
|
|
};
|