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