1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 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 contains the new window class.
 27  */
 28 
 29 /**
 30  * Creates a controller to run <code>ZmNewWindow</code>. Do not call directly, instead use
 31  * the <code>run()</code> factory method.
 32  * @class
 33  * This class is the controller for a window created outside the main client
 34  * window. It is a very stripped down and specialized version of {@link ZmZimbraMail}.
 35  * The child window is single-use; it does not support switching among multiple
 36  * views.
 37  *
 38  * @author Parag Shah
 39  * 
 40  * @extends	ZmController
 41  * 
 42  * @see		#run
 43  */
 44 ZmNewWindow = function() {
 45 
 46 	ZmController.call(this, null);
 47 
 48 	appCtxt.setAppController(this);
 49 
 50 	//update body class to reflect user selected font
 51 	document.body.className = "user_font_" + appCtxt.get(ZmSetting.FONT_NAME);
 52 	//update root html elment class to reflect user selected font size
 53 	Dwt.addClass(document.documentElement, "user_font_size_" + appCtxt.get(ZmSetting.FONT_SIZE));
 54 
 55 
 56 	this._settings = appCtxt.getSettings();
 57 	this._settings.setReportScriptErrorsSettings(AjxException, ZmController.handleScriptError); //must set this for child window since AjxException is fresh for this window. Also must pass AjxException and the handler since we want it to update the one from this child window, and not the parent window
 58 
 59 	this._shell = appCtxt.getShell();
 60 
 61 	// Register keymap and global key action handler w/ shell's keyboard manager
 62 	this._kbMgr = appCtxt.getKeyboardMgr();
 63 	if (appCtxt.get(ZmSetting.USE_KEYBOARD_SHORTCUTS)) {
 64 		this._kbMgr.enable(true);
 65 		this._kbMgr.registerKeyMap(new ZmKeyMap());
 66 		this._kbMgr.pushDefaultHandler(this);
 67 	}
 68 
 69 	this._apps = {};
 70 };
 71 
 72 ZmNewWindow.prototype = new ZmController;
 73 ZmNewWindow.prototype.constructor = ZmNewWindow;
 74 
 75 ZmNewWindow.prototype.isZmNewWindow = true;
 76 ZmNewWindow.prototype.toString = function() { return "ZmNewWindow"; };
 77 
 78 // Public methods
 79 
 80 /**
 81  * Sets up new window and then starts it by calling its constructor. It is assumed that the
 82  * CSFE is on the same host.
 83  * 
 84  */
 85 ZmNewWindow.run =
 86 function() {
 87 
 88 	// We're using a custom pkg that includes the mail classes we'll need, so pretend that
 89 	// we've already loaded mail packages so the real ones don't get loaded as well.
 90 	AjxDispatcher.setLoaded("MailCore", true);
 91 	AjxDispatcher.setLoaded("Mail", true);
 92 
 93 	var winOpener = window.opener || window;
 94 
 95 	if (!window.parentController) {
 96 		window.parentController = winOpener._zimbraMail;
 97 	}
 98 
 99 	// Create the global app context
100 	window.appCtxt = new ZmAppCtxt();
101 	window.appCtxt.isChildWindow = true;
102 
103 	// XXX: DO NOT MOVE THIS LINE
104 	// redefine ZmSetting from parent window since it loses this info.
105 	window.parentAppCtxt = winOpener.appCtxt;
106 	appCtxt.setSettings(parentAppCtxt.getSettings());
107 	appCtxt.isOffline = parentAppCtxt.isOffline;
108 	appCtxt.multiAccounts = parentAppCtxt.multiAccounts;
109     appCtxt.sendAsEmails = parentAppCtxt.sendAsEmails;
110     appCtxt.sendOboEmails = parentAppCtxt.sendOboEmails;
111     window.ZmSetting = winOpener.ZmSetting;
112 
113 	ZmOperation.initialize();
114 	ZmApp.initialize();
115 
116 	var shell = new DwtShell({className:"MainShell"});
117 	window.onbeforeunload = ZmNewWindow._confirmExitMethod;
118 	appCtxt.setShell(shell);
119 
120 	// create new window and Go!
121 	var newWindow = new ZmNewWindow();
122     newWindow.startup();
123 	
124 	if (winOpener.onkeydown) {
125 		window.onkeydown = winOpener.onkeydown;
126 	}
127 };
128 
129 /**
130 * Allows this child window to inform parent it's going away
131 */
132 ZmNewWindow.unload = function(ev) {
133 
134 	if (!window || !window.opener || !window.parentController) {
135         return;
136     }
137 
138 	var command = window.newWindowCommand; //bug 54409 - was using wrong attribute for command in unload
139 	if (command == "compose" || command == "composeDetach"
140 			|| (command == "msgViewDetach" && appCtxt.composeCtlrSessionId)) { //msgViewDetach might turn into a compose session if user hits "reply"/etc
141 		// compose controller adds listeners to parent window's list so we
142 		// need to remove them before closing this window!
143 		var cc = AjxDispatcher.run("GetComposeController", appCtxt.composeCtlrSessionId);
144 		if (cc) {
145 			cc.dispose();
146 		}
147 	}
148 
149 	if (command == "msgViewDetach") {
150 		// msg controller (as a ZmListController) adds listener to tag list
151 		var mc = AjxDispatcher.run("GetMsgController", appCtxt.msgCtlrSessionId);
152 		if (mc) {
153 			mc.dispose();
154 		}
155 	}
156 
157 	if (window.parentController) {
158 		window.parentController.removeChildWindow(window);
159 	}
160 };
161 
162 /**
163  * Presents a view based on a command passed through the window object. Possible commands are:
164  *
165  * <ul>
166  * <li><b>compose</b> compose window launched in child window</li>
167  * <li><b>composeDetach</b> compose window detached from client</li>
168  * <li><b>msgViewDetach</b> msg view detached from client</li>
169  * </ul>
170  * 
171  */
172 ZmNewWindow.prototype.startup =
173 function() {
174 	// get params from parent window b/c of Safari bug #7162
175 	// and in case of a refresh, our old window parameters are still stored there
176 	if (window.parentController) {
177 		var childWinObj = window.parentController.getChildWindow(window);
178 		if (childWinObj) {
179 			window.newWindowCommand = childWinObj.command;
180 			window.newWindowParams = childWinObj.params;
181 		}
182 	}
183 
184     if (!this._appViewMgr) {
185         this._appViewMgr = new ZmAppViewMgr(this._shell, this, true, false);
186         this._statusView = new ZmStatusView(this._shell, "ZmStatus", Dwt.ABSOLUTE_STYLE, ZmId.STATUS_VIEW);
187     }
188 
189     var cmd = window.newWindowCommand;
190 	var params = window.newWindowParams;
191 	if (cmd == "shortcuts") {
192 		var apps = {};
193 		apps[ZmApp.PREFERENCES] = true;
194 		this._createEnabledApps(apps);
195 		this._createView();
196 		return;
197 	}
198 
199 	DBG.println(AjxDebug.DBG1, " ************ Hello from new window!");
200 
201 	var rootTg = appCtxt.getRootTabGroup();
202 
203 	var apps = {};
204 	apps[ZmApp.SEARCH] = true;
205 	apps[ZmApp.MAIL] = true;
206 	apps[ZmApp.CONTACTS] = true;
207 	// only load calendar app if we're dealing with an invite
208 	var msg = (cmd == "msgViewDetach") ? params.msg : null;
209 	if (msg &&
210         (msg.isInvite() || this._checkShareType(msg.share, "appointment"))) {
211 		apps[ZmApp.CALENDAR] = true;
212 	} else if (msg && this._checkShareType(msg.share, "task")) {
213 		apps[ZmApp.TASKS] = true;
214 	}
215 	apps[ZmApp.PREFERENCES] = true;
216     apps[ZmApp.BRIEFCASE] = true;  //Need this for both Compose & Msg View detach window.
217 	this._createEnabledApps(apps);
218 
219 	// inherit parent's identity collection
220 	var parentPrefsApp = parentAppCtxt.getApp(ZmApp.MAIL);
221     if (parentPrefsApp) {
222         appCtxt.getApp(ZmApp.MAIL)._identityCollection = parentPrefsApp.getIdentityCollection();
223     }
224 
225 	// Find target first.
226 	var target;
227 	if (cmd == "compose" || cmd == "composeDetach") {
228 		target = "compose-window";
229 	} else if (cmd == "msgViewDetach") {
230 		target = "view-window";
231 	}
232 
233 	ZmZimbraMail.prototype._registerOrganizers.call(this);
234 	ZmZimbraMail.registerViewsToTypeMap();
235 
236     
237 	// setup zimlets, Load it first becoz.. zimlets has to get processed first.
238 	if (target) {
239 		var allzimlets = parentAppCtxt.get(ZmSetting.ZIMLETS);
240 		allzimlets = allzimlets || [];
241 		var zimletArray = this._settings._getCheckedZimlets(allzimlets);
242 		if (this._hasZimletsForTarget(zimletArray, target)) {
243 			var zimletMgr = appCtxt.getZimletMgr();
244 			var userProps = this._getUserProps();
245 			var createViewCallback =  new AjxCallback(this, this._createView);
246             appCtxt.setZimletsPresent(true);
247 			zimletMgr.loadZimlets(zimletArray, userProps, target, createViewCallback, true);
248 			return;
249 		}
250 	}
251 
252 	this._createView();
253 };
254 
255 /**
256  * @private
257  */
258 ZmNewWindow.prototype._checkShareType =
259 function(share, type) {
260     return share && share.link && (type === share.link.view);
261 };
262 ZmNewWindow.prototype._createView =
263 function() {
264 
265 	var cmd = window.newWindowCommand;
266 	var params = window.newWindowParams;
267 
268 	var rootTg = appCtxt.getRootTabGroup();
269 	var startupFocusItem;
270 
271 	//I null composeCtlrSessionId so it's not kept from irrelevant sessions from parent window.
272 	// (since I set it in every compose session, in ZmMailApp.prototype.compose).
273 	// This is important in case of cmd == "msgViewDetach"
274 	appCtxt.composeCtlrSessionId = null;  
275 	// depending on the command, do the right thing
276 	if (cmd == "compose" || cmd == "composeDetach") {
277 		var cc = AjxDispatcher.run("GetComposeController");	// get a new compose ctlr
278 		appCtxt.composeCtlrSessionId = cc.getSessionId();
279 		if (params.action == ZmOperation.REPLY_ALL) {
280 			params.msg = this._deepCopyMsg(params.msg);
281 		}
282 		if (cmd == "compose") {
283 			cc._setView(params);
284 		} else {
285 			AjxDispatcher.require(["MailCore", "ContactsCore", "CalendarCore"]);
286 			var op = params.action || ZmOperation.NEW_MESSAGE;
287 			if (params.msg && params.msg._mode) {
288 				switch (params.msg._mode) {
289 					case ZmAppt.MODE_DELETE:
290 					case ZmAppt.MODE_DELETE_INSTANCE:
291 					case ZmAppt.MODE_DELETE_SERIES: {
292 						op = ZmOperation.REPLY_CANCEL;
293 						break;
294 					}
295 				}
296 			}
297 			params.action = op;
298 			cc._setView(params);
299 			cc._composeView.setDetach(params);
300 
301 			// bug fix #5887 - get the parent window's compose controller based on its session ID
302 			var parentCC = window.parentController.getApp(ZmApp.MAIL).getComposeController(params.sessionId);
303 			if (parentCC && parentCC._composeView) {
304 				// once everything is set in child window, pop parent window's compose view
305 				parentCC._composeView.reset(true);
306 				parentCC._app.popView(true);
307 			}
308 		}
309 		cc._setComposeTabGroup();
310 		rootTg.addMember(cc.getTabGroup());
311 		startupFocusItem = cc._getDefaultFocusItem();
312 
313 		target = "compose-window";
314 	} else if (cmd == "msgViewDetach") {
315 		//bug 52366 - not sure why only REPLY_ALL causes the problem (and not REPLY for example), but in this case the window is opened first for view. But
316 		//the user might of course click "reply to all" later in the window so I deep copy here in any case.
317 		var msg = this._deepCopyMsg(params.msg);
318 		msg.isRfc822 = params.isRfc822; //simpler
319 		params.msg.addChangeListener(msg.detachedChangeListener.bind(msg));
320 
321 		var msgController = AjxDispatcher.run("GetMsgController");
322 		appCtxt.msgCtlrSessionId = msgController.getSessionId();
323 		msgController.show(msg, params.parentController);
324 		rootTg.addMember(msgController.getTabGroup());
325 		startupFocusItem = msgController.getCurrentView();
326 
327 		target = "view-window";
328 	} else if (cmd == 'documentEdit') {
329 		AjxDispatcher.require(["Docs"]);
330  		ZmDocsEditApp.setFile(params.id, params.name, params.folderId);
331 		ZmDocsEditApp.restUrl = params.restUrl;
332 		new ZmDocsEditApp();
333 		if (params.name) {
334 			Dwt.setTitle(params.name);
335 		}
336 	} else if (cmd == "shortcuts") {
337 		var panel = appCtxt.getShortcutsPanel();
338 		panel.popup(params.cols);
339 	}
340 
341 	if (this._appViewMgr.loadingView) {
342 		this._appViewMgr.loadingView.setVisible(false);
343 	}
344 
345 	this._kbMgr.setTabGroup(rootTg);
346 	this._kbMgr.grabFocus(startupFocusItem);
347 };
348 
349 /**
350  * HACK: This should go away once we have a cleaner server solution that
351  *       allows us to get just those zimlets for the specified target.
352  *       
353  * @private
354  */
355 ZmNewWindow.prototype._hasZimletsForTarget =
356 function(zimletArray, target) {
357 	var targetRe = new RegExp("\\b"+target+"\\b");
358 	for (var i=0; i < zimletArray.length; i++) {
359 		var zimletObj = zimletArray[i];
360 		var zimlet0 = zimletObj.zimlet[0];
361 		if (targetRe.test(zimlet0.target || "main")) {
362 			return true;
363 		}
364 	}
365 	return false;
366 };
367 
368 /**
369  * @private
370  */
371 ZmNewWindow.prototype._getUserProps =
372 function() {
373 	var userPropsArray = parentAppCtxt.get(ZmSetting.USER_PROPS);
374 
375 	// default to original user props
376 	userPropsArray = userPropsArray ? [].concat(userPropsArray) : [];
377 
378 	// current user props take precedence, if available
379 	var zimletHash = parentAppCtxt.getZimletMgr().getZimletsHash();
380 	var zimletArray = parentAppCtxt.get(ZmSetting.ZIMLETS);
381 	for (var i = 0; i < zimletArray.length; i++) {
382 		var zname = zimletArray[i].zimlet[0].name;
383 		var zimlet = zimletHash[zname];
384 		if (!zimlet || !zimlet.userProperties) continue;
385 		for (var j = 0; j < zimlet.userProperties.length; j++) {
386 			var userProp = zimlet.userProperties[j];
387 			var userPropObj = { zimlet: zname, name: userProp.name, _content: userProp.value };
388 			userPropsArray.push(userPropObj);
389 		}
390 	}
391 
392 	// return user properties
393 	return userPropsArray;
394 };
395 
396 /**
397  * Cancels the request.
398  * 
399  * @param	{String}	reqId		the request id
400  * @param	{AjxCallback}	errorCallback		the callback
401  * @param	{Boolean}	noBusyOverlay	if <code>true</code>, do not show a busy overlay
402  */
403 ZmNewWindow.prototype.cancelRequest =
404 function(reqId, errorCallback, noBusyOverlay) {
405 	return window.parentController ? window.parentController.cancelRequest(reqId, errorCallback, noBusyOverlay) : null;
406 };
407 
408 /**
409  * Sends the server requests to the main controller.
410  * 
411  * @param	{Hash}	params		a hash of parameters
412  */
413 ZmNewWindow.prototype.sendRequest =
414 function(params) {
415     // reset onbeforeunload on send
416     window.onbeforeunload = null;
417 	// bypass error callback to get control over exceptions in the childwindow.
418 	params.errorCallback = new AjxCallback(this, this._handleException, [( params.errorCallback || null )]);
419 	params.fromChildWindow = true;
420 	return window.parentController ? window.parentController.sendRequest(params) : null;
421 };
422 
423 /**
424  * @private
425  */
426 ZmNewWindow.prototype._handleException =
427 function(errCallback, ex) {
428 	var handled = false;
429 	if (errCallback) {
430 		handled = errCallback.run(ex);
431 	}
432 	if (!handled) {
433 		ZmController.prototype._handleException.apply(this, [ex]);
434 	}
435 	return true;
436 };
437 
438 /**
439  * Popup the error dialog.
440  * 
441  * @param	{String}	msg		the message
442  * @param	{AjxException}	ex	the exception
443  * @param	{Boolean}	noExecReset
444  * @param	{Boolean}	hideReportButton
445  */
446 ZmNewWindow.prototype.popupErrorDialog =
447 function(msg, ex, noExecReset, hideReportButton)  {
448 	// Since ex is from parent window, all the types seems like objects, so need
449 	// to filter the functions
450 	var detailStr;
451 	if (ex instanceof Object || typeof ex == "object") {
452 		var details = [];
453 		ex.msg = ex.msg || msg;
454 		for (var prop in ex) {
455 			if (typeof ex[prop] == "function" ||
456 				(typeof ex[prop] == "object" && ex[prop].apply && ex[prop].call))
457 			{
458 				continue;
459 			}
460 			details.push([prop, ": ", ex[prop], "<br/>\n"].join(""));
461 		}
462 		detailStr = details.join("");
463 	}
464 	ZmController.prototype.popupErrorDialog.call(this, msg, ( detailStr || ex ), noExecReset, hideReportButton);
465 };
466 
467 /**
468  * Set status messages via the main controller, so they show up in the client's status area.
469  * 
470  * @param	{Hash}	params		a hash of parameters
471  */
472 ZmNewWindow.prototype.setStatusMsg =
473 function(params) {
474 	// bug: 26478. Changed status msg to be displayed within the child window.
475 	params = Dwt.getParams(arguments, ZmStatusView.MSG_PARAMS);
476 	this._statusView.setStatusMsg(params);
477 };
478 
479 /**
480  * Gets a handle to the given app.
481  *
482  * @param {String}	appName		the app name
483  * @return	{ZmApp}		the application
484  */
485 ZmNewWindow.prototype.getApp =
486 function(appName) {
487 	if (!this._apps[appName]) {
488 		this._createApp(appName);
489 	}
490 	return this._apps[appName];
491 };
492 
493 /**
494  * Gets a handle to the app view manager.
495  * 
496  * @return	{ZmAppViewMgr}	the view manager
497  */
498 ZmNewWindow.prototype.getAppViewMgr =
499 function() {
500 	return this._appViewMgr;
501 };
502 
503 // App view mgr calls this, we don't need it to do anything.
504 ZmNewWindow.prototype.setActiveApp = function() {};
505 
506 /**
507  * Gets the key map manager.
508  * 
509  * @return	{DwtKeyMapMgr}	the key map manager
510  */
511 ZmNewWindow.prototype.getKeyMapMgr =
512 function() {
513 	return this._kbMgr.__keyMapMgr;
514 };
515 
516 /**
517  * Gets the key map name.
518  * 
519  * @return	{String}	the key map name
520  */
521 ZmNewWindow.prototype.getKeyMapName =
522 function() {
523 	var ctlr = appCtxt.getCurrentController();
524 	if (ctlr && ctlr.getKeyMapName) {
525 		return ctlr.getKeyMapName();
526 	}
527 	return ZmKeyMap.MAP_GLOBAL;
528 };
529 
530 /**
531  * Handles the key action.
532  * 
533  * @param	{Object}	actionCode		the action code
534  * @param	{Object}	ev		the event
535  * @return	{Boolean}	<code>true</code> if the action is handled
536  */
537 ZmNewWindow.prototype.handleKeyAction = function(actionCode, ev) {
538 
539     // Ignore global shortcuts since they don't make sense in a child window
540     if (ZmApp.GOTO_ACTION_CODE_R[actionCode]) {
541         return false;
542     }
543     switch (actionCode) {
544         case ZmKeyMap.QUICK_REMINDER:
545         case ZmKeyMap.FOCUS_SEARCH_BOX:
546         case ZmKeyMap.FOCUS_CONTENT_PANE:
547         case ZmKeyMap.FOCUS_TOOLBAR:
548         case ZmKeyMap.SHORTCUTS:
549             return false;
550     }
551 
552     // Hand shortcut to current controller
553 	var ctlr = appCtxt.getCurrentController();
554 	if (ctlr && ctlr.handleKeyAction) {
555 		return ctlr.handleKeyAction(actionCode, ev);
556 	}
557 
558 	return false;
559 };
560 
561 
562 // Private methods
563 
564 /**
565  * Instantiates enabled apps. An optional argument may be given limiting the set
566  * of apps that may be created.
567  *
568  * @param {Hash}	apps	the set of apps to create
569  * 
570  * @private
571  */
572 ZmNewWindow.prototype._createEnabledApps =
573 function(apps) {
574 	for (var app in ZmApp.CLASS) {
575 		if (!apps || apps[app]) {
576 			ZmApp.APPS.push(app);
577 		}
578 	}
579 	ZmApp.APPS.sort(function(a, b) {
580 		return ZmZimbraMail.hashSortCompare(ZmApp.LOAD_SORT, a, b);
581 	});
582 
583 	// instantiate enabled apps - this will invoke app registration
584 	for (var i = 0; i < ZmApp.APPS.length; i++) {
585 		var app = ZmApp.APPS[i];
586 		if (app != ZmApp.IM) { // Don't create im app. Seems like the safest way to avoid ever logging in.
587 			var setting = ZmApp.SETTING[app];
588 			if (!setting || appCtxt.get(setting)) {
589 				this._createApp(app);
590 			}
591 		}
592 	}
593 };
594 
595 /**
596  * Creates an app object, which doesn't necessarily do anything just yet.
597  * 
598  * @private
599  */
600 ZmNewWindow.prototype._createApp =
601 function(appName) {
602 	if (this._apps[appName]) return;
603 	var appClass = eval(ZmApp.CLASS[appName]);
604 	this._apps[appName] = appClass && new appClass(this._shell, window.parentController);
605 };
606 
607 /**
608  * @private
609  * TODO: get rid of this function
610  */
611 ZmNewWindow.prototype._deepCopyMsg =
612 function(msg) {
613 	// initialize new ZmSearch if applicable
614 	var newSearch = null;
615 	var oldSearch = msg.list.search;
616 
617 	if (oldSearch) {
618 		newSearch = new ZmSearch();
619 
620 		for (var i in oldSearch) {
621 			if ((typeof oldSearch[i] == "object") || (typeof oldSearch[i] == "function")) { continue; }
622 			newSearch[i] = oldSearch[i];
623 		}
624 
625 		// manually add objects since they are no longer recognizable
626 		newSearch.types = new AjxVector();
627 		var types = oldSearch.types.getArray();
628 		for (var i = 0;  i < types.length; i++) {
629 			newSearch.types.add(types[i]);
630 		}
631 	}
632 
633 	// initialize new ZmMailList
634 	var newMailList = new ZmMailList(msg.list.type, newSearch);
635 	for (var i in msg.list) {
636 		if ((typeof msg.list[i] == "object") || (typeof msg.list[i] == "function")) { continue; }
637 		newMailList[i] = msg.list[i];
638 	}
639 
640 	// finally, initialize new ZmMailMsg
641 	var newMsg = new ZmMailMsg(msg.id, newMailList);
642 
643 	for (var i in msg) {
644 		if ((typeof msg[i] == "object") || (typeof msg[i] == "function")) { continue; }
645 		newMsg[i] = msg[i];
646 	}
647 
648 	// manually add any objects since they are no longer recognizable
649 	for (var i in msg._addrs) {
650 		var addrs = msg._addrs[i].getArray();
651 		for (var j = 0; j < addrs.length; j++) {
652 			newMsg._addrs[i].add(addrs[j]);
653 		}
654 	}
655 
656 	if (msg.attachments && msg.attachments.length > 0) {
657 		for (var i = 0; i < msg.attachments.length; i++) {
658 			newMsg.attachments.push(msg.attachments[i]);
659 		}
660 	}
661 
662 	for (var i = 0; i < msg._bodyParts.length; i++) {
663 		newMsg._bodyParts.push(msg._bodyParts[i]);
664 	}
665 	
666 	for (var ct in msg._contentType) {
667 		newMsg._contentType[ct] = true;
668 	}
669 
670 	if (msg._topPart) {
671 		newMsg._topPart = new ZmMimePart();
672 		for (var i in msg._topPart) {
673 			if ((typeof msg._topPart[i] == "object") || (typeof msg._topPart[i] == "function"))
674 				continue;
675 			newMsg._topPart[i] = msg._topPart[i];
676 		}
677 		var children = msg._topPart.children.getArray();
678 		for (var i = 0; i < children.length; i++) {
679 			newMsg._topPart.children.add(children[i]);
680 		}
681 	}
682 
683 	if (msg.invite) {
684 		newMsg.invite = msg.invite;
685 	}
686 
687 	if (msg.share) {
688 		newMsg.share = msg.share;
689 	}
690 
691 	newMsg.subscribeReq = msg.subscribeReq;
692 
693 	// TODO: When/if you get rid of this function, also remove the cloneOf uses in:
694 	//		ZmBaseController.prototype._doTag
695 	//		ZmBaseController.prototype._setTagMenu
696 	//		ZmMailMsgView.prototype._setTags
697 	//		ZmMailMsgView.prototype._handleResponseSet
698 	//		ZmMailListController.prototype._handleResponseFilterListener
699 	//		ZmMailListController.prototype._handleResponseNewApptListener
700 	//		ZmMailListController.prototype._handleResponseNewTaskListener
701 	newMsg.cloneOf = msg;
702 
703 	return newMsg;
704 };
705 
706 
707 // Static Methods
708 
709 /**
710  * @private
711  */
712 ZmNewWindow._confirmExitMethod = function(ev) {
713 
714 	if (!appCtxt.get(ZmSetting.WARN_ON_EXIT) || !window.parentController) {
715 		return;
716     }
717 
718 	var cmd = window.newWindowCommand;
719 
720 	if (cmd === "compose" || cmd === "composeDetach")	{
721 		var cc = AjxDispatcher.run("GetComposeController", appCtxt.composeCtlrSessionId),
722             cv = cc && cc._composeView,
723             viewId = cc.getCurrentViewId(),
724             avm = appCtxt.getAppViewMgr();
725 
726 		// only show native confirmation dialog if compose view is dirty
727 		if (cv && avm.isVisible(viewId) && cv.isDirty()) {
728 			return ZmMsg.newWinComposeExit;
729 		}
730 	} else if (cmd == 'documentEdit') {
731 		var ctrl = ZmDocsEditApp._controller;
732 		var msg = ctrl.checkForChanges();
733 		return msg || ctrl.exit();
734 	}
735 };
736