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