/* 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/. */ import { MailServices } from "resource:///modules/MailServices.sys.mjs"; import { cal } from "resource:///modules/calendar/calUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineLazyGetter(lazy, "log", () => { return console.createInstance({ prefix: "calendar", maxLogLevel: "Warn", maxLogLevelPref: "calendar.loglevel", }); }); ChromeUtils.defineLazyGetter( lazy, "l10n", () => new Localization(["calendar/calendar-itip.ftl"], true) ); /** * CalItipEmailTransport is used to send iTIP messages via email. Outside * callers should use the static `createInstance()` method instead of this * constructor directly. */ export class CalItipEmailTransport { wrappedJSObject = this; QueryInterface = ChromeUtils.generateQI(["calIItipTransport"]); classID = Components.ID("{d4d7b59e-c9e0-4a7a-b5e8-5958f85515f0}"); mSenderAddress = null; constructor(defaultAccount, defaultIdentity) { this.mDefaultAccount = defaultAccount; this.mDefaultIdentity = defaultIdentity; } get scheme() { return "mailto"; } get type() { return "email"; } get senderAddress() { return this.mSenderAddress; } set senderAddress(aValue) { this.mSenderAddress = aValue; } /** * Creates a new calIItipTransport instance configured with the default * account and identity if available. If not available or an error occurs, an * instance that cannot send any items out is returned. */ static createInstance() { try { const defaultAccount = MailServices.accounts.defaultAccount; let defaultIdentity = defaultAccount ? defaultAccount.defaultIdentity : null; if (!defaultIdentity) { // If there isn't a default identity (i.e Local Folders is your // default identity, then go ahead and use the first available // identity. const allIdentities = MailServices.accounts.allIdentities; if (allIdentities.length > 0) { defaultIdentity = allIdentities[0]; } } if (defaultAccount && defaultIdentity) { return new CalItipEmailTransport(defaultAccount, defaultIdentity); } } catch (ex) { // Fall through to below. } lazy.log.debug("CalITipEmailTransport.createInstance: No XPCOM Mail available."); return new CalItipNoEmailTransport(); } _prepareItems(aItipItem, aFromAttendee) { const item = aItipItem.getItemList()[0]; // Get ourselves some default text - when we handle organizer properly // We'll need a way to configure the Common Name attribute and we should // use it here rather than the email address const summary = item.getProperty("SUMMARY") || ""; let subject = ""; let body = ""; switch (aItipItem.responseMethod) { case "REQUEST": { const usePrefixes = Services.prefs.getBoolPref( "calendar.itip.useInvitationSubjectPrefixes", true ); if (usePrefixes) { const seq = item.getProperty("SEQUENCE"); const subjectId = seq && seq > 0 ? "itip-request-updated-subject" : "itip-request-subject"; subject = lazy.l10n.formatValueSync(subjectId, { summary }); } else { subject = summary; } body = lazy.l10n.formatValueSync("itip-request-body", { organizer: item.organizer ? item.organizer.toString() : "", summary, }); break; } case "CANCEL": { subject = lazy.l10n.formatValueSync("itip-cancel-subject", { summary }); body = lazy.l10n.formatValueSync("itip-cancel-body", { organizer: item.organizer ? item.organizer.toString() : "", summary, }); break; } case "DECLINECOUNTER": { subject = lazy.l10n.formatValueSync("itip-decline-counter-subject", { summary }); body = lazy.l10n.formatValueSync("itip-decline-counter-body", { organizer: item.organizer ? item.organizer.toString() : "", summary, }); break; } case "REPLY": { // Get my participation status if (!aFromAttendee && aItipItem.identity) { aFromAttendee = item.getAttendeeById(cal.email.prependMailTo(aItipItem.identity)); } if (!aFromAttendee) { // should not happen anymore return false; } // work around BUG 351589, the below just removes RSVP: aItipItem.setAttendeeStatus(aFromAttendee.id, aFromAttendee.participationStatus); const myPartStat = aFromAttendee.participationStatus; const name = aFromAttendee.toString(); // Generate proper body from my participation status let subjectId, bodyId; switch (myPartStat) { case "ACCEPTED": subjectId = "itip-reply-subject-accept"; bodyId = "itip-reply-body-accept"; break; case "TENTATIVE": subjectId = "itip-reply-subject-tentative"; bodyId = "itip-reply-body-accept"; break; case "DECLINED": subjectId = "itip-reply-subject-decline"; bodyId = "itip-reply-body-decline"; break; default: subjectId = "itip-reply-subject"; bodyId = "itip-reply-body-accept"; break; } subject = lazy.l10n.formatValueSync(subjectId, { summary }); body = lazy.l10n.formatValueSync(bodyId, { attendee: name }); break; } } // Retrieve the COMMENT property const comment = item.getProperty("COMMENT"); // Append COMMENT field to the body if (comment) { body += "\n\n" + lazy.l10n.formatValueSync("imip-html-comment") + "\n" + comment; } return { subject, body, }; } _sendXpcomMail(aToList, aSubject, aBody, aItipItem) { const { identity, account } = this.getIdentityAndAccount(aItipItem); switch (aItipItem.autoResponse) { case Ci.calIItipItem.USER: { lazy.log.debug("sendXpcomMail: Found USER autoResponse type."); // We still need this as a last resort if a user just deletes or // drags an invitation related event let parent = Services.wm.getMostRecentWindow(null); if (parent.closed) { parent = cal.window.getCalendarWindow(); } const cancelled = Services.prompt.confirmEx( parent, lazy.l10n.formatValueSync("imip-send-mail-title"), lazy.l10n.formatValueSync("imip-send-mail-text"), Services.prompt.STD_YES_NO_BUTTONS, null, null, null, null, {} ); if (cancelled) { lazy.log.debug("sendXpcomMail: Sending of invitation email aborted by user!"); break; } // else go on with auto sending for now } // falls through intended case Ci.calIItipItem.AUTO: { // don't show log message in case of falling through if (aItipItem.autoResponse == Ci.calIItipItem.AUTO) { lazy.log.debug("sendXpcomMail: Found AUTO autoResponse type."); } const cbEmail = function (aVal) { const email = cal.email.getAttendeeEmail(aVal, true); if (!email.length) { lazy.log.debug( "sendXpcomMail: Invalid recipient for email transport: " + aVal.toString() ); } return email; }; const toMap = aToList.map(cbEmail).filter(value => value.length); if (toMap.length < aToList.length) { // at least one invalid recipient, so we skip sending for this message return false; } const toList = toMap.join(", "); const composeUtils = Cc["@mozilla.org/messengercompose/computils;1"].createInstance( Ci.nsIMsgCompUtils ); const messageId = composeUtils.msgGenerateMessageId(identity, null); const mailFile = this._createTempImipFile( toList, aSubject, aBody, aItipItem, identity, messageId ); // compose fields for message: from/to etc need to be specified both here and in the file const composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance( Ci.nsIMsgCompFields ); composeFields.to = toList; const mailfrom = identity.fullName.length ? identity.fullName + " <" + identity.email + ">" : identity.email; composeFields.from = cal.email.validateRecipientList(mailfrom) == mailfrom ? mailfrom : identity.email; composeFields.replyTo = identity.replyTo; composeFields.organization = identity.organization; composeFields.messageId = messageId; let validRecipients; if (identity.doCc) { validRecipients = cal.email.validateRecipientList(identity.doCcList); if (validRecipients != "") { composeFields.cc = validRecipients; } } if (identity.doBcc) { validRecipients = cal.email.validateRecipientList(identity.doBccList); if (validRecipients != "") { composeFields.bcc = validRecipients; } } // xxx todo: add send/progress UI, maybe recycle // "@mozilla.org/messengercompose/composesendlistener;1" // and/or "chrome://messenger/content/messengercompose/sendProgress.xhtml" // i.e. bug 432662 this.getMsgSend().sendMessageFile( identity, account.key, composeFields, mailFile, true, // deleteSendFileOnCompletion false, // digest_p Services.io.offline ? Ci.nsIMsgSend.nsMsgQueueForLater : Ci.nsIMsgSend.nsMsgDeliverNow, null, // nsIMsgDBHdr msgToReplace null, // nsIMsgSendListener aListener "" ); // password return true; } case Ci.calIItipItem.NONE: { // we shouldn't get here, as we stopped processing in this case // earlier in checkAndSend in calItipUtils.sys.mjs lazy.log.debug("sendXpcomMail: Found NONE autoResponse type."); break; } default: { // Also of this case should have been taken care at the same place throw new Error("sendXpcomMail: Unknown autoResponse type: " + aItipItem.autoResponse); } } return false; } _createTempImipFile(aToList, aSubject, aBody, aItipItem, aIdentity, aMessageId) { const itemList = aItipItem.getItemList(); const serializer = Cc["@mozilla.org/calendar/ics-serializer;1"].createInstance( Ci.calIIcsSerializer ); serializer.addItems(itemList); const methodProp = cal.icsService.createIcalProperty("METHOD"); methodProp.value = aItipItem.responseMethod; serializer.addProperty(methodProp); const calText = serializer.serializeToString(); const utf8CalText = cal.invitation.encodeUTF8(calText); // Home-grown mail composition; I'd love to use nsIMimeEmitter, but it's not clear to me whether // it can cope with nested attachments, // like multipart/alternative with enclosed text/calendar and text/plain. let mailText = cal.invitation.getHeaderSection(aMessageId, aIdentity, aToList, aSubject); mailText += 'Content-type: multipart/mixed; boundary="Boundary_(ID_qyG4ZdjoAsiZ+Jo19dCbWQ)"\r\n' + "\r\n\r\n" + "--Boundary_(ID_qyG4ZdjoAsiZ+Jo19dCbWQ)\r\n" + "Content-type: multipart/alternative;\r\n" + ' boundary="Boundary_(ID_ryU4ZdJoASiZ+Jo21dCbwA)"\r\n' + "\r\n\r\n" + "--Boundary_(ID_ryU4ZdJoASiZ+Jo21dCbwA)\r\n" + "Content-type: text/plain; charset=UTF-8\r\n" + "Content-transfer-encoding: 8BIT\r\n" + "\r\n" + cal.invitation.encodeUTF8(aBody) + "\r\n\r\n\r\n" + "--Boundary_(ID_ryU4ZdJoASiZ+Jo21dCbwA)\r\n" + "Content-type: text/calendar; method=" + aItipItem.responseMethod + "; charset=UTF-8\r\n" + "Content-transfer-encoding: 8BIT\r\n" + "\r\n" + utf8CalText + "\r\n\r\n" + "--Boundary_(ID_ryU4ZdJoASiZ+Jo21dCbwA)--\r\n" + "\r\n" + "--Boundary_(ID_qyG4ZdjoAsiZ+Jo19dCbWQ)\r\n" + "Content-type: application/ics; name=invite.ics\r\n" + "Content-transfer-encoding: 8BIT\r\n" + "Content-disposition: attachment; filename=invite.ics\r\n" + "\r\n" + utf8CalText + "\r\n\r\n" + "--Boundary_(ID_qyG4ZdjoAsiZ+Jo19dCbWQ)--\r\n"; lazy.log.debug("mail text:\n" + mailText); const tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile); tempFile.append("itipTemp"); tempFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0600", 8)); const outputStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( Ci.nsIFileOutputStream ); // Let's write the file - constants from file-utils.js const MODE_WRONLY = 0x02; const MODE_CREATE = 0x08; const MODE_TRUNCATE = 0x20; outputStream.init(tempFile, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, parseInt("0600", 8), 0); outputStream.write(mailText, mailText.length); outputStream.close(); lazy.log.debug(`Created itipTemp file: ${tempFile.path}`); return tempFile; } /** * Provides a new nsIMsgSend instance to use when sending the message. This * method can be overridden in child classes for testing or other purposes. */ getMsgSend() { return Cc["@mozilla.org/messengercompose/send;1"].createInstance(Ci.nsIMsgSend); } /** * Provides the identity and account to use when sending iTIP emails. By * default prefers whatever the item's calendar is configured to use or the * default configuration when not set. This method can be overridden to change * that behaviour. * * @param {calIItipItem} aItipItem * @returns {object} - An object containing a property for the identity and * one for the account. */ getIdentityAndAccount(aItipItem) { let identity; let account; if (aItipItem.targetCalendar) { identity = aItipItem.targetCalendar.getProperty("imip.identity"); if (identity) { identity = identity.QueryInterface(Ci.nsIMsgIdentity); account = aItipItem.targetCalendar .getProperty("imip.account") .QueryInterface(Ci.nsIMsgAccount); } else { lazy.log.warn("No email identity configured for calendar " + aItipItem.targetCalendar.name); } } if (!identity) { // use some default identity/account: identity = this.mDefaultIdentity; account = this.mDefaultAccount; } return { identity, account }; } sendItems(aRecipients, aItipItem, aFromAttendee) { lazy.log.debug("sendItems: Preparing to send an invitation email..."); const items = this._prepareItems(aItipItem, aFromAttendee); if (items === false) { return false; } return this._sendXpcomMail(aRecipients, items.subject, items.body, aItipItem); } } /** * CalItipNoEmailTransport is a transport used in place of CalItipEmaiTransport * when we are unable to send messages due to missing configuration. */ class CalItipNoEmailTransport extends CalItipEmailTransport { wrappedJSObject = this; QueryInterface = ChromeUtils.generateQI(["calIItipTransport"]); sendItems() { return false; } } /** * CalItipDefaultEmailTransport always uses the identity and account provided * as default instead of the one configured for the calendar. */ export class CalItipDefaultEmailTransport extends CalItipEmailTransport { getIdentityAndAccount() { return { identity: this.mDefaultIdentity, account: this.mDefaultAccount }; } }