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 controller to manage message composition. 26 * @constructor 27 * @class 28 * This class manages message composition. 29 * 30 * @author Conrad Damon 31 * 32 * @param {DwtShell} container the containing shell 33 * @param {ZmApp} mailApp the containing app 34 * @param {constant} type controller type 35 * @param {string} sessionId the session id 36 * 37 * @extends ZmController 38 */ 39 ZmComposeController = function(container, mailApp, type, sessionId) { 40 41 ZmController.apply(this, arguments); 42 43 this._action = null; 44 45 ZmComposeController._setStatics(); 46 47 this._listeners = {}; 48 this._listeners[ZmOperation.SEND] = this._sendListener.bind(this); 49 this._listeners[ZmOperation.SEND_MENU] = this._sendListener.bind(this); 50 this._listeners[ZmOperation.SEND_LATER] = this._sendLaterListener.bind(this); 51 this._listeners[ZmOperation.CANCEL] = this._cancelListener.bind(this); 52 this._listeners[ZmOperation.ATTACHMENT] = this._attachmentListener.bind(this); 53 this._listeners[ZmOperation.DETACH_COMPOSE] = this._detachListener.bind(this); 54 this._listeners[ZmOperation.SAVE_DRAFT] = this._saveDraftListener.bind(this); 55 this._listeners[ZmOperation.SPELL_CHECK] = this._spellCheckListener.bind(this); 56 this._listeners[ZmOperation.COMPOSE_OPTIONS] = this._optionsListener.bind(this); 57 58 this._dialogPopdownListener = this._dialogPopdownActionListener.bind(this); 59 60 this._autoSaveTimer = null; 61 this._draftType = ZmComposeController.DRAFT_TYPE_NONE; 62 this._elementsToHide = ZmAppViewMgr.LEFT_NAV; 63 }; 64 65 ZmComposeController.prototype = new ZmController(); 66 ZmComposeController.prototype.constructor = ZmComposeController; 67 68 ZmComposeController.prototype.isZmComposeController = true; 69 ZmComposeController.prototype.toString = function() { return "ZmComposeController"; }; 70 71 // 72 // Constants 73 // 74 75 ZmComposeController.SIGNATURE_KEY = "sigKeyId"; 76 77 // Constants for defining the reason for saving a draft message. 78 /** 79 * Defines the "none" draft type reason. 80 */ 81 ZmComposeController.DRAFT_TYPE_NONE = "none"; 82 /** 83 * Defines the "manual" draft type reason. 84 */ 85 ZmComposeController.DRAFT_TYPE_MANUAL = "manual"; 86 /** 87 * Defines the "auto" draft type reason. 88 */ 89 ZmComposeController.DRAFT_TYPE_AUTO = "auto"; 90 /** 91 * Defines the "delaysend" draft type reason. 92 */ 93 ZmComposeController.DRAFT_TYPE_DELAYSEND = "delaysend"; 94 95 ZmComposeController.DEFAULT_TAB_TEXT = ZmMsg.compose; 96 97 ZmComposeController.NEW_WINDOW_WIDTH = 975; 98 ZmComposeController.NEW_WINDOW_HEIGHT = 475; 99 100 // Message dialogs 101 ZmComposeController.MSG_DIALOG_1 = 1; // OK 102 ZmComposeController.MSG_DIALOG_2 = 2; // OK Cancel 103 104 ZmComposeController._setStatics = 105 function() { 106 107 if (ZmComposeController.RADIO_GROUP) { 108 return; 109 } 110 111 // radio groups for options items 112 ZmComposeController.RADIO_GROUP = {}; 113 ZmComposeController.RADIO_GROUP[ZmOperation.REPLY] = 1; 114 ZmComposeController.RADIO_GROUP[ZmOperation.REPLY_ALL] = 1; 115 ZmComposeController.RADIO_GROUP[ZmOperation.CAL_REPLY] = 1; 116 ZmComposeController.RADIO_GROUP[ZmOperation.CAL_REPLY_ALL] = 1; 117 ZmComposeController.RADIO_GROUP[ZmOperation.FORMAT_HTML] = 2; 118 ZmComposeController.RADIO_GROUP[ZmOperation.FORMAT_TEXT] = 2; 119 ZmComposeController.RADIO_GROUP[ZmOperation.INC_ATTACHMENT] = 3; 120 ZmComposeController.RADIO_GROUP[ZmOperation.INC_BODY] = 3; 121 ZmComposeController.RADIO_GROUP[ZmOperation.INC_NONE] = 3; 122 ZmComposeController.RADIO_GROUP[ZmOperation.INC_SMART] = 3; 123 124 // translate between include settings and operations 125 ZmComposeController.INC_OP = {}; 126 ZmComposeController.INC_OP[ZmSetting.INC_ATTACH] = ZmOperation.INC_ATTACHMENT; 127 ZmComposeController.INC_OP[ZmSetting.INC_BODY] = ZmOperation.INC_BODY; 128 ZmComposeController.INC_OP[ZmSetting.INC_NONE] = ZmOperation.INC_NONE; 129 ZmComposeController.INC_OP[ZmSetting.INC_SMART] = ZmOperation.INC_SMART; 130 ZmComposeController.INC_MAP = {}; 131 for (var i in ZmComposeController.INC_OP) { 132 ZmComposeController.INC_MAP[ZmComposeController.INC_OP[i]] = i; 133 } 134 135 ZmComposeController.OPTIONS_TT = {}; 136 ZmComposeController.OPTIONS_TT[ZmOperation.NEW_MESSAGE] = "composeOptions"; 137 ZmComposeController.OPTIONS_TT[ZmOperation.REPLY] = "replyOptions"; 138 ZmComposeController.OPTIONS_TT[ZmOperation.REPLY_ALL] = "replyOptions"; 139 ZmComposeController.OPTIONS_TT[ZmOperation.CAL_REPLY] = "replyOptions"; 140 ZmComposeController.OPTIONS_TT[ZmOperation.CAL_REPLY_ALL] = "replyOptions"; 141 ZmComposeController.OPTIONS_TT[ZmOperation.FORWARD_ATT] = "forwardOptions"; 142 ZmComposeController.OPTIONS_TT[ZmOperation.FORWARD_INLINE] = "forwardOptions"; 143 144 ZmComposeController.OP_CHECK = {}; 145 ZmComposeController.OP_CHECK[ZmOperation.SHOW_BCC] = true; 146 ZmComposeController.OP_CHECK[ZmOperation.REQUEST_READ_RECEIPT] = true; 147 ZmComposeController.OP_CHECK[ZmOperation.USE_PREFIX] = true; 148 ZmComposeController.OP_CHECK[ZmOperation.INCLUDE_HEADERS] = true; 149 150 // Classification hashes for a compose action 151 ZmComposeController.IS_INVITE_REPLY = {}; 152 ZmComposeController.IS_INVITE_REPLY[ZmOperation.REPLY_ACCEPT] = true; 153 ZmComposeController.IS_INVITE_REPLY[ZmOperation.REPLY_CANCEL] = true; 154 ZmComposeController.IS_INVITE_REPLY[ZmOperation.REPLY_DECLINE] = true; 155 ZmComposeController.IS_INVITE_REPLY[ZmOperation.REPLY_TENTATIVE] = true; 156 ZmComposeController.IS_INVITE_REPLY[ZmOperation.REPLY_MODIFY] = true; 157 ZmComposeController.IS_INVITE_REPLY[ZmOperation.REPLY_NEW_TIME] = true; 158 159 ZmComposeController.IS_CAL_REPLY = AjxUtil.hashCopy(ZmComposeController.IS_INVITE_REPLY); 160 ZmComposeController.IS_CAL_REPLY[ZmOperation.CAL_REPLY] = true; 161 ZmComposeController.IS_CAL_REPLY[ZmOperation.CAL_REPLY_ALL] = true; 162 163 ZmComposeController.IS_REPLY = AjxUtil.hashCopy(ZmComposeController.IS_CAL_REPLY); 164 ZmComposeController.IS_REPLY[ZmOperation.REPLY] = true; 165 ZmComposeController.IS_REPLY[ZmOperation.REPLY_ALL] = true; 166 167 ZmComposeController.IS_FORWARD = {}; 168 ZmComposeController.IS_FORWARD[ZmOperation.FORWARD_INLINE] = true; 169 ZmComposeController.IS_FORWARD[ZmOperation.FORWARD_ATT] = true; 170 171 ZmComposeController.PRIORITY_FLAG_TO_OP = {}; 172 ZmComposeController.PRIORITY_FLAG_TO_OP[ZmItem.FLAG_LOW_PRIORITY] = ZmOperation.PRIORITY_LOW; 173 ZmComposeController.PRIORITY_FLAG_TO_OP[ZmItem.FLAG_HIGH_PRIORITY] = ZmOperation.PRIORITY_HIGH; 174 }; 175 176 // 177 // Public methods 178 // 179 180 ZmComposeController.getDefaultViewType = 181 function() { 182 return ZmId.VIEW_COMPOSE; 183 }; 184 ZmComposeController.prototype.getDefaultViewType = ZmComposeController.getDefaultViewType; 185 186 /** 187 * Called by ZmNewWindow.unload to remove ZmSettings listeners (which reside in 188 * the parent window). Otherwise, after the child window is closed, the parent 189 * window is still referencing the child window's compose controller, which has 190 * been unloaded!! 191 * 192 * @private 193 */ 194 ZmComposeController.prototype.dispose = 195 function() { 196 var settings = appCtxt.getSettings(); 197 if (ZmComposeController.SETTINGS) { //no SETTINGS in child window 198 for (var i = 0; i < ZmComposeController.SETTINGS.length; i++) { 199 settings.getSetting(ZmComposeController.SETTINGS[i]).removeChangeListener(this._settingChangeListener); 200 } 201 } 202 this._composeView.dispose(); 203 204 var app = this.getApp(); 205 app.disposeTreeControllers(); 206 appCtxt.notifyZimlets("onDisposeComposeController", [this]); 207 208 }; 209 210 /** 211 * Begins a compose session by presenting a form to the user. 212 * 213 * @param {Hash} params a hash of parameters: 214 * @param {constant} action the new message, reply, forward, or an invite action 215 * @param {Boolean} inNewWindow if <code>true</code>, we are in detached window 216 * @param {ZmMailMsg} msg the original message (reply/forward), or address (new message) 217 * @param {String} toOverride the initial value for To: field 218 * @param {String} ccOverride Cc: addresses (optional) 219 * @param {String} subjOverride the initial value for Subject: field 220 * @param {String} extraBodyText the canned text to prepend to body (invites) 221 * @param {AjxCallback} callback the callback to run after view has been set 222 * @param {String} accountName the on-behalf-of From address 223 * @param {String} accountName on-behalf-of From address 224 * @param {boolean} hideView if true, don't show compose view 225 */ 226 ZmComposeController.prototype.doAction = 227 function(params) { 228 229 params = params || {}; 230 var ac = window.parentAppCtxt || window.appCtxt; 231 232 // in zdesktop, it's possible there are no accounts that support smtp 233 if (ac.isOffline && !ac.get(ZmSetting.OFFLINE_COMPOSE_ENABLED)) { 234 this._showMsgDialog(ZmComposeController.MSG_DIALOG_1, ZmMsg.composeDisabled, DwtMessageDialog.CRITICAL_STYLE); 235 return; 236 } 237 238 params.action = params.action || ZmOperation.NEW_MESSAGE; 239 params.inNewWindow = !appCtxt.isWebClientOffline() && !this.isHidden && (params.inNewWindow || this._app._inNewWindow(params.ev)); 240 this._msgSent = false; 241 if (params.inNewWindow) { 242 var msgId = (params.msg && params.msg.nId) || Dwt.getNextId(); 243 var newWinObj = ac.getNewWindow(false, ZmComposeController.NEW_WINDOW_WIDTH, ZmComposeController.NEW_WINDOW_HEIGHT, ZmId.VIEW_COMPOSE + "_" + msgId.replace(/\s|\-/g, '_')); 244 if (newWinObj) { 245 // this is how child window knows what to do once loading: 246 newWinObj.command = "compose"; 247 newWinObj.params = params; 248 if (newWinObj.win) { 249 newWinObj.win.focus(); 250 } 251 } 252 } else { 253 this._setView(params); 254 this._listController = params.listController; 255 } 256 }; 257 258 /** 259 * Toggles the spell check button. 260 * 261 * @param {Boolean} selected if <code>true</code>, toggle the spell check to "selected" 262 * 263 */ 264 ZmComposeController.prototype.toggleSpellCheckButton = 265 function(selected) { 266 var spellCheckButton = this._toolbar.getButton(ZmOperation.SPELL_CHECK); 267 if (spellCheckButton) { 268 spellCheckButton.setSelected((selected || false)); 269 spellCheckButton.setAttribute('aria-pressed', selected); 270 } 271 }; 272 273 /** 274 * Detaches compose view to child window. 275 * 276 */ 277 ZmComposeController.prototype.detach = 278 function() { 279 // bug fix #7192 - disable detach toolbar button 280 this._toolbar.enable(ZmOperation.DETACH_COMPOSE, false); 281 282 var view = this._composeView; 283 var msg = this._msg || view._origMsg || view._msg; 284 var subj = view._subjectField.value; 285 var msgAttId = view._msgAttId; //include original as attachment 286 var body = this._getBodyContent(); 287 var composeMode = view.getComposeMode(); 288 var backupForm = view.backupForm; 289 var sendUID = view.sendUID; 290 var action = view._action || this._action; 291 var identity = view.getIdentity(); 292 var requestReadReceipt = this.isRequestReadReceipt(); 293 var selectedIdentityIndex = view.identitySelect && view.identitySelect.getSelectedIndex(); 294 295 var addrList = {}; 296 for (var i = 0; i < ZmMailMsg.COMPOSE_ADDRS.length; i++) { 297 var type = ZmMailMsg.COMPOSE_ADDRS[i]; 298 addrList[type] = view.getAddrInputField(type).getAddresses(true); 299 } 300 301 var partToAttachmentMap = AjxUtil.map(view._partToAttachmentMap, function(member) { 302 return AjxUtil.hashCopy(member); 303 }); 304 305 // this is how child window knows what to do once loading: 306 var msgId = (msg && msg.nId) || Dwt.getNextId(); 307 var newWinObj = appCtxt.getNewWindow(false, ZmComposeController.NEW_WINDOW_WIDTH, ZmComposeController.NEW_WINDOW_HEIGHT, ZmId.VIEW_COMPOSE + "_" + msgId.replace(/\s|\-/g, '_')); 308 if (newWinObj && newWinObj.win) { 309 newWinObj.win.focus(); 310 } 311 newWinObj.command = "composeDetach"; 312 newWinObj.params = { 313 action: action, 314 msg: msg, 315 addrs: addrList, 316 subj: subj, 317 priority: this._getPriority(), 318 attHtml: view._attcDiv.innerHTML, 319 msgAttId: msgAttId, 320 msgIds: msg && msg.isDraft ? null : this._msgIds, 321 draftType: this._draftType, 322 draftMsg: this._draftMsg, 323 body: body, 324 composeMode: composeMode, 325 identityId: selectedIdentityIndex, 326 accountName: this._accountName, 327 backupForm: backupForm, 328 sendUID: sendUID, 329 sessionId: this.getSessionId(), 330 readReceipt: requestReadReceipt, 331 sigId: this.getSelectedSignature(), 332 incOptions: this._curIncOptions, 333 partMap: partToAttachmentMap, 334 origMsgAtt: view._origMsgAtt ? AjxUtil.hashCopy(view._origMsgAtt) : null, 335 origAction: this._origAction 336 }; 337 }; 338 339 ZmComposeController.prototype.popShield = 340 function() { 341 var dirty = this._composeView.isDirty(true, true); 342 if (!dirty && (this._draftType != ZmComposeController.DRAFT_TYPE_AUTO)) { 343 return true; 344 } 345 346 var ps = this._popShield = appCtxt.getYesNoCancelMsgDialog(); 347 if (this._draftType == ZmComposeController.DRAFT_TYPE_AUTO) { 348 // Message has been saved, but never explicitly by the user. 349 // Ask if he wants to keep the autosaved draft. 350 ps.reset(); 351 ps.setMessage(ZmMsg.askSaveAutosavedDraft, DwtMessageDialog.WARNING_STYLE); 352 if (dirty) { 353 ps.registerCallback(DwtDialog.YES_BUTTON, this._popShieldYesCallback, this); 354 } else { 355 ps.registerCallback(DwtDialog.YES_BUTTON, this._popShieldNoCallback, this); 356 } 357 ps.registerCallback(DwtDialog.NO_BUTTON, this._popShieldDiscardCallback, this); 358 ps.registerCallback(DwtDialog.CANCEL_BUTTON, this._popShieldDismissCallback, this); 359 } else if (this._canSaveDraft()) { 360 ps.reset(); 361 ps.setMessage(ZmMsg.askSaveDraft, DwtMessageDialog.WARNING_STYLE); 362 ps.registerCallback(DwtDialog.YES_BUTTON, this._popShieldYesCallback, this); 363 ps.registerCallback(DwtDialog.NO_BUTTON, this._popShieldNoCallback, this); 364 ps.registerCallback(DwtDialog.CANCEL_BUTTON, this._popShieldDismissCallback, this); 365 } else { 366 ps = this._popShield = appCtxt.getYesNoMsgDialog(); 367 ps.setMessage(ZmMsg.askLeaveCompose, DwtMessageDialog.WARNING_STYLE); 368 ps.registerCallback(DwtDialog.YES_BUTTON, this._popShieldYesCallback, this); 369 ps.registerCallback(DwtDialog.NO_BUTTON, this._popShieldDismissCallback, this); 370 } 371 ps.addPopdownListener(this._dialogPopdownListener); 372 ps.popup(); 373 374 return false; 375 }; 376 377 // We don't call ZmController._preHideCallback here because it saves the current 378 // focus member, and we want to start over each time 379 ZmComposeController.prototype._preHideCallback = function(view, force) { 380 381 DBG.println('draft', 'ZmComposeController._preHideCallback for ' + view + ': force = ' + force + ', _dontSavePreHide = ' + this._dontSavePreHide); 382 if (this._autoSaveTimer) { 383 //the following is a bit suspicious to me. I assume maybe this method might be called with force == true 384 //in a way that is not after the popShield was activated? That would be the only explanation to have this. 385 //I wonder if that's the case that leaves orphan drafts 386 if (force) { 387 // auto-save if we leave this compose tab and the message has not yet been sent 388 // this is a refactoring/fix of code initially from bug 72106 (since it's confusing I mention this bug to keep this knowledge) 389 if (this._dontSavePreHide) { 390 this._dontSavePreHide = false; 391 } 392 else { 393 this._autoSaveCallback(true); 394 } 395 } 396 } 397 398 return force ? true : this.popShield(); 399 }; 400 401 ZmComposeController.prototype._preUnloadCallback = 402 function(view) { 403 return !this._composeView.isDirty(true, true); 404 }; 405 406 407 ZmComposeController.prototype._preShowCallback = function() { 408 409 this._composeView.enableInputs(true); 410 411 return true; 412 }; 413 414 ZmComposeController.prototype._postShowCallback = 415 function() { 416 // always reset auto save every time this view is shown. This covers the 417 // case where a compose tab is inactive and becomes active when user clicks 418 // on compose tab. 419 this._initAutoSave(); 420 421 if (!appCtxt.isChildWindow) { 422 // no need to "restore" focus between windows 423 ZmController.prototype._postShowCallback.call(this); 424 } 425 var view = this._composeView; 426 var composeMode = view.getComposeMode(); 427 if (this._action != ZmOperation.NEW_MESSAGE && 428 this._action != ZmOperation.FORWARD_INLINE && 429 this._action != ZmOperation.FORWARD_ATT) 430 { 431 if (composeMode == Dwt.HTML) { 432 setTimeout(view._focusHtmlEditor.bind(view), 100); 433 } 434 this._composeView._setBodyFieldCursor(); 435 } 436 }; 437 438 ZmComposeController.prototype._postHideCallback = function() { 439 440 DBG.println('draft', 'ZmComposeController._postHideCallback for ' + this._currentViewId); 441 if (this._autoSaveTimer) { 442 this._autoSaveTimer.kill(); 443 } 444 445 // hack to kill the child window when replying to an invite 446 if (appCtxt.isChildWindow && ZmComposeController.IS_INVITE_REPLY[this._action]) { 447 window.close(); 448 } 449 }; 450 451 /** 452 * This method gets called if user clicks on mailto link while compose view is 453 * already being used. 454 * 455 * @private 456 */ 457 ZmComposeController.prototype.resetComposeForMailto = 458 function(params) { 459 if (this._popShield && this._popShield.isPoppedUp()) { 460 return false; 461 } 462 463 var ps = this._popShield = appCtxt.getYesNoCancelMsgDialog(); 464 ps.reset(); 465 ps.setMessage(ZmMsg.askSaveDraft, DwtMessageDialog.WARNING_STYLE); 466 ps.registerCallback(DwtDialog.YES_BUTTON, this._popShieldYesCallback, this, params); 467 ps.registerCallback(DwtDialog.NO_BUTTON, this._popShieldNoCallback, this, params); 468 ps.registerCallback(DwtDialog.CANCEL_BUTTON, this._popShieldDismissCallback, this); 469 ps.addPopdownListener(this._dialogPopdownListener); 470 ps.popup(); 471 472 return true; 473 }; 474 475 /** 476 * Sends the message represented by the content of the compose view. 477 * 478 * @param {String} attId the id 479 * @param {constant} draftType the draft type (see <code>ZmComposeController.DRAFT_TYPE_</code> constants) 480 * @param {AjxCallback} callback the callback 481 * @param {Boolean} processImages remove webkit-fake-url images and upload data uri images 482 */ 483 ZmComposeController.prototype.sendMsg = 484 function(attId, draftType, callback, contactId, processImages) { 485 486 if (processImages !== false && this._composeView) { 487 //Dont use bind as its arguments cannot be modified before its execution. 488 var processImagesCallback = new AjxCallback(this, this._sendMsg, [attId, null, draftType, callback, contactId]); 489 var result = this._processImages(processImagesCallback); 490 if (result) { 491 return; 492 } 493 } 494 return this._sendMsg(attId, null, draftType, callback, contactId); 495 }; 496 497 /** 498 * Sends the message represented by the content of the compose view with specified docIds as attachment. 499 * 500 * @param {Array} docIds the document Ids 501 * @param {constant} draftType the draft type (see <code>ZmComposeController.DRAFT_TYPE_</code> constants) 502 * @param {AjxCallback} callback the callback 503 */ 504 ZmComposeController.prototype.sendDocs = 505 function(docIds, draftType, callback, contactId) { 506 return this._sendMsg(null, docIds, draftType, callback, contactId); 507 }; 508 509 /** 510 * Sends the message represented by the content of the compose view. 511 * 512 * @private 513 */ 514 ZmComposeController.prototype._sendMsg = 515 function(attId, docIds, draftType, callback, contactId) { 516 517 var isTimed = Boolean(this._sendTime); 518 draftType = draftType || (isTimed ? ZmComposeController.DRAFT_TYPE_DELAYSEND : ZmComposeController.DRAFT_TYPE_NONE); 519 var isDraft = draftType != ZmComposeController.DRAFT_TYPE_NONE; 520 var isAutoSave = draftType == ZmComposeController.DRAFT_TYPE_AUTO; 521 // bug fix #38408 - briefcase attachments need to be set *before* calling 522 // getMsg() but we cannot do that without having a ZmMailMsg to store it in. 523 // File this one under WTF. 524 var tempMsg; 525 if (docIds) { 526 tempMsg = new ZmMailMsg(); 527 this._composeView.setDocAttachments(tempMsg, docIds); 528 } 529 var msg = this._composeView.getMsg(attId, isDraft, tempMsg, isTimed, contactId); 530 531 if (!msg) { 532 return; 533 } 534 535 if (this._autoSaveTimer) { 536 //If this._autoSaveTimer._timer is null then this._autoSaveTimer.kill(); is already called. 537 //Due to browsers cleartimeout taking some time, we are again checking this._autoSaveTimer._timer to prevent unnecessary autosave call 538 //Bug:74148 539 if (isAutoSave && isDraft && !this._autoSaveTimer._timer) { 540 return; 541 } 542 //kill the timer, no save is attempted while message is pending 543 this._autoSaveTimer.kill(); 544 } 545 546 var origMsg = msg._origMsg; 547 var isCancel = (msg.inviteMode == ZmOperation.REPLY_CANCEL); 548 var isModify = (msg.inviteMode == ZmOperation.REPLY_MODIFY); 549 550 if (isCancel || isModify) { 551 var appt = origMsg._appt; 552 var respCallback = this._handleResponseCancelOrModifyAppt.bind(this); 553 if (isCancel) { 554 appt.cancel(origMsg._mode, msg, respCallback); 555 } else { 556 appt.save(); 557 } 558 return; 559 } 560 561 var ac = window.parentAppCtxt || window.appCtxt; 562 var acctName = appCtxt.multiAccounts 563 ? this._composeView.getFromAccount().name : this._accountName; 564 if (msg.delegatedSenderAddr && !msg.delegatedSenderAddrIsDL) { 565 acctName = msg.delegatedSenderAddr; 566 } 567 568 if (isDraft) { 569 if (appCtxt.multiAccounts) { 570 // for offline, save drafts based on account owner of From: dropdown 571 acctName = ac.accountList.getAccount(msg.fromSelectValue.accountId).name; 572 } else { 573 acctName = ac.getActiveAccount().name; 574 } 575 if (msg._origMsg && msg._origMsg.isDraft) { 576 // if shared folder, make sure we save the draft under the owner account name 577 var folder = msg.folderId ? ac.getById(msg.folderId) : null; 578 if (folder && folder.isRemote()) { 579 acctName = folder.getOwner(); 580 } 581 } 582 } else { 583 // if shared folder, make sure we send the email on-behalf-of 584 var folder = msg.folderId ? ac.getById(msg.folderId) : null; 585 if (folder && folder.isRemote() && this._composeView.sendMsgOboIsOK()) { 586 acctName = folder.getOwner(); 587 } 588 } 589 590 if (origMsg) { 591 origMsg.sendAsMe = !this._composeView.sendMsgOboIsOK(); 592 } 593 594 // If this message had been saved from draft and it has a sender (meaning 595 // it's a reply from someone else's account) then get the account name from 596 // the from field. 597 if (!acctName && !isDraft && origMsg && origMsg.isDraft) { 598 if (this._composeView.sendMsgOboIsOK()) { 599 if (origMsg._addrs[ZmMailMsg.HDR_FROM] && 600 origMsg._addrs[ZmMailMsg.HDR_SENDER] && 601 origMsg._addrs[ZmMailMsg.HDR_SENDER].size()) 602 { 603 acctName = origMsg._addrs[ZmMailMsg.HDR_FROM].get(0).address; 604 } 605 } else { 606 origMsg.sendAsMe = true; // hack. 607 } 608 } 609 610 // check for read receipt 611 var requestReadReceipt = !this.isHidden && this.isRequestReadReceipt(); 612 613 var respCallback = this._handleResponseSendMsg.bind(this, draftType, msg, callback); 614 var errorCallback = this._handleErrorSendMsg.bind(this, draftType, msg); 615 msg.send(isDraft, respCallback, errorCallback, acctName, null, requestReadReceipt, null, this._sendTime, isAutoSave); 616 this._resetDelayTime(); 617 }; 618 619 ZmComposeController.prototype._handleResponseSendMsg = 620 function(draftType, msg, callback, result) { 621 var resp = result.getResponse(); 622 // Reset the autosave interval to the default 623 delete(this._autoSaveInterval); 624 // Re-enable autosave 625 if (draftType !== ZmComposeController.DRAFT_TYPE_NONE) { 626 //only re-init autosave if it's a draft, NOT if it's an actual message send. (user clicked "send"). 627 //In order to avoid a potential race condition, and there's no reason to init auto save anyway. 628 this._initAutoSave(); 629 } 630 var needToPop = this._processSendMsg(draftType, msg, resp); 631 632 // this._msg = msg; 633 634 if (callback) { 635 callback.run(result); 636 } 637 638 if(this.sendMsgCallback) { 639 this.sendMsgCallback.run(result); 640 } 641 642 appCtxt.notifyZimlets("onSendMsgSuccess", [this, msg, draftType]);//notify Zimlets on success 643 644 if (needToPop) { 645 this._dontSavePreHide = true; 646 this._app.popView(true, this._currentView); 647 } 648 649 }; 650 651 ZmComposeController.prototype._handleResponseCancelOrModifyAppt = 652 function() { 653 this._app.popView(true); 654 appCtxt.setStatusMsg(ZmMsg.messageSent); 655 }; 656 657 ZmComposeController.prototype._handleErrorSendMsg = function(draftType, msg, ex, params) { 658 659 if (draftType !== ZmComposeController.DRAFT_TYPE_NONE && !AjxUtil.isUndefined(this._wasDirty)) { 660 this._composeView._isDirty = this._wasDirty; 661 delete this._wasDirty; 662 } 663 664 var retVal = false; 665 if (!this.isHidden) { 666 this.resetToolbarOperations(); 667 this._composeView.enableInputs(true); 668 } 669 670 appCtxt.notifyZimlets("onSendMsgFailure", [this, ex, msg]);//notify Zimlets on failure 671 if (ex && ex.code) { 672 673 var errorMsg = null; 674 var showMsg = false; 675 var style = null; 676 if (ex.code === ZmCsfeException.MAIL_SEND_ABORTED_ADDRESS_FAILURE) { 677 var invalid = ex.getData ? ex.getData(ZmCsfeException.MAIL_SEND_ADDRESS_FAILURE_INVALID) : null; 678 var invalidMsg = invalid && invalid.length ? AjxMessageFormat.format(ZmMsg.sendErrorInvalidAddresses, invalid.join(", ")) : null; 679 errorMsg = ZmMsg.sendErrorAbort + "<br/>" + AjxStringUtil.htmlEncode(invalidMsg); 680 this.popupErrorDialog(errorMsg, ex, true, true, false, true); 681 retVal = true; 682 } 683 else if (ex.code === ZmCsfeException.MAIL_SEND_PARTIAL_ADDRESS_FAILURE) { 684 var invalid = ex.getData ? ex.getData(ZmCsfeException.MAIL_SEND_ADDRESS_FAILURE_INVALID) : null; 685 errorMsg = invalid && invalid.length ? AjxMessageFormat.format(ZmMsg.sendErrorPartial, AjxStringUtil.htmlEncode(invalid.join(", "))) : ZmMsg.sendErrorAbort; 686 showMsg = true; 687 } 688 else if (ex.code == AjxException.CANCELED) { 689 if (draftType === ZmComposeController.DRAFT_TYPE_AUTO) { 690 //note - this interval is not really used anymore. Only the idle timer is used. This is only used as a boolean checkbox now. I'm pretty sure. 691 if (!this._autoSaveInterval) { 692 // Request was cancelled due to a ZmRequestMgr send timeout. 693 // The server can either be hung or this particular message is taking 694 // too long to process. Backoff the send interval - restored to 695 // default upon first successful save 696 this._autoSaveInterval = appCtxt.get(ZmSetting.AUTO_SAVE_DRAFT_INTERVAL); 697 } 698 if (this._autoSaveInterval) { 699 // Cap the save attempt interval at 5 minutes 700 this._autoSaveInterval *= 2; 701 if (this._autoSaveInterval > 300) { 702 this._autoSaveInterval = 300; 703 } 704 } 705 } 706 errorMsg = ZmMsg.cancelSendMsgWarning; 707 this._composeView.setBackupForm(); 708 retVal = true; 709 } 710 else if (ex.code === ZmCsfeException.MAIL_QUOTA_EXCEEDED) { 711 errorMsg = ZmMsg.errorQuotaExceeded; 712 } 713 else if (ex.code === ZmCsfeException.MAIL_NO_SUCH_CONTACT) { 714 errorMsg = ZmMsg.vcardContactGone; 715 showMsg = true; 716 } 717 718 if (this._uploadingProgress){ 719 this._initAutoSave(); 720 this._composeView._resetUpload(true); 721 if (ex.code === ZmCsfeException.MAIL_MESSAGE_TOO_BIG) { 722 errorMsg = AjxMessageFormat.format(ZmMsg.attachmentSizeError, AjxUtil.formatSize(appCtxt.get(ZmSetting.MESSAGE_SIZE_LIMIT))); 723 style = DwtMessageDialog.WARNING_STYLE; 724 showMsg = true; 725 } 726 else if (ex.code === ZmCsfeException.MAIL_NO_SUCH_MSG) { 727 // The message was deleted while upload was in progress (likely a discarded draft). Ignore the error. 728 DBG.println(AjxDebug.DBG1, "Message was deleted while uploading a file; ignore the SaveDraft 'No Such Message' error." ); 729 retVal = true; 730 showMsg = false; 731 } else { 732 errorMsg = errorMsg || ZmMsg.attachingFilesError + "<br>" + (ex.msg || ""); 733 showMsg = true; 734 } 735 } 736 737 if (errorMsg && showMsg) { 738 this._showMsgDialog(ZmComposeController.MSG_DIALOG_1, errorMsg, style || DwtMessageDialog.CRITICAL_STYLE, null, true); 739 retVal = true; 740 } 741 } 742 743 // Assume the user stays on the compose view, so we need the timer. 744 // (it was canceled when send was called) 745 this._initAutoSave(); 746 return retVal; 747 }; 748 749 750 /** 751 * Creates a new ZmComposeView if one does not already exist 752 */ 753 ZmComposeController.prototype.initComposeView = 754 function() { 755 756 if (this._composeView) { return; } 757 758 if (!this.isHidden) { 759 this._composeView = new ZmComposeView(this._container, this, this._composeMode, this._action); 760 var callbacks = {}; 761 callbacks[ZmAppViewMgr.CB_PRE_HIDE] = this._preHideCallback.bind(this); 762 callbacks[ZmAppViewMgr.CB_PRE_UNLOAD] = this._preUnloadCallback.bind(this); 763 callbacks[ZmAppViewMgr.CB_POST_SHOW] = this._postShowCallback.bind(this); 764 callbacks[ZmAppViewMgr.CB_PRE_SHOW] = this._preShowCallback.bind(this); 765 callbacks[ZmAppViewMgr.CB_POST_HIDE] = this._postHideCallback.bind(this); 766 this._initializeToolBar(); 767 var elements = this.getViewElements(null, this._composeView, this._toolbar); 768 769 this._app.createView({ viewId: this._currentViewId, 770 viewType: this._currentViewType, 771 elements: elements, 772 hide: this._elementsToHide, 773 controller: this, 774 callbacks: callbacks, 775 tabParams: this._getTabParams()}); 776 777 if (this._composeView.identitySelect) { 778 this._composeView.identitySelect.addChangeListener(this._identityChangeListener.bind(this)); 779 } 780 } 781 else { 782 this._composeView = new ZmHiddenComposeView(this, this._composeMode); 783 } 784 }; 785 786 ZmComposeController.prototype._getTabParams = 787 function() { 788 return {id:this.tabId, image:"CloseGray", hoverImage:"Close", text:ZmComposeController.DEFAULT_TAB_TEXT, textPrecedence:75, 789 tooltip:ZmComposeController.DEFAULT_TAB_TEXT, style: DwtLabel.IMAGE_RIGHT}; 790 }; 791 792 ZmComposeController.prototype.isTransient = 793 function(oldView, newView) { 794 return (appCtxt.getViewTypeFromId(newView) == ZmId.VIEW_MAIL_CONFIRM); 795 }; 796 797 ZmComposeController.prototype._identityChangeListener = 798 function(event) { 799 800 var cv = this._composeView; 801 var signatureId = cv._getSignatureIdForAction(null, this._action); 802 803 // don't do anything if signature is same 804 if (signatureId == this._currentSignatureId) { return; } 805 806 var okCallback = this._switchIdentityOkCallback.bind(this); 807 var cancelCallback = this._switchIdentityCancelCallback.bind(this, cv.identitySelect.getValue()); 808 if (!this._warnUserAboutChanges(ZmId.OP_ADD_SIGNATURE, okCallback, cancelCallback)) { 809 this._switchIdentityOkCallback(); 810 } 811 }; 812 813 ZmComposeController.prototype._switchIdentityOkCallback = 814 function() { 815 if(this._currentDlg) { 816 this._currentDlg.popdown(); 817 } 818 this._switchIdentity(); 819 }; 820 821 ZmComposeController.prototype._switchIdentityCancelCallback = 822 function(identityId) { 823 this._currentDlg.popdown(); 824 this._composeView.identitySelect.setSelectedValue(this._currentIdentityId); 825 }; 826 827 ZmComposeController.prototype._switchIdentity = 828 function() { 829 var identity = this._composeView.getIdentity(); 830 var sigId = this._composeView._getSignatureIdForAction(identity); 831 this.setSelectedSignature(sigId); 832 var params = { 833 action: this._action, 834 msg: this._msg, 835 extraBodyText: this._composeView.getUserText(), 836 keepAttachments: true, 837 op: ZmId.OP_ADD_SIGNATURE 838 }; 839 this._composeView.resetBody(params); 840 this._setAddSignatureVisibility(); 841 if (identity) { 842 this.resetIdentity(identity.id); 843 } 844 this.resetSignature(sigId); 845 }; 846 847 ZmComposeController.prototype._handleSelectSignature = 848 function(ev) { 849 850 var sigId = ev.item.getData(ZmComposeController.SIGNATURE_KEY); 851 var okCallback = this._switchSignatureOkCallback.bind(this, sigId); 852 var cancelCallback = this._switchSignatureCancelCallback.bind(this); 853 //TODO: There shouldn't be a need to warn the user now that we're preserving quoted text and headers. 854 //(see bugs 91743 and 92086 855 //However since it's the release time, it's safer to keep warning the user. 856 //Revisit this after the release. 857 if (!this._warnUserAboutChanges(ZmId.OP_ADD_SIGNATURE, okCallback, cancelCallback)) { 858 this._switchSignature(sigId); 859 } 860 }; 861 862 ZmComposeController.prototype._switchSignatureOkCallback = 863 function(sigId) { 864 this._currentDlg.popdown(); 865 this._switchSignature(sigId); 866 }; 867 868 ZmComposeController.prototype._switchSignatureCancelCallback = 869 function() { 870 this._currentDlg.popdown(); 871 this.setSelectedSignature(this._currentSignatureId); 872 }; 873 874 ZmComposeController.prototype._switchSignature = 875 function(sigId) { 876 this.setSelectedSignature(sigId); 877 var params = { 878 keepAttachments: true, 879 action: this._action, 880 msg: this._msg, 881 extraBodyText: this._composeView.getUserText(), 882 op: ZmId.OP_ADD_SIGNATURE 883 }; 884 this._composeView._updateSignatureVcard(this._currentSignatureId, sigId); 885 this._composeView.resetBody(params); 886 this.resetSignature(sigId); 887 }; 888 889 /** 890 * Sets the tab stops for the compose form. All address fields are added; they're 891 * not actual tab stops unless they're visible. The textarea for plain text and 892 * the HTML editor for HTML compose are swapped in and out depending on the mode. 893 * 894 * @private 895 */ 896 ZmComposeController.prototype._setComposeTabGroup = 897 function() { 898 var tg = this._createTabGroup(); 899 var rootTg = appCtxt.getRootTabGroup(); 900 tg.newParent(rootTg); 901 tg.addMember(this._toolbar); 902 tg.addMember(this._composeView.getTabGroupMember()); 903 }; 904 905 ZmComposeController.prototype.getKeyMapName = 906 function() { 907 return ZmKeyMap.MAP_COMPOSE; 908 }; 909 910 ZmComposeController.prototype.handleKeyAction = 911 function(actionCode) { 912 switch (actionCode) { 913 case ZmKeyMap.CANCEL: 914 this._cancelCompose(); 915 break; 916 917 case ZmKeyMap.SAVE: // Save to draft 918 if (this._uploadingProgress) { 919 break; 920 } 921 if (this._canSaveDraft()) { 922 this.saveDraft(); 923 } 924 break; 925 926 case ZmKeyMap.SEND: // Send message 927 if (!appCtxt.get(ZmSetting.USE_SEND_MSG_SHORTCUT) || this._uploadingProgress) { 928 break; 929 } 930 this._sendListener(); 931 break; 932 933 case ZmKeyMap.ATTACHMENT: 934 this._attachmentListener(); 935 break; 936 937 case ZmKeyMap.SPELLCHECK: 938 if (!appCtxt.isSpellCheckerAvailable()) { 939 break; 940 } 941 this.toggleSpellCheckButton(true); 942 this._spellCheckListener(); 943 break; 944 945 case ZmKeyMap.HTML_FORMAT: 946 if (appCtxt.get(ZmSetting.HTML_COMPOSE_ENABLED)) { 947 var mode = this._composeView.getComposeMode(); 948 var newMode = (mode == Dwt.TEXT) ? Dwt.HTML : Dwt.TEXT; 949 this._setFormat(newMode); 950 this._setOptionsMenu(newMode); 951 } 952 break; 953 954 case ZmKeyMap.ADDRESS_PICKER: 955 this._composeView.getAddressButtonListener(null, AjxEmailAddress.TO); 956 break; 957 958 case ZmKeyMap.NEW_WINDOW: 959 if (!appCtxt.isChildWindow) { 960 this._detachListener(); 961 } 962 break; 963 964 default: 965 return ZmMailListController.prototype.handleKeyAction.call(this, actionCode); 966 break; 967 } 968 return true; 969 }; 970 971 ZmComposeController.prototype.mapSupported = 972 function(map) { 973 return (map == "editor"); 974 }; 975 976 /** 977 * Gets the selected signature. 978 * 979 * @return {String} the selected signature key or <code>null</code> if none selected 980 */ 981 ZmComposeController.prototype.getSelectedSignature = 982 function() { 983 if (!this.isHidden) { 984 var button = this._getSignatureButton(); 985 var menu = button ? button.getMenu() : null; 986 if (menu) { 987 var menuitem = menu.getSelectedItem(DwtMenuItem.RADIO_STYLE); 988 return menuitem ? menuitem.getData(ZmComposeController.SIGNATURE_KEY) : null; 989 } 990 } 991 else { 992 // for hidden compose, return the default signature 993 var ac = window.parentAppCtxt || window.appCtxt; 994 var collection = ac.getIdentityCollection(); 995 return this._composeView._getSignatureIdForAction(collection.defaultIdentity); 996 } 997 }; 998 999 /** 1000 * Gets the selected signature. 1001 * 1002 * @param {String} value the selected signature key 1003 */ 1004 ZmComposeController.prototype.setSelectedSignature = 1005 function(value) { 1006 var button = this._getSignatureButton(); 1007 var menu = button ? button.getMenu() : null; 1008 if (menu) { 1009 if (value === ZmIdentity.SIG_ID_NONE) { 1010 value = ""; 1011 } 1012 menu.checkItem(ZmComposeController.SIGNATURE_KEY, value, true); 1013 } 1014 }; 1015 1016 ZmComposeController.prototype.resetSignature = 1017 function(sigId) { 1018 this._currentSignatureId = sigId; 1019 }; 1020 1021 ZmComposeController.prototype.resetIdentity = 1022 function(identityId) { 1023 this._currentIdentityId = identityId; 1024 }; 1025 1026 // 1027 // Protected methods 1028 // 1029 1030 ZmComposeController.prototype._deleteDraft = 1031 function(delMsg) { 1032 1033 if (!delMsg) { return; } 1034 var ac = window.parentAppCtxt || window.appCtxt; 1035 if (delMsg && delMsg.isSent) { 1036 var folder = delMsg.folderId ? ac.getById(delMsg.folderId) : null; 1037 if (folder && folder.isRemote() && !folder.isPermAllowed(ZmOrganizer.PERM_DELETE)) { 1038 return; //remote folder no permission to delete, exit 1039 } 1040 } 1041 1042 delMsg.doDelete(); 1043 }; 1044 1045 /** 1046 * Creates the compose view based on the mode we're in. Lazily creates the 1047 * compose toolbar, a contact picker, and the compose view itself. 1048 * 1049 * @param action [constant] new message, reply, forward, or an invite action 1050 * @param msg [ZmMailMsg]* the original message (reply/forward), or address (new message) 1051 * @param toOverride [string]* initial value for To: field 1052 * @param subjOverride [string]* initial value for Subject: field 1053 * @param extraBodyText [string]* canned text to prepend to body (invites) 1054 * @param composeMode [constant]* HTML or text compose 1055 * @param accountName [string]* on-behalf-of From address 1056 * @param msgIds [Array]* list of msg Id's to be added as attachments 1057 * @param readReceipt [boolean] true/false read receipt setting 1058 */ 1059 ZmComposeController.prototype._setView = 1060 function(params) { 1061 1062 if (this._autoSaveTimer) { 1063 this._autoSaveTimer.kill(); 1064 } 1065 1066 // msg is the original msg for a reply or when editing a draft (not a newly saved draft or sent msg) 1067 var msg = this._msg = params.msg; 1068 if (msg && msg.isInvite() && ZmComposeController.IS_FORWARD[params.action]) { 1069 params.action = ZmOperation.FORWARD_INLINE; 1070 } 1071 var action = this._action = params.action; 1072 this._origAction = params.origAction; 1073 1074 this._toOverride = params.toOverride; 1075 this._ccOverride = params.ccOverride; 1076 this._subjOverride = params.subjOverride; 1077 this._extraBodyText = params.extraBodyText; 1078 this._msgIds = params.msgIds; 1079 this._accountName = params.accountName; 1080 var identity = params.identity = this._getIdentity(msg); 1081 1082 this._composeMode = params.composeMode || this._getComposeMode(msg, identity, params); 1083 1084 var cv = this._composeView; 1085 if (!cv) { 1086 this.initComposeView(); 1087 cv = this._composeView; 1088 } else { 1089 cv.setComposeMode(this._composeMode, true); 1090 } 1091 1092 if (identity) { 1093 this.resetSignature(cv._getSignatureIdForAction(identity, action)); 1094 } 1095 1096 if (!this.isHidden) { 1097 this._initializeToolBar(); 1098 this.resetToolbarOperations(); 1099 this._setOptionsMenu(this._composeMode, params.incOptions); 1100 } 1101 cv.set(params); 1102 1103 if (!this.isHidden) { 1104 this._setOptionsMenu(); // reset now that compose view has figured out the inc options 1105 appCtxt.notifyZimlets("initializeToolbar", [this._app, this._toolbar, this, this._currentViewId], {waitUntilLoaded:true}); 1106 this._setAddSignatureVisibility(); 1107 if (params.sigId) { 1108 this.setSelectedSignature(params.sigId); 1109 this.resetSignature(params.sigId); 1110 } 1111 1112 // preserve priority for drafts 1113 if (appCtxt.get(ZmSetting.MAIL_PRIORITY_ENABLED)) { 1114 if (msg && action === ZmOperation.DRAFT) { 1115 var priority = msg.isHighPriority ? ZmItem.FLAG_HIGH_PRIORITY : msg.isLowPriority ? ZmItem.FLAG_LOW_PRIORITY : ""; 1116 if (priority) { 1117 this._setPriority(priority); 1118 } 1119 } 1120 else { 1121 this._setPriority(); 1122 } 1123 } 1124 1125 if (params.readReceipt) { 1126 var menu = this._optionsMenu[action]; 1127 var mi = menu && menu.getOp(ZmOperation.REQUEST_READ_RECEIPT); 1128 if (mi && this.isReadReceiptEnabled()) { 1129 mi.setChecked(true, true); 1130 } 1131 } 1132 1133 this._setComposeTabGroup(); 1134 if (!params.hideView) { 1135 this._app.pushView(this._currentViewId); 1136 } 1137 if (!appCtxt.isChildWindow) { 1138 cv.updateTabTitle(); 1139 } 1140 cv.reEnableDesignMode(); 1141 1142 this._draftMsg = params.draftMsg; 1143 this._draftType = params.draftType || ZmComposeController.DRAFT_TYPE_NONE; 1144 if ((this._msgIds || cv._msgAttId) && !appCtxt.isChildWindow) { 1145 this.saveDraft(ZmComposeController.DRAFT_TYPE_AUTO); 1146 } 1147 else if (msg && (action == ZmOperation.DRAFT)) { 1148 this._draftType = ZmComposeController.DRAFT_TYPE_MANUAL; 1149 if (msg.autoSendTime) { 1150 this.saveDraft(ZmComposeController.DRAFT_TYPE_MANUAL, null, null, msg.setAutoSendTime.bind(msg)); 1151 this._showMsgDialog(ZmComposeController.MSG_DIALOG_1, ZmMsg.messageAutoSaveAborted); 1152 } 1153 } 1154 } 1155 1156 cv.checkAttachments(); 1157 this.sendMsgCallback = params.sendMsgCallback; 1158 1159 if (params.callback) { 1160 params.callback.run(this); 1161 } 1162 }; 1163 1164 ZmComposeController.prototype._getIdentity = 1165 function(msg) { 1166 var account = (appCtxt.multiAccounts && appCtxt.getActiveAccount().isMain) 1167 ? appCtxt.accountList.defaultAccount : null; 1168 var identityCollection = appCtxt.getIdentityCollection(account); 1169 if (!msg) { 1170 var ac = window.parentAppCtxt || window.appCtxt; 1171 var curSearch = ac.getApp(ZmApp.MAIL).currentSearch; 1172 var folderId = curSearch && curSearch.folderId; 1173 if (folderId) { 1174 return identityCollection.selectIdentityFromFolder(folderId); 1175 } 1176 } else { 1177 msg = this._getInboundMsg(msg); 1178 } 1179 return (msg && msg.identity) ? msg.identity : identityCollection.selectIdentity(msg); 1180 }; 1181 1182 /** 1183 * find the first message after msg (or msg itself) in the conv, that's inbound (not outbound). This is since inbound ones are 1184 * relevant to the rules to select identify (such as folder rules, outbound could be in "sent" for example, or "to" address rules) 1185 * 1186 * Also, just return the message if it does not have an associated folder - this occurs when a blank message is created in 1187 * Briefcase, for the 'Send as Attachment' command. 1188 * @param msg 1189 * @returns {ZmMailMsg} 1190 */ 1191 ZmComposeController.prototype._getInboundMsg = 1192 function(msg) { 1193 var folder = appCtxt.getById(msg.folderId); 1194 if (!folder || !folder.isOutbound()) { 1195 return msg; 1196 } 1197 var conv = appCtxt.getById(msg.cid); 1198 if (!conv || !conv.msgs) { 1199 return msg; 1200 } 1201 //first find the message in the conv. 1202 var msgs = conv.msgs.getArray(); 1203 for (var i = 0; i < msgs.length; i++) { 1204 if (msgs[i].id === msg.id) { 1205 break; 1206 } 1207 } 1208 //now find the first msg after it that's not outbound 1209 for (i = i + 1; i < msgs.length; i++) { 1210 var nextMsg = msgs[i]; 1211 folder = appCtxt.getById(nextMsg.folderId); 1212 if (!folder.isOutbound()) { 1213 return nextMsg; 1214 } 1215 } 1216 return msg; 1217 }; 1218 1219 ZmComposeController.prototype._initializeToolBar = 1220 function() { 1221 1222 if (this._toolbar) { return; } 1223 1224 var buttons = []; 1225 if (this._canSaveDraft() && appCtxt.get(ZmSetting.MAIL_SEND_LATER_ENABLED)) { 1226 buttons.push(ZmOperation.SEND_MENU); 1227 } else { 1228 buttons.push(ZmOperation.SEND); 1229 } 1230 1231 buttons.push(ZmOperation.CANCEL, ZmOperation.SEP, ZmOperation.SAVE_DRAFT); 1232 1233 if (appCtxt.isSpellCheckerAvailable()) { 1234 buttons.push(ZmOperation.SEP, ZmOperation.SPELL_CHECK); 1235 } 1236 buttons.push(ZmOperation.SEP, ZmOperation.COMPOSE_OPTIONS, ZmOperation.FILLER); 1237 1238 if (appCtxt.get(ZmSetting.DETACH_COMPOSE_ENABLED) && !appCtxt.isChildWindow && !appCtxt.isWebClientOffline()) { 1239 buttons.push(ZmOperation.DETACH_COMPOSE); 1240 } 1241 1242 var tb = this._toolbar = new ZmButtonToolBar({ 1243 parent: this._container, 1244 buttons: buttons, 1245 className: (appCtxt.isChildWindow ? "ZmAppToolBar_cw" : "ZmAppToolBar") + " ImgSkin_Toolbar itemToolbar", 1246 context: this._currentViewId 1247 }); 1248 1249 for (var i = 0; i < tb.opList.length; i++) { 1250 var button = tb.opList[i]; 1251 if (this._listeners[button]) { 1252 tb.addSelectionListener(button, this._listeners[button]); 1253 } 1254 } 1255 1256 if (appCtxt.get(ZmSetting.SIGNATURES_ENABLED) || appCtxt.multiAccounts) { 1257 var sc = appCtxt.getSignatureCollection(); 1258 sc.addChangeListener(this._signatureChangeListener.bind(this)); 1259 1260 var button = tb.getButton(ZmOperation.ADD_SIGNATURE); 1261 if (button) { 1262 button.setMenu(new AjxCallback(this, this._createSignatureMenu)); 1263 } 1264 } 1265 1266 var actions = [ZmOperation.NEW_MESSAGE, ZmOperation.REPLY, ZmOperation.FORWARD_ATT, ZmOperation.DECLINE_PROPOSAL, ZmOperation.CAL_REPLY]; 1267 this._optionsMenu = {}; 1268 for (var i = 0; i < actions.length; i++) { 1269 this._optionsMenu[actions[i]] = this._createOptionsMenu(actions[i]); 1270 } 1271 this._optionsMenu[ZmOperation.REPLY_ALL] = this._optionsMenu[ZmOperation.REPLY]; 1272 this._optionsMenu[ZmOperation.CAL_REPLY_ALL] = this._optionsMenu[ZmOperation.CAL_REPLY]; 1273 this._optionsMenu[ZmOperation.FORWARD_INLINE] = this._optionsMenu[ZmOperation.FORWARD_ATT]; 1274 this._optionsMenu[ZmOperation.REPLY_CANCEL] = this._optionsMenu[ZmOperation.REPLY_ACCEPT] = 1275 this._optionsMenu[ZmOperation.DECLINE_PROPOSAL] = this._optionsMenu[ZmOperation.REPLY_DECLINE] = this._optionsMenu[ZmOperation.REPLY_TENTATIVE] = 1276 this._optionsMenu[ZmOperation.SHARE] = this._optionsMenu[ZmOperation.DRAFT] = 1277 this._optionsMenu[ZmOperation.NEW_MESSAGE]; 1278 1279 // change default button style to select for spell check button 1280 var spellCheckButton = tb.getButton(ZmOperation.SPELL_CHECK); 1281 if (spellCheckButton) { 1282 spellCheckButton.setAlign(DwtLabel.IMAGE_LEFT | DwtButton.TOGGLE_STYLE); 1283 } 1284 1285 var button = tb.getButton(ZmOperation.SEND_MENU); 1286 if (button) { 1287 var menu = new ZmPopupMenu(button, null, null, this); 1288 var sendItem = menu.createMenuItem(ZmOperation.SEND, ZmOperation.defineOperation(ZmOperation.SEND)); 1289 sendItem.addSelectionListener(this._listeners[ZmOperation.SEND]); 1290 var sendLaterItem = menu.createMenuItem(ZmOperation.SEND_LATER, ZmOperation.defineOperation(ZmOperation.SEND_LATER)); 1291 sendLaterItem.addSelectionListener(this._listeners[ZmOperation.SEND_LATER]); 1292 button.setMenu(menu); 1293 } 1294 }; 1295 1296 ZmComposeController.prototype._initAutoSave = 1297 function() { 1298 if (!this._canSaveDraft()) { return; } 1299 if (appCtxt.get(ZmSetting.AUTO_SAVE_DRAFT_INTERVAL)) { 1300 if (!this._autoSaveTimer) { 1301 this._autoSaveTimer = new DwtIdleTimer(ZmMailApp.AUTO_SAVE_IDLE_TIME * 1000, new AjxCallback(this, this._autoSaveCallback)); 1302 } 1303 else{ 1304 this._autoSaveTimer.resurrect(ZmMailApp.AUTO_SAVE_IDLE_TIME * 1000); 1305 } 1306 } 1307 }; 1308 1309 ZmComposeController.prototype._getOptionsMenu = 1310 function() { 1311 return this._toolbar.getButton(ZmOperation.COMPOSE_OPTIONS).getMenu(); 1312 }; 1313 1314 1315 /** 1316 * returns the signature button - not exactly a button but a menu item in the options menu, that has a sub-menu attached to it. 1317 */ 1318 ZmComposeController.prototype._getSignatureButton = 1319 function() { 1320 var menu = this._getOptionsMenu(); 1321 return menu && menu.getItemById(ZmOperation.MENUITEM_ID, ZmOperation.ADD_SIGNATURE); 1322 }; 1323 1324 // only show signature submenu if the account has at least one signature 1325 ZmComposeController.prototype._setAddSignatureVisibility = 1326 function(account) { 1327 var ac = window.parentAppCtxt || window.appCtxt; 1328 if (!ac.get(ZmSetting.SIGNATURES_ENABLED, null, account)) { 1329 return; 1330 } 1331 1332 var button = this._getSignatureButton(); 1333 if (button) { 1334 var visible = ac.getSignatureCollection(account).getSize() > 0; 1335 button.setVisible(visible); 1336 button.parent.cleanupSeparators(); 1337 } 1338 this._setOptionsVisibility(); 1339 }; 1340 1341 ZmComposeController.prototype._setOptionsVisibility = 1342 function() { 1343 1344 var button = this._toolbar.getButton(ZmOperation.COMPOSE_OPTIONS); 1345 var menu = button.getMenu(), 1346 opList = menu && menu.opList, 1347 optionsEmpty = !opList || opList.length === 0; 1348 1349 if (opList && opList.length === 1 && opList[0] === ZmOperation.ADD_SIGNATURE) { 1350 //this is kinda ugly, special case for the signature menu that is empty. It gets hidden instead of removed so it's still here. 1351 var sigButton = this._getSignatureButton(); 1352 optionsEmpty = !sigButton.getVisible(); 1353 } 1354 button.setVisible(!optionsEmpty); 1355 }; 1356 1357 ZmComposeController.prototype._createOptionsMenu = 1358 function(action) { 1359 1360 var isReply = ZmComposeController.IS_REPLY[action]; 1361 var isCalReply = ZmComposeController.IS_CAL_REPLY[action]; 1362 var isInviteReply = ZmComposeController.IS_INVITE_REPLY[action]; 1363 var isForward = ZmComposeController.IS_FORWARD[action]; 1364 var list = []; 1365 var ac = window.parentAppCtxt || window.appCtxt; 1366 if (isReply || isCalReply) { 1367 list.push(ZmOperation.REPLY, ZmOperation.REPLY_ALL, ZmOperation.SEP); 1368 } 1369 if (ac.get(ZmSetting.HTML_COMPOSE_ENABLED)) { 1370 list.push(ZmOperation.FORMAT_HTML, ZmOperation.FORMAT_TEXT, ZmOperation.SEP); 1371 } 1372 if (isInviteReply) { // Accept/decline/etc... an appointment invitation 1373 list.push(ZmOperation.SEP, ZmOperation.INC_NONE, ZmOperation.INC_BODY, ZmOperation.INC_SMART); 1374 } 1375 else if (isCalReply) { // Regular reply to an appointment 1376 list.push(ZmOperation.SEP, ZmOperation.INC_NONE, ZmOperation.INC_BODY, ZmOperation.INC_SMART); 1377 } 1378 else if (isReply) { // Message reply 1379 list.push(ZmOperation.SEP, ZmOperation.INC_NONE, ZmOperation.INC_BODY, ZmOperation.INC_SMART, ZmOperation.INC_ATTACHMENT); 1380 } 1381 else if (isForward) { // Message forwarding 1382 list.push(ZmOperation.SEP, ZmOperation.INC_BODY, ZmOperation.INC_ATTACHMENT); 1383 } 1384 1385 if (isReply || isForward || isCalReply) { 1386 list.push(ZmOperation.SEP, ZmOperation.USE_PREFIX, ZmOperation.INCLUDE_HEADERS); 1387 } 1388 1389 if (appCtxt.get(ZmSetting.SIGNATURES_ENABLED)) { 1390 list.push(ZmOperation.SEP, ZmOperation.ADD_SIGNATURE); 1391 } 1392 1393 list.push(ZmOperation.SEP, ZmOperation.SHOW_BCC); 1394 1395 if (appCtxt.get(ZmSetting.MAIL_PRIORITY_ENABLED)) { 1396 list.push(ZmOperation.SEP); 1397 list.push(ZmOperation.PRIORITY_HIGH); 1398 list.push(ZmOperation.PRIORITY_NORMAL); 1399 list.push(ZmOperation.PRIORITY_LOW); 1400 } 1401 1402 if (ac.get(ZmSetting.MAIL_READ_RECEIPT_ENABLED, null, ac.getActiveAccount())) { 1403 list.push(ZmOperation.SEP, ZmOperation.REQUEST_READ_RECEIPT); 1404 } 1405 1406 var button = this._toolbar.getButton(ZmOperation.COMPOSE_OPTIONS); 1407 1408 var overrides = {}; 1409 for (var i = 0; i < list.length; i++) { 1410 var op = list[i]; 1411 if (op == ZmOperation.SEP) { continue; } 1412 overrides[op] = {}; 1413 if (ZmComposeController.OP_CHECK[op]) { 1414 overrides[op].style = DwtMenuItem.CHECK_STYLE; 1415 } else { 1416 overrides[op].style = DwtMenuItem.RADIO_STYLE; 1417 overrides[op].radioGroupId = ZmComposeController.RADIO_GROUP[op]; 1418 } 1419 if (op == ZmOperation.REPLY || op == ZmOperation.CAL_REPLY) { 1420 overrides[op].text = ZmMsg.replySender; 1421 } 1422 } 1423 1424 var menu = new ZmActionMenu({parent:button, menuItems:list, overrides:overrides, 1425 context:[this._currentViewId, action].join("_")}); 1426 1427 for (var i = 0; i < list.length; i++) { 1428 var op = list[i]; 1429 var mi = menu.getOp(op); 1430 if (!mi) { continue; } 1431 if (op == ZmOperation.FORMAT_HTML) { 1432 mi.setData(ZmHtmlEditor.VALUE, Dwt.HTML); 1433 } else if (op == ZmOperation.FORMAT_TEXT) { 1434 mi.setData(ZmHtmlEditor.VALUE, Dwt.TEXT); 1435 } 1436 mi.setData(ZmOperation.KEY_ID, op); 1437 mi.addSelectionListener(this._listeners[ZmOperation.COMPOSE_OPTIONS]); 1438 } 1439 1440 return menu; 1441 }; 1442 1443 ZmComposeController.prototype._setOptionsMenu = 1444 function(composeMode, incOptions) { 1445 1446 composeMode = composeMode || this._composeMode; 1447 incOptions = incOptions || this._curIncOptions || {}; 1448 var ac = window.parentAppCtxt || window.appCtxt; 1449 1450 var button = this._toolbar.getButton(ZmOperation.COMPOSE_OPTIONS); 1451 button.noMenuBar = true; 1452 button.setToolTipContent(ZmMsg[ZmComposeController.OPTIONS_TT[this._action]], true); 1453 var menu = this._optionsMenu[this._action]; 1454 if (!menu) { return; } 1455 1456 if (ac.get(ZmSetting.HTML_COMPOSE_ENABLED)) { 1457 menu.checkItem(ZmHtmlEditor.VALUE, composeMode, true); 1458 } 1459 1460 if (ac.get(ZmSetting.MAIL_READ_RECEIPT_ENABLED) || ac.multiAccounts) { 1461 var mi = menu.getOp(ZmOperation.REQUEST_READ_RECEIPT); 1462 if (mi) { 1463 // did this draft have "request read receipt" option set? 1464 if (this._msg && this._msg.isDraft) { 1465 mi.setChecked(this._msg.readReceiptRequested); 1466 } else { 1467 // bug: 41329 - always re-init read-receipt option to be off 1468 //read receipt default state will be based on the preference configured 1469 mi.setChecked(appCtxt.get(ZmSetting.AUTO_READ_RECEIPT_ENABLED), true); 1470 } 1471 1472 if (ac.multiAccounts) { 1473 mi.setEnabled(ac.get(ZmSetting.MAIL_READ_RECEIPT_ENABLED, null, this._composeView.getFromAccount())); 1474 } 1475 } 1476 } 1477 1478 if (this._action == ZmOperation.REPLY || this._action == ZmOperation.REPLY_ALL || 1479 this._action == ZmOperation.CAL_REPLY || this._action == ZmOperation.CAL_REPLY_ALL) { 1480 menu.checkItem(ZmOperation.KEY_ID, this._action, true); 1481 } 1482 1483 this._setDependentOptions(incOptions); 1484 1485 var showBcc = appCtxt.get(ZmSetting.SHOW_BCC); 1486 var mi = menu.getOp(ZmOperation.SHOW_BCC); 1487 if (mi) { 1488 mi.setChecked(showBcc); 1489 this._composeView._recipients._toggleBccField(showBcc); 1490 } 1491 1492 button.setMenu(menu); 1493 this._setOptionsVisibility(); 1494 }; 1495 1496 ZmComposeController.prototype._setDependentOptions = 1497 function(incOptions) { 1498 1499 incOptions = incOptions || this._curIncOptions || {}; 1500 1501 var menu = this._optionsMenu[this._action]; 1502 if (!menu) { return; } 1503 1504 // handle options for what's included 1505 var what = incOptions.what; 1506 menu.checkItem(ZmOperation.KEY_ID, ZmComposeController.INC_OP[what], true); 1507 var allowOptions = (what == ZmSetting.INC_BODY || what == ZmSetting.INC_SMART); 1508 var mi = menu.getOp(ZmOperation.USE_PREFIX); 1509 if (mi) { 1510 mi.setEnabled(allowOptions); 1511 mi.setChecked(incOptions.prefix, true); 1512 } 1513 mi = menu.getOp(ZmOperation.INCLUDE_HEADERS); 1514 if (mi) { 1515 mi.setEnabled(allowOptions); 1516 mi.setChecked(incOptions.headers, true); 1517 } 1518 //If we attach multiple messages, disable changing from "include as attachment" to "include original" (inc_body, a.k.a. include inline). Bug 74467 1519 var incOptionsDisabled = (this._msgIds && this._msgIds.length > 1) || this._origAction === ZmOperation.FORWARD_CONV; 1520 mi = menu.getOp(ZmOperation.INC_BODY); 1521 if (mi) { 1522 mi.setEnabled(!incOptionsDisabled); 1523 } 1524 mi = menu.getOp(ZmOperation.INC_ATTACHMENT); 1525 if (mi) { 1526 mi.setEnabled(!incOptionsDisabled); 1527 } 1528 mi = menu.getOp(ZmOperation.REQUEST_READ_RECEIPT); 1529 if (mi) { 1530 var fid = this._msg && this._msg.folderId; 1531 var ac = window.parentAppCtxt || window.appCtxt; 1532 var folder = fid ? ac.getById(fid) : null; 1533 mi.setEnabled(!folder || (folder && !folder.isRemote())); 1534 } 1535 }; 1536 1537 /** 1538 * Called in multi-account mode, when an account has been changed 1539 */ 1540 ZmComposeController.prototype._resetReadReceipt = 1541 function(newAccount) { 1542 var menu = this._optionsMenu[this._action]; 1543 var mi = menu && menu.getOp(ZmOperation.REQUEST_READ_RECEIPT); 1544 if (mi) { 1545 var isEnabled = appCtxt.get(ZmSetting.MAIL_READ_RECEIPT_ENABLED, null, newAccount); 1546 if (!isEnabled) { 1547 mi.setChecked(false, true); 1548 } 1549 mi.setEnabled(isEnabled); 1550 } 1551 }; 1552 1553 ZmComposeController.prototype._getComposeMode = 1554 function(msg, identity, params) { 1555 1556 // depending on COS/user preference set compose format 1557 var composeMode = Dwt.TEXT; 1558 var ac = window.parentAppCtxt || window.appCtxt; 1559 if (ac.get(ZmSetting.HTML_COMPOSE_ENABLED)) { 1560 if (this._action == ZmOperation.NEW_MESSAGE) { 1561 if (ac.get(ZmSetting.COMPOSE_AS_FORMAT) == ZmSetting.COMPOSE_HTML) { 1562 composeMode = Dwt.HTML; 1563 } 1564 } 1565 else if (this._action == ZmOperation.DRAFT) { 1566 if (params && params.isEditAsNew) { //For Edit As New option Bug:73479 1567 if (ac.get(ZmSetting.COMPOSE_AS_FORMAT) === ZmSetting.COMPOSE_HTML) { 1568 composeMode = Dwt.HTML; 1569 } 1570 } 1571 else if (msg && msg.isHtmlMail()) { 1572 composeMode = Dwt.HTML; 1573 } 1574 } 1575 else if (identity) { 1576 var sameFormat = ac.get(ZmSetting.COMPOSE_SAME_FORMAT); 1577 var asFormat = ac.get(ZmSetting.COMPOSE_AS_FORMAT); 1578 if ((!sameFormat && asFormat == ZmSetting.COMPOSE_HTML) || (sameFormat && msg && msg.isHtmlMail())) { 1579 composeMode = Dwt.HTML; 1580 } 1581 } 1582 } 1583 1584 return composeMode; 1585 }; 1586 1587 ZmComposeController.prototype._getBodyContent = 1588 function() { 1589 return this._composeView.getHtmlEditor().getContent(); 1590 }; 1591 1592 ZmComposeController.prototype._setFormat = 1593 function(mode) { 1594 1595 var curMode = this._composeView.getComposeMode(); 1596 if (mode === curMode) { return false; } 1597 1598 var op = (mode === Dwt.HTML) ? ZmOperation.FORMAT_HTML : ZmOperation.FORMAT_TEXT; 1599 var okCallback = this._formatOkCallback.bind(this, mode); 1600 var cancelCallback = this._formatCancelCallback.bind(this, curMode); 1601 //TODO: There shouldn't be a need to warn the user now that we're preserving quoted text and headers. 1602 //(see bugs 91743 and 92086 1603 //However since it's the release time, it's safer to keep warning the user. 1604 //Revisit this after the release. 1605 if (!this._warnUserAboutChanges(op, okCallback, cancelCallback)) { 1606 this._composeView.setComposeMode(mode); 1607 return true; 1608 } 1609 1610 return false; 1611 }; 1612 1613 /** 1614 * 1615 * @return {Boolean} needToPop - whether we need to pop the view 1616 */ 1617 ZmComposeController.prototype._processSendMsg = 1618 function(draftType, msg, resp) { 1619 1620 this._msgSent = true; 1621 var isScheduled = draftType == ZmComposeController.DRAFT_TYPE_DELAYSEND; 1622 var isDraft = (draftType != ZmComposeController.DRAFT_TYPE_NONE && !isScheduled); 1623 var needToPop = false; 1624 if (!isDraft) { 1625 needToPop = true; 1626 var popped = false; 1627 if (appCtxt.get(ZmSetting.SHOW_MAIL_CONFIRM)) { 1628 var confirmController = AjxDispatcher.run("GetMailConfirmController"); 1629 confirmController.showConfirmation(msg, this._currentViewId, this.tabId, this); 1630 needToPop = false; // don't pop confirm page 1631 } else { 1632 if (appCtxt.isChildWindow && window.parentController) { 1633 window.onbeforeunload = null; 1634 if (draftType == ZmComposeController.DRAFT_TYPE_DELAYSEND) { 1635 window.parentController.setStatusMsg(ZmMsg.messageScheduledSent); 1636 } 1637 else if (!appCtxt.isOffline) { // see bug #29372 1638 window.parentController.setStatusMsg(ZmMsg.messageSent); 1639 } 1640 } else { 1641 if (draftType == ZmComposeController.DRAFT_TYPE_DELAYSEND) { 1642 appCtxt.setStatusMsg(ZmMsg.messageScheduledSent); 1643 } else if (!appCtxt.isOffline) { // see bug #29372 1644 appCtxt.setStatusMsg(ZmMsg.messageSent); 1645 } 1646 } 1647 } 1648 1649 if (resp || !appCtxt.get(ZmSetting.SAVE_TO_SENT)) { 1650 1651 // bug 36341 1652 if (!appCtxt.isOffline && resp && appCtxt.get(ZmSetting.SAVE_TO_IMAP_SENT) && msg.identity) { 1653 var datasources = appCtxt.getDataSourceCollection(); 1654 var datasource = datasources && datasources.getById(msg.identity.id); 1655 if (datasource && datasource.type == ZmAccount.TYPE_IMAP) { 1656 var parent = appCtxt.getById(datasource.folderId); 1657 var folder; 1658 if (parent) { 1659 // try to find the sent folder from list of possible choices 1660 var prefix = parent.getName(false, null, true, true) + "/"; 1661 var folderNames = [ 1662 appCtxt.get(ZmSetting.SENT_FOLDER_NAME) || "Sent", 1663 ZmMsg.sent, "Sent Messages", "[Gmail]/Sent Mail" 1664 ]; 1665 for (var i = 0; i < folderNames.length; i++) { 1666 folder = parent.getByPath(prefix+folderNames[i]); 1667 if (folder) break; 1668 } 1669 } 1670 if (folder) { 1671 var jsonObj = { 1672 ItemActionRequest: { 1673 _jsns: "urn:zimbraMail", 1674 action: { 1675 id: resp.m[0].id, 1676 op: "move", 1677 l: folder.id 1678 } 1679 } 1680 }; 1681 var params = { 1682 jsonObj: jsonObj, 1683 asyncMode: true, 1684 noBusyOverlay: true 1685 }; 1686 appCtxt.getAppController().sendRequest(params); 1687 } 1688 } 1689 } 1690 } 1691 } else { 1692 if (draftType != ZmComposeController.DRAFT_TYPE_AUTO) { 1693 var transitions = [ ZmToast.FADE_IN, ZmToast.IDLE, ZmToast.PAUSE, ZmToast.FADE_OUT ]; 1694 appCtxt.setStatusMsg(ZmMsg.draftSaved, ZmStatusView.LEVEL_INFO, null, transitions); 1695 } 1696 this._draftMsg = msg; 1697 this._composeView.processMsgDraft(msg); 1698 // TODO - disable save draft button indicating a draft was saved 1699 1700 var listController = this._listController; // non child window case 1701 if (appCtxt.isChildWindow) { 1702 //Check if Mail App view has been created and then update the MailListController 1703 if (window.parentAppCtxt.getAppViewMgr().getAppView(ZmApp.MAIL)) { 1704 listController = window.parentAppCtxt.getApp(ZmApp.MAIL).getMailListController(); 1705 } 1706 } 1707 1708 //listController is available only when editing an existing draft. 1709 if (listController && listController._draftSaved) { 1710 var savedMsg = appCtxt.isChildWindow ? null : msg; 1711 var savedResp = appCtxt.isChildWindow ? resp.m[0] : null; //Pass the mail response to the parent window such that the ZmMailMsg obj is created in the parent window. 1712 listController._draftSaved(savedMsg, savedResp); 1713 } 1714 } 1715 1716 if (isScheduled) { 1717 if (appCtxt.isChildWindow) { 1718 var pAppCtxt = window.parentAppCtxt; 1719 if (pAppCtxt.getAppViewMgr().getAppView(ZmApp.MAIL)) { 1720 var listController = pAppCtxt.getApp(ZmApp.MAIL).getMailListController(); 1721 if (listController && listController._draftSaved) { 1722 //Pass the mail response to the parent window such that the ZmMailMsg obj is created in the parent window. 1723 listController._draftSaved(null, resp.m[0]); 1724 } 1725 } 1726 } else { 1727 if (this._listController && this._listController._draftSaved) { 1728 this._listController._draftSaved(msg); 1729 } 1730 } 1731 } 1732 return needToPop; 1733 }; 1734 1735 1736 // Listeners 1737 1738 // Send button was pressed 1739 ZmComposeController.prototype._sendListener = 1740 function(ev) { 1741 if (!appCtxt.notifyZimlets("onSendButtonClicked", [this, this._msg])) { 1742 this._send(); 1743 } 1744 }; 1745 1746 ZmComposeController.prototype._send = 1747 function() { 1748 this._toolbar.enableAll(false); // thwart multiple clicks on Send button 1749 this._resetDelayTime(); 1750 this.sendMsg(); 1751 }; 1752 1753 ZmComposeController.prototype._sendLaterListener = 1754 function(ev) { 1755 this.showDelayDialog(); 1756 }; 1757 1758 // Cancel button was pressed 1759 ZmComposeController.prototype._cancelListener = 1760 function(ev) { 1761 this._cancelCompose(); 1762 }; 1763 1764 ZmComposeController.prototype._cancelCompose = function() { 1765 1766 var dirty = this._composeView.isDirty(true, true); 1767 // Prompt the user if compose view is dirty (they may want to save), or if a draft has been 1768 // auto-saved and they might want to delete it 1769 var needPrompt = dirty || (this._draftType === ZmComposeController.DRAFT_TYPE_AUTO); 1770 this._composeView.enableInputs(!needPrompt); 1771 this._composeView.reEnableDesignMode(); 1772 this._app.popView(!needPrompt); 1773 }; 1774 1775 // Attachment button was pressed 1776 ZmComposeController.prototype._attachmentListener = 1777 function(isInline) { 1778 var type = 1779 isInline ? ZmComposeView.UPLOAD_INLINE : ZmComposeView.UPLOAD_COMPUTER; 1780 this._composeView.showAttachmentDialog(type); 1781 }; 1782 1783 ZmComposeController.prototype._optionsListener = 1784 function(ev) { 1785 1786 var op = ev.item.getData(ZmOperation.KEY_ID); 1787 if (op === ZmOperation.REQUEST_READ_RECEIPT) { 1788 return; 1789 } 1790 1791 // Click on "Options" button. 1792 if (op === ZmOperation.COMPOSE_OPTIONS && this._optionsMenu[this._action]) { 1793 var button = this._toolbar.getButton(ZmOperation.COMPOSE_OPTIONS); 1794 var bounds = button.getBounds(); 1795 this._optionsMenu[this._action].popup(0, bounds.x, bounds.y + bounds.height, false); 1796 return; 1797 } 1798 1799 // ignore UNCHECKED for radio buttons 1800 if (ev.detail !== DwtMenuItem.CHECKED && !ZmComposeController.OP_CHECK[op]) { 1801 return; 1802 } 1803 1804 if (op === ZmOperation.REPLY || op === ZmOperation.REPLY_ALL || op === ZmOperation.CAL_REPLY || op === ZmOperation.CAL_REPLY_ALL) { 1805 var cv = this._composeView; 1806 cv.setAddress(AjxEmailAddress.TO, ""); 1807 cv.setAddress(AjxEmailAddress.CC, ""); 1808 cv._setAddresses(op, AjxEmailAddress.TO, this._toOverride); 1809 if (this._ccOverride && (op === ZmOperation.REPLY_ALL || op === ZmOperation.CAL_REPLY_ALL)) { 1810 cv._setAddresses(op, AjxEmailAddress.CC, this._ccOverride); 1811 } 1812 } 1813 else if (op === ZmOperation.FORMAT_HTML || op === ZmOperation.FORMAT_TEXT) { 1814 if (op === ZmOperation.FORMAT_TEXT && this._msg) { 1815 this._msg._resetAllInlineAttachments(); 1816 } 1817 this._setFormat(ev.item.getData(ZmHtmlEditor.VALUE)); 1818 } 1819 else if (op === ZmOperation.SHOW_BCC) { 1820 this._composeView._recipients._toggleBccField(); 1821 appCtxt.set(ZmSetting.SHOW_BCC, !appCtxt.get(ZmSetting.SHOW_BCC)); 1822 } 1823 else if (ZmComposeController.INC_MAP[op] || op === ZmOperation.USE_PREFIX || op === ZmOperation.INCLUDE_HEADERS) { 1824 // user is changing include options 1825 if (this._setInclude(op)) { 1826 this._switchInclude(op); 1827 this._setDependentOptions(); 1828 } 1829 } 1830 }; 1831 1832 ZmComposeController.prototype._setInclude = 1833 function(op) { 1834 1835 // Only give warning if user has typed text that can't be preserved 1836 var okCallback = this._switchIncludeOkCallback.bind(this, op); 1837 var cancelCallback = this._switchIncludeCancelCallback.bind(this, AjxUtil.hashCopy(this._curIncOptions)); 1838 return (!this._warnUserAboutChanges(op, okCallback, cancelCallback)); 1839 }; 1840 1841 /** 1842 * Returns the priority flag corresponding to the currently selected priority option. 1843 * 1844 * @returns {string} 1845 * @private 1846 */ 1847 ZmComposeController.prototype._getPriority = function() { 1848 1849 var menu = this._optionsMenu[this._action], 1850 map = ZmComposeController.PRIORITY_FLAG_TO_OP; 1851 1852 for (var flag in map) { 1853 var op = map[flag], 1854 mi = menu && menu.getOp(op); 1855 if (mi && mi.getChecked()) { 1856 return flag; 1857 } 1858 } 1859 1860 return ""; 1861 }; 1862 1863 /** 1864 * Sets the priority option in the options menu that corresponds to the given priority flag. 1865 * 1866 * @param {String} priority ZmItem.FLAG_*_PRIORITY 1867 * @private 1868 */ 1869 ZmComposeController.prototype._setPriority = function(priority) { 1870 1871 var op = priority ? ZmComposeController.PRIORITY_FLAG_TO_OP[priority] : ZmOperation.PRIORITY_NORMAL, 1872 menu = this._optionsMenu[this._action], 1873 mi = menu && menu.getOp(op); 1874 1875 if (mi) { 1876 mi.setChecked(true, true); 1877 } 1878 }; 1879 1880 ZmComposeController.prototype._switchInclude = function(op) { 1881 1882 var menu = this._optionsMenu[this._action], 1883 cv = this._composeView; 1884 1885 if (op === ZmOperation.USE_PREFIX || op === ZmOperation.INCLUDE_HEADERS) { 1886 var mi = menu.getOp(op); 1887 if (mi) { 1888 if (op === ZmOperation.USE_PREFIX) { 1889 this._curIncOptions.prefix = mi.getChecked(); 1890 } 1891 else { 1892 this._curIncOptions.headers = mi.getChecked(); 1893 } 1894 } 1895 } 1896 else if (ZmComposeController.INC_MAP[op]) { 1897 if (this._curIncOptions.what === ZmSetting.INC_ATTACH) { 1898 cv.removeOrigMsgAtt(); 1899 } 1900 this._curIncOptions.what = ZmComposeController.INC_MAP[op]; 1901 } 1902 1903 var cv = this._composeView; 1904 if (op != ZmOperation.FORMAT_HTML && op != ZmOperation.FORMAT_TEXT) { 1905 if (cv._composeMode == Dwt.TEXT) { 1906 AjxTimedAction.scheduleAction(new AjxTimedAction(this, function() { cv.getHtmlEditor().moveCaretToTop(); }), 200); 1907 } 1908 } 1909 1910 // forwarding actions are tied to inc option 1911 var what = this._curIncOptions.what; 1912 if (this._action == ZmOperation.FORWARD_INLINE && what == ZmSetting.INC_ATTACH) { 1913 this._action = ZmOperation.FORWARD_ATT; 1914 } 1915 if (this._action == ZmOperation.FORWARD_ATT && what != ZmSetting.INC_ATTACH) { 1916 this._action = ZmOperation.FORWARD_INLINE; 1917 } 1918 1919 var params = { 1920 action: this._action, 1921 msg: this._msg, 1922 extraBodyText: this._composeView.getUserText(), 1923 op: op, 1924 keepAttachments: true 1925 }; 1926 this._composeView.resetBody(params); 1927 if (op === ZmOperation.INC_ATTACHMENT) { 1928 this.saveDraft(ZmComposeController.DRAFT_TYPE_AUTO); 1929 } 1930 }; 1931 1932 ZmComposeController.prototype._detachListener = 1933 function(ev) { 1934 if (!appCtxt.isWebClientOffline()) { 1935 var atts = this._composeView.getAttFieldValues(); 1936 if (atts.length) { 1937 this._showMsgDialog(ZmComposeController.MSG_DIALOG_2, ZmMsg.importErrorUpload, null, this._detachCallback.bind(this)); 1938 } else { 1939 this.detach(); 1940 } 1941 } 1942 }; 1943 1944 // Save Draft button was pressed 1945 ZmComposeController.prototype._saveDraftListener = 1946 function(ev) { 1947 this.saveDraft(); 1948 }; 1949 1950 ZmComposeController.prototype._autoSaveCallback = 1951 function(idle) { 1952 DBG.println('draft', 'DRAFT autosave check from ' + this._currentViewId); 1953 if (idle && !DwtBaseDialog.getActiveDialog() && !this._composeView.getHtmlEditor().isSpellCheckMode() && this._composeView.isDirty(true, true)) { 1954 this.saveDraft(ZmComposeController.DRAFT_TYPE_AUTO); 1955 } 1956 }; 1957 1958 ZmComposeController.prototype.saveDraft = 1959 function(draftType, attId, docIds, callback, contactId) { 1960 1961 if (!this._canSaveDraft()) { return; } 1962 1963 this._wasDirty = this._composeView._isDirty; 1964 this._composeView._isDirty = false; 1965 draftType = draftType || ZmComposeController.DRAFT_TYPE_MANUAL; 1966 var respCallback = this._handleResponseSaveDraftListener.bind(this, draftType, callback); 1967 this._resetDelayTime(); 1968 if (!docIds) { 1969 DBG.println('draft', 'SAVE DRAFT for ' + this.getCurrentViewId() + ', type is ' + draftType); 1970 this.sendMsg(attId, draftType, respCallback, contactId); 1971 } else { 1972 this.sendDocs(docIds, draftType, respCallback, contactId); 1973 } 1974 }; 1975 1976 ZmComposeController.prototype._handleResponseSaveDraftListener = 1977 function(draftType, callback, result) { 1978 if (draftType == ZmComposeController.DRAFT_TYPE_AUTO && 1979 this._draftType == ZmComposeController.DRAFT_TYPE_NONE) { 1980 this._draftType = ZmComposeController.DRAFT_TYPE_AUTO; 1981 } else if (draftType == ZmComposeController.DRAFT_TYPE_MANUAL) { 1982 this._draftType = ZmComposeController.DRAFT_TYPE_MANUAL; 1983 } 1984 // this._action = ZmOperation.DRAFT; 1985 // Notify the htmlEditor that the draft has been saved and is not dirty any more. 1986 this._composeView._htmlEditor.clearDirty(); 1987 if (draftType === ZmComposeController.DRAFT_TYPE_MANUAL) { 1988 this._setCancelText(ZmMsg.close) 1989 } 1990 1991 if (callback) { 1992 callback.run(result); 1993 } 1994 }; 1995 1996 ZmComposeController.prototype._spellCheckListener = function(ev) { 1997 1998 var spellCheckButton = this._toolbar.getButton(ZmOperation.SPELL_CHECK); 1999 var htmlEditor = this._composeView.getHtmlEditor(); 2000 2001 if (spellCheckButton) { 2002 if (spellCheckButton.isToggled()) { 2003 var callback = this.toggleSpellCheckButton.bind(this); 2004 if (!htmlEditor.spellCheck(callback)) { 2005 this.toggleSpellCheckButton(false); 2006 } 2007 } else { 2008 htmlEditor.discardMisspelledWords(); 2009 } 2010 } 2011 }; 2012 2013 ZmComposeController.prototype.showDelayDialog = 2014 function() { 2015 if (!this._delayDialog) { 2016 this._delayDialog = new ZmTimeDialog({parent:this._shell, buttons:[DwtDialog.OK_BUTTON, DwtDialog.CANCEL_BUTTON]}); 2017 this._delayDialog.setButtonListener(DwtDialog.OK_BUTTON, this._handleDelayDialog.bind(this)); 2018 } 2019 this._delayDialog.popup(); 2020 }; 2021 2022 ZmComposeController.prototype._handleDelayDialog = 2023 function() { 2024 this._delayDialog.popdown(); 2025 var time = this._delayDialog.getValue(); //Returns {date: Date, timezone: String (see AjxTimezone)} 2026 2027 var date = time.date; 2028 var dateOffset = AjxTimezone.getOffset(AjxTimezone.getClientId(time.timezone), date); 2029 var utcDate = new Date(date.getTime() - dateOffset*60*1000); 2030 2031 var now = new Date(); 2032 var nowOffset = AjxTimezone.getOffset(AjxTimezone.DEFAULT_RULE, now); 2033 var utcNow = new Date(now.getTime() - nowOffset*60*1000); 2034 2035 if(!this._delayDialog.isValidDateStr()){ 2036 this.showInvalidDateDialog(); 2037 } 2038 else if (utcDate < utcNow) { 2039 this.showDelayPastDialog(); 2040 } else { 2041 this._toolbar.enableAll(false); // thwart multiple clicks on Send button 2042 this._sendTime = time; 2043 this.sendMsg(null, ZmComposeController.DRAFT_TYPE_DELAYSEND, null); 2044 } 2045 }; 2046 2047 ZmComposeController.prototype.showDelayPastDialog = 2048 function() { 2049 this._showMsgDialog(ZmComposeController.MSG_DIALOG_2, ZmMsg.sendLaterPastError, null, this._handleDelayPastDialog.bind(this)); 2050 }; 2051 2052 ZmComposeController.prototype.showInvalidDateDialog = 2053 function() { 2054 this._showMsgDialog(ZmComposeController.MSG_DIALOG_1, ZmMsg.invalidDateFormat, DwtMessageDialog.CRITICAL_STYLE, this._handleInvalidDateDialog.bind(this)); 2055 }; 2056 2057 ZmComposeController.prototype._handleInvalidDateDialog = 2058 function() { 2059 this._currentDlg.popdown(); 2060 this._sendLaterListener(); 2061 } 2062 2063 ZmComposeController.prototype._handleDelayPastDialog = 2064 function() { 2065 this._currentDlg.popdown(); 2066 this._send(); 2067 }; 2068 2069 ZmComposeController.prototype._resetDelayTime = 2070 function() { 2071 this._sendTime = null; 2072 }; 2073 2074 // Callbacks 2075 2076 ZmComposeController.prototype._detachCallback = 2077 function() { 2078 // get rid of any lingering attachments since they cannot be detached 2079 this._composeView.cleanupAttachments(); 2080 this._currentDlg.popdown(); 2081 this.detach(); 2082 }; 2083 2084 ZmComposeController.prototype._formatOkCallback = 2085 function(mode) { 2086 this._currentDlg.popdown(); 2087 this._composeView.setComposeMode(mode); 2088 this._composeView._isDirty = true; 2089 }; 2090 2091 ZmComposeController.prototype._formatCancelCallback = 2092 function(mode) { 2093 this._currentDlg.popdown(); 2094 2095 // reset the radio button for the format button menu 2096 var menu = this._toolbar.getButton(ZmOperation.COMPOSE_OPTIONS).getMenu(); 2097 menu.checkItem(ZmHtmlEditor.VALUE, mode, true); 2098 2099 this._composeView.reEnableDesignMode(); 2100 }; 2101 2102 /** 2103 * Called as: Yes, save as draft 2104 * Yes, go ahead and leave compose 2105 * Yes, keep the auto-saved draft (view is dirty, new draft will be saved) 2106 * 2107 * @param mailtoParams [Object]* Used by offline client to pass on mailto handler params 2108 */ 2109 ZmComposeController.prototype._popShieldYesCallback = function(mailtoParams) { 2110 2111 this._popShield.removePopdownListener(this._dialogPopdownListener); 2112 this._popShield.popdown(); 2113 this._composeView.enableInputs(true); 2114 if (this._canSaveDraft()) { 2115 // save as draft 2116 var callback = mailtoParams ? this.doAction.bind(this, mailtoParams) : this._popShieldYesDraftSaved.bind(this); 2117 this._resetDelayTime(); 2118 this.sendMsg(null, ZmComposeController.DRAFT_TYPE_MANUAL, callback); 2119 } 2120 else { 2121 // cancel 2122 if (appCtxt.isChildWindow && window.parentController) { 2123 window.onbeforeunload = null; 2124 } 2125 if (mailtoParams) { 2126 this.doAction(mailtoParams); 2127 } 2128 else { 2129 this._dontSavePreHide = true; 2130 appCtxt.getAppViewMgr().showPendingView(true); 2131 } 2132 } 2133 }; 2134 2135 ZmComposeController.prototype._popShieldYesDraftSaved = 2136 function() { 2137 appCtxt.getAppViewMgr().showPendingView(true); 2138 }; 2139 2140 /** 2141 * Called as: No, don't save as draft 2142 * No, don't leave compose 2143 * Yes, keep the auto-saved draft (view is not dirty, no need to save again) 2144 * 2145 * @param mailtoParams [Object]* Used by offline client to pass on mailto handler params 2146 */ 2147 ZmComposeController.prototype._popShieldNoCallback = function(mailtoParams) { 2148 2149 this._popShield.removePopdownListener(this._dialogPopdownListener); 2150 this._popShield.popdown(); 2151 this._composeView.enableInputs(true); 2152 this._dontSavePreHide = true; 2153 2154 if (this._canSaveDraft()) { 2155 if (appCtxt.isChildWindow && window.parentController) { 2156 window.onbeforeunload = null; 2157 } 2158 if (!mailtoParams) { 2159 appCtxt.getAppViewMgr().showPendingView(true); 2160 } 2161 } 2162 else { 2163 if (!mailtoParams) { 2164 appCtxt.getAppViewMgr().showPendingView(false); 2165 } 2166 this._composeView.reEnableDesignMode(); 2167 } 2168 2169 if (mailtoParams) { 2170 this.doAction(mailtoParams); 2171 } 2172 }; 2173 2174 // Called as: No, do not keep the auto-saved draft 2175 ZmComposeController.prototype._popShieldDiscardCallback = 2176 function() { 2177 this._deleteDraft(this._draftMsg); 2178 this._popShieldNoCallback(); 2179 }; 2180 2181 // Called as: I changed my mind, just make the pop shield go away 2182 ZmComposeController.prototype._popShieldDismissCallback = 2183 function() { 2184 this._popShield.removePopdownListener(this._dialogPopdownListener); 2185 this._popShield.popdown(); 2186 this._cancelViewPop(); 2187 this._initAutoSave(); //re-init autosave since it was killed in preHide 2188 2189 }; 2190 2191 ZmComposeController.prototype._switchIncludeOkCallback = 2192 function(op) { 2193 this._currentDlg.popdown(); 2194 this._switchInclude(op); 2195 this._setDependentOptions(); 2196 }; 2197 2198 ZmComposeController.prototype._switchIncludeCancelCallback = 2199 function(origIncOptions) { 2200 this._currentDlg.popdown(); 2201 this._setOptionsMenu(null, origIncOptions); 2202 }; 2203 2204 /** 2205 * Handles re-enabling inputs if the pop shield is dismissed via 2206 * Esc. Otherwise, the handling is done explicitly by a callback. 2207 */ 2208 ZmComposeController.prototype._dialogPopdownActionListener = 2209 function() { 2210 this._cancelViewPop(); 2211 }; 2212 2213 ZmComposeController.prototype._cancelViewPop = 2214 function() { 2215 this._composeView.enableInputs(true); 2216 appCtxt.getAppViewMgr().showPendingView(false); 2217 this._composeView.reEnableDesignMode(); 2218 }; 2219 2220 ZmComposeController.prototype._getDefaultFocusItem = 2221 function() { 2222 if (this._action == ZmOperation.NEW_MESSAGE || 2223 this._action == ZmOperation.FORWARD_INLINE || 2224 this._action == ZmOperation.FORWARD_ATT) 2225 { 2226 return this._composeView.getAddrInputField(AjxEmailAddress.TO); 2227 } 2228 2229 return (this._composeView.getComposeMode() == Dwt.TEXT) 2230 ? this._composeView._bodyField 2231 : this._composeView._htmlEditor; 2232 }; 2233 2234 ZmComposeController.prototype._createSignatureMenu = 2235 function(button, account) { 2236 if (!this._composeView) { return null; } 2237 2238 var button = this._getSignatureButton(); 2239 if (!button) { return null; } 2240 2241 var menu; 2242 var options = appCtxt.getSignatureCollection(account).getSignatureOptions(); 2243 if (options.length > 0) { 2244 menu = new DwtMenu({parent:button}); 2245 var listener = this._handleSelectSignature.bind(this); 2246 var radioId = this._composeView._htmlElId + "_sig"; 2247 for (var i = 0; i < options.length; i++) { 2248 var option = options[i]; 2249 var menuitem = new DwtMenuItem({parent:menu, style:DwtMenuItem.RADIO_STYLE, radioGroupId:radioId}); 2250 menuitem.setText(AjxStringUtil.htmlEncode(option.displayValue)); 2251 menuitem.setData(ZmComposeController.SIGNATURE_KEY, option.value); 2252 menuitem.addSelectionListener(listener); 2253 menu.checkItem(ZmComposeController.SIGNATURE_KEY, option.value, option.selected); 2254 } 2255 } 2256 return menu; 2257 }; 2258 2259 ZmComposeController.prototype._signatureChangeListener = 2260 function(ev) { 2261 this.resetSignatureToolbar(this.getSelectedSignature()); 2262 }; 2263 2264 /** 2265 * Resets the signature dropdown based on the given account and selects the 2266 * given signature if provided. 2267 * 2268 * @param selected [String]* ID of the signature to select 2269 * @param account [ZmZimbraAccount]* account for which to load signatures 2270 */ 2271 ZmComposeController.prototype.resetSignatureToolbar = 2272 function(selected, account) { 2273 var button = this._getSignatureButton(); 2274 if (!button) { 2275 return; 2276 } 2277 var previousMenu = button.getMenu(); 2278 previousMenu && previousMenu.dispose(); 2279 2280 var menu = this._createSignatureMenu(null, account); 2281 if (menu) { 2282 button.setMenu(menu); 2283 this.setSelectedSignature(selected || ""); 2284 } 2285 2286 this._setAddSignatureVisibility(account); 2287 }; 2288 2289 ZmComposeController.prototype.resetToolbarOperations = 2290 function() { 2291 if (this.isHidden) { return; } 2292 this._toolbar.enableAll(true); 2293 if (ZmComposeController.IS_INVITE_REPLY[this._action]) { 2294 var ops = [ ZmOperation.SAVE_DRAFT, ZmOperation.ATTACHMENT ]; 2295 this._toolbar.enable(ops, false); 2296 this._composeView.enableAttachButton(false); 2297 } else { 2298 this._composeView.enableAttachButton(true); 2299 } 2300 2301 this._setCancelText(this._action === ZmId.OP_DRAFT ? ZmMsg.close : ZmMsg.cancel); 2302 2303 appCtxt.notifyZimlets("resetToolbarOperations", [this._toolbar, 1]); 2304 }; 2305 2306 ZmComposeController.prototype._setCancelText = 2307 function(text) { 2308 var cancel = this._toolbar.getButton(ZmOperation.CANCEL); 2309 cancel.setText(text); 2310 }; 2311 2312 ZmComposeController.prototype._canSaveDraft = 2313 function() { 2314 return !this.isHidden && appCtxt.get(ZmSetting.SAVE_DRAFT_ENABLED) && !ZmComposeController.IS_INVITE_REPLY[this._action]; 2315 }; 2316 2317 /* 2318 * Return true/false if read receipt is being requested 2319 */ 2320 ZmComposeController.prototype.isRequestReadReceipt = 2321 function(){ 2322 2323 // check for read receipt 2324 var requestReadReceipt = false; 2325 var isEnabled = this.isReadReceiptEnabled(); 2326 if (isEnabled) { 2327 var menu = this._toolbar.getButton(ZmOperation.COMPOSE_OPTIONS).getMenu(); 2328 if (menu) { 2329 var mi = menu.getItemById(ZmOperation.KEY_ID, ZmOperation.REQUEST_READ_RECEIPT); 2330 requestReadReceipt = (!!(mi && mi.getChecked())); 2331 } 2332 } 2333 2334 return requestReadReceipt; 2335 }; 2336 2337 /* 2338 * Return true/false if read receipt is enabled 2339 */ 2340 ZmComposeController.prototype.isReadReceiptEnabled = 2341 function(){ 2342 var acctName = appCtxt.multiAccounts 2343 ? this._composeView.getFromAccount().name : this._accountName; 2344 var acct = acctName && appCtxt.accountList.getAccountByName(acctName); 2345 if (appCtxt.get(ZmSetting.MAIL_READ_RECEIPT_ENABLED, null, acct)){ 2346 return true; 2347 } 2348 2349 return false; 2350 }; 2351 2352 /** 2353 * Return ZmMailMsg object 2354 * @return {ZmMailMsg} message object 2355 */ 2356 ZmComposeController.prototype.getMsg = 2357 function(){ 2358 return this._msg; 2359 }; 2360 2361 ZmComposeController.prototype._processImages = 2362 function(callback){ 2363 2364 var idoc = this._composeView._getIframeDoc();//editor iframe document 2365 if (!idoc) { 2366 return; 2367 } 2368 2369 var imgArray = idoc.getElementsByTagName("img"), 2370 length = imgArray.length; 2371 if (length === 0) {//No image elements in the editor document 2372 return; 2373 } 2374 2375 var isWebkitFakeURLImage = false; 2376 for (var i = 0; i < length; i++) { 2377 var img = imgArray[i], 2378 imgSrc = img.src; 2379 2380 if (imgSrc && imgSrc.indexOf("webkit-fake-url://") === 0) { 2381 img.parentNode.removeChild(img); 2382 length--; 2383 i--; 2384 isWebkitFakeURLImage = true; 2385 } 2386 } 2387 2388 if (isWebkitFakeURLImage) { 2389 appCtxt.setStatusMsg(ZmMsg.invalidPastedImages); 2390 if (length === 0) {//No image elements in the editor document 2391 return; 2392 } 2393 } 2394 2395 return this._processDataURIImages(imgArray, length, callback); 2396 }; 2397 2398 ZmComposeController.prototype._processDataURIImages = 2399 function (imgArray, length, callback) { 2400 2401 if ( !(typeof window.atob === "function" && typeof window.Blob === "function") || appCtxt.isWebClientOffline()) { 2402 return; 2403 } 2404 2405 for (var i = 0, blobArray = []; i < length; i++) { 2406 var img = imgArray[i]; 2407 if (img) { 2408 var imgSrc = img.src; 2409 if (imgSrc && imgSrc.indexOf("data:") !== -1) { 2410 var blob = AjxUtil.dataURItoBlob(imgSrc); 2411 if (blob) { 2412 //setting data-zim-uri attribute for image replacement in callback 2413 var id = Dwt.getNextId(); 2414 img.setAttribute("id", id); 2415 img.setAttribute("data-zim-uri", id); 2416 blob.id = id; 2417 blobArray.push(blob); 2418 } 2419 } 2420 } 2421 } 2422 2423 length = blobArray.length; 2424 if (length === 0) { 2425 return; 2426 } 2427 2428 this._uploadedImageArray = []; 2429 this._dataURIImagesLength = length; 2430 2431 for (i = 0; i < length; i++) { 2432 var blob = blobArray[i]; 2433 var uploadImageCallback = this._handleUploadImage.bind(this, callback, blob.id); 2434 this._uploadImage(blob, uploadImageCallback); 2435 } 2436 return true; 2437 }; 2438 2439 ZmComposeController.prototype._initUploadMyComputerFile = 2440 function(files) { 2441 if (appCtxt.isWebClientOffline()) { 2442 return this._handleOfflineUpload(files); 2443 } else { 2444 var curView = this._composeView; 2445 if (curView) { 2446 var params = { 2447 attachment: true, 2448 files: files, 2449 notes: "", 2450 allResponses: null, 2451 start: 0, 2452 curView: this._composeView, 2453 preAllCallback: this._preUploadAll.bind(this), 2454 initOneUploadCallback: curView._startUploadAttachment.bind(curView), 2455 errorCallback: curView._resetUpload.bind(curView, true), 2456 completeOneCallback: curView.updateAttachFileNode.bind(curView), 2457 completeAllCallback: this._completeAllUpload.bind(this) 2458 } 2459 params.progressCallback = curView._uploadFileProgress.bind(curView, params); 2460 2461 2462 // Do a SaveDraft at the start, since we will suppress autosave during the upload 2463 var uploadManager = appCtxt.getZmUploadManager(); 2464 var uploadCallback = uploadManager.upload.bind(uploadManager, params); 2465 this.saveDraft(ZmComposeController.DRAFT_TYPE_AUTO, null, null, uploadCallback); 2466 } 2467 } 2468 }; 2469 2470 ZmComposeController.prototype._preUploadAll = 2471 function(fileName) { 2472 var curView = this._composeView; 2473 if (!curView) { 2474 return; 2475 } 2476 curView._initProgressSpan(fileName); 2477 // Disable autosave while uploading 2478 if (this._autoSaveTimer) { 2479 this._autoSaveTimer.kill(); 2480 } 2481 }; 2482 2483 ZmComposeController.prototype._completeAllUpload = 2484 function(allResponses) { 2485 var curView = this._composeView; 2486 if (!curView){ 2487 return; 2488 } 2489 var callback = curView._resetUpload.bind(curView); 2490 // Init autosave, otherwise saveDraft thinks this is a suppressed autosave, and aborts w/o saving 2491 this._initAutoSave(); 2492 this.saveDraft(ZmComposeController.DRAFT_TYPE_AUTO, this._syncPrevData(allResponses), null, callback); 2493 } 2494 2495 ZmComposeController.prototype._syncPrevData = 2496 function(attaData){ 2497 var result = [] 2498 for (var i=0;i < attaData.length ; i++){ 2499 if (attaData[i] && (i === attaData.length -1 || document.getElementById(attaData[i].aid))){ 2500 result.push(attaData[i]); 2501 } 2502 } 2503 2504 return result; 2505 }; 2506 2507 ZmComposeController.prototype._uploadImage = function(blob, callback, errorCallback){ 2508 var req = new XMLHttpRequest(); 2509 req.open("POST", appCtxt.get(ZmSetting.CSFE_ATTACHMENT_UPLOAD_URI)+"?fmt=extended,raw", true); 2510 req.setRequestHeader("Cache-Control", "no-cache"); 2511 req.setRequestHeader("X-Requested-With", "XMLHttpRequest"); 2512 req.setRequestHeader("Content-Type", blob.type); 2513 req.setRequestHeader("Content-Disposition", 'attachment; filename="' + AjxUtil.convertToEntities(blob.name) + '"'); 2514 if (window.csrfToken) { 2515 req.setRequestHeader("X-Zimbra-Csrf-Token", window.csrfToken); 2516 } 2517 req.onreadystatechange = function() { 2518 if (req.readyState === 4) { 2519 if (req.status === 200) { 2520 var resp = eval("["+req.responseText+"]"); 2521 callback.run(resp[2]); 2522 } 2523 else { 2524 errorCallback && errorCallback(); 2525 } 2526 } 2527 }; 2528 req.send(blob); 2529 }; 2530 2531 ZmComposeController.prototype._handleUploadImage = function(callback, id, response){ 2532 this._dataURIImagesLength--; 2533 var uploadedImageArray = this._uploadedImageArray; 2534 if( response && id ){ 2535 response[0]["id"] = id; 2536 uploadedImageArray.push(response[0]); 2537 } 2538 if( this._dataURIImagesLength === 0 && callback ){ 2539 if( uploadedImageArray.length > 0 && callback.args ){ 2540 uploadedImageArray.clipboardPaste = true; 2541 if(callback.args[0]){ //attid argument of _sendMsg method 2542 callback.args[0] = callback.args[0].concat(uploadedImageArray); 2543 } 2544 else{ 2545 callback.args[0] = uploadedImageArray; 2546 } 2547 } 2548 callback.run(); 2549 delete this._uploadedImageArray; 2550 delete this._dataURIImagesLength; 2551 } 2552 }; 2553 2554 ZmComposeController.prototype._warnUserAboutChanges = 2555 function(op, okCallback, cancelCallback) { 2556 2557 var cv = this._composeView; 2558 var switchToText = (op === ZmOperation.FORMAT_TEXT && 2559 !AjxUtil.isEmpty(this._getBodyContent())); 2560 var willLoseChanges = cv.componentContentChanged(ZmComposeView.BC_SIG_PRE) || 2561 cv.componentContentChanged(ZmComposeView.BC_DIVIDER) || 2562 cv.componentContentChanged(ZmComposeView.BC_HEADERS) || 2563 cv.componentContentChanged(ZmComposeView.BC_SIG_POST) || 2564 !cv.canPreserveQuotedText(op) || 2565 switchToText; 2566 if (willLoseChanges) { 2567 var callbacks = {}; 2568 callbacks[DwtDialog.OK_BUTTON] = okCallback; 2569 callbacks[DwtDialog.CANCEL_BUTTON] = cancelCallback; 2570 var msg = (willLoseChanges && switchToText) ? ZmMsg.switchIncludeAndFormat : 2571 willLoseChanges ? ZmMsg.switchIncludeWarning : ZmMsg.switchToText; 2572 this._showMsgDialog(ZmComposeController.MSG_DIALOG_2, msg, null, callbacks); 2573 return true; 2574 } 2575 2576 return false; 2577 }; 2578 2579 ZmComposeController.prototype._showMsgDialog = 2580 function(dlgType, msg, style, callbacks) { 2581 2582 var ac = window.appCtxt; 2583 var dlg = this._currentDlg = (dlgType === ZmComposeController.MSG_DIALOG_1) ? ac.getMsgDialog() : 2584 (dlgType === ZmComposeController.MSG_DIALOG_2) ? ac.getOkCancelMsgDialog() : ac.getYesNoCancelMsgDialog(); 2585 dlg.reset(); 2586 if (msg) { 2587 dlg.setMessage(msg, style || DwtMessageDialog.WARNING_STYLE); 2588 } 2589 if (callbacks) { 2590 if (typeof callbacks === "function") { 2591 var cb = {}; 2592 cb[DwtDialog.OK_BUTTON] = callbacks; 2593 callbacks = cb; 2594 } 2595 for (var buttonId in callbacks) { 2596 dlg.registerCallback(buttonId, callbacks[buttonId]); 2597 } 2598 } 2599 dlg.popup(); 2600 }; 2601 2602 ZmComposeController.prototype._handleOfflineUpload = 2603 function(files) { 2604 var callback = this._readFilesAsDataURLCallback.bind(this); 2605 ZmComposeController.readFilesAsDataURL(files, callback); 2606 }; 2607 2608 /** 2609 * Read files in DataURL Format and execute the callback with param dataURLArray. 2610 * 2611 * dataURLArray is an array of objects, with each object containing name, type, size and data in data-url format for an file. 2612 * 2613 * @param {FileList} files Object containing one or more files 2614 * @param {AjxCallback/Bound} callback the success callback 2615 * @param {AjxCallback/Bound} errorCallback the error callback 2616 * 2617 * @public 2618 */ 2619 ZmComposeController.readFilesAsDataURL = 2620 function(files, callback, errorCallback) { 2621 var i = 0, 2622 filesLength = files.length, 2623 dataURLArray = [], 2624 fileReadErrorCount = 0; 2625 2626 if (!window.FileReader || filesLength === 0) { 2627 if (errorCallback) { 2628 errorCallback.run(dataURLArray, fileReadErrorCount); 2629 } 2630 return; 2631 } 2632 2633 for (; i < filesLength; i++) { 2634 2635 var file = files[i], 2636 reader = new FileReader(); 2637 2638 reader.onload = function(file, ev) { 2639 dataURLArray.push( 2640 { 2641 filename : file.name, 2642 ct : file.type, 2643 s : file.size, 2644 data : ev.target.result, 2645 aid : new Date().getTime().toString(), 2646 isOfflineUploaded : true 2647 } 2648 ); 2649 }.bind(null, file); 2650 2651 reader.onerror = function() { 2652 fileReadErrorCount++; 2653 }; 2654 2655 //Called when the read is completed, whether successful or not. This is called after either onload or onerror. 2656 reader.onloadend = function() { 2657 if ((dataURLArray.length + fileReadErrorCount) === filesLength) { 2658 if (fileReadErrorCount > 0 && errorCallback) { 2659 errorCallback.run(dataURLArray, fileReadErrorCount); 2660 } 2661 if (callback) { 2662 callback.run(dataURLArray, fileReadErrorCount); 2663 } 2664 } 2665 }; 2666 2667 reader.readAsDataURL(file); 2668 } 2669 }; 2670 2671 ZmComposeController.prototype._readFilesAsDataURLCallback = 2672 function(filesArray) { 2673 for (var j = 0; j < filesArray.length; j++) { 2674 var file = filesArray[j]; 2675 if (file) { 2676 appCtxt.cacheSet(file.aid, file); 2677 } 2678 } 2679 var curView = this._composeView, 2680 callback = curView._resetUpload.bind(curView); 2681 this.saveDraft(ZmComposeController.DRAFT_TYPE_AUTO, filesArray, null, callback); 2682 }; 2683