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 * Creates a new compose view. The view does not display itself on construction. 26 * @constructor 27 * @class 28 * This class provides a form for composing a message. 29 * 30 * @author Conrad Damon 31 * 32 * @param {DwtControl} parent the element that created this view 33 * @param {ZmController} controller the controller managing this view 34 * @param {constant} composeMode passed in so detached window knows which mode to be in on startup 35 * 36 * @extends DwtComposite 37 * 38 * @private 39 */ 40 ZmComposeView = function(parent, controller, composeMode, action) { 41 42 if (arguments.length === 0) { return; } 43 44 this.TEMPLATE = "mail.Message#Compose"; 45 this._view = controller.getCurrentViewId(); 46 this._sessionId = controller.getSessionId(); 47 48 DwtComposite.call(this, {parent:parent, className:"ZmComposeView", posStyle:Dwt.ABSOLUTE_STYLE, id:ZmId.getViewId(this._view)}); 49 50 ZmComposeView.NOTIFY_ACTION_MAP = {}; 51 ZmComposeView.NOTIFY_ACTION_MAP[ZmOperation.REPLY_ACCEPT] = ZmOperation.REPLY_ACCEPT_NOTIFY; 52 ZmComposeView.NOTIFY_ACTION_MAP[ZmOperation.REPLY_DECLINE] = ZmOperation.REPLY_DECLINE_NOTIFY; 53 ZmComposeView.NOTIFY_ACTION_MAP[ZmOperation.REPLY_TENTATIVE] = ZmOperation.REPLY_TENTATIVE_NOTIFY; 54 55 ZmComposeView.MOVE_TO_FIELD = {}; 56 ZmComposeView.MOVE_TO_FIELD[ZmOperation.MOVE_TO_TO] = AjxEmailAddress.TO; 57 ZmComposeView.MOVE_TO_FIELD[ZmOperation.MOVE_TO_CC] = AjxEmailAddress.CC; 58 ZmComposeView.MOVE_TO_FIELD[ZmOperation.MOVE_TO_BCC] = AjxEmailAddress.BCC; 59 60 this._onMsgDataChange = this._onMsgDataChange.bind(this); 61 62 this._controller = controller; 63 64 var recipParams = {}; 65 recipParams.resetContainerSizeMethod = this._resetBodySize.bind(this); 66 recipParams.enableContainerInputs = this.enableInputs.bind(this); 67 recipParams.reenter = this.reEnableDesignMode.bind(this); 68 recipParams.contactPopdownListener = this._controller._dialogPopdownListener; 69 recipParams.contextId = this._controller.getCurrentViewId(); 70 71 this._recipients = new ZmRecipients(recipParams); 72 this._attcTabGroup = new DwtTabGroup('ZmComposeViewAttachments'); 73 74 this._firstTimeFixImages = true; 75 76 this._initialize(composeMode, action); 77 78 // make sure no unnecessary scrollbars show up 79 this.setScrollStyle(Dwt.CLIP); 80 }; 81 82 ZmComposeView.prototype = new DwtComposite; 83 ZmComposeView.prototype.constructor = ZmComposeView; 84 85 ZmComposeView.prototype.isZmComposeView = true; 86 ZmComposeView.prototype.toString = function() { return "ZmComposeView"; }; 87 88 // 89 // Constants 90 // 91 92 // Consts related to compose fields 93 ZmComposeView.QUOTED_HDRS = [ 94 ZmMailMsg.HDR_FROM, 95 ZmMailMsg.HDR_TO, 96 ZmMailMsg.HDR_CC, 97 ZmMailMsg.HDR_DATE, 98 ZmMailMsg.HDR_SUBJECT 99 ]; 100 101 ZmComposeView.BAD = "_bad_addrs_"; 102 103 // Message dialog placement 104 ZmComposeView.DIALOG_X = 50; 105 ZmComposeView.DIALOG_Y = 100; 106 107 // Attachment related 108 ZmComposeView.UPLOAD_FIELD_NAME = "attUpload"; 109 ZmComposeView.FORWARD_ATT_NAME = "ZmComposeView_forAttName"; 110 ZmComposeView.FORWARD_MSG_NAME = "ZmComposeView_forMsgName"; 111 ZmComposeView.ADD_ORIG_MSG_ATTS = "add_original_attachments"; 112 ZmComposeView.MAX_ATTM_NAME_LEN = 30; 113 114 // max # of attachments to show 115 ZmComposeView.SHOW_MAX_ATTACHMENTS = AjxEnv.is800x600orLower ? 2 : 3; 116 ZmComposeView.MAX_ATTACHMENT_HEIGHT = (ZmComposeView.SHOW_MAX_ATTACHMENTS * 23) + "px"; 117 118 // Reply/forward stuff 119 ZmComposeView.EMPTY_FORM_RE = /^[\s\|]*$/; 120 ZmComposeView.HTML_TAG_RE = /(<[^>]+>)/g; 121 ZmComposeView.QUOTED_CONTENT_RE = new RegExp("^----- ", "m"); 122 ZmComposeView.HTML_QUOTED_CONTENT_RE = new RegExp("<br>----- ", "i"); 123 124 // Address components 125 ZmComposeView.OP = {}; 126 ZmComposeView.OP[AjxEmailAddress.TO] = ZmId.CMP_TO; 127 ZmComposeView.OP[AjxEmailAddress.CC] = ZmId.CMP_CC; 128 ZmComposeView.OP[AjxEmailAddress.BCC] = ZmId.CMP_BCC; 129 130 // Upload sources 131 ZmComposeView.UPLOAD_COMPUTER = 'computer'; 132 ZmComposeView.UPLOAD_INLINE = 'inline'; 133 ZmComposeView.UPLOAD_BRIEFCASE = 'briefcase'; 134 135 // Quoted content - distinguish "" from a lack of quoted content 136 ZmComposeView.EMPTY = '__empty__'; 137 138 // Public methods 139 140 /** 141 * Sets the current view, based on the given action. The compose form is 142 * created and laid out and everything is set up for interaction with the user. 143 * 144 * @param {Hash} params a hash of parameters: 145 * @param {constant} action new message, reply, forward, or an invite action 146 * @param {ZmMailMsg} msg the original message (reply/forward), or address (new message) 147 * @param {ZmIdentity} identity identity of sender 148 * @param {String} toOverride To: addresses (optional) 149 * @param {String} ccOverride Cc: addresses (optional) 150 * @param {String} subjectOverride subject for new msg (optional) 151 * @param {String} extraBodyText text to prepend to body 152 * @param {Array} msgIds list of msg Id's to be added as attachments 153 * @param {String} accountName on-behalf-of From address 154 */ 155 ZmComposeView.prototype.set = 156 function(params) { 157 158 var action = this._action = params.action; 159 this._origAction = this._action; 160 if (this._msg) { 161 this._msg.onChange = null; 162 } 163 164 this._isIncludingOriginalAttachments = false; 165 this._originalAttachmentsInitialized = false; 166 167 this.isEditAsNew = params.isEditAsNew; 168 169 this._acceptFolderId = params.acceptFolderId; 170 var msg = this._msg = this._origMsg = params.msg; 171 var oboMsg = msg || (params.selectedMessages && params.selectedMessages.length && params.selectedMessages[0]); 172 var obo = this._getObo(params.accountName, oboMsg); 173 if (msg) { 174 msg.onChange = this._onMsgDataChange; 175 } 176 177 // list of msg Id's to add as attachments 178 this._msgIds = params.msgIds; 179 180 this.reset(true); 181 182 this._setFromSelect(msg); 183 184 if (obo) { 185 this.identitySelect.setSelectedValue(obo); 186 this._controller.resetIdentity(obo); 187 } 188 189 if (params.identity) { 190 if (this.identitySelect) { 191 this.identitySelect.setSelectedValue(params.identity.id); 192 this._controller.resetIdentity(params.identity.id); 193 } 194 if (appCtxt.get(ZmSetting.SIGNATURES_ENABLED) || appCtxt.multiAccounts) { 195 var selected = this._getSignatureIdForAction(params.identity) || ""; 196 var account = appCtxt.multiAccounts && this.getFromAccount(); 197 this._controller.resetSignatureToolbar(selected, account); 198 } 199 } 200 201 this._recipients.setup(); 202 203 if (!ZmComposeController.IS_FORWARD[action]) { 204 // populate fields based on the action and user prefs 205 this._setAddresses(action, AjxEmailAddress.TO, params.toOverride); 206 if (params.ccOverride) { 207 this._setAddresses(action, AjxEmailAddress.CC, params.ccOverride); 208 } 209 if (params.bccOverride) { 210 this._setAddresses(action, AjxEmailAddress.BCC, params.bccOverride); 211 } 212 } 213 214 this._setSubject(action, msg || (params.selectedMessages && params.selectedMessages[0]), params.subjOverride); 215 this._setBody(action, msg, params.extraBodyText, false, false, params.extraBodyTextIsExternal, params.incOptions); 216 if (params.extraBodyText) { 217 this._isDirty = true; 218 } 219 220 //Force focus on body only for reply and replyAll 221 if (ZmComposeController.IS_REPLY[action]) { 222 this._moveCaretOnTimer(params.extraBodyText ? params.extraBodyText.length : 0); 223 } 224 225 if (action !== ZmOperation.FORWARD_ATT) { 226 this._saveExtraMimeParts(); 227 } 228 229 // save form state (to check for change later) 230 if (this._composeMode === Dwt.HTML) { 231 var ta = new AjxTimedAction(this, this._setFormValue); 232 AjxTimedAction.scheduleAction(ta, 10); 233 } else { 234 this._setFormValue(); 235 } 236 // Force focus on the TO field 237 if (!ZmComposeController.IS_REPLY[action]) { 238 appCtxt.getKeyboardMgr().grabFocus(this._recipients.getAddrInputField(AjxEmailAddress.TO)); 239 } 240 }; 241 242 ZmComposeView.prototype._getObo = 243 function(obo, msg) { 244 if (msg) { 245 var folder = !obo ? appCtxt.getById(msg.folderId) : null; 246 obo = (folder && folder.isRemote()) ? folder.getOwner() : null; 247 248 // check if this is a draft that was originally composed obo 249 var isFromDataSource = msg.identity && msg.identity.isFromDataSource; 250 if (!obo && msg.isDraft && !appCtxt.multiAccounts && !isFromDataSource && !appCtxt.get(ZmSetting.ALLOW_ANY_FROM_ADDRESS)) { 251 var ac = window.parentAppCtxt || window.appCtxt; 252 var from = msg.getAddresses(AjxEmailAddress.FROM).get(0); 253 if (from && (from.address.toLowerCase() !== ac.accountList.mainAccount.getEmail().toLowerCase()) && !appCtxt.isMyAddress(from.address.toLowerCase())) { 254 obo = from.address; 255 } 256 } 257 } 258 return obo; 259 }; 260 261 ZmComposeView.prototype._saveExtraMimeParts = 262 function() { 263 264 var bodyParts = this._msg ? this._msg.getBodyParts() : []; 265 for (var i = 0; i < bodyParts.length; i++) { 266 var bodyPart = bodyParts[i]; 267 var contentType = bodyPart.contentType; 268 269 if (contentType === ZmMimeTable.TEXT_PLAIN) { continue; } 270 if (contentType === ZmMimeTable.TEXT_HTML) { continue; } 271 if (ZmMimeTable.isRenderableImage(contentType) && bodyPart.contentDisposition === "inline") { continue; } // bug: 28741 272 273 var mimePart = new ZmMimePart(); 274 mimePart.setContentType(contentType); 275 mimePart.setContent(bodyPart.getContent()); 276 this.addMimePart(mimePart); 277 } 278 }; 279 280 /** 281 * Called automatically by the attached ZmMailMsg object when data is 282 * changed, in order to support Zimlets modify subject or other values 283 * (bug: 10540) 284 * 285 * @private 286 */ 287 ZmComposeView.prototype._onMsgDataChange = 288 function(what, val) { 289 if (what === "subject") { 290 this._subjectField.value = val; 291 this.updateTabTitle(); 292 } 293 }; 294 295 ZmComposeView.prototype.getComposeMode = 296 function() { 297 return this._composeMode; 298 }; 299 300 ZmComposeView.prototype.getController = 301 function() { 302 return this._controller; 303 }; 304 305 ZmComposeView.prototype.getHtmlEditor = 306 function() { 307 return this._htmlEditor; 308 }; 309 310 /** 311 * Gets the title. 312 * 313 * @return {String} the title 314 */ 315 ZmComposeView.prototype.getTitle = 316 function() { 317 var text; 318 if (ZmComposeController.IS_REPLY[this._action]) { 319 text = ZmMsg.reply; 320 } else if (ZmComposeController.IS_FORWARD[this._action]) { 321 text = ZmMsg.forward; 322 } else { 323 text = ZmMsg.compose; 324 } 325 return [ZmMsg.zimbraTitle, text].join(": "); 326 }; 327 328 /** 329 * Gets the field values for each of the addr fields. 330 * 331 * @return {Array} an array of addresses 332 */ 333 ZmComposeView.prototype.getRawAddrFields = 334 function() { 335 return this._recipients.getRawAddrFields(); 336 }; 337 338 // returns address fields that are currently visible 339 ZmComposeView.prototype.getAddrFields = 340 function() { 341 return this._recipients.getAddrFields(); 342 }; 343 344 ZmComposeView.prototype.getTabGroupMember = function() { 345 346 if (!this._tabGroup) { 347 var tg = this._tabGroup = new DwtTabGroup('ZmComposeView'); 348 tg.addMember(this._fromSelect); 349 tg.addMember(this.identitySelect); 350 tg.addMember(this._recipients.getTabGroupMember()); 351 tg.addMember(this._subjectField); 352 tg.addMember(this._attButton); 353 tg.addMember(this._attcTabGroup); 354 tg.addMember(this._htmlEditor.getTabGroupMember()); 355 } 356 357 return this._tabGroup; 358 }; 359 360 ZmComposeView.prototype.getAddrInputField = 361 function(type) { 362 return this._recipients.getAddrInputField(type); 363 }; 364 365 ZmComposeView.prototype.getRecipientField = 366 function(type) { 367 return this._recipients.getField(type); 368 }; 369 370 ZmComposeView.prototype.getAddressButtonListener = 371 function(ev, addrType) { 372 return this._recipients.addressButtonListener(ev, addrType); 373 }; 374 375 ZmComposeView.prototype.setAddress = 376 function(type, addr) { 377 return this._recipients.setAddress(type, addr); 378 }; 379 380 ZmComposeView.prototype.collectAddrs = 381 function() { 382 return this._recipients.collectAddrs(); 383 }; 384 385 // returns list of attachment field values (used by detachCompose) 386 ZmComposeView.prototype.getAttFieldValues = 387 function() { 388 var attList = []; 389 var atts = document.getElementsByName(ZmComposeView.UPLOAD_FIELD_NAME); 390 391 for (var i = 0; i < atts.length; i++) { 392 attList.push(atts[i].value); 393 } 394 395 return attList; 396 }; 397 398 ZmComposeView.prototype.setBackupForm = 399 function() { 400 this.backupForm = this._backupForm(); 401 }; 402 403 /** 404 * Saves *ALL* form value data to test against whether user made any changes 405 * since canceling SendMsgRequest. If user attempts to send again, we compare 406 * form data with this value and if not equal, send a new UID otherwise, re-use. 407 * 408 * @private 409 */ 410 ZmComposeView.prototype._backupForm = 411 function() { 412 var val = this._formValue(true, true); 413 414 // keep track of attachments as well 415 var atts = document.getElementsByName(ZmComposeView.UPLOAD_FIELD_NAME); 416 for (var i = 0; i < atts.length; i++) { 417 if (atts[i].value.length) { 418 val += atts[i].value; 419 } 420 } 421 422 // keep track of "uploaded" attachments as well :/ 423 val += this._getForwardAttIds(ZmComposeView.FORWARD_ATT_NAME + this._sessionId).join(""); 424 val += this._getForwardAttIds(ZmComposeView.FORWARD_MSG_NAME + this._sessionId).join(""); 425 426 return val; 427 }; 428 429 ZmComposeView.prototype._setAttInline = 430 function(opt){ 431 this._isAttachInline = (opt === true); 432 }; 433 434 ZmComposeView.prototype._getIsAttInline = 435 function(opt){ 436 return(this._isAttachInline); 437 }; 438 439 440 ZmComposeView.prototype._isInline = 441 function(msg) { 442 443 if (this._attachDialog) { 444 return this._attachDialog.isInline(); 445 } 446 447 msg = msg || this._origMsg; 448 449 if (msg && this._msgAttId && msg.id === this._msgAttId) { 450 return false; 451 } 452 453 if (msg && msg.attachments) { 454 var atts = msg.attachments; 455 for (var i = 0; i < atts.length; i++) { 456 if (atts[i].contentId) { 457 return true; 458 } 459 } 460 } 461 462 return false; 463 }; 464 465 466 ZmComposeView.prototype._addReplyAttachments = 467 function(){ 468 this._showForwardField(this._msg, ZmComposeView.ADD_ORIG_MSG_ATTS, true); 469 }; 470 471 ZmComposeView.prototype._handleInlineAtts = 472 function(msg, handleInlineDocs){ 473 474 var handled = false, ci, cid, dfsrc, inlineAtt, attached = {}; 475 476 var idoc = this._htmlEditor._getIframeDoc(); 477 var images = idoc ? idoc.getElementsByTagName("img") : []; 478 for (var i = 0; i < images.length; i++) { 479 dfsrc = images[i].getAttribute("dfsrc") || images[i].getAttribute("data-mce-src") || images[i].src; 480 if (dfsrc) { 481 if (dfsrc.substring(0,4) === "cid:") { 482 cid = dfsrc.substring(4).replace("%40","@"); 483 var docpath = images[i].getAttribute("doc"); 484 var mid = images[i].getAttribute('data-zimbra-id'); 485 var part = images[i].getAttribute('data-zimbra-part'); 486 487 if (docpath){ 488 msg.addInlineDocAttachment(cid, null, docpath); 489 handled = true; 490 } else if (mid && part) { 491 images[i].removeAttribute('data-zimbra-id'); 492 images[i].removeAttribute('data-zimbra-part'); 493 msg.addInlineAttachmentId(cid, mid, part, true); 494 handled = true; 495 } else { 496 ci = "<" + cid + ">"; 497 inlineAtt = msg.findInlineAtt(ci); 498 if (!inlineAtt && this._msg) { 499 inlineAtt = this._msg.findInlineAtt(ci); 500 } 501 if (inlineAtt) { 502 var id = [cid, inlineAtt.part].join("_"); 503 if (!attached[id]) { 504 msg.addInlineAttachmentId(cid, null, inlineAtt.part); 505 handled = true; 506 attached[id] = true; 507 } 508 } 509 } 510 } 511 } 512 } 513 514 return handled; 515 }; 516 517 ZmComposeView.prototype._generateCid = 518 function() { 519 var timeStr = "" + new Date().getTime(); 520 var hash = AjxSHA1.hex_sha1(timeStr + Dwt.getNextId()); 521 return hash + "@zimbra"; 522 }; 523 524 /** 525 * Returns the message from the form, after some basic input validation. 526 */ 527 ZmComposeView.prototype.getMsg = 528 function(attId, isDraft, dummyMsg, forceBail, contactId) { 529 530 // Check destination addresses. 531 var addrs = this._recipients.collectAddrs(); 532 533 // Any addresses at all provided? If not, bail. 534 if ((!isDraft || forceBail) && !addrs.gotAddress) { 535 this.enableInputs(false); 536 var msgDialog = appCtxt.getMsgDialog(); 537 msgDialog.setMessage(ZmMsg.noAddresses, DwtMessageDialog.CRITICAL_STYLE); 538 msgDialog.popup(); 539 msgDialog.registerCallback(DwtDialog.OK_BUTTON, this._okCallback, this); 540 this.enableInputs(true); 541 return; 542 } 543 544 var cd = appCtxt.getOkCancelMsgDialog(); 545 cd.reset(); 546 547 // Is there a subject? If not, ask the user if they want to send anyway. 548 var subject = AjxStringUtil.trim(this._subjectField.value); 549 if ((!isDraft || forceBail) && subject.length === 0 && !this._noSubjectOkay) { 550 this.enableInputs(false); 551 cd.setMessage(ZmMsg.compSubjectMissing, DwtMessageDialog.WARNING_STYLE); 552 cd.registerCallback(DwtDialog.OK_BUTTON, this._noSubjectOkCallback, this, cd); 553 cd.registerCallback(DwtDialog.CANCEL_BUTTON, this._noSubjectCancelCallback, this, cd); 554 cd.popup(); 555 return; 556 } 557 558 // Any bad addresses? If there are bad ones, ask the user if they want to send anyway. 559 if ((!isDraft || forceBail) && addrs[ZmComposeView.BAD].size() && !this._badAddrsOkay) { 560 this.enableInputs(false); 561 var bad = AjxStringUtil.htmlEncode(addrs[ZmComposeView.BAD].toString(AjxEmailAddress.SEPARATOR)); 562 var msg = AjxMessageFormat.format(ZmMsg.compBadAddresses, bad); 563 cd.setMessage(msg, DwtMessageDialog.WARNING_STYLE); 564 cd.registerCallback(DwtDialog.OK_BUTTON, this._badAddrsOkCallback, this, cd); 565 cd.registerCallback(DwtDialog.CANCEL_BUTTON, this._badAddrsCancelCallback, this, [addrs.badType, cd]); 566 cd.setVisible(true); // per fix for bug 3209 567 cd.popup(); 568 return; 569 } else { 570 this._badAddrsOkay = false; 571 } 572 573 // Mandatory Spell Check 574 if ((!isDraft || forceBail) && appCtxt.get(ZmSetting.SPELL_CHECK_ENABLED) && 575 appCtxt.get(ZmSetting.MAIL_MANDATORY_SPELLCHECK) && !this._spellCheckOkay) { 576 if (this._htmlEditor.checkMisspelledWords(this._spellCheckShield.bind(this), null, this._spellCheckErrorShield.bind(this))) { 577 return; 578 } 579 } else { 580 this._spellCheckOkay = false; 581 } 582 583 // Create Msg Object - use dummy if provided 584 var msg = dummyMsg || (new ZmMailMsg()); 585 msg.setSubject(subject); 586 587 var zeroSizedAttachments = false; 588 // handle Inline Attachments 589 if (attId && (this._getIsAttInline() || (this._attachDialog && this._attachDialog.isInline()) || attId.clipboardPaste)) { 590 for (var i = 0; i < attId.length; i++) { 591 var att = attId[i]; 592 if (att.s === 0) { 593 zeroSizedAttachments = true; 594 continue; 595 } 596 var contentType = att.ct; 597 if (contentType && contentType.indexOf("image") !== -1) { 598 var cid = this._generateCid(); 599 if( att.hasOwnProperty("id") ){ 600 this._htmlEditor.replaceImage(att.id, "cid:" + cid); 601 } 602 else { 603 this._htmlEditor.insertImage("cid:" + cid, AjxEnv.isIE); 604 } 605 msg.addInlineAttachmentId(cid, att.aid); 606 } else { 607 msg.addAttachmentId(att.aid); 608 } 609 } 610 } else if (attId && typeof attId !== "string") { 611 for (var i = 0; i < attId.length; i++) { 612 if (attId[i].s === 0) { 613 zeroSizedAttachments = true; 614 continue; 615 } 616 msg.addAttachmentId(attId[i].aid); 617 } 618 } else if (attId) { 619 msg.addAttachmentId(attId); 620 } 621 622 if (zeroSizedAttachments) { 623 appCtxt.setStatusMsg(ZmMsg.zeroSizedAtts); 624 } 625 626 // check if this is a resend 627 if (this.sendUID && this.backupForm) { 628 // if so, check if user changed anything since canceling the send 629 if (isDraft || this._backupForm() !== this.backupForm) { 630 this.sendUID = (new Date()).getTime(); 631 } 632 } else { 633 this.sendUID = (new Date()).getTime(); 634 } 635 636 // get list of message part id's for any forwarded attachments 637 var forwardAttIds = this._getForwardAttIds(ZmComposeView.FORWARD_ATT_NAME + this._sessionId, !isDraft && this._hideOriginalAttachments), 638 forwardAttObjs = this._getForwardAttObjs(forwardAttIds), 639 attachedMsgIds = AjxUtil.arrayAsHash(AjxUtil.map(forwardAttObjs, function(m) { return m.mid; })), 640 forwardMsgIds = []; 641 642 if (this._msgIds) { 643 // Get any message ids added via the attachment dialog (See 644 // _attsDoneCallback which adds new forwarded attachments to msgIds) 645 forwardMsgIds = this._msgIds; 646 this._msgIds = null; 647 } 648 if (this._msgAttId) { 649 // Forward one message or Reply as attachment 650 forwardMsgIds.push(this._msgAttId); 651 } 652 if (this._origMsgAtt) { 653 attachedMsgIds[this._origMsgAtt.mid] = true; 654 } 655 // make sure we're not attaching a msg twice by checking for its ID in our list of forwarded attachments 656 forwardMsgIds = AjxUtil.filter(forwardMsgIds, function(m) { return !attachedMsgIds[m]; }); 657 658 // -------------------------------------------- 659 // Passed validation checks, message ok to send 660 // -------------------------------------------- 661 662 // build MIME 663 var top = this._getTopPart(msg, isDraft); 664 665 msg.setTopPart(top); 666 msg.setSubject(subject); 667 msg.setForwardAttIds(forwardAttIds); 668 msg.setForwardAttObjs(forwardAttObjs); 669 if (!contactId) { 670 //contactId not passed in, but vcard signature may be set 671 if (this._msg && this._msg._contactAttIds) { 672 contactId = this._msg._contactAttIds; 673 this._msg.setContactAttIds([]); 674 } 675 } 676 msg.setContactAttIds(contactId); 677 for (var i = 0; i < ZmMailMsg.COMPOSE_ADDRS.length; i++) { 678 var type = ZmMailMsg.COMPOSE_ADDRS[i]; 679 if (addrs[type] && addrs[type].all.size() > 0) { 680 msg.setAddresses(type, addrs[type].all); 681 } 682 } 683 msg.identity = this.getIdentity(); 684 msg.sendUID = this.sendUID; 685 686 if (!msg.identity) { 687 msg.delegatedSenderAddr = this.identitySelect.getValue(); 688 var option = this.identitySelect.getSelectedOption(); 689 msg.delegatedSenderAddrIsDL = option.getExtraData("isDL"); 690 msg.isOnBehalfOf = option.getExtraData("isObo"); 691 } 692 // save a reference to the original message 693 msg._origMsg = this._msg; 694 if (this._msg && this._msg._instanceDate) { 695 msg._instanceDate = this._msg._instanceDate; 696 } 697 698 this._setMessageFlags(msg); 699 700 if (this._action === ZmOperation.DRAFT && this._origAcctMsgId) { 701 msg.origAcctMsgId = this._origAcctMsgId; 702 } 703 704 // replied/forw msg or draft shouldn't have att ID (a repl/forw voicemail mail msg may) 705 if (this._msg && this._msg.attId) { 706 msg.addAttachmentId(this._msg.attId); 707 } 708 709 msg.setMessageAttachmentId(forwardMsgIds); 710 711 var priority = this._controller._getPriority(); 712 if (priority) { 713 msg.flagLocal(priority, true); 714 } 715 716 if (this._fromSelect) { 717 msg.fromSelectValue = this._fromSelect.getSelectedOption(); 718 } 719 720 if (!this._zimletCheck(msg, isDraft, forceBail)) { 721 return; 722 } 723 724 return msg; 725 }; 726 727 ZmComposeView.prototype._getTopPart = 728 function(msg, isDraft, bodyContent) { 729 730 // set up message parts as necessary 731 var top = new ZmMimePart(); 732 var textContent; 733 var content = bodyContent || this._getEditorContent(); 734 735 if (this._composeMode == Dwt.HTML) { 736 top.setContentType(ZmMimeTable.MULTI_ALT); 737 738 // experimental code for generating text part 739 if (false && this._htmlEditor) { 740 var userText = AjxStringUtil.convertHtml2Text(this.getUserText()); 741 this.setComponent(ZmComposeView.BC_TEXT_PRE, userText) 742 this._setReturns(Dwt.TEXT); 743 var xxx = this._layoutBodyComponents(this._compList, Dwt.TEXT); 744 // this.resetBody({ extraBodyText:userText }, true); 745 // this._composeMode = Dwt.HTML; 746 this._setReturns(Dwt.HTML); 747 } 748 749 // create two more mp's for text and html content types 750 var textPart = new ZmMimePart(); 751 textPart.setContentType(ZmMimeTable.TEXT_PLAIN); 752 textContent = this._htmlToText(content); 753 textPart.setContent(textContent); 754 top.children.add(textPart); 755 756 var htmlPart = new ZmMimePart(); 757 htmlPart.setContentType(ZmMimeTable.TEXT_HTML); 758 759 if (this._htmlEditor) { 760 var idoc = this._htmlEditor._getIframeDoc(); 761 this._cleanupFileRefImages(idoc); 762 this._restoreMultipartRelatedImages(idoc); 763 if (!isDraft) { 764 this._cleanupSignatureIds(idoc); 765 } 766 htmlPart.setContent(this._fixStyles(this._getEditorContent(!isDraft))); 767 } 768 else { 769 htmlPart.setContent(bodyContent); 770 } 771 772 var content = "<html><body>" + AjxStringUtil.defangHtmlContent(htmlPart.getContent()) + "</body></html>"; 773 774 htmlPart.setContent(content); 775 776 if (this._htmlEditor) { 777 this._handleInlineAtts(msg, true); 778 } 779 var inlineAtts = msg.getInlineAttachments(); 780 var inlineDocAtts = msg.getInlineDocAttachments(); 781 var iAtts = [].concat(inlineAtts, inlineDocAtts); 782 if (iAtts && iAtts.length > 0) { 783 var relatedPart = new ZmMimePart(); 784 relatedPart.setContentType(ZmMimeTable.MULTI_RELATED); 785 relatedPart.children.add(htmlPart); 786 top.children.add(relatedPart); 787 } else { 788 top.children.add(htmlPart); 789 } 790 } 791 else { 792 var inline = this._isInline(); 793 794 var textPart = (this._extraParts || inline) ? new ZmMimePart() : top; 795 textPart.setContentType(ZmMimeTable.TEXT_PLAIN); 796 textContent = content; 797 textPart.setContent(textContent); 798 799 if (inline) { 800 top.setContentType(ZmMimeTable.MULTI_ALT); 801 var relatedPart = new ZmMimePart(); 802 relatedPart.setContentType(ZmMimeTable.MULTI_RELATED); 803 relatedPart.children.add(textPart); 804 top.children.add(relatedPart); 805 } else { 806 if (this._extraParts) { 807 top.setContentType(ZmMimeTable.MULTI_ALT); 808 top.children.add(textPart); 809 } 810 } 811 } 812 813 // add extra message parts 814 if (this._extraParts) { 815 for (var i = 0; i < this._extraParts.length; i++) { 816 var mimePart = this._extraParts[i]; 817 top.children.add(mimePart); 818 } 819 } 820 821 // store text-content of the current email for zimlets to work with 822 // TODO: zimlets are being lazy here, and text content could be large; zimlets should get content from parts 823 msg.textBodyContent = !this._htmlEditor ? textContent : (this._composeMode === Dwt.HTML) 824 ? this._htmlEditor.getTextVersion() 825 : this._getEditorContent(); 826 827 return top; 828 }; 829 830 // Returns the editor content with any markers stripped (unless told not to strip them) 831 ZmComposeView.prototype._getEditorContent = 832 function(leaveMarkers) { 833 var content = ""; 834 if (this._htmlEditor) { 835 content = this._htmlEditor.getContent(true); 836 if (!leaveMarkers && (this._composeMode === Dwt.TEXT)) { 837 content = this._removeMarkers(content); 838 } 839 } 840 return content; 841 }; 842 843 // Bug 27422 - Firefox and Safari implementation of execCommand("bold") 844 // etc use styles, and some email clients (Entourage) don't process the 845 // styles and the text remains plain. So we post-process and convert 846 // those to the tags (which are what the IE version of execCommand() does). 847 ZmComposeView.prototype._fixStyles = 848 function(text) { 849 if (AjxEnv.isFirefox) { 850 text = text.replace(/<span style="font-weight: bold;">(.+?)<\/span>/, "<strong>$1</strong>"); 851 text = text.replace(/<span style="font-style: italic;">(.+?)<\/span>/, "<em>$1</em>"); 852 text = text.replace(/<span style="text-decoration: underline;">(.+?)<\/span>/, "<u>$1</u>"); 853 text = text.replace(/<span style="text-decoration: line-through;">(.+?)<\/span>/, "<strike>$1</strike>"); 854 } else if (AjxEnv.isSafari) { 855 text = text.replace(/<span class="Apple-style-span" style="font-weight: bold;">(.+?)<\/span>/, "<strong>$1</strong>"); 856 text = text.replace(/<span class="Apple-style-span" style="font-style: italic;">(.+?)<\/span>/, "<em>$1</em>"); 857 text = text.replace(/<span class="Apple-style-span" style="text-decoration: underline;">(.+?)<\/span>/, "<u>$1</u>"); 858 text = text.replace(/<span class="Apple-style-span" style="text-decoration: line-through;">(.+?)<\/span>/, "<strike>$1</strike>"); 859 } 860 return text; 861 }; 862 863 ZmComposeView.prototype._setMessageFlags = 864 function(msg) { 865 866 if (this._msg) { 867 var isInviteReply = ZmComposeController.IS_INVITE_REPLY[this._action]; 868 if (this._action === ZmOperation.DRAFT || this._msg.isDraft) { 869 msg.isReplied = (this._msg.rt === "r"); 870 msg.isForwarded = (this._msg.rt === "w"); 871 msg.isDraft = this._msg.isDraft; 872 // check if we're resaving a draft that was originally a reply/forward 873 if (msg.isDraft) { 874 // if so, set both origId and the draft id 875 msg.origId = (msg.isReplied || msg.isForwarded) ? this._msg.origId : null; 876 msg.id = this._msg.id; 877 msg.nId = this._msg.nId; 878 } 879 } else { 880 msg.isReplied = ZmComposeController.IS_REPLY[this._action]; 881 msg.isForwarded = ZmComposeController.IS_FORWARD[this._action]; 882 msg.origId = this._msg.id; 883 } 884 msg.isOfflineCreated = this._msg.isOfflineCreated; 885 msg.isInviteReply = isInviteReply; 886 msg.acceptFolderId = this._acceptFolderId; 887 var notifyActionMap = ZmComposeView.NOTIFY_ACTION_MAP || {}; 888 var inviteMode = notifyActionMap[this._action] ? notifyActionMap[this._action] : this._action; 889 msg.inviteMode = isInviteReply ? inviteMode : null; 890 if (!this.isEditAsNew && this._action !== ZmOperation.NEW_MESSAGE && (!msg.isDraft || msg.isReplied)){ //Bug: 82942 - in-reply-to shouldn't be added to new messages. 891 //when editing a saved draft (only from the drafts folder "edit") - _origMsg is the draft msg instead of the replied to message. 892 msg.irtMessageId = this._origMsg.isDraft ? this._origMsg.irtMessageId : this._origMsg.messageId; 893 } 894 msg.folderId = this._msg.folderId; 895 } 896 }; 897 898 ZmComposeView.prototype._zimletCheck = 899 function(msg, isDraft, forceBail) { 900 901 /* 902 * finally, check for any errors via zimlets.. 903 * A Zimlet can listen to emailErrorCheck action to perform further check and 904 * alert user about the error just before sending email. We will be showing 905 * yes/no dialog. This zimlet must return an object {hasError:<true or false>, 906 * errorMsg:<Some Error msg>, zimletName:<zimletName>} e.g: {hasError:true, 907 * errorMsg:"you might have forgotten attaching an attachment, do you want to 908 * continue?", zimletName:"com_zimbra_attachmentAlert"} 909 **/ 910 if ((!isDraft || forceBail) && appCtxt.areZimletsLoaded()) { 911 var boolAndErrorMsgArray = []; 912 var showErrorDlg = false; 913 var errorMsg = ""; 914 var zimletName = ""; 915 appCtxt.notifyZimlets("emailErrorCheck", [msg, boolAndErrorMsgArray]); 916 var blen = boolAndErrorMsgArray.length; 917 for (var k = 0; k < blen; k++) { 918 var obj = boolAndErrorMsgArray[k]; 919 if (obj === null || obj === undefined) { continue; } 920 921 var hasError = obj.hasError; 922 zimletName = obj.zimletName; 923 if (Boolean(hasError)) { 924 if (this._ignoredZimlets) { 925 if (this._ignoredZimlets[zimletName]) { // if we should ignore this zimlet 926 delete this._ignoredZimlets[zimletName]; 927 continue; // skip 928 } 929 } 930 showErrorDlg = true; 931 errorMsg = obj.errorMsg; 932 break; 933 } 934 } 935 if (showErrorDlg) { 936 this.enableInputs(false); 937 var cd = appCtxt.getOkCancelMsgDialog(); 938 cd.setMessage(errorMsg, DwtMessageDialog.WARNING_STYLE); 939 var params = {errDialog:cd, zimletName:zimletName}; 940 cd.registerCallback(DwtDialog.OK_BUTTON, this._errViaZimletOkCallback, this, params); 941 cd.registerCallback(DwtDialog.CANCEL_BUTTON, this._errViaZimletCancelCallback, this, params); 942 cd.popup(); 943 return false; 944 } 945 } 946 947 return true; 948 }; 949 950 ZmComposeView.prototype.setDocAttachments = 951 function(msg, docIds) { 952 953 if (!docIds) { return; } 954 955 var zeroSizedAttachments = false; 956 var inline = this._isInline(); 957 for (var i = 0; i < docIds.length; i++) { 958 var docAtt = docIds[i]; 959 var contentType = docAtt.ct; 960 if (docAtt.s === 0) { 961 zeroSizedAttachments = true; 962 continue; 963 } 964 if (this._attachDialog && inline) { 965 if (contentType && contentType.indexOf("image") !== -1) { 966 var cid = this._generateCid(); 967 this._htmlEditor.insertImage("cid:" + cid, AjxEnv.isIE); 968 msg.addInlineDocAttachment(cid, docAtt.id); 969 } else { 970 msg.addDocumentAttachment(docAtt); 971 } 972 } else { 973 msg.addDocumentAttachment(docAtt); 974 } 975 } 976 if (zeroSizedAttachments){ 977 appCtxt.setStatusMsg(ZmMsg.zeroSizedAtts); 978 } 979 }; 980 981 // Sets the mode the editor should be in. 982 ZmComposeView.prototype.setComposeMode = 983 function(composeMode, initOnly) { 984 985 if (composeMode === this._composeMode) { return; } 986 987 var htmlMode = (composeMode === Dwt.HTML); 988 if (htmlMode && !appCtxt.get(ZmSetting.HTML_COMPOSE_ENABLED)) { return; } 989 990 var previousMode = this._composeMode; 991 var modeSwitch = (!initOnly && previousMode && composeMode && previousMode !== composeMode); 992 var userText = modeSwitch && this.getUserText(); 993 var quotedText = modeSwitch && this._getQuotedText(); 994 this._composeMode = composeMode; 995 this._setReturns(); 996 997 // switch the editor's mode 998 this._htmlEditor.setContent(""); 999 this._htmlEditor.setMode(composeMode); 1000 1001 if (modeSwitch) { 1002 userText = htmlMode ? AjxStringUtil.convertToHtml(userText) : AjxStringUtil.trim(this._htmlToText(userText)) + this._crlf; 1003 var op = htmlMode ? ZmOperation.FORMAT_HTML : ZmOperation.FORMAT_TEXT; 1004 this.resetBody({ extraBodyText:userText, quotedText:quotedText, op:op, keepAttachments:true }); 1005 } 1006 1007 // reset the body field Id and object ref 1008 this._bodyFieldId = this._htmlEditor.getBodyFieldId(); 1009 this._bodyField = Dwt.byId(this._bodyFieldId); 1010 if (this._bodyField.disabled) { 1011 this._bodyField.disabled = false; 1012 } 1013 1014 this._resetBodySize(); 1015 1016 // recalculate form value since HTML mode inserts HTML tags 1017 this._setFormValue(); 1018 1019 if (!htmlMode) { 1020 this._retryHtmlEditorFocus(); //this was previously in a block I removed, so keeping it here. (don't want to create rare focus regressions) 1021 this._moveCaretOnTimer(); 1022 } 1023 1024 if (this._msg && this._isInline(this._msg) && composeMode === Dwt.TEXT) { 1025 this._showForwardField(this._msg, this._action, true); 1026 } 1027 }; 1028 1029 ZmComposeView.prototype._retryHtmlEditorFocus = 1030 function() { 1031 if (this._htmlEditor.hasFocus()) { 1032 setTimeout(this._focusHtmlEditor, 10); 1033 } 1034 }; 1035 1036 /** 1037 * Handles compose in new window. 1038 * 1039 * @param params 1040 */ 1041 ZmComposeView.prototype.setDetach = 1042 function(params) { 1043 1044 this._action = params.action; 1045 this._controller._origAction = params.origAction; 1046 this._msg = params.msg; 1047 1048 // set the addr fields as populated 1049 for (var type in params.addrs) { 1050 this._recipients.setAddress(type, ""); 1051 var addrs = AjxUtil.toArray(params.addrs[type]); 1052 this._recipients.addAddresses(type, AjxVector.fromArray(addrs)); 1053 } 1054 1055 this._subjectField.value = params.subj || ""; 1056 this._controller._setPriority(params.priority); 1057 this.updateTabTitle(); 1058 1059 var content = params.body || ""; 1060 if ((content == "") && (this.getComposeMode() == Dwt.HTML)) { 1061 content = "<br>"; 1062 } 1063 this._htmlEditor.setContent(content); 1064 1065 this._msgAttId = params.msgAttId; 1066 if (params.attHtml) { 1067 this._attcDiv.innerHTML = params.attHtml; 1068 } 1069 if (params.partMap && params.partMap.length) { 1070 this._partToAttachmentMap = params.partMap; 1071 } 1072 if (params.origMsgAtt && params.origMsgAtt.part) { 1073 this._origMsgAtt = params.origMsgAtt; 1074 } 1075 1076 if (params.identityId && this.identitySelect) { 1077 var opt = this.identitySelect.getOptionAtIndex(params.identityId); 1078 this.identitySelect.setSelectedOption(opt); 1079 this._controller.resetIdentity(params.identity.id); 1080 } 1081 1082 this.backupForm = params.backupForm; 1083 this.sendUID = params.sendUID; 1084 1085 // bug 14322 -- in Windows Firefox, DEL/BACKSPACE don't work 1086 // when composing in new window until we (1) enter some text 1087 // or (2) resize the window (!). I chose the latter. 1088 if (AjxEnv.isGeckoBased && AjxEnv.isWindows) { 1089 window.resizeBy(1, 1); 1090 } 1091 }; 1092 1093 ZmComposeView.prototype.reEnableDesignMode = 1094 function() { 1095 if (this._composeMode === Dwt.HTML) { 1096 this._htmlEditor.reEnableDesignMode(); 1097 } 1098 }; 1099 1100 // user just saved draft, update compose view as necessary 1101 ZmComposeView.prototype.processMsgDraft = 1102 function(msgDraft) { 1103 1104 if (this._isInline(msgDraft)) { 1105 this._handleInline(msgDraft); 1106 } 1107 this.reEnableDesignMode(); 1108 this._msg = msgDraft; 1109 1110 var incOptions = this._controller._curIncOptions; 1111 if (!this._origMsgAtt && this._msgAttId && incOptions && incOptions.what === ZmSetting.INC_ATTACH) { 1112 var msgIdx = AjxUtil.indexOf(msgDraft._msgAttIds, this._msgAttId), 1113 attMsgs = AjxUtil.filter(msgDraft.attachments, function(att) { 1114 return att.getContentType() === ZmMimeTable.MSG_RFC822; 1115 }), 1116 attMsg = attMsgs[msgIdx]; 1117 if (attMsg) { 1118 this._origMsgAtt = { 1119 size: attMsg.size, 1120 part: attMsg.part, 1121 mid: this._msgAttId, 1122 draftId: msgDraft.id 1123 } 1124 } 1125 } 1126 1127 this._msgAttId = null; 1128 // always redo att links since user couldve removed att before saving draft 1129 this.cleanupAttachments(true); 1130 this._showForwardField(msgDraft, ZmOperation.DRAFT); 1131 this._resetBodySize(); 1132 // save form state (to check for change later) 1133 this._setFormValue(); 1134 }; 1135 1136 ZmComposeView.prototype._handleInline = 1137 function(msgObj) { 1138 return this._fixMultipartRelatedImages(msgObj || this._msg, this._htmlEditor._getIframeDoc()); 1139 }; 1140 1141 ZmComposeView.prototype._fixMultipartRelatedImages_onTimer = 1142 function(msg, account) { 1143 // The first time the editor is initialized, idoc.getElementsByTagName("img") is empty. 1144 // Use a callback to fix images after editor is initialized. 1145 var idoc = this._htmlEditor._getIframeDoc(); 1146 if (this._firstTimeFixImages) { 1147 var callback = this._fixMultipartRelatedImages.bind(this, msg, idoc, account); 1148 this._htmlEditor.addOnContentInitializedListener(callback); 1149 //set timeout in case ZmHtmlEditor.prototype.onLoadContent is never called in which case the listener above won't be called. 1150 window.setTimeout(callback, 3000); 1151 } else { 1152 this._fixMultipartRelatedImages(msg, idoc, account); 1153 } 1154 }; 1155 1156 /** 1157 * Twiddle the img tags so that the HTML editor can display the images. Instead of 1158 * a cid (which is relevant only within the MIME msg), point to the img with a URL. 1159 * 1160 * @private 1161 */ 1162 ZmComposeView.prototype._fixMultipartRelatedImages = 1163 function(msg, idoc, account) { 1164 1165 if (this._firstTimeFixImages) { 1166 this._htmlEditor.clearOnContentInitializedListeners(); 1167 var self = this; // Fix possible hiccups during compose in new window 1168 setTimeout(function() { 1169 self._fixMultipartRelatedImages(msg, self._htmlEditor._getIframeDoc(), account); 1170 }, 10); 1171 this._firstTimeFixImages = false; 1172 return; 1173 } 1174 1175 idoc = idoc || this._htmlEditor._getIframeDoc(); 1176 if (!idoc) { return; } 1177 1178 var showImages = false; 1179 if (msg) { 1180 var addr = msg.getAddress(AjxEmailAddress.FROM); 1181 var sender = msg.getAddress(AjxEmailAddress.SENDER); // bug fix #10652 - check invite if sentBy is set (means on-behalf-of) 1182 var sentBy = (sender && sender.address) ? sender : addr; 1183 var sentByAddr = sentBy && sentBy.getAddress(); 1184 if (sentByAddr) { 1185 msg.sentByAddr = sentByAddr; 1186 msg.sentByDomain = sentByAddr.substr(sentByAddr.indexOf("@")+1); 1187 showImages = this._isTrustedSender(msg); 1188 } 1189 } 1190 1191 var images = idoc.getElementsByTagName("img"); 1192 var num = 0; 1193 for (var i = 0; i < images.length; i++) { 1194 var dfsrc = images[i].getAttribute("dfsrc") || images[i].getAttribute("data-mce-src") || images[i].src; 1195 if (dfsrc) { 1196 if (dfsrc.substring(0,4) === "cid:") { 1197 num++; 1198 var cid = "<" + dfsrc.substring(4).replace("%40","@") + ">"; 1199 var src = msg && msg.getContentPartAttachUrl(ZmMailMsg.CONTENT_PART_ID, cid); 1200 if (src) { 1201 //Cache cleared, becoz part id's may change. 1202 src = src + "&t=" + (new Date()).getTime(); 1203 images[i].src = src; 1204 images[i].setAttribute("dfsrc", dfsrc); 1205 } 1206 } else if (dfsrc.substring(0,4) === "doc:") { 1207 var src = [appCtxt.get(ZmSetting.REST_URL, null, account), ZmFolder.SEP, dfsrc.substring(4)].join(''); 1208 images[i].src = AjxStringUtil.fixCrossDomainReference(src, false, true);; 1209 } else if (dfsrc.indexOf("//") === -1) { // check for content-location verison 1210 var src = msg && msg.getContentPartAttachUrl(ZmMailMsg.CONTENT_PART_LOCATION, dfsrc); 1211 if (src) { 1212 //Cache cleared, becoz part id's may change. 1213 src = src + "&t=" + (new Date()).getTime(); 1214 num++; 1215 images[i].src = src; 1216 images[i].setAttribute("dfsrc", dfsrc); 1217 } 1218 } 1219 else if (showImages) { 1220 var src = dfsrc;//x + "&t=" + (new Date()).getTime(); 1221 num++; 1222 images[i].src = src; 1223 images[i].setAttribute("dfsrc", dfsrc); 1224 } 1225 } 1226 } 1227 return num === images.length; 1228 }; 1229 1230 ZmComposeView.prototype._isTrustedSender = 1231 function(msg) { 1232 var trustedList = this.getTrustedSendersList(); 1233 return trustedList.contains(msg.sentByAddr.toLowerCase()) || trustedList.contains(msg.sentByDomain.toLowerCase()); 1234 }; 1235 1236 ZmComposeView.prototype.getTrustedSendersList = 1237 function() { 1238 return this._controller.getApp().getTrustedSendersList(); 1239 }; 1240 1241 ZmComposeView.prototype._cleanupFileRefImages = 1242 function(idoc) { 1243 1244 function removeImg(img){ 1245 var parent = img.parentNode; 1246 parent.removeChild(img); 1247 } 1248 1249 if (idoc) { 1250 var images = idoc.getElementsByTagName("img"); 1251 var len = images.length, fileImgs=[], img, src; 1252 for (var i = 0; i < images.length; i++) { 1253 img = images[i]; 1254 try { 1255 src = img.src; 1256 } catch(e) { 1257 //IE8 throws invalid pointer exception for src attribute when src is a data uri 1258 } 1259 if (img && src && src.indexOf('file://') == 0) { 1260 removeImg(img); 1261 i--; //removeImg reduces the images.length by 1. 1262 } 1263 } 1264 } 1265 }; 1266 1267 /** 1268 * the comment below is no longer true, but I keep it for history purposes as this is so complicated. Bug 50178 changed to setting dfsrc instead of src... 1269 * todo - perhaps rewrite the whole thing regarding inline attachments. 1270 * Change the src tags on inline img's to point to cid's, which is what we 1271 * want for an outbound MIME msg. 1272 */ 1273 ZmComposeView.prototype._restoreMultipartRelatedImages = 1274 function(idoc) { 1275 1276 if (idoc) { 1277 var images = idoc.getElementsByTagName("img"); 1278 var num = 0; 1279 for (var i = 0; i < images.length; i++) { 1280 var img = images[i]; 1281 var cid = ""; 1282 var src = img.src && unescape(img.src); 1283 var dfsrc = img.getAttribute("dfsrc") || img.getAttribute("data-mce-src"); 1284 if (dfsrc && dfsrc.indexOf("cid:") === 0) { 1285 return; //dfsrc already set so nothing to do 1286 } else if (img.src && img.src.indexOf("cid:") === 0) { 1287 cid = img.src; 1288 } else if ( dfsrc && dfsrc.substring(0,4) === "doc:"){ 1289 cid = "cid:" + this._generateCid(); 1290 img.removeAttribute("dfsrc"); 1291 img.setAttribute("doc", dfsrc.substring(4, dfsrc.length)); 1292 } else if (src && src.indexOf(appCtxt.get(ZmSetting.CSFE_MSG_FETCHER_URI)) === 0) { 1293 // bug 85129 - handle images copied from another mail 1294 var qsparams = AjxStringUtil.parseQueryString(src); 1295 1296 if (qsparams.id && qsparams.part) { 1297 cid = "cid:" + this._generateCid(); 1298 img.setAttribute('data-zimbra-id', qsparams.id); 1299 img.setAttribute('data-zimbra-part', qsparams.part); 1300 } 1301 } else { 1302 // If "Display External Images" is false then handle Reply/Forward 1303 if (dfsrc && (!this._msg || this._msg.showImages)) 1304 //IE: Over HTTPS, http src urls for images might cause an issue. 1305 try { 1306 img.src = dfsrc; 1307 } catch(ex) {}; 1308 } 1309 if (cid) { 1310 img.setAttribute("dfsrc", cid); 1311 } 1312 } 1313 } 1314 }; 1315 1316 ZmComposeView.prototype._cleanupSignatureIds = 1317 function(idoc){ 1318 var signatureEl = idoc && idoc.getElementById(this._controller._currentSignatureId); 1319 if (signatureEl) { 1320 signatureEl.removeAttribute("id"); 1321 } 1322 }; 1323 1324 /** 1325 * Display an attachment dialog - either a direct and native upload dialog or 1326 * the legacy dialog. 1327 * 1328 * @param {constant} type One of the <code>ZmComposeView.UPLOAD_</code> constants. 1329 */ 1330 ZmComposeView.prototype.showAttachmentDialog = 1331 function(type) { 1332 1333 if (this._disableAttachments) { return }; 1334 1335 // collapse the attachment menu, just in case 1336 this.collapseAttMenu(); 1337 1338 if (AjxEnv.supportsHTML5File && 1339 type !== ZmComposeView.UPLOAD_BRIEFCASE) { 1340 var isinline = (type === ZmComposeView.UPLOAD_INLINE); 1341 var fileInputElement = ZmComposeView.FILE_INPUT; 1342 if (fileInputElement) { 1343 fileInputElement.value = ""; 1344 } 1345 else { 1346 ZmComposeView.FILE_INPUT = fileInputElement = document.createElement('INPUT'); 1347 fileInputElement.type = "file"; 1348 fileInputElement.title = ZmMsg.uploadNewFile; 1349 fileInputElement.multiple = true; 1350 fileInputElement.style.display = "none"; 1351 document.body.appendChild(fileInputElement); 1352 } 1353 fileInputElement.onchange = this._submitMyComputerAttachments.bind(this, null, fileInputElement, isinline); 1354 fileInputElement.click(); 1355 return; 1356 } 1357 1358 var attachDialog = this._attachDialog = appCtxt.getAttachDialog(); 1359 1360 if (type === ZmComposeView.UPLOAD_BRIEFCASE) { 1361 attachDialog.getBriefcaseView(); 1362 } else { 1363 attachDialog.getMyComputerView(); 1364 } 1365 1366 var callback = this._attsDoneCallback.bind(this, true); 1367 attachDialog.setUploadCallback(callback); 1368 attachDialog.popup(); 1369 attachDialog.enableInlineOption(this._composeMode === Dwt.HTML); 1370 1371 if (type === ZmComposeView.UPLOAD_INLINE) 1372 attachDialog.setInline(true); 1373 1374 }; 1375 1376 /** 1377 * Revert compose view to a clean state (usually called before popping compose view). 1378 * 1379 * @param {Boolean} bEnableInputs if <code>true</code>, enable the input fields 1380 */ 1381 ZmComposeView.prototype.reset = function(bEnableInputs) { 1382 1383 DBG.println('draft', 'ZmComposeView.reset for ' + this._view); 1384 this.backupForm = null; 1385 this.sendUID = null; 1386 1387 // reset autocomplete list 1388 if (this._acAddrSelectList) { 1389 this._acAddrSelectList.reset(); 1390 this._acAddrSelectList.show(false); 1391 } 1392 1393 this._recipients.reset(); 1394 1395 // reset subject / body fields 1396 this._subjectField.value = this._subject = ""; 1397 this.updateTabTitle(); 1398 1399 this._htmlEditor.resetSpellCheck(); 1400 this._htmlEditor.clear(); 1401 1402 // this._htmlEditor.clear() resets html editor body field. 1403 // Setting this._bodyField to its latest value 1404 this._bodyField = this._htmlEditor.getBodyField(); 1405 this._bodyContent = {}; 1406 1407 // the div that holds the attc.table and null out innerHTML 1408 this.cleanupAttachments(true); 1409 1410 this._resetBodySize(); 1411 this._controller._curIncOptions = null; 1412 this._msgAttId = null; 1413 this._origMsgAtt = null; 1414 this._clearFormValue(); 1415 this._components = {}; 1416 1417 // reset dirty shields 1418 this._noSubjectOkay = this._badAddrsOkay = this._spellCheckOkay = false; 1419 1420 Dwt.setVisible(this._oboRow, false); 1421 1422 // Resetting Add attachments from original link option 1423 Dwt.setVisible(ZmId.getViewId(this._view, ZmId.CMP_REPLY_ATT_ROW), false); 1424 1425 // remove extra mime parts 1426 this._extraParts = null; 1427 1428 // enable/disable input fields 1429 this.enableInputs(bEnableInputs); 1430 1431 // reset state of the spell check button 1432 this._controller.toggleSpellCheckButton(false); 1433 1434 //reset state of previous Signature cache variable. 1435 this._previousSignature = null; 1436 this._previousSignatureMode = null; 1437 1438 // used by drafts handling in multi-account 1439 this._origAcctMsgId = null; 1440 }; 1441 1442 ZmComposeView.prototype.enableInputs = 1443 function(bEnable) { 1444 DBG.println('draft', 'ZmComposeView.enableInputs for ' + this._view + ': ' + bEnable); 1445 this._recipients.enableInputs(bEnable); 1446 this._subjectField.disabled = this._bodyField.disabled = !bEnable; 1447 }; 1448 1449 /** 1450 * Adds an extra MIME part to the message. The extra parts will be 1451 * added, in order, to the end of the parts after the primary message 1452 * part. 1453 * 1454 * @private 1455 */ 1456 ZmComposeView.prototype.addMimePart = 1457 function(mimePart) { 1458 if (!this._extraParts) { 1459 this._extraParts = []; 1460 } 1461 this._extraParts.push(mimePart); 1462 }; 1463 1464 // Returns the full content for the signature, including surrounding tags if in HTML mode. 1465 ZmComposeView.prototype._getSignatureContentSpan = 1466 function(params) { 1467 1468 var signature = params.signature || this.getSignatureById(this._controller.getSelectedSignature(), params.account); 1469 if (!signature) { return ""; } 1470 1471 var signatureId = signature.id; 1472 var mode = params.mode || this._composeMode; 1473 var sigContent = params.sigContent || this.getSignatureContent(signatureId, mode); 1474 if (mode === Dwt.HTML) { 1475 var markerHtml = ""; 1476 if (params.style === ZmSetting.SIG_OUTLOOK) { 1477 markerHtml = " " + ZmComposeView.BC_HTML_MARKER_ATTR + "='" + params.marker + "'"; 1478 } 1479 sigContent = ["<div id=\"", signatureId, "\"", markerHtml, ">", sigContent, "</div>"].join(''); 1480 } 1481 1482 return this._getSignatureSeparator(params) + sigContent; 1483 }; 1484 1485 ZmComposeView.prototype._attachSignatureVcard = 1486 function(signatureId) { 1487 1488 var signature = this.getSignatureById(signatureId); 1489 if (signature && signature.contactId && !this._findVcardAtt(this._msg, signature, false)) { 1490 if (!this._msg) { 1491 this._msg = new ZmMailMsg(); 1492 } 1493 if (this._msg._contactAttIds) { 1494 this._msg._contactAttIds.push(signature.contactId); 1495 } else { 1496 this._msg.setContactAttIds(signature.contactId); 1497 } 1498 1499 //come back later and see if we need to save the draft 1500 AjxTimedAction.scheduleAction(this._checkSaveDraft.bind(this), 500); 1501 } 1502 }; 1503 1504 ZmComposeView.prototype._updateSignatureVcard = 1505 function(oldSignatureId, newSignatureId) { 1506 1507 if (oldSignatureId) { 1508 var hadVcard = false; 1509 // uncheck box for previous vcard att so it gets removed 1510 var oldSig = this.getSignatureById(oldSignatureId); 1511 if (oldSig && oldSig.contactId) { 1512 var vcardPart = this._findVcardAtt(this._msg, oldSig, true), 1513 inputs = document.getElementsByName(ZmComposeView.FORWARD_ATT_NAME + this._sessionId); 1514 1515 if (inputs && inputs.length) { 1516 for (var i = 0; i < inputs.length; i++) { 1517 if (inputs[i].value === vcardPart) { 1518 var span = inputs[i].parentNode && inputs[i].parentNode.parentNode; 1519 if (span && span.id) { 1520 this._removeAttachedMessage(span.id, vcardPart); 1521 hadVcard = true; 1522 } 1523 } 1524 } 1525 } 1526 } 1527 if (hadVcard && !newSignatureId) { 1528 this._controller.saveDraft(ZmComposeController.DRAFT_TYPE_MANUAL); 1529 } 1530 } 1531 }; 1532 1533 /** 1534 * Searches for a vcard in a message's attachments. If the contact has not yet been loaded, we assume that 1535 * a vcard attachment that we find is for that contact. Multiple vcard attachment where the first one is not 1536 * from the signature should be very rare. 1537 * 1538 * @param {ZmMailMsg} msg mail message 1539 * @param {ZmSignature} signature a signature 1540 * @param {boolean} removeAtt if true, remove the vcard attachment from the msg 1541 * 1542 * @returns {string} part number of vcard, or "undefined" if not found 1543 * @private 1544 */ 1545 ZmComposeView.prototype._findVcardAtt = function(msg, signature, removeAtt) { 1546 1547 if (signature && signature.contactId) { 1548 1549 var vcardPart, 1550 atts = msg && msg.attachments; 1551 1552 if (atts && atts.length) { 1553 //we need to figure out what input to uncheck 1554 var sigContact, 1555 item = appCtxt.cacheGet(signature.contactId); 1556 1557 if (item && item.isZmContact) { 1558 sigContact = item; 1559 } 1560 1561 for (var i = 0; i < atts.length && !vcardPart; i++) { 1562 var att = atts[i]; 1563 if (ZmMimeTable.isVcard(att.contentType)) { 1564 //we may have multiple vcards, determine which one to remove based on signature in cache 1565 if (sigContact) { 1566 // remove the .vcf file extension and try to match on the contact's name 1567 var name = att.fileName.substring(0, att.fileName.length - 4); 1568 if (name === sigContact._fileAs) { 1569 vcardPart = att.part; 1570 } 1571 } 1572 else { 1573 vcardPart = att.part; 1574 } 1575 if (removeAtt) { 1576 atts.splice(i, 1); 1577 } 1578 } 1579 } 1580 } 1581 } 1582 1583 return vcardPart; 1584 }; 1585 1586 ZmComposeView.prototype._checkSaveDraft = 1587 function() { 1588 if (this._msg && this._msg._contactAttIds && this._msg._contactAttIds.length > 0) { 1589 this._controller.saveDraft(ZmComposeController.DRAFT_TYPE_MANUAL, null, null, null, this._msg._contactAttIds); 1590 } 1591 }; 1592 1593 /* 1594 * Convertor for text nodes that, unlike the one in AjxStringUtil._traverse, doesn't append spaces to the results 1595 */ 1596 ZmComposeView._convertTextNode = 1597 function(el, ctxt) { 1598 1599 if (el.nodeValue.search(AjxStringUtil._NON_WHITESPACE) !== -1) { 1600 if (ctxt.lastNode === "ol" || ctxt.lastNode === "ul") { 1601 return "\n"; 1602 } 1603 if (ctxt.isPreformatted) { 1604 return AjxStringUtil.trim(el.nodeValue); 1605 } else { 1606 return AjxStringUtil.trim(el.nodeValue.replace(AjxStringUtil._LF, "")); 1607 } 1608 } 1609 return ""; 1610 }; 1611 1612 ZmComposeView.prototype.dispose = 1613 function() { 1614 if (this._identityChangeListenerObj) { 1615 var collection = appCtxt.getIdentityCollection(); 1616 if (collection) { 1617 collection.removeChangeListener(this._identityChangeListenerObj); 1618 } 1619 } 1620 DwtComposite.prototype.dispose.call(this); 1621 }; 1622 1623 ZmComposeView.prototype.getSignatureById = 1624 function(signatureId, account) { 1625 signatureId = signatureId || this._controller.getSelectedSignature(); 1626 return appCtxt.getSignatureCollection(account).getById(signatureId); 1627 }; 1628 1629 ZmComposeView.prototype.getSignatureContent = 1630 function(signatureId, mode) { 1631 1632 var extraSignature = this._getExtraSignature(); 1633 signatureId = signatureId || this._controller.getSelectedSignature(); 1634 1635 if (!signatureId && !extraSignature) { return; } 1636 1637 var signature; 1638 1639 // for multi-account, search all accounts for this signature ID 1640 if (appCtxt.multiAccounts) { 1641 var ac = window.parentAppCtxt || window.appCtxt; 1642 var list = ac.accountList.visibleAccounts; 1643 for (var i = 0; i < list.length; i++) { 1644 var collection = appCtxt.getSignatureCollection(list[i]); 1645 if (collection) { 1646 signature = collection.getById(signatureId); 1647 if (signature) { 1648 break; 1649 } 1650 } 1651 } 1652 } else { 1653 signature = appCtxt.getSignatureCollection().getById(signatureId); 1654 } 1655 1656 if (!signature && !extraSignature) { return; } 1657 1658 mode = mode || this._composeMode; 1659 var htmlMode = (mode === Dwt.HTML); 1660 var sig = signature ? signature.getValue(htmlMode ? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN) : ""; 1661 sig = AjxStringUtil.trim(sig + extraSignature) + (htmlMode ? "" : this._crlf); 1662 1663 return sig; 1664 }; 1665 1666 /** 1667 * Returns "" or extra signature (like a quote or legal disclaimer) via zimlet 1668 */ 1669 ZmComposeView.prototype._getExtraSignature = 1670 function() { 1671 var extraSignature = ""; 1672 if (appCtxt.zimletsPresent()) { 1673 var buffer = []; 1674 appCtxt.notifyZimlets("appendExtraSignature", [buffer]); 1675 extraSignature = buffer.join(this._crlf); 1676 if (extraSignature) { 1677 extraSignature = this._crlf + extraSignature; 1678 } 1679 } 1680 return extraSignature; 1681 }; 1682 1683 ZmComposeView.prototype._getSignatureSeparator = 1684 function(params) { 1685 1686 var sep = ""; 1687 params = params || {}; 1688 if (params.style === ZmSetting.SIG_INTERNET) { 1689 var mode = params.mode || this._composeMode; 1690 if (mode === Dwt.HTML) { 1691 sep = "<div " + ZmComposeView.BC_HTML_MARKER_ATTR + "='" + params.marker + "'>-- " + this._crlf + "</div>"; 1692 } 1693 else { 1694 sep += "-- " + this._crlf; 1695 } 1696 } 1697 return sep; 1698 }; 1699 1700 ZmComposeView.prototype._getSignatureIdForAction = 1701 function(identity, action) { 1702 1703 identity = identity || this.getIdentity(); 1704 action = action || this._action; 1705 var field = (ZmComposeController.IS_REPLY[action] || ZmComposeController.IS_FORWARD[action]) ? ZmIdentity.REPLY_SIGNATURE : ZmIdentity.SIGNATURE; 1706 return identity && identity.getField(field); 1707 }; 1708 1709 /** 1710 * Returns true if form contents have changed, or if they are empty. 1711 * 1712 * @param incAddrs takes addresses into consideration 1713 * @param incSubject takes subject into consideration 1714 * 1715 * @private 1716 */ 1717 ZmComposeView.prototype.isDirty = 1718 function(incAddrs, incSubject) { 1719 1720 if (this._isDirty) { 1721 DBG.println('draft', 'ZmComposeView.isDirty ' + this._view + ': true'); 1722 return true; 1723 } 1724 1725 // Addresses, Subject, non-html mode edit content 1726 var curFormValue = this._formValue(incAddrs, incSubject); 1727 // Html editor content changed 1728 var htmlEditorDirty = this._htmlEditor && this._htmlEditor.isDirty(), 1729 dirty = (curFormValue !== this._origFormValue) || htmlEditorDirty; 1730 1731 DBG.println('draft', 'ZmComposeView.isDirty ' + this._view + ': ' + dirty); 1732 1733 return dirty; 1734 }; 1735 1736 ZmComposeView.removeAttachedFile = function(ev, cvId, spanId, partId) { 1737 1738 var composeView = DwtControl.fromElementId(cvId); 1739 if (composeView) { 1740 if (ev.type === 'click' || (ev.type === 'keypress' && DwtKeyEvent.getCharCode(ev) === 13)) { 1741 composeView._removeAttachedFile(spanId, partId); 1742 } 1743 } 1744 }; 1745 1746 ZmComposeView.prototype._removeAttachedFile = 1747 function(spanId, attachmentPart) { 1748 1749 var node = document.getElementById(spanId), 1750 parent = node && node.parentNode; 1751 this._attachCount--; 1752 1753 if (parent) { 1754 parent.removeChild(node); 1755 } 1756 1757 /* Not sure about the purpose of below code so commenting it out instead of deleting. 1758 When a attachment is removed it should not change the original message. See bug 76776. 1759 1760 if (attachmentPart) { 1761 var numAttachments = (this._msg && this._msg.attachments && this._msg.attachments.length ) || 0; 1762 for (var i = 0; i < numAttachments; i++) { 1763 if (this._msg.attachments[i].part === attachmentPart) { 1764 this._msg.attachments.splice(i, 1); 1765 break; 1766 } 1767 } 1768 }*/ 1769 1770 if (!parent.childNodes.length) { 1771 this.cleanupAttachments(true); 1772 } 1773 }; 1774 1775 ZmComposeView.prototype._removeAttachedMessage = 1776 function(spanId, id){ 1777 1778 // Forward/Reply one message 1779 if (!id) { 1780 this._msgAttId = this._origMsgAtt = null; 1781 } 1782 else { 1783 var index = this._msgIds && this._msgIds.length ? AjxUtil.indexOf(this._msgIds, id) : -1; 1784 if (index !== -1) { 1785 // Remove message from attached messages 1786 this._msgIds.splice(index, 1); 1787 } 1788 } 1789 1790 this._removeAttachedFile(spanId); 1791 }; 1792 1793 ZmComposeView.prototype.cleanupAttachments = 1794 function(all) { 1795 1796 var attachDialog = this._attachDialog; 1797 if (attachDialog && attachDialog.isPoppedUp()) { 1798 attachDialog.popdown(); 1799 } 1800 1801 if (all) { 1802 var hint = AjxEnv.supportsHTML5File && !this._disableAttachments ? 1803 ZmMsg.dndTooltip : " "; 1804 1805 this._attcDiv.innerHTML = 1806 AjxTemplate.expand('mail.Message#NoAttachments', { hint: hint }); 1807 this._attcDiv.style.height = ""; 1808 this._attcTabGroup.removeAllMembers(); 1809 this._attachCount = 0; 1810 } 1811 1812 // make sure att IDs don't get reused 1813 if (this._msg) { 1814 this._msg.attId = null; 1815 this._msg._contactAttIds = []; 1816 } 1817 }; 1818 1819 ZmComposeView.prototype.sendMsgOboIsOK = 1820 function() { 1821 return Dwt.getVisible(this._oboRow) ? this._oboCheckbox.checked : false; 1822 }; 1823 1824 ZmComposeView.prototype.updateTabTitle = 1825 function() { 1826 var buttonText = this._subjectField.value 1827 ? this._subjectField.value.substr(0, ZmAppViewMgr.TAB_BUTTON_MAX_TEXT) 1828 : ZmComposeController.DEFAULT_TAB_TEXT; 1829 appCtxt.getAppViewMgr().setTabTitle(this._controller.getCurrentViewId(), buttonText); 1830 }; 1831 1832 /** 1833 * Used in multi-account mode to determine which account this composer is 1834 * belongs to. 1835 */ 1836 ZmComposeView.prototype.getFromAccount = 1837 function() { 1838 var ac = window.parentAppCtxt || window.appCtxt; 1839 return this._fromSelect 1840 ? (ac.accountList.getAccount(this._fromSelect.getSelectedOption().accountId)) 1841 : (ac.accountList.defaultAccount || ac.accountList.activeAccount || ac.accountList.mainAccount); 1842 }; 1843 1844 // Private / protected methods 1845 1846 ZmComposeView.prototype._getForwardAttObjs = 1847 function(parts) { 1848 var forAttObjs = []; 1849 for (var i = 0; i < this._partToAttachmentMap.length; i++) { 1850 for (var j = 0; j < parts.length; j++) { 1851 if (this._partToAttachmentMap[i].part === parts[j]) { 1852 forAttObjs.push( { part : parts[j], mid : this._partToAttachmentMap[i].mid } ); 1853 break; 1854 } 1855 } 1856 } 1857 return forAttObjs; 1858 }; 1859 1860 ZmComposeView.prototype._getForwardAttIds = 1861 function(name, removeOriginalAttachments) { 1862 1863 var forAttIds = []; 1864 var forAttList = document.getElementsByName(name); 1865 1866 // walk collection of input elements 1867 for (var i = 0; i < forAttList.length; i++) { 1868 var part = forAttList[i].value; 1869 if (this._partToAttachmentMap.length && removeOriginalAttachments) { 1870 var att = this._partToAttachmentMap[i].part; 1871 var original = this._originalAttachments[att.label]; 1872 original = original && att.sizeInBytes; 1873 if (removeOriginalAttachments && original) { 1874 continue; 1875 } 1876 } 1877 forAttIds.push(part); 1878 } 1879 1880 return forAttIds; 1881 }; 1882 1883 /** 1884 * Set various address headers based on the original message and the mode we're in. 1885 * Make sure not to duplicate any addresses, even across fields. Figures out what 1886 * addresses to put in To: and Cc: unless the caller passes addresses to use (along 1887 * with their type). 1888 * 1889 * @param {string} action compose action 1890 * @param {string} type address type 1891 * @param {AjxVector|array} override addresses to use 1892 */ 1893 ZmComposeView.prototype._setAddresses = 1894 function(action, type, override) { 1895 1896 if (override) { 1897 this._recipients.addAddresses(type, override); 1898 } 1899 else { 1900 var identityId = this.identitySelect && this.identitySelect.getValue(); 1901 var addresses = ZmComposeView.getReplyAddresses(action, this._msg, this._origMsg, identityId); 1902 if (addresses) { 1903 var toAddrs = addresses[AjxEmailAddress.TO]; 1904 if (!(toAddrs && toAddrs.length)) { 1905 // make sure we have at least one TO address if possible 1906 var addrVec = this._origMsg.getAddresses(AjxEmailAddress.TO); 1907 addresses[AjxEmailAddress.TO] = addrVec.getArray().slice(0, 1); 1908 } 1909 for (var i = 0; i < ZmMailMsg.COMPOSE_ADDRS.length; i++) { 1910 var type = ZmMailMsg.COMPOSE_ADDRS[i]; 1911 this._recipients.addAddresses(type, addresses[type]); 1912 } 1913 } 1914 } 1915 }; 1916 1917 ZmComposeView.getReplyAddresses = 1918 function(action, msg, addrsMsg, identityId) { 1919 1920 addrsMsg = addrsMsg || msg; 1921 var addresses = {}; 1922 if ((action == ZmOperation.NEW_MESSAGE) || !msg || !addrsMsg) { 1923 return null; 1924 } 1925 1926 ZmComposeController._setStatics(); 1927 if (ZmComposeController.IS_REPLY[action]) { 1928 var ac = window.parentAppCtxt || window.appCtxt; 1929 1930 // Prevent user's login name and aliases from becoming recipient addresses 1931 var userAddrs = {}; 1932 var account = appCtxt.multiAccounts && msg.getAccount(); 1933 var uname = ac.get(ZmSetting.USERNAME, null, account); 1934 if (uname) { 1935 userAddrs[uname.toLowerCase()] = true; 1936 } 1937 var aliases = ac.get(ZmSetting.MAIL_ALIASES, null, account); 1938 for (var i = 0, count = aliases.length; i < count; i++) { 1939 userAddrs[aliases[i].toLowerCase()] = true; 1940 } 1941 1942 // Check for canonical addresses 1943 var defaultIdentity = ac.getIdentityCollection(account).defaultIdentity; 1944 if (defaultIdentity && defaultIdentity.sendFromAddress) { 1945 // Note: sendFromAddress is same as appCtxt.get(ZmSetting.USERNAME) 1946 // if the account does not have any canonical address assigned. 1947 userAddrs[defaultIdentity.sendFromAddress.toLowerCase()] = true; 1948 } 1949 1950 // When updating address lists, use addresses msg instead of msg, because 1951 // msg changes after a draft is saved. 1952 var isDefaultIdentity = !identityId || (identityId && (defaultIdentity.id === identityId)); 1953 var addrVec = addrsMsg.getReplyAddresses(action, userAddrs, isDefaultIdentity, true); 1954 addresses[AjxEmailAddress.TO] = addrVec ? addrVec.getArray() : []; 1955 if (action === ZmOperation.REPLY_ALL || action === ZmOperation.CAL_REPLY_ALL) { 1956 var toAddrs = addrsMsg.getAddresses(AjxEmailAddress.TO, userAddrs, false, true); 1957 var ccAddrs = addrsMsg.getAddresses(AjxEmailAddress.CC, userAddrs, false, true); 1958 toAddrs.addList(ccAddrs); 1959 addresses[AjxEmailAddress.CC] = toAddrs.getArray(); 1960 } 1961 } else if (action === ZmOperation.DRAFT || action === ZmOperation.SHARE) { 1962 for (var i = 0; i < ZmMailMsg.COMPOSE_ADDRS.length; i++) { 1963 var type = ZmMailMsg.COMPOSE_ADDRS[i]; 1964 var addrs = msg.getAddresses(type); 1965 addresses[type] = addrs ? addrs.getArray() : []; 1966 } 1967 } else if (action === ZmOperation.DECLINE_PROPOSAL) { 1968 var toAddrs = addrsMsg.getAddresses(AjxEmailAddress.FROM); 1969 addresses[AjxEmailAddress.TO] = toAddrs ? toAddrs.getArray() : []; 1970 } 1971 1972 if (action === ZmOperation.DRAFT) { 1973 //don't mess with draft addresses, this is what the user wanted, this is what they'll get, including duplicates. 1974 return addresses; 1975 } 1976 1977 // Make a pass to remove duplicate addresses 1978 var addresses1 = {}, used = {}; 1979 for (var i = 0; i < ZmMailMsg.COMPOSE_ADDRS.length; i++) { 1980 var type = ZmMailMsg.COMPOSE_ADDRS[i]; 1981 var addrs1 = addresses1[type] = []; 1982 var addrs = addresses[type]; 1983 if (addrs && addrs.length) { 1984 for (var j = 0, len = addrs.length; j < len; j++) { 1985 var addr = addrs[j]; 1986 if (!used[addr.address]) { 1987 addrs1.push(addr); 1988 } 1989 used[addr.address] = true; 1990 } 1991 } 1992 } 1993 return addresses1; 1994 }; 1995 1996 ZmComposeView.prototype._setSubject = 1997 function(action, msg, subjOverride) { 1998 1999 if ((action === ZmOperation.NEW_MESSAGE && !subjOverride)) { 2000 return; 2001 } 2002 2003 var subj = subjOverride || (msg ? msg.subject : ""); 2004 2005 if (action === ZmOperation.REPLY_CANCEL && !subj) { 2006 var inv = msg && msg.invite; 2007 if (inv) { 2008 subj = inv.getName(); 2009 } 2010 } 2011 2012 if (action !== ZmOperation.DRAFT && subj) { 2013 subj = ZmMailMsg.stripSubjectPrefixes(subj); 2014 } 2015 2016 var prefix = ""; 2017 switch (action) { 2018 case ZmOperation.CAL_REPLY: 2019 case ZmOperation.CAL_REPLY_ALL: 2020 case ZmOperation.REPLY: 2021 case ZmOperation.REPLY_ALL: prefix = "Re: "; break; 2022 case ZmOperation.REPLY_CANCEL: prefix = ZmMsg.cancelled + ": "; break; 2023 case ZmOperation.FORWARD_INLINE: 2024 case ZmOperation.FORWARD_ATT: prefix = "Fwd: "; break; 2025 case ZmOperation.REPLY_ACCEPT: prefix = ZmMsg.subjectAccept + ": "; break; 2026 case ZmOperation.REPLY_DECLINE: prefix = ZmMsg.subjectDecline + ": "; break; 2027 case ZmOperation.REPLY_TENTATIVE: prefix = ZmMsg.subjectTentative + ": "; break; 2028 case ZmOperation.REPLY_NEW_TIME: prefix = ZmMsg.subjectNewTime + ": "; break; 2029 } 2030 2031 subj = this._subject = prefix + (subj || ""); 2032 if (this._subjectField) { 2033 this._subjectField.value = subj; 2034 this.updateTabTitle(); 2035 } 2036 }; 2037 2038 ZmComposeView.prototype._setBody = function(action, msg, extraBodyText, noEditorUpdate, keepAttachments, extraBodyTextIsExternal, incOptions) { 2039 2040 this._setReturns(); 2041 var htmlMode = (this._composeMode === Dwt.HTML); 2042 2043 var isDraft = (action === ZmOperation.DRAFT); 2044 2045 // get reply/forward prefs as necessary 2046 var incOptions = this._controller._curIncOptions = this._controller._curIncOptions || incOptions; 2047 var ac = window.parentAppCtxt || window.appCtxt; 2048 if (!incOptions) { 2049 if (ZmComposeController.IS_REPLY[action]) { 2050 incOptions = {what: ac.get(ZmSetting.REPLY_INCLUDE_WHAT), 2051 prefix: ac.get(ZmSetting.REPLY_USE_PREFIX), 2052 headers: ac.get(ZmSetting.REPLY_INCLUDE_HEADERS)}; 2053 } else if (isDraft) { 2054 incOptions = {what: ZmSetting.INC_BODY}; 2055 } else if (action === ZmOperation.FORWARD_INLINE) { 2056 incOptions = {what: ZmSetting.INC_BODY, 2057 prefix: ac.get(ZmSetting.FORWARD_USE_PREFIX), 2058 headers: ac.get(ZmSetting.FORWARD_INCLUDE_HEADERS)}; 2059 } else if (action === ZmOperation.FORWARD_ATT && msg && !msg.isDraft) { 2060 incOptions = {what: ZmSetting.INC_ATTACH}; 2061 } else if (action === ZmOperation.DECLINE_PROPOSAL) { 2062 incOptions = {what: ZmSetting.INC_BODY}; 2063 } else if (action === ZmOperation.NEW_MESSAGE) { 2064 incOptions = {what: ZmSetting.INC_NONE}; 2065 } else { 2066 incOptions = {}; 2067 } 2068 this._controller._curIncOptions = incOptions; // pointer, not a copy 2069 } 2070 if (incOptions.what === ZmSetting.INC_ATTACH && !this._msg) { 2071 incOptions.what = ZmSetting.INC_NONE; 2072 } 2073 2074 // make sure we've loaded the part with the type we want to reply in, if it's available 2075 if (msg && (incOptions.what === ZmSetting.INC_BODY || incOptions.what === ZmSetting.INC_SMART)) { 2076 var desiredPartType = htmlMode ? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN; 2077 msg.getBodyPart(desiredPartType, this._setBody1.bind(this, action, msg, extraBodyText, noEditorUpdate, keepAttachments, extraBodyTextIsExternal)); 2078 } 2079 else { 2080 this._setBody1(action, msg, extraBodyText, noEditorUpdate, keepAttachments, extraBodyTextIsExternal); 2081 } 2082 }; 2083 2084 ZmComposeView.prototype._setReturns = 2085 function(mode) { 2086 mode = mode || this._composeMode; 2087 var htmlMode = (mode === Dwt.HTML); 2088 this._crlf = htmlMode ? AjxStringUtil.CRLF_HTML : AjxStringUtil.CRLF; 2089 this._crlf2 = htmlMode ? AjxStringUtil.CRLF2_HTML : AjxStringUtil.CRLF2; 2090 }; 2091 2092 // body components 2093 ZmComposeView.BC_NOTHING = "NOTHING"; // marks beginning and ending 2094 ZmComposeView.BC_TEXT_PRE = "TEXT_PRE"; // canned text (might be user-entered or some form of extraBodyText) 2095 ZmComposeView.BC_SIG_PRE = "SIG_PRE"; // a sig that goes above quoted text 2096 ZmComposeView.BC_DIVIDER = "DIVIDER"; // tells reader that quoted text is coming 2097 ZmComposeView.BC_HEADERS = "HEADERS"; // from original msg 2098 ZmComposeView.BC_QUOTED_TEXT = "QUOTED_TEXT"; // quoted text 2099 ZmComposeView.BC_SIG_POST = "SIG_POST"; // a sig that goes below quoted text 2100 2101 ZmComposeView.BC_ALL_COMPONENTS = [ 2102 ZmComposeView.BC_NOTHING, 2103 ZmComposeView.BC_TEXT_PRE, 2104 ZmComposeView.BC_SIG_PRE, 2105 ZmComposeView.BC_DIVIDER, 2106 ZmComposeView.BC_HEADERS, 2107 ZmComposeView.BC_QUOTED_TEXT, 2108 ZmComposeView.BC_SIG_POST, 2109 ZmComposeView.BC_NOTHING 2110 ]; 2111 2112 // Zero-width space character we can use to create invisible separators for text mode 2113 // Note: as of 10/31/14, Chrome Canary does not recognize \u200B (though it does find \uFEFF) 2114 ZmComposeView.BC_MARKER_CHAR = '\u200B'; 2115 ZmComposeView.BC_MARKER_REGEXP = new RegExp(ZmComposeView.BC_MARKER_CHAR, 'g'); 2116 2117 // Create a unique marker sequence (vary by length) for each component, and regexes to find them 2118 ZmComposeView.BC_TEXT_MARKER = {}; 2119 ZmComposeView.BC_TEXT_MARKER_REGEX1 = {}; 2120 ZmComposeView.BC_TEXT_MARKER_REGEX2 = {}; 2121 2122 AjxUtil.foreach(ZmComposeView.BC_ALL_COMPONENTS, function(comp, index) { 2123 if (comp !== ZmComposeView.BC_NOTHING) { 2124 // Note: relies on BC_NOTHING coming first 2125 var markerChar = ZmComposeView.BC_MARKER_CHAR; 2126 var marker = ZmComposeView.BC_TEXT_MARKER[comp] = AjxStringUtil.repeat(markerChar, index); 2127 2128 ZmComposeView.BC_TEXT_MARKER_REGEX1[comp] = new RegExp("^" + marker + "[^" + markerChar + "]"); 2129 ZmComposeView.BC_TEXT_MARKER_REGEX2[comp] = new RegExp("[^" + markerChar + "]" + marker + "[^" + markerChar + "]"); 2130 } 2131 }); 2132 2133 // HTML marker is an expando attr whose value is the name of the component 2134 ZmComposeView.BC_HTML_MARKER_ATTR = "data-marker"; 2135 2136 ZmComposeView.prototype._setBody1 = 2137 function(action, msg, extraBodyText, noEditorUpdate, keepAttachments, extraBodyTextIsExternal) { 2138 2139 var htmlMode = (this._composeMode === Dwt.HTML); 2140 var isDraft = (action === ZmOperation.DRAFT); 2141 var incOptions = this._controller._curIncOptions; 2142 2143 // clear in case of switching from "as attachment" back to "include original message" or to "don't include original" 2144 this._msgAttId = null; 2145 2146 if (extraBodyText) { 2147 // convert text if composing as HTML (check for opening < to see if content is already HTML, should work most of the time) 2148 if (extraBodyTextIsExternal && htmlMode && extraBodyText.charAt(0) !== '<') { 2149 extraBodyText = AjxStringUtil.convertToHtml(extraBodyText); 2150 } 2151 this.setComponent(ZmComposeView.BC_TEXT_PRE, this._normalizeText(extraBodyText, htmlMode)); 2152 } 2153 2154 var compList = ZmComposeView.BC_ALL_COMPONENTS; 2155 2156 if (action === ZmOperation.DRAFT) { 2157 compList = [ZmComposeView.BC_QUOTED_TEXT]; 2158 } 2159 else if (action === ZmOperation.REPLY_CANCEL) { 2160 compList = [ZmComposeView.BC_TEXT_PRE, ZmComposeView.BC_SIG_PRE, ZmComposeView.BC_SIG_POST]; 2161 } 2162 else if (incOptions.what === ZmSetting.INC_NONE || incOptions.what === ZmSetting.INC_ATTACH) { 2163 compList = [ZmComposeView.BC_NOTHING, ZmComposeView.BC_TEXT_PRE, ZmComposeView.BC_SIG_PRE, ZmComposeView.BC_SIG_POST]; 2164 if (this._msg && incOptions.what == ZmSetting.INC_ATTACH) { 2165 this._msgAttId = this._origMsg ? this._origMsg.id : this._msg.id; 2166 } 2167 } 2168 2169 var isHtmlEditorInitd = this._htmlEditor && this._htmlEditor.isHtmlModeInited(); 2170 if (this._htmlEditor && !noEditorUpdate && !isHtmlEditorInitd) { 2171 this._fixMultipartRelatedImages_onTimer(msg); 2172 this._htmlEditor.addOnContentInitializedListener(this._saveComponentContent.bind(this, true)); 2173 //set timeout in case ZmHtmlEditor.prototype.onLoadContent is never called in which case the listener above won't be called. 2174 //but don't pass "force" so if the above was called first, don't do anything. 2175 window.setTimeout(this._saveComponentContent.bind(this), 3000); 2176 } 2177 2178 var bodyInfo = {}; 2179 var what = incOptions.what; 2180 if (msg && (what === ZmSetting.INC_BODY || what === ZmSetting.INC_SMART)) { 2181 bodyInfo = this._getBodyContent(msg, htmlMode, what); 2182 } 2183 var params = {action:action, msg:msg, incOptions:incOptions, bodyInfo:bodyInfo}; 2184 var value = this._layoutBodyComponents(compList, null, params); 2185 2186 if (this._htmlEditor && !noEditorUpdate) { 2187 this._htmlEditor.setContent(value); 2188 if (!htmlMode && ZmComposeController.IS_REPLY[action]) { 2189 this._setBodyFieldCursor(); 2190 } 2191 } 2192 2193 if (isHtmlEditorInitd && !noEditorUpdate) { 2194 this._fixMultipartRelatedImages_onTimer(msg); 2195 this._saveComponentContent(true); 2196 } 2197 2198 var ac = window.parentAppCtxt || window.appCtxt; 2199 var hasInlineImages = (bodyInfo.hasInlineImages) || !ac.get(ZmSetting.VIEW_AS_HTML); 2200 if (!keepAttachments) { 2201 //do not call this when switching between text and html editor. 2202 this._showForwardField(msg || this._msg, action, hasInlineImages, bodyInfo.hasInlineAtts); 2203 } 2204 2205 var sigId = this._controller.getSelectedSignature(); 2206 if (sigId && !isDraft) { 2207 this._attachSignatureVcard(sigId); 2208 } 2209 2210 if (!this._htmlEditor && htmlMode) { 2211 // wrap <html> and <body> tags around content, and set font style 2212 value = ZmHtmlEditor._embedHtmlContent(value, true); 2213 } 2214 2215 this._bodyContent[this._composeMode] = value; 2216 }; 2217 2218 /** 2219 * Sets the value of the given component. 2220 * 2221 * @param {string} comp component identifier (ZmComposeView.BC_*) 2222 * @param {string} compValue value 2223 * @param {string} mode compose mode 2224 */ 2225 ZmComposeView.prototype.setComponent = 2226 function(comp, compValue, mode) { 2227 2228 this._components[Dwt.TEXT] = this._components[Dwt.TEXT] || {}; 2229 this._components[Dwt.HTML] = this._components[Dwt.HTML] || {}; 2230 2231 mode = mode || this._composeMode; 2232 this._components[mode][comp] = compValue; 2233 }; 2234 2235 /** 2236 * Returns the current value of the given component. 2237 * 2238 * @param {string} comp component identifier (ZmComposeView.BC_*) 2239 * @param {string} mode compose mode 2240 * @param {hash} params msg, include options, and compose mode 2241 */ 2242 ZmComposeView.prototype.getComponent = 2243 function(comp, mode, params) { 2244 2245 mode = mode || this._composeMode; 2246 var value = this._components[mode] && this._components[mode][comp]; 2247 if (value || value === ZmComposeView.EMPTY) { 2248 return value === ZmComposeView.EMPTY ? "" : value; 2249 } 2250 2251 switch (comp) { 2252 case ZmComposeView.BC_SIG_PRE: { 2253 return this._getSignatureComponent(ZmSetting.SIG_OUTLOOK, mode); 2254 } 2255 case ZmComposeView.BC_DIVIDER: { 2256 return this._getDivider(mode, params); 2257 } 2258 case ZmComposeView.BC_HEADERS: { 2259 return this._getHeaders(mode, params); 2260 } 2261 case ZmComposeView.BC_QUOTED_TEXT: { 2262 return this._getBodyComponent(mode, params || {}); 2263 } 2264 case ZmComposeView.BC_SIG_POST: 2265 return this._getSignatureComponent(ZmSetting.SIG_INTERNET, mode); 2266 } 2267 }; 2268 2269 /** 2270 * Returns true if the given component is part of the compose body. 2271 * 2272 * @param {string} comp component identifier (ZmComposeView.BC_*) 2273 */ 2274 ZmComposeView.prototype.hasComponent = 2275 function(comp) { 2276 return AjxUtil.arrayContains(this._compList, comp); 2277 }; 2278 2279 /** 2280 * Takes the given list of components and returns the text that represents the aggregate of 2281 * their content. 2282 * 2283 * @private 2284 * @param {array} components list of component IDs 2285 * @param {hash} params msg, include options, and compose mode 2286 */ 2287 ZmComposeView.prototype._layoutBodyComponents = 2288 function(components, mode, params) { 2289 2290 if (!(components && components.length)) { 2291 return ""; 2292 } 2293 2294 mode = mode || this._composeMode; 2295 var htmlMode = (mode === Dwt.HTML); 2296 this._headerText = ""; 2297 this._compList = []; 2298 var value = ""; 2299 var prevComp, prevValue; 2300 for (var i = 0; i < components.length; i++) { 2301 var comp = components[i]; 2302 var compValue = this.getComponent(comp, mode, params) || ""; 2303 var spacing = (prevComp && compValue) ? this._getComponentSpacing(prevComp, comp, prevValue, compValue) : ""; 2304 if (compValue || (comp === ZmComposeView.BC_NOTHING)) { 2305 prevComp = comp; 2306 prevValue = compValue; 2307 } 2308 if (compValue) { 2309 if (!htmlMode) { 2310 compValue = this._getMarker(Dwt.TEXT, comp) + compValue; 2311 } 2312 value += spacing + compValue; 2313 this._compList.push(comp); 2314 } 2315 } 2316 2317 return value; 2318 }; 2319 2320 ZmComposeView.prototype._getMarker = 2321 function(mode, comp) { 2322 return (this._marker && this._marker[mode] && this._marker[mode][comp]) || ""; 2323 }; 2324 2325 // Chart for determining number of blank lines between non-empty components. 2326 ZmComposeView.BC_SPACING = AjxUtil.arrayAsHash(ZmComposeView.BC_ALL_COMPONENTS, 2327 function() { return Object() }); 2328 2329 ZmComposeView.BC_SPACING[ZmComposeView.BC_NOTHING][ZmComposeView.BC_SIG_PRE] = 2; 2330 ZmComposeView.BC_SPACING[ZmComposeView.BC_NOTHING][ZmComposeView.BC_DIVIDER] = 2; 2331 ZmComposeView.BC_SPACING[ZmComposeView.BC_NOTHING][ZmComposeView.BC_SIG_POST] = 2; 2332 ZmComposeView.BC_SPACING[ZmComposeView.BC_TEXT_PRE][ZmComposeView.BC_SIG_PRE] = 1; 2333 ZmComposeView.BC_SPACING[ZmComposeView.BC_TEXT_PRE][ZmComposeView.BC_DIVIDER] = 1; 2334 ZmComposeView.BC_SPACING[ZmComposeView.BC_TEXT_PRE][ZmComposeView.BC_SIG_POST] = 1; 2335 ZmComposeView.BC_SPACING[ZmComposeView.BC_SIG_PRE][ZmComposeView.BC_DIVIDER] = 1; 2336 ZmComposeView.BC_SPACING[ZmComposeView.BC_DIVIDER][ZmComposeView.BC_QUOTED_TEXT] = 1; 2337 ZmComposeView.BC_SPACING[ZmComposeView.BC_HEADERS][ZmComposeView.BC_QUOTED_TEXT] = 1; 2338 ZmComposeView.BC_SPACING[ZmComposeView.BC_QUOTED_TEXT][ZmComposeView.BC_SIG_POST] = 1; 2339 2340 // Returns the proper amount of space (blank lines) between two components. 2341 ZmComposeView.prototype._getComponentSpacing = 2342 function(comp1, comp2, val1, val2) { 2343 2344 if (!(comp1 && comp2)) { 2345 return ""; 2346 } 2347 2348 val1 = val1 || !!(this.getComponent(comp1) || comp1 == ZmComposeView.BC_NOTHING); 2349 val2 = val2 || !!(this.getComponent(comp2) || comp2 == ZmComposeView.BC_NOTHING); 2350 2351 var num = (val1 && val2) && ZmComposeView.BC_SPACING[comp1][comp2]; 2352 // special case - HTML with headers or prefixes will create space after divider, so we don't need to add spacing 2353 var incOptions = this._controller._curIncOptions; 2354 var htmlMode = (this._composeMode === Dwt.HTML); 2355 if (htmlMode && comp1 === ZmComposeView.BC_DIVIDER && comp2 === ZmComposeView.BC_QUOTED_TEXT && 2356 (incOptions.prefix || incOptions.headers)) { 2357 num = 0; 2358 } 2359 // minimize the gap between two BLOCKQUOTE sections (which have the blue line on the left) 2360 if (htmlMode && comp1 === ZmComposeView.BC_HEADERS && comp2 === ZmComposeView.BC_QUOTED_TEXT && incOptions.prefix) { 2361 num = 0; 2362 } 2363 2364 return (num === 2) ? this._crlf2 : (num === 1) ? this._crlf : ""; 2365 }; 2366 2367 ZmComposeView.prototype._getSignatureComponent = 2368 function(style, mode) { 2369 2370 var value = ""; 2371 var ac = window.parentAppCtxt || window.appCtxt; 2372 var account = ac.multiAccounts && this.getFromAccount(); 2373 if (ac.get(ZmSetting.SIGNATURES_ENABLED, null, account) && ac.get(ZmSetting.SIGNATURE_STYLE, null, account) === style) { 2374 var comp = (style === ZmSetting.SIG_OUTLOOK) ? ZmComposeView.BC_SIG_PRE : ZmComposeView.BC_SIG_POST; 2375 var params = { 2376 style: style, 2377 account: account, 2378 mode: mode, 2379 marker: this._getMarker(mode, comp) 2380 } 2381 value = this._getSignatureContentSpan(params); 2382 } 2383 return value; 2384 }; 2385 2386 ZmComposeView.prototype._getDivider = 2387 function(mode, params) { 2388 2389 mode = mode || this._composeMode; 2390 var htmlMode = (mode === Dwt.HTML); 2391 var action = (params && params.action) || this._action; 2392 var msg = (params && params.msg) || this._msg; 2393 var incOptions = (params && params.incOptions) || this._controller._curIncOptions; 2394 var preface = ""; 2395 var marker = htmlMode && this._getMarker(mode, ZmComposeView.BC_DIVIDER); 2396 if (incOptions && incOptions.headers) { 2397 // divider is just a visual separator if there are headers below it 2398 if (htmlMode) { 2399 preface = '<hr id="' + AjxStringUtil.HTML_SEP_ID + '" ' + ZmComposeView.BC_HTML_MARKER_ATTR + '="' + marker + '">'; 2400 } else { 2401 var msgText = (action === ZmOperation.FORWARD_INLINE) ? AjxMsg.forwardedMessage : AjxMsg.origMsg; 2402 preface = [ZmMsg.DASHES, " ", msgText, " ", ZmMsg.DASHES, this._crlf].join(""); 2403 } 2404 } 2405 else if (msg) { 2406 // no headers, so summarize them by showing date, time, name, email 2407 var msgDate = msg.sentDate || msg.date; 2408 var now = new Date(msgDate); 2409 var date = AjxDateFormat.getDateInstance(AjxDateFormat.MEDIUM).format(now); 2410 var time = AjxDateFormat.getTimeInstance(AjxDateFormat.SHORT).format(now); 2411 var fromAddr = msg.getAddress(AjxEmailAddress.FROM); 2412 var fromName = fromAddr && fromAddr.getName(); 2413 var fromEmail = fromAddr && fromAddr.getAddress(); 2414 var address = fromName; 2415 if (fromEmail) { 2416 fromEmail = htmlMode ? AjxStringUtil.htmlEncode("<" + fromEmail + ">") : fromEmail; 2417 address = [address, fromEmail].join(" "); 2418 } 2419 preface = AjxMessageFormat.format(ZmMsg.replyPrefix, [date, time, address]); 2420 preface += this._crlf; 2421 if (htmlMode) { 2422 preface = '<span id="' + AjxStringUtil.HTML_SEP_ID + '" ' + ZmComposeView.BC_HTML_MARKER_ATTR + '="' + marker + '">' + preface + '</span>'; 2423 } 2424 } 2425 2426 return preface; 2427 }; 2428 2429 ZmComposeView.prototype._getHeaders = 2430 function(mode, params) { 2431 2432 mode = mode || this._composeMode; 2433 var htmlMode = (mode === Dwt.HTML); 2434 params = params || {}; 2435 var action = (params && params.action) || this._action; 2436 var msg = (params && params.msg) || this._msg; 2437 var incOptions = (params && params.incOptions) || this._controller._curIncOptions; 2438 2439 var value = ""; 2440 var headers = []; 2441 if (incOptions.headers && msg) { 2442 for (var i = 0; i < ZmComposeView.QUOTED_HDRS.length; i++) { 2443 var hdr = msg.getHeaderStr(ZmComposeView.QUOTED_HDRS[i], htmlMode); 2444 if (hdr) { 2445 headers.push(hdr); 2446 } 2447 } 2448 } 2449 2450 if (headers.length) { 2451 //TODO: this could be simplified and maybe refactored with the similar code in _getBodyComponent() 2452 //(see bug 91743) 2453 //Revisit this after the release. 2454 var text = headers.join(this._crlf) + this._crlf; 2455 var wrapParams = { 2456 text: text, 2457 preserveReturns: true, 2458 htmlMode: htmlMode, 2459 isHeaders: true 2460 } 2461 var marker = this._getMarker(Dwt.HTML, ZmComposeView.BC_HEADERS); 2462 if (incOptions.prefix) { 2463 incOptions.pre = !htmlMode && appCtxt.get(ZmSetting.REPLY_PREFIX); 2464 wrapParams.prefix = incOptions.pre; 2465 if (htmlMode) { 2466 wrapParams.before = '<div ' + ZmComposeView.BC_HTML_MARKER_ATTR + '="' + marker + '">' + AjxStringUtil.HTML_QUOTE_PREFIX_PRE; 2467 wrapParams.after = AjxStringUtil.HTML_QUOTE_PREFIX_POST + '</div>'; 2468 } 2469 value = AjxStringUtil.wordWrap(wrapParams); 2470 } 2471 else if (htmlMode) { 2472 wrapParams.before = '<div ' + ZmComposeView.BC_HTML_MARKER_ATTR + '="' + marker + '">'; 2473 wrapParams.after = '</div>'; 2474 value = AjxStringUtil.wordWrap(wrapParams); 2475 } 2476 else { 2477 value = text; 2478 } 2479 } 2480 2481 return value; 2482 }; 2483 2484 ZmComposeView.prototype._getBodyComponent = 2485 function(mode, params) { 2486 2487 mode = mode || this._composeMode; 2488 params = params || {}; 2489 var action = (params && params.action) || this._action; 2490 var htmlMode = (mode === Dwt.HTML); 2491 var msg = (params && params.msg) || this._msg; 2492 var incOptions = (params && params.incOptions) || this._controller._curIncOptions; 2493 var what = incOptions.what; 2494 var bodyInfo = params.bodyInfo || this._getBodyContent(msg, htmlMode, what); 2495 2496 var value = ""; 2497 var body = ""; 2498 if (msg && (what === ZmSetting.INC_BODY || what === ZmSetting.INC_SMART)) { 2499 body = bodyInfo.body; 2500 // Bug 7160: Strip off the ~*~*~*~ from invite replies. 2501 if (ZmComposeController.IS_INVITE_REPLY[action]) { 2502 body = body.replace(ZmItem.NOTES_SEPARATOR, ""); 2503 } 2504 if (htmlMode && body) { 2505 body = this._normalizeText(body, htmlMode); 2506 } 2507 } 2508 2509 body = AjxStringUtil.trim(body); 2510 if (body) { 2511 //TODO: this could be simplified and maybe refactored with the similar code in _getHeaders() 2512 //(see bug 91743) 2513 //Revisit this after the release. 2514 var wrapParams = { 2515 text: body, 2516 preserveReturns: true, 2517 htmlMode: htmlMode 2518 } 2519 if (htmlMode) { 2520 var marker = this._getMarker(Dwt.HTML, ZmComposeView.BC_QUOTED_TEXT); 2521 if (incOptions.prefix) { 2522 wrapParams.before = '<div ' + ZmComposeView.BC_HTML_MARKER_ATTR + '="' + marker + '">' + AjxStringUtil.HTML_QUOTE_PREFIX_PRE; 2523 wrapParams.after = AjxStringUtil.HTML_QUOTE_PREFIX_POST + '</div>'; 2524 wrapParams.prefix = appCtxt.get(ZmSetting.REPLY_PREFIX); 2525 } 2526 else { 2527 wrapParams.before = '<div ' + ZmComposeView.BC_HTML_MARKER_ATTR + '="' + marker + '">'; 2528 wrapParams.after = '</div>'; 2529 } 2530 value = AjxStringUtil.wordWrap(wrapParams); 2531 } 2532 else { 2533 if (incOptions.prefix) { 2534 wrapParams.prefix = appCtxt.get(ZmSetting.REPLY_PREFIX); 2535 value = AjxStringUtil.wordWrap(wrapParams); 2536 } 2537 else { 2538 value = body; 2539 } 2540 } 2541 } 2542 2543 return value; 2544 }; 2545 2546 // Removes the invisible markers we use in text mode, since we should not send those out as part of the msg 2547 ZmComposeView.prototype._removeMarkers = 2548 function(text) { 2549 return text.replace(ZmComposeView.BC_MARKER_REGEXP, ''); 2550 }; 2551 2552 ZmComposeView.prototype._normalizeText = 2553 function(text, isHtml) { 2554 2555 text = AjxStringUtil.trim(text); 2556 if (isHtml) { 2557 text = AjxStringUtil.trimHtml(text); 2558 } 2559 else { 2560 text = this._removeMarkers(text); 2561 text = text.replace(/\n+$/g, "\n"); // compress trailing line returns 2562 } 2563 2564 return AjxStringUtil._NON_WHITESPACE.test(text) ? text + this._crlf : ""; 2565 }; 2566 2567 /** 2568 * Returns the value of the given component as extracted from the content of the editor. 2569 * 2570 * @param {string} comp component identifier (ZmComposeView.BC_*) 2571 */ 2572 ZmComposeView.prototype.getComponentContent = 2573 function(comp) { 2574 2575 var htmlMode = (this._composeMode === Dwt.HTML); 2576 var content = this._getEditorContent(true); 2577 var compContent = ""; 2578 2579 var firstComp = this._compList[0]; 2580 for (var i = 0; i < this._compList.length; i++) { 2581 if (this._compList[i] === comp) { break; } 2582 } 2583 var nextComp = this._compList[i + 1]; 2584 var lastComp = this._compList[this._compList.length - 1]; 2585 2586 if (htmlMode) { 2587 var marker = this._getMarker(this._composeMode, comp); 2588 var idx1 = content.indexOf(marker); 2589 if (idx1 !== -1) { 2590 var chunk = content.substring(0, idx1); 2591 // found the marker (an element ID), now back up to opening of tag 2592 idx1 = chunk.lastIndexOf("<"); 2593 if (idx1 !== -1) { 2594 if (comp === lastComp) { 2595 compContent = content.substring(idx1); 2596 } 2597 else { 2598 marker = this._getMarker(Dwt.HTML, nextComp); 2599 var idx2 = marker && content.indexOf(marker); 2600 if (idx2 !== -1) { 2601 chunk = content.substring(0, idx2); 2602 idx2 = chunk.lastIndexOf("<"); 2603 if (idx2 !== -1) { 2604 compContent = content.substring(idx1, idx2); 2605 } 2606 } 2607 } 2608 } 2609 } 2610 } 2611 else { 2612 // In text mode, components are separated by markers which are varying lengths of a zero-width space 2613 var marker1 = this._getMarker(this._composeMode, comp), 2614 regex1 = ZmComposeView.BC_TEXT_MARKER_REGEX1[comp], // matches marker at beginning 2615 regex2 = ZmComposeView.BC_TEXT_MARKER_REGEX2[comp], // matches marker elsewhere 2616 start, marker2; 2617 2618 var prePreText = ""; 2619 // look for this component's marker 2620 if (regex1.test(content)) { 2621 // found it at the start of content 2622 start = marker1.length; 2623 } 2624 else if (regex2.test(content)) { 2625 // found it somewhere after the start 2626 var markerIndex = content.search(regex2) + 1; // add one to account for non-matching char at beginning of regex 2627 start = markerIndex + marker1.length; 2628 if (comp === ZmComposeView.BC_TEXT_PRE) { 2629 //special case - include stuff before the first marker for the pre text (user can add stuff before it by clicking and/or moving the cursor beyond the invisible marker) 2630 prePreText = content.substring(0, markerIndex); 2631 } 2632 } 2633 if (start > 0) { 2634 marker2 = this._getMarker(this._composeMode, nextComp); 2635 // look for the next component's marker so we know where this component's content ends 2636 regex2 = marker2 && ZmComposeView.BC_TEXT_MARKER_REGEX2[nextComp]; 2637 idx2 = regex2 && content.search(regex2) + 1; 2638 if (idx2) { 2639 // found it, take what's in between 2640 compContent = content.substring(start, idx2); 2641 } 2642 else { 2643 // this comp is last component 2644 compContent = content.substr(start); 2645 } 2646 compContent = prePreText + compContent; 2647 } 2648 } 2649 2650 return this._normalizeText(compContent, htmlMode); 2651 }; 2652 2653 ZmComposeView.prototype._saveComponentContent = 2654 function(force) { 2655 if (this._compContent && !force) { 2656 return; 2657 } 2658 this._compContent = {}; 2659 for (var i = 0; i < this._compList.length; i++) { 2660 var comp = this._compList[i]; 2661 this._compContent[comp] = this.getComponentContent(comp); 2662 } 2663 }; 2664 2665 ZmComposeView.prototype.componentContentChanged = 2666 function(comp) { 2667 return this._compContent && this.hasComponent(comp) && (this._compContent[comp] !== this.getComponentContent(comp)); 2668 }; 2669 2670 /** 2671 * Returns text that the user has typed into the editor, as long as it comes first. 2672 */ 2673 ZmComposeView.prototype.getUserText = 2674 function() { 2675 2676 var htmlMode = (this._composeMode === Dwt.HTML); 2677 var content = this._getEditorContent(true); 2678 var userText = content; 2679 if (htmlMode) { 2680 var firstComp; 2681 for (var i = 0; i < this._compList.length; i++) { 2682 if (this._compList[i] !== ZmComposeView.BC_TEXT_PRE) { 2683 firstComp = this._compList[i]; 2684 break; 2685 } 2686 } 2687 var marker = this._getMarker(this._composeMode, firstComp); 2688 var idx = content.indexOf(marker); 2689 if (idx !== -1) { 2690 var chunk = content.substring(0, idx); 2691 // found the marker (an element ID), now back up to opening of tag 2692 idx = chunk.lastIndexOf("<"); 2693 if (idx !== -1) { 2694 // grab everything before the marked element 2695 userText = chunk.substring(0, idx); 2696 } 2697 } 2698 } 2699 else { 2700 if (this.hasComponent(ZmComposeView.BC_TEXT_PRE)) { 2701 userText = this.getComponentContent(ZmComposeView.BC_TEXT_PRE); 2702 } 2703 else if (this._compList.length > 0) { 2704 var idx = content.indexOf(this._getMarker(this._composeMode, this._compList[0])); 2705 if (idx !== -1) { 2706 userText = content.substring(0, idx); 2707 } 2708 } 2709 } 2710 2711 return this._normalizeText(userText, htmlMode); 2712 }; 2713 2714 // Returns the block of quoted text from the editor, so that we can see if the user has changed it. 2715 ZmComposeView.prototype._getQuotedText = 2716 function() { 2717 return this.getComponentContent(ZmComposeView.BC_QUOTED_TEXT); 2718 }; 2719 2720 // If the user has changed the section of quoted text (eg by inline replying), preserve the changes 2721 // across whatever operation the user is performing. If we're just checking whether changes can be 2722 // preserved, return true if they can be preserved (otherwise we need to warn the user). 2723 ZmComposeView.prototype._preserveQuotedText = 2724 function(op, quotedText, check) { 2725 2726 var savedQuotedText = this._compContent && this._compContent[ZmComposeView.BC_QUOTED_TEXT]; 2727 if (check && !savedQuotedText) { 2728 return true; 2729 } 2730 quotedText = quotedText || this._getQuotedText(); 2731 var changed = (quotedText !== savedQuotedText); 2732 if (check && (!quotedText || !changed)) { 2733 return true; 2734 } 2735 2736 // track whether user has changed quoted text during this compose session 2737 this._quotedTextChanged = this._quotedTextChanged || changed; 2738 2739 if (op === ZmId.OP_ADD_SIGNATURE || op === ZmOperation.INCLUDE_HEADERS || (this._quotedTextChanged && !changed)) { 2740 // just retain quoted text as is, no conversion needed 2741 } 2742 if (op === ZmOperation.USE_PREFIX) { 2743 if (check) { 2744 return true; 2745 } 2746 var htmlMode = (this._composeMode === Dwt.HTML); 2747 var incOptions = this._controller._curIncOptions; 2748 if (incOptions.prefix) { 2749 var wrapParams = { 2750 text: quotedText, 2751 htmlMode: htmlMode, 2752 preserveReturns: true, 2753 prefix: appCtxt.get(ZmSetting.REPLY_PREFIX) 2754 } 2755 if (htmlMode) { 2756 var marker = this._getMarker(Dwt.HTML, ZmComposeView.BC_QUOTED_TEXT); 2757 wrapParams.before = '<div ' + ZmComposeView.BC_HTML_MARKER_ATTR + '="' + marker + '">' + AjxStringUtil.HTML_QUOTE_PREFIX_PRE; 2758 wrapParams.after = AjxStringUtil.HTML_QUOTE_PREFIX_POST + '</div>'; 2759 } 2760 quotedText = AjxStringUtil.wordWrap(wrapParams); 2761 } 2762 else { 2763 if (htmlMode) { 2764 quotedText = this._removeHtmlPrefix(quotedText); 2765 } 2766 else { 2767 // remove leading > or | (prefix) with optional space after it (for text there's a space, for additional level prefix there isn't) 2768 quotedText = quotedText.replace(/^[>|] ?/, ""); 2769 quotedText = quotedText.replace(/\n[>|] ?/g, "\n"); 2770 } 2771 } 2772 } 2773 else if (ZmComposeController.INC_MAP[op]) { 2774 return false; 2775 } 2776 else if (op === ZmOperation.FORMAT_HTML || op === ZmOperation.FORMAT_TEXT) { 2777 if (check) { 2778 return true; 2779 } 2780 if (op === ZmOperation.FORMAT_HTML) { 2781 var marker = this._getMarker(Dwt.HTML, ZmComposeView.BC_QUOTED_TEXT); 2782 var openTag = AjxStringUtil.HTML_QUOTE_PREFIX_PRE; 2783 var closeTag = AjxStringUtil.HTML_QUOTE_PREFIX_POST; 2784 quotedText = AjxStringUtil.convertToHtml(quotedText, true, openTag, closeTag); 2785 quotedText = '<div ' + ZmComposeView.BC_HTML_MARKER_ATTR + '="' + marker + '">' + quotedText + '</div>'; 2786 } 2787 else { 2788 quotedText = this._htmlToText(quotedText); 2789 } 2790 } 2791 2792 if (!check) { 2793 this.setComponent(ZmComposeView.BC_QUOTED_TEXT, quotedText || ZmComposeView.EMPTY); 2794 } 2795 2796 return true; 2797 }; 2798 2799 // Removes the first level of <blockquote> styling 2800 ZmComposeView.prototype._removeHtmlPrefix = 2801 function(html, prefixEl) { 2802 prefixEl = prefixEl || "blockquote"; 2803 var oldDiv = Dwt.parseHtmlFragment(html); 2804 var newDiv = document.createElement("div"); 2805 newDiv[ZmComposeView.BC_HTML_MARKER_ATTR] = this._getMarker(Dwt.HTML, ZmComposeView.BC_QUOTED_TEXT); 2806 while (oldDiv.childNodes.length) { 2807 var el = oldDiv.childNodes[0]; 2808 if (el.nodeName.toLowerCase() === prefixEl) { 2809 while (el.childNodes.length) { 2810 newDiv.appendChild(el.removeChild(el.childNodes[0])); 2811 } 2812 oldDiv.removeChild(el); 2813 } 2814 else { 2815 newDiv.appendChild(oldDiv.removeChild(el)); 2816 } 2817 } 2818 2819 return newDiv.outerHTML; 2820 }; 2821 2822 /** 2823 * Returns true unless changes have been made to quoted text and they cannot be preserved. 2824 * 2825 * @param {string} op action user is performing 2826 * @param {string} quotedText quoted text (optional) 2827 */ 2828 ZmComposeView.prototype.canPreserveQuotedText = 2829 function(op, quotedText) { 2830 return this._preserveQuotedText(op, quotedText, true); 2831 }; 2832 2833 ZmComposeView.prototype._getBodyContent = 2834 function(msg, htmlMode, incWhat) { 2835 2836 var body, bodyPart, hasInlineImages, hasInlineAtts; 2837 var crlf = htmlMode ? AjxStringUtil.CRLF_HTML : AjxStringUtil.CRLF; 2838 var crlf2 = htmlMode ? AjxStringUtil.CRLF2_HTML : AjxStringUtil.CRLF2; 2839 var getOrig = (incWhat === ZmSetting.INC_SMART); 2840 2841 var content; 2842 2843 // bug fix #7271 - if we have multiple body parts, append them all first 2844 var parts = msg.getBodyParts(); 2845 if (msg.hasMultipleBodyParts()) { 2846 var bodyArr = []; 2847 for (var k = 0; k < parts.length; k++) { 2848 var part = parts[k]; 2849 // bug: 28741 2850 if (ZmMimeTable.isRenderableImage(part.contentType)) { 2851 bodyArr.push([crlf, "[", part.contentType, ":", (part.fileName || "..."), "]", crlf].join("")); 2852 hasInlineImages = true; 2853 } else if (part.fileName && part.contentDisposition === "inline") { 2854 var attInfo = ZmMimeTable.getInfo(part.contentType); 2855 attInfo = attInfo ? attInfo.desc : part.contentType; 2856 bodyArr.push([crlf, "[", attInfo, ":", (part.fileName||"..."), "]", crlf].join("")); 2857 hasInlineAtts = true; 2858 } else if (part.contentType === ZmMimeTable.TEXT_PLAIN || (part.body && ZmMimeTable.isTextType(part.contentType))) { 2859 content = getOrig ? AjxStringUtil.getOriginalContent(part.getContent(), false) : part.getContent(); 2860 bodyArr.push( htmlMode ? AjxStringUtil.convertToHtml(content) : content ); 2861 } else if (part.contentType === ZmMimeTable.TEXT_HTML) { 2862 content = getOrig ? AjxStringUtil.getOriginalContent(part.getContent(), true) : part.getContent(); 2863 if (htmlMode) { 2864 bodyArr.push(content); 2865 } else { 2866 var div = document.createElement("div"); 2867 div.innerHTML = content; 2868 bodyArr.push(AjxStringUtil.convertHtml2Text(div)); 2869 } 2870 } 2871 } 2872 body = bodyArr.join(crlf); 2873 } else { 2874 // at this point, we should have the type of part we want if we're dealing with multipart/alternative 2875 if (htmlMode) { 2876 content = msg.getBodyContent(ZmMimeTable.TEXT_HTML); 2877 if (!content) { 2878 // just grab the first body part and convert it to HTML 2879 content = AjxStringUtil.convertToHtml(msg.getBodyContent()); 2880 } 2881 body = getOrig ? AjxStringUtil.getOriginalContent(content, true) : content; 2882 } else { 2883 hasInlineImages = msg.hasInlineImagesInMsgBody(); 2884 bodyPart = msg.getTextBodyPart(); 2885 if (bodyPart) { 2886 // cool, got a textish body part 2887 content = bodyPart.getContent(); 2888 } 2889 else { 2890 // if we can find an HTML body part, convert it to text 2891 var html = msg.getBodyContent(ZmMimeTable.TEXT_HTML, true); 2892 content = html ? this._htmlToText(html) : ""; 2893 } 2894 content = content || msg.getBodyContent(); // just grab first body part 2895 body = getOrig ? AjxStringUtil.getOriginalContent(content, false) : content; 2896 } 2897 } 2898 2899 body = body || ""; 2900 2901 if (bodyPart && AjxUtil.isObject(bodyPart) && bodyPart.isTruncated) { 2902 body += crlf2 + ZmMsg.messageTruncated + crlf2; 2903 } 2904 2905 if (!this._htmlEditor && this.getComposeMode() === Dwt.HTML) { 2906 // strip wrapper tags from original msg 2907 body = body.replace(/<\/?(html|head|body)[^>]*>/gi, ''); 2908 } 2909 2910 return {body:body, bodyPart:bodyPart, hasInlineImages:hasInlineImages, hasInlineAtts:hasInlineAtts}; 2911 }; 2912 2913 ZmComposeView.BQ_BEGIN = "BQ_BEGIN"; 2914 ZmComposeView.BQ_END = "BQ_END"; 2915 2916 ZmComposeView.prototype._htmlToText = 2917 function(html) { 2918 2919 var convertor = { 2920 "blockquote": function(el) { 2921 return "\n" + ZmComposeView.BQ_BEGIN + "\n"; 2922 }, 2923 "/blockquote": function(el) { 2924 return "\n" + ZmComposeView.BQ_END + "\n"; 2925 }, 2926 "_after": AjxCallback.simpleClosure(this._applyHtmlPrefix, this, ZmComposeView.BQ_BEGIN, ZmComposeView.BQ_END) 2927 } 2928 return AjxStringUtil.convertHtml2Text(html, convertor); 2929 }; 2930 2931 ZmComposeView.prototype._applyHtmlPrefix = 2932 function(tagStart, tagEnd, text) { 2933 2934 var incOptions = this._controller._curIncOptions; 2935 if (incOptions && incOptions.prefix) { 2936 var wrapParams = { 2937 preserveReturns: true, 2938 prefix: appCtxt.get(ZmSetting.REPLY_PREFIX) 2939 } 2940 2941 var lines = text.split("\n"); 2942 var level = 0; 2943 var out = []; 2944 var k = 0; 2945 for (var i = 0; i < lines.length; i++) { 2946 var line = lines[i]; 2947 if (line === tagStart) { 2948 level++; 2949 } else if (line === tagEnd) { 2950 level--; 2951 } else { 2952 if (!line) { 2953 var lastLine = lines[i-1]; 2954 if (lastLine && (lastLine !== tagStart && lastLine !== tagEnd)) { 2955 out[k++] = line; 2956 } 2957 } else { 2958 for (var j = 0; j < level; j++) { 2959 wrapParams.text = line; 2960 line = AjxStringUtil.wordWrap(wrapParams); 2961 } 2962 line = line.replace(/^\n|\n$/, ""); 2963 out[k++] = line; 2964 } 2965 } 2966 } 2967 return out.join("\n"); 2968 } else { 2969 return text.replace(tagStart, "").replace(tagEnd, ""); 2970 } 2971 }; 2972 2973 /** 2974 * Reconstructs the content of the body area after some sort of change (for example: format, 2975 * signature, or include options). 2976 * 2977 * @param {string} action compose action 2978 * @param {ZmMailMsg} msg original msg (in case of reply) 2979 * @param {string} extraBodyText canned text to include 2980 * @param {hash} incOptions include options 2981 * @param {boolean} keepAttachments do not cleanup the attachments 2982 * @param {boolean} noEditorUpdate if true, do not change content of HTML editor 2983 */ 2984 ZmComposeView.prototype.resetBody = 2985 function(params, noEditorUpdate) { 2986 2987 params = params || {}; 2988 var action = params.action || this._origAction || this._action; 2989 if (this._action === ZmOperation.DRAFT) { 2990 action = this._origAction; 2991 } 2992 var msg = (this._action === ZmOperation.DRAFT) ? this._origMsg : params.msg || this._origMsg; 2993 var incOptions = params.incOptions || this._controller._curIncOptions; 2994 2995 this._components = {}; 2996 2997 this._preserveQuotedText(params.op, params.quotedText); 2998 if (!params.keepAttachments) { 2999 this.cleanupAttachments(true); 3000 } 3001 this._isDirty = this._isDirty || this.isDirty(); 3002 this._setBody(action, msg, params.extraBodyText, noEditorUpdate, params.keepAttachments); 3003 this._setFormValue(); 3004 this._resetBodySize(); 3005 }; 3006 3007 /** 3008 * Removes the attachment corresponding to the original message. 3009 */ 3010 ZmComposeView.prototype.removeOrigMsgAtt = function() { 3011 3012 for (var i = 0; i < this._partToAttachmentMap.length; i++) { 3013 var att = this._partToAttachmentMap[i]; 3014 if (att.rfc822Part && this._origMsgAtt && att.sizeInBytes === this._origMsgAtt.size) { 3015 this._removeAttachedMessage(att.spanId); 3016 } 3017 } 3018 }; 3019 3020 // Generic routine for attaching an event handler to a field. Since "this" for the handlers is 3021 // the incoming event, we need a way to get at ZmComposeView, so it's added to the event target. 3022 ZmComposeView.prototype._setEventHandler = 3023 function(id, event, addrType) { 3024 var field = document.getElementById(id); 3025 field._composeViewId = this._htmlElId; 3026 if (addrType) { 3027 field._addrType = addrType; 3028 } 3029 var lcEvent = event.toLowerCase(); 3030 field[lcEvent] = ZmComposeView["_" + event]; 3031 }; 3032 3033 ZmComposeView.prototype._setBodyFieldCursor = 3034 function(extraBodyText) { 3035 3036 if (this._composeMode === Dwt.HTML) { return; } 3037 3038 // this code moves the cursor to the beginning of the body 3039 if (AjxEnv.isIE) { 3040 var tr = this._bodyField.createTextRange(); 3041 if (extraBodyText) { 3042 tr.move('character', extraBodyText.length + 1); 3043 } else { 3044 tr.collapse(true); 3045 } 3046 tr.select(); 3047 } else { 3048 var index = extraBodyText ? (extraBodyText.length + 1) : 0; 3049 Dwt.setSelectionRange(this._bodyField, index, index); 3050 } 3051 }; 3052 3053 /** 3054 * This should be called only once for when compose view loads first time around 3055 * 3056 * @private 3057 */ 3058 ZmComposeView.prototype._initialize = 3059 function(composeMode, action) { 3060 3061 this._internalId = AjxCore.assignId(this); 3062 3063 // init html 3064 this._createHtml(); 3065 3066 // init drag and drop 3067 this._initDragAndDrop(); 3068 3069 // init compose view w/ based on user prefs 3070 var bComposeEnabled = appCtxt.get(ZmSetting.HTML_COMPOSE_ENABLED); 3071 var composeFormat = appCtxt.get(ZmSetting.COMPOSE_AS_FORMAT); 3072 var defaultCompMode = bComposeEnabled && composeFormat === ZmSetting.COMPOSE_HTML 3073 ? Dwt.HTML : Dwt.TEXT; 3074 this._composeMode = composeMode || defaultCompMode; 3075 this._clearFormValue(); 3076 3077 // init html editor 3078 var attmcallback = 3079 this.showAttachmentDialog.bind(this, ZmComposeView.UPLOAD_INLINE); 3080 3081 // Focus on the editor body if its not a new message/forwarded message (where it focuses on the 'To' field). 3082 var autoFocus = (action !== ZmOperation.NEW_MESSAGE) && 3083 (action !== ZmOperation.FORWARD_INLINE) && 3084 (action !== ZmOperation.FORWARD_ATT); 3085 3086 this._htmlEditor = 3087 new ZmHtmlEditor({ 3088 parent: this, 3089 posStyle: DwtControl.RELATIVE_STYLE, 3090 mode: this._composeMode, 3091 autoFocus: autoFocus, 3092 initCallback: this._controlListener.bind(this), 3093 pasteCallback: this._uploadDoneCallback.bind(this), 3094 attachmentCallback: attmcallback 3095 }); 3096 this._bodyFieldId = this._htmlEditor.getBodyFieldId(); 3097 this._bodyField = document.getElementById(this._bodyFieldId); 3098 this._includedPreface = ""; 3099 3100 this._marker = {}; 3101 this._marker[Dwt.TEXT] = ZmComposeView.BC_TEXT_MARKER; 3102 this._marker[Dwt.HTML] = {}; 3103 for (var i = 0; i < ZmComposeView.BC_ALL_COMPONENTS.length; i++) { 3104 var comp = ZmComposeView.BC_ALL_COMPONENTS[i]; 3105 this._marker[Dwt.HTML][comp] = '__' + comp + '__'; 3106 } 3107 3108 // misc. inits 3109 this.setScrollStyle(DwtControl.SCROLL); 3110 this._attachCount = 0; 3111 3112 // init listeners 3113 this.addControlListener(new AjxListener(this, this._controlListener)); 3114 }; 3115 3116 ZmComposeView.prototype._createHtml = 3117 function(templateId) { 3118 3119 var data = { 3120 id: this._htmlElId, 3121 headerId: ZmId.getViewId(this._view, ZmId.CMP_HEADER), 3122 fromSelectId: ZmId.getViewId(this._view, ZmId.CMP_FROM_SELECT), 3123 toRowId: ZmId.getViewId(this._view, ZmId.CMP_TO_ROW), 3124 toPickerId: ZmId.getViewId(this._view, ZmId.CMP_TO_PICKER), 3125 toInputId: ZmId.getViewId(this._view, ZmId.CMP_TO_INPUT), 3126 toCellId: ZmId.getViewId(this._view, ZmId.CMP_TO_CELL), 3127 ccRowId: ZmId.getViewId(this._view, ZmId.CMP_CC_ROW), 3128 ccPickerId: ZmId.getViewId(this._view, ZmId.CMP_CC_PICKER), 3129 ccInputId: ZmId.getViewId(this._view, ZmId.CMP_CC_INPUT), 3130 ccCellId: ZmId.getViewId(this._view, ZmId.CMP_CC_CELL), 3131 bccRowId: ZmId.getViewId(this._view, ZmId.CMP_BCC_ROW), 3132 bccPickerId: ZmId.getViewId(this._view, ZmId.CMP_BCC_PICKER), 3133 bccInputId: ZmId.getViewId(this._view, ZmId.CMP_BCC_INPUT), 3134 bccCellId: ZmId.getViewId(this._view, ZmId.CMP_BCC_CELL), 3135 subjectRowId: ZmId.getViewId(this._view, ZmId.CMP_SUBJECT_ROW), 3136 subjectInputId: ZmId.getViewId(this._view, ZmId.CMP_SUBJECT_INPUT), 3137 oboRowId: ZmId.getViewId(this._view, ZmId.CMP_OBO_ROW), 3138 oboCheckboxId: ZmId.getViewId(this._view, ZmId.CMP_OBO_CHECKBOX), 3139 oboLabelId: ZmId.getViewId(this._view, ZmId.CMP_OBO_LABEL), 3140 identityRowId: ZmId.getViewId(this._view, ZmId.CMP_IDENTITY_ROW), 3141 identitySelectId: ZmId.getViewId(this._view, ZmId.CMP_IDENTITY_SELECT), 3142 replyAttRowId: ZmId.getViewId(this._view, ZmId.CMP_REPLY_ATT_ROW), 3143 attRowId: ZmId.getViewId(this._view, ZmId.CMP_ATT_ROW), 3144 attDivId: ZmId.getViewId(this._view, ZmId.CMP_ATT_DIV), 3145 attBtnId: ZmId.getViewId(this._view, ZmId.CMP_ATT_BTN) 3146 }; 3147 3148 this._createHtmlFromTemplate(templateId || this.TEMPLATE, data); 3149 }; 3150 3151 ZmComposeView.prototype._addSendAsAndSendOboAddresses = 3152 function(menu) { 3153 3154 var optData = null; 3155 var myDisplayName = appCtxt.getUsername(); 3156 this._addSendAsOrSendOboAddresses(menu, appCtxt.sendAsEmails, false, function(addr, displayName) { 3157 return displayName ? AjxMessageFormat.format(ZmMsg.sendAsAddress, [addr, displayName]) : addr; 3158 }); 3159 this._addSendAsOrSendOboAddresses(menu, appCtxt.sendOboEmails, true, function(addr, displayName) { 3160 return AjxMessageFormat.format(displayName ? ZmMsg.sendOboAddressAndDispName : ZmMsg.sendOboAddress, [myDisplayName, addr, displayName]); 3161 }); 3162 }; 3163 3164 ZmComposeView.prototype._addSendAsOrSendOboAddresses = 3165 function(menu, emails, isObo, displayValueFunc) { 3166 for (var i = 0; i < emails.length; i++) { 3167 var email = emails[i]; 3168 var addr = email.addr; 3169 var extraData = {isDL: email.isDL, isObo: isObo}; 3170 var displayValue = displayValueFunc(addr, email.displayName); 3171 var optData = new DwtSelectOptionData(addr, displayValue, null, null, null, null, extraData); 3172 menu.addOption(optData); 3173 } 3174 }; 3175 3176 3177 ZmComposeView.prototype._createHtmlFromTemplate = 3178 function(templateId, data) { 3179 3180 DwtComposite.prototype._createHtmlFromTemplate.call(this, templateId, data); 3181 3182 // global identifiers 3183 this._identityDivId = data.identityRowId; 3184 3185 this._recipients.createRecipientHtml(this, this._view, data.id, ZmMailMsg.COMPOSE_ADDRS); 3186 this._acAddrSelectList = this._recipients.getACAddrSelectList(); 3187 3188 // save reference to DOM objects per ID's 3189 this._headerEl = document.getElementById(data.headerId); 3190 this._subjectField = document.getElementById(data.subjectInputId); 3191 this._oboRow = document.getElementById(data.oboRowId); 3192 this._oboCheckbox = document.getElementById(data.oboCheckboxId); 3193 this._oboLabel = document.getElementById(data.oboLabelId); 3194 this._attcDiv = document.getElementById(data.attDivId); 3195 this._attcBtn = document.getElementById(data.attBtnId); 3196 3197 this._setEventHandler(data.subjectInputId, "onKeyUp"); 3198 this._setEventHandler(data.subjectInputId, "onFocus"); 3199 3200 if (appCtxt.multiAccounts) { 3201 if (!this._fromSelect) { 3202 this._fromSelect = new DwtSelect({parent:this, index: 0, id:this.getHTMLElId() + "_fromSelect", parentElement:data.fromSelectId}); 3203 //this._addSendAsAndSendOboAddresses(this._fromSelect); 3204 this._fromSelect.addChangeListener(new AjxListener(this, this._handleFromListener)); 3205 this._recipients.attachFromSelect(this._fromSelect); 3206 } 3207 } else { 3208 // initialize identity select 3209 var identityOptions = this._getIdentityOptions(); 3210 this.identitySelect = new DwtSelect({parent:this, index: 0, id:this.getHTMLElId() + "_identitySelect", options:identityOptions}); 3211 this._addSendAsAndSendOboAddresses(this.identitySelect); 3212 this.identitySelect.setToolTipContent(ZmMsg.chooseIdentity, true); 3213 3214 if (!this._identityChangeListenerObj) { 3215 this._identityChangeListenerObj = new AjxListener(this, this._identityChangeListener); 3216 } 3217 var ac = window.parentAppCtxt || window.appCtxt; 3218 var accounts = ac.accountList.visibleAccounts; 3219 for (var i = 0; i < accounts.length; i++) { 3220 var identityCollection = ac.getIdentityCollection(accounts[i]); 3221 identityCollection.addChangeListener(this._identityChangeListenerObj); 3222 } 3223 3224 this.identitySelect.replaceElement(data.identitySelectId); 3225 this._setIdentityVisible(); 3226 } 3227 3228 var attButtonId = ZmId.getButtonId(this._view, ZmId.CMP_ATT_BTN); 3229 this._attButton = new DwtButton({parent:this, id:attButtonId}); 3230 this._attButton.setText(ZmMsg.attach); 3231 3232 this._attButton.setMenu(new AjxCallback(this, this._attachButtonMenuCallback)); 3233 this._attButton.reparentHtmlElement(data.attBtnId); 3234 this._attButton.setToolTipContent(ZmMsg.attach, true); 3235 this._attButton.addSelectionListener( 3236 this.showAttachmentDialog.bind(this, ZmComposeView.UPLOAD_COMPUTER, false) 3237 ); 3238 }; 3239 3240 ZmComposeView.prototype._initDragAndDrop = 3241 function() { 3242 this._dnd = new ZmDragAndDrop(this); 3243 }; 3244 3245 ZmComposeView.prototype.collapseAttMenu = 3246 function() { 3247 var menu = this._attButton && this._attButton.getMenu(); 3248 menu.popdown(); 3249 }; 3250 3251 ZmComposeView.prototype._handleFromListener = 3252 function(ev) { 3253 var newVal = ev._args.newValue; 3254 var oldVal = ev._args.oldValue; 3255 if (oldVal === newVal) { return; } 3256 3257 var ac = window.parentAppCtxt || window.appCtxt; 3258 var newOption = this._fromSelect.getOptionWithValue(newVal); 3259 var newAccount = ac.accountList.getAccount(newOption.accountId); 3260 var collection = ac.getIdentityCollection(newAccount); 3261 var identity = collection && collection.getById(newVal); 3262 3263 var sigId = this._getSignatureIdForAction(identity || collection.defaultIdentity) || ""; 3264 3265 this._controller._accountName = newAccount.name; 3266 this._controller.resetSignatureToolbar(sigId, newAccount); 3267 this._controller.resetSignature(sigId, newAccount); 3268 this._controller._resetReadReceipt(newAccount); 3269 3270 // reset account for autocomplete to use 3271 if (this._acAddrSelectList) { 3272 this._acAddrSelectList.setActiveAccount(newAccount); 3273 } 3274 3275 // if this message is a saved draft, check whether it needs to be moved 3276 // based on newly selected value. 3277 if (this._msg && this._msg.isDraft) { 3278 var oldOption = this._fromSelect.getOptionWithValue(oldVal); 3279 var oldAccount = ac.accountList.getAccount(oldOption.accountId); 3280 3281 // cache old info so we know what to delete after new save 3282 var msgId = this._origAcctMsgId = this._msg.id; 3283 3284 this._msg = this._origMsg = null; 3285 var callback = new AjxCallback(this, this._handleMoveDraft, [oldAccount.name, msgId]); 3286 this._controller.saveDraft(this._controller._draftType, null, null, callback); 3287 } 3288 3289 this._recipients.resetPickerButtons(newAccount); 3290 }; 3291 3292 ZmComposeView.prototype._handleMoveDraft = 3293 function(accountName, msgId) { 3294 var jsonObj = { 3295 ItemActionRequest: { 3296 _jsns: "urn:zimbraMail", 3297 action: { id:msgId, op:"delete" } 3298 } 3299 }; 3300 var params = { 3301 jsonObj: jsonObj, 3302 asyncMode: true, 3303 accountName: accountName 3304 }; 3305 appCtxt.getAppController().sendRequest(params); 3306 }; 3307 3308 3309 ZmComposeView.prototype._createAttachMenuItem = 3310 function(menu, text, listner) { 3311 var item = DwtMenuItem.create({parent:menu, text:text}); 3312 item.value = text; 3313 if (listner) { 3314 item.addSelectionListener(listner); 3315 } 3316 return item; 3317 }; 3318 3319 ZmComposeView.prototype._startUploadAttachment = 3320 function() { 3321 this._attButton.setEnabled(false); 3322 this.enableToolbarButtons(this._controller, false); 3323 this._controller._uploadingProgress = true; 3324 }; 3325 3326 ZmComposeView.prototype.checkAttachments = 3327 function() { 3328 if (!this._attachCount) { return; } 3329 }; 3330 3331 ZmComposeView.prototype.updateAttachFileNode = 3332 function(files,index, aid) { 3333 var curFileName = this._clipFile(files[index].name, true); 3334 3335 this._loadingSpan.firstChild.innerHTML = curFileName; 3336 this._loadingSpan.firstChild.nextSibling.innerHTML = curFileName; 3337 // Set the next files progress back to 0 3338 this._setLoadingProgress(this._loadingSpan, 0); 3339 if (aid){ 3340 var prevFileName = this._clipFile(files[index-1].name, true); 3341 var element = document.createElement("span"); 3342 element.innerHTML = AjxTemplate.expand("mail.Message#MailAttachmentBubble", {fileName:prevFileName, id:aid}); 3343 var newSpan = element.firstChild; 3344 if (this._loadingSpan.nextSibling) { 3345 this._loadingSpan.parentNode.insertBefore(newSpan, this._loadingSpan.nextSibling); 3346 } else { 3347 this._loadingSpan.parentNode.appendChild(element); 3348 } 3349 // Set the previous files progress to 100% 3350 this._setLoadingProgress(newSpan, 1); 3351 } 3352 3353 }; 3354 3355 ZmComposeView.prototype.enableToolbarButtons = 3356 function(controller, enabled) { 3357 var toolbar = controller._toolbar; 3358 var sendLater = appCtxt.get(ZmSetting.MAIL_SEND_LATER_ENABLED); 3359 toolbar.getButton(sendLater ? ZmId.OP_SEND_MENU : ZmId.OP_SEND).setEnabled(enabled); 3360 toolbar.getButton(ZmId.OP_SAVE_DRAFT).setEnabled(enabled); 3361 var optionsButton = toolbar.getButton(ZmId.OP_COMPOSE_OPTIONS); 3362 if (optionsButton) { 3363 var optionsMenu = optionsButton.getMenu(); 3364 if (optionsMenu) { 3365 var menuItemsToEnable = [ZmId.OP_INC_NONE, ZmId.OP_INC_BODY, ZmId.OP_INC_SMART, ZmId.OP_INC_ATTACHMENT, ZmId.OP_USE_PREFIX, ZmId.OP_INCLUDE_HEADERS]; 3366 AjxUtil.foreach(menuItemsToEnable, function(menuItemId) { 3367 var menuItem = optionsMenu.getMenuItem(menuItemId); 3368 if (menuItem) { 3369 menuItem.setEnabled(enabled); 3370 } 3371 }); 3372 } 3373 } 3374 var detachComposeButton = toolbar.getButton(ZmId.OP_DETACH_COMPOSE); 3375 if (detachComposeButton) { 3376 detachComposeButton.setEnabled(enabled); 3377 } 3378 appCtxt.notifyZimlets("enableComposeToolbarButtons", [toolbar, enabled]); 3379 }; 3380 3381 ZmComposeView.prototype.enableAttachButton = 3382 function(option) { 3383 if (this._attButton) { 3384 this._attButton.setEnabled(option); 3385 var attachElement = this._attButton.getHtmlElement(); 3386 var node = attachElement && attachElement.getElementsByTagName("input"); 3387 if (node && node.length) { 3388 node[0].disabled = !(option); 3389 } 3390 } 3391 3392 this._disableAttachments = !(option); 3393 }; 3394 3395 3396 ZmComposeView.prototype._resetUpload = 3397 function(err) { 3398 this._attButton.setEnabled(true); 3399 this.enableToolbarButtons(this._controller, true); 3400 this._setAttInline(false); 3401 this._controller._uploadingProgress = false; 3402 if (this._controller._uploadAttReq) { 3403 this._controller._uploadAttReq = null; 3404 } 3405 3406 if (this.si) { 3407 clearTimeout(this.si); 3408 } 3409 if (err === true && this._loadingSpan) { 3410 this._loadingSpan.parentNode.removeChild(this._loadingSpan); 3411 this._controller.saveDraft(ZmComposeController.DRAFT_TYPE_AUTO);// Save the previous state 3412 } 3413 3414 if (this._loadingSpan) { 3415 this._loadingSpan = null; 3416 } 3417 3418 if (this._uploadElementForm) { 3419 this._uploadElementForm.reset(); 3420 this._uploadElementForm = null; 3421 } 3422 }; 3423 3424 ZmComposeView.prototype._uploadDoneCallback = 3425 function(resp) { 3426 var response = resp && resp.length && resp[2]; 3427 this._controller.saveDraft(ZmComposeController.DRAFT_TYPE_AUTO, response); 3428 }; 3429 3430 3431 ZmComposeView.prototype._uploadFileProgress = 3432 function(params, progress) { 3433 if (!this._loadingSpan || (!progress.lengthComputable) ) { 3434 return; 3435 } 3436 this._setLoadingProgress(this._loadingSpan, progress.loaded / progress.total); 3437 }; 3438 3439 ZmComposeView.prototype._abortUploadFile = 3440 function() { 3441 if (this._controller._uploadAttReq){ 3442 this._controller._uploadAttReq.aborted = true; 3443 this._controller._uploadAttReq.abort(); 3444 } 3445 }; 3446 3447 ZmComposeView.prototype._progress = 3448 function() { 3449 var span1 = this._loadingSpan && this._loadingSpan.firstChild; 3450 var span2 = span1 && span1.nextSibling; 3451 if (span2){ 3452 span1.style.width = ((span1.offsetWidth + 1) % span2.offsetWidth) + "px"; 3453 } 3454 }; 3455 3456 /* 3457 Set the loading progress to a specific percentage 3458 @param {Number} progress - fraction of progress (0 to 1, 1 is 100%). 3459 */ 3460 ZmComposeView.prototype._setLoadingProgress = 3461 function(loadingSpan, progress) { 3462 var finishedSpan = loadingSpan.childNodes[0]; 3463 var allSpan = loadingSpan.childNodes[1]; 3464 finishedSpan.style.width = (allSpan.offsetWidth * progress) + "px"; 3465 }; 3466 3467 ZmComposeView.prototype._initProgressSpan = 3468 function(fileName) { 3469 fileName = this._clipFile(fileName, true); 3470 3471 var firstBubble = this._attcDiv.getElementsByTagName("span")[0]; 3472 if (firstBubble) { 3473 var tempBubbleWrapper = document.createElement("span"); 3474 tempBubbleWrapper.innerHTML = AjxTemplate.expand("mail.Message#MailAttachmentBubble", {fileName: fileName}); 3475 var newBubble = tempBubbleWrapper.firstChild; 3476 firstBubble.parentNode.insertBefore(newBubble, firstBubble); //insert new bubble before first bubble. 3477 } 3478 else { 3479 //first one is enclosed in a wrapper (the template already expands the mail.Message#MailAttachmentBubble template inside the wrapper) 3480 this._attcDiv.innerHTML = AjxTemplate.expand("mail.Message#UploadProgressContainer", {fileName: fileName}); 3481 } 3482 this._loadingSpan = this._attcDiv.getElementsByTagName("span")[0]; 3483 }; 3484 3485 3486 ZmComposeView.prototype._submitMyComputerAttachments = 3487 function(files, node, isInline) { 3488 var name = ""; 3489 3490 if (!AjxEnv.supportsHTML5File) { 3491 // IE, FF 3.5 and lower 3492 this.showAttachmentDialog(ZmMsg.myComputer); 3493 return; 3494 } 3495 3496 if (!files) 3497 files = node.files; 3498 3499 var size = 0; 3500 if (files) { 3501 for (var j = 0; j < files.length; j++) { 3502 var file = files[j]; 3503 //Check the total size of the files we upload this time (we don't know the previously uploaded files total size so we do the best we can). 3504 //NOTE - we compare to the MTA message size limit since there's no limit on specific attachments. 3505 size += file.size || file.fileSize /*Safari*/ || 0; 3506 if ((-1 /* means unlimited */ != appCtxt.get(ZmSetting.MESSAGE_SIZE_LIMIT)) && 3507 (size > appCtxt.get(ZmSetting.MESSAGE_SIZE_LIMIT))) { 3508 var msgDlg = appCtxt.getMsgDialog(); 3509 var errorMsg = AjxMessageFormat.format(ZmMsg.attachmentSizeError, AjxUtil.formatSize(appCtxt.get(ZmSetting.MESSAGE_SIZE_LIMIT))); 3510 msgDlg.setMessage(errorMsg, DwtMessageDialog.WARNING_STYLE); 3511 msgDlg.popup(); 3512 return false; 3513 } 3514 } 3515 } 3516 3517 this._setAttInline(isInline); 3518 this._initProgressSpan(files[0].name); 3519 3520 this._controller._initUploadMyComputerFile(files); 3521 3522 }; 3523 3524 ZmComposeView.prototype._clipFile = function(name, encode) { 3525 var r = AjxStringUtil.clipFile(name, ZmComposeView.MAX_ATTM_NAME_LEN); 3526 3527 return encode ? AjxStringUtil.htmlEncode(r) : r; 3528 }; 3529 3530 ZmComposeView.prototype._checkMenuItems = 3531 function(menuItem) { 3532 var isHTML = (this._composeMode === Dwt.HTML); 3533 menuItem.setEnabled(isHTML); 3534 }; 3535 3536 ZmComposeView.prototype._attachButtonMenuCallback = 3537 function() { 3538 var menu = new DwtMenu({parent:this._attButton}); 3539 3540 var listener = 3541 this.showAttachmentDialog.bind(this, ZmComposeView.UPLOAD_COMPUTER); 3542 this._createAttachMenuItem(menu, ZmMsg.myComputer, listener); 3543 3544 if (AjxEnv.supportsHTML5File) { 3545 // create the item for making inline attachments 3546 var listener = 3547 this.showAttachmentDialog.bind(this, ZmComposeView.UPLOAD_INLINE); 3548 var mi = this._createAttachMenuItem(menu, ZmMsg.attachInline, listener); 3549 menu.addPopupListener(new AjxListener(this, this._checkMenuItems,[mi])); 3550 } 3551 3552 if (appCtxt.multiAccounts || appCtxt.get(ZmSetting.BRIEFCASE_ENABLED)) { 3553 var listener = 3554 this.showAttachmentDialog.bind(this, 3555 ZmComposeView.UPLOAD_BRIEFCASE); 3556 var briefcaseItem = this._createAttachMenuItem(menu, ZmMsg.briefcase, listener); 3557 briefcaseItem.setEnabled(!appCtxt.isWebClientOffline()); 3558 3559 } 3560 appCtxt.notifyZimlets("initializeAttachPopup", [menu, this], {waitUntilLoaded:true}); 3561 3562 return menu; 3563 }; 3564 3565 ZmComposeView.prototype._getIdentityOptions = 3566 function() { 3567 var options = []; 3568 var identityCollection = appCtxt.getIdentityCollection(); 3569 var identities = identityCollection.getIdentities(true); 3570 for (var i = 0, count = identities.length; i < count; i++) { 3571 var identity = identities[i]; 3572 options.push(new DwtSelectOptionData(identity.id, this._getIdentityText(identity))); 3573 } 3574 return options; 3575 }; 3576 3577 ZmComposeView.prototype._getIdentityText = 3578 function(identity, account) { 3579 var name = identity.name; 3580 if (identity.isDefault && name === ZmIdentity.DEFAULT_NAME) { 3581 name = account ? account.getDisplayName() : ZmMsg.accountDefault; 3582 } 3583 3584 // default replacement parameters 3585 var defaultIdentity = appCtxt.getIdentityCollection().defaultIdentity; 3586 var addr = (identity.sendFromAddressType === ZmSetting.SEND_ON_BEHALF_OF) ? (appCtxt.getUsername() + " " + ZmMsg.sendOnBehalfOf + " " + identity.sendFromAddress) : identity.sendFromAddress; 3587 var params = [ 3588 name, 3589 (identity.sendFromDisplay || ""), 3590 addr, 3591 ZmMsg.accountDefault, 3592 appCtxt.get(ZmSetting.DISPLAY_NAME), 3593 defaultIdentity.sendFromAddress 3594 ]; 3595 3596 // get appropriate pattern 3597 var pattern; 3598 if (identity.isDefault) { 3599 pattern = ZmMsg.identityTextPrimary; 3600 } 3601 else if (identity.isFromDataSource) { 3602 var ds = appCtxt.getDataSourceCollection().getById(identity.id); 3603 params[1] = params[1] || ds.userName || ""; 3604 params[2] = ds.getEmail(); 3605 var provider = ZmDataSource.getProviderForAccount(ds); 3606 if (provider) { 3607 pattern = ZmMsg["identityText-"+provider.id]; 3608 } 3609 else if (params[0] && params[1] && params[2] && 3610 (params[0] !== params[1] !== params[2])) 3611 { 3612 pattern = ZmMsg.identityTextPersona; 3613 } 3614 else { 3615 pattern = ZmMsg.identityTextExternal; 3616 } 3617 } 3618 else { 3619 pattern = ZmMsg.identityTextPersona; 3620 } 3621 3622 // format text 3623 return AjxMessageFormat.format(pattern, params); 3624 }; 3625 3626 ZmComposeView.prototype._identityChangeListener = 3627 function(ev) { 3628 3629 if (!this.identitySelect) { return; } 3630 3631 var identity = ev.getDetail("item"); 3632 if (!identity) { return; } 3633 if (ev.event === ZmEvent.E_CREATE) { 3634 // TODO: add identity in sort position 3635 var text = this._getIdentityText(identity); 3636 var option = new DwtSelectOptionData(identity.id, text); 3637 this.identitySelect.addOption(option); 3638 this._setIdentityVisible(); 3639 } else if (ev.event === ZmEvent.E_DELETE) { 3640 this.identitySelect.removeOptionWithValue(identity.id); 3641 this._setIdentityVisible(); 3642 } else if (ev.event === ZmEvent.E_MODIFY) { 3643 // TODO: see if it was actually name that changed 3644 // TODO: re-sort list 3645 var text = this._getIdentityText(identity); 3646 this.identitySelect.rename(identity.id, text); 3647 } 3648 }; 3649 3650 ZmComposeView.prototype._setIdentityVisible = 3651 function() { 3652 var div = document.getElementById(this._identityDivId); 3653 if (!div) { return; } 3654 3655 var visible = this.identitySelect.getOptionCount() > 1; 3656 Dwt.setVisible(div, visible); 3657 this.identitySelect.setVisible(visible); 3658 }; 3659 3660 ZmComposeView.prototype.getIdentity = 3661 function() { 3662 var ac = window.parentAppCtxt || window.appCtxt; 3663 3664 if (appCtxt.multiAccounts) { 3665 var newVal = this._fromSelect.getValue(); 3666 var newOption = this._fromSelect.getOptionWithValue(newVal); 3667 var newAccount = ac.accountList.getAccount(newOption.accountId); 3668 var collection = ac.getIdentityCollection(newAccount); 3669 return collection && collection.getById(newVal); 3670 } 3671 3672 if (this.identitySelect) { 3673 var collection = ac.getIdentityCollection(); 3674 var val = this.identitySelect.getValue(); 3675 var identity = collection.getById(val); 3676 return identity; 3677 } 3678 }; 3679 3680 ZmComposeView.prototype._showForwardField = 3681 function(msg, action, includeInlineImages, includeInlineAtts) { 3682 3683 var html = ""; 3684 var attIncludeOrigLinkId = null; 3685 this._partToAttachmentMap = []; 3686 var appCtxt = window.parentAppCtxt || window.appCtxt 3687 var messages = []; 3688 3689 var hasAttachments = msg && msg.attachments && msg.attachments.length > 0; 3690 3691 if (!this._originalAttachmentsInitialized) { //only the first time we determine which attachments are original 3692 this._originalAttachments = []; //keep it associated by label and size (label => size => true) since that's the only way the client has to identify attachments from previous msg version. 3693 this._hideOriginalAttachments = msg && hasAttachments && (action === ZmOperation.REPLY || action === ZmOperation.REPLY_ALL); 3694 } 3695 if (msg && (hasAttachments || includeInlineImages || includeInlineAtts || (action === ZmComposeView.ADD_ORIG_MSG_ATTS))) { 3696 var attInfo = msg.getAttachmentInfo(false, includeInlineImages, includeInlineAtts); 3697 3698 if (action === ZmComposeView.ADD_ORIG_MSG_ATTS) { 3699 if (this._replyAttachments !== this._msg.attachments) { 3700 attInfo = attInfo.concat(this._replyAttachInfo); 3701 this._msg.attachments = this._msg.attachments.concat(this._replyAttachments); 3702 } 3703 this._replyAttachInfo = this._replyAttachments = []; 3704 Dwt.setVisible(ZmId.getViewId(this._view, ZmId.CMP_REPLY_ATT_ROW), false); 3705 } else if (action === ZmOperation.REPLY || action === ZmOperation.REPLY_ALL) { 3706 if (attInfo && attInfo.length && !appCtxt.isWebClientOffline()) { 3707 this._replyAttachInfo = attInfo; 3708 this._replyAttachments = this._msg.attachments; 3709 this._attachCount = 0; 3710 Dwt.setVisible(ZmId.getViewId(this._view, ZmId.CMP_REPLY_ATT_ROW), true); 3711 } 3712 3713 return; 3714 } 3715 3716 if (attInfo.length > 0 && !(action === ZmOperation.FORWARD_INLINE && appCtxt.isWebClientOffline())) { 3717 var rowId = this._htmlElId + '_attach'; 3718 for (var i = 0; i < attInfo.length; i++) { 3719 var att = attInfo[i]; 3720 var params = { 3721 att: att, 3722 id: [this._view, att.part, ZmMailMsgView.ATT_LINK_MAIN].join("_"), 3723 text: this._clipFile(att.label), 3724 mid: att.mid, 3725 rfc822Part: att.rfc822Part 3726 }; 3727 att.link = ZmMailMsgView.getMainAttachmentLinkHtml(params); 3728 this._partToAttachmentMap[i] = att; 3729 if (!this._originalAttachmentsInitialized) { 3730 if (!this._originalAttachments[att.label]) { 3731 this._originalAttachments[att.label] = []; 3732 } 3733 this._originalAttachments[att.label][att.sizeInBytes] = true; 3734 } 3735 att.spanId = Dwt.getNextId(rowId); 3736 att.closeHandler = "ZmComposeView.removeAttachedFile(event, '" + [ this._htmlElId, att.spanId, att.part ].join("', '") + "');"; 3737 } 3738 attIncludeOrigLinkId = Dwt.getNextId(ZmId.getViewId(this._view, ZmId.CMP_ATT_INCL_ORIG_LINK)); 3739 3740 3741 if (action === ZmComposeView.ADD_ORIG_MSG_ATTS) { 3742 action = this._action; 3743 } 3744 3745 var data = { 3746 attachments: attInfo, 3747 messagesFwdFieldName: (ZmComposeView.FORWARD_MSG_NAME + this._sessionId), 3748 isNew: (action === ZmOperation.NEW_MESSAGE), 3749 isForward: (action === ZmOperation.FORWARD), 3750 isForwardInline: (action === ZmOperation.FORWARD_INLINE), 3751 isDraft: (action === ZmOperation.DRAFT), 3752 hideOriginalAttachments: this._hideOriginalAttachments, 3753 attIncludeOrigLinkId: attIncludeOrigLinkId, 3754 originalAttachments: this._originalAttachments, 3755 fwdFieldName: (ZmComposeView.FORWARD_ATT_NAME + this._sessionId), 3756 rowId: rowId 3757 }; 3758 html = AjxTemplate.expand("mail.Message#ForwardAttachments", data); 3759 this._attachCount = attInfo.length; 3760 this.checkAttachments(); 3761 } 3762 } 3763 3764 this._originalAttachmentsInitialized = true; //ok, done setting it for the first time. 3765 3766 if (this._attachCount > 0) { 3767 this._attcDiv.innerHTML = html; 3768 } else if (!this._loadingSpan) { 3769 this.cleanupAttachments(true); 3770 } 3771 3772 // include original attachments 3773 if (attIncludeOrigLinkId) { 3774 this._attIncludeOrigLinkEl = document.getElementById(attIncludeOrigLinkId); 3775 if (this._attIncludeOrigLinkEl) { 3776 Dwt.setHandler(this._attIncludeOrigLinkEl, DwtEvent.ONCLICK, AjxCallback.simpleClosure(this._includeOriginalAttachments, this)); 3777 } 3778 } 3779 3780 this._attcTabGroup.removeAllMembers(); 3781 var links = Dwt.byClassName('AttLink', this._attcDiv), 3782 closeButtons = Dwt.byClassName('AttachmentClose', this._attcDiv); 3783 for (var i = 0; i < links.length; i++) { 3784 var link = links[i], 3785 closeButton = closeButtons[i]; 3786 // MailMsg attachments are not displayed via the href, but rather using onClick. 3787 var onClick = link.onclick; 3788 this._makeFocusable(link); 3789 if (onClick) { 3790 Dwt.clearHandler(link, DwtEvent.ONCLICK); 3791 Dwt.setHandler(link, DwtEvent.ONCLICK, onClick); 3792 } 3793 this._attcTabGroup.addMember(link); 3794 this._attcTabGroup.addMember(closeButton); 3795 } 3796 }; 3797 3798 ZmComposeView.prototype._includeOriginalAttachments = 3799 function(ev, force) { 3800 this._hideOriginalAttachments = false; 3801 this._isIncludingOriginalAttachments = true; 3802 this._showForwardField(this._msg, this._action, true); 3803 }; 3804 3805 3806 // Miscellaneous methods 3807 ZmComposeView.prototype._resetBodySize = 3808 function() { 3809 if (!this._htmlEditor) 3810 return; 3811 3812 var size = Dwt.insetBounds(this.getInsetBounds(), 3813 this._htmlEditor.getInsets()); 3814 3815 if (size) { 3816 size.height -= Dwt.getSize(this._headerEl).y; 3817 3818 this._htmlEditor.setSize(size.width, size.height); 3819 } 3820 }; 3821 3822 3823 ZmComposeView.prototype._setFromSelect = 3824 function(msg) { 3825 if (!this._fromSelect) { return; } 3826 3827 this._fromSelect.clearOptions(); 3828 3829 var ac = window.parentAppCtxt || window.appCtxt; 3830 var identity; 3831 var active = ac.getActiveAccount(); 3832 var accounts = ac.accountList.visibleAccounts; 3833 3834 for (var i = 0; i < accounts.length; i++) { 3835 var acct = accounts[i]; 3836 if (appCtxt.isOffline && acct.isMain) { continue; } 3837 3838 var identities = ac.getIdentityCollection(acct).getIdentities(); 3839 if (ac.isFamilyMbox || ac.get(ZmSetting.OFFLINE_SMTP_ENABLED, null, acct)) { 3840 for (var j = 0; j < identities.length; j++) { 3841 identity = identities[j]; 3842 3843 var text = this._getIdentityText(identity, acct); 3844 var icon = appCtxt.isOffline ? acct.getIcon() : null; 3845 var option = new DwtSelectOption(identity.id, false, text, null, null, icon); 3846 option.addr = new AjxEmailAddress(identity.sendFromAddress, AjxEmailAddress.FROM, identity.sendFromDisplay); 3847 option.accountId = acct.id; 3848 3849 this._fromSelect.addOption(option); 3850 } 3851 } 3852 } 3853 3854 var selectedIdentity; 3855 if (msg) { 3856 var coll = ac.getIdentityCollection(msg.getAccount()); 3857 selectedIdentity = (msg.isDraft) 3858 ? coll.selectIdentity(msg, AjxEmailAddress.FROM) 3859 : coll.selectIdentity(msg); 3860 if (!selectedIdentity) { 3861 selectedIdentity = coll.defaultIdentity; 3862 } 3863 } 3864 3865 if (!selectedIdentity) { 3866 selectedIdentity = ac.getIdentityCollection(active).defaultIdentity; 3867 } 3868 3869 if (selectedIdentity && selectedIdentity.id) { 3870 this._fromSelect.setSelectedValue(selectedIdentity.id); 3871 } 3872 3873 // for cross account searches, the active account isn't necessarily the 3874 // account of the selected conv/msg so reset it based on the selected option. 3875 // if active-account is local/main acct, reset it based on selected option. 3876 if ((appCtxt.getSearchController().searchAllAccounts && this._fromSelect) || active.isMain) { 3877 active = this.getFromAccount(); 3878 this._controller._accountName = active.name; 3879 } 3880 3881 if (this._acAddrSelectList) { 3882 this._acAddrSelectList.setActiveAccount(active); 3883 } 3884 3885 this._recipients.resetPickerButtons(active); 3886 }; 3887 3888 3889 3890 // Returns a string representing the form content 3891 ZmComposeView.prototype._formValue = function(incAddrs, incSubject) { 3892 3893 var vals = []; 3894 if (incAddrs) { 3895 for (var i = 0; i < ZmMailMsg.COMPOSE_ADDRS.length; i++) { 3896 var type = ZmMailMsg.COMPOSE_ADDRS[i]; 3897 if (this._recipients.getUsing(type)) { 3898 vals.push(this._recipients.getAddrFieldValue(type)); 3899 } 3900 } 3901 } 3902 3903 if (incSubject) { 3904 vals.push(this._subjectField.value); 3905 } 3906 3907 var htmlMode = (this._composeMode === Dwt.HTML); 3908 if (!htmlMode) { 3909 var content = this._getEditorContent(); 3910 vals.push(content); 3911 } 3912 3913 return AjxUtil.collapseList(vals).join("|"); 3914 }; 3915 3916 // Listeners 3917 3918 3919 ZmComposeView.prototype._controlListener = 3920 function() { 3921 this._resetBodySize(); 3922 }; 3923 3924 3925 // Callbacks 3926 3927 // this callback is triggered when an event occurs inside the html editor (when in HTML mode) 3928 // it is used to set focus to the To: field when user hits the TAB key 3929 ZmComposeView.prototype._htmlEditorEventCallback = 3930 function(args) { 3931 var rv = true; 3932 if (args.type === "keydown") { 3933 var key = DwtKeyEvent.getCharCode(args); 3934 if (key === DwtKeyEvent.KEY_TAB) { 3935 var toField = this._recipients.getField(AjxEmailAddress.TO); 3936 if (toField) { 3937 appCtxt.getKeyboardMgr().grabFocus(toField); 3938 } 3939 rv = false; 3940 } 3941 } 3942 return rv; 3943 }; 3944 3945 // needed to reset design mode when in html compose format for gecko 3946 ZmComposeView.prototype._okCallback = 3947 function() { 3948 appCtxt.getMsgDialog().popdown(); 3949 this._controller.resetToolbarOperations(); 3950 this.reEnableDesignMode(); 3951 }; 3952 3953 // User has agreed to send message without a subject 3954 ZmComposeView.prototype._noSubjectOkCallback = 3955 function(dialog) { 3956 this._noSubjectOkay = true; 3957 this._popDownAlertAndSendMsg(dialog); 3958 }; 3959 3960 //this is used by several kinds of alert dialogs 3961 ZmComposeView.prototype._popDownAlertAndSendMsg = 3962 function(dialog) { 3963 // not sure why: popdown (in FF) seems to create a race condition, 3964 // we can't get the attachments from the document anymore. 3965 // W/in debugger, it looks fine, but remove the debugger and any 3966 // alerts, and gotAttachments will return false after the popdown call. 3967 3968 if (AjxEnv.isIE) { 3969 dialog.popdown(); 3970 } 3971 // bug fix# 3209 3972 // - hide the dialog instead of popdown (since window will go away anyway) 3973 if (AjxEnv.isNav && appCtxt.isChildWindow) { 3974 dialog.setVisible(false); 3975 } 3976 3977 // dont make any calls after sendMsg if child window since window gets destroyed 3978 if (appCtxt.isChildWindow && !AjxEnv.isNav) { 3979 // bug fix #68774 Empty warning window when sending message without subject in chrome 3980 dialog.popdown(); 3981 this._controller.sendMsg(); 3982 } else { 3983 // bug fix #3251 - call popdown BEFORE sendMsg 3984 dialog.popdown(); 3985 this._controller.sendMsg(); 3986 } 3987 }; 3988 3989 // User has canceled sending message without a subject 3990 ZmComposeView.prototype._noSubjectCancelCallback = 3991 function(dialog) { 3992 this.enableInputs(true); 3993 dialog.popdown(); 3994 appCtxt.getKeyboardMgr().grabFocus(this._subjectField); 3995 this._controller.resetToolbarOperations(); 3996 this.reEnableDesignMode(); 3997 }; 3998 3999 ZmComposeView.prototype._errViaZimletOkCallback = 4000 function(params) { 4001 var dialog = params.errDialog; 4002 var zimletName = params.zimletName; 4003 //add this zimlet to ignoreZimlet string 4004 this._ignoredZimlets = this._ignoredZimlets || {}; 4005 this._ignoredZimlets[zimletName] = true; 4006 this._popDownAlertAndSendMsg(dialog); 4007 }; 4008 4009 ZmComposeView.prototype._errViaZimletCancelCallback = 4010 function(params) { 4011 var dialog = params.errDialog; 4012 var zimletName = params.zimletName; 4013 this.enableInputs(true); 4014 dialog.popdown(); 4015 this._controller.resetToolbarOperations(); 4016 this.reEnableDesignMode(); 4017 }; 4018 4019 // User has agreed to send message with bad addresses 4020 ZmComposeView.prototype._badAddrsOkCallback = 4021 function(dialog) { 4022 this.enableInputs(true); 4023 this._badAddrsOkay = true; 4024 dialog.popdown(); 4025 this._controller.sendMsg(); 4026 }; 4027 4028 // User has declined to send message with bad addresses - set focus to bad field 4029 ZmComposeView.prototype._badAddrsCancelCallback = 4030 function(type, dialog) { 4031 this.enableInputs(true); 4032 this._badAddrsOkay = false; 4033 dialog.popdown(); 4034 if (this._recipients.getUsing(type)) { 4035 appCtxt.getKeyboardMgr().grabFocus(this._recipients.getField(type)); 4036 } 4037 this._controller.resetToolbarOperations(); 4038 this.reEnableDesignMode(); 4039 }; 4040 4041 ZmComposeView.prototype._closeAttachDialog = 4042 function() { 4043 if (this._attachDialog) 4044 this._attachDialog.popdown(); 4045 4046 this._initProgressSpan(ZmMsg.uploadingAttachment); 4047 4048 var progress = function (obj) { 4049 var selfobject = obj; 4050 obj.si = window.setInterval (function() {selfobject._progress();}, 500); 4051 }; 4052 progress(this); 4053 }; 4054 4055 ZmComposeView.prototype._setAttachedMsgIds = 4056 function(msgIds) { 4057 this._msgIds = msgIds; 4058 }; 4059 4060 // Files have been uploaded, re-initiate the send with an attachment ID. 4061 ZmComposeView.prototype._attsDoneCallback = 4062 function(isDraft, status, attId, docIds, msgIds) { 4063 DBG.println(AjxDebug.DBG1, "Attachments: isDraft = " + isDraft + ", status = " + status + ", attId = " + attId); 4064 this._closeAttachDialog(); 4065 if (status === AjxPost.SC_OK) { 4066 if (msgIds) { 4067 this._setAttachedMsgIds(msgIds); 4068 } 4069 var callback = this._resetUpload.bind(this); 4070 this._startUploadAttachment(); 4071 this._controller.saveDraft(ZmComposeController.DRAFT_TYPE_AUTO, attId, docIds, callback); 4072 } else if (status === AjxPost.SC_UNAUTHORIZED) { 4073 // auth failed during att upload - let user relogin, continue with compose action 4074 this._resetUpload(true); 4075 var ex = new AjxException("401 response during attachment upload", ZmCsfeException.SVC_AUTH_EXPIRED); 4076 var callback = new AjxCallback(this._controller, isDraft ? this._controller.saveDraft : this._controller._send); 4077 this._controller._handleException(ex, {continueCallback:callback}); 4078 } else { 4079 // bug fix #2131 - handle errors during attachment upload. 4080 this._resetUpload(true); 4081 this._controller.popupUploadErrorDialog(ZmItem.MSG, status, 4082 ZmMsg.errorTryAgain); 4083 this._controller.resetToolbarOperations(); 4084 } 4085 }; 4086 4087 4088 //Mandatory Spellcheck Callback 4089 ZmComposeView.prototype._spellCheckShield = 4090 function(words) { 4091 if (words && words.available && words.misspelled && words.misspelled.length !== 0) { 4092 var msgDialog = new DwtMessageDialog({parent: appCtxt.getShell(), buttons:[DwtDialog.YES_BUTTON, DwtDialog.NO_BUTTON], id: Dwt.getNextId("SpellCheckConfirm_")}); 4093 msgDialog.setMessage(AjxMessageFormat.format(ZmMsg.misspellingsMessage, [words.misspelled.length]), DwtMessageDialog.WARNING_STYLE); 4094 msgDialog.registerCallback(DwtDialog.YES_BUTTON, this._spellCheckShieldOkListener, this, [ msgDialog, words ] ); 4095 msgDialog.registerCallback(DwtDialog.NO_BUTTON, this._spellCheckShieldCancelListener, this, msgDialog); 4096 msgDialog.associateEnterWithButton(DwtDialog.NO_BUTTON); 4097 msgDialog.getButton(DwtDialog.YES_BUTTON).setText(ZmMsg.correctSpelling); 4098 msgDialog.getButton(DwtDialog.NO_BUTTON).setText(ZmMsg.sendAnyway); 4099 var composeView = this; 4100 msgDialog.handleKeyAction = function(actionCode, ev) { if (actionCode && actionCode==DwtKeyMap.CANCEL) { composeView._spellCheckShieldOkListener(msgDialog, words, ev); return(true); } }; 4101 msgDialog.popup(null, DwtDialog.NO_BUTTON); 4102 } else { 4103 this._spellCheckOkay = true; 4104 this._controller.sendMsg(); 4105 } 4106 }; 4107 4108 ZmComposeView.prototype._spellCheckShieldOkListener = 4109 function(msgDialog, words, ev) { 4110 4111 this._controller._toolbar.enableAll(true); 4112 this.enableInputs(true); 4113 this._controller.toggleSpellCheckButton(true); 4114 this._htmlEditor.discardMisspelledWords(); 4115 4116 this._spellCheckOkay = false; 4117 msgDialog.popdown(); 4118 4119 this._htmlEditor.onExitSpellChecker = new AjxCallback(this._controller, this._controller.toggleSpellCheckButton, true) 4120 this._htmlEditor._spellCheckCallback(words); 4121 }; 4122 4123 ZmComposeView.prototype._spellCheckShieldCancelListener = 4124 function(msgDialog, ev) { 4125 this._spellCheckOkay = true; 4126 msgDialog.popdown(); 4127 this._controller.sendMsg(); 4128 }; 4129 4130 ZmComposeView.prototype._spellCheckErrorShield = 4131 function(ex) { 4132 var msgDialog = appCtxt.getYesNoMsgDialog(); 4133 msgDialog.setMessage(ZmMsg.spellCheckFailed); 4134 msgDialog.registerCallback(DwtDialog.YES_BUTTON, this._spellCheckErrorShieldOkListener, this, msgDialog ); 4135 msgDialog.registerCallback(DwtDialog.NO_BUTTON, this._spellCheckErrorShieldCancelListener, this, msgDialog); 4136 msgDialog.associateEnterWithButton(DwtDialog.NO_BUTTON); 4137 msgDialog.popup(null, DwtDialog.NO_BUTTON); 4138 4139 return true; 4140 }; 4141 4142 ZmComposeView.prototype._spellCheckErrorShieldOkListener = 4143 function(msgDialog, ev) { 4144 4145 this._controller._toolbar.enableAll(true); 4146 this._controller.toggleSpellCheckButton(false); 4147 this._htmlEditor.discardMisspelledWords(); 4148 msgDialog.popdown(); 4149 4150 this._spellCheckOkay = true; 4151 this._controller.sendMsg(); 4152 4153 }; 4154 4155 ZmComposeView.prototype._spellCheckErrorShieldCancelListener = 4156 function(msgDialog, ev) { 4157 this._controller._toolbar.enableAll(true); 4158 this._controller.toggleSpellCheckButton(false); 4159 this._htmlEditor.discardMisspelledWords(); 4160 msgDialog.popdown(); 4161 }; 4162 4163 ZmComposeView.prototype._setFormValue = 4164 function() { 4165 this._origFormValue = this._formValue(true, true); 4166 }; 4167 4168 ZmComposeView.prototype._clearFormValue = function() { 4169 4170 DBG.println('draft', 'ZmComposeView._clearFormValue for ' + this._view); 4171 this._origFormValue = ""; 4172 this._isDirty = false; 4173 if (this._htmlEditor) { 4174 this._htmlEditor.clearDirty(); 4175 } 4176 }; 4177 4178 ZmComposeView.prototype._focusHtmlEditor = 4179 function() { 4180 this._htmlEditor.focus(); 4181 }; 4182 4183 4184 // Static methods 4185 4186 // Update tab text when content of Subject field changes 4187 ZmComposeView._onKeyUp = function(ev) { 4188 4189 var cv = ZmComposeView._getComposeViewFromEvent(ev); 4190 if (cv) { 4191 cv.updateTabTitle(); 4192 } 4193 4194 return true; 4195 }; 4196 4197 // Subject field has gotten focus 4198 ZmComposeView._onFocus = function(ev) { 4199 4200 var cv = ZmComposeView._getComposeViewFromEvent(ev); 4201 if (cv) { 4202 appCtxt.getKeyboardMgr().updateFocus(cv._subjectField); 4203 } 4204 }; 4205 4206 ZmComposeView._getComposeViewFromEvent = function(ev) { 4207 4208 ev = DwtUiEvent.getEvent(ev); 4209 var element = DwtUiEvent.getTargetWithProp(ev, "id"); 4210 return element && DwtControl.fromElementId(element._composeViewId); 4211 }; 4212 4213 // for com.zimbra.dnd zimlet 4214 ZmComposeView.prototype.uploadFiles = 4215 function() { 4216 var attachDialog = appCtxt.getAttachDialog(); 4217 var callback = new AjxCallback(this, this._attsDoneCallback, [true]); 4218 attachDialog.setUploadCallback(callback); 4219 attachDialog.upload(callback, document.getElementById("zdnd_form")); 4220 }; 4221 4222 ZmComposeView.prototype.deactivate = 4223 function() { 4224 this._controller.inactive = true; 4225 }; 4226 4227 ZmComposeView.prototype._getIframeDoc = 4228 function() { 4229 return this._htmlEditor && this._htmlEditor._getIframeDoc(); 4230 }; 4231 4232 /** 4233 * Moves the cursor to the beginning of the editor. 4234 * 4235 * @param {number} delay timer delay in ms 4236 * @param {number} offset number of characters to skip ahead when placing cursor 4237 * 4238 * @private 4239 */ 4240 ZmComposeView.prototype._moveCaretOnTimer = 4241 function(offset, delay) { 4242 4243 delay = (delay !== null) ? delay : 200; 4244 var len = this._getEditorContent().length; 4245 AjxTimedAction.scheduleAction(new AjxTimedAction(this, function() { 4246 if (this._getEditorContent().length === len) { 4247 this.getHtmlEditor().moveCaretToTop(offset); 4248 } 4249 }), delay); 4250 }; 4251 4252 4253 /** 4254 * @overview 4255 * This class is used to manage the creation of a composed message, without a UI. For example, 4256 * it an be used to reply to a msg with some canned or user-provided text. 4257 * 4258 * @param controller 4259 * @param composeMode 4260 */ 4261 ZmHiddenComposeView = function(controller, composeMode) { 4262 // no need to invoke parent ctor since we don't need to create a UI 4263 this._controller = controller; 4264 this._composeMode = composeMode; 4265 this.reset(); 4266 }; 4267 4268 ZmHiddenComposeView.prototype = new ZmComposeView; 4269 ZmHiddenComposeView.prototype.constructor = ZmHiddenComposeView; 4270 4271 ZmHiddenComposeView.prototype.isZmHiddenComposeView = true; 4272 ZmHiddenComposeView.prototype.toString = function() { return "ZmHiddenComposeView"; }; 4273 4274 /** 4275 * Sets the current view, based on the given action. The compose form is 4276 * created and laid out and everything is set up for interaction with the user. 4277 * 4278 * @param {Hash} params a hash of parameters: 4279 * @param {constant} action new message, reply, or forward 4280 * @param {ZmMailMsg} msg the original message (reply/forward), or address (new message) 4281 * @param {ZmIdentity} identity identity of sender 4282 * @param {String} toOverride To: addresses (optional) 4283 * @param {String} ccOverride Cc: addresses (optional) 4284 * @param {String} subjectOverride subject for new msg (optional) 4285 * @param {String} extraBodyText text for new msg 4286 * @param {String} accountName on-behalf-of From address 4287 */ 4288 ZmHiddenComposeView.prototype.set = 4289 function(params) { 4290 4291 this.reset(); 4292 4293 var action = this._action = params.action; 4294 var msg = this._msg = this._origMsg = params.msg; 4295 4296 if (!ZmComposeController.IS_FORWARD[action]) { 4297 this._setAddresses(action, AjxEmailAddress.TO, params.toOverride); 4298 if (params.ccOverride) { 4299 this._setAddresses(action, AjxEmailAddress.CC, params.ccOverride); 4300 } 4301 if (params.bccOverride) { 4302 this._setAddresses(action, AjxEmailAddress.BCC, params.bccOverride); 4303 } 4304 } 4305 this._setSubject(action, msg, params.subjectOverride); 4306 this._setBody(action, msg, params.extraBodyText, true); 4307 var oboMsg = msg || (params.selectedMessages && params.selectedMessages.length && params.selectedMessages[0]); 4308 var obo = this._getObo(params.accountName, oboMsg); 4309 4310 if (action !== ZmOperation.FORWARD_ATT) { 4311 this._saveExtraMimeParts(); 4312 } 4313 }; 4314 4315 ZmHiddenComposeView.prototype.setComposeMode = 4316 function(composeMode) { 4317 this._composeMode = composeMode; 4318 }; 4319 4320 // no-op anything that relies on UI components 4321 ZmHiddenComposeView.prototype.applySignature = function() {}; 4322 ZmHiddenComposeView.prototype.enableInputs = function() {}; 4323 ZmHiddenComposeView.prototype._showForwardField = function() {}; 4324 ZmHiddenComposeView.prototype.cleanupAttachments = function() {}; 4325 ZmHiddenComposeView.prototype.resetBody = function() {}; 4326 ZmHiddenComposeView.prototype._resetBodySize = function() {}; 4327 4328 /** 4329 * Returns a msg created from prior input. 4330 */ 4331 ZmHiddenComposeView.prototype.getMsg = 4332 function() { 4333 4334 var addrs = this._recipients.collectAddrs(); 4335 var subject = this._subject; 4336 4337 // Create Msg Object - use dummy if provided 4338 var msg = new ZmMailMsg(); 4339 msg.setSubject(subject); 4340 4341 this.sendUID = (new Date()).getTime(); 4342 4343 // build MIME 4344 var top = this._getTopPart(msg, false, this._bodyContent[this._composeMode]); 4345 4346 msg.setTopPart(top); 4347 msg.setSubject(subject); 4348 4349 //vcard signature may be set 4350 if (this._msg && this._msg._contactAttIds) { 4351 msg.setContactAttIds(this._msg._contactAttIds); 4352 this._msg.setContactAttIds([]); 4353 } 4354 4355 for (var i = 0; i < ZmMailMsg.COMPOSE_ADDRS.length; i++) { 4356 var type = ZmMailMsg.COMPOSE_ADDRS[i]; 4357 var a = addrs[type]; 4358 if (a && a.length) { 4359 msg.setAddresses(type, AjxVector.fromArray(a)); 4360 } 4361 } 4362 msg.identity = this.getIdentity(); 4363 msg.sendUID = this.sendUID; 4364 4365 // save a reference to the original message 4366 msg._origMsg = this._msg; 4367 if (this._msg && this._msg._instanceDate) { 4368 msg._instanceDate = this._msg._instanceDate; 4369 } 4370 4371 this._setMessageFlags(msg); 4372 4373 return msg; 4374 }; 4375 4376 ZmHiddenComposeView.prototype.reset = 4377 function(bEnableInputs) { 4378 4379 this.sendUID = null; 4380 this._recipients = new ZmHiddenRecipients(); 4381 this._subject = ""; 4382 this._controller._curIncOptions = null; 4383 this._msgAttId = null; 4384 this._addresses = {}; 4385 this._bodyContent = {}; 4386 this._components = {}; 4387 this._quotedTextChanged = false; 4388 4389 // remove extra mime parts 4390 this._extraParts = null; 4391 }; 4392 4393 ZmHiddenComposeView.prototype.getIdentity = 4394 function() { 4395 //get the same identity we would have gotten as the selected one in full compose view persona select. 4396 return this._controller._getIdentity(this._msg); 4397 }; 4398 4399 ZmHiddenComposeView.prototype.__initCtrl = function() {}; 4400 4401 /** 4402 * Minimal version of ZmRecipients that has no UI. Note that addresses are stored in 4403 * arrays rather than vectors. 4404 */ 4405 ZmHiddenRecipients = function() { 4406 this._addresses = {}; 4407 }; 4408 4409 ZmHiddenRecipients.prototype.setAddress = 4410 function(type, addr) { 4411 if (type && addr) { 4412 this._addresses[type] = this._addresses[type] || []; 4413 this._addresses[type].push(addr); 4414 } 4415 }; 4416 4417 ZmHiddenRecipients.prototype.addAddresses = 4418 function(type, addrVec, used) { 4419 4420 var addrAdded = false; 4421 used = used || {}; 4422 var addrs = AjxUtil.toArray(addrVec); 4423 if (addrs && addrs.length) { 4424 if (!this._addresses[type]) { 4425 this._addresses[type] = []; 4426 } 4427 for (var i = 0, len = addrs.length; i < len; i++) { 4428 var addr = addrs[i]; 4429 addr = addr.isAjxEmailAddress ? addr : AjxEmailAddress.parse(addr); 4430 if (addr) { 4431 var email = addr.getAddress(); 4432 if (!email) { continue; } 4433 email = email.toLowerCase(); 4434 if (!used[email]) { 4435 this._addresses[type].push(addr); 4436 used[email] = true; 4437 addrAdded = true; 4438 } 4439 } 4440 } 4441 } 4442 return addrAdded; 4443 }; 4444 4445 ZmHiddenRecipients.prototype.collectAddrs = 4446 function() { 4447 return this._addresses; 4448 }; 4449