mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-02-19 16:36:23 +00:00
1153 lines
38 KiB
JavaScript
1153 lines
38 KiB
JavaScript
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
/**
|
|
* @name Message
|
|
* @constructor
|
|
* @param {string} accountId - the account ID
|
|
* @param {string} mailboxPath - an array of the mailbox path components
|
|
* @param {object} futureAddressBookData - either an object literal or a promise
|
|
* @param {bool} lazy - do "lazy loading" so we are very quick at initializing message instances
|
|
*/
|
|
function Message(accountId, mailbox, futureMessageData, lazy) {
|
|
this.accountId = accountId;
|
|
this.$mailbox = mailbox;
|
|
this.$hasUnsafeContent = false;
|
|
this.$loadUnsafeContent = false;
|
|
this.editable = { to: [], cc: [], bcc: [] };
|
|
this.selected = false;
|
|
|
|
// Data is immediately available
|
|
if (typeof futureMessageData.then !== 'function') {
|
|
//console.debug(JSON.stringify(futureMessageData, undefined, 2));
|
|
if (angular.isUndefined(lazy) || !lazy) {
|
|
this.init(futureMessageData);
|
|
}
|
|
this.uid = parseInt(futureMessageData.uid);
|
|
this.selected = !!futureMessageData.selected;
|
|
this.level = parseInt(futureMessageData.level);
|
|
this.first = parseInt(futureMessageData.first) === 1;
|
|
this.flags = [];
|
|
if (this.first) {
|
|
this.threadCount = parseInt(futureMessageData.count);
|
|
this.collapsed = (futureMessageData.collapsed === true);
|
|
}
|
|
else if (!isNaN(this.level) && this.level >= 0) {
|
|
this.threadMember = true;
|
|
}
|
|
}
|
|
else {
|
|
// The promise will be unwrapped first
|
|
this.$unwrap(futureMessageData);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @memberof Message
|
|
* @desc The factory we'll use to register with Angular
|
|
* @returns the Message constructor
|
|
*/
|
|
Message.$factory = ['$q', '$timeout', '$log', 'sgSettings', 'sgMessage_STATUS', 'Resource', 'Preferences', function ($q, $timeout, $log, Settings, Message_STATUS, Resource, Preferences) {
|
|
angular.extend(Message, {
|
|
STATUS: Message_STATUS,
|
|
$q: $q,
|
|
$timeout: $timeout,
|
|
$log: $log,
|
|
$$resource: new Resource(Settings.activeUser('folderURL') + 'Mail', Settings.activeUser()),
|
|
$Preferences: Preferences,
|
|
$avatar: angular.bind(Preferences, Preferences.avatar)
|
|
});
|
|
|
|
// Initialize tags form user's defaults
|
|
if (Preferences.defaults.SOGoMailLabelsColors) {
|
|
Message.$tags = Preferences.defaults.SOGoMailLabelsColors;
|
|
} else {
|
|
Message.$tags = {};
|
|
}
|
|
if (Preferences.defaults.SOGoMailDisplayRemoteInlineImages &&
|
|
Preferences.defaults.SOGoMailDisplayRemoteInlineImages == 'always') {
|
|
Message.$displayRemoteInlineImages = true;
|
|
}
|
|
|
|
return Message; // return constructor
|
|
}];
|
|
|
|
/**
|
|
* @module SOGo.MailerUI
|
|
* @desc Factory registration of Message in Angular module.
|
|
*/
|
|
try {
|
|
angular.module('SOGo.MailerUI');
|
|
}
|
|
catch (e) {
|
|
angular.module('SOGo.MailerUI', ['SOGo.Common']);
|
|
}
|
|
angular.module('SOGo.MailerUI')
|
|
.constant('sgMessage_STATUS', {
|
|
NOT_LOADED: 0,
|
|
DELAYED_LOADING: 1,
|
|
LOADING: 2,
|
|
LOADED: 3,
|
|
DELAYED_MS: 300
|
|
})
|
|
.factory('Message', Message.$factory);
|
|
|
|
/**
|
|
* @function filterTags
|
|
* @memberof Message.prototype
|
|
* @desc Search for tags (ie., mail labels) matching some criterias
|
|
* @param {string} search - the search string to match
|
|
* @returns a collection of strings
|
|
*/
|
|
Message.filterTags = function (query, excludedTags) {
|
|
var re = new RegExp(query, 'i'),
|
|
results = [];
|
|
|
|
_.forEach(_.keys(Message.$tags), function (tag) {
|
|
var pair = Message.$tags[tag];
|
|
if (pair[0].search(re) != -1) {
|
|
if (!_.includes(excludedTags, tag))
|
|
results.push({ name: tag, description: pair[0], color: pair[1] });
|
|
}
|
|
});
|
|
|
|
return results;
|
|
};
|
|
|
|
/**
|
|
* @function init
|
|
* @memberof Message.prototype
|
|
* @desc Extend instance with new data and massage some attributes.
|
|
* @param {object} data - attributes of message
|
|
*/
|
|
Message.prototype.init = function (data) {
|
|
var _this = this;
|
|
angular.extend(this, data);
|
|
this.$formatFullAddresses();
|
|
this.$loadUnsafeContent = false;
|
|
_.forEach(this.flags, function (flag, i) {
|
|
if (flag.charAt(0) == '$') {
|
|
_this.flags.splice(i, 1, '_' + flag);
|
|
}
|
|
});
|
|
// isread will be undefined when composing a new message -- assume unseen flag is not set.
|
|
this.isread = angular.isDefined(this.isread) ? !!this.isread : true;
|
|
};
|
|
|
|
/**
|
|
* @function $absolutePath
|
|
* @memberof Message.prototype
|
|
* @desc Build the path of the message
|
|
* @returns a string representing the path relative to the mail module
|
|
*/
|
|
Message.prototype.$absolutePath = function (options) {
|
|
var _this = this, id = this.id;
|
|
|
|
function buildPath() {
|
|
var path;
|
|
path = _.map(_this.$mailbox.path.split('/'), function (component) {
|
|
return 'folder' + component.asCSSIdentifier();
|
|
});
|
|
path.splice(0, 0, _this.accountId); // insert account ID
|
|
return path.join('/');
|
|
}
|
|
|
|
if (angular.isUndefined(this.id) || options && options.nocache) {
|
|
this.id = buildPath() + '/' + this.uid; // add message UID
|
|
id = this.id;
|
|
}
|
|
if (options && options.asDraft && this.draftId) {
|
|
id = buildPath() + '/' + this.draftId; // add draft ID
|
|
}
|
|
if (options && options.withResourcePath) {
|
|
id = Message.$$resource.path(id); // return absolute URL
|
|
}
|
|
|
|
return id;
|
|
};
|
|
|
|
/**
|
|
* @function $setUID
|
|
* @memberof Message.prototype
|
|
* @desc Change the UID of the message. This happens when saving a draft.
|
|
* @param {number} uid - the new message UID
|
|
*/
|
|
Message.prototype.$setUID = function (uid) {
|
|
var oldUID = (this.uid || -1), _this = this, index;
|
|
|
|
if (oldUID != parseInt(uid)) {
|
|
this.uid = parseInt(uid);
|
|
this.$absolutePath({ nocache: true });
|
|
if (oldUID > -1) {
|
|
oldUID = oldUID.toString();
|
|
if (angular.isDefined(this.$mailbox.uidsMap[oldUID])) {
|
|
index = this.$mailbox.uidsMap[oldUID];
|
|
this.$mailbox.uidsMap[uid] = index;
|
|
delete this.$mailbox.uidsMap[oldUID];
|
|
this.$mailbox.$messages[index].uid = this.uid;
|
|
|
|
// Update messages list of mailbox
|
|
_.forEach(['from', 'to', 'subject'], function (attr) {
|
|
_this.$mailbox.$messages[index][attr] = _this.editable[attr];
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
// Refresh selected folder if it's the drafts mailbox
|
|
if (this.$mailbox.constructor.selectedFolder &&
|
|
this.$mailbox.constructor.selectedFolder.type == 'draft') {
|
|
this.$mailbox.constructor.selectedFolder.$filter();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @function $formatFullAddresses
|
|
* @memberof Message.prototype
|
|
* @desc Format all sender and recipients addresses with a complete description (name <email>).
|
|
* This function also generates the avatar URL for each email address and a short name
|
|
*/
|
|
Message.prototype.$formatFullAddresses = function () {
|
|
var _this = this;
|
|
var identities = _.map(_this.$mailbox.$account.identities, 'email');
|
|
|
|
// Build long representation of email addresses
|
|
_.forEach(['from', 'to', 'cc', 'bcc', 'reply-to'], function (type) {
|
|
_.forEach(_this[type], function (data) {
|
|
if (data.name && data.name != data.email) {
|
|
data.full = data.name + ' <' + data.email + '>';
|
|
|
|
if (data.name.length < 10)
|
|
// Name is already short
|
|
data.shortname = data.name;
|
|
else if (data.name.split(' ').length)
|
|
// If we have "Alice Foo" or "Foo, Alice" as name, we grab "Alice"
|
|
data.shortname = _.first(_.last(data.name.split(/, */)).split(/ +/)).replace('\'', '');
|
|
}
|
|
else if (data.email) {
|
|
data.full = '<' + data.email + '>';
|
|
data.shortname = data.email.split('@')[0];
|
|
}
|
|
|
|
data.image = Message.$avatar(data.email, 32);
|
|
|
|
// If the current user is the recepient, overwrite
|
|
// the short name with 'me'
|
|
if (_.indexOf(identities, data.email) >= 0)
|
|
data.shortname = l('me');
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $shortRecipients
|
|
* @memberof Message.prototype
|
|
* @desc Format all recipients into a very compact string
|
|
* @returns a compacted string of all recipients
|
|
*/
|
|
Message.prototype.$shortRecipients = function (max) {
|
|
var _this = this, result = [], count = 0, total = 0;
|
|
|
|
// Build short representation of email addresses
|
|
_.forEach(['to', 'cc', 'bcc'], function (type) {
|
|
total += _this[type] ? _this[type].length : 0;
|
|
_.forEach(_this[type], function (data, i) {
|
|
if (count < max)
|
|
result.push(data.shortname);
|
|
count++;
|
|
});
|
|
});
|
|
|
|
if (total > max)
|
|
result.push(l('and %{0} more...', (total - max)));
|
|
|
|
return result.join(', ');
|
|
};
|
|
|
|
/**
|
|
* @function $shortAddress
|
|
* @memberof Message.prototype
|
|
* @desc Format the first address of a specific type with a short description.
|
|
* @returns a string of the name or the email of the envelope address type
|
|
*/
|
|
Message.prototype.$shortAddress = function (type, fullEmail) {
|
|
var address = '';
|
|
if (this[type]) {
|
|
if (angular.isString(this[type])) {
|
|
// The recipient is a string; try to extract the name
|
|
var emailRE = /<?(([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?))/i;
|
|
var match = this[type].match(String.emailRE);
|
|
if (match) {
|
|
address = this[type].substring(0, match.index);
|
|
address = address.replace(/^\"? *(.+?)\"? *$/, "$1");
|
|
}
|
|
if (!address.length)
|
|
address = this[type];
|
|
}
|
|
else if (this[type].length > 0) {
|
|
// We have an array of objects; pick the first one
|
|
if(!fullEmail)
|
|
address = this[type][0].name || this[type][0].email || '';
|
|
else if(this[type][0].name && this[type][0].email)
|
|
address = this[type][0].name + ' <' + this[type][0].email +'>';
|
|
else if(this[type][0].name)
|
|
address = this[type][0].name;
|
|
else if(this[type][0].email)
|
|
address = this[type][0].email;
|
|
else
|
|
address = '';
|
|
}
|
|
}
|
|
|
|
return punycode.toUnicode(address);
|
|
};
|
|
|
|
/**
|
|
* @function allowReplyAll
|
|
* @memberof Message.prototype
|
|
* @desc Check if 'Reply to All' is an appropriate action on the message.
|
|
* @returns true if the message is not a draft and has more than one recipient
|
|
*/
|
|
Message.prototype.allowReplyAll = function () {
|
|
var identities = _.map(this.$mailbox.$account.identities, 'email');
|
|
var recipientsCount = 0;
|
|
recipientsCount = _.reduce(['to', 'cc', 'bcc', 'reply-to'], _.bind(function (count, type) {
|
|
var typeCount = 0;
|
|
if (this[type]) {
|
|
typeCount = this[type].length;
|
|
_.forEach(this[type], function (recipient) {
|
|
if (_.indexOf(identities, recipient.email) >= 0) {
|
|
typeCount--;
|
|
}
|
|
});
|
|
return count + typeCount;
|
|
}
|
|
else {
|
|
return count;
|
|
}
|
|
}, this), recipientsCount);
|
|
|
|
return !this.isDraft && recipientsCount > 1;
|
|
};
|
|
|
|
/**
|
|
* @function loadUnsafeContent
|
|
* @memberof Message.prototype
|
|
* @desc Mark the message to load unsafe resources when calling $content().
|
|
*/
|
|
Message.prototype.loadUnsafeContent = function () {
|
|
this.$loadUnsafeContent = true;
|
|
delete this.$parts;
|
|
};
|
|
|
|
/**
|
|
* @function $content
|
|
* @memberof Message.prototype
|
|
* @desc Get the message body as accepted by SCE (Angular Strict Contextual Escaping).
|
|
* @returns the HTML representation of the body
|
|
*/
|
|
Message.prototype.$content = function () {
|
|
// Punycode
|
|
if (this.to && this.to.length > 0) {
|
|
this.to.forEach(function (element, i, arr) {
|
|
if (element.email && element.email.indexOf('@') > 0)
|
|
arr[i].email = punycode.toUnicode(element.email);
|
|
});
|
|
}
|
|
if (this.from && this.from.indexOf('@') > 0)
|
|
this.from = punycode.toUnicode(this.from);
|
|
|
|
var _this = this,
|
|
parts = [],
|
|
|
|
|
|
|
|
_visit = function (part) {
|
|
part.msgclass = 'msg-attachment-other';
|
|
if (part.type == 'UIxMailPartAlternativeViewer') {
|
|
_visit(_.find(part.content, function (alternatePart) {
|
|
return part.preferredPart == alternatePart.contentType;
|
|
}));
|
|
}
|
|
// Can be used for UIxMailPartMixedViewer, UIxMailPartMessageViewer, and UIxMailPartSignedViewer
|
|
else if (angular.isArray(part.content)) {
|
|
if (part.type == 'UIxMailPartSignedViewer' && part['supports-smime'] === 1) {
|
|
_this.signed = {
|
|
valid: part.valid,
|
|
certificate: part.certificates[part.certificates.length - 1],
|
|
message: part.message
|
|
};
|
|
}
|
|
else if (part.type == 'UIxMailPartEncryptedViewer') {
|
|
if (part.encrypted) {
|
|
_this.encrypted = {
|
|
valid: part.decrypted
|
|
};
|
|
if (part.decrypted)
|
|
_this.encrypted.message = l("This message is encrypted");
|
|
else
|
|
_this.encrypted.message = l("This message can't be decrypted. Please make sure you have uploaded your S/MIME certificate from the mail preferences module.");
|
|
}
|
|
if (part.opaqueSigned) {
|
|
_this.signed = {
|
|
valid: part.valid,
|
|
certificate: part.certificates[part.certificates.length - 1],
|
|
message: part.message
|
|
};
|
|
}
|
|
}
|
|
var winmail = _.find(part.content, function (mixedPart) {
|
|
// Ignore empty content -- that could mean a decoding error server-side.
|
|
return mixedPart.type == 'UIxMailPartTnefViewer' && mixedPart.content.length > 0;
|
|
});
|
|
|
|
if (winmail && !_.find(part.content, function (mixedPart) {
|
|
return mixedPart.type == 'UIxMailPartAlternativeViewer';
|
|
})) {
|
|
// If there's no alternate part in the message, show the winmail.dat attachment only.
|
|
// Otherwise, show all parts.
|
|
_visit(winmail);
|
|
}
|
|
else {
|
|
_.forEach(part.content, function (mixedPart) {
|
|
_visit(mixedPart);
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
if (angular.isUndefined(part.safeContent)) {
|
|
// Keep a copy of the original content
|
|
part.safeContent = part.content;
|
|
_this.$hasUnsafeContent |= (part.safeContent.indexOf(' unsafe-') > -1);
|
|
}
|
|
if (part.type == 'UIxMailPartHTMLViewer') {
|
|
part.html = true;
|
|
if (_this.$loadUnsafeContent || Message.$displayRemoteInlineImages) {
|
|
if (angular.isUndefined(part.unsafeContent)) {
|
|
part.unsafeContent = document.createElement('div');
|
|
part.unsafeContent.innerHTML = part.safeContent;
|
|
angular.forEach(['src', 'data', 'classid', 'background', 'style'], function (suffix) {
|
|
var elements = part.unsafeContent.querySelectorAll('[unsafe-' + suffix + ']'),
|
|
element,
|
|
value,
|
|
i;
|
|
for (i = 0; i < elements.length; i++) {
|
|
element = angular.element(elements[i]);
|
|
value = element.attr('unsafe-' + suffix);
|
|
element.attr(suffix, value);
|
|
element.removeAttr('unsafe-' + suffix);
|
|
}
|
|
});
|
|
_this.$hasUnsafeContent = false;
|
|
}
|
|
part.content = part.unsafeContent.innerHTML;
|
|
}
|
|
else {
|
|
part.content = part.safeContent;
|
|
}
|
|
parts.push(part);
|
|
}
|
|
else if (part.type == 'UIxMailPartICalViewer' ||
|
|
part.type == 'UIxMailPartImageViewer' ||
|
|
part.type == 'UIxMailPartLinkViewer') {
|
|
|
|
if (part.type == 'UIxMailPartImageViewer')
|
|
part.msgclass = 'msg-attachment-image';
|
|
else if (part.type == 'UIxMailPartLinkViewer')
|
|
part.msgclass = 'msg-attachment-link';
|
|
|
|
// Trusted content that can be compiled (Angularly-speaking)
|
|
part.compile = true;
|
|
if (!Object.hasOwn(part, 'shouldDisplayAttachment') || 1 == part.shouldDisplayAttachment ) {
|
|
parts.push(part);
|
|
}
|
|
}
|
|
else {
|
|
part.html = true;
|
|
part.content = part.safeContent;
|
|
parts.push(part);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
if (this.$parts)
|
|
// Use the cache
|
|
return this.$parts;
|
|
|
|
else if (this.parts)
|
|
_visit(this.parts);
|
|
|
|
|
|
// Highlight words
|
|
if (parts && this.$mailbox && this.$mailbox.getHighlightWords().length > 0) {
|
|
var i = 0, j = 0;
|
|
for (i = 0; i < parts.length; i++) {
|
|
if (parts[i]
|
|
&& parts[i].type
|
|
&& ("UIxMailPartHTMLViewer" == parts[i].type
|
|
|| "UIxMailPartTextViewer" == parts[i].type)) {
|
|
// Content
|
|
parts[i].content = this.highlightSearchTerms(parts[i].content, false);
|
|
// Title
|
|
this.subject = this.getHighlightSubject();
|
|
// From
|
|
this.from = this.getHighlightFrom();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache result
|
|
this.$parts = parts;
|
|
|
|
return parts;
|
|
};
|
|
|
|
/**
|
|
* @function highlightSearchTerms
|
|
* @memberof Message.prototype
|
|
* @desc Returns the data with highlight search
|
|
* @returns the data with highlighted search terms
|
|
*/
|
|
Message.prototype.highlightSearchTerms = function (data, encodeEntities) {
|
|
var i = 0;
|
|
if (this.$mailbox.getHighlightWords()
|
|
&& this.$mailbox.getHighlightWords().length > 0
|
|
&& data
|
|
&& -1 === data.indexOf("data-markjs")) {
|
|
var dom = document.createElement("DIV");
|
|
dom.innerHTML = encodeEntities ? data.encodeEntities() : data;
|
|
var markInstance = new Mark(dom);
|
|
markInstance.mark(this.$mailbox.getHighlightWords());
|
|
data = dom.innerHTML;
|
|
dom.remove();
|
|
} else if (encodeEntities) {
|
|
data = data.encodeEntities();
|
|
}
|
|
|
|
return data;
|
|
};
|
|
|
|
/**
|
|
* @function getHighlightSubject
|
|
* @memberof Message.prototype
|
|
* @desc Returns the subject with highlight search
|
|
* @returns the subject with highlighted search terms
|
|
*/
|
|
Message.prototype.getHighlightSubject = function () {
|
|
return this.highlightSearchTerms(this.subject, false);
|
|
};
|
|
|
|
/**
|
|
* @function getHighlightFrom
|
|
* @memberof Message.prototype
|
|
* @desc Returns the from with highlight search
|
|
* @returns the from with highlighted search terms
|
|
*/
|
|
Message.prototype.getHighlightFrom = function () {
|
|
var i = 0;
|
|
for (i = 0; i < this.from.length; i++) {
|
|
this.from[i].fullHighlighted = this.highlightSearchTerms(this.from[i].full, false);
|
|
this.from[i].nameHighlighted = this.highlightSearchTerms(this.from[i].name, false);
|
|
}
|
|
|
|
return this.from;
|
|
};
|
|
|
|
/**
|
|
* @function $editableContent
|
|
* @memberof Message.prototype
|
|
* @desc First, fetch the draft ID that corresponds to the temporary draft object on the SOGo server.
|
|
* Secondly, fetch the editable message body along with other metadata such as the recipients.
|
|
* @returns the HTML representation of the body
|
|
*/
|
|
Message.prototype.$editableContent = function () {
|
|
var _this = this;
|
|
|
|
return Message.$$resource.fetch(this.$absolutePath(), 'edit').then(function (data) {
|
|
angular.extend(_this, data);
|
|
return Message.$$resource.fetch(_this.$absolutePath({ asDraft: true }), 'edit').then(function (data) {
|
|
// Try to match a known account identity from the specified "from" address
|
|
var identity = _.find(_this.$mailbox.$account.identities, function (identity) {
|
|
return data.from && data.from.toLowerCase().indexOf(identity.email) !== -1;
|
|
});
|
|
if (identity)
|
|
data.from = identity.full;
|
|
var accountDefaults = Message.$Preferences.defaults.AuxiliaryMailAccounts[_this.$mailbox.$account.id];
|
|
if (accountDefaults.security) {
|
|
if (accountDefaults.security.alwaysSign)
|
|
data.sign = true;
|
|
if (accountDefaults.security.alwaysEncrypt)
|
|
data.encrypt = true;
|
|
}
|
|
Message.$log.debug('editable = ' + JSON.stringify(data, undefined, 2));
|
|
angular.extend(_this.editable, data);
|
|
return data.text;
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $plainContent
|
|
* @memberof Message.prototype
|
|
* @returns the a plain text representation of the subject and body
|
|
*/
|
|
Message.prototype.$plainContent = function () {
|
|
return Message.$$resource.fetch(this.$absolutePath(), 'viewplain');
|
|
};
|
|
|
|
/**
|
|
* @function addTag
|
|
* @memberof Message.prototype
|
|
* @desc Add a mail tag on the current message.
|
|
* @param {string} tag - the tag name
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.addTag = function (tag) {
|
|
var _this = this,
|
|
_tag = tag.replace(/^_\$/, '$');
|
|
return this.$mailbox.getLabels().then(function (labels) {
|
|
var reload = !_.find(labels, function (label) {
|
|
return label.imapName == _tag;
|
|
});
|
|
return _this.$addOrRemoveTag('add', tag).then(function () {
|
|
if (reload)
|
|
// Update the list of labels for the mailbox
|
|
_this.$mailbox.getLabels({ reload: true });
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function removeTag
|
|
* @memberof Message.prototype
|
|
* @desc Remove a mail tag from the current message.
|
|
* @param {string} tag - the tag name
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.removeTag = function (tag) {
|
|
return this.$addOrRemoveTag('remove', tag);
|
|
};
|
|
|
|
/**
|
|
* @function $addOrRemoveTag
|
|
* @memberof Message.prototype
|
|
* @desc Add or remove a mail tag on the current message.
|
|
* @param {string} operation - the operation name to perform
|
|
* @param {string} tag - the tag name
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.$addOrRemoveTag = function (operation, tag) {
|
|
var data = {
|
|
operation: operation,
|
|
msgUIDs: [this.uid],
|
|
flags: tag.replace(/^_\$/, '$')
|
|
};
|
|
|
|
if (tag)
|
|
return Message.$$resource.post(this.$mailbox.$id(), 'addOrRemoveLabel', data);
|
|
};
|
|
|
|
/**
|
|
* @function toggleRead
|
|
* @memberof Message.prototype
|
|
* @desc Toggle message unseen status
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.toggleRead = function () {
|
|
var _this = this;
|
|
|
|
if (this.isread)
|
|
return Message.$$resource.fetch(this.$absolutePath(), 'markMessageUnread').then(function () {
|
|
Message.$timeout(function () {
|
|
_this.isread = false;
|
|
_this.$mailbox.unseenCount++;
|
|
});
|
|
});
|
|
else
|
|
return Message.$$resource.fetch(this.$absolutePath(), 'markMessageRead').then(function () {
|
|
Message.$timeout(function () {
|
|
_this.isread = true;
|
|
_this.$mailbox.unseenCount--;
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $imipAction
|
|
* @memberof Message.prototype
|
|
* @desc Perform IMIP actions on the current message.
|
|
* @param {string} path - the path of the IMIP calendar part
|
|
* @param {string} action - the the IMIP action to perform
|
|
* @param {object} data - the delegation info
|
|
*/
|
|
Message.prototype.$imipAction = function (path, action, data) {
|
|
var _this = this;
|
|
Message.$$resource.post([this.$absolutePath(), path].join('/'), action, data).then(function (data) {
|
|
Message.$timeout(function () {
|
|
_this.$reload();
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $sendMDN
|
|
* @memberof Message.prototype
|
|
* @desc Send MDN response for current email message
|
|
*/
|
|
Message.prototype.$sendMDN = function () {
|
|
this.shouldAskReceipt = 0;
|
|
return Message.$$resource.post(this.$absolutePath(), 'sendMDN');
|
|
};
|
|
|
|
/**
|
|
* @function hasAttachments
|
|
* @memberof Message.prototype
|
|
* @returns true if there's one ore more attached files
|
|
*/
|
|
Message.prototype.hasAttachments = function (content) {
|
|
var _this = this;
|
|
|
|
return !!_.find(content || this.parts.content, function (part) {
|
|
if (angular.isArray(part.content)) {
|
|
return _this.hasAttachments(part.content);
|
|
}
|
|
return part.type == 'UIxMailPartLinkViewer' || part.type == 'UIxMailPartImageViewer';
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $deleteAttachment
|
|
* @memberof Message.prototype
|
|
* @desc Delete an attachment from a message being composed
|
|
* @param {string} filename - the filename of the attachment to delete
|
|
*/
|
|
Message.prototype.$deleteAttachment = function (filename) {
|
|
var data = { 'filename': filename };
|
|
var _this = this;
|
|
return Message.$$resource.fetch(this.$absolutePath({ asDraft: true }), 'deleteAttachment', data).then(function () {
|
|
Message.$timeout(function () {
|
|
_this.editable.attachmentAttrs = _.filter(_this.editable.attachmentAttrs, function (attachment) {
|
|
return attachment.filename != filename;
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function toggleFlag
|
|
* @memberof Message.prototype
|
|
* @desc Add or remove a the \\Flagged flag on the current message.
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.toggleFlag = function () {
|
|
var _this = this,
|
|
action = 'markMessageFlagged';
|
|
|
|
if (this.isflagged)
|
|
action = 'markMessageUnflagged';
|
|
|
|
return Message.$$resource.post(this.$absolutePath(), action).then(function (data) {
|
|
Message.$timeout(function () {
|
|
_this.isflagged = !_this.isflagged;
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function toggleThread
|
|
* @memberof Message.prototype
|
|
* @desc Collapse or expand mail thread
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.toggleThread = function () {
|
|
var _this = this,
|
|
action = 'markMessageCollapse';
|
|
|
|
if (this.collapsed)
|
|
action = 'markMessageUncollapse';
|
|
|
|
this.collapsed = !this.collapsed;
|
|
this.$mailbox.updateVisibleMessages();
|
|
|
|
return Message.$$resource.post(this.$absolutePath(), action).catch(function () {
|
|
this.collapsed = !this.collapsed;
|
|
_this.$mailbox.updateVisibleMessages();
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $isLoading
|
|
* @memberof Message.prototype
|
|
* @returns true if the Message content is still being retrieved from server after a specific delay
|
|
* @see sgMessage_STATUS
|
|
*/
|
|
Message.prototype.$isLoading = function () {
|
|
return this.$loaded == Message.STATUS.LOADING;
|
|
};
|
|
|
|
/**
|
|
* @function $reload
|
|
* @memberof Message.prototype
|
|
* @desc Fetch the viewable message body along with other metadata such as the list of attachments.
|
|
* @param {object} [options] - set {useCache: true} to use already fetched data, {raw: true} to remove web mail alteration
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.$reload = function (options) {
|
|
var _this = this, futureMessageData;
|
|
|
|
if (options && options.useCache && this.$futureMessageData) {
|
|
// The message has already been fetched.
|
|
if (!this.isread) {
|
|
if (Message.$Preferences.defaults.SOGoMailAutoMarkAsReadDelay > -1)
|
|
// Automatically mark message as read
|
|
_this.$markAsReadPromise = Message.$timeout(function () {
|
|
Message.$$resource.fetch(_this.$absolutePath(), 'markMessageRead').then(function () {
|
|
_this.isread = true;
|
|
_this.$mailbox.unseenCount--;
|
|
});
|
|
}, Message.$Preferences.defaults.SOGoMailAutoMarkAsReadDelay * 1000);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
if (options && options.raw)
|
|
futureMessageData = Message.$$resource.fetch(this.$absolutePath(options), 'viewRaw');
|
|
else
|
|
futureMessageData = Message.$$resource.fetch(this.$absolutePath(options), 'view');
|
|
|
|
return this.$unwrap(futureMessageData);
|
|
};
|
|
|
|
/**
|
|
* @function $parseMailto
|
|
* @memberof Message.prototype
|
|
* @desc Extend the editable content of the message with the
|
|
* information parsed from the specified "mailto:" link.
|
|
*/
|
|
Message.prototype.$parseMailto = function (mailto) {
|
|
var to, data = {}, match = /^mailto:([^\?]+)/.exec(mailto);
|
|
if (match) {
|
|
// Recipients
|
|
to = _.map(decodeURIComponent(match[1]).split(','), function (email) {
|
|
return '<' + email.trim() + '>';
|
|
});
|
|
data = { to: to };
|
|
}
|
|
// Subject & body
|
|
_.forEach(['subject', 'body'], function (param) {
|
|
var re = new RegExp(param + '=([^&]+)');
|
|
param = (param == 'body') ? 'text' : param;
|
|
match = re.exec(mailto);
|
|
if (match)
|
|
data[param] = decodeURIComponent(match[1]);
|
|
});
|
|
if ('html' == Message.$Preferences.defaults.SOGoMailComposeMessageType && data.text && data.text.length > 0) {
|
|
data.text = data.text.replace(/(\r\n|\n|\r)/g, '<br/>');
|
|
}
|
|
|
|
// Other Recipients
|
|
_.forEach(['cc', 'bcc'], function (param) {
|
|
var re = new RegExp(param + '=([^&]+)');
|
|
match = re.exec(mailto);
|
|
if (match)
|
|
data[param] = _.map(decodeURIComponent(match[1]).split(','), function (email) {
|
|
return '<' + email.trim() + '>';
|
|
});
|
|
});
|
|
if (!_.isEmpty(data))
|
|
angular.extend(this.editable, data);
|
|
};
|
|
|
|
/**
|
|
* @function $reply
|
|
* @memberof Message.prototype
|
|
* @desc Prepare a new Message object as a reply to the sender.
|
|
* @returns a promise of the HTTP operations
|
|
*/
|
|
Message.prototype.$reply = function () {
|
|
return this.$newDraft('reply');
|
|
};
|
|
|
|
/**
|
|
* @function $replyAll
|
|
* @memberof Message.prototype
|
|
* @desc Prepare a new Message object as a reply to the sender and all recipients.
|
|
* @returns a promise of the HTTP operations
|
|
*/
|
|
Message.prototype.$replyAll = function () {
|
|
return this.$newDraft('replyall');
|
|
};
|
|
|
|
/**
|
|
* @function $forward
|
|
* @memberof Message.prototype
|
|
* @desc Prepare a new Message object as a forward.
|
|
* @returns a promise of the HTTP operations
|
|
*/
|
|
Message.prototype.$forward = function () {
|
|
return this.$newDraft('forward');
|
|
};
|
|
|
|
/**
|
|
* @function $compose
|
|
* @memberof Message.prototype
|
|
* @desc Prepare a new Message object as a new draft from a copy of this message.
|
|
* @returns a promise of the HTTP operations
|
|
*/
|
|
Message.prototype.$compose = function () {
|
|
return this.$newDraft('compose');
|
|
};
|
|
|
|
/**
|
|
* @function $newDraft
|
|
* @memberof Message.prototype
|
|
* @desc Prepare a new Message object as a reply, a forward or a copy of the current message and associated
|
|
* to the draft mailbox.
|
|
* @see {@link Account.$newMessage}
|
|
* @see {@link Message.$editableContent}
|
|
* @see {@link Message.$reply}
|
|
* @see {@link Message.$replyAll}
|
|
* @see {@link Message.$forwad}
|
|
* @param {string} action - the HTTP action to perform on the message
|
|
* @returns a promise of the HTTP operations
|
|
*/
|
|
Message.prototype.$newDraft = function (action) {
|
|
var _this = this;
|
|
|
|
// Query server for draft folder and draft UID
|
|
return Message.$$resource.fetch(this.$absolutePath(), action).then(function (data) {
|
|
var mailbox, message;
|
|
Message.$log.debug('New ' + action + ': ' + JSON.stringify(data, undefined, 2));
|
|
mailbox = _this.$mailbox.$account.$getMailboxByPath(data.mailboxPath);
|
|
message = new Message(data.accountId, mailbox, data);
|
|
// Fetch draft initial data
|
|
return Message.$$resource.fetch(message.$absolutePath({ asDraft: true }), 'edit').then(function (data) {
|
|
Message.$log.debug('New ' + action + ': ' + JSON.stringify(data, undefined, 2) + ' original UID: ' + _this.uid);
|
|
var accountDefaults = Message.$Preferences.defaults.AuxiliaryMailAccounts[_this.$mailbox.$account.id];
|
|
if (accountDefaults.security) {
|
|
if (accountDefaults.security.alwaysSign)
|
|
data.sign = true;
|
|
if (accountDefaults.security.alwaysEncrypt)
|
|
data.encrypt = true;
|
|
}
|
|
if (data.isHTML) {
|
|
// Sanitize HTML replies to properly display quoted content in CKEditor.
|
|
// Don't use the DOM to avoid triggering any event.
|
|
var html = data.text;
|
|
html = html.replace(/<\/?html[^>]*>/g, '');
|
|
html = html.replace(/<\/?body[^>]*>/g, '');
|
|
html = html.replace(/<meta[^>]*>.*<\/meta>/g, '');
|
|
html = html.replace(/<link[^>]*>.*<\/link>/g, '');
|
|
html = html.replace(/<base[^>]*>.*<\/base>/g, '');
|
|
html = html.replace(/<title[^>]*>.*<\/title>/g, '');
|
|
data.text = html;
|
|
}
|
|
angular.extend(message.editable, data);
|
|
|
|
// We keep a reference to our original message in order to update the flags
|
|
message.origin = { message: _this, action: action };
|
|
return message;
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $save
|
|
* @memberof Message.prototype
|
|
* @desc Save the message to the server.
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.$save = function () {
|
|
var _this = this,
|
|
data = this.$omit();
|
|
|
|
Message.$log.debug('save = ' + JSON.stringify(data, undefined, 2));
|
|
|
|
return Message.$$resource.save(this.$absolutePath({ asDraft: true }), data).then(function (response) {
|
|
Message.$log.debug('save = ' + JSON.stringify(response, undefined, 2));
|
|
_this.$setUID(response.uid);
|
|
_this.$reload(); // fetch a new viewable version of the message
|
|
_this.isNew = false;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $punycode
|
|
* @memberof Message.prototype
|
|
* @desc Encode an email address string
|
|
* @returns an RFC 3492 email encoded
|
|
*/
|
|
Message.prototype.punycode = function (element) {
|
|
var re = /<(.*)>|^([\w\-\.@]+)$/gm;
|
|
var r = re.exec(element);
|
|
var puny = element;
|
|
if (r && r.length > 0 && r[1]) {
|
|
puny = r[1];
|
|
}
|
|
return element.replace(puny, punycode.toASCII(puny));
|
|
};
|
|
|
|
/**
|
|
* @function $send
|
|
* @memberof Message.prototype
|
|
* @desc Send the message.
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.$send = function () {
|
|
var _this = this,
|
|
data = this.$omit();
|
|
|
|
Message.$log.debug('send = ' + JSON.stringify(data, undefined, 2));
|
|
|
|
// Punycode
|
|
if (data.to && data.to.length > 0) {
|
|
data.to.forEach(function (element, i, arr) {
|
|
arr[i] = _this.punycode(element);
|
|
});
|
|
}
|
|
if (data.bcc && data.bcc.length > 0) {
|
|
data.bcc.forEach(function (element, i, arr) {
|
|
arr[i] = _this.punycode(element);
|
|
});
|
|
}
|
|
if (data.cc && data.cc.length > 0) {
|
|
data.cc.forEach(function (element, i, arr) {
|
|
arr[i] = _this.punycode(element);
|
|
});
|
|
}
|
|
data.from = _this.punycode(data.from);
|
|
|
|
return Message.$$resource.post(this.$absolutePath({ asDraft: true }), 'send', data).then(function (response) {
|
|
if (response.status == 'success') {
|
|
if (angular.isDefined(_this.origin)) {
|
|
if (_this.origin.action.startsWith('reply'))
|
|
_this.origin.message.isanswered = true;
|
|
else if (_this.origin.action == 'forward')
|
|
_this.origin.message.isforwarded = true;
|
|
}
|
|
return response;
|
|
}
|
|
else {
|
|
return Message.$q.reject(response.data);
|
|
}
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @function $unwrap
|
|
* @memberof Message.prototype
|
|
* @desc Unwrap a promise.
|
|
* @param {promise} futureMessageData - a promise of some of the Message's data
|
|
*/
|
|
Message.prototype.$unwrap = function (futureMessageData) {
|
|
var _this = this;
|
|
|
|
// Message is not loaded yet
|
|
this.$loaded = Message.STATUS.DELAYED_LOADING;
|
|
Message.$timeout(function () {
|
|
if (_this.$loaded != Message.STATUS.LOADED)
|
|
_this.$loaded = Message.STATUS.LOADING;
|
|
}, Message.STATUS.DELAYED_MS);
|
|
|
|
// Resolve and expose the promise
|
|
this.$futureMessageData = futureMessageData.then(function (data) {
|
|
// Calling $timeout will force Angular to refresh the view
|
|
if (!data.isRead) {
|
|
if (Message.$Preferences.defaults.SOGoMailAutoMarkAsReadDelay > -1)
|
|
// Automatically mark message as read
|
|
_this.$markAsReadPromise = Message.$timeout(function () {
|
|
Message.$$resource.fetch(_this.$absolutePath(), 'markMessageRead').then(function () {
|
|
_this.isread = true;
|
|
_this.$mailbox.unseenCount--;
|
|
});
|
|
}, Message.$Preferences.defaults.SOGoMailAutoMarkAsReadDelay * 1000);
|
|
}
|
|
else if (!_this.isread) {
|
|
// Message as already been marked read on the server
|
|
_this.isread = true;
|
|
_this.$mailbox.unseenCount--;
|
|
}
|
|
return Message.$timeout(function () {
|
|
delete _this.$parts;
|
|
_this.$loaded = Message.STATUS.LOADED;
|
|
_this.init(data);
|
|
return _this;
|
|
});
|
|
});
|
|
|
|
return this.$futureMessageData;
|
|
};
|
|
|
|
/**
|
|
* @function $omit
|
|
* @memberof Message.prototype
|
|
* @desc Return a sanitized object used to send to the server.
|
|
* @return an object literal copy of the Message instance
|
|
*/
|
|
Message.prototype.$omit = function (options) {
|
|
var message = {},
|
|
privateAttributes = options && options.privateAttributes,
|
|
source = privateAttributes ? this : this.editable;
|
|
angular.forEach(source, function (value, key) {
|
|
if (_.includes(['to', 'cc', 'bcc'], key) && !privateAttributes) {
|
|
message[key] = _.map(value, function (addr) {
|
|
return addr.toString();
|
|
});
|
|
}
|
|
else if (key != 'constructor' && key[0] != '$' || privateAttributes) {
|
|
message[key] = value;
|
|
}
|
|
});
|
|
|
|
return message;
|
|
};
|
|
|
|
/**
|
|
* @function downloadArchive
|
|
* @memberof Message.prototype
|
|
* @desc Download the current message as a zip archive
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.downloadArchive = function () {
|
|
var data, options;
|
|
|
|
data = { uids: [this.uid] };
|
|
options = { filename: this.subject + '.zip' };
|
|
|
|
return Message.$$resource.download(this.$mailbox.id, 'saveMessages', data, options);
|
|
};
|
|
|
|
/**
|
|
* @function download
|
|
* @memberof Message.prototype
|
|
* @desc Download the current message as a eml file
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.download = function () {
|
|
var options;
|
|
|
|
options = { filename: this.subject + '.eml', type: 'message/rfc822' };
|
|
return Message.$$resource.download(this.$absolutePath(), 'export', undefined, options);
|
|
};
|
|
|
|
/**
|
|
* @function downloadAttachments
|
|
* @memberof Message.prototype
|
|
* @desc Download a zip archive of all attachments
|
|
* @returns a promise of the HTTP operation
|
|
*/
|
|
Message.prototype.downloadAttachmentsArchive = function () {
|
|
var options;
|
|
|
|
options = { filename: l('attachments') + "-" + this.uid + ".zip" };
|
|
|
|
return Message.$$resource.download(this.$absolutePath(), 'archiveAttachments', null, options);
|
|
};
|
|
|
|
})();
|