1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5var EXPORTED_SYMBOLS = ["CalItipEmailTransport", "CalItipDefaultEmailTransport"]; 6 7var { MailServices } = ChromeUtils.import("resource:///modules/MailServices.jsm"); 8var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); 9var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 10 11/** 12 * CalItipEmailTransport is used to send iTIP messages via email. Outside 13 * callers should use the static `createInstance()` method instead of this 14 * constructor directly. 15 */ 16class CalItipEmailTransport { 17 wrappedJSObject = this; 18 QueryInterface = ChromeUtils.generateQI(["calIItipTransport"]); 19 classID = Components.ID("{d4d7b59e-c9e0-4a7a-b5e8-5958f85515f0}"); 20 21 mSenderAddress = null; 22 23 constructor(defaultAccount, defaultIdentity) { 24 this.mDefaultAccount = defaultAccount; 25 this.mDefaultIdentity = defaultIdentity; 26 } 27 28 get scheme() { 29 return "mailto"; 30 } 31 32 get type() { 33 return "email"; 34 } 35 36 get senderAddress() { 37 return this.mSenderAddress; 38 } 39 40 set senderAddress(aValue) { 41 this.mSenderAddress = aValue; 42 } 43 44 /** 45 * Creates a new calIItipTransport instance configured with the default 46 * account and identity if available. If not available or an error occurs, an 47 * instance that cannot send any items out is returned. 48 */ 49 static createInstance() { 50 try { 51 let defaultAccount = MailServices.accounts.defaultAccount; 52 let defaultIdentity = defaultAccount ? defaultAccount.defaultIdentity : null; 53 54 if (!defaultIdentity) { 55 // If there isn't a default identity (i.e Local Folders is your 56 // default identity, then go ahead and use the first available 57 // identity. 58 let allIdentities = MailServices.accounts.allIdentities; 59 if (allIdentities.length > 0) { 60 defaultIdentity = allIdentities[0]; 61 } 62 } 63 64 if (defaultAccount && defaultIdentity) { 65 return new CalItipEmailTransport(defaultAccount, defaultIdentity); 66 } 67 } catch (ex) { 68 // Fall through to below. 69 } 70 71 cal.LOG("CalITipEmailTransport.createInstance: No XPCOM Mail available."); 72 return new CalItipNoEmailTransport(); 73 } 74 75 _prepareItems(aItipItem, aFromAttendee) { 76 let item = aItipItem.getItemList()[0]; 77 78 // Get ourselves some default text - when we handle organizer properly 79 // We'll need a way to configure the Common Name attribute and we should 80 // use it here rather than the email address 81 82 let summary = item.getProperty("SUMMARY") || ""; 83 let subject = ""; 84 let body = ""; 85 switch (aItipItem.responseMethod) { 86 case "REQUEST": { 87 let usePrefixes = Services.prefs.getBoolPref( 88 "calendar.itip.useInvitationSubjectPrefixes", 89 true 90 ); 91 if (usePrefixes) { 92 let seq = item.getProperty("SEQUENCE"); 93 let subjectKey = seq && seq > 0 ? "itipRequestUpdatedSubject2" : "itipRequestSubject2"; 94 subject = cal.l10n.getLtnString(subjectKey, [summary]); 95 } else { 96 subject = summary; 97 } 98 body = cal.l10n.getLtnString("itipRequestBody", [ 99 item.organizer ? item.organizer.toString() : "", 100 summary, 101 ]); 102 break; 103 } 104 case "CANCEL": { 105 subject = cal.l10n.getLtnString("itipCancelSubject2", [summary]); 106 body = cal.l10n.getLtnString("itipCancelBody", [ 107 item.organizer ? item.organizer.toString() : "", 108 summary, 109 ]); 110 break; 111 } 112 case "DECLINECOUNTER": { 113 subject = cal.l10n.getLtnString("itipDeclineCounterSubject", [summary]); 114 body = cal.l10n.getLtnString("itipDeclineCounterBody", [ 115 item.organizer ? item.organizer.toString() : "", 116 summary, 117 ]); 118 break; 119 } 120 case "REPLY": { 121 // Get my participation status 122 if (!aFromAttendee && aItipItem.identity) { 123 aFromAttendee = item.getAttendeeById(cal.email.prependMailTo(aItipItem.identity)); 124 } 125 if (!aFromAttendee) { 126 // should not happen anymore 127 return false; 128 } 129 130 // work around BUG 351589, the below just removes RSVP: 131 aItipItem.setAttendeeStatus(aFromAttendee.id, aFromAttendee.participationStatus); 132 let myPartStat = aFromAttendee.participationStatus; 133 let name = aFromAttendee.toString(); 134 135 // Generate proper body from my participation status 136 let subjectKey, bodyKey; 137 switch (myPartStat) { 138 case "ACCEPTED": 139 subjectKey = "itipReplySubjectAccept2"; 140 bodyKey = "itipReplyBodyAccept"; 141 break; 142 case "TENTATIVE": 143 subjectKey = "itipReplySubjectTentative2"; 144 bodyKey = "itipReplyBodyAccept"; 145 break; 146 case "DECLINED": 147 subjectKey = "itipReplySubjectDecline2"; 148 bodyKey = "itipReplyBodyDecline"; 149 break; 150 default: 151 subjectKey = "itipReplySubject2"; 152 bodyKey = "itipReplyBodyAccept"; 153 break; 154 } 155 subject = cal.l10n.getLtnString(subjectKey, [summary]); 156 body = cal.l10n.getLtnString(bodyKey, [name]); 157 break; 158 } 159 } 160 161 return { 162 subject, 163 body, 164 }; 165 } 166 167 _sendXpcomMail(aToList, aSubject, aBody, aItipItem) { 168 let { identity, account } = this.getIdentityAndAccount(aItipItem); 169 170 switch (aItipItem.autoResponse) { 171 case Ci.calIItipItem.USER: { 172 cal.LOG("sendXpcomMail: Found USER autoResponse type."); 173 // We still need this as a last resort if a user just deletes or 174 // drags an invitation related event 175 let parent = Services.wm.getMostRecentWindow(null); 176 if (parent.closed) { 177 parent = cal.window.getCalendarWindow(); 178 } 179 let cancelled = Services.prompt.confirmEx( 180 parent, 181 cal.l10n.getLtnString("imipSendMail.title"), 182 cal.l10n.getLtnString("imipSendMail.text"), 183 Services.prompt.STD_YES_NO_BUTTONS, 184 null, 185 null, 186 null, 187 null, 188 {} 189 ); 190 if (cancelled) { 191 cal.LOG("sendXpcomMail: Sending of invitation email aborted by user!"); 192 break; 193 } // else go on with auto sending for now 194 } 195 // falls through intended 196 case Ci.calIItipItem.AUTO: { 197 // don't show log message in case of falling through 198 if (aItipItem.autoResponse == Ci.calIItipItem.AUTO) { 199 cal.LOG("sendXpcomMail: Found AUTO autoResponse type."); 200 } 201 let cbEmail = function(aVal, aInd, aArr) { 202 let email = cal.email.getAttendeeEmail(aVal, true); 203 if (!email.length) { 204 cal.LOG("sendXpcomMail: Invalid recipient for email transport: " + aVal.toString()); 205 } 206 return email; 207 }; 208 let toMap = aToList.map(cbEmail).filter(value => value.length); 209 if (toMap.length < aToList.length) { 210 // at least one invalid recipient, so we skip sending for this message 211 return false; 212 } 213 let toList = toMap.join(", "); 214 let composeUtils = Cc["@mozilla.org/messengercompose/computils;1"].createInstance( 215 Ci.nsIMsgCompUtils 216 ); 217 let messageId = composeUtils.msgGenerateMessageId(identity); 218 let mailFile = this._createTempImipFile( 219 toList, 220 aSubject, 221 aBody, 222 aItipItem, 223 identity, 224 messageId 225 ); 226 if (mailFile) { 227 // compose fields for message: from/to etc need to be specified both here and in the file 228 let composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance( 229 Ci.nsIMsgCompFields 230 ); 231 composeFields.to = toList; 232 let mailfrom = identity.fullName.length 233 ? identity.fullName + " <" + identity.email + ">" 234 : identity.email; 235 composeFields.from = 236 cal.email.validateRecipientList(mailfrom) == mailfrom ? mailfrom : identity.email; 237 composeFields.replyTo = identity.replyTo; 238 composeFields.organization = identity.organization; 239 composeFields.messageId = messageId; 240 let validRecipients; 241 if (identity.doCc) { 242 validRecipients = cal.email.validateRecipientList(identity.doCcList); 243 if (validRecipients != "") { 244 // eslint-disable-next-line id-length 245 composeFields.cc = validRecipients; 246 } 247 } 248 if (identity.doBcc) { 249 validRecipients = cal.email.validateRecipientList(identity.doBccList); 250 if (validRecipients != "") { 251 composeFields.bcc = validRecipients; 252 } 253 } 254 255 // xxx todo: add send/progress UI, maybe recycle 256 // "@mozilla.org/messengercompose/composesendlistener;1" 257 // and/or "chrome://messenger/content/messengercompose/sendProgress.xhtml" 258 // i.e. bug 432662 259 let msgSend = Cc["@mozilla.org/messengercompose/send;1"].createInstance(Ci.nsIMsgSend); 260 msgSend.sendMessageFile( 261 identity, 262 account.key, 263 composeFields, 264 mailFile, 265 true, // deleteSendFileOnCompletion 266 false, // digest_p 267 Services.io.offline ? Ci.nsIMsgSend.nsMsgQueueForLater : Ci.nsIMsgSend.nsMsgDeliverNow, 268 null, // nsIMsgDBHdr msgToReplace 269 null, // nsIMsgSendListener aListener 270 null, // nsIMsgStatusFeedback aStatusFeedback 271 "" 272 ); // password 273 return true; 274 } 275 break; 276 } 277 case Ci.calIItipItem.NONE: { 278 // we shouldn't get here, as we stopped processing in this case 279 // earlier in checkAndSend in calItipUtils.jsm 280 cal.LOG("sendXpcomMail: Found NONE autoResponse type."); 281 break; 282 } 283 default: { 284 // Also of this case should have been taken care at the same place 285 throw new Error("sendXpcomMail: Unknown autoResponse type: " + aItipItem.autoResponse); 286 } 287 } 288 return false; 289 } 290 291 _createTempImipFile(aToList, aSubject, aBody, aItipItem, aIdentity, aMessageId) { 292 try { 293 let itemList = aItipItem.getItemList(); 294 let serializer = Cc["@mozilla.org/calendar/ics-serializer;1"].createInstance( 295 Ci.calIIcsSerializer 296 ); 297 serializer.addItems(itemList); 298 let methodProp = cal.getIcsService().createIcalProperty("METHOD"); 299 methodProp.value = aItipItem.responseMethod; 300 serializer.addProperty(methodProp); 301 let calText = serializer.serializeToString(); 302 let utf8CalText = cal.invitation.encodeUTF8(calText); 303 304 // Home-grown mail composition; I'd love to use nsIMimeEmitter, but it's not clear to me whether 305 // it can cope with nested attachments, 306 // like multipart/alternative with enclosed text/calendar and text/plain. 307 let mailText = cal.invitation.getHeaderSection(aMessageId, aIdentity, aToList, aSubject); 308 mailText += 309 'Content-type: multipart/mixed; boundary="Boundary_(ID_qyG4ZdjoAsiZ+Jo19dCbWQ)"\r\n' + 310 "\r\n\r\n" + 311 "--Boundary_(ID_qyG4ZdjoAsiZ+Jo19dCbWQ)\r\n" + 312 "Content-type: multipart/alternative;\r\n" + 313 ' boundary="Boundary_(ID_ryU4ZdJoASiZ+Jo21dCbwA)"\r\n' + 314 "\r\n\r\n" + 315 "--Boundary_(ID_ryU4ZdJoASiZ+Jo21dCbwA)\r\n" + 316 "Content-type: text/plain; charset=UTF-8\r\n" + 317 "Content-transfer-encoding: 8BIT\r\n" + 318 "\r\n" + 319 cal.invitation.encodeUTF8(aBody) + 320 "\r\n\r\n\r\n" + 321 "--Boundary_(ID_ryU4ZdJoASiZ+Jo21dCbwA)\r\n" + 322 "Content-type: text/calendar; method=" + 323 aItipItem.responseMethod + 324 "; charset=UTF-8\r\n" + 325 "Content-transfer-encoding: 8BIT\r\n" + 326 "\r\n" + 327 utf8CalText + 328 "\r\n\r\n" + 329 "--Boundary_(ID_ryU4ZdJoASiZ+Jo21dCbwA)--\r\n" + 330 "\r\n" + 331 "--Boundary_(ID_qyG4ZdjoAsiZ+Jo19dCbWQ)\r\n" + 332 "Content-type: application/ics; name=invite.ics\r\n" + 333 "Content-transfer-encoding: 8BIT\r\n" + 334 "Content-disposition: attachment; filename=invite.ics\r\n" + 335 "\r\n" + 336 utf8CalText + 337 "\r\n\r\n" + 338 "--Boundary_(ID_qyG4ZdjoAsiZ+Jo19dCbWQ)--\r\n"; 339 cal.LOG("mail text:\n" + mailText); 340 341 let tempFile = Services.dirsvc.get("TmpD", Ci.nsIFile); 342 tempFile.append("itipTemp"); 343 tempFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0600", 8)); 344 345 let outputStream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( 346 Ci.nsIFileOutputStream 347 ); 348 // Let's write the file - constants from file-utils.js 349 const MODE_WRONLY = 0x02; 350 const MODE_CREATE = 0x08; 351 const MODE_TRUNCATE = 0x20; 352 outputStream.init( 353 tempFile, 354 MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, 355 parseInt("0600", 8), 356 0 357 ); 358 outputStream.write(mailText, mailText.length); 359 outputStream.close(); 360 361 cal.LOG("_createTempImipFile path: " + tempFile.path); 362 return tempFile; 363 } catch (exc) { 364 cal.ASSERT(false, exc); 365 return null; 366 } 367 } 368 369 /** 370 * Provides the identity and account to use when sending iTIP emails. By 371 * default prefers whatever the item's calendar is configured to use or the 372 * default configuration when not set. This method can be overriden to change 373 * that behaviour. 374 * 375 * @param {calIItipItem} aItipItem 376 * @returns {object} - An object containing a property for the identity and 377 * one for the account. 378 */ 379 getIdentityAndAccount(aItipItem) { 380 let identity; 381 let account; 382 if (aItipItem.targetCalendar) { 383 identity = aItipItem.targetCalendar.getProperty("imip.identity"); 384 if (identity) { 385 identity = identity.QueryInterface(Ci.nsIMsgIdentity); 386 account = aItipItem.targetCalendar 387 .getProperty("imip.account") 388 .QueryInterface(Ci.nsIMsgAccount); 389 } else { 390 cal.WARN("No email identity configured for calendar " + aItipItem.targetCalendar.name); 391 } 392 } 393 if (!identity) { 394 // use some default identity/account: 395 identity = this.mDefaultIdentity; 396 account = this.mDefaultAccount; 397 } 398 return { identity, account }; 399 } 400 401 sendItems(aRecipients, aItipItem, aFromAttendee) { 402 cal.LOG("sendItems: Preparing to send an invitation email..."); 403 let items = this._prepareItems(aItipItem, aFromAttendee); 404 if (items === false) { 405 return false; 406 } 407 408 return this._sendXpcomMail(aRecipients, items.subject, items.body, aItipItem); 409 } 410} 411 412/** 413 * CalItipNoEmailTransport is a transport used in place of CalItipEmaiTransport 414 * when we are unable to send messages due to missing configuration. 415 */ 416class CalItipNoEmailTransport extends CalItipEmailTransport { 417 wrappedJSObject = this; 418 QueryInterface = ChromeUtils.generateQI(["calIItipTransport"]); 419 420 sendItems(aRecipients, aItipItem, aFromAttendee) { 421 return false; 422 } 423} 424 425/** 426 * CalItipDefaultEmailTransport always uses the identity and account provided 427 * as default instead of the one configured for the calendar. 428 */ 429class CalItipDefaultEmailTransport extends CalItipEmailTransport { 430 getIdentityAndAccount() { 431 return { identity: this.mDefaultIdentity, account: this.mDefaultAccount }; 432 } 433} 434