1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * @overview 26 * This file defines the mail message. 27 */ 28 29 /** 30 * @constructor 31 * @class 32 * Creates a new (empty) mail message. 33 * 34 * @param {int} id the unique ID 35 * @param {Array} list the list that contains this message 36 * @param {Boolean} noCache if true, do not cache this message 37 * 38 * @extends ZmMailItem 39 */ 40 ZmMailMsg = function(id, list, noCache) { 41 42 ZmMailItem.call(this, ZmItem.MSG, id, list, noCache); 43 44 this.inHitList = false; 45 this._attHitList = []; 46 this._inviteDescBody = {}; 47 this._addrs = {}; 48 49 // info about MIME parts 50 this.attachments = []; 51 this._bodyParts = []; 52 this._contentType = {}; 53 54 for (var i = 0; i < ZmMailMsg.ADDRS.length; i++) { 55 var type = ZmMailMsg.ADDRS[i]; 56 this._addrs[type] = new AjxVector(); 57 } 58 this.identity = null; 59 }; 60 61 ZmMailMsg.prototype = new ZmMailItem; 62 ZmMailMsg.prototype.constructor = ZmMailMsg; 63 64 ZmMailMsg.prototype.isZmMailMsg = true; 65 ZmMailMsg.prototype.toString = function() { return "ZmMailMsg"; }; 66 67 ZmMailMsg.DL_SUB_VERSION = "0.1"; 68 69 ZmMailMsg.ADDRS = [AjxEmailAddress.FROM, AjxEmailAddress.TO, AjxEmailAddress.CC, 70 AjxEmailAddress.BCC, AjxEmailAddress.REPLY_TO, AjxEmailAddress.SENDER, 71 AjxEmailAddress.RESENT_FROM]; 72 73 ZmMailMsg.COMPOSE_ADDRS = [AjxEmailAddress.TO, AjxEmailAddress.CC, AjxEmailAddress.BCC]; 74 75 ZmMailMsg.HDR_FROM = AjxEmailAddress.FROM; 76 ZmMailMsg.HDR_TO = AjxEmailAddress.TO; 77 ZmMailMsg.HDR_CC = AjxEmailAddress.CC; 78 ZmMailMsg.HDR_BCC = AjxEmailAddress.BCC; 79 ZmMailMsg.HDR_REPLY_TO = AjxEmailAddress.REPLY_TO; 80 ZmMailMsg.HDR_SENDER = AjxEmailAddress.SENDER; 81 ZmMailMsg.HDR_DATE = "DATE"; 82 ZmMailMsg.HDR_SUBJECT = "SUBJECT"; 83 ZmMailMsg.HDR_LISTID = "List-ID"; 84 ZmMailMsg.HDR_XZIMBRADL = "X-Zimbra-DL"; 85 ZmMailMsg.HDR_INREPLYTO = "IN-REPLY-TO"; 86 87 ZmMailMsg.HDR_KEY = {}; 88 ZmMailMsg.HDR_KEY[ZmMailMsg.HDR_FROM] = ZmMsg.from; 89 ZmMailMsg.HDR_KEY[ZmMailMsg.HDR_TO] = ZmMsg.to; 90 ZmMailMsg.HDR_KEY[ZmMailMsg.HDR_CC] = ZmMsg.cc; 91 ZmMailMsg.HDR_KEY[ZmMailMsg.HDR_BCC] = ZmMsg.bcc; 92 ZmMailMsg.HDR_KEY[ZmMailMsg.HDR_REPLY_TO] = ZmMsg.replyTo; 93 ZmMailMsg.HDR_KEY[ZmMailMsg.HDR_SENDER] = ZmMsg.sender; 94 ZmMailMsg.HDR_KEY[ZmMailMsg.HDR_DATE] = ZmMsg.sentAt; 95 ZmMailMsg.HDR_KEY[ZmMailMsg.HDR_SUBJECT] = ZmMsg.subject; 96 97 // Ordered list - first matching status wins 98 ZmMailMsg.STATUS_LIST = ["isScheduled", "isDraft", "isReplied", "isForwarded", "isSent", "isUnread"]; 99 100 ZmMailMsg.STATUS_ICON = {}; 101 ZmMailMsg.STATUS_ICON["isDraft"] = "MsgStatusDraft"; 102 ZmMailMsg.STATUS_ICON["isReplied"] = "MsgStatusReply"; 103 ZmMailMsg.STATUS_ICON["isForwarded"] = "MsgStatusForward"; 104 ZmMailMsg.STATUS_ICON["isSent"] = "MsgStatusSent"; 105 ZmMailMsg.STATUS_ICON["isUnread"] = "MsgStatusUnread"; 106 ZmMailMsg.STATUS_ICON["isScheduled"] = "SendLater"; 107 108 ZmMailMsg.PSTATUS_ACCEPT = "AC"; 109 ZmMailMsg.PSTATUS_DECLINED = "DE"; 110 ZmMailMsg.PSTATUS_TENTATIVE = "TE"; 111 112 ZmMailMsg.STATUS_ICON[ZmMailMsg.PSTATUS_ACCEPT] = "CalInviteAccepted"; 113 ZmMailMsg.STATUS_ICON[ZmMailMsg.PSTATUS_DECLINED] = "CalInviteDeclined"; 114 ZmMailMsg.STATUS_ICON[ZmMailMsg.PSTATUS_TENTATIVE] = "CalInviteTentative"; 115 116 // tooltips for invite status icons 117 ZmMailMsg.TOOLTIP = {}; 118 ZmMailMsg.TOOLTIP["Appointment"] = ZmMsg.appointment; 119 ZmMailMsg.TOOLTIP["CalInviteAccepted"] = ZmMsg.ptstAccept; 120 ZmMailMsg.TOOLTIP["CalInviteDeclined"] = ZmMsg.ptstDeclined; 121 ZmMailMsg.TOOLTIP["CalInviteTentative"] = ZmMsg.ptstTentative; 122 123 // We just hard-code "Re:" or "Fwd:", but other clients may use localized versions 124 ZmMailMsg.SUBJ_PREFIX_RE = new RegExp("^\\s*(Re|Fw|Fwd|" + ZmMsg.re + "|" + ZmMsg.fwd + "|" + ZmMsg.fw + "):" + "\\s*", "i"); 125 126 ZmMailMsg.URL_RE = /((telnet:)|((https?|ftp|gopher|news|file):\/\/)|(www\.[\w\.\_\-]+))[^\s\xA0\(\)\<\>\[\]\{\}\'\"]*/i; 127 128 ZmMailMsg.CONTENT_PART_ID = "ci"; 129 ZmMailMsg.CONTENT_PART_LOCATION = "cl"; 130 131 // Additional headers to request. Also used by ZmConv and ZmSearch 132 ZmMailMsg.requestHeaders = {listId: ZmMailMsg.HDR_LISTID, xZimbraDL: ZmMailMsg.HDR_XZIMBRADL,replyTo:ZmMailMsg.HDR_INREPLYTO}; 133 134 /** 135 * Fetches a message from the server. 136 * 137 * @param {Hash} params a hash of parameters 138 * @param {ZmZimbraMail} params.sender the provides access to sendRequest() 139 * @param {int} params.msgId the ID of the msg to be fetched. 140 * @param {int} params.partId the msg part ID (if retrieving attachment part, i.e. rfc/822) 141 * @param {int} params.ridZ the RECURRENCE-ID in Z (UTC) timezone 142 * @param {Boolean} params.getHtml if <code>true</code>, try to fetch html from the server 143 * @param {Boolean} params.markRead if <code>true</code>, mark msg read 144 * @param {AjxCallback} params.callback the async callback 145 * @param {AjxCallback} params.errorCallback the async error callback 146 * @param {Boolean} params.noBusyOverlay if <code>true</code>, do not put up busy overlay during request 147 * @param {Boolean} params.noTruncate if <code>true</code>, do not truncate message body 148 * @param {ZmBatchCommand} params.batchCmd if set, request gets added to this batch command 149 * @param {String} params.accountName the name of the account to send request on behalf of 150 * @param {boolean} params.needExp if not <code>false</code>, have server check if addresses are DLs 151 */ 152 ZmMailMsg.fetchMsg = 153 function(params) { 154 var jsonObj = {GetMsgRequest:{_jsns:"urn:zimbraMail"}}; 155 var request = jsonObj.GetMsgRequest; 156 var m = request.m = {}; 157 m.id = params.msgId; 158 if (params.partId) { 159 m.part = params.partId; 160 } 161 if (params.markRead) { 162 m.read = 1; 163 } 164 if (params.getHtml) { 165 m.html = 1; 166 } 167 if (params.needExp !== false) { 168 m.needExp = 1; 169 } 170 171 if (params.ridZ) { 172 m.ridZ = params.ridZ; 173 } 174 175 ZmMailMsg.addRequestHeaders(m); 176 177 if (!params.noTruncate) { 178 m.max = appCtxt.get(ZmSetting.MAX_MESSAGE_SIZE) || ZmMailApp.DEFAULT_MAX_MESSAGE_SIZE; 179 } 180 181 if (params.batchCmd) { 182 params.batchCmd.addRequestParams(jsonObj, params.callback); 183 } else { 184 var newParams = { 185 jsonObj: jsonObj, 186 asyncMode: true, 187 offlineCache: true, 188 callback: ZmMailMsg._handleResponseFetchMsg.bind(null, params.callback), 189 errorCallback: params.errorCallback, 190 noBusyOverlay: params.noBusyOverlay, 191 accountName: params.accountName 192 }; 193 newParams.offlineCallback = ZmMailMsg._handleOfflineResponseFetchMsg.bind(null, m.id, newParams.callback); 194 params.sender.sendRequest(newParams); 195 } 196 }; 197 198 ZmMailMsg._handleResponseFetchMsg = 199 function(callback, result) { 200 if (callback) { 201 callback.run(result); 202 } 203 }; 204 205 ZmMailMsg._handleOfflineResponseFetchMsg = 206 function(msgId, callback) { 207 var getItemCallback = ZmMailMsg._handleOfflineResponseFetchMsgCallback.bind(null, callback); 208 ZmOfflineDB.getItem(msgId, ZmApp.MAIL, getItemCallback); 209 }; 210 211 ZmMailMsg._handleOfflineResponseFetchMsgCallback = 212 function(callback, result) { 213 var response = { 214 GetMsgResponse : { 215 m : result 216 } 217 }; 218 if (callback) { 219 callback(new ZmCsfeResult(response)); 220 } 221 }; 222 223 ZmMailMsg.stripSubjectPrefixes = 224 function(subj) { 225 var regex = ZmMailMsg.SUBJ_PREFIX_RE; 226 while (regex.test(subj)) { 227 subj = subj.replace(regex, ""); 228 } 229 return subj; 230 }; 231 232 // Public methods 233 234 /** 235 * Gets a vector of addresses of the given type. 236 * 237 * @param {constant} type an email address type 238 * @param {Hash} used an array of addresses that have been used. If not <code>null</code>, 239 * then this method will omit those addresses from the 240 * returned vector and will populate used with the additional new addresses 241 * @param {Boolean} addAsContact if <code>true</code>, emails should be converted to {@link ZmContact} objects 242 * @param {boolean} dontUpdateUsed if true, do not update the hash of used addresses 243 * 244 * @return {AjxVector} a vector of email addresses 245 */ 246 ZmMailMsg.prototype.getAddresses = 247 function(type, used, addAsContact, dontUpdateUsed) { 248 if (!used) { 249 return this._addrs[type]; 250 } else { 251 var a = this._addrs[type].getArray(); 252 var addrs = []; 253 for (var i = 0; i < a.length; i++) { 254 var addr = a[i]; 255 var email = addr.getAddress(); 256 if (!email) { continue; } 257 email = email.toLowerCase(); 258 if (!used[email]) { 259 var contact = addr; 260 if (addAsContact) { 261 var cl = AjxDispatcher.run("GetContacts"); 262 contact = cl.getContactByEmail(email); 263 if (contact == null) { 264 contact = new ZmContact(null); 265 contact.initFromEmail(addr); 266 } 267 } 268 addrs.push(contact); 269 } 270 if (!dontUpdateUsed) { 271 used[email] = true; 272 } 273 } 274 return AjxVector.fromArray(addrs); 275 } 276 }; 277 278 /** 279 * Gets a Reply-To address if there is one, otherwise the From address 280 * unless this message was sent by the user, in which case, it is the To 281 * field (but only in the case of Reply All). A list is returned, since 282 * theoretically From and Reply To can have multiple addresses. 283 * 284 * @return {AjxVector} an array of {@link AjxEmailAddress} objects 285 */ 286 ZmMailMsg.prototype.getReplyAddresses = 287 function(mode, aliases, isDefaultIdentity) { 288 289 if (!this.isSent) { //ignore reply_to for sent messages. 290 // reply-to has precedence over everything else 291 var addrVec = this._addrs[AjxEmailAddress.REPLY_TO]; 292 } 293 if (!addrVec && this.isInvite() && this.needsRsvp()) { 294 var invEmail = this.invite.getOrganizerEmail(0); 295 if (invEmail) { 296 return AjxVector.fromArray([new AjxEmailAddress(invEmail)]); 297 } 298 } 299 300 if (!(addrVec && addrVec.size())) { 301 if (mode == ZmOperation.REPLY_CANCEL || (this.isSent && mode == ZmOperation.REPLY_ALL)) { 302 addrVec = this.isInvite() ? this._getAttendees() : this.getAddresses(AjxEmailAddress.TO, aliases, false, true); 303 } else { 304 addrVec = this.getAddresses(AjxEmailAddress.FROM, aliases, false, true); 305 if (addrVec.size() == 0) { 306 addrVec = this.getAddresses(AjxEmailAddress.TO, aliases, false, true); 307 } 308 } 309 } 310 return addrVec; 311 }; 312 313 ZmMailMsg.prototype._getAttendees = 314 function() { 315 var attendees = this.invite.components[0].at; 316 var emails = new AjxVector(); 317 for (var i = 0; i < (attendees ? attendees.length : 0); i++) { 318 var at = attendees[i]; 319 emails.add(new AjxEmailAddress(at.a, null, null, at.d)); 320 } 321 322 return emails; 323 }; 324 325 /** 326 * Gets the first address in the vector of addresses of the given type. 327 * 328 * @param {constant} type the type 329 * @return {String} the address 330 */ 331 ZmMailMsg.prototype.getAddress = 332 function(type) { 333 return this._addrs[type].get(0); 334 }; 335 336 /** 337 * Gets the fragment. If maxLen is given, will truncate fragment to maxLen and add ellipsis. 338 * 339 * @param {int} maxLen the maximum length 340 * @return {String} the fragment 341 */ 342 ZmMailMsg.prototype.getFragment = 343 function(maxLen) { 344 var frag = this.fragment; 345 346 if (maxLen && frag && frag.length) { 347 frag = frag.substring(0, maxLen); 348 if (this.fragment.length > maxLen) 349 frag += "..."; 350 } 351 return frag; 352 }; 353 354 /** 355 * Checks if the message is read only. 356 * 357 * @return {Boolean} <code>true</code> if read only 358 */ 359 ZmMailMsg.prototype.isReadOnly = 360 function() { 361 if (this._isReadOnly == null) { 362 var folder = appCtxt.getById(this.folderId); 363 this._isReadOnly = (folder ? folder.isReadOnly() : false); 364 } 365 return this._isReadOnly; 366 }; 367 368 /** 369 * Gets the header string. 370 * 371 * @param {constant} hdr the header (see <code>ZmMailMsg.HDR_</code> constants) 372 * @param {boolean} htmlMode if true, format as HTML 373 * @return {String} the value 374 */ 375 ZmMailMsg.prototype.getHeaderStr = 376 function(hdr, htmlMode) { 377 378 var key, value; 379 if (hdr == ZmMailMsg.HDR_DATE) { 380 if (this.sentDate) { 381 var formatter = AjxDateFormat.getDateTimeInstance(AjxDateFormat.FULL, AjxDateFormat.MEDIUM); 382 value = formatter.format(new Date(this.sentDate)); 383 } 384 } else if (hdr == ZmMailMsg.HDR_SUBJECT) { 385 value = this.subject; 386 } else { 387 var addrs = this.getAddresses(hdr); 388 value = addrs.toString(", ", true); 389 } 390 391 var key = ZmMailMsg.HDR_KEY[hdr] + ": "; 392 if (!value) { return; } 393 if (htmlMode) { 394 key = "<b>" + key + "</b>"; 395 value = AjxStringUtil.convertToHtml(value); 396 } 397 398 return key + value; 399 }; 400 401 /** 402 * Checks if this message has html parts. 403 * 404 * @return {Boolean} <code>true</code> if this message has HTML 405 */ 406 ZmMailMsg.prototype.isHtmlMail = 407 function() { 408 if (this.isInvite()) { 409 return this.invite.isHtmlInvite(); 410 } 411 else { 412 return this.getBodyPart(ZmMimeTable.TEXT_HTML) != null; 413 } 414 }; 415 416 // Setters 417 418 /** 419 * Sets the vector of addresses of the given type to the given vector of addresses 420 * 421 * @param {constant} type the address type 422 * @param {AjxVector} addrs a vector of {@link AjxEmailAddress} objects 423 */ 424 ZmMailMsg.prototype.setAddresses = 425 function(type, addrs) { 426 this._onChange("address", type, addrs); 427 this._addrs[type] = addrs; 428 }; 429 430 /** 431 * Sets the vector of addresses of the given type to the address given. 432 * 433 * @param {constant} type the address type 434 * @param {AjxEmailAddress} addr an address 435 */ 436 ZmMailMsg.prototype.setAddress = 437 function(type, addr) { 438 this._onChange("address", type, addr); 439 this._addrs[type].removeAll(); 440 this._addrs[type] = new AjxVector(); 441 this._addrs[type].add(addr); 442 }; 443 444 /** 445 * Clears out all the address vectors. 446 * 447 */ 448 ZmMailMsg.prototype.clearAddresses = 449 function() { 450 for (var i = 0; i < ZmMailMsg.ADDRS.length; i++) { 451 var type = ZmMailMsg.ADDRS[i]; 452 this._addrs[type].removeAll(); 453 } 454 }; 455 456 /** 457 * Adds the given vector of addresses to the vector of addresses of the given type 458 * 459 * @param {constant} type the address type 460 * @param {AjxVector} addrs a vector of {@link AjxEmailAddress} objects 461 */ 462 ZmMailMsg.prototype.addAddresses = 463 function(type, addrs) { 464 var size = addrs.size(); 465 for (var i = 0; i < size; i++) { 466 this._addrs[type].add(addrs.get(i)); 467 } 468 }; 469 470 /** 471 * Adds the given address to the vector of addresses of the given type 472 * 473 * @param {AjxEmailAddress} addr an address 474 */ 475 ZmMailMsg.prototype.addAddress = 476 function(addr, type) { 477 type = type || addr.type || AjxEmailAddress.TO; 478 this._addrs[type].add(addr); 479 }; 480 481 /** 482 * Sets the subject 483 * 484 * @param {String} subject the subject 485 */ 486 ZmMailMsg.prototype.setSubject = 487 function(subject) { 488 this._onChange("subject", subject); 489 this.subject = subject; 490 }; 491 492 /** 493 * Sets the message's top part to the given MIME part 494 * 495 * @param {String} part a MIME part 496 */ 497 ZmMailMsg.prototype.setTopPart = 498 function(part) { 499 this._onChange("topPart", part); 500 this._topPart = part; 501 }; 502 503 /** 504 * Sets the body parts. 505 * 506 * @param {array} parts an array of ZmMimePart 507 * 508 */ 509 ZmMailMsg.prototype.setBodyParts = 510 function(parts) { 511 this._onChange("bodyParts", parts); 512 this._bodyParts = parts; 513 this._loaded = this._bodyParts.length > 0 || this.attachments.length > 0; 514 }; 515 516 /** 517 * Sets the ID of any attachments which have already been uploaded. 518 * 519 * @param {String} id an attachment ID 520 */ 521 ZmMailMsg.prototype.addAttachmentId = 522 function(id) { 523 if (this.attId) { 524 id = this.attId + "," + id; 525 } 526 this._onChange("attachmentId", id); 527 this.attId = id; 528 }; 529 530 /** 531 * Adds an inline attachment. 532 * 533 * @param {String} cid the content id 534 * @param {String} aid the attachment id 535 * @param {String} part the part 536 * @param {Boolean} ismsg if true, aid is a message id 537 */ 538 ZmMailMsg.prototype.addInlineAttachmentId = 539 function (cid, aid, part, ismsg) { 540 if (!this._inlineAtts) { 541 this._inlineAtts = []; 542 } 543 this._onChange("inlineAttachments",aid); 544 if (ismsg && aid && part) { 545 this._inlineAtts.push({"cid":cid, "mid":aid, "part": part}); 546 } else if (aid) { 547 this._inlineAtts.push({"cid":cid, "aid":aid}); 548 } else if (part) { 549 this._inlineAtts.push({"cid":cid, "part":part}); 550 } 551 }; 552 553 ZmMailMsg.prototype._resetAllInlineAttachments = 554 function(){ 555 this._inlineAtts = []; 556 for (var i = 0; i < this.attachments.length; i++) { 557 this.attachments[i].foundInMsgBody = false; 558 } 559 } 560 561 /** 562 * Adds an inline document attachment. 563 * 564 * @param {String} cid the content id 565 * @param {String} docId the document id 566 * @param {String} docpath the document path 567 * @param {String} part the part 568 */ 569 ZmMailMsg.prototype.addInlineDocAttachment = 570 function (cid, docId, docpath, part) { 571 if (!this._inlineDocAtts) { 572 this._inlineDocAtts = []; 573 } 574 this._onChange("inlineDocAttachments", docId, docpath, part); 575 if (docId) { 576 this._inlineDocAtts.push({"cid":cid,"docid":docId}); 577 } else if (docpath) { 578 this._inlineDocAtts.push({"cid":cid,"docpath":docpath}); 579 }else if (part) { 580 this._inlineDocAtts.push({"cid":cid,"part":part}); 581 } 582 }; 583 584 ZmMailMsg.prototype.setInlineAttachments = 585 function(inlineAtts){ 586 if (inlineAtts) { 587 this._inlineAtts = inlineAtts; 588 } 589 }; 590 591 /** 592 * Gets the inline attachments. 593 * 594 * @return {Array} an array of attachments 595 */ 596 ZmMailMsg.prototype.getInlineAttachments = 597 function() { 598 return this._inlineAtts || []; 599 }; 600 601 602 /** 603 * Gets the inline document attachments. 604 * 605 * @return {Array} an array of attachments 606 */ 607 ZmMailMsg.prototype.getInlineDocAttachments = 608 function() { 609 return this._inlineDocAtts || []; 610 }; 611 612 /** 613 * Finds the attachment in this message for the given CID. 614 * 615 * @param {String} cid the content id 616 * @return {Object} the attachment or <code>null</code> if not found 617 */ 618 ZmMailMsg.prototype.findInlineAtt = 619 function(cid) { 620 if (!(this.attachments && this.attachments.length)) { return null; } 621 622 for (var i = 0; i < this.attachments.length; i++) { 623 if (this.attachments[i].contentId == cid) { 624 return this.attachments[i]; 625 } 626 } 627 return null; 628 }; 629 630 /** 631 * Sets the IDs of messages to attach (as a forward) 632 * 633 * @param {Array} ids a list of mail message IDs 634 */ 635 ZmMailMsg.prototype.setMessageAttachmentId = 636 function(ids) { 637 this._onChange("messageAttachmentId", ids); 638 this._msgAttIds = ids; 639 }; 640 641 /** 642 * Sets the IDs of docs to attach 643 * 644 * @param {Array} ids a list of document IDs 645 */ 646 ZmMailMsg.prototype.setDocumentAttachments = 647 function(docs) { 648 this._onChange("documentAttachmentId", docs); 649 this._docAtts = docs; 650 }; 651 652 ZmMailMsg.prototype.addDocumentAttachment = 653 function(doc) { 654 if(!this._docAtts) { 655 this._docAtts = []; 656 } 657 this._docAtts.push(doc); 658 }; 659 660 /** 661 * Sets the list of attachment (message part) IDs to be forwarded 662 * 663 * @param {Array} ids a list of attachment IDs 664 */ 665 ZmMailMsg.prototype.setForwardAttIds = 666 function(ids) { 667 this._onChange("forwardAttIds", ids); 668 this._forAttIds = ids; 669 }; 670 671 /** 672 * Sets the list of attachments details(message id and message part) to be forwarded 673 * 674 * @param {Array} objs a list of attachments details {id, part} 675 */ 676 ZmMailMsg.prototype.setForwardAttObjs = 677 function(objs) { 678 this._forAttObjs = objs; 679 }; 680 681 /** 682 * Sets the ID of the contacts that are to be attached as vCards 683 * 684 * @param {Array} ids a list of contact IDs 685 */ 686 ZmMailMsg.prototype.setContactAttIds = 687 function(ids) { 688 ids = AjxUtil.toArray(ids); 689 this._onChange("contactAttIds", ids); 690 this._contactAttIds = ids; 691 }; 692 693 // Actions 694 695 /** 696 * Fills in the message from the given message node. Whatever attributes and child nodes 697 * are available will be used. The message node is not always fully populated, since it 698 * may have been created as part of getting a conversation. 699 * 700 * @param {Object} node a message node 701 * @param {Hash} args a hash of arguments 702 * @param {Boolean} noCache if true, do not cache this message 703 * @return {ZmMailMsg} the message 704 */ 705 ZmMailMsg.createFromDom = 706 function(node, args, noCache) { 707 var msg = new ZmMailMsg(node.id, args.list, noCache); 708 msg._loadFromDom(node); 709 return msg; 710 }; 711 712 /** 713 * Gets the full message object from the back end based on the current message ID, and 714 * fills in the message. 715 * 716 * @param {Hash} params a hash of parameters: 717 * @param {Boolean} params.getHtml if <code>true</code>, try to fetch html from the server 718 * @param {Boolean} params.markRead if <code>true</code>, mark msg read 719 * @param {Boolean} params.forceLoad if <code>true</code>, get msg from server 720 * @param {AjxCallback} params.callback the async callback 721 * @param {AjxCallback} params.errorCallback the async error callback 722 * @param {Boolean} params.noBusyOverlay if <code>true</code>, do not put up busy overlay during request 723 * @param {Boolean} params.noTruncate if <code>true</code>, do not set max limit on size of msg body 724 * @param {ZmBatchCommand} params.batchCmd if set, request gets added to this batch command 725 * @param {String} params.accountName the name of the account to send request on behalf of 726 * @param {boolean} params.needExp if not <code>false</code>, have server check if addresses are DLs 727 */ 728 ZmMailMsg.prototype.load = 729 function(params) { 730 if (this._loading && !params.forceLoad) { 731 //the only way to not get partial results is to try in some timeout. 732 //this method will be called again, eventually, the message will be finished loading, and the callback would be called safely. 733 this._loadingWaitCount = (this._loadingWaitCount || 0) + 1; 734 if (this._loadingWaitCount > 20) { 735 //give up after 20 timeouts (about 10 seconds) - maybe request got lost. send another request below. 736 this._loadingWaitCount = 0; 737 this._loading = false; 738 } 739 else { 740 setTimeout(this.load.bind(this, params), 500); 741 return; 742 } 743 } 744 // If we are already loaded, then don't bother loading 745 if (!this._loaded || params.forceLoad) { 746 this._loading = true; 747 var respCallback = this._handleResponseLoad.bind(this, params, params.callback); 748 params.getHtml = params.getHtml || this.isDraft || appCtxt.get(ZmSetting.VIEW_AS_HTML); 749 params.sender = appCtxt.getAppController(); 750 params.msgId = this.id; 751 params.partId = this.partId; 752 params.callback = respCallback; 753 var errorCallback = this._handleResponseLoadFail.bind(this, params, params.errorCallback); 754 params.errorCallback = errorCallback; 755 ZmMailMsg.fetchMsg(params); 756 } else { 757 if (params.callback) { 758 params.callback.run(new ZmCsfeResult()); // return exceptionless result 759 } 760 } 761 }; 762 763 ZmMailMsg.prototype._handleResponseLoad = 764 function(params, callback, result) { 765 var response = result.getResponse().GetMsgResponse; 766 767 this.clearAddresses(); 768 769 // clear all participants (since it'll get re-parsed w/ diff. ID's) 770 if (this.participants) { 771 this.participants.removeAll(); 772 } 773 774 this._loadFromDom(response.m[0]); 775 if (!this.isReadOnly() && params.markRead) { 776 this.markRead(); 777 } else { 778 // Setup the _evt.item field and list._evt.item in order to insure proper notifications. 779 this._setupNotify(); 780 } 781 this.findAttsFoundInMsgBody(); 782 783 this._loading = false; 784 785 // return result so callers can check for exceptions if they want 786 if (this._loadCallback) { 787 // overriding callback (see ZmMsgController::show) 788 this._loadCallback.run(result); 789 this._loadCallback = null; 790 } else if (callback) { 791 callback.run(result); 792 } 793 }; 794 795 ZmMailMsg.prototype.markRead = function() { 796 if (!this.isReadOnly()) { 797 //For offline mode keep isUnread property as true so that additional MsgActionRequest gets fired. 798 //MsgActionRequest also gets stored in outbox queue and it also sends notify header for reducing the folder unread count. 799 this._markReadLocal(!appCtxt.isWebClientOffline()); 800 } 801 }; 802 803 ZmMailMsg.prototype._handleResponseLoadFail = 804 function(params, callback, result) { 805 this._loading = false; 806 if (callback) { 807 return callback.run(result); 808 } 809 }; 810 811 ZmMailMsg.prototype._handleIndexedDBResponse = 812 function(params, requestParams, result) { 813 814 var obj = result[0], 815 msgNode, 816 data = {}, 817 methodName = requestParams.methodName; 818 819 if (obj) { 820 msgNode = obj[obj.methodName]["m"]; 821 if (msgNode) { 822 msgNode.su = msgNode.su._content; 823 msgNode.fr = msgNode.mp[0].content._content; 824 msgNode.mp[0].content = msgNode.fr; 825 if (msgNode.fr) { 826 msgNode.mp[0].body = true; 827 } 828 data[methodName.replace("Request", "Response")] = { "m" : [msgNode] }; 829 var csfeResult = new ZmCsfeResult(data); 830 this._handleResponseLoad(params, params.callback, csfeResult); 831 } 832 } 833 }; 834 835 ZmMailMsg.prototype.isLoaded = 836 function() { 837 return this._loaded; 838 }; 839 840 /** 841 * Returns the list of body parts. 842 * 843 * @param {string} contentType preferred MIME type of alternative parts (optional) 844 */ 845 ZmMailMsg.prototype.getBodyParts = 846 function(contentType) { 847 848 if (contentType) { 849 this._lastContentType = contentType; 850 } 851 852 // no multi/alt, so we have a plain list 853 if (!this.hasContentType(ZmMimeTable.MULTI_ALT)) { 854 return this._bodyParts; 855 } 856 857 // grab the preferred type out of multi/alt parts 858 contentType = contentType || this._lastContentType; 859 var parts = []; 860 for (var i = 0; i < this._bodyParts.length; i++) { 861 var part = this._bodyParts[i]; 862 if (part.isZmMimePart) { 863 parts.push(part); 864 } 865 else if (part) { 866 // part is a hash of alternative parts by content type 867 var altPart = contentType && part[contentType]; 868 parts.push(altPart || AjxUtil.values(part)[0]); 869 } 870 } 871 872 return parts; 873 }; 874 875 /** 876 * Returns true if this msg has loaded a part with the given content type. 877 * 878 * @param {string} contentType MIME type 879 */ 880 ZmMailMsg.prototype.hasContentType = 881 function(contentType) { 882 return this._contentType[contentType]; 883 }; 884 885 /** 886 * Returns true is the msg has more than one body part. The server marks parts that 887 * it considers to be body parts. 888 * 889 * @return {boolean} 890 */ 891 ZmMailMsg.prototype.hasMultipleBodyParts = 892 function() { 893 var parts = this.getBodyParts(); 894 return (parts && parts.length > 1); 895 }; 896 897 /** 898 * Returns the first body part, of the given type if provided. May invoke a 899 * server call if it needs to fetch an alternative part. 900 * 901 * @param {string} contentType MIME type 902 * @param {callback} callback callback 903 * 904 * @return {ZmMimePart} MIME part 905 */ 906 ZmMailMsg.prototype.getBodyPart = 907 function(contentType, callback) { 908 909 if (contentType) { 910 this._lastContentType = contentType; 911 } 912 913 function getPart(ct) { 914 var bodyParts = this.getBodyParts(ct); 915 for (var i = 0; i < bodyParts.length; i++) { 916 var part = bodyParts[i]; 917 // should be a ZmMimePart, but check just in case 918 part = part.isZmMimePart ? part : part[ct]; 919 if (!ct || (part.contentType === ct)) { 920 return part; 921 } 922 } 923 } 924 var bodyPart = getPart.call(this, contentType); 925 926 if (this.isInvite()) { 927 if (!bodyPart) { 928 if (contentType === ZmMimeTable.TEXT_HTML) { 929 //text/html not available so look for text/plain 930 bodyPart = getPart.call(this,ZmMimeTable.TEXT_PLAIN); 931 } else if (contentType === ZmMimeTable.TEXT_PLAIN) { 932 //text/plain not available so look for text/html 933 bodyPart = getPart.call(this,ZmMimeTable.TEXT_HTML); 934 } 935 } 936 // bug: 46071, handle missing body part/content 937 if (!bodyPart || (bodyPart && !bodyPart.getContent())) { 938 bodyPart = this.getInviteDescriptionContent(contentType); 939 } 940 } 941 942 if (callback) { 943 if (bodyPart) { 944 callback.run(bodyPart); 945 } 946 // see if we should try to fetch an alternative part 947 else if (this.hasContentType(ZmMimeTable.MULTI_ALT) && 948 ((contentType == ZmMimeTable.TEXT_PLAIN && this.hasContentType(ZmMimeTable.TEXT_PLAIN)) || 949 (contentType == ZmMimeTable.TEXT_HTML && this.hasContentType(ZmMimeTable.TEXT_HTML)))) { 950 951 ZmMailMsg.fetchMsg({ 952 sender: appCtxt.getAppController(), 953 msgId: this.id, 954 getHtml: (contentType == ZmMimeTable.TEXT_HTML), 955 callback: this._handleResponseFetchAlternativePart.bind(this, contentType, callback) 956 }); 957 } 958 else { 959 callback.run(); 960 } 961 } 962 963 return bodyPart; 964 }; 965 966 /** 967 * Fetches the requested alternative part and adds it to our MIME structure, and body parts. 968 * 969 * @param {string} contentType MIME type of part to fetch 970 * @param {callback} callback 971 */ 972 ZmMailMsg.prototype.fetchAlternativePart = 973 function(contentType, callback) { 974 975 var respCallback = this._handleResponseFetchAlternativePart.bind(this, contentType, callback); 976 ZmMailMsg.fetchMsg({ 977 sender: appCtxt.getAppController(), 978 msgId: this.id, 979 getHtml: (contentType == ZmMimeTable.TEXT_HTML), 980 callback: respCallback 981 }); 982 }; 983 984 ZmMailMsg.prototype._handleResponseFetchAlternativePart = 985 function(contentType, callback, result) { 986 987 // look for first multi/alt with child of type we want, add it; assumes at most one multi/alt per msg 988 var response = result.getResponse().GetMsgResponse; 989 var altPart = this._topPart && this._topPart.addAlternativePart(response.m[0].mp[0], contentType, 0); 990 if (altPart) { 991 var found = false; 992 for (var i = 0; i < this._bodyParts.length; i++) { 993 var bp = this._bodyParts[i]; 994 // a hash rather than a ZmMimePart indicates multi/alt 995 if (!bp.isZmMimePart) { 996 bp[altPart.contentType] = altPart; 997 } 998 } 999 } 1000 1001 if (callback) { 1002 callback.run(); 1003 } 1004 }; 1005 1006 /** 1007 * Gets the content of the first body part of the given content type (if provided). 1008 * If HTML is requested, may return content set via setHtmlContent(). 1009 * 1010 * @param {string} contentType MIME type 1011 * @param {boolean} useOriginal if true, do not grab the copy w/ the images defanged (HTML only) 1012 * 1013 * @return {string} the content 1014 */ 1015 ZmMailMsg.prototype.getBodyContent = 1016 function(contentType, useOriginal) { 1017 1018 if (contentType) { 1019 this._lastContentType = contentType; 1020 } 1021 1022 if (contentType == ZmMimeTable.TEXT_HTML && !useOriginal && this._htmlBody) { 1023 return this._htmlBody; 1024 } 1025 1026 var bodyPart = this._loaded && this.getBodyPart(contentType); 1027 return bodyPart ? bodyPart.getContent() : ""; 1028 }; 1029 1030 /** 1031 * Extracts and returns the text content out of a text/calendar part. 1032 * 1033 * @param {ZmMimePart} bodyPart a text/calendar MIME part 1034 * @return {string} text content 1035 */ 1036 ZmMailMsg.getTextFromCalendarPart = 1037 function(bodyPart) { 1038 1039 // NOTE: IE doesn't match multi-line regex, even when explicitly 1040 // specifying the "m" attribute. 1041 var bpContent = bodyPart ? bodyPart.getContent() : ""; 1042 var lines = bpContent.split(/\r\n/); 1043 var desc = []; 1044 var content = ""; 1045 for (var i = 0; i < lines.length; i++) { 1046 var line = lines[i]; 1047 if (line.match(/^DESCRIPTION:/)) { 1048 desc.push(line.substr(12)); 1049 for (var j = i + 1; j < lines.length; j++) { 1050 line = lines[j]; 1051 if (line.match(/^\s+/)) { 1052 desc.push(line.replace(/^\s+/, " ")); 1053 continue; 1054 } 1055 break; 1056 } 1057 break; 1058 } 1059 else if (line.match(/^COMMENT:/)) { 1060 //DESCRIPTION is sent as COMMENT in Lotus notes. 1061 desc.push(line.substr(8)); 1062 for (var j = i + 1; j < lines.length; j++) { 1063 line = lines[j]; 1064 if (line.match(/^\s+/)) { 1065 desc.push(line.replace(/^\s+/, " ")); 1066 continue; 1067 } 1068 break; 1069 } 1070 break; 1071 } 1072 } 1073 if (desc.length > 0) { 1074 content = desc.join(""); 1075 content = content.replace(/\\t/g, "\t"); 1076 content = content.replace(/\\n/g, "\n"); 1077 content = content.replace(/\\(.)/g, "$1"); 1078 } 1079 return content; 1080 }; 1081 1082 /** 1083 * Returns a text/plain or text-like (not HTML or calendar) body part 1084 * 1085 * @return {ZmMimePart} MIME part 1086 */ 1087 ZmMailMsg.prototype.getTextBodyPart = 1088 function() { 1089 var bodyPart = this.getBodyPart(ZmMimeTable.TEXT_PLAIN) || this.getBodyPart(); 1090 return (bodyPart && bodyPart.isBody && ZmMimeTable.isTextType(bodyPart.contentType)) ? bodyPart : null; 1091 }; 1092 1093 /** 1094 * Returns true if this message has an inline image 1095 * 1096 * @return {boolean} 1097 */ 1098 ZmMailMsg.prototype.hasInlineImage = 1099 function() { 1100 for (var i = 0; i < this._bodyParts.length; i++) { 1101 var bp = this._bodyParts[i]; 1102 if (bp.isZmMimePart && bp.contentDisposition == "inline" && bp.fileName && ZmMimeTable.isRenderableImage(bp.contentType)) { 1103 return true; 1104 } 1105 } 1106 return false; 1107 }; 1108 1109 /** 1110 * Sets the html content, overriding that of any HTML body part. 1111 * 1112 * @param {string} content the HTML content 1113 */ 1114 ZmMailMsg.prototype.setHtmlContent = 1115 function(content) { 1116 this._onChange("htmlContent", content); 1117 this._htmlBody = content; 1118 }; 1119 1120 /** 1121 * Sets the invite description. 1122 * 1123 * @param {String} contentType the content type ("text/plain" or "text/html") 1124 * @param {String} content the content 1125 */ 1126 ZmMailMsg.prototype.setInviteDescriptionContent = 1127 function(contentType, content) { 1128 this._inviteDescBody[contentType] = content; 1129 }; 1130 1131 /** 1132 * Gets the invite description content value. 1133 * 1134 * @param {String} contentType the content type ("text/plain" or "text/html") 1135 * @return {String} the content value 1136 */ 1137 ZmMailMsg.prototype.getInviteDescriptionContentValue = 1138 function(contentType) { 1139 return this._inviteDescBody[contentType]; 1140 } 1141 /** 1142 * Gets the invite description content. 1143 * 1144 * @param {String} contentType the content type ("text/plain" or "text/html") 1145 * @return {String} the content 1146 */ 1147 ZmMailMsg.prototype.getInviteDescriptionContent = 1148 function(contentType) { 1149 1150 if (!contentType) { 1151 contentType = ZmMimeTable.TEXT_HTML; 1152 } 1153 1154 var desc = this._inviteDescBody[contentType]; 1155 1156 if (!desc) { 1157 var htmlContent = this._inviteDescBody[ZmMimeTable.TEXT_HTML]; 1158 var textContent = this._inviteDescBody[ZmMimeTable.TEXT_PLAIN]; 1159 1160 if (!htmlContent && textContent) { 1161 htmlContent = AjxStringUtil.convertToHtml(textContent); 1162 } 1163 1164 if (!textContent && htmlContent) { 1165 textContent = AjxStringUtil.convertHtml2Text(htmlContent); 1166 } 1167 1168 desc = (contentType == ZmMimeTable.TEXT_HTML) ? htmlContent : textContent; 1169 } 1170 1171 var idx = desc ? desc.indexOf(ZmItem.NOTES_SEPARATOR) : -1; 1172 1173 if (idx == -1 && this.isInvite()) { 1174 var inviteSummary = this.invite.getSummary((contentType == ZmMimeTable.TEXT_HTML)); 1175 desc = desc ? (inviteSummary + desc) : null; 1176 } 1177 1178 if (desc != null) { 1179 var part = new ZmMimePart(); 1180 part.contentType = part.ct = contentType; 1181 part.size = part.s = desc.length; 1182 part.node = {content: desc}; 1183 return part; 1184 } 1185 }; 1186 1187 ZmMailMsg.prototype.sendInviteReply = 1188 function(edited, componentId, callback, errorCallback, instanceDate, accountName, ignoreNotify) { 1189 this._origMsg = this._origMsg || this; 1190 if (componentId == 0){ // editing reply, custom message 1191 this._origMsg._customMsg = true; 1192 } 1193 this._sendInviteReply(edited, componentId || 0, callback, errorCallback, instanceDate, accountName, ignoreNotify); 1194 }; 1195 1196 ZmMailMsg.prototype._sendInviteReply = 1197 function(edited, componentId, callback, errorCallback, instanceDate, accountName, ignoreNotify) { 1198 var jsonObj = {SendInviteReplyRequest:{_jsns:"urn:zimbraMail"}}; 1199 var request = jsonObj.SendInviteReplyRequest; 1200 1201 request.id = this._origMsg.id; 1202 request.compNum = componentId; 1203 1204 var verb = "ACCEPT"; 1205 var needsRsvp = true; 1206 var newPtst; 1207 1208 var toastMessage; //message to display after action is done. 1209 1210 switch (this.inviteMode) { 1211 case ZmOperation.REPLY_ACCEPT_IGNORE: //falls-through on purpose 1212 needsRsvp = false; 1213 case ZmOperation.REPLY_ACCEPT_NOTIFY: //falls-through on purpose 1214 case ZmOperation.REPLY_ACCEPT: 1215 verb = "ACCEPT"; 1216 newPtst = ZmCalBaseItem.PSTATUS_ACCEPT; 1217 toastMessage = ZmMsg.inviteAccepted; 1218 break; 1219 case ZmOperation.REPLY_DECLINE_IGNORE: //falls-through on purpose 1220 needsRsvp = false; 1221 case ZmOperation.REPLY_DECLINE_NOTIFY: //falls-through on purpose 1222 case ZmOperation.REPLY_DECLINE: 1223 verb = "DECLINE"; 1224 newPtst = ZmCalBaseItem.PSTATUS_DECLINED; 1225 toastMessage = ZmMsg.inviteDeclined; 1226 break; 1227 case ZmOperation.REPLY_TENTATIVE_IGNORE: //falls-through on purpose 1228 needsRsvp = false; 1229 case ZmOperation.REPLY_TENTATIVE_NOTIFY: //falls-through on purpose 1230 case ZmOperation.REPLY_TENTATIVE: 1231 verb = "TENTATIVE"; 1232 newPtst = ZmCalBaseItem.PSTATUS_TENTATIVE; 1233 toastMessage = ZmMsg.inviteAcceptedTentatively; 1234 break; 1235 } 1236 request.verb = verb; 1237 1238 var inv = this._origMsg.invite; 1239 //update the ptst to new one (we currently don't use the rest of the info in "replies" so it's ok to remove it for now) 1240 //note - this updated value is used later in _handleResponseSendInviteReply, and also in the list view when 1241 // re-displaying the message (not reloaded from server) 1242 if (newPtst) { 1243 inv.replies = [{ 1244 reply: [{ 1245 ptst: newPtst 1246 }] 1247 }]; 1248 if (appCtxt.isWebClientOffline()) { 1249 // Update the offline entry and appt too. Depending upon whether this is invoked from mail or appointments, 1250 // msgId will either be a single id, or the composite msg-appt id 1251 var msgId = inv.getMessageId(); 1252 var invId = msgId; 1253 if (msgId.indexOf("-") >= 0) { 1254 // Composite id 1255 msgId = msgId.split("-")[1]; 1256 } else { 1257 invId = [inv.getAppointmentId(), msgId].join("-"); 1258 } 1259 var inviteUpdateCallback = this.applyPtstOffline.bind(this, msgId, newPtst); 1260 appCtxt.updateOfflineAppt(invId, "ptst", newPtst, null, inviteUpdateCallback); 1261 } 1262 } 1263 if (this.getAddress(AjxEmailAddress.TO) == null && !inv.isOrganizer()) { 1264 var to = inv.getOrganizerEmail() || inv.getSentBy(); 1265 if (to == null) { 1266 var ac = window.parentAppCtxt || window.appCtxt; 1267 var mainAcct = ac.accountList.mainAccount.getEmail(); 1268 var from = this._origMsg.getAddresses(AjxEmailAddress.FROM).get(0); 1269 //bug: 33639 when organizer component is missing from invitation 1270 if (from && from.address != mainAcct) { 1271 to = from.address; 1272 } 1273 } 1274 if (to) { 1275 this.setAddress(AjxEmailAddress.TO, (new AjxEmailAddress(to))); 1276 } 1277 } 1278 1279 if(!this.identity) { 1280 var ac = window.parentAppCtxt || window.appCtxt; 1281 var account = (ac.multiAccounts && ac.getActiveAccount().isMain) 1282 ? ac.accountList.defaultAccount : null; 1283 var identityCollection = ac.getIdentityCollection(account); 1284 this.identity = identityCollection ? identityCollection.selectIdentity(this._origMsg) : null; 1285 } 1286 1287 if (this.identity) { 1288 request.idnt = this.identity.id; 1289 } 1290 1291 if (ignoreNotify) { //bug 53974 1292 needsRsvp = false; 1293 } 1294 this._sendInviteReplyContinue(jsonObj, needsRsvp ? "TRUE" : "FALSE", edited, callback, errorCallback, instanceDate, accountName, toastMessage); 1295 }; 1296 1297 ZmMailMsg.prototype.applyPtstOffline = function(msgId, newPtst) { 1298 var applyPtstOfflineCallback = this._applyPtstOffline.bind(this, newPtst); 1299 ZmOfflineDB.getItem(msgId, ZmApp.MAIL, applyPtstOfflineCallback); 1300 }; 1301 ZmMailMsg.prototype._applyPtstOffline = function(newPtst, result) { 1302 if (result && result[0] && result[0].inv && result[0].inv[0]) { 1303 var inv = result[0].inv[0]; 1304 if (!inv.replies) { 1305 // See _sendInviteReply - patch the invite status 1306 inv.replies = [{ 1307 reply: [{ 1308 ptst: newPtst 1309 }] 1310 }]; 1311 } else { 1312 inv.replies[0].reply[0].ptst = newPtst; 1313 } 1314 // Finally, Alter the offline folder - upon accepting an invite, it moves to the Trash folder 1315 result[0].l = ZmFolder.ID_TRASH; 1316 ZmOfflineDB.setItem(result, ZmApp.MAIL); 1317 1318 // With the Ptst of an invite altered offline, move the message to trash locally 1319 var originalMsg = this._origMsg; 1320 originalMsg.moveLocal(ZmFolder.ID_TRASH); 1321 if (originalMsg.list) { 1322 originalMsg.list.moveLocal([originalMsg], ZmFolder.ID_TRASH); 1323 } 1324 var details = {oldFolderId:originalMsg.folderId}; 1325 originalMsg._notify(ZmEvent.E_MOVE, details); 1326 1327 } 1328 } 1329 1330 ZmMailMsg.prototype._sendInviteReplyContinue = 1331 function(jsonObj, updateOrganizer, edited, callback, errorCallback, instanceDate, accountName, toastMessage) { 1332 1333 var request = jsonObj.SendInviteReplyRequest; 1334 request.updateOrganizer = updateOrganizer; 1335 1336 if (instanceDate) { 1337 var serverDateTime = AjxDateUtil.getServerDateTime(instanceDate); 1338 var timeZone = AjxTimezone.getServerId(AjxTimezone.DEFAULT); 1339 var clientId = AjxTimezone.getClientId(timeZone); 1340 ZmTimezone.set(request, clientId, null, true); 1341 request.exceptId = {d:serverDateTime, tz:timeZone}; 1342 } 1343 1344 if (edited) { 1345 this._createMessageNode(request, null, accountName); 1346 } 1347 1348 var respCallback = new AjxCallback(this, this._handleResponseSendInviteReply, [callback, toastMessage]); 1349 this._sendMessage({ jsonObj:jsonObj, 1350 isInvite:true, 1351 isDraft:false, 1352 callback:respCallback, 1353 errorCallback:errorCallback, 1354 accountName:accountName 1355 }); 1356 }; 1357 1358 ZmMailMsg.prototype._handleResponseSendInviteReply = 1359 function(callback, toastMessage, result) { 1360 var resp = result.getResponse(); 1361 1362 var id = resp.id ? resp.id.split("-")[0] : null; 1363 var statusOK = (id || resp.status == "OK"); 1364 1365 if (statusOK) { 1366 this._notifySendListeners(); 1367 this._origMsg.folderId = ZmFolder.ID_TRASH; 1368 } 1369 1370 // allow or disallow move logic: 1371 var allowMove; 1372 if ((this.acceptFolderId != ZmOrganizer.ID_CALENDAR) || 1373 (appCtxt.multiAccounts && 1374 !this.getAccount().isMain && 1375 this.acceptFolderId == ZmOrganizer.ID_CALENDAR)) 1376 { 1377 allowMove = true; 1378 } 1379 1380 if (this.acceptFolderId && allowMove && resp.apptId != null) { 1381 this.moveApptItem(resp.apptId, this.acceptFolderId); 1382 } 1383 1384 if (window.parentController) { 1385 window.close(); 1386 } 1387 1388 if (toastMessage) { 1389 //note - currently this is not called from child window, but just in case it will in the future. 1390 var ctxt = window.parentAppCtxt || window.appCtxt; //show on parent window if this is a child window, since we close this child window on accept/decline/etc 1391 ctxt.setStatusMsg(toastMessage); 1392 } 1393 1394 if (callback) { 1395 callback.run(result, this._origMsg.getPtst()); // the ptst was updated in _sendInviteReply 1396 } 1397 }; 1398 1399 /** 1400 * returns this user's reply to this invite. 1401 */ 1402 ZmMailMsg.prototype.getPtst = 1403 function() { 1404 return this.invite && this.invite.replies && this.invite.replies[0].reply[0].ptst; 1405 }; 1406 1407 ZmMailMsg.APPT_TRASH_FOLDER = 3; 1408 1409 ZmMailMsg.prototype.isInviteCanceled = 1410 function() { 1411 var invite = this.invite; 1412 if (!invite) { 1413 return false; 1414 } 1415 return invite.components[0].ciFolder == ZmMailMsg.APPT_TRASH_FOLDER; 1416 }; 1417 1418 ZmMailMsg.prototype.moveApptItem = 1419 function(itemId, nfolder) { 1420 var callback = new AjxCallback(this, this._handleMoveApptResponse, [nfolder]); 1421 var errorCallback = new AjxCallback(this, this._handleMoveApptError, [nfolder]); 1422 var ac = window.parentAppCtxt || window.appCtxt; 1423 var accountName = ac.multiAccounts && ac.accountList.mainAccount.name; 1424 ZmItem.move(itemId, nfolder, callback, errorCallback, accountName); 1425 }; 1426 1427 ZmMailMsg.prototype._handleMoveApptResponse = 1428 function(nfolder, resp) { 1429 this._lastApptFolder = nfolder; 1430 // TODO: Display some sort of confirmation? 1431 }; 1432 1433 ZmMailMsg.prototype._handleMoveApptError = 1434 function(nfolder, resp) { 1435 var params = { 1436 msg: ZmMsg.errorMoveAppt, 1437 level: ZmStatusView.LEVEL_CRITICAL 1438 }; 1439 appCtxt.setStatusMsg(params); 1440 return true; 1441 }; 1442 1443 /** 1444 * Sends the message. 1445 * 1446 * @param {Boolean} isDraft if <code>true</code>, this a draft 1447 * @param {AjxCallback} callback the callback to trigger after send 1448 * @param {AjxCallback} errorCallback the error callback to trigger 1449 * @param {String} accountName the account to send on behalf of 1450 * @param {Boolean} noSave if set, a copy will *not* be saved to sent regardless of account/identity settings 1451 * @param {Boolean} requestReadReceipt if set, a read receipt is sent to *all* recipients 1452 * @param {ZmBatchCommand} batchCmd if set, request gets added to this batch command 1453 * @param {Date} sendTime if set, tell server that this message should be sent at the specified time 1454 * @param {Boolean} isAutoSave if <code>true</code>, this an auto-save draft 1455 */ 1456 ZmMailMsg.prototype.send = 1457 function(isDraft, callback, errorCallback, accountName, noSave, requestReadReceipt, batchCmd, sendTime, isAutoSave) { 1458 1459 var aName = accountName; 1460 if (!aName) { 1461 // only set the account name if this *isnt* the main/parent account 1462 var acct = appCtxt.getActiveAccount(); 1463 if (acct && !acct.isMain) { 1464 aName = acct.name; 1465 } 1466 } 1467 // if we have an invite reply, we have to send a different message 1468 if (this.isInviteReply && !isDraft) { 1469 // TODO: support for batchCmd here as well 1470 return this.sendInviteReply(true, 0, callback, errorCallback, this._instanceDate, aName, false); 1471 } else { 1472 var jsonObj, request; 1473 if (isDraft) { 1474 jsonObj = {SaveDraftRequest:{_jsns:"urn:zimbraMail"}}; 1475 request = jsonObj.SaveDraftRequest; 1476 } else { 1477 jsonObj = {SendMsgRequest:{_jsns:"urn:zimbraMail"}}; 1478 request = jsonObj.SendMsgRequest; 1479 if (this.sendUID) { 1480 request.suid = this.sendUID; 1481 } 1482 } 1483 if (noSave) { 1484 request.noSave = 1; 1485 } 1486 this._createMessageNode(request, isDraft, aName, requestReadReceipt, sendTime); 1487 appCtxt.notifyZimlets("addExtraMsgParts", [request, isDraft]); 1488 var params = { 1489 jsonObj: jsonObj, 1490 isInvite: false, 1491 isDraft: isDraft, 1492 isAutoSave: isAutoSave, 1493 accountName: aName, 1494 callback: (new AjxCallback(this, this._handleResponseSend, [isDraft, callback])), 1495 errorCallback: errorCallback, 1496 batchCmd: batchCmd, 1497 skipOfflineCheck: true 1498 }; 1499 this._sendMessage(params); 1500 } 1501 }; 1502 1503 ZmMailMsg.prototype._handleResponseSend = 1504 function(isDraft, callback, result) { 1505 var resp = result.getResponse().m[0]; 1506 1507 // notify listeners of successful send message 1508 if (!isDraft) { 1509 if (resp.id || !appCtxt.get(ZmSetting.SAVE_TO_SENT)) { 1510 this._notifySendListeners(); 1511 } 1512 } else { 1513 this._loadFromDom(resp); 1514 if (resp.autoSendTime) { 1515 this._notifySendListeners(); 1516 } 1517 } 1518 1519 if (callback) { 1520 callback.run(result); 1521 } 1522 }; 1523 1524 ZmMailMsg.prototype._createMessageNode = 1525 function(request, isDraft, accountName, requestReadReceipt, sendTime) { 1526 1527 var msgNode = request.m = {}; 1528 var ac = window.parentAppCtxt || window.appCtxt; 1529 var activeAccount = ac.accountList.activeAccount; 1530 var mainAccount = ac.accountList.mainAccount; 1531 1532 //When fwding an email in Parent's(main) account(main == active), but we are sending on-behalf-of child(active != accountName) 1533 var doQualifyIds = !ac.isOffline && accountName && mainAccount.name !== accountName; 1534 1535 // if origId is given, means we're saving a draft or sending a msg that was 1536 // originally a reply/forward 1537 if (this.origId) { 1538 // always Qualify ID when forwarding mail using a child account 1539 if (appCtxt.isOffline) { 1540 var origAccount = this._origMsg && this._origMsg.getAccount(); 1541 doQualifyIds = ac.multiAccounts && origAccount.id == mainAccount.id; 1542 } 1543 var id = this.origId; 1544 if(doQualifyIds) { 1545 id = ZmOrganizer.getSystemId(this.origId, mainAccount, true); 1546 } 1547 msgNode.origid = id; 1548 } 1549 // if id is given, means we are re-saving a draft 1550 var oboDraftMsgId = null; // On Behalf of Draft MsgId 1551 if ((isDraft || this.isDraft) && this.id) { 1552 // bug fix #26508 - check whether previously saved draft was moved to Trash 1553 var msg = ac.getById(this.id); 1554 var folder = msg ? ac.getById(msg.folderId) : null; 1555 if (!folder || (folder && !folder.isInTrash())) { 1556 if (!ac.isOffline && !isDraft && this._origMsg && this._origMsg.isDraft) { 1557 var defaultAcct = ac.accountList.defaultAccount || ac.accountList.mainAccount; 1558 var from = this._origMsg.getAddresses(AjxEmailAddress.FROM).get(0); 1559 // this means we're sending a draft msg obo 1560 if (from && from.address != defaultAcct.getEmail()) { 1561 oboDraftMsgId = (this.id.indexOf(":") == -1) 1562 ? ([defaultAcct.id, ":", this.id].join("")) : this.id; 1563 msgNode.id = oboDraftMsgId; 1564 } else { 1565 msgNode.id = this.nId; 1566 } 1567 } else { 1568 msgNode.id = this.nId; 1569 } 1570 1571 if (!isDraft) { // not saveDraftRequest 1572 var did = this.nId || this.id; // set draft id 1573 if (doQualifyIds) { 1574 did = ZmOrganizer.getSystemId(did, mainAccount, true); 1575 } 1576 msgNode.did = did; 1577 } 1578 } 1579 } 1580 1581 if (this.isForwarded) { 1582 msgNode.rt = "w"; 1583 } else if (this.isReplied) { 1584 msgNode.rt = "r"; 1585 } 1586 if (this.identity) { 1587 msgNode.idnt = this.identity.id; 1588 } 1589 1590 if (this.isHighPriority) { 1591 msgNode.f = ZmItem.FLAG_HIGH_PRIORITY; 1592 } else if (this.isLowPriority) { 1593 msgNode.f = ZmItem.FLAG_LOW_PRIORITY; 1594 } 1595 1596 if (this.isPriority) { 1597 msgNode.f = ZmItem.FLAG_PRIORITY; 1598 } 1599 1600 if (this.isOfflineCreated) { 1601 msgNode.f = msgNode.f || ""; 1602 if (msgNode.f.indexOf(ZmItem.FLAG_OFFLINE_CREATED) === -1) { 1603 msgNode.f = msgNode.f + ZmItem.FLAG_OFFLINE_CREATED; 1604 } 1605 } 1606 1607 var addrNodes = msgNode.e = []; 1608 for (var i = 0; i < ZmMailMsg.COMPOSE_ADDRS.length; i++) { 1609 var type = ZmMailMsg.COMPOSE_ADDRS[i]; 1610 this._addAddressNodes(addrNodes, type, isDraft); 1611 } 1612 this._addFrom(addrNodes, msgNode, isDraft, accountName); 1613 this._addReplyTo(addrNodes); 1614 if (requestReadReceipt) { 1615 this._addReadReceipt(addrNodes, accountName); 1616 } 1617 if (addrNodes.length) { 1618 msgNode.e = addrNodes; 1619 } 1620 1621 //Let Zimlets set custom mime headers. They need to push header-name and header-value like below: 1622 //customMimeHeaders.push({name:"header1", _content:"headerValue"}) 1623 var customMimeHeaders = []; 1624 appCtxt.notifyZimlets("addCustomMimeHeaders", [customMimeHeaders]); 1625 if((customMimeHeaders instanceof Array) && customMimeHeaders.length > 0) { 1626 msgNode.header = customMimeHeaders; 1627 } 1628 msgNode.su = {_content:this.subject}; 1629 1630 var topNode = {ct:this._topPart.getContentType()}; 1631 msgNode.mp = [topNode]; 1632 1633 // if the top part has sub parts, add them as children 1634 var numSubParts = this._topPart.children ? this._topPart.children.size() : 0; 1635 if (numSubParts > 0) { 1636 var partNodes = topNode.mp = []; 1637 for (var i = 0; i < numSubParts; i++) { 1638 var part = this._topPart.children.get(i); 1639 var content = part.getContent(); 1640 var numSubSubParts = part.children ? part.children.size() : 0; 1641 if (content == null && numSubSubParts == 0) { continue; } 1642 1643 var partNode = {ct:part.getContentType()}; 1644 1645 if (numSubSubParts > 0) { 1646 // If each part again has subparts, add them as children 1647 var subPartNodes = partNode.mp = []; 1648 for (var j = 0; j < numSubSubParts; j++) { 1649 var subPart = part.children.get(j); 1650 subPartNodes.push({ct:subPart.getContentType(), content:{_content:subPart.getContent()}}); 1651 } 1652 // Handle Related SubPart , a specific condition 1653 if (part.getContentType() == ZmMimeTable.MULTI_RELATED) { 1654 // Handle Inline Attachments 1655 var inlineAtts = this.getInlineAttachments() || []; 1656 if (inlineAtts.length) { 1657 for (j = 0; j < inlineAtts.length; j++) { 1658 var inlineAttNode = {ci:inlineAtts[j].cid}; 1659 var attachNode = inlineAttNode.attach = {}; 1660 if (inlineAtts[j].aid) { 1661 attachNode.aid = inlineAtts[j].aid; 1662 } else { 1663 var id = inlineAtts[j].mid 1664 || (isDraft || this.isDraft) 1665 ? (oboDraftMsgId || this.id || this.origId) 1666 : (this.origId || this.id); 1667 1668 if (!id && this._origMsg) { 1669 id = this._origMsg.id; 1670 } 1671 if (id && doQualifyIds) { 1672 id = ZmOrganizer.getSystemId(id, mainAccount, true); 1673 } 1674 if(id) { 1675 attachNode.mp = [{mid:id, part:inlineAtts[j].part}]; 1676 } 1677 } 1678 subPartNodes.push(inlineAttNode); 1679 } 1680 } 1681 // Handle Inline Attachments 1682 var inlineDocAtts = this.getInlineDocAttachments() || []; 1683 if (inlineDocAtts.length) { 1684 for (j = 0; j < inlineDocAtts.length; j++) { 1685 var inlineDocAttNode = {ci:inlineDocAtts[j].cid}; 1686 var attachNode = inlineDocAttNode.attach = {}; 1687 if (inlineDocAtts[j].docpath) { 1688 attachNode.doc = [{path: inlineDocAtts[j].docpath, optional:1 }]; 1689 } else if (inlineDocAtts[j].docid) { 1690 attachNode.doc = [{id: inlineDocAtts[j].docid}]; 1691 } 1692 subPartNodes.push(inlineDocAttNode); 1693 } 1694 } 1695 } 1696 } else { 1697 partNode.content = {_content:content}; 1698 } 1699 partNodes.push(partNode); 1700 } 1701 } else { 1702 topNode.content = {_content:this._topPart.getContent()}; 1703 } 1704 1705 if (this.irtMessageId) { 1706 msgNode.irt = {_content:this.irtMessageId}; 1707 } 1708 1709 if (this.attId || 1710 (this._msgAttIds && this._msgAttIds.length) || 1711 (this._docAtts && this._docAtts.length) || 1712 (this._forAttIds && this._forAttIds.length) || 1713 (this._contactAttIds && this._contactAttIds.length)) 1714 { 1715 var attachNode = msgNode.attach = {}; 1716 if (this.attId) { 1717 attachNode.aid = this.attId; 1718 } 1719 1720 // attach mail msgs 1721 if (this._msgAttIds && this._msgAttIds.length) { 1722 var msgs = attachNode.m = []; 1723 for (var i = 0; i < this._msgAttIds.length; i++) { 1724 msgs.push({id:this._msgAttIds[i]}); 1725 } 1726 } 1727 1728 1729 // attach docs 1730 if (this._docAtts) { 1731 var docs = attachNode.doc = []; 1732 for (var i = 0; i < this._docAtts.length; i++) { 1733 var d = this._docAtts[i]; 1734 // qualify doc id 1735 var docId = (d.id.indexOf(":") == -1) 1736 ? ([mainAccount.id, d.id].join(":")) : d.id; 1737 var props = {id: docId}; 1738 if(d.ver) props.ver = d.ver; 1739 docs.push(props); 1740 } 1741 } 1742 1743 // attach msg attachments 1744 if (this._forAttObjs && this._forAttObjs.length) { 1745 var parts = attachNode.mp = this._forAttObjs; 1746 if (doQualifyIds) { 1747 for (var i = 0; i < parts.length; i++) { 1748 var part = parts[i]; 1749 part.mid = ZmOrganizer.getSystemId(part.mid, mainAccount, true); 1750 } 1751 } 1752 } 1753 1754 if (this._contactAttIds && this._contactAttIds.length) { 1755 attachNode.cn = []; 1756 for (var i = 0; i < this._contactAttIds.length; i++) { 1757 attachNode.cn.push({id:this._contactAttIds[i]}); 1758 } 1759 } 1760 } 1761 1762 if (sendTime && sendTime.date) { 1763 var date = sendTime.date; // See ZmTimeDialog.prototype.getValue 1764 var timezone = sendTime.timezone || AjxTimezone.DEFAULT; 1765 var offset = AjxTimezone.getOffset(timezone, date); 1766 var utcEpochTime = date.getTime() - ((date.getTimezoneOffset() + offset) * 60 * 1000); 1767 // date.getTime() is the selected timestamp in local machine time (NOT UTC) 1768 // date.getTimezoneOffset() is negative minutes to UTC from local time (+ for West, - for East) 1769 // offset is minutes to UTC from selected time (- for West, + for East) 1770 msgNode.autoSendTime = utcEpochTime; 1771 } 1772 }; 1773 1774 /** 1775 * Sends this message to its recipients. 1776 * 1777 * @param params [hash] hash of params: 1778 * jsonObj [object] JSON object 1779 * isInvite [boolean] true if this message is an invite 1780 * isDraft [boolean] true if this message is a draft 1781 * callback [AjxCallback] async callback 1782 * errorCallback [AjxCallback] async error callback 1783 * batchCmd [ZmBatchCommand] if set, request gets added to this batch command 1784 * 1785 * @private 1786 */ 1787 ZmMailMsg.prototype._sendMessage = 1788 function(params) { 1789 var respCallback = new AjxCallback(this, this._handleResponseSendMessage, [params]), 1790 offlineCallback = this._handleOfflineResponseSendMessage.bind(this, params); 1791 /* bug fix 63798 removing sync request and making it async 1792 // bug fix #4325 - its safer to make sync request when dealing w/ new window 1793 if (window.parentController) { 1794 var newParams = { 1795 jsonObj: params.jsonObj, 1796 accountName: params.accountName, 1797 errorCallback: params.errorCallback 1798 }; 1799 var resp = appCtxt.getAppController().sendRequest(newParams); 1800 if (!resp) { return; } // bug fix #9154 1801 if (params.toastMessage) { 1802 parentAppCtxt.setStatusMsg(params.toastMessage); //show on parent window since this is a child window, since we close this child window on accept/decline/etc 1803 } 1804 1805 if (resp.SendInviteReplyResponse) { 1806 return resp.SendInviteReplyResponse; 1807 } else if (resp.SaveDraftResponse) { 1808 resp = resp.SaveDraftResponse; 1809 this._loadFromDom(resp.m[0]); 1810 return resp; 1811 } else if (resp.SendMsgResponse) { 1812 return resp.SendMsgResponse; 1813 } 1814 } else if (params.batchCmd) {*/ 1815 if (params.batchCmd) { 1816 params.batchCmd.addNewRequestParams(params.jsonObj, respCallback, params.errorCallback); 1817 } else { 1818 appCtxt.getAppController().sendRequest({jsonObj:params.jsonObj, 1819 asyncMode:true, 1820 noBusyOverlay:params.isDraft && params.isAutoSave, 1821 callback:respCallback, 1822 errorCallback:params.errorCallback, 1823 offlineCallback:offlineCallback, 1824 accountName:params.accountName, 1825 timeout: ( ( params.isDraft && this.attId ) ? 0 : null ) 1826 }); 1827 } 1828 }; 1829 1830 ZmMailMsg.prototype._handleResponseSendMessage = 1831 function(params, result) { 1832 var response = result.getResponse(); 1833 if (params.isInvite) { 1834 result.set(response.SendInviteReplyResponse); 1835 } else if (params.isDraft) { 1836 result.set(response.SaveDraftResponse); 1837 } else { 1838 result.set(response.SendMsgResponse); 1839 } 1840 if (params.callback) { 1841 params.callback.run(result); 1842 } 1843 }; 1844 1845 ZmMailMsg.prototype._handleOfflineResponseSendMessage = 1846 function(params) { 1847 1848 var jsonObj = $.extend(true, {}, params.jsonObj),//Always clone the object 1849 methodName = Object.keys(jsonObj)[0], 1850 msgNode = jsonObj[methodName].m, 1851 msgNodeAttach = msgNode.attach, 1852 origMsg = this._origMsg, 1853 currentTime = new Date().getTime(), 1854 callback, 1855 aid = []; 1856 var folderId = this.getFolderId(); 1857 1858 jsonObj.methodName = methodName; 1859 msgNode.d = currentTime; //for displaying date and time in the outbox/Drafts folder 1860 1861 if (msgNodeAttach && msgNodeAttach.aid) { 1862 var msgNodeAttachIds = msgNodeAttach.aid.split(","); 1863 for (var i = 0; i < msgNodeAttachIds.length; i++) { 1864 var msgNodeAttachId = msgNodeAttachIds[i]; 1865 if (msgNodeAttachId) { 1866 aid.push(msgNodeAttachId); 1867 msgNodeAttach[msgNodeAttachId] = appCtxt.getById(msgNodeAttachId); 1868 appCtxt.cacheRemove(msgNodeAttachId); 1869 } 1870 } 1871 } 1872 1873 if (origMsg && origMsg.hasAttach) {//Always append origMsg attachments for offline handling 1874 var origMsgAttachments = origMsg.attachments; 1875 if (msgNodeAttach) { 1876 delete msgNodeAttach.mp;//Have to rewrite the code for including original attachments 1877 } else { 1878 msgNodeAttach = msgNode.attach = {}; 1879 } 1880 for (var j = 0; j < origMsgAttachments.length; j++) { 1881 var node = origMsgAttachments[j].node; 1882 if (node && node.isOfflineUploaded) { 1883 aid.push(node.aid); 1884 msgNodeAttach[node.aid] = node; 1885 } 1886 } 1887 } 1888 1889 if (msgNodeAttach) { 1890 if (aid.length > 0) { 1891 msgNodeAttach.aid = aid.join(); 1892 } 1893 //If msgNodeAttach is an empty object then delete it 1894 if (Object.keys(msgNodeAttach).length === 0) { 1895 delete msgNode.attach; 1896 } 1897 } 1898 1899 // Checking for inline Attachment 1900 if (this.getInlineAttachments().length > 0 || (origMsg && origMsg.getInlineAttachments().length > 0)) { 1901 msgNode.isInlineAttachment = true; 1902 } 1903 1904 callback = this._handleOfflineResponseSendMessageCallback.bind(this, params, jsonObj); 1905 1906 //For outbox item, message id will be always undefined. 1907 if (folderId == ZmFolder.ID_OUTBOX) { 1908 msgNode.id = origMsg && origMsg.id; 1909 } 1910 if (msgNode.id) { //Existing drafts created online or offline 1911 jsonObj.id = msgNode.id; 1912 var value = { 1913 update : true, 1914 methodName : methodName, 1915 id : msgNode.id, 1916 value : jsonObj 1917 }; 1918 ZmOfflineDB.setItemInRequestQueue(value, callback); 1919 } 1920 else { 1921 jsonObj.id = msgNode.id = currentTime.toString(); //Id should be string 1922 msgNode.f = (msgNode.f || "").replace(ZmItem.FLAG_OFFLINE_CREATED, "").concat(ZmItem.FLAG_OFFLINE_CREATED); 1923 ZmOfflineDB.setItemInRequestQueue(jsonObj, callback); 1924 } 1925 }; 1926 1927 ZmMailMsg.prototype._handleOfflineResponseSendMessageCallback = 1928 function(params, jsonObj) { 1929 1930 var m = ZmOffline.generateMsgResponse(jsonObj); 1931 var data = {}, 1932 header = this._generateOfflineHeader(params, jsonObj, m), 1933 notify = header.context.notify[0], 1934 result; 1935 if (!params.isInvite) { 1936 // If existing invite message - do not overwrite it. The online code does not reload 1937 // the invite msg, it just patches it in-memory. When the cal item ptst is patched in the db, it will 1938 // make a call to patch the invite too. 1939 ZmOfflineDB.setItem(m, ZmApp.MAIL); 1940 } 1941 1942 data[jsonObj.methodName.replace("Request", "Response")] = notify.modified; 1943 result = new ZmCsfeResult(data, false, header); 1944 this._handleResponseSendMessage(params, result); 1945 appCtxt.getRequestMgr()._notifyHandler(notify); 1946 1947 if (!params.isDraft && !params.isInvite) { 1948 var key = { 1949 methodName : "SaveDraftRequest", 1950 id : jsonObj[jsonObj.methodName].m.id 1951 }; 1952 ZmOfflineDB.deleteItemInRequestQueue(key);//Delete any drafts for this message id 1953 } 1954 }; 1955 1956 ZmMailMsg.prototype._generateOfflineHeader = 1957 function(params, jsonObj, m) { 1958 1959 var folderArray = [], 1960 header = { 1961 context : { 1962 notify : [{ 1963 created : { 1964 m : m 1965 }, 1966 modified : { 1967 folder : folderArray, 1968 m : m 1969 } 1970 }] 1971 } 1972 }; 1973 1974 if (!params.isInvite) { 1975 var folderId = this.getFolderId(); 1976 if (params.isDraft || params.isAutoSave) { 1977 //For new auto save or draft folderId will not be equal to ZmFolder.ID_DRAFTS 1978 if (folderId != ZmFolder.ID_DRAFTS) { 1979 folderArray.push({ 1980 id : ZmFolder.ID_DRAFTS, 1981 n : appCtxt.getById(ZmFolder.ID_DRAFTS).numTotal + 1 1982 }); 1983 } 1984 } 1985 else { 1986 if (folderId != ZmFolder.ID_OUTBOX) { 1987 folderArray.push({ 1988 id : ZmFolder.ID_OUTBOX, 1989 n : appCtxt.getById(ZmFolder.ID_OUTBOX).numTotal + 1 1990 }); 1991 } 1992 if (folderId == ZmFolder.ID_DRAFTS) { 1993 folderArray.push({ 1994 id : ZmFolder.ID_DRAFTS, 1995 n : appCtxt.getById(ZmFolder.ID_DRAFTS).numTotal - 1 1996 }); 1997 } 1998 } 1999 } 2000 return header; 2001 }; 2002 2003 ZmMailMsg.prototype._notifySendListeners = 2004 function() { 2005 var flag, msg; 2006 if (this.isForwarded) { 2007 flag = ZmItem.FLAG_FORWARDED; 2008 msg = this._origMsg; 2009 } else if (this.isReplied) { 2010 flag = ZmItem.FLAG_REPLIED; 2011 msg = this._origMsg; 2012 } 2013 2014 if (flag && msg) { 2015 msg[ZmItem.FLAG_PROP[flag]] = true; 2016 if (msg.list) { 2017 msg.list._notify(ZmEvent.E_FLAGS, {items: [msg.list], flags: [flag]}); 2018 } 2019 } 2020 }; 2021 2022 /** 2023 * from a child window - since we clone the message, the cloned message needs to listen to changes on the original (parent window) message. 2024 * @param ev 2025 */ 2026 ZmMailMsg.prototype.detachedChangeListener = 2027 function(ev) { 2028 var parentWindowMsg = ev.item; 2029 //for now I only need it for keeping up with the isUnread and isFlagged status of the detached message. Keep it simple. 2030 this.isUnread = parentWindowMsg.isUnread; 2031 this.isFlagged = parentWindowMsg.isFlagged; 2032 }; 2033 2034 2035 2036 ZmMailMsg.prototype.isRealAttachment = 2037 function(attachment) { 2038 var type = attachment.contentType; 2039 2040 // bug fix #6374 - ignore if attachment is body unless content type is message/rfc822 2041 if (ZmMimeTable.isIgnored(type)) { 2042 return false; 2043 } 2044 2045 // bug fix #8751 - dont ignore text/calendar type if msg is not an invite 2046 if (type == ZmMimeTable.TEXT_CAL && appCtxt.get(ZmSetting.CALENDAR_ENABLED) && this.isInvite()) { 2047 return false; 2048 } 2049 2050 return true; 2051 }; 2052 2053 // this is a helper method to get an attachment url for multipart/related content 2054 ZmMailMsg.prototype.getContentPartAttachUrl = 2055 function(contentPartType, contentPart) { 2056 if (contentPartType != ZmMailMsg.CONTENT_PART_ID && 2057 contentPartType != ZmMailMsg.CONTENT_PART_LOCATION) { 2058 return null; 2059 } 2060 var url = this._getContentPartAttachUrlFromCollection(this.attachments, contentPartType, contentPart); 2061 if (url) { 2062 return url; 2063 } 2064 return this._getContentPartAttachUrlFromCollection(this._bodyParts, contentPartType, contentPart); 2065 }; 2066 2067 ZmMailMsg.prototype._getContentPartAttachUrlFromCollection = 2068 function(collection, contentPartType, contentPart) { 2069 if (!collection) { 2070 return null; 2071 } 2072 for (var i = 0; i < collection.length; i++) { 2073 var attach = collection[i]; 2074 if (attach[contentPartType] == contentPart) { 2075 return this.getUrlForPart(attach); 2076 } 2077 } 2078 return null; 2079 }; 2080 2081 2082 ZmMailMsg.prototype.findAttsFoundInMsgBody = 2083 function() { 2084 if (this.findAttsFoundInMsgBodyDone) { return; } 2085 2086 var content = "", cid; 2087 var bodyParts = this.getBodyParts(); 2088 for (var i = 0; i < bodyParts.length; i++) { 2089 var bodyPart = bodyParts[i]; 2090 if (bodyPart.contentType == ZmMimeTable.TEXT_HTML) { 2091 content = bodyPart.getContent(); 2092 var msgRef = this; 2093 content.replace(/src=([\x27\x22])cid:([^\x27\x22]+)\1/ig, function(s, q, cid) { 2094 var attach = msgRef.findInlineAtt("<" + AjxStringUtil.urlComponentDecode(cid) + ">"); 2095 if (attach) { 2096 attach.foundInMsgBody = true; 2097 } 2098 }); 2099 } 2100 } 2101 this.findAttsFoundInMsgBodyDone = true; 2102 }; 2103 2104 ZmMailMsg.prototype.hasInlineImagesInMsgBody = 2105 function() { 2106 var body = this.getBodyContent(ZmMimeTable.TEXT_HTML); 2107 return (body && body.search(/src=([\x27\x22])cid:([^\x27\x22]+)\1/ig) != -1); 2108 }; 2109 2110 /** 2111 * Returns the number of attachments in this msg. 2112 * 2113 * @param {boolean} includeInlineAtts 2114 */ 2115 ZmMailMsg.prototype.getAttachmentCount = 2116 function(includeInlineAtts) { 2117 var attachments = includeInlineAtts ? [].concat(this.attachments, this._getInlineAttachments()) : this.attachments; 2118 return attachments ? attachments.length : 0; 2119 }; 2120 2121 ZmMailMsg.prototype._getInlineAttachments = 2122 function() { 2123 var atts = []; 2124 var parts = this.getBodyParts(); 2125 if (parts && parts.length > 1) { 2126 var part; 2127 for (var k = 0; k < parts.length; k++) { 2128 part = parts[k]; 2129 if (part.fileName && part.contentDisposition == "inline") { 2130 atts.push(part); 2131 } 2132 } 2133 } 2134 return atts; 2135 }; 2136 2137 /** 2138 * Returns an array of objects containing meta info about attachments 2139 */ 2140 ZmMailMsg.prototype.getAttachmentInfo = 2141 function(findHits, includeInlineImages, includeInlineAtts) { 2142 2143 this._attInfo = []; 2144 2145 var attachments = (includeInlineAtts || includeInlineImages) ? [].concat(this.attachments, this._getInlineAttachments()) : this.attachments; 2146 if (attachments && attachments.length > 0) { 2147 this.findAttsFoundInMsgBody(); 2148 2149 for (var i = 0; i < attachments.length; i++) { 2150 var attach = attachments[i]; 2151 2152 if (!this.isRealAttachment(attach) || 2153 (attach.contentType.match(/^image/) && attach.contentId && attach.foundInMsgBody && !includeInlineImages) || 2154 (attach.contentDisposition == "inline" && attach.fileName && ZmMimeTable.isRenderable(attach.contentType, true) && !includeInlineAtts)) { 2155 continue; 2156 } 2157 2158 var props = {}; 2159 props.links = {}; // flags that indicate whether to include a certain type of link 2160 2161 // set a viable label for this attachment 2162 props.label = attach.name || attach.fileName || (ZmMsg.unknown + " <" + attach.contentType + ">"); 2163 2164 // use content location instead of built href flag 2165 var useCL = false; 2166 // set size info if any 2167 props.sizeInBytes = attach.s || 0; 2168 if (attach.size != null && attach.size >= 0) { 2169 var numFormatter = AjxNumberFormat.getInstance(); 2170 if (attach.size < 1024) { 2171 props.size = numFormatter.format(attach.size) + " " + ZmMsg.b; 2172 } 2173 else if (attach.size < (1024 * 1024)) { 2174 props.size = numFormatter.format(Math.round((attach.size / 1024) * 10) / 10) + " " + ZmMsg.kb; 2175 } 2176 else { 2177 props.size = numFormatter.format(Math.round((attach.size / (1024 * 1024)) * 10) / 10) + " " + ZmMsg.mb; 2178 } 2179 } 2180 2181 if (attach.part) { 2182 useCL = attach.contentLocation && (attach.relativeCl || ZmMailMsg.URL_RE.test(attach.contentLocation)); 2183 } else { 2184 useCL = attach.contentLocation && true; 2185 } 2186 2187 // see if rfc822 is an invite 2188 if (attach.contentType == ZmMimeTable.MSG_RFC822) { 2189 props.rfc822Part = attach.part; 2190 var calPart = (attach.children.size() == 1) && attach.children.get(0); 2191 if (appCtxt.get(ZmSetting.CALENDAR_ENABLED) && calPart && (calPart.contentType == ZmMimeTable.TEXT_CAL)) { 2192 props.links.importICS = true; 2193 props.rfc822CalPart = calPart.part; 2194 } 2195 } else { 2196 // set the anchor html for the link to this attachment on the server 2197 var url = useCL ? attach.contentLocation : this.getUrlForPart(attach); 2198 2199 // bug fix #6500 - append filename w/in so "Save As" wont append .html at the end 2200 if (!useCL) { 2201 var insertIdx = url.indexOf("?auth=co&"); 2202 var fn = AjxStringUtil.urlComponentEncode(attach.fileName); 2203 fn = fn.replace(/\x27/g, "%27"); 2204 url = url.substring(0,insertIdx) + fn + url.substring(insertIdx); 2205 } 2206 if (!useCL) { 2207 props.links.download = true; 2208 } 2209 2210 var folder = appCtxt.getById(this.folderId); 2211 if ((attach.name || attach.fileName) && appCtxt.get(ZmSetting.BRIEFCASE_ENABLED)) { 2212 if (!useCL) { 2213 props.links.briefcase = true; 2214 } 2215 } 2216 2217 var isICSAttachment = (attach.fileName && attach.fileName.match(/\./) && attach.fileName.replace(/^.*\./, "").toLowerCase() == "ics"); 2218 2219 if (appCtxt.get(ZmSetting.CALENDAR_ENABLED) && ((attach.contentType == ZmMimeTable.TEXT_CAL) || isICSAttachment)) { 2220 props.links.importICS = true; 2221 } 2222 2223 if (!useCL) { 2224 // check for vcard *first* since we dont care to view it in HTML 2225 if (ZmMimeTable.isVcard(attach.contentType)) { 2226 props.links.vcard = true; 2227 } 2228 else if (ZmMimeTable.hasHtmlVersion(attach.contentType) && appCtxt.get(ZmSetting.VIEW_ATTACHMENT_AS_HTML)) { 2229 props.links.html = true; 2230 } 2231 else { 2232 // set the objectify flag 2233 var contentType = attach.contentType; 2234 props.objectify = contentType && contentType.match(/^image/) && !contentType.match(/tif/); //see bug 82807 - Tiffs are not really supported by browsers, so don't objectify. 2235 } 2236 } else { 2237 props.url = url; 2238 } 2239 2240 if (attach.part) { 2241 // bug: 233 - remove attachment 2242 props.links.remove = true; 2243 } 2244 } 2245 2246 // set the link icon 2247 var mimeInfo = ZmMimeTable.getInfo(attach.contentType); 2248 props.linkIcon = mimeInfo ? mimeInfo.image : "GenericDoc"; 2249 props.ct = attach.contentType; 2250 2251 // set other meta info 2252 props.isHit = findHits && this._isAttInHitList(attach); 2253 // S/MIME: recognize client-side generated attachments, 2254 // and stash the cache key for the applet in the part, as 2255 // it's the only data which we retain later on 2256 if (attach.part) { 2257 props.part = attach.part; 2258 } else { 2259 props.generated = true; 2260 props.part = attach.cachekey; 2261 } 2262 if (!useCL) { 2263 if (attach.node && attach.node.isOfflineUploaded) { //for offline upload attachments 2264 props.url = attach.node.data; 2265 } else { 2266 props.url = this.getUrlForPart(attach); 2267 } 2268 } 2269 if (attach.contentId || (includeInlineImages && attach.contentDisposition == "inline")) { // bug: 28741 2270 props.ci = true; 2271 } 2272 props.mid = this.id; 2273 props.foundInMsgBody = attach.foundInMsgBody; 2274 2275 // and finally, add to attLink array 2276 this._attInfo.push(props); 2277 } 2278 } 2279 2280 return this._attInfo; 2281 }; 2282 ZmMailMsg.prototype.getAttachmentLinks = ZmMailMsg.prototype.getAttachmentInfo; 2283 2284 ZmMailMsg.prototype.removeAttachments = 2285 function(partIds, callback) { 2286 var jsonObj = {RemoveAttachmentsRequest: {_jsns:"urn:zimbraMail"}}; 2287 var request = jsonObj.RemoveAttachmentsRequest; 2288 request.m = { 2289 id: this.id, 2290 part: partIds.join(",") 2291 }; 2292 2293 var params = { 2294 jsonObj: jsonObj, 2295 asyncMode: true, 2296 callback: callback, 2297 noBusyOverlay: true 2298 }; 2299 return appCtxt.getAppController().sendRequest(params); 2300 }; 2301 2302 2303 // Private methods 2304 2305 /** 2306 * Processes a message node, getting attributes and child nodes to fill in the message. 2307 * This method may be called on an existing msg, since only metadata is returned when a 2308 * conv is expanded via SearchConvRequest. That is why we check values before setting 2309 * them, and why we don't clear out all the msg properties here first. 2310 */ 2311 ZmMailMsg.prototype._loadFromDom = 2312 function(msgNode) { 2313 // this method could potentially be called twice (SearchConvResponse and 2314 // GetMsgResponse) so always check param before setting! 2315 if (msgNode.id) { this.id = msgNode.id; } 2316 if (msgNode.part) { this.partId = msgNode.part; } 2317 if (msgNode.cid) { this.cid = msgNode.cid; } 2318 if (msgNode.s) { this.size = msgNode.s; } 2319 if (msgNode.d) { this.date = msgNode.d; } 2320 if (msgNode.sd) { this.sentDate = msgNode.sd; } 2321 if (msgNode.l) { this.folderId = msgNode.l; } 2322 if (msgNode.tn) { this._parseTagNames(msgNode.tn); } 2323 if (msgNode.cm) { this.inHitList = msgNode.cm; } 2324 if (msgNode.su) { this.subject = msgNode.su; } 2325 if (msgNode.fr) { this.fragment = msgNode.fr; } 2326 if (msgNode.rt) { this.rt = msgNode.rt; } 2327 if (msgNode.origid) { this.origId = msgNode.origid; } 2328 if (msgNode.hp) { this._attHitList = msgNode.hp; } 2329 if (msgNode.mid) { this.messageId = msgNode.mid; } 2330 if (msgNode.irt) { this.irtMessageId = msgNode.irt; } 2331 if (msgNode._attrs) { this.attrs = msgNode._attrs; } 2332 if (msgNode.sf) { this.sf = msgNode.sf; } 2333 if (msgNode.cif) { this.cif = msgNode.cif; } 2334 if (msgNode.md) { this.md = msgNode.md; } 2335 if (msgNode.ms) { this.ms = msgNode.ms; } 2336 if (msgNode.rev) { this.rev = msgNode.rev; } 2337 2338 if (msgNode.idnt) { 2339 var identityColl = appCtxt.getIdentityCollection(); 2340 this.identity = identityColl && identityColl.getById(msgNode.idnt); 2341 } 2342 2343 //Copying msg. header's 2344 if (msgNode.header) { 2345 this.headers = {}; 2346 for (var i = 0; i < msgNode.header.length; i++) { 2347 this.headers[msgNode.header[i].n] = msgNode.header[i]._content; 2348 } 2349 } 2350 2351 //Grab the metadata, keyed off the section name 2352 if (msgNode.meta) { 2353 this.meta = {}; 2354 for (var i = 0; i < msgNode.meta.length; i++) { 2355 var section = msgNode.meta[i].section; 2356 this.meta[section] = {}; 2357 this.meta[section]._attrs = {}; 2358 for (a in msgNode.meta[i]._attrs) { 2359 this.meta[section]._attrs[a] = msgNode.meta[i]._attrs[a]; 2360 } 2361 } 2362 } 2363 2364 // set the "normalized" Id if this message belongs to a shared folder 2365 var idx = this.id.indexOf(":"); 2366 this.nId = (idx != -1) ? (this.id.substr(idx + 1)) : this.id; 2367 2368 if (msgNode._convCreateNode) { 2369 this._convCreateNode = msgNode._convCreateNode; 2370 } 2371 2372 if (msgNode.cid && msgNode.l) { 2373 var conv = appCtxt.getById(msgNode.cid); 2374 if (conv) { 2375 // update conv's folder list 2376 if (conv.folders) { 2377 conv.folders[msgNode.l] = true; 2378 } 2379 var folders = AjxUtil.keys(conv.folders); 2380 AjxDebug.println(AjxDebug.NOTIFY, "update conv folder list: conv spans " + folders.length + " folder(s): " + folders.join(" ")); 2381 // update msg list if none exists since we know this conv has at least one msg 2382 if (!conv.msgIds) { 2383 conv.msgIds = [this.id]; 2384 } 2385 2386 if(conv.isMute) { 2387 this.isMute = true; 2388 } 2389 } 2390 } 2391 2392 // always call parseFlags even if server didn't return any 2393 this._parseFlags(msgNode.f); 2394 2395 if (msgNode.mp) { 2396 // clear all attachments and body data 2397 this.attachments = []; 2398 this._bodyParts = []; 2399 this._contentType = {}; 2400 this.findAttsFoundInMsgBodyDone = false; 2401 var ctxt = { 2402 attachments: this.attachments, 2403 bodyParts: this._bodyParts, 2404 contentTypes: this._contentType 2405 }; 2406 this._topPart = ZmMimePart.createFromDom(msgNode.mp[0], ctxt); 2407 this._loaded = this._bodyParts.length > 0 || this.attachments.length > 0; 2408 this._cleanupCIds(); 2409 } 2410 2411 if (msgNode.shr) { 2412 // TODO: Make server output better msgNode.shr property... 2413 var shareXmlDoc = AjxXmlDoc.createFromXml(msgNode.shr[0].content); 2414 try { 2415 AjxDispatcher.require("Share"); 2416 this.share = ZmShare.createFromDom(shareXmlDoc.getDoc()); 2417 this.share._msgId = msgNode.id; 2418 } catch (ex) { 2419 // not a version we support, ignore 2420 } 2421 } 2422 if (msgNode.dlSubs) { 2423 var dlSubsXmlDoc = AjxXmlDoc.createFromXml(msgNode.dlSubs[0].content); 2424 try { 2425 this.subscribeReq = ZmMailMsg.createDlSubFromDom(dlSubsXmlDoc.getDoc()); 2426 this.subscribeReq._msgId = msgNode.id; 2427 } 2428 catch (ex) { 2429 // not a version we support, or missing element, ignore - Not sure I like this approach but copying Share - Eran 2430 DBG.println(AjxDebug.DBG1, "createDlSubFromDom failed, content is:" + msgNode.dlSubs[0].content + " ex:" + ex); 2431 } 2432 } 2433 2434 if (msgNode.e && this.participants && this.participants.size() == 0) { 2435 for (var i = 0; i < msgNode.e.length; i++) { 2436 this._parseParticipantNode(msgNode.e[i]); 2437 } 2438 this.clearAddresses(); 2439 var parts = this.participants.getArray(); 2440 for (var j = 0; j < parts.length; j++ ) { 2441 this.addAddress(parts[j]); 2442 } 2443 } 2444 2445 if (msgNode.autoSendTime) { 2446 var timestamp = parseInt(msgNode.autoSendTime) || null; 2447 if (timestamp) { 2448 this.setAutoSendTime(new Date(timestamp)); 2449 } 2450 } 2451 2452 if (msgNode.inv) { 2453 try { 2454 this.invite = ZmInvite.createFromDom(msgNode.inv); 2455 if (this.invite.isEmpty()) return; 2456 this.invite.setMessageId(this.id); 2457 // bug fix #18613 2458 var desc = this.invite.getComponentDescription(); 2459 var descHtml = this.invite.getComponentDescriptionHtml(); 2460 if (descHtml) { 2461 this.setHtmlContent(descHtml); 2462 this.setInviteDescriptionContent(ZmMimeTable.TEXT_HTML, descHtml); 2463 } 2464 2465 if (desc) { 2466 this.setInviteDescriptionContent(ZmMimeTable.TEXT_PLAIN, desc); 2467 } 2468 2469 if (!appCtxt.get(ZmSetting.CALENDAR_ENABLED) && this.invite.type == "appt") { 2470 this.flagLocal(ZmItem.FLAG_ATTACH, true); 2471 } 2472 2473 } catch (ex) { 2474 // do nothing - this means we're trying to load an ZmInvite in new 2475 // window, which we dont currently load (re: support). 2476 } 2477 } 2478 }; 2479 2480 ZmMailMsg.createDlSubFromDom = 2481 function(doc) { 2482 // NOTE: This code initializes DL subscription info from the Zimbra dlSub format, v0.1 2483 var sub = {}; 2484 2485 var node = doc.documentElement; 2486 sub.version = node.getAttribute("version"); 2487 sub.subscribe = node.getAttribute("action") == "subscribe"; 2488 if (sub.version != ZmMailMsg.DL_SUB_VERSION) { 2489 throw "Zimbra dl sub version must be " + ZmMailMsg.DL_SUB_VERSION; 2490 } 2491 2492 for (var child = node.firstChild; child != null; child = child.nextSibling) { 2493 if (child.nodeName != "dl" && child.nodeName != "user") { 2494 continue; 2495 } 2496 sub[child.nodeName] = { 2497 id: child.getAttribute("id"), 2498 email: child.getAttribute("email"), 2499 name: child.getAttribute("name") 2500 }; 2501 } 2502 if (!sub.dl) { 2503 throw "missing dl element"; 2504 } 2505 if (!sub.user) { 2506 throw "missing user element"; 2507 } 2508 2509 return sub; 2510 }; 2511 2512 ZmMailMsg.prototype.hasNoViewableContent = 2513 function() { 2514 if (this.isRfc822) { 2515 //this means this message is not the top level one - but rather an attached message. 2516 return false; //till I can find a working heuristic that is not the fragment - size does not work as it includes probably stuff like subject and email addresses, and it's always bigger than 0. 2517 } 2518 var hasInviteContent = this.invite && !this.invite.isEmpty(); 2519 //the general case - use the fragment, so that cases where the text is all white space are taken care of as "no content". 2520 return !this.fragment && !hasInviteContent && !this.hasInlineImagesInMsgBody() && !this.hasInlineImage() 2521 }; 2522 2523 ZmMailMsg.prototype._cleanupCIds = 2524 function(atts) { 2525 atts = atts || this.attachments; 2526 if (!atts || atts.length == 0) { return; } 2527 2528 for (var i = 0; i < atts.length; i++) { 2529 var att = atts[i]; 2530 if (att.contentId && !/^<.+>$/.test(att.contentId)) { 2531 att.contentId = '<' + att.contentId + '>'; 2532 } 2533 } 2534 }; 2535 2536 ZmMailMsg.prototype.mute = 2537 function () { 2538 this.isMute = true; 2539 }; 2540 2541 ZmMailMsg.prototype.unmute = 2542 function () { 2543 this.isMute = false; 2544 }; 2545 2546 ZmMailMsg.prototype.isInvite = 2547 function () { 2548 return (this.invite != null); 2549 }; 2550 2551 ZmMailMsg.prototype.forwardAsInvite = 2552 function () { 2553 if(!this.invite) { 2554 return false; 2555 } 2556 return this.invite.getInviteMethod() == "REQUEST"; 2557 }; 2558 2559 ZmMailMsg.prototype.needsRsvp = 2560 function () { 2561 if (!this.isInvite() || this.invite.isOrganizer()) { return false; } 2562 2563 var needsRsvp = false; 2564 var accEmail = appCtxt.getActiveAccount().getEmail(); 2565 if (this.isInvite()) { 2566 var at = this.invite.getAttendees(); 2567 for (var i in at) { 2568 if (at[i].url == accEmail) { 2569 return at[i].rsvp; 2570 } 2571 if (at[i].rsvp) { 2572 needsRsvp = true; 2573 } 2574 } 2575 at = this.invite.getResources(); 2576 for (var i in at) { 2577 if (at[i].url == accEmail) { 2578 return at[i].rsvp; 2579 } 2580 if (at[i].rsvp) { 2581 needsRsvp = true; 2582 } 2583 } 2584 } 2585 2586 return needsRsvp; 2587 }; 2588 2589 // Adds child address nodes for the given address type. 2590 ZmMailMsg.prototype._addAddressNodes = 2591 function(addrNodes, type, isDraft) { 2592 2593 var addrs = this._addrs[type]; 2594 var num = addrs.size(); 2595 var contactsApp; 2596 if (num) { 2597 if (appCtxt.isOffline) { 2598 contactsApp = appCtxt.getApp(ZmApp.CONTACTS) 2599 } else { 2600 contactsApp = appCtxt.get(ZmSetting.CONTACTS_ENABLED) && appCtxt.getApp(ZmApp.CONTACTS); 2601 } 2602 if (contactsApp && !contactsApp.isContactListLoaded()) { 2603 contactsApp = null; 2604 } 2605 for (var i = 0; i < num; i++) { 2606 var addr = addrs.get(i); 2607 addr = addr.isAjxEmailAddress ? addr : AjxEmailAddress.parse(addr); 2608 if (addr) { 2609 var email = addr.getAddress(); 2610 var name = addr.getName(); 2611 var addrNode = {t:AjxEmailAddress.toSoapType[type], a:email}; 2612 if (name) { 2613 addrNode.p = name; 2614 } 2615 addrNodes.push(addrNode); 2616 } 2617 } 2618 } 2619 }; 2620 2621 ZmMailMsg.prototype._addFrom = 2622 function(addrNodes, parentNode, isDraft, accountName) { 2623 var ac = window.parentAppCtxt || window.appCtxt; 2624 2625 // only use account name if we either dont have any identities to choose 2626 // from or the one we have is the default anyway 2627 var identity = this.identity; 2628 var isPrimary = identity == null || identity.isDefault; 2629 if (this.delegatedSenderAddr && !this.delegatedSenderAddrIsDL) { 2630 isPrimary = false; 2631 } 2632 2633 // If repying to an invite which was addressed to user's alias then accept 2634 // reply should appear from the alias 2635 if (this._origMsg && this._origMsg.isInvite() && 2636 this.isReplied && 2637 (!this._origMsg._customMsg || !identity)) // is default reply or has no identities. 2638 { 2639 var origTos = this._origMsg._getAttendees(); 2640 var size = origTos && origTos.size() > 0 ? origTos.size() : 0; 2641 var aliazesString = "," + appCtxt.get(ZmSetting.MAIL_ALIASES).join(",") + ","; 2642 for (var i = 0; i < size; i++) { 2643 var origTo = origTos.get(i).address; 2644 if (origTo && aliazesString.indexOf("," + origTo + ",") >= 0) { 2645 var addrNode = {t:"f", a:origTo}; 2646 addrNodes.push(addrNode); 2647 return; // We have already added appropriate alias as a "from". return from here. 2648 } 2649 } 2650 } 2651 2652 //TODO: OPTIMIZE CODE by aggregating the common code. 2653 if (!appCtxt.isOffline && accountName && isPrimary) { 2654 var mainAcct = ac.accountList.mainAccount.getEmail(); 2655 var onBehalfOf = false; 2656 2657 var folder = appCtxt.getById(this.folderId); 2658 if ((!folder || folder.isRemote()) && (!this._origMsg || !this._origMsg.sendAsMe)) { 2659 accountName = (folder && folder.getOwner()) || accountName; 2660 onBehalfOf = (accountName != mainAcct); 2661 } 2662 2663 if (this._origMsg && this._origMsg.isDraft && !this._origMsg.sendAsMe) { 2664 var from = this._origMsg.getAddresses(AjxEmailAddress.FROM).get(0); 2665 // this means we're sending a draft msg obo so reset account name 2666 if (from && from.address.toLowerCase() != mainAcct.toLowerCase()) { 2667 accountName = from.address; 2668 onBehalfOf = true; 2669 } 2670 } 2671 2672 // bug #44857 - replies/forwards should save sent message into respective account 2673 if (!onBehalfOf && appCtxt.isFamilyMbox && this._origMsg && folder) { 2674 onBehalfOf = (folder.getOwner() != mainAcct); 2675 } 2676 2677 var addr, displayName; 2678 if (this.fromSelectValue) { 2679 addr = this.fromSelectValue.addr.address; 2680 displayName = this.fromSelectValue.addr.name; 2681 } else if (this._origMsg && this._origMsg.isInvite() && appCtxt.multiAccounts) { 2682 identity = this._origMsg.getAccount().getIdentity(); 2683 addr = identity ? identity.sendFromAddress : this._origMsg.getAccount().name; 2684 displayName = identity && identity.sendFromDisplay; 2685 } else { 2686 if (onBehalfOf) { 2687 addr = accountName; 2688 } else { 2689 addr = identity ? identity.sendFromAddress : (this.delegatedSenderAddr || accountName); 2690 onBehalfOf = this.isOnBehalfOf; 2691 displayName = identity && identity.sendFromDisplay; 2692 } 2693 } 2694 2695 var node = {t:"f", a:addr}; 2696 if (displayName) { 2697 node.p = displayName; 2698 } 2699 addrNodes.push(node); 2700 if (onBehalfOf || !(ac.multiAccounts || isDraft)) { 2701 // the main account is *always* the sender 2702 addrNodes.push({t:"s", a:mainAcct}); 2703 } 2704 } else{ 2705 2706 var mainAcct = ac.accountList.mainAccount.getEmail(); 2707 var onBehalfOf = false; 2708 2709 var folder = appCtxt.getById(this.folderId); 2710 if (folder && folder.isRemote() && !this._origMsg.sendAsMe) { 2711 accountName = folder.getOwner(); 2712 onBehalfOf = (accountName != mainAcct); 2713 } 2714 2715 if (this._origMsg && this._origMsg.isDraft && !this._origMsg.sendAsMe) { 2716 var from = this._origMsg.getAddresses(AjxEmailAddress.FROM).get(0); 2717 // this means we're sending a draft msg obo so reset account name 2718 if (from && from.address.toLowerCase() != mainAcct.toLowerCase() && !appCtxt.isMyAddress(from.address.toLowerCase())) { 2719 accountName = from.address; 2720 onBehalfOf = true; 2721 } 2722 } 2723 2724 var addr, displayName; 2725 if (onBehalfOf) { 2726 addr = accountName; 2727 } else if (identity) { 2728 if (identity.sendFromAddressType == ZmSetting.SEND_ON_BEHALF_OF){ 2729 addr = identity.sendFromAddress.replace(ZmMsg.onBehalfOfMidLabel + " ", ""); 2730 onBehalfOf = true; 2731 } else { 2732 addr = identity.sendFromAddress || mainAcct; 2733 } 2734 displayName = identity.sendFromDisplay; 2735 2736 } else { 2737 addr = this.delegatedSenderAddr || mainAcct; 2738 onBehalfOf = this.isOnBehalfOf; 2739 } 2740 2741 var addrNode = {t:"f", a:addr}; 2742 if( displayName) { 2743 addrNode.p = displayName; 2744 } 2745 addrNodes.push(addrNode); 2746 2747 if (onBehalfOf) { 2748 addrNodes.push({t:"s", a:mainAcct}); 2749 } 2750 2751 if (identity && identity.isFromDataSource) { 2752 var dataSource = ac.getDataSourceCollection().getById(identity.id); 2753 if (dataSource) { 2754 // mail is "from" external account 2755 addrNode.t = "f"; 2756 addrNode.a = dataSource.getEmail(); 2757 if (ac.get(ZmSetting.DEFAULT_DISPLAY_NAME)) { 2758 var dispName = dataSource.identity && dataSource.identity.sendFromDisplay; 2759 addrNode.p = dispName || dataSource.userName || dataSource.getName(); 2760 } 2761 } 2762 } 2763 } 2764 }; 2765 2766 ZmMailMsg.prototype._addReplyTo = 2767 function(addrNodes) { 2768 if (this.identity) { 2769 if (this.identity.setReplyTo && this.identity.setReplyToAddress) { 2770 var addrNode = {t:"r", a:this.identity.setReplyToAddress}; 2771 if (this.identity.setReplyToDisplay) { 2772 addrNode.p = this.identity.setReplyToDisplay; 2773 } 2774 addrNodes.push(addrNode); 2775 } 2776 } 2777 }; 2778 2779 ZmMailMsg.prototype._addReadReceipt = 2780 function(addrNodes, accountName) { 2781 var addrNode = {t:"n"}; 2782 if (this.identity) { 2783 addrNode.a = this.identity.readReceiptAddr || this.identity.sendFromAddress; 2784 addrNode.p = this.identity.sendFromDisplay; 2785 } else { 2786 addrNode.a = accountName || appCtxt.getActiveAccount().getEmail(); 2787 } 2788 addrNodes.push(addrNode); 2789 }; 2790 2791 ZmMailMsg.prototype._isAttInHitList = 2792 function(attach) { 2793 for (var i = 0; i < this._attHitList.length; i++) { 2794 if (attach.part == this._attHitList[i].part) { return true; } 2795 } 2796 2797 return false; 2798 }; 2799 2800 ZmMailMsg.prototype._onChange = 2801 function(what, a, b, c) { 2802 if (this.onChange) { 2803 this.onChange.run(what, a, b, c); 2804 } 2805 }; 2806 2807 /** 2808 * Gets the status icon. 2809 * 2810 * @return {String} the icon 2811 */ 2812 ZmMailMsg.prototype.getStatusIcon = 2813 function() { 2814 2815 if (this.isInvite() && appCtxt.get(ZmSetting.CALENDAR_ENABLED)) { 2816 var method = this.invite.getInviteMethod(); 2817 var status; 2818 if (method == ZmCalendarApp.METHOD_REPLY) { 2819 var attendees = this.invite.getAttendees(); 2820 status = attendees && attendees[0] && attendees[0].ptst; 2821 } else if (method == ZmCalendarApp.METHOD_CANCEL) { 2822 status = ZmMailMsg.PSTATUS_DECLINED; 2823 } 2824 return ZmMailMsg.STATUS_ICON[status] || "Appointment"; 2825 } 2826 2827 for (var i = 0; i < ZmMailMsg.STATUS_LIST.length; i++) { 2828 var status = ZmMailMsg.STATUS_LIST[i]; 2829 if (this[status]) { 2830 return ZmMailMsg.STATUS_ICON[status]; 2831 } 2832 } 2833 2834 return "MsgStatusRead"; 2835 }; 2836 2837 /** 2838 * Gets the status tool tip. 2839 * 2840 * @return {String} the tool tip 2841 */ 2842 ZmMailMsg.prototype.getStatusTooltip = 2843 function() { 2844 // keep in sync with ZmConv.prototype.getStatusTooltip 2845 var status = []; 2846 if (this.isInvite()) { 2847 var icon = this.getStatusIcon(); 2848 status.push(ZmMailMsg.TOOLTIP[icon]); 2849 } 2850 if (this.isScheduled) { status.push(ZmMsg.scheduled); } 2851 if (this.isUnread) { status.push(ZmMsg.unread); } 2852 if (this.isReplied) { status.push(ZmMsg.replied); } 2853 if (this.isForwarded) { status.push(ZmMsg.forwarded); } 2854 if (this.isDraft) { 2855 status.push(ZmMsg.draft); 2856 } 2857 else if (this.isSent) { 2858 status.push(ZmMsg.sentAt); //sentAt is for some reason "sent", which is what we need. 2859 } 2860 if (status.length == 0) { 2861 status = [ZmMsg.read]; 2862 } 2863 2864 return status.join(", "); 2865 }; 2866 2867 ZmMailMsg.prototype.notifyModify = 2868 function(obj, batchMode) { 2869 if (obj.cid != null) { 2870 this.cid = obj.cid; 2871 } 2872 2873 return ZmMailItem.prototype.notifyModify.apply(this, arguments); 2874 }; 2875 2876 ZmMailMsg.prototype.isResourceInvite = 2877 function() { 2878 if (!this.cif || !this.invite) { return false; } 2879 2880 var resources = this.invite.getResources(); 2881 for (var i in resources) { 2882 if (resources[i] && resources[i].url == this.cif) { 2883 return true; 2884 } 2885 } 2886 return false; 2887 }; 2888 2889 ZmMailMsg.prototype.setAutoSendTime = 2890 function(autoSendTime) { 2891 this._setAutoSendTime(autoSendTime); 2892 }; 2893 2894 ZmMailMsg.prototype._setAutoSendTime = 2895 function(autoSendTime) { 2896 ZmMailItem.prototype.setAutoSendTime.call(this, autoSendTime); 2897 var conv = this.cid && appCtxt.getById(this.cid); 2898 if (Dwt.instanceOf(conv, "ZmConv")) { 2899 conv.setAutoSendTime(autoSendTime); 2900 } 2901 }; 2902 2903 /** 2904 * Sends a read receipt. 2905 * 2906 * @param {closure} callback response callback 2907 */ 2908 ZmMailMsg.prototype.sendReadReceipt = 2909 function(callback) { 2910 2911 var jsonObj = {SendDeliveryReportRequest:{_jsns:"urn:zimbraMail"}}; 2912 var request = jsonObj.SendDeliveryReportRequest; 2913 request.mid = this.id; 2914 var ac = window.parentAppCtxt || window.appCtxt; 2915 ac.getRequestMgr().sendRequest({jsonObj:jsonObj, asyncMode:true, callback:callback}); 2916 }; 2917 2918 2919 // Execute the mail redirect server side call 2920 ZmMailMsg.prototype.redirect = 2921 function(addrs, callback) { 2922 2923 var jsonObj = {BounceMsgRequest:{_jsns:"urn:zimbraMail"}}; 2924 var request = jsonObj.BounceMsgRequest; 2925 request.m = {id:this.id}; 2926 var e = request.m.e = []; 2927 for (var iType = 0; iType < ZmMailMsg.COMPOSE_ADDRS.length; iType++) { 2928 if (addrs[ZmMailMsg.COMPOSE_ADDRS[iType]]) { 2929 var all = addrs[ZmMailMsg.COMPOSE_ADDRS[iType]].all; 2930 for (var i = 0, len = all.size(); i < len; i++) { 2931 var addr = all.get(i); 2932 var rType = AjxEmailAddress.toSoapType[addr.type]; 2933 e.push({t:rType, a:addr.address}); 2934 } 2935 } 2936 } 2937 2938 // No Success callback, nothing of interest returned 2939 var acct = appCtxt.multiAccounts && appCtxt.accountList.mainAccount; 2940 appCtxt.getAppController().sendRequest({ 2941 jsonObj: jsonObj, 2942 asyncMode: true, 2943 accountName: acct, 2944 callback: callback 2945 }); 2946 }; 2947 2948 ZmMailMsg.prototype.doDelete = 2949 function() { 2950 var params = {jsonObj:{MsgActionRequest:{_jsns:"urn:zimbraMail",action:{id:this.id, op:"delete"}}}, asyncMode:true}; 2951 2952 // Bug 84549: The params object is a property of the child window, because it 2953 // was constructed using this window's Object constructor. But when the child 2954 // window closes immediately after the request is sent, the object would be 2955 // garbage-collected by the browser (or otherwise become invalid). 2956 // Therefore, we need to pass an object that is native to the parent window 2957 if (appCtxt.isChildWindow && (AjxEnv.isIE || AjxEnv.isModernIE)) { 2958 var cp = function(from){ 2959 var to = window.opener.Object(); 2960 for (var key in from) { 2961 var value = from[key]; 2962 to[key] = (AjxUtil.isObject(value)) ? cp(value) : value; 2963 } 2964 return to; 2965 }; 2966 params = cp(params); 2967 } 2968 2969 var ac = window.parentAppCtxt || window.appCtxt; 2970 ac.getRequestMgr().sendRequest(params); 2971 }; 2972 2973 /** 2974 * If message is sent on behalf of returns sender address otherwise returns from address 2975 * @return {String} email address 2976 */ 2977 ZmMailMsg.prototype.getMsgSender = 2978 function() { 2979 var from = this.getAddress(AjxEmailAddress.FROM); 2980 var sender = this.getAddress(AjxEmailAddress.SENDER); 2981 if (sender && sender.address != (from && from.address)) { 2982 return sender.address; 2983 } 2984 return from && from.address; 2985 }; 2986 2987 /** 2988 * Return list header id if it exists, otherwise returns null 2989 * @return {String} list id 2990 */ 2991 ZmMailMsg.prototype.getListIdHeader = 2992 function() { 2993 var id = null; 2994 if (this.attrs && this.attrs[ZmMailMsg.HDR_LISTID]) { 2995 //extract <ID> from header 2996 var listId = this.attrs[ZmMailMsg.HDR_LISTID]; 2997 id = listId.match(/<(.*)>/); 2998 if (AjxUtil.isArray(id)) { 2999 id = id[id.length-1]; //make it the last match 3000 } 3001 } 3002 return id; 3003 }; 3004 3005 /** 3006 * Return the zimbra DL header if it exists, otherwise return null 3007 * @return {AjxEmailAddress} AjxEmailAddress object if header exists 3008 **/ 3009 ZmMailMsg.prototype.getXZimbraDLHeader = 3010 function() { 3011 if (this.attrs && this.attrs[ZmMailMsg.HDR_XZIMBRADL]) { 3012 return AjxEmailAddress.parseEmailString(this.attrs[ZmMailMsg.HDR_XZIMBRADL]); 3013 } 3014 return null; 3015 }; 3016 3017 /** 3018 * Return mime header id if it exists, otherwise returns null 3019 * @return {String} mime header value 3020 */ 3021 ZmMailMsg.prototype.getMimeHeader = 3022 function(name) { 3023 var value = null; 3024 if (this.attrs && this.attrs[name]) { 3025 value = this.attrs[name]; 3026 } 3027 return value; 3028 }; 3029 3030 /** 3031 * Adds optional headers to the given request. 3032 * 3033 * @param {object|AjxSoapDoc} req SOAP document or JSON parent object (probably a <m> msg object) 3034 */ 3035 ZmMailMsg.addRequestHeaders = 3036 function(req) { 3037 3038 if (!req) { return; } 3039 if (req.isAjxSoapDoc) { 3040 for (var hdr in ZmMailMsg.requestHeaders) { 3041 var headerNode = req.set('header', null, null); 3042 headerNode.setAttribute('n', ZmMailMsg.requestHeaders[hdr]); 3043 } 3044 } 3045 else { 3046 var hdrs = ZmMailMsg.requestHeaders; 3047 if (hdrs) { 3048 req.header = req.header || []; 3049 for (var hdr in hdrs) { 3050 req.header.push({n:hdrs[hdr]}); 3051 } 3052 } 3053 } 3054 }; 3055 3056 /** 3057 * Returns a URL that can be used to fetch the given part of this message. 3058 * 3059 * @param {ZmMimePart} bodyPart MIME part to fetch 3060 * 3061 * @returns {string} URL to fetch the part 3062 */ 3063 ZmMailMsg.prototype.getUrlForPart = function(bodyPart) { 3064 3065 return appCtxt.get(ZmSetting.CSFE_MSG_FETCHER_URI) + "&loc=" + AjxEnv.DEFAULT_LOCALE + "&id=" + this.id + "&part=" + bodyPart.part; 3066 }; 3067