/* -*- 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 ). * 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 = / 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, '
'); } // 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>/g, ''); html = html.replace(/]*>.*<\/link>/g, ''); html = html.replace(/]*>.*<\/base>/g, ''); html = html.replace(/]*>.*<\/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); }; })();