/* 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/. */ var EXPORTED_SYMBOLS = ["CalItipEmailTransport"]; var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); var { ltn } = ChromeUtils.import("resource:///modules/calendar/ltnInvitationUtils.jsm"); function CalItipEmailTransport() { this.wrappedJSObject = this; this._initEmailTransport(); } CalItipEmailTransport.prototype = { QueryInterface: ChromeUtils.generateQI(["calIItipTransport"]), classID: Components.ID("{d4d7b59e-c9e0-4a7a-b5e8-5958f85515f0}"), mHasXpcomMail: false, mDefaultAccount: null, mDefaultIdentity: null, mDefaultSmtpServer: null, get scheme() { return "mailto"; }, get type() { return "email"; }, mSenderAddress: null, get senderAddress() { return this.mSenderAddress; }, set senderAddress(aValue) { this.mSenderAddress = aValue; }, sendItems(aRecipients, aItipItem) { if (this.mHasXpcomMail) { cal.LOG("sendItems: Preparing to send an invitation email..."); let items = this._prepareItems(aItipItem); if (items === false) { return false; } return this._sendXpcomMail(aRecipients, items.subject, items.body, aItipItem); } // sending xpcom mail is not available if no identity has been set throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); }, _prepareItems(aItipItem) { let 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 let summary = item.getProperty("SUMMARY") || ""; let subject = ""; let body = ""; switch (aItipItem.responseMethod) { case "REQUEST": { let usePrefixes = Services.prefs.getBoolPref( "calendar.itip.useInvitationSubjectPrefixes", true ); if (usePrefixes) { let seq = item.getProperty("SEQUENCE"); let subjectKey = seq && seq > 0 ? "itipRequestUpdatedSubject2" : "itipRequestSubject2"; subject = cal.l10n.getLtnString(subjectKey, [summary]); } else { subject = summary; } body = cal.l10n.getLtnString("itipRequestBody", [ item.organizer ? item.organizer.toString() : "", summary, ]); break; } case "CANCEL": { subject = cal.l10n.getLtnString("itipCancelSubject2", [summary]); body = cal.l10n.getLtnString("itipCancelBody", [ item.organizer ? item.organizer.toString() : "", summary, ]); break; } case "DECLINECOUNTER": { subject = cal.l10n.getLtnString("itipDeclineCounterSubject", [summary]); body = cal.l10n.getLtnString("itipDeclineCounterBody", [ item.organizer ? item.organizer.toString() : "", summary, ]); break; } case "REPLY": { // Get my participation status let att = cal.itip.getInvitedAttendee(item, aItipItem.targetCalendar); if (!att && aItipItem.identity) { att = item.getAttendeeById(cal.email.prependMailTo(aItipItem.identity)); } if (!att) { // should not happen anymore return false; } // work around BUG 351589, the below just removes RSVP: aItipItem.setAttendeeStatus(att.id, att.participationStatus); let myPartStat = att.participationStatus; let name = att.toString(); // Generate proper body from my participation status let subjectKey, bodyKey; switch (myPartStat) { case "ACCEPTED": subjectKey = "itipReplySubjectAccept2"; bodyKey = "itipReplyBodyAccept"; break; case "TENTATIVE": subjectKey = "itipReplySubjectTentative2"; bodyKey = "itipReplyBodyAccept"; break; case "DECLINED": subjectKey = "itipReplySubjectDecline2"; bodyKey = "itipReplyBodyDecline"; break; default: subjectKey = "itipReplySubject2"; bodyKey = "itipReplyBodyAccept"; break; } subject = cal.l10n.getLtnString(subjectKey, [summary]); body = cal.l10n.getLtnString(bodyKey, [name]); break; } } return { subject, body, }; }, _initEmailTransport() { this.mHasXpcomMail = true; try { this.mDefaultSmtpServer = MailServices.smtp.defaultServer; this.mDefaultAccount = MailServices.accounts.defaultAccount; this.mDefaultIdentity = this.mDefaultAccount ? this.mDefaultAccount.defaultIdentity : null; if (!this.mDefaultIdentity) { // If there isn't a default identity (i.e Local Folders is your // default identity, then go ahead and use the first available // identity. let allIdentities = MailServices.accounts.allIdentities; if (allIdentities.length > 0) { this.mDefaultIdentity = allIdentities[0]; } else { // If there are no identities, then we are in the same // situation as if we didn't have Xpcom Mail. this.mHasXpcomMail = false; cal.LOG("initEmailService: No XPCOM Mail available"); } } } catch (ex) { // Then we must resort to operating system specific means this.mHasXpcomMail = false; } }, _sendXpcomMail(aToList, aSubject, aBody, aItipItem) { let identity = null; 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 { cal.WARN("No email identity configured for calendar " + aItipItem.targetCalendar.name); } } if (!identity) { // use some default identity/account: identity = this.mDefaultIdentity; account = this.mDefaultAccount; if (!account || !identity) { throw new Error("sendXpcomMail: No usable account and identity found"); } } switch (aItipItem.autoResponse) { case Ci.calIItipItem.USER: { cal.LOG("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(); } let cancelled = Services.prompt.confirmEx( parent, cal.l10n.getLtnString("imipSendMail.title"), cal.l10n.getLtnString("imipSendMail.text"), Services.prompt.STD_YES_NO_BUTTONS, null, null, null, null, {} ); if (cancelled) { cal.LOG("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) { cal.LOG("sendXpcomMail: Found AUTO autoResponse type."); } let cbEmail = function(aVal, aInd, aArr) { let email = cal.email.getAttendeeEmail(aVal, true); if (!email.length) { cal.LOG("sendXpcomMail: Invalid recipient for email transport: " + aVal.toString()); } return email; }; let 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; } let toList = toMap.join(", "); let composeUtils = Cc["@mozilla.org/messengercompose/computils;1"].createInstance( Ci.nsIMsgCompUtils ); let messageId = composeUtils.msgGenerateMessageId(identity); let mailFile = this._createTempImipFile( toList, aSubject, aBody, aItipItem, identity, messageId ); if (mailFile) { // compose fields for message: from/to etc need to be specified both here and in the file let composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance( Ci.nsIMsgCompFields ); composeFields.to = toList; let 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 != "") { // eslint-disable-next-line id-length 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 let msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(Ci.nsIMsgSend); msgSend.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 null, // nsIMsgStatusFeedback aStatusFeedback "" ); // password return true; } break; } case Ci.calIItipItem.NONE: { // we shouldn't get here, as we stopped processing in this case // earlier in checkAndSend in calItipUtils.jsm cal.LOG("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) { try { let itemList = aItipItem.getItemList(); let serializer = Cc["@mozilla.org/calendar/ics-serializer;1"].createInstance( Ci.calIIcsSerializer ); serializer.addItems(itemList); let methodProp = cal.getIcsService().createIcalProperty("METHOD"); methodProp.value = aItipItem.responseMethod; serializer.addProperty(methodProp); let calText = serializer.serializeToString(); let utf8CalText = ltn.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 = ltn.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" + ltn.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"; cal.LOG("mail text:\n" + mailText); let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile); tempFile.append("itipTemp"); tempFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0600", 8)); let 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(); cal.LOG("_createTempImipFile path: " + tempFile.path); return tempFile; } catch (exc) { cal.ASSERT(false, exc); return null; } }, };