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 ZmMailMsgView = function(params) {
 25 
 26 	if (arguments.length == 0) { return; }
 27 
 28 	params.className = params.className || "ZmMailMsgView";
 29 	ZmMailItemView.call(this, params);
 30 
 31 	this._mode = params.mode;
 32 	this._controller = params.controller;
 33 	this._viewId = this._getViewId(params.sessionId);
 34 
 35 	this._displayImagesId	= ZmId.getViewId(this._viewId, ZmId.MV_DISPLAY_IMAGES, this._mode);
 36 	this._msgTruncatedId	= ZmId.getViewId(this._viewId, ZmId.MV_MSG_TRUNC, this._mode);
 37 	this._infoBarId			= ZmId.getViewId(this._viewId, ZmId.MV_INFO_BAR, this._mode);
 38 	this._tagRowId			= ZmId.getViewId(this._viewId, ZmId.MV_TAG_ROW, this._mode);
 39 	this._tagCellId			= ZmId.getViewId(this._viewId, ZmId.MV_TAG_CELL, this._mode);
 40 	this._attLinksId		= ZmId.getViewId(this._viewId, ZmId.MV_ATT_LINKS, this._mode);
 41 
 42 	this._scrollWithIframe = params.scrollWithIframe;
 43 	this._limitAttachments = this._scrollWithIframe ? 3 : 0; //making it local
 44 	this._attcMaxSize = this._limitAttachments * 16 + 8;
 45 	this.setScrollStyle(this._scrollWithIframe ? DwtControl.CLIP : DwtControl.SCROLL);
 46 
 47 	ZmTagsHelper.setupListeners(this); //setup tags related listeners.
 48 
 49 	this._setMouseEventHdlrs(); // needed by object manager
 50 	this._objectManager = true;
 51 
 52 	this._changeListener = this._msgChangeListener.bind(this);
 53 	this.addListener(DwtEvent.ONSELECTSTART, this._selectStartListener.bind(this));
 54 	this.addListener(DwtEvent.CONTROL, this._controlEventListener.bind(this));
 55 
 56 	// bug fix #25724 - disable right click selection for offline
 57 	if (!appCtxt.isOffline) {
 58 		this._setAllowSelection();
 59 	}
 60 
 61 	this.noTab = true;
 62     this._attachmentLinkIdToFileNameMap = null;
 63 	this._bubbleParams = {};
 64 
 65 	if (this._controller && this._controller._checkKeepReading) {
 66 		Dwt.setHandler(this.getHtmlElement(), DwtEvent.ONSCROLL, ZmDoublePaneController.handleScroll);
 67 	};
 68 
 69 	this._tabGroupMember = new DwtTabGroup("ZmMailMsgView");
 70 	this._headerTabGroup = new DwtTabGroup("ZmMailMsgView (header)");
 71 	this._attachmentTabGroup = new DwtTabGroup("ZmMailMsgView (attachments)");
 72 	this._bodyTabGroup = new DwtTabGroup("ZmMailMsgView (body)");
 73 	this._footerTabGroup = new DwtTabGroup("ZmMailMsgView (footer)");
 74 
 75 	this._tabGroupMember.addMember([
 76 		this._headerTabGroup, this._bodyTabGroup, this._footerTabGroup
 77 	]);
 78 
 79 	if (this._mode === ZmId.VIEW_TRAD) {
 80 		this.setAttribute('role', 'region');
 81 	}
 82 };
 83 
 84 ZmMailMsgView.prototype = new ZmMailItemView;
 85 ZmMailMsgView.prototype.constructor = ZmMailMsgView;
 86 
 87 ZmMailMsgView.prototype.isZmMailMsgView = true;
 88 ZmMailMsgView.prototype.toString = function() {	return "ZmMailMsgView"; };
 89 
 90 
 91 // displays any additional headers in messageView
 92 //pass ZmMailMsgView.displayAdditionalHdrsInMsgView[<actualHeaderName>] = <DisplayName>
 93 //pass ZmMailMsgView.displayAdditionalHdrsInMsgView["X-Mailer"] = "Sent Using:"
 94 ZmMailMsgView.displayAdditionalHdrsInMsgView = {};
 95 
 96 
 97 // Consts
 98 
 99 ZmMailMsgView.SCROLL_WITH_IFRAME	= true;
100 ZmMailMsgView.LIMIT_ATTACHMENTS 	= ZmMailMsgView.SCROLL_WITH_IFRAME ? 3 : 0;
101 ZmMailMsgView.ATTC_COLUMNS			= 2;
102 ZmMailMsgView.ATTC_MAX_SIZE			= ZmMailMsgView.LIMIT_ATTACHMENTS * 16 + 8;
103 ZmMailMsgView.QUOTE_DEPTH_MOD 		= 3;
104 ZmMailMsgView.MAX_SIG_LINES 		= 8;
105 ZmMailMsgView.SIG_LINE 				= /^(- ?-+)|(__+)\r?$/;
106 ZmMailMsgView._inited 				= false;
107 ZmMailMsgView.SHARE_EVENT 			= "share";
108 ZmMailMsgView.SUBSCRIBE_EVENT 		= "subscribe";
109 ZmMailMsgView.IMG_FIX_RE			= new RegExp("(<img\\s+.*dfsrc\\s*=\\s*)[\"']http[^'\"]+part=([\\d\\.]+)[\"']([^>]*>)", "gi");
110 ZmMailMsgView.FILENAME_INV_CHARS_RE = /[\.\/?*:;{}'\\]/g; // Chars we do not allow in a filename
111 ZmMailMsgView.SETHEIGHT_MAX_TRIES	= 3;
112 
113 ZmMailMsgView._URL_RE = /^((https?|ftps?):\x2f\x2f.+)$/;
114 ZmMailMsgView._MAILTO_RE = /^mailto:[\x27\x22]?([^@?&\x22\x27]+@[^@?&]+\.[^@?&\x22\x27]+)[\x27\x22]?/;
115 
116 ZmMailMsgView.MAX_ADDRESSES_IN_FIELD = 10;
117 
118 // tags that are trusted in HTML content that is not displayed in an iframe
119 ZmMailMsgView.TRUSTED_TAGS = ["#text", "a", "abbr", "acronym", "address", "article", "b", "basefont", "bdo", "big",
120 	"blockquote", "body", "br", "caption", "center", "cite", "code", "col", "colgroup", "dd", "del", "dfn", "dir",
121 	"div", "dl", "dt", "em", "font", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", "html", "i", "img",
122 	"ins", "kbd", "li", "mark", "menu", "meter", "nav", "ol", "p", "pre", "q", "s", "samp", "section", "small",
123 	"span", "strike", "strong", "sub", "sup", "table", "tbody", "td", "tfoot", "th", "thead", "time", "tr", "tt",
124 	"u", "ul", "var", "wbr"];
125 
126 // attributes that we don't want to appear in HTML displayed in a div
127 ZmMailMsgView.UNTRUSTED_ATTRS = ["id", "class", "name", "profile"];
128 
129 // Public methods
130 
131 ZmMailMsgView.prototype.getController =
132 function() {
133 	return this._controller;
134 };
135 
136 ZmMailMsgView.prototype.reset =
137 function() {
138 	// Bug 23692: cancel any pending actions
139 	if (this._resizeAction) {
140 		AjxTimedAction.cancelAction(this._resizeAction);
141 		this._resizeAction = null;
142 	}
143 	if (this._objectsAction) {
144 		AjxTimedAction.cancelAction(this._objectsAction);
145 		this._objectsAction = null;
146 	}
147 	this._msg = this._item = null;
148 	this._htmlBody = null;
149 	this._containerEl = null;
150 
151 	// TODO: reuse all these controls that are being disposed here.....
152 	if (this._ifw) {
153 		this._ifw.dispose();
154 		this._ifw = null;
155 	}
156 	if (this._inviteMsgView) {
157 		this._inviteMsgView.reset(true);
158 	}
159 	
160 	var el = this.getHtmlElement();
161 	if (el) {
162 		el.innerHTML = "";
163 	}
164 	if (this._objectManager && this._objectManager.reset) {
165 		this._objectManager.reset();
166 	}
167 	this.setScrollWithIframe(this._scrollWithIframe);
168 };
169 
170 ZmMailMsgView.prototype.dispose =
171 function() {
172 	ZmTagsHelper.disposeListeners(this);
173 	ZmMailItemView.prototype.dispose.apply(this, arguments);
174 };
175 
176 ZmMailMsgView.prototype.preventSelection =
177 function() {
178 	return false;
179 };
180 
181 ZmMailMsgView.prototype.set =
182 function(msg, force, dayViewCallback) {
183 	
184 	if (!force && this._msg && msg && !msg.force && (this._msg == msg)) { return; }
185 
186 	var oldMsg = this._msg;
187 	this.reset();
188 	var contentDiv = this._getContainer();
189 	this._msg = this._item = msg;
190 
191 	if (!msg) {
192 		if (this._inviteMsgView) {
193 			this._inviteMsgView.resize(true); //make sure the msg preview pane takes the entire area, in case we were viewing an invite. (since then it was resized to allow for day view) - bug 53098
194 		}
195 		contentDiv.innerHTML = (this._controller.getList().size()) ? AjxTemplate.expand("mail.Message#viewMessage") : "";
196 		return;
197 	}
198 
199 	msg.force = false;
200 	var respCallback = this._handleResponseSet.bind(this, msg, oldMsg, dayViewCallback);
201 	this._renderMessage(msg, contentDiv, respCallback);
202 };
203 
204 ZmMailMsgView.prototype._getContainer =
205 function() {
206 	return this.getHtmlElement();
207 };
208 
209 ZmMailMsgView.prototype.__hasMountpoint =
210 function(share) {
211 	var tree = appCtxt.getFolderTree();
212 	return tree
213 		? this.__hasMountpoint2(tree.root, share.grantor.id, share.link.id)
214 		: false;
215 };
216 
217 ZmMailMsgView.prototype.__hasMountpoint2 =
218 function(organizer, zid, rid) {
219 	if (organizer.zid == zid && organizer.rid == rid)
220 		return true;
221 
222 	if (organizer.children) {
223 		var children = organizer.children.getArray();
224 		for (var i = 0; i < children.length; i++) {
225 			var found = this.__hasMountpoint2(children[i], zid, rid);
226 			if (found) {
227 				return true;
228 			}
229 		}
230 	}
231 	return false;
232 };
233 
234 ZmMailMsgView.prototype.highlightObjects =
235 function(origText) {
236 	if (origText != null) {
237 		// we get here only for text messages; it's a lot
238 		// faster to call findObjects on the whole text rather
239 		// than parsing the DOM.
240 		DBG.timePt("START - highlight objects on-demand, text msg.");
241 		this._lazyCreateObjectManager();
242 		var html = this._objectManager.findObjects(origText, true, null, true);
243 		html = html.replace(/^ /mg, " ")
244 			.replace(/\t/g, "<pre style='display:inline;'>\t</pre>")
245 			.replace(/\n/g, "<br>");
246 		var container = this.getContentContainer();
247 		container.innerHTML = html;
248 		DBG.timePt("END - highlight objects on-demand, text msg.");
249 	} else {
250 		this._processHtmlDoc();
251 	}
252 };
253 
254 ZmMailMsgView.prototype.resetMsg =
255 function(newMsg) {
256 	// Remove listener for current msg if it exists
257 	if (this._msg) {
258 		this._msg.removeChangeListener(this._changeListener);
259 	}
260 };
261 
262 ZmMailMsgView.prototype.getMsg =
263 function() {
264 	return this._msg;
265 };
266 
267 ZmMailMsgView.prototype.getItem = ZmMailMsgView.prototype.getMsg;
268 
269 // Following two overrides are a hack to allow this view to pretend it's a list view
270 ZmMailMsgView.prototype.getSelection =
271 function() {
272 	return [this._msg];
273 };
274 
275 ZmMailMsgView.prototype.getSelectionCount =
276 function() {
277 	return 1;
278 };
279 
280 ZmMailMsgView.prototype.getMinHeight =
281 function() {
282 	if (!this._headerHeight) {
283 		var headerObj = document.getElementById(this._hdrTableId);
284 		this._headerHeight = headerObj ? Dwt.getSize(headerObj).y : 0;
285 	}
286 	return this._headerHeight;
287 };
288 
289 // returns true if the current message was rendered in HTML
290 ZmMailMsgView.prototype.hasHtmlBody =
291 function() {
292 	return this._htmlBody != null;
293 };
294 
295 // returns the IFRAME's document if we are using one, or the window document
296 ZmMailMsgView.prototype.getDocument =
297 function() {
298 	return this._usingIframe ? Dwt.getIframeDoc(this.getIframe()) : document;
299 };
300 
301 // returns the IFRAME element if we are using one
302 ZmMailMsgView.prototype.getIframe =
303 function() {
304 
305 	if (!this._usingIframe) { return null; }
306 	
307 	var iframe = this._iframeId && document.getElementById(this._iframeId);
308 	iframe = iframe || (this._ifw && this._ifw.getIframe());
309 	return iframe;
310 };
311 ZmMailMsgView.prototype.getIframeElement = ZmMailMsgView.prototype.getIframe;
312 
313 // Returns a BODY element if we are using an IFRAME, the container DIV if we are not.
314 ZmMailMsgView.prototype.getContentContainer =
315 function() {
316 	if (this._usingIframe) {
317 		var idoc = this.getDocument();
318 		var body = idoc && idoc.body;
319 		return body && body.childNodes.length === 1 ? body.firstChild : body;
320 	}
321 	else {
322 		return this._containerEl;
323 	}
324 };
325 
326 ZmMailMsgView.prototype.getContent =
327 function() {
328 	var container = this.getContentContainer();
329 	return container ? container.innerHTML : "";
330 };
331 
332 ZmMailMsgView.prototype.addInviteReplyListener =
333 function(listener) {
334 	this.addListener(ZmInviteMsgView.REPLY_INVITE_EVENT, listener);
335 };
336 
337 ZmMailMsgView.prototype.addShareListener =
338 function(listener) {
339 	this.addListener(ZmMailMsgView.SHARE_EVENT, listener);
340 };
341 
342 ZmMailMsgView.prototype.addSubscribeListener =
343 function(listener) {
344 	this.addListener(ZmMailMsgView.SUBSCRIBE_EVENT, listener);
345 };
346 
347 ZmMailMsgView.prototype.getTabGroupMember =
348 function() {
349 	return this._tabGroupMember;
350 };
351 
352 ZmMailMsgView.prototype._getMessageTabMember =
353 function() {
354 	if (this._usingIframe) {
355 		return this.getIframe().parentNode;
356 	} else {
357 		return Dwt.byId(this._msgBodyDivId);
358 	}
359 };
360 
361 ZmMailMsgView.prototype.setVisible =
362 function(visible, readingPaneOnRight,msg) {
363 	DwtComposite.prototype.setVisible.apply(this, arguments);
364 	var inviteMsgView = this._inviteMsgView;
365 	if (!inviteMsgView) {
366 		return;
367 	}
368 
369 	if (visible && this._msg) {
370 		if (this._msg != msg) {
371 			var dayView = inviteMsgView.getDayView();
372 			if (!dayView) {
373 				return;
374 			}
375 			dayView.setIsRight(readingPaneOnRight);
376 
377 			inviteMsgView.set(this._msg);
378 			inviteMsgView.repositionCounterToolbar(this._hdrTableId);
379 			inviteMsgView.showMoreInfo(null, null, readingPaneOnRight);
380 		}
381 	}
382 	else {
383 		inviteMsgView.reset();
384 	}
385 };
386 
387 
388 // Private / protected methods
389 
390 ZmMailMsgView.prototype._getSubscribeToolbar =
391 function(req) {
392 	if (this._subscribeToolbar) {
393 		if (AjxEnv.isIE) {
394 			//reparenting on IE does not work. So recreating in this case. (similar to bug 52412 for the invite toolbar)
395 			this._subscribeToolbar.dispose();
396 			this._subscribeToolbar = null;
397 		}
398 		else {
399 			return this._subscribeToolbar;
400 		}
401 	}
402 
403 	this._subscribeToolbar = this._getButtonToolbar([ZmOperation.SUBSCRIBE_APPROVE, ZmOperation.SUBSCRIBE_REJECT],
404 												ZmId.TB_SUBSCRIBE,
405 												this._subscribeToolBarListener.bind(this, req));
406 
407 	return this._subscribeToolbar;
408 };
409 
410 
411 
412 ZmMailMsgView.prototype._getShareToolbar =
413 function() {
414 	if (this._shareToolbar) {
415 		if (AjxEnv.isIE) {
416 			//reparenting on IE does not work. So recreating in this case. (similar to bug 52412 for the invite toolbar)
417 			this._shareToolbar.dispose();
418 			this._shareToolbar = null;
419 		}
420 		else {
421 			return this._shareToolbar;
422 		}
423 	}
424 
425 	this._shareToolbar = this._getButtonToolbar([ZmOperation.SHARE_ACCEPT, ZmOperation.SHARE_DECLINE],
426 												ZmId.TB_SHARE,
427 												this._shareToolBarListener.bind(this));
428 
429 	return this._shareToolbar;
430 };
431 
432 ZmMailMsgView.prototype._getButtonToolbar =
433 function(buttonIds, toolbarType, listener) {
434 
435 	var params = {
436 		parent: this,
437 		buttons: buttonIds,
438 		posStyle: DwtControl.STATIC_STYLE,
439 		className: "ZmShareToolBar",
440 		buttonClassName: "DwtToolbarButton",
441 		context: this._mode,
442 		toolbarType: toolbarType
443 	};
444 	var toolbar = new ZmButtonToolBar(params);
445 
446 	for (var i = 0; i < buttonIds.length; i++) {
447 		var id = buttonIds[i];
448 
449 		// HACK: IE doesn't support multiple class names.
450 		var b = toolbar.getButton(id);
451 		b._hoverClassName = b._className + "-" + DwtCssStyle.HOVER;
452 		b._activeClassName = b._className + "-" + DwtCssStyle.ACTIVE;
453 
454 		toolbar.addSelectionListener(id, listener);
455 	}
456 
457 	return toolbar;
458 };
459 
460 ZmMailMsgView.prototype._handleResponseSet =
461 function(msg, oldMsg, dayViewCallback) {
462 
463 	var bubblesCreated = false;
464 	if (this._inviteMsgView) {
465 		// always show F/B view if in stand-alone message view otherwise, check if reading pane is on
466 		if (this._inviteMsgView.isActive() && (this._controller.isReadingPaneOn() || (this._controller.isZmMsgController))) {
467 			bubblesCreated = true;
468 			appCtxt.notifyZimlets("onMsgView", [msg, oldMsg, this]);
469 			this._inviteMsgView.showMoreInfo(this._createBubbles.bind(this), dayViewCallback);
470 		}
471 		else {
472 			// resize the message view without F/B view
473 			this._inviteMsgView.resize(true);
474 		}
475     }
476 
477 	this._setTags(msg);
478 	// Remove listener for current msg if it exists
479 	if (oldMsg) {
480 		oldMsg.removeChangeListener(this._changeListener);
481 	}
482 	msg.addChangeListener(this._changeListener);
483 
484 	if (msg.cloneOf) {
485 		msg.cloneOf.addChangeListener(this._changeListener);
486 	}
487 	if (oldMsg && oldMsg.cloneOf) {
488 		oldMsg.cloneOf.removeChangeListener(this._changeListener);
489 	}
490 
491 	// reset scroll view to top most
492 	var htmlElement = this.getHtmlElement();
493 	htmlElement.scrollTop = 0;
494 	if (htmlElement.scrollTop != 0 && this._usingIframe) {
495 		/* situation that happens only on Chrome, without repro steps - bug 55775/57090 */
496 		AjxDebug.println(AjxDebug.SCROLL, "scrollTop not set to 0. scrollTop=" + htmlElement.scrollTop + " offsetHeight=" + htmlElement.offsetHeight + " scrollHeight=" + htmlElement.scrollHeight + " browser=" + navigator.userAgent);
497 		AjxDebug.dumpObj(AjxDebug.SCROLL, htmlElement.outerHTML);
498 		/*
499 			trying this hack for solution -
500 			explanation: The scroll bar does not appear if the scrollHeight of the div is bigger than the total height of the iframe and header together (i.e. if htmlElement.scrollHeight >= htmlElement.offsetHeight)
501 			If the scrollbar does not appear it's set to, and stays 0 when the scrollbar reappears due to resizing the iframe in _resetIframeHeight (which is later, I think always on timer).
502 			So what I do here is set the height of the iframe to very small (since the default is 150px), so the scroll bar disappears.
503 			it will reappear when we reset the size in _resetIframeHeight. I hope this will solve the issue.
504 		*/
505 		var iframe = this.getIframe();
506 		if (iframe) {
507 			iframe.style.height = "1px";
508 			AjxDebug.println(AjxDebug.SCROLL, "scrollTop after resetting it with the hack =" + htmlElement.scrollTop);
509 		}
510 
511 	}
512 
513 	if (!bubblesCreated) {
514 		this._createBubbles();
515 		appCtxt.notifyZimlets("onMsgView", [msg, oldMsg, this]);
516 	}
517 
518 	if (!msg.isDraft && msg.readReceiptRequested) {
519 		this._controller.sendReadReceipt(msg);
520 	}
521 };
522 
523 // This is needed for Gecko only: for some reason, clicking on a local link will
524 // open the full Zimbra chrome in the iframe :-( so we fake a scroll to the link
525 // target here. (bug 7927)
526 ZmMailMsgView.__localLinkClicked =
527 function(msgView, ev) {
528 	// note that this function is called in the context of the link ('this' is an A element)
529 	var id = this.getAttribute("href");
530 	var el = null;
531 	var doc = this.ownerDocument;
532 
533 	if (id.substr(0, 1) == "#") {
534 		id = id.substr(1);
535 		el = doc.getElementById(id);
536 		if (!el) {
537 			try {
538 				el = doc.getElementsByName(id)[0];
539 			} catch(ex) {}
540 		}
541 		if (!el) {
542 			id = decodeURIComponent(id);
543 			el = doc.getElementById(id);
544 			if (!el) {
545 				try {
546 					el = doc.getElementsByName(id)[0];
547 				} catch(ex) {}
548 			}
549 		}
550 	}
551 
552 	// attempt #1: doesn't work at all -- we're not scrolling with the IFRAME :-(
553 	// 		if (el) {
554 	// 			var pos = Dwt.getLocation(el);
555 	// 			doc.contentWindow.scrollTo(pos.x, pos.y);
556 	// 		}
557 
558 	// attempt #2: works pretty well, but the target node will showup at the bottom of the frame
559 	// 		var foo = doc.createElement("a");
560 	// 		foo.href = "#";
561 	// 		foo.innerHTML = "foo";
562 	// 		el.parentNode.insertBefore(foo, el);
563 	// 		foo.focus();
564 
565 	// the final monstrosity: scroll the containing DIV
566 	// (that is the whole msgView).  Note we have to take
567 	// into account the headers, "display images", etc --
568 	// so we add iframe.offsetTop/Left.
569 	if (el) {
570 		var div = msgView.getHtmlElement();
571 		var iframe = msgView.getIframe();
572 		var pos = Dwt.getLocation(el);
573 		div.scrollTop = pos.y + iframe.offsetTop - 20; // fuzz factor necessary for unknown reason :-(
574 		div.scrollLeft = pos.x + iframe.offsetLeft;
575 	}
576 	if (ev) {
577 		ev.stopPropagation();
578 		ev.preventDefault();
579 	}
580 	return false;
581 };
582 
583 ZmMailMsgView.prototype.hasValidHref =
584 function (node) {
585 	// Bug 22958: IE can throw when you try and get the href if it doesn't like
586 	// the value, so we wrap the test in a try/catch.
587 	// hrefs formatted like http://www.name@domain.com can cause this to happen.
588 	try {
589 		var href = node.href;
590 		return ZmMailMsgView._URL_RE.test(href) || ZmMailMsgView._MAILTO_RE.test(href);
591 	} catch (e) {
592 		return false;
593 	}
594 };
595 
596 // Dives recursively into the given DOM node.  Creates ObjectHandlers in text
597 // nodes and cleans the mess in element nodes.  Discards by default "script",
598 // "link", "object", "style", "applet" and "iframe" (most of them shouldn't
599 // even be here since (1) they belong in the <head> and (2) are discarded on
600 // the server-side, but we check, just in case..).
601 ZmMailMsgView.prototype._processHtmlDoc =
602 function() {
603 
604 	var parent = this._usingIframe ? this.getDocument() : this._containerEl;
605 	if (!parent) { return; }
606 
607 	DBG.timePt("Starting ZmMailMsgView.prototype._processHtmlDoc");
608 	// bug 8632
609 	var images = parent.getElementsByTagName("img");
610 	if (images.length > 0) {
611 		var length = images.length;
612 		for (var i = 0; i < images.length; i++) {
613 			this._checkImgInAttachments(images[i]);
614 		}
615 	}
616 
617 	//Find Zimlet Objects lazly
618 	this.lazyFindMailMsgObjects(500);
619 
620 	DBG.timePt("-- END _processHtmlDoc");
621 };
622 
623 ZmMailMsgView.prototype.lazyFindMailMsgObjects = function(interval) {
624 
625     var isSpam = (this._msg && this._msg.folderId == ZmOrganizer.ID_SPAM);
626     if (this._objectManager && !this._disposed && !isSpam) {
627 		this._lazyCreateObjectManager();
628 		this._objectsAction = new AjxTimedAction(this, this._findMailMsgObjects);
629 		AjxTimedAction.scheduleAction(this._objectsAction, ( interval || 500 ));
630 	}
631 };
632 
633 ZmMailMsgView.prototype._findMailMsgObjects =
634 function() {
635 	var doc = this.getDocument();
636 	if (doc) {
637 		var container = this.getContentContainer();
638 		this._objectManager.processObjectsInNode(doc, container);
639 	}
640 };
641 
642 ZmMailMsgView.prototype._checkImgInAttachments =
643 function(img) {
644     if (!this._msg) { return; }
645 
646     if (img.getAttribute("zmforced")){
647         img.className = "InlineImage";
648         return;
649     }
650 
651 	var attachments = this._msg.attachments;
652 	var csfeMsgFetch = appCtxt.get(ZmSetting.CSFE_MSG_FETCHER_URI);
653 	try {
654 		var src = img.getAttribute("src") || img.getAttribute("dfsrc");
655 	}
656 	catch(e) {
657 		AjxDebug.println(AjxDebug.DATA_URI, "_checkImgInAttachments: couldn't access attribute src or dfsrc");
658 	}
659 	var cid;
660 	if (/^cid:(.*)/.test(src)) {
661 		cid = "<" + RegExp.$1 + ">";
662 	}
663 
664 	for (var i = 0; i < attachments.length; i++) {
665 		var att = attachments[i];
666 
667 		if (att.foundInMsgBody) { continue; }
668 
669 		if (cid && att.contentId == cid) {
670 			att.foundInMsgBody = true;
671 			break;
672 		} else if (src && src.indexOf(csfeMsgFetch) == 0) {
673 			var mpId = src.substring(src.lastIndexOf("=") + 1);
674 			if (mpId == att.part) {
675 				att.foundInMsgBody = true;
676 				break;
677 			}
678 		} else if (att.contentLocation && src) {
679 			var filename = src.substring(src.lastIndexOf("/") + 1);
680 			if (filename == att.fileName) {
681 				att.foundInMsgBody = true;
682 				break;
683 			}
684 		}
685 	}
686 };
687 
688 ZmMailMsgView.prototype._fixMultipartRelatedImages =
689 function(msg, parent) {
690 	// fix <img> tags
691 	var images = parent.getElementsByTagName("img");
692 	var hasExternalImages = false;
693 	if (this._usingIframe) {
694 		var self = this;
695 		var onload = function() {
696 			//resize iframe onload of image
697 			ZmMailMsgView._resetIframeHeight(self);
698 			this.onload = null; // *this* is reference to <img> el.
699 		};
700 	}
701 	for (var i = 0; i < images.length; i++) {
702 		var img = images[i];
703 		var external = ZmMailMsgView._isExternalImage(img);	// has "dfsrc" attr
704 		if (!external) { //Inline image
705 			ZmMailMsgView.__unfangInternalImage(msg, img, "src", false);
706 			if (onload) {
707 				img.onload = onload;
708 			}
709 		}
710         else {
711 			img.src = "/img/zimbra/1x1-trans.png";
712 			img.setAttribute('savedDisplayMode', img.style.display);
713 			img.style.display = 'none';
714         }
715 		hasExternalImages = external || hasExternalImages;
716 	}
717 	// fix all elems with "background" attribute
718 	hasExternalImages = this._fixMultipartRelatedImagesRecurse(msg, this._usingIframe ? parent.body : parent) || hasExternalImages;
719 
720 	// did we get them all?
721 	return !hasExternalImages;
722 };
723 
724 ZmMailMsgView.prototype._fixMultipartRelatedImagesRecurse =
725 function(msg, node) {
726 
727 	var hasExternalImages = false;
728 
729 	function recurse(node){
730 		var child = node.firstChild;
731 		while (child) {
732 			if (child.nodeType == AjxUtil.ELEMENT_NODE) {
733 				hasExternalImages = ZmMailMsgView.__unfangInternalImage(msg, child, "background", true) || hasExternalImages;
734 				recurse(child);
735 			}
736 			child = child.nextSibling;
737 		}
738 	}
739 
740 	if (node.innerHTML.indexOf("dfbackground") != -1) {
741 		recurse(node);
742 	}
743 	else if (node.attributes && node.getAttribute("dfbackground") != -1) {
744 		hasExternalImages = ZmMailMsgView.__unfangInternalImage(msg, node, "background", true);	
745 	}
746 	
747 	if (!hasExternalImages && $(node).find("table[dfbackground], td[dfbackground]").length) {
748 		hasExternalImages = true;
749 	}
750 
751 	return hasExternalImages;
752 };
753 
754 /**
755  * Determines if an img element references an external image
756  * @param elem {HTMLelement}
757  * @return {Boolean} true if image is external
758  */
759 ZmMailMsgView._isExternalImage = 
760 function(elem) {
761 	if (!elem) {
762 		return false;
763 	}
764 	return Boolean(elem.getAttribute("dfsrc"));
765 }
766 
767 /**
768  * Reverses the work of the (server-side) defanger, so that images are displayed.
769  * 
770  * @param {ZmMailMsg}	msg			mail message
771  * @param {Element}		elem		element to be checked (img)
772  * @param {string}		aname		attribute name
773  * @param {boolean}		external	if true, look only for external images
774  * 
775  * @return	true if the image is external
776  */
777 ZmMailMsgView.__unfangInternalImage =
778 function(msg, elem, aname, external) {
779 	
780 	var avalue, pnsrc;
781 	try {
782 		if (external) {
783 			avalue = elem.getAttribute("df" + aname);
784 		}
785 		else {
786 			pnsrc = avalue = elem.getAttribute("pn" + aname);
787 			avalue = avalue || elem.getAttribute(aname);
788 		}
789 	}
790 	catch(e) {
791 		AjxDebug.println(AjxDebug.DATA_URI, "__unfangInternalImage: couldn't access attribute " + aname);
792 	}
793 
794 	if (avalue) {
795 		if (avalue.substr(0,4) == "cid:") {
796 			var cid = "<" + AjxStringUtil.urlComponentDecode(avalue.substr(4)) + ">"; // Parse percent-escaping per bug #52085 (especially %40 to @)
797 			avalue = msg.getContentPartAttachUrl(ZmMailMsg.CONTENT_PART_ID, cid);
798 			if (avalue) {
799 				elem.setAttribute(aname, avalue);
800 			}
801 			return false;
802 		} else if (avalue.substring(0,4) == "doc:") {
803 			avalue = [appCtxt.get(ZmSetting.REST_URL), ZmFolder.SEP, avalue.substring(4)].join('');
804 			if (avalue) {
805 				elem.setAttribute(aname, avalue);
806 				return false;
807 			}
808 		} else if (pnsrc) { // check for content-location verison
809 			avalue = msg.getContentPartAttachUrl(ZmMailMsg.CONTENT_PART_LOCATION, avalue);
810 			if (avalue) {
811 				elem.setAttribute(aname, avalue);
812 				return false;
813 			}
814 		} else if (avalue.substring(0,5) == "data:") {
815 			return false;
816 		}
817 		return true;	// not recognized as inline img
818 	}
819 	return false;
820 };
821 
822 ZmMailMsgView.prototype._createDisplayImageClickClosure =
823 function(msg, parent, id) {
824 	var self = this;
825 	return function(ev) {
826         var target = DwtUiEvent.getTarget(ev),
827             targetId = target ? target.id : null,
828             addrToAdd = "";
829         var diEl = document.getElementById(id);
830         
831         //This is required in case of the address is marked as trusted, the function is called without any target being set
832         var force = (msg && msg.showImages) ||  appCtxt.get(ZmSetting.DISPLAY_EXTERNAL_IMAGES);
833 
834         if (!force) {
835             if (!targetId) { return; }
836             if (targetId.indexOf("domain") != -1) {
837                 //clicked on domain
838                 addrToAdd = msg.sentByDomain;
839             }
840             else if (targetId.indexOf("email") != -1) {
841                 //clicked on email
842                 addrToAdd = msg.sentByAddr;
843             }
844             else if (targetId.indexOf("dispImgs") != -1) {
845                //do nothing here - just load the images
846             }
847             else if (targetId.indexOf("close") != -1) {
848 				Dwt.setVisible(diEl, false);
849                 return;
850             }
851             else {
852                 //clicked elsewhere in the info bar - DO NOTHING AND RETURN
853                 return;
854             }
855         }
856         //Create a modifyprefs req and add the addr to modify
857         if (addrToAdd) {
858             var trustedList = self.getTrustedSendersList();
859             trustedList.add(addrToAdd, null, true);
860 			var callback = self._addTrustedAddrCallback.bind(self, addrToAdd);
861 			var errorCallback = self._addTrustedAddrErrorCallback.bind(self, addrToAdd); 
862             self._controller.addTrustedAddr(trustedList.getArray(), callback, errorCallback);
863         }
864 
865 		var images = parent.getElementsByTagName("img");
866 		var onload = null;
867 		if (self._usingIframe) {
868 			onload = function() {            
869 				ZmMailMsgView._resetIframeHeight(self);
870 				this.onload = null; // *this* is reference to <img> el.
871 				DBG.println(AjxDebug.DBG3, "external image onload called for  " + this.src);
872 			};
873 		}
874 		for (var i = 0; i < images.length; i++) {
875 			var dfsrc = images[i].getAttribute("dfsrc");
876 			if (dfsrc && dfsrc.match(/https?:\/\/([-\w\.]+)+(:\d+)?(\/([\w\_\.]*(\?\S+)?)?)?/)) {
877                 images[i].onload = onload;
878 				// Fix for IE: Over HTTPS, http src urls for images might cause an issue.
879 				try {
880 					DBG.println(AjxDebug.DBG3, "displaying external images. src = " + images[i].src);
881 					images[i].src = ''; //unload it first
882 					images[i].src = images[i].getAttribute("dfsrc");
883 					DBG.println(AjxDebug.DBG3, "displaying external images. src is now = " + images[i].src);
884 				} catch (ex) {
885 					// do nothing
886 				}
887 				images[i].style.display = images[i].getAttribute('savedDisplayMode');
888 			}
889 		}
890 		//determine if any tables or table cells have an external background image
891 		var tableCells = $(parent).find("table[dfbackground], td[dfbackground]");
892 		for (var i=0; i<tableCells.length; i++) {
893 			var dfbackground = $(tableCells[i]).attr("dfbackground");
894 			if (ZmMailMsgView._URL_RE.test(dfbackground)) {
895 				$(tableCells[i]).attr("background", dfbackground);
896 			}
897 		}
898 
899 		Dwt.setVisible(diEl, false);
900 		self._htmlBody = self.getContentContainer().innerHTML;
901 		if (msg) {
902 			msg.setHtmlContent(self._htmlBody);
903 			msg.showImages = true;
904 		}
905         //Make sure the link is not followed
906         return false;
907 	};
908 };
909 
910 ZmMailMsgView.prototype._resetIframeHeightOnTimer =
911 function(attempt) {
912 	
913 	if (!this._usingIframe) { return; }
914 
915 	DBG.println(AjxDebug.DBG1, "_resetIframeHeightOnTimer attempt: " + (attempt != null ? attempt : "null"));
916 	// Because sometimes our view contains images that are slow to download, wait a
917 	// little while before resizing the iframe.
918 	var act = this._resizeAction = new AjxTimedAction(this, ZmMailMsgView._resetIframeHeight, [this, attempt]);
919 	AjxTimedAction.scheduleAction(act, 200);
920 };
921 
922 ZmMailMsgView.prototype._makeHighlightObjectsDiv =
923 function(origText) {
924 	var self = this;
925 	function func() {
926 		var div = document.getElementById(self._highlightObjectsId);
927 		div.innerHTML = ZmMsg.pleaseWaitHilitingObjects;
928 		setTimeout(function() {
929 			self.highlightObjects(origText);
930             div.parentNode.removeChild(div);
931             ZmMailMsgView._resetIframeHeight(self);
932         }, 3);
933 		return false;
934 	}
935 	// avoid closure memory leaks
936 	(function() {
937 		var infoBarDiv = document.getElementById(self._infoBarId);
938 		if (infoBarDiv) {
939 			self._highlightObjectsId = ZmId.getViewId(self._viewId, ZmId.MV_HIGHLIGHT_OBJ, self._mode);
940 			var subs = {
941 				id: self._highlightObjectsId,
942 				text: ZmMsg.objectsNotDisplayed,
943 				link: ZmMsg.hiliteObjects
944 			};
945 			var html = AjxTemplate.expand("mail.Message#InformationBar", subs);
946 			infoBarDiv.appendChild(Dwt.parseHtmlFragment(html));
947 
948 			var div = document.getElementById(subs.id+"_link");
949 			Dwt.setHandler(div, DwtEvent.ONCLICK, func);
950 		}
951 	})();
952 };
953 
954 ZmMailMsgView.prototype._stripHtmlComments =
955 function(html) {
956 	// bug: 38273 - Remove HTML Comments <!-- -->
957 	// But make sure not to remove inside style|script tags.
958 	var regex =  /<(?:!(?:--[\s\S]*?--\s*)?(>)\s*|(?:script|style|SCRIPT|STYLE)[\s\S]*?<\/(?:script|style|SCRIPT|STYLE)>)/g;
959 	html = html.replace(regex,function(m, $1) {
960 		return $1 ? '':m;
961 	});
962 	return html;
963 };
964 
965 // Returns true (the default) if we should display content in an IFRAME as opposed to a DIV.
966 ZmMailMsgView.prototype._useIframe =
967 function(isTextMsg, html, isTruncated) {
968 	return true;
969 };
970 
971 // Displays the given content in an IFRAME or a DIV.
972 ZmMailMsgView.prototype._displayContent =
973 function(params) {
974 
975 	var html = params.html || "";
976 	
977 	if (!params.isTextMsg) {
978 		//Microsoft silly smilies
979 		html = html.replace(/<span style="font-family:Wingdings">J<\/span>/g, "\u263a"); // :)
980 		html = html.replace(/<span style="font-family:Wingdings">L<\/span>/g, "\u2639"); // :(
981 	}
982 
983 	// The info bar allows the user to load external images. We show it if:
984 	// - msg is HTML
985 	// - user pref says not to show images up front, or this is Spam folder
986 	// - we're not already showing images
987 	// - there are <img> tags OR tags with dfbackground set
988 	var isSpam = (this._msg && this._msg.folderId == ZmOrganizer.ID_SPAM);
989 	var imagesNotShown = (!this._msg || !this._msg.showImages);
990 	this._needToShowInfoBar = (!params.isTextMsg &&
991 		(!appCtxt.get(ZmSetting.DISPLAY_EXTERNAL_IMAGES) || isSpam) &&
992 		imagesNotShown &&
993 		(/<img/i.test(html) || /<[^>]+dfbackground/.test(html)));
994 
995 	var displayImages;
996 	if (this._needToShowInfoBar) {
997 		displayImages = this._showInfoBar(this._infoBarId);
998 	}
999 
1000 	var callback;
1001 	var msgSize = (html.length / 1024);
1002 	var maxHighlightSize = appCtxt.get(ZmSetting.HIGHLIGHT_OBJECTS);
1003 	if (params.isTextMsg) {
1004 		if (this._objectManager) {
1005 			if (msgSize <= maxHighlightSize) {
1006 				callback = this.lazyFindMailMsgObjects.bind(this, 500);
1007 			} else {
1008 				this._makeHighlightObjectsDiv(params.origText);
1009 			}
1010 		}
1011 		if (AjxEnv.isSafari) {
1012 			html = "<html><head></head><body>" + html + "</body></html>";
1013 		}
1014 	} else {
1015 		html = this._stripHtmlComments(html);
1016 		if (this._objectManager) {
1017 			var images = html.match(/<img[^>]+>/ig);
1018 			msgSize = (images) ? msgSize - (images.join().length / 1024) : msgSize; // Excluding images in the message
1019 			
1020 			if (msgSize <= maxHighlightSize) {
1021 				callback = this._processHtmlDoc.bind(this);
1022 			} else {
1023 				this._makeHighlightObjectsDiv();
1024 			}
1025 		}
1026 	}
1027 
1028 	var msgTruncated;
1029 	this._isMsgTruncated = false;
1030 	if (params.isTruncated) {
1031 		this._isMsgTruncated = true;
1032 		var msgTruncatedDiv = document.getElementById(this._msgTruncatedId);
1033 		if (!msgTruncatedDiv) {
1034 			var infoBarDiv = document.getElementById(this._infoBarId);
1035 			if (infoBarDiv) {
1036 				var subs = {
1037 					id: this._msgTruncatedId,
1038 					text: ZmMsg.messageTooLarge,
1039 					link: ZmMsg.viewEntireMessage
1040 				};
1041 				var msgTruncatedHtml = AjxTemplate.expand("mail.Message#InformationBar", subs);
1042 				msgTruncated = Dwt.parseHtmlFragment(msgTruncatedHtml);
1043 				infoBarDiv.appendChild(msgTruncated);
1044 				Dwt.setHandler(msgTruncated, DwtEvent.ONCLICK, this._handleMsgTruncated.bind(this));
1045 			}
1046 		}
1047 	}
1048 
1049 	this._msgBodyDivId = [this._htmlElId, ZmId.MV_MSG_BODY].join("_");
1050 	this._bodyTabGroup.removeAllMembers();
1051 	
1052 	this._usingIframe = this._useIframe(params.isTextMsg, html, params.isTruncated);
1053 	DBG.println(AjxDebug.DBG1, "Use IFRAME: " + this._usingIframe);
1054 	
1055 	if (this._usingIframe) {
1056 		// bug fix #9475 - IE isnt resolving MsgBody class in iframe so set styles explicitly
1057 		var inner_styles = AjxEnv.isIE ? ".MsgBody-text, .MsgBody-text * { font: 10pt monospace; }" : "";
1058 		var params1 = {
1059 			parent:					this,
1060 			parentElement:			params.container,
1061 			index:					params.index,
1062 			className:				this._getBodyClass(),
1063 			id:						this._msgBodyDivId,
1064 			hidden:					true,
1065 			html:					"<div>" + (this._cleanedHtml || html) + "</div>",
1066 			styles:					inner_styles,
1067 			noscroll:				!this._scrollWithIframe,
1068 			posStyle:				DwtControl.STATIC_STYLE,
1069 			processHtmlCallback:	callback,
1070 			useKbMgmt:				true,
1071 			title:                  this._getIframeTitle()
1072 		};
1073 
1074 		// TODO: cache iframes
1075 		var ifw = this._ifw = new DwtIframe(params1);
1076 		if (ifw.initFailed) {
1077 			AjxDebug.println(AjxDebug.MSG_DISPLAY, "Message display: IFRAME was not ready");
1078 			appCtxt.setStatusMsg(ZmMsg.messageDisplayProblem);
1079 			return;
1080 		}
1081 		this._iframeId = ifw.getIframe().id;
1082 
1083 		var idoc = ifw.getDocument();
1084 
1085 		if (AjxEnv.isGeckoBased) {
1086 			// patch local links (pass null as object so it gets called in context of link)
1087 			var geckoScrollCallback = ZmMailMsgView.__localLinkClicked.bind(null, this);
1088 			var links = idoc.getElementsByTagName("a");
1089 			for (var i = links.length; --i >= 0;) {
1090 				var link = links[i];
1091 				if (!link.target) {
1092 					link.onclick = geckoScrollCallback; // has chances to be a local link
1093 				}
1094 			}
1095 		}
1096 
1097 		//update root html elment class to reflect user selected font size - so that if we use our relative font size properties in CSS inside (stuff from msgview.css) it would be relative to this and not to the browser default.
1098 		Dwt.addClass(idoc.documentElement, "user_font_size_" + appCtxt.get(ZmSetting.FONT_SIZE));
1099 		Dwt.addClass(idoc.documentElement, "user_font_" + appCtxt.get(ZmSetting.FONT_NAME));
1100 
1101 		// assign the right class name to the iframe body
1102 		idoc.body.className = this._getBodyClass() + (params.isTextMsg ? " MsgBody-text" : " MsgBody-html");
1103 
1104 		idoc.body.style.height = "auto"; //see bug 56899 - if the body has height such as 100% or 94%, it causes a problem in FF in calcualting the iframe height. Make sure the height is clear.
1105 
1106 		ifw.getIframe().onload = this._onloadIframe.bind(this, ifw);
1107 
1108 		// import the object styles
1109 		var head = idoc.getElementsByTagName("head")[0];
1110 		if (!head) {
1111 			head = idoc.createElement("head");
1112 			idoc.body.parentNode.insertBefore(head, idoc.body);
1113 		}
1114 	
1115 		if (!ZmMailMsgView._CSS) {
1116 			// Make a synchronous request for the CSS. Should we do this earlier?
1117 			var cssUrl = [appContextPath, "/css/msgview.css?v=", cacheKillerVersion, "&locale=", window.appRequestLocaleId, "&skin=", window.appCurrentSkin].join("");
1118 			if (AjxEnv.supported.localstorage) {
1119 				ZmMailMsgView._CSS = localStorage.getItem(cssUrl);
1120 			}
1121 			if (!ZmMailMsgView._CSS) {
1122 				var result = AjxRpc.invoke(null, cssUrl, null, null, true);
1123 				ZmMailMsgView._CSS = result && result.text;
1124 			}
1125 		}
1126 		var style = document.createElement('style');
1127 		var rules = document.createTextNode(ZmMailMsgView._CSS);
1128 		style.type = 'text/css';
1129 		if (style.styleSheet) {
1130 			style.styleSheet.cssText = rules.nodeValue;
1131 		}
1132 		else {
1133 			style.appendChild(rules);
1134 		}
1135 		head.appendChild(style);
1136 	
1137 		ifw.getIframe().style.visibility = "";
1138 
1139 		this._bodyTabGroup.addMember(ifw);
1140 
1141 	}
1142 	else {
1143 		var div = this._containerEl = document.createElement("div");
1144 		div.id = this._msgBodyDivId;
1145 		div.className = "MsgBody MsgBody-" + (params.isTextMsg ? "text" : "html");
1146 		var parent = this.getHtmlElement();
1147 		if (!parent) {
1148 			AjxDebug.println(AjxDebug.MSG_DISPLAY, "Message display: DIV was not ready");
1149 			appCtxt.setStatusMsg(ZmMsg.messageDisplayProblem);
1150 			return;
1151 		}
1152 		if (params.index != null) {
1153 			parent.insertBefore(div, parent.childNodes[params.index])
1154 		}
1155 		else {
1156 			parent.appendChild(div);
1157 		}
1158 		div.innerHTML = this._cleanedHtml || html;
1159 
1160 		this._makeFocusable(div);
1161 		this._bodyTabGroup.addMember(div);
1162 	}
1163 
1164 	if (!params.isTextMsg) {
1165 		this._htmlBody = this.getContentContainer().innerHTML;
1166 
1167 		// TODO: only call this if top-level is multipart/related?
1168 		// setup the click handler for the images
1169 		var didAllImages = this._fixMultipartRelatedImages(this._msg, idoc || this._containerEl);
1170 		if (didAllImages) {
1171 			Dwt.setVisible(displayImages, false);
1172 			this._needToShowInfoBar = false;
1173 		} else {
1174 			this._setupInfoBarClicks(displayImages);
1175 		}
1176 	}
1177 
1178 	this._resetIframeHeightOnTimer();
1179 	if (callback) {
1180 		callback.run();
1181 	}
1182 };
1183 ZmMailMsgView.prototype._makeIframeProxy = ZmMailMsgView.prototype._displayContent;
1184 
1185 ZmMailMsgView.prototype._showInfoBar =
1186 function(parentEl, html, isTextMsg) {
1187 
1188 	parentEl = (typeof(parentEl) == "string") ? document.getElementById(parentEl) : parentEl;
1189 	if (!parentEl) { return; }
1190 	
1191 	// prevent appending the "Display Images" info bar more than once
1192 	var displayImages;
1193 	var dispImagesDiv = document.getElementById(this._displayImagesId);
1194 	if (!dispImagesDiv) {
1195 		if (parentEl) {
1196 			var subs = {
1197 				id:			this._displayImagesId,
1198 				text:		ZmMsg.externalImages,
1199 				link:		ZmMsg.displayExternalImages,
1200 				alwaysText:	ZmMsg.alwaysDisplayExternalImages,
1201 				domain:		this._msg.sentByDomain,
1202 				email:		this._msg.sentByAddr,
1203 				or:			ZmMsg.or
1204 			};
1205 			var extImagesHtml = AjxTemplate.expand("mail.Message#ExtImageInformationBar", subs);
1206 			displayImages = Dwt.parseHtmlFragment(extImagesHtml);
1207 			parentEl.appendChild(displayImages);
1208 		}
1209 	}
1210 	return displayImages;
1211 };
1212 
1213 ZmMailMsgView.prototype._setupInfoBarClicks =
1214 function(displayImages) {
1215 
1216 	var parent = this._usingIframe ? this.getDocument() : this._containerEl;
1217 	var func = this._createDisplayImageClickClosure(this._msg, parent, this._displayImagesId);
1218 	if (displayImages) {
1219 		Dwt.setHandler(displayImages, DwtEvent.ONCLICK, func);
1220 	}
1221 	else if (appCtxt.get(ZmSetting.DISPLAY_EXTERNAL_IMAGES) ||
1222 			 (this._msg && this._msg.showImages))
1223 	{
1224 		func.call();
1225 	}
1226 };
1227 
1228 ZmMailMsgView.prototype._getBodyClass =
1229 function() {
1230 	return "MsgBody";
1231 };
1232 
1233 ZmMailMsgView.prototype._addTrustedAddrCallback =
1234 function(addr) {
1235     this.getTrustedSendersList().add(addr, null, true);
1236     appCtxt.set(ZmSetting.TRUSTED_ADDR_LIST, this.getTrustedSendersList().getArray());
1237     var prefApp = appCtxt.getApp(ZmApp.PREFERENCES);
1238     var func = prefApp && prefApp["refresh"];
1239     if (func && (typeof(func) == "function")) {
1240         func.apply(prefApp, [null, addr]);
1241     }
1242 };
1243 
1244 ZmMailMsgView.prototype._addTrustedAddrErrorCallback =
1245 function(addr) {
1246     this.getTrustedSendersList().remove(addr);
1247 };
1248 
1249 ZmMailMsgView.prototype._isTrustedSender =
1250 function(msg) {
1251     var trustedList = this.getTrustedSendersList();
1252     if (trustedList.contains(msg.sentByAddr.toLowerCase()) || trustedList.contains(msg.sentByDomain.toLowerCase())){
1253         return true;
1254     }
1255     return false;
1256 };
1257 
1258 ZmMailMsgView.prototype.getTrustedSendersList =
1259 function() {
1260     return this._controller.getApp().getTrustedSendersList();
1261 };
1262 
1263 ZmMailMsgView.showMore =
1264 function(elementId, type) {
1265 
1266 	var showMore = document.getElementById(this._getShowMoreId(elementId, type));
1267 	if (showMore) {
1268 		Dwt.setVisible(showMore, false);
1269 	}
1270 	var more = document.getElementById(this._getMoreId(elementId, type));
1271 	if (more) {
1272 		more.style.display = "inline";
1273 	}
1274 };
1275 
1276 ZmMailMsgView._getShowMoreId =
1277 function(elementId, type) {
1278 	return elementId + 'showmore_' + type;
1279 };
1280 
1281 ZmMailMsgView._getMoreId =
1282 function(elementId, type) {
1283 	return elementId + 'more_addrs_' + type;
1284 };
1285 
1286 /**
1287  *
1288  * formats the array of addresses as HTML with possible "show more" expand link if more than a certain number of addresses are in the field.
1289  *
1290  * @param addrs array of addresses
1291  * @param options
1292  * @param type some type identifier (one per page)
1293  * @param om {ZmObjectManager}
1294  * @param htmlElId - unique view id so it works with multiple views open.
1295  *
1296  * returns object with the html and ShowMore link id
1297  */
1298 ZmMailMsgView.prototype.getAddressesFieldHtmlHelper =
1299 function(addrs, options, type) {
1300 
1301 	var addressInfo = {};
1302 	var idx = 0, parts = [];
1303 
1304 	for (var i = 0; i < addrs.length; i++) {
1305 		if (i > 0) {
1306 			// no need for separator since we're showing addr bubbles
1307 			parts[idx++] = " ";
1308 		}
1309 
1310 		if (i == ZmMailMsgView.MAX_ADDRESSES_IN_FIELD) {
1311 			var showMoreId = ZmMailMsgView._getShowMoreId(this._htmlElId, type);
1312 			addressInfo.showMoreLinkId = showMoreId + "_link";
1313 			var moreId = ZmMailMsgView._getMoreId(this._htmlElId, type);
1314 			parts[idx++] = "<span id='" + showMoreId + "' style='white-space:nowrap'> ";
1315 			parts[idx++] = "<a id='" + addressInfo.showMoreLinkId + "' href='' onclick='ZmMailMsgView.showMore(\"" + this._htmlElId + "\", \"" + type + "\"); return false;'>";
1316 			parts[idx++] = ZmMsg.showMore;
1317 			parts[idx++] = "</a></span><span style='display:none;' id='" + moreId + "'>";
1318 		}
1319 		var email = addrs[i];
1320 		if (email.address) {
1321 			parts[idx++] = this._getBubbleHtml(email, options);
1322 		}
1323 		else {
1324 			parts[idx++] = AjxStringUtil.htmlEncode(email.name);
1325 		}
1326 	}
1327 	if (addressInfo.showMoreLinkId) {
1328 		parts[idx++] = "</span>";
1329 	}
1330 	addressInfo.html =  parts.join("");
1331 	return addressInfo;
1332 };
1333 
1334 ZmMailMsgView.prototype._getBubbleHtml = function(addr, options) {
1335 	if (!addr) {
1336 		return "";
1337 	}
1338 
1339 	options = options || {};
1340 
1341 	addr = addr.isAjxEmailAddress ? addr : new AjxEmailAddress(addr);
1342 
1343 	var canExpand = addr.isGroup && addr.canExpand && appCtxt.get("EXPAND_DL_ENABLED"),
1344 		ctlr = this._controller;
1345 
1346 	if (canExpand && !this._aclv) {
1347 		// create a hidden ZmAutocompleteListView to handle DL expansion
1348 		var aclvParams = {
1349 			dataClass:		    appCtxt.getAutocompleter(),
1350 			matchValue:		    ZmAutocomplete.AC_VALUE_FULL,
1351 			options:		    { massDLComplete:true },
1352 			selectionCallback:	ctlr._dlAddrSelected.bind(ctlr),
1353 			contextId:		    this.toString()
1354 		};
1355 		this._aclv = new ZmAutocompleteListView(aclvParams);
1356 	}
1357 
1358 	// We'll be creating controls (bubbles) later, so we provide the tooltip now and let the control manage
1359 	// it instead of the zimlet framework.
1360 	var id = ZmId.create({
1361 		app:            ZmId.APP_MAIL,
1362 		containingView: this._viewId,
1363 		field:          ZmId.FLD_PARTICIPANT
1364 	});
1365 
1366 	var bubbleParams = {
1367 		parent:		appCtxt.getShell(),
1368 		parentId:	this._htmlElId,
1369 		addrObj:	addr,
1370 		id:			id,
1371 		canExpand:	canExpand,
1372 		email:		addr.address
1373 	};
1374 	ZmAddressInputField.BUBBLE_OBJ_ID[id] = this._htmlElId;	// pretend to be a ZmAddressInputField for DL expansion
1375 	this._bubbleParams[id] = bubbleParams;
1376 
1377 	return "<span id='" + id + "'></span>";
1378 };
1379 
1380 ZmMailMsgView.prototype._clearBubbles = function() {
1381 
1382 	if (this._bubbleList) {
1383 		this._bubbleList.clear();
1384 	}
1385 	this._bubbleList = new ZmAddressBubbleList();
1386 	var ctlr = this._controller;
1387 	this._bubbleList.addSelectionListener(ctlr._bubbleSelectionListener.bind(ctlr));
1388 	this._bubbleList.addActionListener(ctlr._bubbleActionListener.bind(ctlr));
1389 	this._bubbleParams = {};
1390 };
1391 
1392 ZmMailMsgView.prototype._createBubbles = function() {
1393 
1394 	for (var id in this._bubbleParams) {
1395 		// make sure SPAN was actually added to DOM (may have been ignored by template, for example)
1396 		if (!document.getElementById(id)) {
1397 			continue;
1398 		}
1399 		var bubbleParams = this._bubbleParams[id];
1400 		if (bubbleParams.created) {
1401 			continue;
1402 		}
1403 		bubbleParams.created = true;
1404 		var bubble = new ZmAddressBubble(bubbleParams);
1405 		bubble.replaceElement(id);
1406 		if (this._bubbleList) {
1407 			this._bubbleList.add(bubble);
1408 			this._headerTabGroup.addMember(bubble);
1409 		}
1410 	}
1411 };
1412 
1413 /**
1414  *
1415  * formats the array of addresses as HTML with possible "show more" expand link if more than a certain number of addresses are in the field.
1416  *
1417  * @param addrs array of addresses
1418  * @param options
1419  * @param type some type identifier (one per page)
1420  *
1421  * returns object with the html and ShowMore link id
1422  */
1423 ZmMailMsgView.prototype.getAddressesFieldInfo =
1424 function(addrs, options, type, htmlElId) {
1425 	return this.getAddressesFieldHtmlHelper(addrs, options, type, this._objectManager, htmlElId || this._htmlElId);
1426 };
1427 
1428 ZmMailMsgView.prototype._renderMessage =
1429 function(msg, container, callback) {
1430 	
1431 	this._renderMessageHeader(msg, container);
1432 	this._renderMessageBody(msg, container, callback);
1433 	this._renderMessageFooter(msg, container);
1434 	Dwt.setLoadedTime("ZmMailItem");
1435 };
1436 
1437 ZmMailMsgView.prototype._renderMessageHeader =
1438 function(msg, container, doNotClearBubbles) {
1439 
1440 	if (!doNotClearBubbles) {
1441 		this._clearBubbles();
1442 	}
1443 
1444 	this._renderInviteToolbar(msg, container);
1445 	
1446 	var ai = this._getAddrInfo(msg);
1447 	
1448 	var subject = AjxStringUtil.htmlEncode(msg.subject || ZmMsg.noSubject);
1449 	var dateFormatter = AjxDateFormat.getDateTimeInstance(AjxDateFormat.LONG, AjxDateFormat.SHORT);
1450 	// bug fix #31512 - if no sent date then display received date
1451 	var date = new Date(msg.sentDate || msg.date);
1452 	var dateString = dateFormatter.format(date);
1453 
1454 	var additionalHdrs = [];
1455 	var invite = msg.invite;
1456 	var autoSendTime = AjxUtil.isDate(msg.autoSendTime) ? AjxDateFormat.getDateTimeInstance(AjxDateFormat.FULL, AjxDateFormat.MEDIUM).format(msg.autoSendTime) : null;
1457 
1458 	if (msg.attrs) {
1459 		for (var hdrName in ZmMailMsgView.displayAdditionalHdrsInMsgView) {
1460 			if (msg.attrs[hdrName]) {
1461 				additionalHdrs.push({hdrName:ZmMailMsgView.displayAdditionalHdrsInMsgView[hdrName], hdrVal: msg.attrs[hdrName]});
1462 			}
1463 		}
1464 	}
1465 
1466 	var options = {};
1467 	options.shortAddress = appCtxt.get(ZmSetting.SHORT_ADDRESS);
1468 	
1469 	var attachmentsCount = msg.getAttachmentCount(true);
1470 
1471 	// do we add a close button in the header section?
1472 
1473 	var folder = appCtxt.getById(msg.folderId);
1474 	var isSyncFailureMsg = (folder && folder.nId == ZmOrganizer.ID_SYNC_FAILURES);
1475     if (!msg.showImages) {
1476         msg.showImages = folder && folder.isFeed();
1477     }
1478 
1479 	this._hdrTableId		= ZmId.getViewId(this._viewId, ZmId.MV_HDR_TABLE, this._mode);
1480 	var reportBtnCellId		= ZmId.getViewId(this._viewId, ZmId.MV_REPORT_BTN_CELL, this._mode);
1481 	this._expandRowId		= ZmId.getViewId(this._viewId, ZmId.MV_EXPAND_ROW, this._mode);
1482 
1483 	// the message view adapts to whatever height the image has, but
1484 	// more than 96 pixels is a bit silly...
1485 	var imageURL = ai.sentByContact && ai.sentByContact.getImageUrl(48, 96),
1486 		imageAltText = imageURL && ai.sentByContact && ai.sentByContact.getFullName();
1487 
1488 	var subs = {
1489 		id: 				this._htmlElId,
1490 		hdrTableId: 		this._hdrTableId,
1491 		hdrTableTopRowId:	ZmId.getViewId(this._viewId, ZmId.MV_HDR_TABLE_TOP_ROW, this._mode),
1492 		expandRowId:		this._expandRowId,
1493 		attachId:			this._attLinksId,
1494 		infoBarId:			this._infoBarId,
1495 		subject:			subject,
1496 		imageURL:			imageURL || ZmZimbraMail.DEFAULT_CONTACT_ICON,
1497 		imageAltText:		imageAltText || ZmMsg.noContactImage,
1498 		dateString:			dateString,
1499 		hasAttachments:		(attachmentsCount != 0),
1500 		attachmentsCount:	attachmentsCount,
1501 		bwo:                ai.bwo,
1502 		bwoAddr:            ai.bwoAddr,
1503 		bwoId:              ZmId.getViewId(this._viewId, ZmId.CMP_BWO_SPAN, this._mode)
1504 	};
1505 
1506 	if (msg.isHighPriority || msg.isLowPriority) {
1507 		subs.priority =			msg.isHighPriority ? "high" : "low";
1508 		subs.priorityImg =		msg.isHighPriority ? "ImgPriorityHigh_list" : "ImgPriorityLow_list";
1509 		subs.priorityDivId =	ZmId.getViewId(this._view, ZmId.MV_PRIORITY);
1510 	}
1511 
1512 	if (invite && !invite.isEmpty() && this._inviteMsgView) {
1513 		this._getInviteSubs(subs, ai.sentBy, ai.sentByAddr, ai.sender ? ai.fromAddr : null);
1514 	}
1515 	else {
1516 		subs.sentBy = ai.sentBy;
1517 		subs.sentByNormal = ai.sentByAddr;
1518 		subs.sentByAddr = ai.sentByAddr;
1519 		subs.obo = ai.obo;
1520 		subs.oboAddr = ai.oboAddr;
1521 		subs.oboId = ZmId.getViewId(this._viewId, ZmId.CMP_OBO_SPAN, this._mode)
1522 		subs.addressTypes = ai.addressTypes;
1523 		subs.participants = ai.participants;
1524 		subs.reportBtnCellId = reportBtnCellId;
1525 		subs.isSyncFailureMsg = isSyncFailureMsg;
1526 		subs.autoSendTime = autoSendTime;
1527 		subs.additionalHdrs = additionalHdrs;
1528 		subs.isOutDated = invite && invite.isEmpty();
1529 	}
1530 
1531 	var template = (invite && !invite.isEmpty() && this._inviteMsgView)
1532 		? "mail.Message#InviteHeader" : "mail.Message#MessageHeader";
1533 	var html = AjxTemplate.expand(template, subs);
1534 
1535 	var el = container || this.getHtmlElement();
1536 	el.setAttribute('aria-label', subject);
1537 	el.appendChild(Dwt.parseHtmlFragment(html));
1538 	this._headerElement = Dwt.byId(this._htmlElId + "_headerElement");
1539 	this._makeFocusable(this._headerElement);
1540 
1541 	this._headerTabGroup.removeAllMembers();
1542 	this._headerTabGroup.addMember(this._headerElement);
1543 
1544     if (this._inviteMsgView) {
1545         if (this._inviteToolbarCellId && this._inviteToolbarCellId && this._inviteMsgView._inviteToolbar) {
1546             this._inviteMsgView._inviteToolbar.reparentHtmlElement(this._inviteToolbarCellId, 0);
1547         }
1548         if (this._calendarSelectCellId && this._inviteMsgView._inviteMoveSelect) {
1549             this._inviteMsgView._inviteMoveSelect.reparentHtmlElement(this._calendarSelectCellId, 0);
1550         }
1551         this._inviteMsgView.repositionCounterToolbar(this._hdrTableId);
1552 		this._headerTabGroup.addMember(this._inviteMsgView._inviteToolbar);
1553     }
1554 
1555 
1556 	/**************************************************************************/
1557 	/* Add to DOM based on Id's used to generate HTML via templates           */
1558 	/**************************************************************************/
1559 	// add the report button if applicable
1560 	var reportBtnCell = document.getElementById(reportBtnCellId);
1561 	if (reportBtnCell) {
1562 		var id = ZmId.getButtonId(this._mode, ZmId.REPORT, ZmId.MSG_VIEW);
1563 		var reportBtn = new DwtButton({parent:this, id:id, parentElement:reportBtnCell});
1564 		reportBtn.setText(ZmMsg.reportSyncFailure);
1565 		reportBtn.addSelectionListener(this._reportButtonListener.bind(this, msg));
1566 	}
1567 
1568 	if (this._hasShareToolbar) {
1569 		var topToolbar = this._getShareToolbar();
1570 		topToolbar.reparentHtmlElement(container);
1571 		topToolbar.setVisible(Dwt.DISPLAY_BLOCK);
1572 		this._headerTabGroup.addMember(topToolbar);
1573 	}
1574 };
1575 
1576 // Returns a hash with what we need to show the message's address headers
1577 ZmMailMsgView.prototype._getAddrInfo =
1578 function(msg) {
1579 	
1580 	var acctId = appCtxt.getActiveAccount().id;
1581 	var cl;
1582 	if (appCtxt.get(ZmSetting.CONTACTS_ENABLED) && appCtxt.getApp(ZmApp.CONTACTS).contactsLoaded[acctId]) {
1583 		cl = AjxDispatcher.run("GetContacts");
1584 	}
1585 	var fromAddr = msg.getAddress(AjxEmailAddress.FROM);
1586 	// if we have no FROM address and msg is in an outbound folder, assume current user is the sender
1587 	if (!fromAddr) {
1588 		var folder = msg.folderId && appCtxt.getById(msg.folderId);
1589 		if (folder && folder.isOutbound()) {
1590 			var identity = appCtxt.getIdentityCollection().defaultIdentity;
1591 			if (identity) {
1592 				fromAddr = new AjxEmailAddress(identity.sendFromAddress, AjxEmailAddress.FROM, identity.sendFromDisplay);
1593 			}
1594 		}
1595 	}
1596 	var sender = msg.getAddress(AjxEmailAddress.SENDER); // bug fix #10652 - Sender: header means on-behalf-of
1597 	var sentBy = (sender && sender.address) ? sender : fromAddr;
1598 	var from = AjxStringUtil.htmlEncode(fromAddr ? fromAddr.toString(true) : ZmMsg.unknown);
1599 	var sentByAddr = sentBy && sentBy.getAddress();
1600     if (sentByAddr) {
1601         msg.sentByAddr = sentByAddr;
1602         msg.sentByDomain = sentByAddr.substr(sentByAddr.indexOf("@") + 1);
1603         msg.showImages = this._isTrustedSender(msg);
1604     }
1605 	var sentByContact = cl && cl.getContactByEmail(sentBy && sentBy.getAddress()); //bug 78163 originally
1606 	var obo = sender ? fromAddr : null;
1607 	var oboAddr = obo && obo.getAddress();
1608 
1609 	var bwo = msg.getAddress(AjxEmailAddress.RESENT_FROM);
1610 	var bwoAddr = bwo ? bwo.getAddress() : null;
1611 	
1612 	// find addresses we may need to search for contacts for, so that we can
1613 	// aggregate them into a single search
1614 	var contactsApp = appCtxt.getApp(ZmApp.CONTACTS);
1615 	if (contactsApp) {
1616 		var lookupAddrs = [];
1617 		if (sentBy) { lookupAddrs.push(sentBy); }
1618 		if (obo) { lookupAddrs.push(obo); }
1619 		for (var i = 1; i < ZmMailMsg.ADDRS.length; i++) {
1620 			var type = ZmMailMsg.ADDRS[i];
1621 			if ((type == AjxEmailAddress.SENDER) || (type == AjxEmailAddress.RESENT_FROM)) { continue; }
1622 			var addrs = msg.getAddresses(type).getArray();
1623 			for (var j = 0; j < addrs.length; j++) {
1624 				if (addrs[j]) {
1625 					lookupAddrs.push(addrs[j].address);
1626 				}
1627 			}
1628 		}
1629 		if (lookupAddrs.length > 1) {
1630 			contactsApp.setAddrLookupGroup(lookupAddrs);
1631 		}
1632 	}
1633 
1634 	var options = {};
1635 	options.shortAddress = appCtxt.get(ZmSetting.SHORT_ADDRESS);
1636 
1637 	if (this._objectManager) {
1638 		this._lazyCreateObjectManager();
1639 		appCtxt.notifyZimlets("onFindMsgObjects", [msg, this._objectManager, this]);
1640 	}
1641 
1642 	sentBy = this._getBubbleHtml(sentBy);
1643 	obo = obo && this._getBubbleHtml(fromAddr);
1644 	bwo = bwo && this._getBubbleHtml(bwo);
1645 
1646 	var showMoreIds = {};
1647 	var addressTypes = [], participants = {};
1648 	for (var i = 0; i < ZmMailMsg.ADDRS.length; i++) {
1649 		var type = ZmMailMsg.ADDRS[i];
1650 		if ((type == AjxEmailAddress.FROM) || (type == AjxEmailAddress.SENDER) || (type == AjxEmailAddress.RESENT_FROM)) { continue; }
1651 
1652 		var addrs = AjxEmailAddress.dedup(msg.getAddresses(type).getArray());
1653 
1654         if (type == AjxEmailAddress.REPLY_TO){  // bug: 79175 - Reply To shouldn't be shown when it matches From
1655             var k = addrs.length;
1656             for (var j = 0; j < k;){
1657                 if (addrs[j].address === fromAddr.address){
1658                     addrs.splice(j,1);
1659                     k--;
1660                 }
1661                 else {
1662                     j++;
1663                 }
1664             }
1665         }
1666 
1667 		if (addrs.length > 0) {
1668 			var prefix = AjxStringUtil.htmlEncode(ZmMsg[AjxEmailAddress.TYPE_STRING[type]]);
1669 			var addressInfo = this.getAddressesFieldInfo(addrs, options, type);
1670 			addressTypes.push(type);
1671 			participants[type] = { prefix: prefix, partStr: addressInfo.html };
1672 			if (addressInfo.showMoreLinkId) {
1673 			    showMoreIds[addressInfo.showMoreLinkId] = true;
1674 			}
1675 		}
1676 	}
1677 	
1678 	return {
1679 		fromAddr:		fromAddr,
1680 		from:			from,
1681 		sender:			sender,
1682 		sentBy:			sentBy,
1683 		sentByAddr:		sentByAddr,
1684 		sentByContact:	sentByContact,
1685 		obo:			obo,
1686 		oboAddr:		oboAddr,
1687 		bwo:			bwo,
1688 		bwoAddr:		bwoAddr,
1689 		addressTypes:	addressTypes,
1690 		participants:	participants,
1691         showMoreIds:    showMoreIds
1692 	};
1693 };
1694 
1695 ZmMailMsgView.prototype._getInviteSubs =
1696 function(subs, sentBy, sentByAddr, sender, addr) {
1697 	this._inviteMsgView.addSubs(subs, sentBy, sentByAddr, sender ? addr : null);
1698     var imv = this._inviteMsgView;
1699     if (imv._inviteToolbar && imv._inviteToolbar.getVisible()) {
1700         subs.toolbarCellId = this._inviteToolbarCellId =
1701             [this._viewId, "inviteToolbarCell"].join("_");
1702     }
1703     if (imv._inviteMoveSelect && imv._inviteMoveSelect.getVisible()) {
1704         subs.calendarSelectCellId = this._calendarSelectCellId =
1705             [this._viewId, "calendarSelectToolbarCell"].join("_");
1706     }
1707 };
1708 
1709 ZmMailMsgView.prototype._renderInviteToolbar =
1710 function(msg, container) {
1711 
1712 	this._dateObjectHandlerDate = new Date(msg.sentDate || msg.date);
1713 	this._hasShareToolbar = this._hasSubToolbar = false;
1714 
1715 	var invite = msg.invite;
1716 	var ac = window.parentAppCtxt || window.appCtxt;
1717 
1718 	if ((ac.get(ZmSetting.CALENDAR_ENABLED) || ac.multiAccounts) && 
1719 		(invite && !invite.isEmpty() && invite.type != "task"))
1720 	{
1721 		if (!this._inviteMsgView) {
1722 			this._inviteMsgView = new ZmInviteMsgView({parent:this, mode:this._mode});
1723 		}
1724 		this._inviteMsgView.set(msg);
1725 	}
1726 	else if (appCtxt.get(ZmSetting.SHARING_ENABLED) && msg.share &&
1727              ZmOrganizer.normalizeId(msg.folderId) != ZmFolder.ID_TRASH &&
1728              ZmOrganizer.normalizeId(msg.folderId) != ZmFolder.ID_SENT &&
1729              appCtxt.getActiveAccount().id != msg.share.grantor.id)
1730 	{
1731 		AjxDispatcher.require("Share");
1732 		var action = msg.share.action;
1733 		var isNew = action == ZmShare.NEW;
1734 		var isEdit = action == ZmShare.EDIT;
1735 		var folder = appCtxt.getById(msg.folderId);
1736 		var isDataSource = (folder && folder.isDataSource(null, true) && (msg.folderId != ZmFolder.ID_INBOX));
1737 
1738 		if (!isDataSource &&
1739 			(isNew || (isEdit && !this.__hasMountpoint(msg.share))) &&
1740 			msg.share.link.perm)
1741 		{
1742 			this._hasShareToolbar = true;
1743 		}
1744 	}
1745 	else if (msg.subscribeReq && msg.folderId != ZmFolder.ID_TRASH) {
1746 		var topToolbar = this._getSubscribeToolbar(msg.subscribeReq);
1747 		topToolbar.reparentHtmlElement(container);
1748 		topToolbar.setVisible(Dwt.DISPLAY_BLOCK);
1749 		this._hasSubToolbar = true;
1750 	}
1751 };
1752 
1753 /**
1754  * Renders the message body. There is a chance a server call will be made to fetch an alternative part.
1755  * 
1756  * @param {ZmMailMsg}	msg
1757  * @param {Element}		container
1758  * @param {callback}	callback
1759  */
1760 ZmMailMsgView.prototype._renderMessageBody =
1761 function(msg, container, callback, index) {
1762 
1763 	var htmlMode = appCtxt.get(ZmSetting.VIEW_AS_HTML);
1764 	var contentType = htmlMode ? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN;
1765 	msg.getBodyPart(contentType, this._renderMessageBody1.bind(this, {
1766         msg:        msg,
1767         container:  container,
1768         callback:   callback,
1769         index:      index
1770     }));
1771 };
1772 
1773 // The second argument 'part' is added to the callback by getBodyPart() above. We ignore it
1774 // and just get the body parts from the loaded msg.
1775 ZmMailMsgView.prototype._renderMessageBody1 = function(params, part) {
1776 
1777 	var msg = params.msg,
1778 	    htmlMode = appCtxt.get(ZmSetting.VIEW_AS_HTML),
1779 	    preferredContentType = params.forceType || (htmlMode ? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN),
1780         hasHtmlPart = (preferredContentType === ZmMimeTable.TEXT_HTML && msg.hasContentType(ZmMimeTable.TEXT_HTML)) || msg.hasInlineImage(),
1781         hasMultipleBodyParts = msg.hasMultipleBodyParts(),
1782         bodyParts = hasMultipleBodyParts ? msg.getBodyParts(preferredContentType) : [ msg.getBodyPart(preferredContentType) || msg.getBodyPart() ],
1783         invite = msg.invite,
1784         hasInviteContent = invite && !invite.isEmpty(),
1785         origText,
1786         isTextMsg = !hasHtmlPart,
1787         isTruncated = false,
1788         hasViewableTextContent = false,
1789         html = [];
1790 
1791     bodyParts = AjxUtil.collapseList(bodyParts);
1792 
1793     // The server tells us which parts are worth displaying by marking them as body parts. In general,
1794     // we just append them in order to the output, with some special handling for each based on its content type.
1795 
1796     for (var i = 0; i < bodyParts.length; i++) {
1797 
1798         var bp = bodyParts[i],
1799             ct = bp.contentType,
1800             content = this._getBodyContent(bp),
1801             isImage = ZmMimeTable.isRenderableImage(ct),
1802             isHtml = (ct === ZmMimeTable.TEXT_HTML),
1803             isPlain = (ct === ZmMimeTable.TEXT_PLAIN);
1804 
1805         isTruncated = isTruncated || this.isTruncated(bp);
1806 
1807         // first let's check for invite notes and use those as content if present
1808         if (hasInviteContent && !hasMultipleBodyParts) {
1809             if (!msg.getMimeHeader(ZmMailMsg.HDR_INREPLYTO)) {
1810                 // Hack - bug 70603 -  Do not truncate the message for forwarded invites
1811                 // The InReplyTo rfc822 header would be present in most of the forwarded invites
1812                 content = ZmInviteMsgView.truncateBodyContent(content, isHtml);
1813             }
1814             // if the notes are empty, don't bother rendering them
1815             var tmp = AjxStringUtil.stripTags(content);
1816             if (!AjxStringUtil._NON_WHITESPACE.test(tmp)) {
1817                 content = "";
1818             }
1819         }
1820 
1821         // Handle the part based on its Content-Type
1822 
1823         // images
1824         if (isImage) {
1825             var src = (hasMultipleBodyParts && content.length > 0) ? content : msg.getUrlForPart(bp),
1826                 classAttr = hasMultipleBodyParts ? "class='InlineImage' " : " ";
1827 
1828             content = "<img " + [ "zmforced='1' " + classAttr + "src='" + src + "'>"].join("");
1829         }
1830 
1831         // calendar part in ICS format
1832         else if (ct === ZmMimeTable.TEXT_CAL) {
1833             content = ZmMailMsg.getTextFromCalendarPart(bp);
1834         }
1835 
1836         // HTML
1837         else if (isHtml) {
1838             if (htmlMode) {
1839                 // fix broken inline images - take one like this: <img dfsrc="http:...part=1.2.2">
1840                 // and make it look like this: <img dfsrc="cid:DWT123"> by looking up the cid for that part
1841                 if (msg._attachments && ZmMailMsgView.IMG_FIX_RE.test(content)) {
1842                     var partToCid = {};
1843                     for (var j = 0; j < msg._attachments.length; j++) {
1844                         var att = msg._attachments[j];
1845                         if (att.contentId) {
1846                             partToCid[att.part] = att.contentId.substring(1, att.contentId.length - 1);
1847                         }
1848                     }
1849                     content = content.replace(ZmMailMsgView.IMG_FIX_RE, function(s, p1, p2, p3) {
1850                         return partToCid[p2] ? [ p1, '"cid:', partToCid[p2], '"', p3 ].join("") : s;
1851                     });
1852                 }
1853             }
1854             else {
1855                 // this can happen if a message only has an HTML part and the user wants to view mail as text
1856                 content = "<div style='white-space:pre-wrap;'>" + AjxStringUtil.convertHtml2Text(content) + "</div>"
1857             }
1858         }
1859 
1860         // plain text
1861         else if (isPlain) {
1862             origText = content;
1863             if (bp.format === ZmMimeTable.FORMAT_FLOWED) {
1864                 var wrapParams = {
1865                     text:		content,
1866                     isFlowed:	true
1867                 }
1868                 content = AjxStringUtil.wordWrap(wrapParams);
1869             }
1870             content = AjxStringUtil.convertToHtml(content);
1871             if (content && hasMultipleBodyParts && hasHtmlPart) {
1872                 content = "<pre>" + content + "</pre>";
1873             }
1874         }
1875 
1876         // something else
1877         else {
1878             content = AjxStringUtil.convertToHtml(content);
1879         }
1880 
1881         // wrap it in a DIV to be safe
1882         if (content && content.length) {
1883             if (!isImage && AjxStringUtil.trimHtml(content).length > 0) {
1884                 content = "<div>" + content + "</div>";
1885                 hasViewableTextContent = true;
1886             }
1887             html.push(content);
1888         }
1889     }
1890 
1891     // Handle empty messages
1892     if (!hasMultipleBodyParts && !hasViewableTextContent && msg.hasNoViewableContent()) {
1893         // if we got nothing for one alternative type, try the other
1894         if (msg.hasContentType(ZmMimeTable.MULTI_ALT) && !params.forceType) {
1895             var otherType = (preferredContentType === ZmMimeTable.TEXT_HTML) ? ZmMimeTable.TEXT_PLAIN : ZmMimeTable.TEXT_HTML;
1896             params.forceType = otherType;
1897             msg.getBodyPart(otherType, this._renderMessageBody1.bind(this, params));
1898             return;
1899         }
1900         var empty = AjxTemplate.expand("mail.Message#EmptyMessage");
1901         html.push(content ? [empty, content].join("<br><br>") : empty);
1902     }
1903 
1904     if (html.length > 0) {
1905         this._displayContent({
1906             container:		params.container || this.getHtmlElement(),
1907             html:			html.join(""),
1908             isTextMsg:		isTextMsg,
1909             isTruncated:	isTruncated,
1910             index:			params.index,
1911             origText:		origText
1912         });
1913     }
1914 
1915     this._completeMessageBody(params.callback, isTextMsg);
1916 };
1917 
1918 ZmMailMsgView.prototype.isTruncated =
1919 function(part) {
1920 	return part.isTruncated;
1921 };
1922 
1923 ZmMailMsgView.prototype._completeMessageBody = function(callback, isTextMsg) {
1924 
1925 	// Used in ZmConvView2._setExpansion : if false, create the message body (the
1926 	// first time a message is expanded).
1927 	this._msgBodyCreated = true;
1928 	this._setAttachmentLinks(AjxUtil.isBoolean(isTextMsg) ? isTextMsg : appCtxt.get(ZmSetting.VIEW_AS_HTML));
1929 
1930 	if (callback) {
1931         callback.run();
1932     }
1933 };
1934 
1935 ZmMailMsgView.prototype._getBodyContent =
1936 function(bodyPart) {
1937 	return bodyPart ? bodyPart.getContent() : "";
1938 };
1939 
1940 ZmMailMsgView.prototype._renderMessageFooter = function(msg, container) {};
1941 
1942 ZmMailMsgView.prototype._setTags =
1943 function(msg) {
1944 	if (!msg) {
1945 		msg = this._item;
1946 	}
1947 	if (msg && msg.cloneOf) {
1948 		msg = msg.cloneOf;
1949 	}
1950 	//use the helper to get the tags.
1951 	var tagsHtml = ZmTagsHelper.getTagsHtml(msg, this);
1952 
1953 	var table = document.getElementById(this._hdrTableId);
1954 	if (!table) { return; }
1955 	var tagRow = $(table).find(document.getElementById(this._tagRowId));
1956 	
1957 	if (tagRow.length) {
1958 		tagRow.remove();
1959 	}
1960 	if (tagsHtml.length > 0) {
1961 		var cell =  this._insertTagRow(table, this._tagCellId);
1962 		cell.innerHTML = tagsHtml;
1963 	}
1964 };
1965 
1966 ZmMailMsgView.prototype._insertTagRow =
1967 function(table, tagCellId) {
1968 	
1969 	if (!table) { return; }
1970 	
1971 	var tagRow = table.insertRow(-1);
1972 	tagRow.id = this._tagRowId;
1973 	var tagLabelCell = tagRow.insertCell(-1);
1974 	tagLabelCell.className = "LabelColName";
1975 	tagLabelCell.innerHTML = ZmMsg.tags + ":";
1976 	tagLabelCell.style.verticalAlign = "middle";
1977 	var tagCell = tagRow.insertCell(-1);
1978 	tagCell.id = tagCellId;
1979 	return tagCell;
1980 };
1981 
1982 
1983 // Types of links for each attachment
1984 ZmMailMsgView.ATT_LINK_MAIN			= "main";
1985 ZmMailMsgView.ATT_LINK_CALENDAR		= "calendar";
1986 ZmMailMsgView.ATT_LINK_DOWNLOAD		= "download";
1987 ZmMailMsgView.ATT_LINK_BRIEFCASE	= "briefcase";
1988 ZmMailMsgView.ATT_LINK_VCARD		= "vcard";
1989 ZmMailMsgView.ATT_LINK_HTML			= "html";
1990 ZmMailMsgView.ATT_LINK_REMOVE		= "remove";
1991 
1992 ZmMailMsgView.prototype._setAttachmentLinks = function(isTextMsg) {
1993 
1994     this._attachmentLinkIdToFileNameMap = null;
1995 	var attInfo = this._msg.getAttachmentInfo(true, false, isTextMsg);
1996 	var el = document.getElementById(this._attLinksId + "_container");
1997 	if (el) {
1998 		el.style.display = (attInfo.length == 0) ? "none" : "";
1999 	}
2000 	if (attInfo.length == 0) { return; }
2001 
2002 	// prevent appending attachment links more than once
2003 	var attLinksTable = document.getElementById(this._attLinksId + "_table");
2004 	if (attLinksTable) { return; }
2005 
2006 	var htmlArr = [];
2007 	var idx = 0;
2008 	var imageAttsFound = 0;
2009 
2010 	var attColumns = (this._controller.isReadingPaneOn() && this._controller.isReadingPaneOnRight()) ? 1 : ZmMailMsgView.ATTC_COLUMNS;
2011 	var dividx = idx;	// we might get back here
2012 	htmlArr[idx++] = "<table id='" + this._attLinksId + "_table' border=0 cellpadding=0 cellspacing=0>";
2013 
2014 	var attLinkIds = [];
2015 	var rows = 0;
2016 	for (var i = 0; i < attInfo.length; i++) {
2017 		var att = attInfo[i];
2018 		
2019 		if ((i % attColumns) == 0) {
2020 			if (i != 0) {
2021 				htmlArr[idx++] = "</tr>";
2022 			}
2023 			htmlArr[idx++] = "<tr>";
2024 			++rows;
2025 		}
2026 
2027 		htmlArr[idx++] = "<td>";
2028 		htmlArr[idx++] = "<table border=0 cellpadding=0 cellspacing=0 style='margin-right:1em; margin-bottom:1px'><tr>";
2029 		htmlArr[idx++] = "<td style='width:18px'>";
2030 		htmlArr[idx++] = AjxImg.getImageHtml({
2031 			imageName: att.linkIcon,
2032 			styles: "position:relative;",
2033 			altText: ZmMsg.attachment
2034 		});
2035 		htmlArr[idx++] = "</td><td style='white-space:nowrap'>";
2036 
2037 		if (appCtxt.get(ZmSetting.ATTACHMENTS_BLOCKED)) {
2038 			// if attachments are blocked, just show the label
2039 			htmlArr[idx++] = att.label;
2040 		} else {
2041 			// main link for the att name
2042 			var linkArr = [];
2043 			var j = 0;
2044             var displayFileName = AjxStringUtil.clipFile(att.label, 30);
2045 			// if name got clipped, set up to show full name in tooltip
2046             if (displayFileName != att.label) {
2047                 if (!this._attachmentLinkIdToFileNameMap) {
2048 					this._attachmentLinkIdToFileNameMap = {};
2049 				}
2050                 this._attachmentLinkIdToFileNameMap[att.attachmentLinkId] = att.label;
2051             }
2052 			var params = {
2053 				att:	    att,
2054 				id:		    this._getAttachmentLinkId(att.part, ZmMailMsgView.ATT_LINK_MAIN),
2055 				text:	    displayFileName,
2056 				mid:        att.mid,
2057 				rfc822Part: att.rfc822Part
2058 			};
2059 			var link = ZmMailMsgView.getMainAttachmentLinkHtml(params);
2060 			link = att.isHit ? "<span class='AttName-matched'>" + link + "</span>" : link;
2061 			// objectify if this attachment is an image
2062 			if (att.objectify && this._objectManager) {
2063 				this._lazyCreateObjectManager();
2064 				var imgHandler = this._objectManager.getImageAttachmentHandler();
2065 				idx = this._objectManager.generateSpan(imgHandler, htmlArr, idx, link, {url:att.url});
2066 			} else {
2067 				htmlArr[idx++] = link;
2068 			}
2069 		}
2070 		
2071 		// add any discretionary links depending on the attachment and what's enabled
2072 		var linkCount = 0;
2073 		var vCardLink = (att.links.vcard && !appCtxt.isWebClientOffline());
2074 		if (!appCtxt.isExternalAccount() && (att.size || att.links.html || vCardLink || att.links.download || att.links.briefcase || att.links.importICS)) {
2075 			// size
2076 			htmlArr[idx++] = " (";
2077 			if (att.size) {
2078 				htmlArr[idx++] = att.size;
2079 				htmlArr[idx++] = ") ";
2080 			}
2081 			// convert to HTML
2082 			if (att.links.html && !appCtxt.get(ZmSetting.ATTACHMENTS_BLOCKED)) {
2083 				var params = {
2084 					id:				this._getAttachmentLinkId(att.part, ZmMailMsgView.ATT_LINK_HTML),
2085 					blankTarget:	true,
2086 					href:			att.url + "&view=html",
2087 					text:			ZmMsg.preview
2088 				};
2089 				htmlArr[idx++] = ZmMailMsgView.getAttachmentLinkHtml(params);
2090 				linkCount++;
2091 				attLinkIds.push(params.id);
2092 			}
2093 			// save as vCard
2094 			else if (vCardLink) {
2095 				var params = {
2096 					id:				this._getAttachmentLinkId(att.part, ZmMailMsgView.ATT_LINK_VCARD),
2097 					jsHref:			true,
2098 					text:			ZmMsg.addressBook
2099 				};
2100 				htmlArr[idx++] = ZmMailMsgView.getAttachmentLinkHtml(params);
2101 				linkCount++;
2102 				attLinkIds.push(params.id);
2103 			}
2104 			// save locally
2105 			if (att.links.download && !appCtxt.get(ZmSetting.ATTACHMENTS_BLOCKED) && !appCtxt.get(ZmSetting.ATTACHMENTS_VIEW_IN_HTML_ONLY)) {
2106 				htmlArr[idx++] = linkCount ? " | " : "";
2107 				var params = {
2108 					id:				this._getAttachmentLinkId(att.part, ZmMailMsgView.ATT_LINK_DOWNLOAD),
2109                     text:			ZmMsg.download
2110                 };
2111                 if (att.url.indexOf("data:") === -1) {
2112                     params.href = att.url + "&disp=a";
2113                 } else {
2114                     params.href = att.url;
2115                     params.download = true;
2116                     params.downloadLabel = att.label;
2117                 }
2118                 htmlArr[idx++] = ZmMailMsgView.getAttachmentLinkHtml(params);
2119 				linkCount++;
2120 				attLinkIds.push(params.id);
2121 			}
2122 			// add as Briefcase file
2123 			if (att.links.briefcase && !appCtxt.get(ZmSetting.ATTACHMENTS_BLOCKED) && !appCtxt.isWebClientOffline()) {
2124 				htmlArr[idx++] = linkCount ? " | " : "";
2125 				var params = {
2126 					id:				this._getAttachmentLinkId(att.part, ZmMailMsgView.ATT_LINK_BRIEFCASE),
2127 					jsHref:			true,
2128 					text:			ZmMsg.addToBriefcase
2129 				};
2130 				htmlArr[idx++] = ZmMailMsgView.getAttachmentLinkHtml(params);
2131 				linkCount++;
2132 				attLinkIds.push(params.id);
2133 			}
2134 			// add ICS as calendar event
2135 			if (att.links.importICS) {
2136 				htmlArr[idx++] = linkCount ? " | " : "";
2137 				var params = {
2138 					id:				this._getAttachmentLinkId(att.part, ZmMailMsgView.ATT_LINK_CALENDAR),
2139 					jsHref:			true,
2140 					text:			ZmMsg.addToCalendar
2141 				};
2142 				htmlArr[idx++] = ZmMailMsgView.getAttachmentLinkHtml(params);
2143 				linkCount++;
2144 				attLinkIds.push(params.id);
2145 			}
2146 			// remove attachment from msg
2147 			if (att.links.remove && !appCtxt.isWebClientOffline()) {
2148 				htmlArr[idx++] = linkCount ? " | " : "";
2149 				var params = {
2150 					id:				this._getAttachmentLinkId(att.part, ZmMailMsgView.ATT_LINK_REMOVE),
2151 					jsHref:			true,
2152 					text:			ZmMsg.remove
2153 				};
2154 				htmlArr[idx++] = ZmMailMsgView.getAttachmentLinkHtml(params);
2155 				linkCount++;
2156 				attLinkIds.push(params.id);
2157 			}
2158 
2159 			// Attachment Link Handlers (optional)
2160 			if (ZmMailMsgView._attachmentHandlers) {
2161 				var contentHandlers = ZmMailMsgView._attachmentHandlers[att.ct];
2162 				var handlerFunc;
2163 				if (contentHandlers) {
2164 					for (var handlerId in contentHandlers) {
2165 						handlerFunc = contentHandlers[handlerId];
2166 						if (handlerFunc) {
2167 							var customHandlerLinkHTML = handlerFunc.call(this, att);
2168 							if (customHandlerLinkHTML) {
2169 								htmlArr[idx++] = " | " + customHandlerLinkHTML;
2170 							}
2171 						}
2172 					}
2173 				}
2174 			}
2175 		}
2176 
2177 		htmlArr[idx++] = "</td></tr></table>";
2178 		htmlArr[idx++] = "</td>";
2179 
2180 		if (att.ct.indexOf("image") != -1) {
2181 			++imageAttsFound;
2182 		}
2183 	}
2184 
2185 	// limit display size.  seems like an attc. row has exactly 16px; we set it
2186 	// to 56px so that it becomes obvious that there are more attachments.
2187 	if (this._limitAttachments != 0 && rows > ZmMailMsgView._limitAttachments) {
2188 		htmlArr[dividx] = "<div style='height:";
2189 		htmlArr[dividx] = this._attcMaxSize;
2190 		htmlArr[dividx] = "px; overflow:auto;' />";
2191 	}
2192 	htmlArr[idx++] = "</tr></table>";
2193 
2194 	var allAttParams;
2195 	var hasGeneratedAttachments = false;
2196 
2197 	for (var i = 0; i < attInfo.length; i++) {
2198 		hasGeneratedAttachments = hasGeneratedAttachments || att.generated;
2199 	}
2200 
2201 	if (!hasGeneratedAttachments && attInfo.length > 1 && !appCtxt.isWebClientOffline()) {
2202 		allAttParams = this._addAllAttachmentsLinks(attInfo, (imageAttsFound > 1), this._msg.subject);
2203 		htmlArr[idx++] = allAttParams.html;
2204 	}
2205 
2206 	// Push all that HTML to the DOM
2207 	var attLinksDiv = document.getElementById(this._attLinksId);
2208 	if (attLinksDiv) {
2209 		attLinksDiv.innerHTML = htmlArr.join("");
2210 	}
2211 
2212 
2213 	// add handlers for individual attachment links
2214 	for (var i = 0; i < attInfo.length; i++) {
2215 		var att = attInfo[i];
2216 		if (att.ct == ZmMimeTable.MSG_RFC822) {
2217 			this._addClickHandler(att.part, ZmMailMsgView.ATT_LINK_MAIN, ZmMailMsgView.rfc822Callback, null, this._msg.id, att.part);
2218 		}
2219 		if (att.links.importICS) {
2220 			this._addClickHandler(att.part, ZmMailMsgView.ATT_LINK_CALENDAR, ZmMailMsgView.addToCalendarCallback, null, this._msg.id, att.part);
2221 		}
2222 		if (att.links.briefcase) {
2223 			this._addClickHandler(att.part, ZmMailMsgView.ATT_LINK_BRIEFCASE, ZmMailMsgView.briefcaseCallback, null, this._msg.id, att.part, att.label.replace(/\x27/g, "'"));
2224 		}
2225 		if (att.links.download) {
2226             if (att.url.indexOf("data:") === -1) {
2227                 this._addClickHandler(att.part, ZmMailMsgView.ATT_LINK_DOWNLOAD, ZmMailMsgView.downloadCallback, null, att.url + "&disp=a");
2228             }
2229 		}
2230 		if (att.links.vcard) {
2231 			this._addClickHandler(att.part, ZmMailMsgView.ATT_LINK_VCARD, ZmMailMsgView.vcardCallback, null, this._msg.id, att.part);
2232 		}
2233 		if (att.links.remove) {
2234 			this._addClickHandler(att.part, ZmMailMsgView.ATT_LINK_REMOVE, this.removeAttachmentCallback, this, att.part);
2235 		}
2236 	}
2237 
2238 	var offlineHandler = appCtxt.webClientOfflineHandler;
2239 	if (offlineHandler) {
2240 		var getLinkIdCallback = this._getAttachmentLinkId.bind(this);
2241 		var linkIds = [ZmMailMsgView.ATT_LINK_MAIN, ZmMailMsgView.ATT_LINK_DOWNLOAD];
2242 		offlineHandler._handleAttachmentsForOfflineMode(attInfo, getLinkIdCallback, linkIds);
2243 	}
2244 
2245     // add handlers for "all attachments" links
2246 	if (allAttParams) {
2247 		var downloadAllLink = document.getElementById(allAttParams.downloadAllLinkId);
2248 		if (downloadAllLink) {
2249 			downloadAllLink.onclick = allAttParams.downloadAllCallback;
2250 		}
2251 		var removeAllLink = document.getElementById(allAttParams.removeAllLinkId);
2252 		if (removeAllLink) {
2253 			removeAllLink.onclick = allAttParams.removeAllCallback;
2254 		}
2255 	}
2256 
2257 	// add all links to the header tab order
2258 	var attLinks = attLinksDiv.querySelectorAll('A.AttLink');
2259 	for (var i = 0; i < attLinks.length; i++) {
2260 		this._headerTabGroup.addMember(attLinks[i]);
2261 	}
2262 };
2263 
2264 /**
2265  * Returns the HTML for an attachment-related link (an <a> tag). The link will have an HREF
2266  * or an ID (so an onclick handler can be added after the element has been created).
2267  * 
2268  * @param {hash}	params		a hash of params:
2269  * @param {string}	id			ID for the link
2270  * @param {string}	href		link target
2271  * @param {boolean}	noUnderline	if true, do not include an underline style
2272  * @param {boolean} blankTarget	if true, set target to _blank
2273  * @param {boolean}	jsHref		empty link target so browser styles it as a link
2274  * @param {string}	text		visible link text
2275  * 
2276  * @private
2277  */
2278 ZmMailMsgView.getAttachmentLinkHtml =
2279 function(params) {
2280 	var html = [], i = 0;
2281 	html[i++] = "<a class='AttLink' ";
2282 	html[i++] = params.id ? "id='" + params.id + "' " : "";
2283 	html[i++] = !params.noUnderline ? "style='text-decoration:underline' " : "";
2284 	html[i++] = params.blankTarget ? "target='_blank' " : "";
2285 	var href = params.href || (params.jsHref && "javascript:;");
2286 	html[i++] = href ? "href='" + href + "' " : "";
2287     html[i++] = params.download ? (" download='"+(params.downloadLabel||"") + "'") : "";
2288 	if (params.isRfc822) {
2289 		html[i++] = " onclick='ZmMailMsgView.rfc822Callback(\"";
2290 		html[i++] = params.mid;
2291 		html[i++] = "\",\"";
2292 		html[i++] = params.rfc822Part;
2293 		html[i++] = "\"); return false;'";
2294 	}
2295 	html[i++] = "title='" + AjxStringUtil.encodeQuotes(AjxStringUtil.htmlEncode(params.label || params.text));
2296 	html[i++] = "'>" + AjxStringUtil.htmlEncode(params.text) + "</a>";
2297 
2298 	return html.join("");
2299 };
2300 
2301 /**
2302  * Returns the HTML for the link for the attachment name (which usually opens the
2303  * content in a new browser tab).
2304  * 
2305  * @param id
2306  */
2307 ZmMailMsgView.getMainAttachmentLinkHtml =
2308 function(params) {
2309 	var params1 = {
2310 		id:				params.id,
2311 		noUnderline:	true,
2312 		text:			params.text,
2313 		label:			params.att.label
2314 	}; 
2315 	// handle rfc/822 attachments differently
2316 	if (params.att.ct == ZmMimeTable.MSG_RFC822) {
2317 		params1.jsHref      = true;
2318 		params1.isRfc822    = true;
2319 		params1.mid         = params.mid;
2320 		params1.rfc822Part  = params.rfc822Part;
2321 	}
2322 	else {
2323 		// open non-JavaScript URLs in a blank target
2324 		if (params.att.url && params.att.url.indexOf('javascript:') !== 0) {
2325 			params1.blankTarget = true;
2326 		}
2327 		params1.href = params.att.url;
2328 	}
2329 	return ZmMailMsgView.getAttachmentLinkHtml(params1);
2330 };
2331 
2332 ZmMailMsgView.prototype._getAttachmentLinkId =
2333 function(part, type) {
2334 	if (!part)
2335 		return;
2336 	return [this._attLinksId, part, type].join("_");
2337 };
2338 
2339 // Adds an onclick handler to the link with the given part and type. I couldn't find an easy
2340 // way to pass and bind a variable number of arguments, so went with three, which is the most
2341 // any of the handlers takes.
2342 ZmMailMsgView.prototype._addClickHandler =
2343 function (part, type, func, obj, arg1, arg2, arg3) {
2344 	var id = this._getAttachmentLinkId(part, type);
2345 	var link = document.getElementById(id);
2346 	if (link) {
2347 		link.onclick = func.bind(obj, arg1, arg2, arg3);
2348 	}
2349 };
2350 
2351 ZmMailMsgView.prototype._addAllAttachmentsLinks =
2352 function(attachments, viewAllImages, filename) {
2353 
2354 	var itemId = this._msg.id;
2355 	if (AjxUtil.isString(filename)) {
2356 		filename = filename.replace(ZmMailMsgView.FILENAME_INV_CHARS_RE, "");
2357 	} else {
2358 		filename = null;
2359 	}
2360 	filename = AjxStringUtil.urlComponentEncode(filename || ZmMsg.downloadAllDefaultFileName);
2361 	var url = [appCtxt.get(ZmSetting.CSFE_MSG_FETCHER_URI), "&id=", itemId, "&filename=", filename,"&charset=", appCtxt.getCharset(), "&part="].join("");
2362 	var parts = [];
2363 	for (var j = 0; j < attachments.length; j++) {
2364 		parts.push(attachments[j].part);
2365 	}
2366 	var partsStr = parts.join(",");
2367 	var params = {
2368 		url:				(url + partsStr),
2369 		downloadAllLinkId:	this._viewId + "_downloadAll",
2370 		removeAllLinkId:	this._viewId + "_removeAll"
2371 	}
2372 	if (viewAllImages) {
2373 		params.viewAllUrl = "/h/viewimages?id=" + itemId;
2374 	}
2375 	params.html = AjxTemplate.expand("mail.Message#AllAttachments", params);
2376 	
2377 	params.downloadAllCallback = ZmZimbraMail.unloadHackCallback.bind(null);
2378 	params.removeAllCallback = this.removeAttachmentCallback.bind(this, partsStr);
2379 	return params;
2380 };
2381 
2382 ZmMailMsgView.prototype.getToolTipContent =
2383 function(evt) {
2384 
2385 	var tgt = DwtUiEvent.getTarget(evt, false);
2386 
2387 	//see if this is the priority icon. If so, it has a "priority" attribute high/low.
2388 	if (tgt.id == ZmId.getViewId(this._view, ZmId.MV_PRIORITY)) {
2389 		return tgt.getAttribute('priority') =='high' ? ZmMsg.highPriorityTooltip : ZmMsg.lowPriorityTooltip;
2390 	}
2391 	
2392     if (!this._attachmentLinkIdToFileNameMap) {return null};
2393 
2394     if (tgt && tgt.nodeName.toLowerCase() == "a") {
2395         var id = tgt.getAttribute("id");
2396         if (id) {
2397             var fileName = this._attachmentLinkIdToFileNameMap[id];
2398             if (fileName) {
2399                 return AjxStringUtil.htmlEncode(fileName);
2400             }
2401         }
2402     }
2403     return null;
2404 };
2405 
2406 // AttachmentLink Handlers
2407 ZmMailMsgView.prototype.addAttachmentLinkHandler =
2408 function(contentType,handlerId,handlerFunc){
2409 	if (!ZmMailMsgView._attachmentHandlers) {
2410 		ZmMailMsgView._attachmentHandlers = {};
2411 	}
2412 
2413 	if (!ZmMailMsgView._attachmentHandlers[contentType]) {
2414 		ZmMailMsgView._attachmentHandlers[contentType] = {};
2415 	}
2416 
2417 	ZmMailMsgView._attachmentHandlers[contentType][handlerId] = handlerFunc;
2418 };
2419 
2420 // Listeners
2421 
2422 ZmMailMsgView.prototype._controlEventListener =
2423 function(ev) {
2424 	// note - we may get here before we have a chance to initialize the IFRAME
2425 	this._resetIframeHeightOnTimer();
2426 	if (this._inviteMsgView && this._inviteMsgView.isActive()) {
2427 		this._inviteMsgView.resize();
2428 	}
2429 };
2430 
2431 ZmMailMsgView.prototype._shareToolBarListener =
2432 function(ev) {
2433 	ev._buttonId = ev.item.getData(ZmOperation.KEY_ID);
2434 	ev._share = this._msg.share;
2435 	this.notifyListeners(ZmMailMsgView.SHARE_EVENT, ev);
2436 };
2437 
2438 ZmMailMsgView.prototype._subscribeToolBarListener =
2439 function(req, ev) {
2440 	ev._buttonId = ev.item.getData(ZmOperation.KEY_ID);
2441 	ev._subscribeReq = req;
2442 	this.notifyListeners(ZmMailMsgView.SUBSCRIBE_EVENT, ev);
2443 };
2444 
2445 
2446 ZmMailMsgView.prototype._msgChangeListener =
2447 function(ev) {
2448 	if (ev.type != ZmEvent.S_MSG) { return; }
2449 	if (ev.event == ZmEvent.E_DELETE || ev.event == ZmEvent.E_MOVE) {
2450 		if (ev.source == this._msg && (appCtxt.getCurrentViewId() == this._viewId)) {
2451 			this._controller._app.popView();
2452 		}
2453 	} else if (ev.event == ZmEvent.E_TAGS || ev.event == ZmEvent.E_REMOVE_ALL) {
2454 		this._setTags(this._msg);
2455 	} else if (ev.event == ZmEvent.E_MODIFY) {
2456 		if (ev.source == this._msg) {
2457 			this.set(ev.source, true);
2458 		}
2459 	}
2460 };
2461 
2462 ZmMailMsgView.prototype._selectStartListener =
2463 function(ev) {
2464 	// reset mouse event to propagate event to browser (allows text selection)
2465 	ev._stopPropagation = false;
2466 	ev._returnValue = true;
2467 };
2468 
2469 
2470 ZmMailMsgView.prototype._reportButtonListener =
2471 function(msg, ev) {
2472 	var proxy = AjxUtil.createProxy(msg);
2473 
2474 	proxy.clearAddresses();
2475 	var toAddress = new AjxEmailAddress(appCtxt.get(ZmSetting.OFFLINE_REPORT_EMAIL));
2476 	proxy._addrs[AjxEmailAddress.TO] = AjxVector.fromArray([toAddress]);
2477 
2478 	var bp = msg.getBodyPart();
2479 	if (bp) {
2480 		var top = new ZmMimePart();
2481 		top.setContentType(bp.ct);
2482 		top.setContent(msg.getBodyPart().getContent());
2483 		proxy.setTopPart(top);
2484 	}
2485 
2486 	var respCallback = this._sendReportCallback.bind(this, msg);
2487 	var errorCallback = this._sendReportError.bind(this);
2488 	proxy.send(false, respCallback, errorCallback, null, true);
2489 };
2490 
2491 ZmMailMsgView.prototype._sendReportCallback =
2492 function(msg) {
2493 	this._controller._doDelete([msg], true);
2494 };
2495 
2496 ZmMailMsgView.prototype._sendReportError =
2497 function() {
2498 	appCtxt.setStatusMsg(ZmMsg.reportSyncError, ZmStatusView.LEVEL_WARNING);
2499 };
2500 
2501 
2502 // Callbacks
2503 
2504 
2505 ZmMailMsgView.prototype._handleMsgTruncated =
2506 function() {
2507 
2508 	// redo selection to trigger loading and display of entire msg
2509 	this._msg.viewEntireMessage = true;	// remember so we reply to entire msg
2510 	this._msg.force = true;				// make sure view re-renders msg
2511 	if (this._controller._setSelectedItem) {
2512 		// list controller
2513 		this._controller._setSelectedItem({noTruncate: true, forceLoad: true, markRead: false});
2514 	}
2515 	else if (this._controller.show) {
2516 		// msg controller
2517 		this._controller.show(this._msg, this._controller, null, false, false, true, true);
2518 	}
2519 	
2520 	Dwt.setVisible(this._msgTruncatedId, false);
2521 };
2522 
2523 // Static methods
2524 
2525 ZmMailMsgView._swapIdAndSrc =
2526 function (image, i, len, msg, parent, view) {
2527 	// Fix for IE: Over HTTPS, http src urls for images might cause an issue.
2528 	try {
2529 		image.src = image.getAttribute("dfsrc");
2530 	}
2531 	catch (ex) {
2532 		// do nothing
2533 	}
2534 
2535 	if (i == len - 1) {
2536 		if (msg) {
2537 			msg.setHtmlContent(parent.innerHTML);
2538 		}
2539 		view._resetIframeHeightOnTimer();
2540 	}
2541 };
2542 
2543 ZmMailMsgView.prototype._onloadIframe =
2544 function(dwtIframe) {
2545 	var iframe = dwtIframe.getIframe();
2546 	try { iframe.onload = null; } catch(ex) {}
2547 	ZmMailMsgView._resetIframeHeight(this);
2548 };
2549 
2550 ZmMailMsgView._resetIframeHeight =
2551 function(self, attempt) {
2552 
2553 	var iframe = self.getIframe();
2554 	if (!iframe) { return; }
2555 
2556 	DBG.println("cv2", "ZmMailMsgView::_resetIframeHeight " + (attempt || "0"));
2557 	var h;
2558 	if (self._scrollWithIframe) {
2559 		h = self.getH();
2560 		function subtract(el) {
2561 			if (el) {
2562 				if (typeof el == "string") {
2563 					el = document.getElementById(el);
2564 				}
2565 				if (el) {
2566 					h -= Dwt.getSize(el).y;
2567 				}
2568 			}
2569 		}
2570 		subtract(self._headerElement);
2571 		subtract(self._displayImagesId);
2572 		subtract(self._highlightObjectsId);
2573 		if (self._isMsgTruncated) {
2574 			subtract(self._msgTruncatedId);
2575 		}
2576 		if (self._inviteMsgView && self._inviteMsgView.isActive()) {
2577 			if (self._inviteMsgView._inviteToolbar) {//if toolbar not created there's nothing to subtract (e.g. sent folder)
2578 				subtract(self._inviteMsgView.getInviteToolbar().getHtmlElement());
2579 			}
2580 			if (self._inviteMsgView._dayView) {
2581 				subtract(self._inviteMsgView._dayView.getHtmlElement());
2582 			}
2583 		}
2584 		if (self._hasShareToolbar && self._shareToolbar) {
2585 			subtract(self._shareToolbar.getHtmlElement());
2586 		}
2587 		iframe.style.height = h + "px";
2588 	} else {
2589 		if (attempt == null) { attempt = 0; }
2590 		try {
2591 			if (!iframe.contentWindow || !iframe.contentWindow.document) {
2592 				if (attempt < ZmMailMsgView.SETHEIGHT_MAX_TRIES) {
2593 					attempt++;
2594 					self._resetIframeHeightOnTimer(attempt);
2595 				}
2596 				return; // give up
2597 			}
2598 		} catch(ex) {
2599 			if (attempt < ZmMailMsgView.SETHEIGHT_MAX_TRIES) {
2600 				attempt++;
2601 				self._resetIframeHeightOnTimer(attempt++); // for IE
2602 			}
2603 			return; // give up
2604 		}
2605 
2606 		var doc = iframe.contentWindow.document;
2607 		var origHeight = doc && doc.body && doc.body.scrollHeight || 0;
2608 
2609 		// first off, make it wide enough to fill ZmMailMsgView.
2610 		iframe.style.width = "100%"; // *** changes height!
2611 
2612 		// remember the current width
2613 		var view_width = iframe.offsetWidth;
2614 
2615 		// if there's a long unbreakable string, the scrollWidth of the body
2616 		// element will be bigger--we must make the iframe that wide, or there
2617 		// won't be any scrollbars.
2618 		var w = doc.body.scrollWidth;
2619 		if (w > view_width) {
2620 			iframe.style.width = w + "px"; // *** changes height!
2621 
2622 			// Now (bug 20743), by forcing the body a determined width (that of
2623 			// the view) we are making the browser wrap those paragraphs that
2624 			// can be wrapped, even if there's a long unbreakable string in the message.
2625 			doc.body.style.overflow = "visible";
2626 			if (view_width > 20) {
2627 				doc.body.style.width = view_width - 20 + "px"; // *** changes height!
2628 			}
2629 		}
2630 
2631 		// we are finally in the right position to determine height.
2632 		h = Math.max(doc.documentElement.scrollHeight, origHeight);
2633 
2634 		iframe.style.height = h + "px";
2635 
2636 		if (AjxEnv.isWebKitBased) {
2637 			// bug: 39434, WebKit specific
2638 			// After the iframe ht is set there is change is body.scrollHeight, weird.
2639 			// So reset ht to make the entire body visible.
2640 			var newHt = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight);
2641 			if (newHt > h) {
2642 				iframe.style.height = newHt + "px";
2643 			}
2644 		}
2645 	}
2646 };
2647 
2648 // note that IE doesn't seem to be able to reset the "scrolling" attribute.
2649 // this function isn't safe to call for IE!
2650 ZmMailMsgView.prototype.setScrollWithIframe =
2651 function(val) {
2652 	
2653 	if (!this._usingIframe) { return; }
2654 	
2655 	this._scrollWithIframe = val;
2656 	this._limitAttachments = this._scrollWithIframe ? 3 : 0; //making it local
2657 	this._attcMaxSize = this._limitAttachments * 16 + 8;
2658 
2659 	this.setScrollStyle(val ? DwtControl.CLIP : DwtControl.SCROLL);
2660 	var iframe = this.getIframe();
2661 	if (iframe) {
2662 		iframe.style.width = "100%";
2663 		iframe.scrolling = val;
2664 		ZmMailMsgView._resetIframeHeight(this);
2665 	}
2666 };
2667 
2668 
2669 
2670 
2671 ZmMailMsgView._detachCallback =
2672 function(isRfc822, parentController, result) {
2673 	var msgNode = result.getResponse().GetMsgResponse.m[0];
2674 	var ac = window.parentAppCtxt || window.appCtxt;
2675 	var ctlr = ac.getApp(ZmApp.MAIL).getMailListController();
2676 	var msg = ZmMailMsg.createFromDom(msgNode, {list: ctlr.getList()}, true);
2677 	msg._loaded = true; // bug fix #8868 - force load for rfc822 msgs since they may not return any content
2678 	msg.readReceiptRequested = false; // bug #36247 - never allow read receipt for rfc/822 message
2679 	ZmMailMsgView.detachMsgInNewWindow(msg, isRfc822, parentController);
2680 };
2681 
2682 ZmMailMsgView.detachMsgInNewWindow =
2683 function(msg, isRfc822, parentController) {
2684 	var appCtxt = window.parentAppCtxt || window.appCtxt;
2685 	var newWinObj = appCtxt.getNewWindow(true);
2686 	if(newWinObj) {// null check for popup blocker
2687 		newWinObj.command = "msgViewDetach";
2688 		newWinObj.params = { msg:msg, isRfc822:isRfc822, parentController:parentController };
2689 	}
2690 };
2691 
2692 // loads a msg and displays it in a new window
2693 ZmMailMsgView.rfc822Callback =
2694 function(msgId, msgPartId, parentController) {
2695 	var isRfc822 = Boolean((msgPartId != null));
2696 	var appCtxt = window.parentAppCtxt || window.appCtxt;
2697 	var params = {
2698 		sender: appCtxt.getAppController(),
2699 		msgId: msgId,
2700 		partId: msgPartId,
2701 		getHtml: appCtxt.get(ZmSetting.VIEW_AS_HTML),
2702 		markRead: appCtxt.isExternalAccount() ? false : true,
2703 		callback: ZmMailMsgView._detachCallback.bind(null, isRfc822, parentController)
2704 	};
2705 	ZmMailMsg.fetchMsg(params);
2706 };
2707 
2708 ZmMailMsgView.vcardCallback =
2709 function(msgId, partId) {
2710 	ZmZimbraMail.unloadHackCallback();
2711 
2712 	var ac = window.parentAppCtxt || window.appCtxt;
2713 	ac.getApp(ZmApp.CONTACTS).createFromVCard(msgId, partId);
2714 };
2715 
2716 ZmMailMsgView.downloadCallback =
2717 function(downloadUrl) {
2718 	ZmZimbraMail.unloadHackCallback();
2719 	location.href = downloadUrl;
2720 };
2721 
2722 ZmMailMsgView.prototype.removeAttachmentCallback =
2723 function(partIds) {
2724 	ZmZimbraMail.unloadHackCallback();
2725 
2726 	if (!(partIds instanceof Array)) { partIds = partIds.split(","); }
2727 
2728 	var msg = (partIds.length > 1)
2729 		? ZmMsg.attachmentConfirmRemoveAll
2730 		: ZmMsg.attachmentConfirmRemove;
2731 
2732 	var dlg = appCtxt.getYesNoMsgDialog();
2733 	dlg.registerCallback(DwtDialog.YES_BUTTON, this._removeAttachmentCallback, this, [partIds]);
2734 	dlg.setMessage(msg, DwtMessageDialog.WARNING_STYLE);
2735 	dlg.popup();
2736 };
2737 
2738 ZmMailMsgView.prototype._removeAttachmentCallback =
2739 function(partIds) {
2740 	appCtxt.getYesNoMsgDialog().popdown();
2741 	this._msg.removeAttachments(partIds, this._handleRemoveAttachment.bind(this));
2742 };
2743 
2744 ZmMailMsgView.prototype._handleRemoveAttachment =
2745 function(result) {
2746 	var msgNode = result.getResponse().RemoveAttachmentsResponse.m[0];
2747 	var ac = window.parentAppCtxt || window.appCtxt;
2748 	var listCtlr = ac.getApp(ZmApp.MAIL).getMailListController(); //todo - getting a list controller from appCtxt always seems suspicious to me (should we get the controller for the current view?)
2749 	var msg = ZmMailMsg.createFromDom(msgNode, {list: listCtlr.getList()}, true);
2750 	this._msg = this._item = null;
2751 	// cache this actioned ID so we can reset selection to it once the CREATE
2752 	// notifications have been processed.
2753 	listCtlr.actionedMsgId = msgNode.id;
2754 	if (this._controller.setMsg) {
2755 		//for the ZmMsgController case. (standalone).
2756 		this._controller.setMsg(msg);
2757 	}
2758 	this.set(msg);
2759 };
2760 
2761 ZmMailMsgView.briefcaseCallback =
2762 function(msgId, partId, name) {
2763 	ZmZimbraMail.unloadHackCallback();
2764 
2765 	// force create deferred folders if not created
2766 	AjxDispatcher.require("BriefcaseCore");
2767 	var aCtxt = appCtxt.isChildWindow ? parentAppCtxt : appCtxt;
2768 	var briefcaseApp = aCtxt.getApp(ZmApp.BRIEFCASE);
2769 	briefcaseApp._createDeferredFolders();
2770 
2771 	appCtxt.getApp(ZmApp.BRIEFCASE).createFromAttachment(msgId, partId, name);
2772 };
2773 
2774 ZmMailMsgView.prototype.deactivate =
2775 function() {
2776 	this._controller.inactive = true;
2777 };
2778 
2779 ZmMailMsgView.addToCalendarCallback =
2780 function(msgId, partId, name) {
2781 	ZmZimbraMail.unloadHackCallback();
2782 
2783 	// force create deferred folders if not created
2784 	AjxDispatcher.require(["MailCore", "CalendarCore"]);
2785 	var aCtxt = appCtxt.isChildWindow ? parentAppCtxt : appCtxt;
2786 	var calApp = aCtxt.getApp(ZmApp.CALENDAR);
2787 	calApp._createDeferredFolders();
2788 
2789 	appCtxt.getApp(ZmApp.CALENDAR).importAppointment(msgId, partId, name);
2790 };
2791 
2792 ZmMailMsgView.prototype.getMsgBodyElement =
2793 function(){
2794     return document.getElementById(this._msgBodyDivId);
2795 };
2796 
2797 ZmMailMsgView.prototype._getViewId =
2798 function() {
2799 	var ctlrViewId = this._controller.getCurrentViewId();
2800 	return this._controller.isZmMsgController ? ctlrViewId : [ctlrViewId, ZmId.VIEW_MSG].join("_");
2801 };
2802 
2803 ZmMailMsgView.prototype._keepReading =
2804 function(check) {
2805 	var cont = this.getHtmlElement();
2806 	var contHeight = Dwt.getSize(cont).y;
2807 	var canScroll = (cont.scrollHeight > contHeight && (cont.scrollTop + contHeight < cont.scrollHeight));
2808 	if (canScroll) {
2809 		if (!check) {
2810 			cont.scrollTop = cont.scrollTop + contHeight;
2811 		}
2812 		return true;
2813 	}
2814 	return false;
2815 };
2816 
2817 ZmMailMsgView.prototype._getIframeTitle = function() {
2818 	return AjxMessageFormat.format(ZmMsg.messageTitle, this._msg.subject);
2819 };
2820