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 contains the Zimbra mail controller class.
 27  * 
 28  */
 29 
 30 /**
 31  * Creates a controller to run ZimbraMail. Do not call directly, instead use the run()
 32  * factory method.
 33  * @constructor
 34  * @class
 35  * This class is the "ubercontroller", as it manages all the apps as well as bootstrapping
 36  * the ZimbraMail application.
 37  *
 38  * @param {Hash}	params	a hash of parameters
 39  * @param {constant}    params.app		the starting app
 40  * @param  {Element}	params.userShell	the top-level skin container
 41  *        
 42  * @extends	ZmController
 43  */
 44 ZmZimbraMail = function(params) {
 45 
 46     if (arguments.length == 0) { return; }
 47 
 48 	ZmController.call(this, null);
 49 
 50 	appCtxt.setZimbraMail(this);
 51     appCtxt.setAppController(this);
 52 
 53 	// ALWAYS set back reference into our world (also used by unload handler)
 54 	window._zimbraMail = this;
 55 
 56     // app event handling
 57     this._evt = new ZmAppEvent();
 58     this._evtMgr = new AjxEventMgr();
 59     // copy over any statically registered listeners
 60     for (var type in ZmZimbraMail._listeners) {
 61         var list = ZmZimbraMail._listeners[type];
 62         if (list && list.length) {
 63             for (var i = 0; i < list.length; i++) {
 64                 this._evtMgr.addListener(type, list[i]);
 65             }
 66         }
 67     }
 68 
 69     // all subsequent calls to register static app listeners go to instance
 70     ZmZimbraMail.addListener = AjxCallback.simpleClosure(this.addListener, this);
 71     ZmZimbraMail.addAppListener = AjxCallback.simpleClosure(this.addAppListener, this);
 72 
 73     // Create generic operations
 74     ZmOperation.initialize();
 75 
 76     // settings
 77     this._createSettings(params);
 78     this._createEnabledApps();
 79     this._initializeSettings(params);
 80 	this._postInitializeSettings();
 81 
 82 	//update body class to reflect user selected font
 83 	document.body.className = "user_font_" + appCtxt.get(ZmSetting.FONT_NAME);
 84 	//update root html elment class to reflect user selected font size (remove the "normal" size that was set by default).
 85 	Dwt.delClass(document.documentElement, "user_font_size_normal", "user_font_size_" + appCtxt.get(ZmSetting.FONT_SIZE));
 86 
 87     // set internal state
 88 	this._shell = appCtxt.getShell();
 89     this._userShell = params.userShell;
 90 
 91     this._requestMgr = new ZmRequestMgr(this); // NOTE: requires settings to be initialized
 92 
 93 	this._appIframeView = {};
 94 	this._activeApp = null;
 95 	this._sessionTimer = new AjxTimedAction(null, ZmZimbraMail.executeSessionTimer);
 96 	this._sessionTimerId = -1;
 97 	this._pollActionId = null;	// AjaxTimedAction ID of timer counting down to next poll time
 98 	this._pollRequest = null;	// HTTP request of poll we've sent to server
 99 	this._pollInstantNotifications = false; // if TRUE, we're in "instant notification" mode
100 	this.statusView = null;
101 	ZmZimbraMail._exitTimer = new AjxTimedAction(null, ZmZimbraMail.exitSession);
102 	ZmZimbraMail._exitTimerId = -1;
103 	ZmZimbraMail.stayOnPagePrompt = false;
104 	ZmZimbraMail.STAYONPAGE_INTERVAL = 2;  //in minutes
105     // setup history support
106     if (appCtxt.get(ZmSetting.HISTORY_SUPPORT_ENABLED) && !AjxEnv.isSafari) {
107         window.historyMgr = appCtxt.getHistoryMgr();
108     }
109 
110     // create app view manager
111     this._appViewMgr = new ZmAppViewMgr(this._shell, this, false, true);
112 	var hidden = [ ZmAppViewMgr.C_SEARCH_RESULTS_TOOLBAR, ZmAppViewMgr.C_TASKBAR ];
113 	if (!appCtxt.get(ZmSetting.CAL_ALWAYS_SHOW_MINI_CAL)) {
114 		hidden.push(ZmAppViewMgr.C_TREE_FOOTER);
115 	}
116 	this._appViewMgr.setHiddenComponents(ZmAppViewMgr.GLOBAL, hidden, true);
117 
118     // register handlers
119 	AjxDispatcher.setPackageLoadFunction("Zimlet", new AjxCallback(this, this._postLoadZimlet));
120 
121 	AjxDispatcher.setPreLoadFunction(new AjxCallback(this, function() {
122 		this._appViewMgr.pushView(ZmId.VIEW_LOADING);
123 	}));
124 	AjxDispatcher.setPostLoadFunction(new AjxCallback(this, function() {
125 		if (!AjxUtil.arrayContains(this._appViewMgr._toRemove, ZmId.VIEW_LOADING)) {
126 			this._appViewMgr._toRemove.push(ZmId.VIEW_LOADING);
127 		}
128 	}));
129 
130 	for (var i in ZmApp.QS_ARG) {
131 		ZmApp.QS_ARG_R[ZmApp.QS_ARG[i]] = i;
132 	}
133 
134 	this._shell.addGlobalSelectionListener(new AjxListener(this, this._globalSelectionListener));
135 
136     // setup webClient offline support
137     appCtxt.initWebOffline();
138     /// go!
139 	this.startup(params);
140 };
141 
142 ZmZimbraMail.prototype = new ZmController;
143 ZmZimbraMail.prototype.constructor = ZmZimbraMail;
144 
145 ZmZimbraMail.prototype.isZmZimbraMail = true;
146 ZmZimbraMail.prototype.toString = function() { return "ZmZimbraMail"; };
147 
148 // REVISIT: This is done so that we when we switch from being "beta"
149 //          to production, we don't have to ensure that all of the
150 //          translations are changed at the same time. We can simply
151 //          remove the beta suffix from the app name.
152 ZmMsg.BETA_documents = [ZmMsg.documents, ZmMsg.beta].join(" ");
153 
154 // dummy app (needed when defining drop targets in _registerOrganizers)
155 ZmApp.MAIN = "ZmZimbraMail";
156 ZmApp.DROP_TARGETS[ZmApp.MAIN] = {};
157 
158 // Static listener registration
159 ZmZimbraMail._listeners = {};
160 
161 // Consts
162 ZmZimbraMail.UI_LOAD_BEGIN		= "ui_load_begin";
163 ZmZimbraMail.UI_LOAD_END		= "ui_load_end";
164 ZmZimbraMail.UI_NETWORK_UP		= "network_up";
165 ZmZimbraMail.UI_NETWORK_DOWN	= "network_down";
166 
167 
168 // Public methods
169 
170 
171 /**
172  * Sets up ZimbraMail, and then starts it by calling its constructor. It is assumed that the
173  * CSFE is on the same host.
174  *
175  * @param {Hash}	params			a hash of parameters
176  * @param {constant}      params.app				te starting app
177  * @param {Boolean}      params.offlineMode		if <code>true</code>, this is the offline client
178  * @param {Boolean}      params.devMode			if <code>true</code>, we are in development environment
179  * @param {Hash}      params.settings			the server prefs/attrs
180  * @param {constant}      params.protocolMode	the protocal mode (http, https or mixed)
181  * @param {Boolean}      params.noSplashScreen	if <code>true</code>, do not show splash screen during startup
182  */
183 ZmZimbraMail.run =
184 function(params) {
185 
186 	if (params.noSplashScreen) {
187 		ZmZimbraMail.killSplash();
188 	}
189 
190 	// Create the global app context
191 	window.appCtxt = new ZmAppCtxt();
192 	appCtxt.rememberMe = false;
193 
194 	// Handle offline mode
195 	if (params.offlineMode) {
196 		DBG.println(AjxDebug.DBG1, "OFFLINE MODE");
197 		appCtxt.isOffline = true;
198 	}
199 
200 	// Create the shell
201 	var userShell = params.userShell = window.document.getElementById(ZmId.SKIN_SHELL);
202 	if (!userShell) {
203 		alert("Could not get user shell - skin file did not load properly");
204 	}
205 	var shell = new DwtShell({userShell:userShell, docBodyScrollable:false, id:ZmId.SHELL});
206 	appCtxt.setShell(shell);
207 
208     // Go!
209     new ZmZimbraMail(params);
210 };
211 
212 /**
213  * Unloads the controller. Allows parent window to walk list of open child windows and either "delete" 
214  * or "disable" them.
215  * 
216  */
217 ZmZimbraMail.unload =
218 function() {
219 
220 	if (!ZmZimbraMail._endSessionDone) {
221 		ZmZimbraMail._endSession();
222 	}
223 
224 	if (ZmZimbraMail._isLogOff) {
225 		ZmZimbraMail._isLogOff = false;
226 		// stop keeping track of user input (if applicable)
227 		if (window._zimbraMail) {
228 			window._zimbraMail.setSessionTimer(false);
229 		}
230 
231 		ZmCsfeCommand.noAuth = true;
232 	}
233 
234 	ZmZimbraMail.closeChildWindows();
235 	
236 	ZmZimbraMail.stayOnPagePrompt = false;
237 	ZmZimbraMail.setExitTimer(false);
238 	ZmZimbraMail.sessionTimerInvoked = false;
239 	window._zimbraMail = window.onload = window.onunload = window.onresize = window.document.onkeypress = null;
240 };
241 
242 ZmZimbraMail.closeChildWindows =
243 function() {
244 	
245 	var childWinList = window._zimbraMail && window._zimbraMail._childWinList;
246 	if (childWinList) {
247 		// close all child windows
248 		for (var i = 0; i < childWinList.size(); i++) {
249 			var childWin = childWinList.get(i);
250 			if (childWin.win) {
251 				childWin.win.onbeforeunload = null;
252 				childWin.win.parentController = null;
253 				childWin.win.close();
254 			}
255 		}
256 	}
257 };
258 
259 /**
260  * Returns sort order using a and b as keys into given hash.
261  *
262  * @param {Hash}	hash		a hash with sort values
263  * @param {String}	a			a key into hash
264  * @param {String}	b			a key into hash
265  * @return	{int}	0 if the items are the same; 1 if "a" is before "b"; -1 if "b" is before "a"
266  */
267 ZmZimbraMail.hashSortCompare =
268 function(hash, a, b) {
269 	var appA = a ? Number(hash[a]) : 0;
270 	var appB = b ? Number(hash[b]) : 0;
271 	if (appA > appB) { return 1; }
272 	if (appA < appB) { return -1; }
273 	return 0;
274 };
275 
276 /**
277  * Hides the splash screen.
278  * 
279  */
280 ZmZimbraMail.killSplash =
281 function() {
282 	// 	Splash screen is now a part of the skin, loaded in statically via the JSP
283 	//	as a well-known ID.  To hide the splash screen, just hide that div.
284 	Dwt.hide("skin_container_splash_screen");
285 };
286 
287 /**
288  * Returns the state of ZCS application if user is logged out in case of browser quit.
289  * The public method is added to take appropriate action in the chat app if user session is ending.
290  *
291  *
292  * @public
293  */
294 ZmZimbraMail.hasSessionEnded =
295     function() {
296         return ZmZimbraMail._endSessionDone;
297     };
298 
299 /**
300  * Startup the mail controller.
301  * 
302  * <p>
303  * The following steps are performed:
304  * <ul>
305  * <li>check for skin, show it</li>
306  * <li>create app view mgr</li>
307  * <li>create components (sash, banner, user info, toolbar above overview, status view)</li>
308  * <li>create apps</li>
309  * <li>load user settings (using a <code><GetInfoRequest></code>)</li>
310  * </ul>
311  * 
312  * @param {Hash}	params		a hash of parameters
313  * @param {constant}	app			the starting app
314  * @param {Hash}	settings		a hash of settings overrides
315  */
316 ZmZimbraMail.prototype.startup =
317 function(params) {
318 
319 	if (appCtxt.isOffline) {
320 		this.sendClientEventNotify(ZmZimbraMail.UI_LOAD_BEGIN);
321 	}
322 
323 	appCtxt.inStartup = true;
324 	if (typeof(skin) == "undefined") {
325 		DBG.println(AjxDebug.DBG1, "No skin!");
326 	}
327 
328 	skin.show("skin", true);
329 	appCtxt.getShell().relayout();
330 
331 	if (!this._components) {
332 		this._components = {};
333 		this._components[ZmAppViewMgr.C_SASH] = new DwtSash({parent:this._shell, style:DwtSash.HORIZONTAL_STYLE,
334 															 className:"console_inset_app_l", threshold:20, id:ZmId.MAIN_SASH});
335 		this._components[ZmAppViewMgr.C_SASH].addListener(DwtEvent.ONMOUSEUP, ZmZimbraMail._folderTreeSashRelease);
336 		this._components[ZmAppViewMgr.C_BANNER] = this._createBanner();
337 		this._components[ZmAppViewMgr.C_USER_INFO] = this._userNameField =
338 			this._createUserInfo("BannerTextUser", ZmAppViewMgr.C_USER_INFO, ZmId.USER_NAME);
339 		this._components[ZmAppViewMgr.C_QUOTA_INFO] = this._usedQuotaField =
340 			this._createUserInfo("BannerTextQuota", ZmAppViewMgr.C_QUOTA_INFO, ZmId.USER_QUOTA);
341 
342 		if (appCtxt.isOffline) {
343 			this._initOfflineUserInfo();
344 		}
345 	}
346 
347 	if (!this.statusView) {
348 		this.statusView = new ZmStatusView(this._shell, "ZmStatus", Dwt.ABSOLUTE_STYLE, ZmId.STATUS_VIEW);
349 	}
350 
351 	this._registerOrganizers();
352 
353 	// set up map of search types to item types
354 	for (var i in ZmSearch.TYPE) {
355 		ZmSearch.TYPE_MAP[ZmSearch.TYPE[i]] = i;
356 	}
357 	ZmZimbraMail.registerViewsToTypeMap();
358 
359 	this._getStartApp(params);
360 	appCtxt.startApp = params.startApp;
361 
362 	this._postRenderCallbacks = [];
363 	this._postRenderLast = 0;
364 	if (params.startApp == ZmApp.MAIL) {
365 		this._doingPostRenderStartup = true;
366 		var callback = new AjxCallback(this,
367 			function() {
368 				AjxDispatcher.require("Startup2");
369 				var account = appCtxt.multiAccounts && appCtxt.accountList.mainAccount;
370 				if (appCtxt.get(ZmSetting.CALENDAR_ENABLED, null, account)) {
371 					this.handleCalendarComponents();
372 				}
373 				if (appCtxt.get(ZmSetting.TASKS_ENABLED, null, account)) {
374 					this.handleTaskComponents();
375 				}
376 				this._appViewMgr.loadingView.setVisible(false);
377 			});
378 		this.addPostRenderCallback(callback, 0, 0, true);
379 
380 		// wait half a minute and load TinyMCE
381 		var callback = new AjxCallback(this, function() {
382 			AjxDispatcher.require("Startup2");
383 
384 			var timer = new DwtIdleTimer(30 * 1000, function() {
385 				AjxDispatcher.require('TinyMCE', true);
386 				timer.kill();
387 			});
388 		});
389 		this.addPostRenderCallback(callback, 0, 0, true);
390 	}
391 
392     // NOTE: We must go through the request mgr for default handling
393     var getInfoResponse = AjxUtil.get(params, "getInfoResponse");
394     if (getInfoResponse) {
395         this._requestMgr.sendRequest({response:getInfoResponse});
396     }
397 
398 	// fetch meta data for the main account
399 	var respCallback = new AjxCallback(this, this._handleResponseGetMetaData, params);
400 	appCtxt.accountList.mainAccount.loadMetaData(respCallback);
401 
402 	//todo - might want to move this call and the methods to ZmMailApp as it's specific to mail app only.
403     this._initDelegatedSenderAddrs();
404     if(appCtxt.isOffline) {
405         var updatePref = appCtxt.get(ZmSetting.OFFLINE_UPDATE_NOTIFY);
406         this._offlineUpdateChannelPref(updatePref)
407     }
408 };
409 
410 
411 ZmZimbraMail.prototype._initDelegatedSenderAddrs =
412 function() {
413     var soapDoc = AjxSoapDoc.create("DiscoverRightsRequest", "urn:zimbraAccount");
414     soapDoc.set("right","sendAs" );
415     soapDoc.set("right","sendOnBehalfOf");
416     soapDoc.set("right","sendAsDistList");
417     soapDoc.set("right","sendOnBehalfOfDistList");
418     var batchCmd = new ZmBatchCommand(null, appCtxt.accountList.mainAccount.name);
419     var callback = this._initDelegatedSenderEmails.bind(this);
420     batchCmd.addNewRequestParams(soapDoc, callback, callback);
421 	var offlineCallback = this._handleOfflineDelegatedSenderEmails.bind(this, callback);
422     batchCmd.run(null, null, offlineCallback);
423 };
424 
425 ZmZimbraMail.prototype._getDelegatedSenderEmails =
426 function(sendRights, sendRight) {
427 	var emails = [];
428 	if (!sendRights || !sendRights.length) {
429 		return emails;
430 	}
431 	for (var i = 0; i < sendRights.length; i++) {
432 		var targets = sendRights[i].target;
433 		var right = sendRights[i].right;
434 		var sendRightDistList = sendRight + "DistList";
435 		if (right !== sendRight && right !== sendRightDistList) {
436 			continue;
437 		}
438 		var isDL = right === sendRightDistList;
439 		for (var j = 0; j < targets.length; j++) {
440 			var target = targets[j];
441 			var emailList = target.email;
442 			for (var k = 0; k < emailList.length; k++) {
443 				var addr = emailList[k].addr;
444 				emails.push({addr: addr, isDL: isDL, displayName: target.d});
445 			}
446 		}
447 
448 	}
449 	return emails;
450 };
451 
452 ZmZimbraMail.prototype._initDelegatedSenderEmails =
453 function(result){
454     var response = result.getResponse();
455 	if (ZmOffline.isOnlineMode()) {
456 		localStorage.setItem("DiscoverRightsResponse", JSON.stringify(response));
457 	}
458 	var discoverRightsResponse = response && response.DiscoverRightsResponse;
459 	var sendRights = discoverRightsResponse && discoverRightsResponse.targets;
460     appCtxt.sendAsEmails = this._getDelegatedSenderEmails(sendRights, 'sendAs');
461     appCtxt.sendOboEmails = this._getDelegatedSenderEmails(sendRights, 'sendOnBehalfOf');
462 };
463 
464 ZmZimbraMail.prototype._handleOfflineDelegatedSenderEmails =
465 function(callback) {
466 	var result = localStorage.getItem("DiscoverRightsResponse");
467 	if (result) {
468 		var csfeResult = new ZmCsfeResult({BatchResponse : JSON.parse(result)});
469 		callback.run(csfeResult);
470 	}
471 };
472 
473 ZmZimbraMail.registerViewsToTypeMap = function() {
474 	// organizer types based on view
475 	for (var i in ZmOrganizer.VIEWS) {
476 		var list = ZmOrganizer.VIEWS[i];
477 		for (var j = 0; j < list.length; j++) {
478 			ZmOrganizer.TYPE[list[j]] = i;
479 		}
480 	}
481 };
482 
483 ZmZimbraMail.prototype._createSettings = function(params) {
484     // We've received canned SOAP responses for GetInfoRequest and SearchRequest from the
485     // launch JSP, wrapped in a BatchRequest. Jiggle them so that they look like real
486     // responses, and pass them along.
487     if (params.batchInfoResponse) {
488         var batchResponse = params.batchInfoResponse.Body.BatchResponse;
489 
490         // always assume there's a get info response
491 		var infoResponse = batchResponse.GetInfoResponse[0];
492 		if(!infoResponse) {
493 			infoResponse ={}
494 		}
495 		//store per-domain settings in infoResponse obj so we can access it like other settings
496 		infoResponse.domainSettings = params.settings;
497         params.getInfoResponse = {
498             Header: params.batchInfoResponse.Header,
499             Body: { GetInfoResponse: infoResponse}
500         };
501         var session = AjxUtil.get(params.getInfoResponse, "Header", "context", "session");
502         if (session) {
503             ZmCsfeCommand.setSessionId(session);
504         }
505         DBG.println(AjxDebug.DBG1, ["<b>RESPONSE (from JSP tag)</b>"].join(""), "GetInfoResponse");
506         DBG.dumpObj(AjxDebug.DBG1, params.getInfoResponse, -1);
507 
508         // we may have an initial search response
509         if (batchResponse.SearchResponse) {
510             params.searchResponse = {
511                 Body: { SearchResponse: batchResponse.SearchResponse[0] }
512             };
513             DBG.println(AjxDebug.DBG1, ["<b>RESPONSE (from JSP tag)</b>"].join(""), "SearchResponse");
514             DBG.dumpObj(AjxDebug.DBG1, params.searchResponse, -1);
515         }
516     }
517 
518     // create settings
519     var settings = new ZmSettings()
520     appCtxt.setSettings(settings);
521 
522     // We have to pre-initialize the settings in order to create
523     // the enabled apps correctly.
524     settings.setUserSettings({info:params.getInfoResponse.Body.GetInfoResponse, preInit:true});
525 };
526 
527 ZmZimbraMail.prototype._initializeSettings = function(params) {
528     var info = params.getInfoResponse.Body.GetInfoResponse;
529 
530     var settings = appCtxt.getSettings();
531     // NOTE: Skip notify to avoid callbacks which reference objects that aren't set, yet
532     settings.setUserSettings(info, null, true, true, true);
533     settings.userSettingsLoaded = true;
534 
535     // settings structure and defaults
536     var branch = appCtxt.get(ZmSetting.BRANCH);
537     if (window.DBG && !DBG.isDisabled()) {
538         DBG.setTitle("Debug (" + branch + ")");
539     }
540 
541     // setting overrides
542     if (params.settings) {
543         for (var name in params.settings) {
544             var id = settings.getSettingByName(name);
545             if (id) {
546                 settings.getSetting(id).setValue(params.settings[name]);
547             }
548         }
549     }
550 
551     // reset polling interval for offline
552     if (appCtxt.isOffline) {
553         appCtxt.set(ZmSetting.POLLING_INTERVAL, 60, null, null, true);
554     }
555 
556     // Handle dev mode
557     if (params.devMode == "1") {
558         DBG.println(AjxDebug.DBG1, "DEV MODE");
559         appCtxt.set(ZmSetting.DEV, true);
560     }
561 
562     // Handle protocol mode - standardize on trailing :
563     if (params.protocolMode) {
564         var proto = (params.protocolMode.indexOf(":") == -1) ? params.protocolMode + ":" : params.protocolMode;
565         appCtxt.set(ZmSetting.PROTOCOL_MODE, proto);
566     }
567     if (params.httpPort) {
568         appCtxt.set(ZmSetting.HTTP_PORT, params.httpPort);
569     }
570     if (params.httpsPort) {
571         appCtxt.set(ZmSetting.HTTPS_PORT, params.httpsPort);
572     }
573 
574     // hide spam if not enabled
575     if (!appCtxt.get(ZmSetting.SPAM_ENABLED)) {
576         ZmFolder.HIDE_ID[ZmFolder.ID_SPAM] = true;
577     }
578 
579 	// Chats hidden by default, check for override
580 	if (appCtxt.get(ZmSetting.SHOW_CHATS_FOLDER)) {
581 		delete ZmFolder.HIDE_ID[ZmOrganizer.ID_CHATS];
582 	}
583 };
584 
585 /**
586  * Perform any additional operation after initializing settings
587  * @private
588  */
589 ZmZimbraMail.prototype._postInitializeSettings =
590 function() {
591 	this._setCustomInvalidEmailPats();
592 };
593 
594 /**
595  * Set an array of invalid Email patterns(values of zimbraMailAddressValidationRegex in ldap) to
596  * AjxEmailAddress.customInvalidEmailPats
597  * @private
598  */
599 ZmZimbraMail.prototype._setCustomInvalidEmailPats =
600 function() {
601  	var customPatSetting = appCtxt.getSettings().getSetting(ZmSetting.EMAIL_VALIDATION_REGEX);
602 	var cPatList = [];
603 	if(customPatSetting) {
604 		cPatList = customPatSetting.value;
605 	}
606 	for(var i = 0; i< cPatList.length; i++) {
607 		var pat = cPatList[i];
608 		if(pat && pat != "") {
609 			  AjxEmailAddress.customInvalidEmailPats.push(new RegExp(pat))
610 		}
611 	}
612 };
613 
614 /**
615  * @private
616  */
617 ZmZimbraMail.prototype._handleResponseGetMetaData =
618 function(params) {
619 
620 	if (appCtxt.get(ZmSetting.CONTACTS_ENABLED)) {
621 		var method = appCtxt.multiAccounts ? "GetContactsForAllAccounts" : "GetContacts";
622 		AjxDispatcher.run({
623 			method:     method,
624 			callback:   this._handleResponseLoadUserSettings.bind(this, params)
625 		});
626 	}
627 	else {
628 		this._handleResponseLoadUserSettings(params);
629 	}
630 };
631 
632 /**
633  * Shows the mini-calendar.
634  * 
635  */
636 ZmZimbraMail.prototype.showMiniCalendar =
637 function() {
638 	var calMgr = appCtxt.getCalManager();
639 	calMgr.getMiniCalendar();
640 	appCtxt.getAppViewMgr().displayComponent(ZmAppViewMgr.C_TREE_FOOTER, true);
641     calMgr.highlightMiniCal();
642     calMgr.startDayRollTimer();
643 };
644 
645 /**
646  * Shows reminders.
647  */
648 ZmZimbraMail.prototype.showReminder =
649 function() {
650     var reminderController = appCtxt.getApp(ZmApp.CALENDAR).getReminderController();
651 	reminderController.refresh();
652 };
653 
654 /**
655  * Shows reminders.
656  */
657 ZmZimbraMail.prototype.showTaskReminder =
658 function() {
659 	var taskMgr = appCtxt.getTaskManager();
660 	var taskReminderController = taskMgr.getReminderController();
661 	taskReminderController.refresh();
662 };
663 
664 ZmZimbraMail.prototype._isProtocolHandlerAccessed =
665 function() {
666     if (AjxEnv.isFirefox){
667       if (!localStorage || localStorage['zimbra_mailto_init']) return true;
668       localStorage['zimbra_mailto_init'] = true;
669     }
670     return false;
671 };
672 
673 /**
674  * @private
675  */
676 ZmZimbraMail.prototype._handleResponseLoadUserSettings =
677 function(params, result) {
678 	if (appCtxt.multiAccounts) {
679 		var callback = new AjxCallback(this, this._handleResponseStartup, [params, result]);
680 		appCtxt.accountList.loadAccounts(callback);
681 	} else {
682 		this._handleResponseStartup(params, result);
683 	}
684 };
685 
686 /**
687  * Startup: part 2
688  * 	- create app toolbar component
689  * 	- determine and launch starting app
690  *
691  * @param {Hash}	params			a hash of parameters
692  * @param       {constant}	params.app				the starting app
693  * @param       {Object}	params.settingOverrides	a hash of overrides of user settings
694  * @param {ZmCsfeResult}	result		the result object from load of user settings
695  * 
696  * @private
697  */
698 ZmZimbraMail.prototype._handleResponseStartup =
699 function(params, result) {
700 
701 	params = params || {};
702 	if (params.settingOverrides) {
703 		this._needOverviewLayout = true;
704 		for (var id in params.settingOverrides) {
705 			var setting = appCtxt.getSetting(id);
706 			if (setting) {
707 				setting.setValue(params.settingOverrides[id]);
708 			}
709 		}
710 	}
711 	if (params.preset) {
712 		var presets = params.preset.split(",");
713 		for (var i = 0; i < presets.length; i++) {
714 			var fields = presets[i].split(":");
715 			var setting = appCtxt.getSettings().getSetting(fields[0]);
716 			if (setting && setting.canPreset) {
717 				setting.setValue(fields[1]);
718 			}
719 		}
720 	}
721 
722 	if (!appCtxt.isOffline) {
723         if (appCtxt.get(ZmSetting.INSTANT_NOTIFY) && appCtxt.get(ZmSetting.INSTANT_NOTIFY_INTERVAL) == appCtxt.get(ZmSetting.POLLING_INTERVAL))
724             AjxTimedAction.scheduleAction(new AjxTimedAction(this, this.setInstantNotify, [true]), 4000);
725         else
726 		    this.setPollInterval(true);
727 	} else {
728 		if (appCtxt.get(ZmSetting.OFFLINE_SUPPORTS_MAILTO) && window.platform && 
729 			window.platform.isRegisteredProtocolHandler("mailto")) {  
730 		    // bug fix #34342 - always register the protocol handler for mac and linux on start up
731 		    this.registerMailtoHandler(!AjxEnv.isWindows, true);
732 		}    
733 	}
734 
735 	window.onbeforeunload = ZmZimbraMail._confirmExitMethod;
736 
737 	if (!this._components[ZmAppViewMgr.C_APP_CHOOSER]) {
738 		this._components[ZmAppViewMgr.C_APP_CHOOSER] = this._appChooser = this._createAppChooser();
739 	}
740 
741 	ZmApp.initialize();
742 
743     if(appCtxt.get(ZmSetting.DEFAULT_TIMEZONE)) {
744         AjxTimezone.DEFAULT_RULE = AjxTimezone._guessMachineTimezone(appCtxt.get(ZmSetting.DEFAULT_TIMEZONE));
745         AjxTimezone.DEFAULT = AjxTimezone.getClientId(AjxTimezone.DEFAULT_RULE.serverId);
746     }
747 
748 	this.notify(ZmAppEvent.PRE_STARTUP);
749 
750 	params.result = result;
751 	var respCallback = new AjxCallback(this, this._handleResponseStartup1, params);
752 
753 	// startup and packages have been optimized for quick mail display
754 	if (this._doingPostRenderStartup) {
755 		this.addAppListener(params.startApp, ZmAppEvent.POST_RENDER, new AjxListener(this, this._postRenderStartup));
756         //For offline mode offline callback will take care
757 		if (!appCtxt.isWebClientOffline()) {
758 	        this._searchResponse = params.searchResponse;
759         }
760 	} else {
761 		AjxDispatcher.require("Startup2");
762 	}
763 
764 	// Set up post-render callbacks
765 
766 	// run app-related startup functions
767 	var callback = new AjxCallback(this,
768 		function() {
769 			this.runAppFunction("startup", false, params.result);
770 		});
771 	this.addPostRenderCallback(callback, 2, 0, true);
772 
773 	callback = new AjxCallback(this,
774 		function() {
775 			this._setupTabGroups();
776 			this.focusContentPane();
777 		});
778 	this.addPostRenderCallback(callback, 3, 100);
779 
780 	// miscellaneous post-startup housekeeping
781 	callback = new AjxCallback(this,
782 		function() {
783 			AjxDispatcher.enableLoadFunctions(true);
784 			appCtxt.inStartup = false;
785 			this.notify(ZmAppEvent.POST_STARTUP);
786 
787 			var sc = appCtxt.getSearchController();
788 			sc.getSearchToolbar().initAutocomplete();
789 
790 			// bug fix #31996
791 			if (appCtxt.isOffline) {
792 				sc.resetSearchToolbar();
793 			}
794 
795 			if (appCtxt.get(ZmSetting.OFFLINE_SUPPORTS_MAILTO) && appCtxt.isOffline) {
796 				this.handleOfflineMailTo(location.search);
797 			}
798 		});
799 	this.addPostRenderCallback(callback, 5, 100);
800 
801     if (appCtxt.get(ZmSetting.MAIL_ENABLED) && !appCtxt.isExternalAccount() && navigator.registerProtocolHandler && !this._isProtocolHandlerAccessed()){
802         callback = new AjxCallback(this,
803             function() {
804                 try {
805                     navigator.registerProtocolHandler("mailto",AjxUtil.formatUrl({qsArgs:{view:'compose',to:'%s'}, qsReset:true}) ,ZmMsg.zimbraTitle);
806                 } catch (err){};
807         });
808         this.addPostRenderCallback(callback, 6, 100);
809     }
810 
811 	this.activateApp(params.startApp, false, respCallback, this._errorCallback, params);
812 
813 	var account = appCtxt.multiAccounts && appCtxt.accountList.mainAccount;
814 	if (appCtxt.get(ZmSetting.CALENDAR_ENABLED, null, account) &&
815 		!this._doingPostRenderStartup &&
816 		(params.startApp != ZmApp.CALENDAR))
817 	{
818 		this.handleCalendarComponents();
819 	}
820 	if (appCtxt.get(ZmSetting.TASKS_ENABLED, null, account) &&
821 		!this._doingPostRenderStartup &&
822 		(params.startApp != ZmApp.TASKS))
823 	{
824 		this.handleTaskComponents();
825 	}
826 
827 	if (appCtxt.get(ZmSetting.IMPORT_ON_LOGIN_ENABLED)) {
828 		var ds = new ZmDataSourceCollection();
829 		var dsCollection = appCtxt.getDataSourceCollection();
830 		var pop3Accounts = dsCollection && dsCollection.getPopAccounts();
831 		var imapAccounts = dsCollection && dsCollection.getImapAccounts();
832 		var sourceMap = {};
833 		if (pop3Accounts) {
834 			for (var i=0; i<pop3Accounts.length; i++) {
835 				sourceMap[pop3Accounts[i].id] = pop3Accounts[i];
836 			}
837 		}
838 		if (imapAccounts) {
839 			for (var i=0; i<imapAccounts.length; i++) {
840 				sourceMap[imapAccounts[i].id] = imapAccounts[i];	
841 			}
842 		}
843 		
844 		if (pop3Accounts || imapAccounts) {
845 			var action = new AjxTimedAction(ds, ds.checkStatus, [sourceMap, 2000]);
846 			AjxTimedAction.scheduleAction(action, 10000);  //kick off check in 10 seconds
847 		}
848 	}
849 };
850 
851 /**
852  * Creates & show Task Reminders on delay
853  *
854  * @private
855  */
856 ZmZimbraMail.prototype.handleTaskComponents =
857 function() {
858 	var reminderAction = new AjxTimedAction(this, this.showTaskReminder);
859 	var delay = appCtxt.isOffline ? 0 : ZmTasksApp.REMINDER_START_DELAY;
860 	AjxTimedAction.scheduleAction(reminderAction, delay);
861 };
862 
863 /**
864  * Creates mini calendar and shows reminders on delay
865  * 
866  * @private
867  */
868 ZmZimbraMail.prototype.handleCalendarComponents =
869 function() {
870 	if (appCtxt.get(ZmSetting.CAL_ALWAYS_SHOW_MINI_CAL)) {
871         var miniCalAction = new AjxTimedAction(this, this.showMiniCalendar);
872 		var delay = appCtxt.isOffline ? 0 : ZmCalendarApp.MINICAL_DELAY;
873         AjxTimedAction.scheduleAction(miniCalAction, delay);
874 	}
875 
876 	AjxDispatcher.require(["ContactsCore", "MailCore", "CalendarCore", "Calendar"]);
877 	var reminderAction = new AjxTimedAction(this, this.showReminder);
878 	var delay = appCtxt.isOffline ? 0 : ZmCalendarApp.REMINDER_START_DELAY;
879 	AjxTimedAction.scheduleAction(reminderAction, delay);
880 };
881 
882 /**
883  * Startup: part 3
884  * 	- populate user info
885  * 	- create search bar
886  * 	- set up keyboard handling (shortcuts and tab groups)
887  * 	- kill splash, show UI
888  * 	- check license
889  *
890  * @param {Hash}	params			a hash of parameters
891  * @param {constant}	params.app				the starting app
892  * @param {Object}	params.settingOverrides	a hash of overrides of user settings
893  *        
894  * @private
895  */
896 ZmZimbraMail.prototype._handleResponseStartup1 =
897 function(params) {
898 
899 	this._setExternalLinks();
900 	this.setUserInfo();
901 	this._setRefresh();
902 
903 	if (appCtxt.get(ZmSetting.SEARCH_ENABLED)) {
904 		this._components[ZmAppViewMgr.C_SEARCH] = appCtxt.getSearchController().getSearchToolbar();
905 	}
906 	else {
907 		Dwt.hide(ZmId.SKIN_SEARCH);
908 	}
909 	
910 	var newButton = this.getNewButton();
911 	var tbParams = {
912 		parent:				this._shell,
913 		context:			ZmOperation.NEW_MENU,
914 		buttons:			ZmOperation.NONE,
915 		controller:			this,
916 		refElementId:		ZmId.SKIN_APP_NEW_BUTTON
917 	};
918 	var tb = this._newToolbar = new ZmButtonToolBar(tbParams);
919 	newButton.reparent(tb);
920 	this._components[ZmAppViewMgr.C_NEW_BUTTON] = tb;
921 	
922 	if (params.unitTest) {
923 		var utm = window.unitTestManager;
924 		appCtxt.addZimletsLoadedListener(utm.runTests.bind(utm), 0);
925 	}
926 
927 	this.getKeyMapMgr();	// make sure keyboard handling is initialized
928 
929 	this.setSessionTimer(true);
930 	ZmZimbraMail.killSplash();
931 
932 	// Give apps a chance to add their own UI components.
933 	this.runAppFunction("addComponents", false, this._components);
934 
935 	// make the UI appear
936 	this._appViewMgr.setViewComponents(ZmAppViewMgr.GLOBAL, this._components, true);
937 
938 	this._checkLicense();
939 
940 	if (!this._doingPostRenderStartup) {
941 		this._postRenderStartup();
942 	}
943 };
944 
945 /**
946  * set the refresh button at the masthead.
947  */
948 ZmZimbraMail.prototype._setRefresh =
949 function() {
950 	var containerEl = document.getElementById(ZmId.SKIN_REFRESH);
951 	if (!containerEl) {
952 		return;
953 	}
954 	var button = appCtxt.refreshButton = new DwtToolBarButton({parent:DwtShell.getShell(window), id: ZmId.OP_CHECK_MAIL}); //use ToolbarButton just for the style, for now it looks ok.
955 	button.setImage("RefreshAll");
956 	button.setToolTipContent(ZmMsg.checkMailPrefUpdate, true);
957 
958 	button.reparentHtmlElement(ZmId.SKIN_REFRESH);
959 
960 	var refreshListener = this._refreshListener.bind(this);
961 	button.addSelectionListener(refreshListener);
962 
963 };
964 
965 /**
966  * refresh button listener. call runRefresh() of all the enabled apps that have this method defined.
967  */
968 ZmZimbraMail.prototype._refreshListener =
969 function() {
970 	if (!appCtxt.isWebClientOffline()) {
971 		this.runAppFunction("runRefresh");
972 	}
973 };
974 
975 // popup a warning dialog if there is a problem with the license
976 ZmZimbraMail.prototype._checkLicense =
977 function(ev) {
978 
979 	var status = appCtxt.get(ZmSetting.LICENSE_STATUS);
980 	var msg = ZmSetting.LICENSE_MSG[status];
981 	if (msg) {
982 		AjxDispatcher.require("Startup2");
983 		var dlg = appCtxt.getMsgDialog();
984 		dlg.reset();
985         dlg.setMessage(msg, DwtMessageDialog.WARNING_STYLE);
986 		dlg.popup();
987 	}
988 };
989 
990 /**
991  * The work to render the start app has been done. Now perform all the startup
992  * work that remains - each piece of work is contained in a callback with an
993  * associated order and delay.
994  * 
995  * @private
996  */
997 ZmZimbraMail.prototype._postRenderStartup =
998 function(ev) {
999 	this._postRenderCallbacks.sort(function(a, b) {
1000 		return a.order - b.order;
1001 	});
1002 	this._runNextPostRenderCallback();
1003 };
1004 
1005 /**
1006  * @private
1007  */
1008 ZmZimbraMail.prototype._runNextPostRenderCallback =
1009 function() {
1010 	DBG.println(AjxDebug.DBG2, "POST-RENDER CALLBACKS: " + this._postRenderCallbacks.length);
1011 	if (this._postRenderCallbacks && this._postRenderCallbacks.length) {
1012 		var prcb = this._postRenderCallbacks.shift();
1013 		if (!prcb) { return; }
1014 		DBG.println(AjxDebug.DBG2, "POST-RENDER CALLBACK: #" + prcb.order + ", delay " + prcb.delay + " in " + prcb.callback.obj.toString());
1015 		AjxTimedAction.scheduleAction(new AjxTimedAction(this,
1016 			function() {
1017 				prcb.callback.run();
1018 				this._runNextPostRenderCallback();
1019 			}), prcb.delay);
1020 	} else {
1021 		if (appCtxt.isOffline) {
1022 			this.sendClientEventNotify(ZmZimbraMail.UI_LOAD_END);
1023 
1024 			if (AjxEnv.isPrism) {
1025 				this._firstTimeNetworkChange = true;
1026 
1027 				var nc = new ZimbraNetworkChecker();
1028 				nc.addEventListener("offline", function(e) { window["appCtxt"].getAppController().handleNetworkChange(false); }, false);
1029 				nc.addEventListener("online", function(e) { window["appCtxt"].getAppController().handleNetworkChange(true); }, false);
1030 			}
1031 		}
1032 	}
1033 };
1034 
1035 /**
1036  * @private
1037  */
1038 ZmZimbraMail.prototype.handleNetworkChange =
1039 function(online) {
1040 	this._isPrismOnline = online;
1041 
1042 	if (this._isUserOnline || this._firstTimeNetworkChange) {
1043 		this._updateNetworkStatus(online);
1044 	}
1045 };
1046 
1047 ZmZimbraMail.prototype._updateNetworkStatus =
1048 function(online) {
1049 	// bug 48108 - Prism sometimes triggers network status change mutliple times
1050 	// So don't bother if the last change is the same as current status
1051 	if ((online && this._currentNetworkStatus == ZmZimbraMail.UI_NETWORK_UP) ||
1052 		(!online && this._currentNetworkStatus == ZmZimbraMail.UI_NETWORK_DOWN))
1053 	{
1054 		return;
1055 	}
1056 
1057 	if (online) {
1058 		if (!this._firstTimeNetworkChange) {
1059 			this.setStatusMsg(ZmMsg.networkChangeOnline);
1060 		} else {
1061 			this._firstTimeNetworkChange = false;
1062 			this._isUserOnline = online;
1063 		}
1064 		this._currentNetworkStatus = ZmZimbraMail.UI_NETWORK_UP;
1065         this.sendClientEventNotify(this._currentNetworkStatus, true);
1066 	} else {
1067 		this.setStatusMsg(ZmMsg.networkChangeOffline, ZmStatusView.LEVEL_WARNING);
1068 		this._currentNetworkStatus = ZmZimbraMail.UI_NETWORK_DOWN;
1069         this.sendClientEventNotify(this._currentNetworkStatus);
1070 	}
1071 
1072 	this._networkStatusIcon.setToolTipContent(online ? ZmMsg.networkStatusOffline : ZmMsg.networkStatusOnline, true);
1073 	this._networkStatusIcon.getHtmlElement().innerHTML = AjxImg.getImageHtml(online ? "Connect" : "Disconnect");
1074 	var netStatus = online ? ZmMsg.netStatusOnline : ZmMsg.netStatusOffline;
1075 	this._networkStatusText.getHtmlElement().innerHTML = netStatus.substr(0, 1).toUpperCase() + netStatus.substr(1);
1076 };
1077 
1078 /**
1079  * Sets up a callback to be run after the starting app has rendered, if we're doing
1080  * post-render callbacks. The callback is registered with an order that determines
1081  * when it will run relative to other callbacks. A delay can also be given, so that
1082  * the UI has a chance to do some work between callbacks.
1083  *
1084  * @param {AjxCallback}	callback		the callback
1085  * @param {int}	order			the run order for the callback
1086  * @param {int}	delay			how long to pause before running the callback
1087  * @param {Boolean}	runNow		if <code>true</code>, we are not doing post-render callbacks, run the callback now and don't add it to the list
1088  */
1089 ZmZimbraMail.prototype.addPostRenderCallback =
1090 function(callback, order, delay, runNow) {
1091 	if (!this._doingPostRenderStartup && runNow) {
1092 		callback.run();
1093 	} else {
1094 		order = order || this._postRenderLast++;
1095 		this._postRenderCallbacks.push({callback:callback, order:order, delay:delay || 0});
1096 	}
1097 };
1098 
1099 ZmZimbraMail.prototype._isInternalApp =
1100 function(app) {
1101 	return !ZmApp.SETTING[app] || (appCtxt.get(ZmApp.SETTING[app], null, appCtxt.multiAccounts && appCtxt.accountList.mainAccount));
1102 };
1103 
1104 ZmZimbraMail.prototype._isIframeApp =
1105 function(app) {
1106 	return !this._isInternalApp(app) && appCtxt.get(ZmApp.UPSELL_SETTING[app]);
1107 };
1108 
1109 /**
1110  * @private
1111  */
1112 ZmZimbraMail.prototype._getStartApp =
1113 function(params) {
1114 	// determine starting app
1115 	var startApp;
1116 	var account = appCtxt.multiAccounts && appCtxt.accountList.mainAccount;
1117 	if (params && params.app) {
1118 		startApp = ZmApp.QS_ARG_R[params.app.toLowerCase()];
1119 		// make sure app given in QS is actually enabled
1120 		// an app is valid if it's enabled as internal, iframe, or external
1121 		if (!this._isInternalApp(startApp) && !this._isIframeApp(startApp)) {
1122 			startApp = null;
1123 		}
1124 	}
1125 	if (!startApp) {
1126 		for (var app in ZmApp.DEFAULT_SORT) {
1127 			ZmApp.DEFAULT_APPS.push(app);
1128 		}
1129 		ZmApp.DEFAULT_APPS.sort(function(a, b) {
1130 			return ZmZimbraMail.hashSortCompare(ZmApp.DEFAULT_SORT, a, b);
1131 		});
1132 		var defaultStartApp = null;
1133 		for (var i = 0; i < ZmApp.DEFAULT_APPS.length; i++) {
1134 			var app = ZmApp.DEFAULT_APPS[i];
1135 			if (this._isInternalApp(app)) {
1136 				defaultStartApp = app;
1137 				break;
1138 			}
1139 		}
1140 		startApp = this._getDefaultStartAppName(account);
1141 	}
1142 
1143 	// parse query string, in case we are coming in with a deep link	
1144 	var qsParams = AjxStringUtil.parseQueryString();
1145 	if (qsParams && qsParams.view && !qsParams.app) {
1146 		startApp = ZmApp.QS_VIEWS[qsParams.view];
1147 	}
1148 
1149 	params.startApp = startApp;
1150 	params.qsParams = qsParams;
1151 };
1152 
1153 /**
1154  * @private
1155  */
1156 ZmZimbraMail.prototype._getDefaultStartAppName =
1157 function(account) {
1158 	account = account || (appCtxt.multiAccounts && appCtxt.accountList.mainAccount) || null;
1159 	
1160 	for (var app in ZmApp.DEFAULT_SORT) {
1161 		ZmApp.DEFAULT_APPS.push(app);
1162 	}
1163 	ZmApp.DEFAULT_APPS.sort(function(a, b) {
1164 		return ZmZimbraMail.hashSortCompare(ZmApp.DEFAULT_SORT, a, b);
1165 	});
1166 	var defaultStartApp = null;
1167 	for (var i = 0; i < ZmApp.DEFAULT_APPS.length; i++) {
1168 		var app = ZmApp.DEFAULT_APPS[i];
1169 		var setting = ZmApp.SETTING[app];
1170 		if (!setting || appCtxt.get(setting, null, account)) {
1171 			return app;
1172 		}
1173 	}
1174 };
1175 
1176 /**
1177  * Cancels the request.
1178  * 
1179  * @param	{String}	reqId		the request id
1180  * @param	{AjxCallback}	errorCallback		the callback
1181  * @param	{Boolean}	noBusyOverlay	if <code>true</code>, do not show busy overlay
1182  * @see	ZmRequestMgr#cancelRequest
1183  */
1184 ZmZimbraMail.prototype.cancelRequest =
1185 function(reqId, errorCallback, noBusyOverlay) {
1186 	this._requestMgr.cancelRequest(reqId, errorCallback, noBusyOverlay);
1187 };
1188 
1189 /**
1190  * Sends the request.
1191  * 
1192  * @param	{Hash}	params		a hash of parameters
1193  * @see	ZmRequestMgr#sendRequest
1194  */
1195 ZmZimbraMail.prototype.sendRequest =
1196 function(params) {
1197 	return this._requestMgr.sendRequest(params);
1198 };
1199 
1200 /**
1201  * Runs the given function for all enabled apps, passing args.
1202  *
1203  * @param {String}	funcName		the function name
1204  * @param {Boolean}	force			if <code>true</code>, run func for disabled apps as well
1205  */
1206 ZmZimbraMail.prototype.runAppFunction =
1207 function(funcName, force) {
1208 	var args = [];
1209 	for (var i = 2; i < arguments.length; i++) {
1210 		args.push(arguments[i]);
1211 	}
1212 	for (var i = 0; i < ZmApp.APPS.length; i++) {
1213 		var appName = ZmApp.APPS[i];
1214 		var setting = this._isIframeApp(appName) ? ZmApp.UPSELL_SETTING[appName] : ZmApp.SETTING[appName];
1215 		var account = appCtxt.multiAccounts && appCtxt.accountList.mainAccount;
1216 		if (!setting || appCtxt.get(setting, null, account) || force) {
1217 			var app = appCtxt.getApp(appName, null, account);
1218 			var func = app && app[funcName];
1219 			if (func && (typeof(func) == "function")) {
1220 				func.apply(app, args);
1221 			}
1222 		}
1223 	}
1224 	appCtxt.notifyZimlets("runAppFunction", [funcName]);
1225 };
1226 
1227 /**
1228  * Instantiates enabled apps. An optional argument may be given limiting the set
1229  * of apps that may be created.
1230  *
1231  * @param {Hash}	apps	the set of apps to create
1232  * 
1233  * @private
1234  */
1235 ZmZimbraMail.prototype._createEnabledApps =
1236 function(apps) {
1237     this._apps = {};
1238 
1239 	for (var app in ZmApp.CLASS) {
1240 		if (!apps || apps[app]) {
1241 			ZmApp.APPS.push(app);
1242 		}
1243 	}
1244 	ZmApp.APPS.sort(function(a, b) {
1245 		return ZmZimbraMail.hashSortCompare(ZmApp.LOAD_SORT, a, b);
1246 	});
1247 
1248 	// Instantiate enabled apps, which will invoke app registration.
1249 	// We also create iframed (external) apps, which will only show the content of a URL in an iframe.
1250 	for (var i = 0; i < ZmApp.APPS.length; i++) {
1251 		var app = ZmApp.APPS[i];
1252 		var isInternal = this._isInternalApp(app);
1253 		var isIframe = this._isIframeApp(app);
1254 		if (isInternal || isIframe || app === ZmApp.BRIEFCASE) {
1255 			ZmApp.ENABLED_APPS[app] = isInternal || isIframe;
1256 			this._createApp(app);
1257 			this._apps[app].isIframe = isIframe;
1258 		}
1259 	}
1260 };
1261 
1262 /**
1263  * Static function to add a listener before this class has been instantiated.
1264  * During construction, listeners are copied to the event manager. This function
1265  * could be used by a skin, for example.
1266  *
1267  * @param {constant}	type		the event type
1268  * @param {AjxListener}	listener	a listener
1269  */
1270 ZmZimbraMail.addListener =
1271 function(type, listener) {
1272 	if (!ZmZimbraMail._listeners[type]) {
1273 		ZmZimbraMail._listeners[type] = [];
1274 	}
1275 	ZmZimbraMail._listeners[type].push(listener);
1276 };
1277 
1278 /**
1279  * Static function to add an app listener before this class has been
1280  * instantiated. This is separate from {@link ZmZimbraMail#addListener}
1281  * so that the caller doesn't need to know the specifics of how we
1282  * twiddle the type name for app events.
1283  * 
1284  * @param	{String}	appName		the application name
1285  * @param {constant}	type		the event type
1286  * @param {AjxListener}	listener	a listener
1287  * 
1288  */
1289 ZmZimbraMail.addAppListener =
1290 function(appName, type, listener) {
1291 	type = [appName, type].join("_");
1292 	ZmZimbraMail.addListener(type, listener);
1293 };
1294 
1295 /**
1296  * Adds a listener for the given event type.
1297  *
1298  * @param {constant}	type		the event type
1299  * @param {AjxListener}	listener	a listener
1300  * @return	{Boolean}	<code>true</code> if the listener is added
1301  * 
1302  */
1303 ZmZimbraMail.prototype.addListener =
1304 function(type, listener) {
1305 	return this._evtMgr.addListener(type, listener);
1306 };
1307 
1308 /**
1309  * Removes a listener for the given event type.
1310  *
1311  * @param {constant}	type		the event type
1312  * @param {AjxListener}	listener	a listener
1313  * @return	{Boolean}	<code>true</code> if the listener is removed
1314  */
1315 ZmZimbraMail.prototype.removeListener =
1316 function(type, listener) {
1317 	return this._evtMgr.removeListener(type, listener);
1318 };
1319 
1320 /**
1321  * Adds a listener for the given event type and app.
1322  *
1323  * @param {constant}	app		the app name
1324  * @param {constant}	type		the event type
1325  * @param {AjxListener}	listener	a listener
1326  * @return	{Boolean}	<code>true</code> if the listener is added
1327  */
1328 ZmZimbraMail.prototype.addAppListener =
1329 function(app, type, listener) {
1330 	type = [app, type].join("_");
1331 	return this.addListener(type, listener);
1332 };
1333 
1334 /**
1335  * Removes a listener for the given event type and app.
1336  *
1337  * @param {constant}	app		the app name
1338  * @param {constant}	type		the event type
1339  * @param {AjxListener}	listener	a listener
1340  * @return	{Boolean}	<code>true</code> if the listener is removed
1341  */
1342 ZmZimbraMail.prototype.removeAppListener =
1343 function(app, type, listener) {
1344 	type = [app, type].join("_");
1345 	return this.removeListener(type, listener);
1346 };
1347 
1348 /**
1349  * Sends a <code><NoOpRequest></code> to the server. Used for '$set:noop'
1350  */
1351 ZmZimbraMail.prototype.sendNoOp =
1352 function() {
1353 	var jsonObj = { NoOpRequest: { _jsns: "urn:zimbraMail" } };
1354 	var accountName = appCtxt.isOffline && appCtxt.accountList.mainAccount.name;
1355 	this.sendRequest({jsonObj:jsonObj, asyncMode:true, noBusyOverlay:true, accountName:accountName});
1356 };
1357 
1358 /**
1359  * Sends a <code><ClientEventNotifyRequest></code> to the server.
1360  * 
1361  * @param	{Object}	event		the event
1362  */
1363 ZmZimbraMail.prototype.sendClientEventNotify =
1364 function(event, isNetworkOn) {
1365 	var params = {
1366 		jsonObj: {
1367 			ClientEventNotifyRequest: {
1368 				_jsns:"urn:zimbraOffline",
1369 				e: event
1370 			}
1371 		},
1372 		asyncMode:true
1373 	};
1374 
1375     if (isNetworkOn) {
1376         params.callback = new AjxCallback(this, this.handleClientEventNotifyResponse, event);
1377         params.noBusyOverlay = true;
1378 
1379         if (this.clientEventNotifyReqId) {
1380             appCtxt.getRequestMgr().cancelRequest(this.clientEventNotifyReqId);
1381         }
1382         this.clientEventNotifyTimerId = 
1383             AjxTimedAction.scheduleAction(new AjxTimedAction(this, this.sendClientEventNotify, [event, true]), 3000);
1384     } else {
1385         params.callback = new AjxCallback(this, this.setInstantNotify, true);
1386     }
1387 
1388     this.clientEventNotifyReqId = this.sendRequest(params);
1389 };
1390 
1391 ZmZimbraMail.prototype.handleClientEventNotifyResponse =
1392 function(event, res) {
1393     if (!res.isException() && res.getResponse()) {
1394         if (this.clientEventNotifyTimerId) {
1395             AjxTimedAction.cancelAction(this.clientEventNotifyTimerId);
1396             this.clientEventNotityTimerId = null;
1397         }
1398         this.setInstantNotify(true);
1399     }
1400 };
1401 
1402 /**
1403  * Sets the client into "instant notifications" mode.
1404  * 
1405  * @param {Boolean}	on				if <code>true</code>, turn on instant notify
1406  */
1407 ZmZimbraMail.prototype.setInstantNotify =
1408 function(on) {
1409 	if (on) {
1410 		this._pollInstantNotifications = true;
1411 		// set nonzero poll interval so cant ever get into a full-speed request loop
1412 		this._pollInterval = appCtxt.get(ZmSetting.INSTANT_NOTIFY_INTERVAL);
1413 		if (this._pollActionId) {
1414 			AjxTimedAction.cancelAction(this._pollActionId);
1415 			this._pollActionId = null;
1416 		}
1417 		this._kickPolling(true);
1418 	} else {
1419 		this._pollInstantNotifications = false;
1420 		this._cancelInstantNotify();
1421 		this.setPollInterval(true);
1422 	}
1423 };
1424 
1425 /**
1426  * Gets the "instant notification" setting.
1427  * 
1428  * @return	{Boolean}	<code>true</code> if instant notification is "ON"
1429  */
1430 ZmZimbraMail.prototype.getInstantNotify =
1431 function() {
1432 	return this._pollInstantNotifications;
1433 };
1434 
1435 ZmZimbraMail.prototype.registerMailtoHandler =
1436 function(regProto, selected) {
1437 	if (appCtxt.get(ZmSetting.OFFLINE_SUPPORTS_MAILTO) && window.platform) {
1438 		try { // add try/catch - see bug #33870
1439 			if (selected) { // user selected zd as default mail app 
1440 				// register mailto handler
1441 				if (regProto) {
1442 					var url = appCtxt.get(ZmSetting.OFFLINE_WEBAPP_URI, null, appCtxt.accountList.mainAccount);
1443 					window.platform.registerProtocolHandler("mailto", url + "&mailto=%s");
1444 				
1445 					// handle "send to mail recipient" on windows (requires mapi@zimbra.com extension)
1446 					if (AjxEnv.isWindows) {
1447 						var shell = new ZimbraDesktopShell;
1448 						shell.defaultClient = true;
1449 					}
1450 				}
1451 
1452 				// register mailto callback
1453 				var callback = AjxCallback.simpleClosure(this.handleOfflineMailTo, this);
1454 				window.platform.registerProtocolCallback("mailto", callback);
1455 			} else { // unselected (box unchecked) 
1456 				window.platform.unregisterProtocolHandler("mailto");
1457 
1458 				if (AjxEnv.isWindows) {
1459 					var shell = new ZimbraDesktopShell;
1460 					shell.defaultClient = false;
1461 				}
1462 			}
1463 		} catch(ex) {
1464 			// do nothing
1465 		}
1466 	}
1467 };
1468 
1469 /**
1470  * @private
1471  */
1472 ZmZimbraMail.prototype.handleOfflineMailTo =
1473 function(uri, callback) {
1474 	if (window.platform && !window.platform.isRegisteredProtocolHandler("mailto")) { return false; }
1475 
1476 	var mailApp = this.getApp(ZmApp.MAIL);
1477 	var idx = (uri.indexOf("mailto"));
1478 	if (idx >= 0) {
1479 		var query = "to=" + decodeURIComponent(uri.substring(idx+7));
1480 		query = query.replace(/\?/g, "&");
1481 		var controller = mailApp._showComposeView(callback, query);
1482         	this._checkOfflineMailToAttachments(controller, query);
1483 		return true;
1484 	}
1485 	return false;
1486 };
1487 
1488 ZmZimbraMail.prototype._checkOfflineMailToAttachments =
1489 function(controller, queryStr) {
1490     var qs = queryStr || location.search;
1491 
1492     var match = qs.match(/\bto=([^&]+)/);
1493     var to = match ? AjxStringUtil.urlComponentDecode(match[1]) : null;
1494 
1495     match = qs.match(/\battachments=([^&]+)/);
1496     var attachments = match ? (AjxStringUtil.urlComponentDecode(match[1]).replace(/\+/g, " ")) : null;
1497 
1498     if (to && to.indexOf('mailto') == 0) {
1499         to = to.replace(/mailto:/,'');
1500         var mailtoQuery = to.split('?');
1501         if (mailtoQuery.length > 1) {
1502             mailtoQuery = mailtoQuery[1];
1503             match = mailtoQuery.match(/\battachments=([^&]+)/);
1504             if(!attachments) attachments = match ? (AjxStringUtil.urlComponentDecode(match[1]).replace(/\+/g, " ")) : null;
1505         }
1506     }
1507 
1508     if(attachments) {
1509         attachments = attachments.replace(/;$/, "");
1510         attachments = attachments.split(";");
1511         this._mailtoAttachmentsLength = attachments.length;
1512         this._attachmentsProcessed = 0;        
1513         this.attachment_ids = [];
1514         for(var i=0; i<attachments.length; i++) {
1515             this._handleMailToAttachment(attachments[i], controller);
1516         }
1517     }
1518 };
1519 
1520 ZmZimbraMail.prototype._handleMailToAttachment =
1521 function(attachment, controller) {
1522 
1523     var filePath = attachment;
1524     var filename = filePath.replace(/^.*\\/, '');
1525 
1526     DBG.println("Uploading File :" + filename + ",filePath:" + filePath);
1527 
1528     //check read file permission;
1529     try {
1530         netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
1531     } catch (e) {
1532         //permission denied to read file
1533         DBG.println("Permission denied to read file");
1534         return;
1535     }
1536 
1537     try {
1538         var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
1539         file.initWithPath( filePath );
1540 
1541         var contentType = this._getAttachmentContentType(file);
1542 
1543         var inputStream = Components.classes[ "@mozilla.org/network/file-input-stream;1" ].createInstance(Components.interfaces.nsIFileInputStream);
1544         inputStream.init(file, -1, -1, false );
1545 
1546         var binary = Components.classes[ "@mozilla.org/binaryinputstream;1" ].createInstance(Components.interfaces.nsIBinaryInputStream);
1547         binary.setInputStream(inputStream);
1548 
1549         var req = new XMLHttpRequest();
1550         req.open("POST", appCtxt.get(ZmSetting.CSFE_UPLOAD_URI)+"&fmt=extended,raw", true);
1551         req.setRequestHeader("Cache-Control", "no-cache");
1552         req.setRequestHeader("X-Requested-With", "XMLHttpRequest");
1553         req.setRequestHeader("Content-Type",  (contentType || "application/octet-stream") );
1554         req.setRequestHeader("Content-Disposition", 'attachment; filename="'+ AjxUtil.convertToEntities(filename) + '"');
1555 
1556         var reqObj = req;
1557         req.onreadystatechange = AjxCallback.simpleClosure(this._handleUploadResponse, this, reqObj, controller);
1558         req.sendAsBinary(binary.readBytes(binary.available()));
1559         delete req;
1560     }catch(ex) {
1561         DBG.println("exception in handling attachment: " + attachment);
1562         DBG.println(ex);
1563         this._attachmentsProcessed++;
1564     }
1565 };
1566 
1567 ZmZimbraMail.prototype._getAttachmentContentType =
1568 function(file) {
1569 	var contentType;
1570 	try {
1571 		contentType = Components.classes["@mozilla.org/mime;1"].getService(Components.interfaces.nsIMIMEService).getTypeFromFile(file);
1572 	}catch(ex) {
1573 		 DBG.println("exception in reading content type: " + ex);
1574 		 contentType =  "application/octet-stream";
1575 	}
1576 	return contentType;
1577 };
1578 
1579 ZmZimbraMail.prototype._handleUploadResponse = function(req, controller) {
1580     if(req) {
1581         if(req.readyState == 4 && req.status == 200) {
1582             var resp = eval("["+req.responseText+"]");
1583             this._attachmentsProcessed++;
1584             this.popupUploadErrorDialog(ZmItem.MSG, resp[0]);
1585             if(resp.length > 2) {
1586                 var respObj = resp[2];
1587                 for (var i = 0; i < respObj.length; i++) {
1588                     if(respObj[i].aid != "undefined") {
1589                         this.attachment_ids.push(respObj[i].aid);
1590                     }
1591                 }
1592 
1593                 if(this.attachment_ids.length > 0 && this._attachmentsProcessed == this._mailtoAttachmentsLength) {
1594                     var attachment_list = this.attachment_ids.join(",");
1595                     if(!controller) {
1596                         var msg = new ZmMailMsg();
1597                         controller = AjxDispatcher.run("GetComposeController");
1598                         controller._setView({action:ZmOperation.NEW_MESSAGE, msg:msg, inNewWindow:false});
1599                     }
1600                     var callback = new AjxCallback (controller,controller._handleResponseSaveDraftListener);
1601         		    controller.sendMsg(attachment_list, ZmComposeController.DRAFT_TYPE_MANUAL,callback);
1602                     this.getAppViewMgr().pushView(controller.getCurrentViewId());
1603                 }
1604             }
1605         }
1606     }
1607 
1608 };
1609 
1610 /**
1611  * Resets the interval between poll requests, based on what's in the settings,
1612  * only if we are not in instant notify mode.
1613  *
1614  * @param {Boolean}	kickMe	if <code>true</code>, start the poll timer
1615  * @return	{Boolean}	<code>true</code> if poll interval started; <code>false</code> if in "instant notification" mode
1616  */
1617 ZmZimbraMail.prototype.setPollInterval =
1618 function(kickMe) {
1619 	if (!this._pollInstantNotifications) {
1620 		this._pollInterval = appCtxt.get(ZmSetting.POLLING_INTERVAL) * 1000;
1621 
1622 		if (this._pollInterval) {
1623 			DBG.println(AjxDebug.DBG1, "poll interval = " + this._pollInterval + "ms");
1624 			if (kickMe)
1625 				this._kickPolling(true);
1626 		} else {
1627 			// cancel timer if it is waiting...
1628 			if (this._pollActionId) {
1629 				AjxTimedAction.cancelAction(this._pollActionId);
1630 				this._pollActionId = null;
1631 			}
1632 		}
1633 		return true;
1634 	} else {
1635 		this._pollInterval = appCtxt.get(ZmSetting.INSTANT_NOTIFY_INTERVAL);
1636 		DBG.println(AjxDebug.DBG1, "Ignoring Poll Interval (in instant-notify mode)");
1637 		return false;
1638 	}
1639 };
1640 
1641 /**
1642  * @private
1643  */
1644 ZmZimbraMail.prototype._cancelInstantNotify =
1645 function() {
1646 	if (this._pollRequest) {
1647 		this._requestMgr.cancelRequest(this._pollRequest);
1648 		this._pollRequest = null;
1649 	}
1650 
1651 	if (this._pollActionId) {
1652 		AjxTimedAction.cancelAction(this._pollActionId);
1653 		this._pollActionId = null;
1654 	}
1655 };
1656 
1657 /**
1658  * Make sure the polling loop is running.  Basic flow:
1659  *
1660  *       1) kickPolling():
1661  *             - cancel any existing timers
1662  *             - set a timer for _pollInterval time
1663  *             - call execPoll() when the timer goes off
1664  *
1665  *       2) execPoll():
1666  *             - make the NoOp request, if we're in "instant notifications"
1667  *               mode, this request will hang on the server until there is more data,
1668  *               otherwise it will return immediately.  Call into a handle() func below
1669  *
1670  *       3) handleDoPollXXXX():
1671  *             - call back to kickPolling() above
1672  *
1673  * resetBackoff = TRUE e.g. if we've just received a successful
1674  * response from the server, or if the user just changed our
1675  * polling settings and we want to start in fast mode
1676  * 
1677  * @private
1678  */
1679 ZmZimbraMail.prototype._kickPolling =
1680 function(resetBackoff) {
1681 	DBG.println(AjxDebug.DBG2, [
1682 		"ZmZimbraMail._kickPolling ",
1683 		this._pollInterval, ", ",
1684 		this._pollActionId, ", ",
1685 		this._pollRequest ? "request_pending" : "no_request_pending"
1686 	].join(""));
1687 
1688 	// reset the polling timeout
1689 	if (this._pollActionId) {
1690 		AjxTimedAction.cancelAction(this._pollActionId);
1691 		this._pollActionId = null;
1692 	}
1693 
1694 	if (resetBackoff && this._pollInstantNotifications) {
1695 		// we *were* backed off -- reset the delay back to 1s fastness
1696 		var interval = appCtxt.get(ZmSetting.INSTANT_NOTIFY_INTERVAL);
1697 		if (this._pollInterval > interval) {
1698 			this._pollInterval = interval;
1699 		}
1700 	}
1701 
1702 	if (this._pollInterval && !this._pollRequest) {
1703 		try {
1704 			this._pollActionId = AjxTimedAction.scheduleAction(new AjxTimedAction(this, this._execPoll), this._pollInterval);
1705 		} catch (ex) {
1706 			this._pollActionId = null;
1707 			DBG.println(AjxDebug.DBG1, "Caught exception in ZmZimbraMail._kickPolling.  Polling chain broken!");
1708 		}
1709 	}
1710 };
1711 
1712 /**
1713  * We've finished waiting, do the actual poll itself
1714  *
1715  * @private
1716  */
1717 ZmZimbraMail.prototype._execPoll =
1718 function() {
1719 
1720 	this._cancelInstantNotify();
1721 
1722 	// It'd be more efficient to make these instance variables, but for some
1723 	// reason that breaks polling in IE.
1724 	var jsonObj = { NoOpRequest: { _jsns: "urn:zimbraMail" } },
1725 		method = jsonObj.NoOpRequest;
1726 
1727 	try {
1728         if (this._pollInstantNotifications) {
1729 			var sessionId = ZmCsfeCommand.getSessionId();
1730 			if (sessionId) {
1731 				method.wait = 1;
1732 				method.limitToOneBlocked = 1;
1733 			}
1734         }
1735 		var params = {
1736 			jsonObj:        jsonObj,
1737 			asyncMode:      true,
1738 			callback:       this._handleResponseDoPoll.bind(this),
1739 			errorCallback:  this._handleErrorDoPoll.bind(this),
1740 			noBusyOverlay:  true,
1741 			timeout:        appCtxt.get(ZmSetting.INSTANT_NOTIFY_TIMEOUT),
1742 			accountName:    appCtxt.isOffline && appCtxt.accountList.mainAccount.name
1743 		};
1744 		this._pollRequest = this.sendRequest(params);
1745 
1746 		// bug #42664 - handle case where sync-status-changes fall between 2 client requests
1747 		var accList = appCtxt.accountList;
1748 		if (appCtxt.isOffline && !accList.isInitialSyncing() && accList.isSyncStatus(ZmZimbraAccount.STATUS_RUNNING)) {
1749 			this.sendNoOp();
1750 		}
1751 	} catch (ex) {
1752 		this._handleErrorDoPoll(ex); // oops!
1753 	}
1754 };
1755 
1756 /**
1757  * @private
1758  */
1759 ZmZimbraMail.prototype._handleErrorDoPoll =
1760 function(ex) {
1761 	if (this._pollRequest) {
1762 		// reset the polling timeout
1763 		if (this._pollActionId) {
1764 			AjxTimedAction.cancelAction(this._pollActionId);
1765 			this._pollActionId = null;
1766 		}
1767 		this._requestMgr.cancelRequest(this._pollRequest);
1768 		this._pollRequest = null;
1769 	}
1770 
1771 	if (this._pollInstantNotifications) {
1772 		// very simple-minded exponential backoff
1773 		this._pollInterval *= 2;
1774 		if (this._pollInterval > (1000 * 60 * 2)) {
1775 			this._pollInterval = 1000 * 60 * 2;
1776 		}
1777 	}
1778 
1779 	var isAuthEx = (ex.code == ZmCsfeException.SVC_AUTH_EXPIRED ||
1780 					ex.code == ZmCsfeException.SVC_AUTH_REQUIRED ||
1781 					ex.code == ZmCsfeException.NO_AUTH_TOKEN);
1782 
1783 	// restart poll timer if we didn't get an auth exception
1784 	if (!isAuthEx) {
1785 		this._kickPolling(false);
1786 	}
1787 
1788 	return !isAuthEx;
1789 };
1790 
1791 /**
1792  * @private
1793  */
1794 ZmZimbraMail.prototype._handleResponseDoPoll =
1795 function(result) {
1796 	this._pollRequest = null;
1797 	var noopResult = result.getResponse().NoOpResponse;
1798 	if (noopResult.waitDisallowed) {
1799 		this._waitDisallowed = true;
1800 		// revert to polling mode - server doesn't want us to use instant notify.
1801 		this.setInstantNotify(false);
1802 	}  else {
1803 		// restart poll timer if we didn't get an exception
1804 		this._kickPolling(true);
1805 	}
1806 };
1807 
1808 /**
1809  * Gets the key map manager.
1810  * 
1811  * @return	{DwtKeyMapMgr}	the key map manager
1812  */
1813 ZmZimbraMail.prototype.getKeyMapMgr =
1814 function() {
1815 	var kbMgr = appCtxt.getKeyboardMgr();
1816 	if (!kbMgr.__keyMapMgr) {
1817 		this._initKeyboardHandling();
1818 	}
1819 	return kbMgr.__keyMapMgr;
1820 };
1821 
1822 /**
1823  * @private
1824  */
1825 ZmZimbraMail.prototype._initKeyboardHandling =
1826 function() {
1827 	var kbMgr = appCtxt.getKeyboardMgr();
1828 	if (kbMgr.__keyMapMgr) { return; }
1829 	if (appCtxt.get(ZmSetting.USE_KEYBOARD_SHORTCUTS)) {
1830 		// Register our keymap and global key action handler with the shell's keyboard manager
1831 		kbMgr.enable(true);
1832 		kbMgr.registerKeyMap(new ZmKeyMap());
1833 		kbMgr.pushDefaultHandler(this);
1834 	} else {
1835 		kbMgr.enable(false);
1836 	}
1837 };
1838 
1839 /**
1840  * @private
1841  */
1842 ZmZimbraMail.prototype._setupTabGroups =
1843 function() {
1844 	DBG.println(AjxDebug.DBG2, "SETTING SEARCH CONTROLLER TAB GROUP");
1845 	var rootTg = appCtxt.getRootTabGroup();
1846 	if (appCtxt.get(ZmSetting.SEARCH_ENABLED)) {
1847 		rootTg.addMember(appCtxt.getSearchController().getSearchToolbar().getTabGroupMember());
1848 	}
1849 
1850 	rootTg.addMember(this._userNameField);
1851 	rootTg.addMember(this._usedQuotaField);
1852 
1853 	if (this._helpButton) {
1854 		rootTg.addMember(this._helpButton);
1855 	}
1856 
1857 	rootTg.addMember(appCtxt.getAppChooser().getTabGroupMember());
1858 	rootTg.addMember(appCtxt.refreshButton);
1859 	rootTg.addMember(this._newToolbar);
1860 
1861 	var curApp = appCtxt.getCurrentApp();
1862 	var overview = curApp && curApp.getOverview();
1863 	if (overview) {
1864 		rootTg.addMember(overview.getTabGroupMember());
1865 		ZmController._currentOverview = overview;
1866 	}
1867 	
1868 	appCtxt.getKeyboardMgr().setTabGroup(rootTg);
1869 };
1870 
1871 /**
1872  * @private
1873  */
1874 ZmZimbraMail.prototype._registerOrganizers =
1875 function() {
1876 
1877 	ZmOrganizer.registerOrg(ZmOrganizer.FOLDER,
1878 							{app:				ZmApp.MAIL,
1879 							 nameKey:			"folder",
1880 							 defaultFolder:		ZmOrganizer.ID_INBOX,
1881 							 soapCmd:			"FolderAction",
1882 							 firstUserId:		256,
1883 							 orgClass:			"ZmFolder",
1884 							 orgPackage:		"MailCore",
1885 							 treeController:	"ZmMailFolderTreeController",
1886 							 labelKey:			"mailFolders",
1887 							 itemsKey:			"messages",
1888 							 hasColor:			true,
1889 							 defaultColor:		ZmOrganizer.C_NONE,
1890 							 treeType:			ZmOrganizer.FOLDER,
1891 							 dropTargets:		[ZmOrganizer.FOLDER],
1892 							 views:				["message", "conversation"],
1893 							 folderKey:			"mailFolder",
1894 							 mountKey:			"mountFolder",
1895 							 createFunc:		"ZmOrganizer.create",
1896 							 compareFunc:		"ZmFolder.sortCompare",
1897 							 newOp:				ZmOperation.NEW_FOLDER,
1898 							 displayOrder:		100,
1899 							 childWindow:		true,
1900 							 openSetting:		ZmSetting.FOLDER_TREE_OPEN
1901 							});
1902 
1903 	ZmOrganizer.registerOrg(ZmOrganizer.SEARCH,
1904 							{app:				ZmApp.MAIN,
1905 							 nameKey:			"savedSearch",
1906 							 precondition:		ZmSetting.SAVED_SEARCHES_ENABLED,
1907 							 soapCmd:			"FolderAction",
1908 							 firstUserId:		256,
1909 							 orgClass:			"ZmSearchFolder",
1910 							 treeController:	"ZmSearchTreeController",
1911 							 labelKey:			"searches",
1912 							 hasColor:			true,
1913 							 defaultColor:		ZmOrganizer.C_NONE,
1914 							 treeType:			ZmOrganizer.FOLDER,
1915 							 folderKey:			"savedSearch",
1916 							 disableShare:		true,
1917  							 dropTargets:		[ZmOrganizer.FOLDER, ZmOrganizer.SEARCH],
1918 							 createFunc:		"ZmSearchFolder.create",
1919 							 compareFunc:		"ZmFolder.sortCompare",
1920 							 openSetting:		ZmSetting.SEARCH_TREE_OPEN,
1921 							 displayOrder:		300
1922 							});
1923 
1924     ZmOrganizer.registerOrg(ZmOrganizer.SHARE, {
1925         orgClass:       "ZmShareProxy",
1926         treeController: "ZmShareTreeController",
1927         labelKey:       "sharedFoldersHeader",
1928         compareFunc:	"ZmFolder.sortCompare",
1929         displayOrder:	101, // NOTE: Always show shares below primary folder tree
1930         hideEmpty:		false
1931     });
1932 
1933 	ZmOrganizer.registerOrg(ZmOrganizer.TAG,
1934 							{app:				ZmApp.MAIN,
1935 							 nameKey:			"tag",
1936 							 precondition:		ZmSetting.TAGGING_ENABLED,
1937 							 soapCmd:			"TagAction",
1938 							 firstUserId:		64,
1939 							 orgClass:			"ZmTag",
1940 							 treeController:	"ZmTagTreeController",
1941 							 hasColor:			true,
1942 							 defaultColor:		ZmOrganizer.C_ORANGE,
1943 							 labelKey:			"tags",
1944 							 treeType:			ZmOrganizer.TAG,
1945 							 createFunc:		"ZmTag.create",
1946 							 compareFunc:		"ZmTag.sortCompare",
1947 							 newOp:				ZmOperation.NEW_TAG,
1948 							 openSetting:		ZmSetting.TAG_TREE_OPEN,
1949 							 displayOrder:		400
1950 							});
1951 
1952 	ZmOrganizer.registerOrg(ZmOrganizer.ZIMLET,
1953 							{orgClass:			"ZmZimlet",
1954 							 treeController:	"ZmZimletTreeController",
1955 							 labelKey:			"zimlets",
1956 							 compareFunc:		"ZmZimlet.sortCompare",
1957 							 openSetting:		ZmSetting.ZIMLET_TREE_OPEN,
1958 							 hideEmpty:			true
1959 							});
1960 	
1961 	// Technically, we don't need to do this because the drop listeners for dragged organizers typically do their
1962 	// own checks on the class of the dragged object. But it's better to do it anyway, in case it ever gets
1963 	// validated within the drop target against the valid types.
1964 	this._name = ZmApp.MAIN;
1965 	ZmApp.prototype._setupDropTargets.call(this);
1966 };
1967 
1968 /**
1969  * Gets a handle to the given app.
1970  *
1971  * @param {String}	appName		the app name
1972  * @return	{ZmApp}	the app
1973  */
1974 ZmZimbraMail.prototype.getApp =
1975 function(appName) {
1976 	if (!ZmApp.ENABLED_APPS[appName]) {
1977 		return null;
1978 	}
1979 	if (!this._apps[appName]) {
1980 		this._createApp(appName);
1981 	}
1982 	return this._apps[appName];
1983 };
1984 
1985 /**
1986  * Gets a handle to the app view manager.
1987  * 
1988  * @return	{ZmAppViewMgr}	the app view manager
1989  */
1990 ZmZimbraMail.prototype.getAppViewMgr =
1991 function() {
1992 	return this._appViewMgr;
1993 };
1994 
1995 /**
1996  * Gets the active app.
1997  * 
1998  * @return	{ZmApp}	the app
1999  */
2000 ZmZimbraMail.prototype.getActiveApp =
2001 function() {
2002 	return this._activeApp;
2003 };
2004 
2005 /**
2006  * Gets the previous application.
2007  * 
2008  * @return	{ZmApp}	the app
2009  */
2010 ZmZimbraMail.prototype.getPreviousApp =
2011 function() {
2012 	return this._previousApp;
2013 };
2014 
2015 /**
2016  * Activates the given application.
2017  *
2018  * @param {constant}	appName		the application name
2019  * @param {Boolean}	force			if <code>true</code>, launch the app
2020  * @param {AjxCallback}	callback		the callback
2021  * @param {AjxCallback}	errorCallback	the error callback
2022  * @param {Hash}	params		a hash of parameters		(see {@link #startup} for full list)
2023  * @param {Boolean}	params.checkQS		if <code>true</code>, check query string for launch args
2024  * @param {ZmCsfeResult}	params.result		the result object from load of user settings
2025  */
2026 ZmZimbraMail.prototype.activateApp =
2027 function(appName, force, callback, errorCallback, params) {
2028 	DBG.println(AjxDebug.DBG1, "activateApp: " + appName + ", current app = " + this._activeApp);
2029 
2030 	var account = appCtxt.multiAccounts && appCtxt.accountList.mainAccount;
2031 	var isIframe = this._isIframeApp(appName);
2032 	var view = this._appViewMgr.getAppView(appName);
2033 	if (view && !force) {
2034 		// if the app has been launched, make its view the current one
2035 		DBG.println(AjxDebug.DBG3, "activateApp, current " + appName + " view: " + view);
2036 		if (this._appViewMgr.pushView(view)) {
2037 			this._appViewMgr.setAppView(appName, view);
2038             if (isIframe) {
2039                 var title = [ZmMsg.zimbraTitle, appName].join(": ");
2040                 Dwt.setTitle(title);
2041             }            
2042 		}
2043 		if (callback) {
2044 			callback.run();
2045 		}
2046 	} else {
2047 		// launch the app
2048 		if (!this._apps[appName]) {
2049 			this._createApp(appName);
2050 		}
2051 
2052 		if (isIframe) {
2053 			this._createAppIframeView(appName);
2054 			if (callback) {
2055 				callback.run();
2056 			}
2057 		}
2058 		else {
2059 			DBG.println(AjxDebug.DBG1, "Launching app " + appName);
2060 			var respCallback = new AjxCallback(this, this._handleResponseActivateApp, [callback, appName]);
2061 			var eventType = [appName, ZmAppEvent.PRE_LAUNCH].join("_");
2062 			this._evt.item = this._apps[appName];
2063 			this.notify(eventType);
2064 			params = params || {};
2065 			params.searchResponse = this._searchResponse;
2066 			this._apps[appName].launch(params, respCallback);
2067 			delete this._searchResponse;
2068 		}
2069 	}
2070 };
2071 
2072 /**
2073  * @private
2074  */
2075 ZmZimbraMail.prototype._handleResponseActivateApp =
2076 function(callback, appName) {
2077 	if (callback) {
2078 		callback.run();
2079 	}
2080 
2081 	if (ZmApp.DEFAULT_SEARCH[appName]) {
2082 		appCtxt.getSearchController().setDefaultSearchType(ZmApp.DEFAULT_SEARCH[appName]);
2083 	}
2084 
2085 	var eventType = [appName, ZmAppEvent.POST_LAUNCH].join("_");
2086 	this._evt.item = this._apps[appName];
2087 	this.notify(eventType);
2088 };
2089 
2090 /**
2091  * Handles a change in which app is current. The change will be reflected in the
2092  * current app toolbar and the overview. The previous and newly current apps are
2093  * notified of the change. This method is called after a new view is pushed.
2094  *
2095  * @param {Object}	view
2096  */
2097 ZmZimbraMail.prototype.setActiveApp =
2098 function(view) {
2099 	var appName = view.app;
2100 
2101 	// update app chooser
2102 	if (!view.isTabView) {
2103 		this._components[ZmAppViewMgr.C_APP_CHOOSER].setSelected(appName);
2104 	}
2105 
2106 	// app not actually enabled if this is result of iframe view push
2107 	var account = appCtxt.multiAccounts && appCtxt.accountList.mainAccount;
2108 	var appEnabled = !ZmApp.SETTING[appName] || appCtxt.get(ZmApp.SETTING[appName], null, account);
2109 
2110 	this._activeTabId = null;	// app is active; tab IDs are for non-apps
2111 
2112 	if (appName === ZmApp.SEARCH) {
2113 		//this is a special case - the search tab - set the new button based on type by using the results type app to get the button props.
2114 		this._setSearchTabNewButtonProps(view.controller._resultsController);
2115 	}
2116 
2117 	if (this._activeApp != appName) {
2118 		// deactivate previous app
2119 	    if (this._activeApp) {
2120 			// some views are not stored in _apps collection, so check if it exists.
2121 			var app = this._apps[this._activeApp];
2122 			if (app) {
2123 				app.activate(false, view.id);
2124 			}
2125 			this._previousApp = this._activeApp;
2126 		}
2127 
2128 		// switch app
2129 		this._activeApp = appName;
2130 		if (appEnabled) {
2131 			var app = this._apps[this._activeApp];
2132 			if (appCtxt.get(ZmSetting.SEARCH_ENABLED)) {
2133 				var searchType;
2134 				var currentSearch;
2135 				if (appName === ZmApp.SEARCH) {
2136 					currentSearch = view.controller._resultsController._currentSearch;
2137 					var types = currentSearch && currentSearch.types;
2138 					searchType = types && types.size() > 0 && types.get(0);
2139 				}
2140 				else {
2141 					currentSearch = app.currentSearch;
2142 					searchType = app.getInitialSearchType();
2143 					if (!searchType) {
2144 						searchType = ZmApp.DEFAULT_SEARCH[appName];
2145 					}
2146 				}
2147 				if (searchType) {
2148 					appCtxt.getSearchController().setDefaultSearchType(searchType);
2149 				}
2150 				// set search string value to match current app's last search, if applicable
2151 				var stb = appCtxt.getSearchController().getSearchToolbar();
2152 				if (appCtxt.get(ZmSetting.SHOW_SEARCH_STRING) && stb) {
2153 					var value = currentSearch ? currentSearch.query : app.currentQuery;
2154 					value = appName === ZmApp.SEARCH ? "" : value;
2155 					stb.setSearchFieldValue(value || "");
2156 				}
2157 			}
2158 
2159 			// activate current app - results in rendering of overview
2160 			if (app) {
2161 				if (appCtxt.inStartup && this._doingPostRenderStartup) {
2162 					var callback = new AjxCallback(this,
2163 						function() {
2164 							app.activate(true);
2165 						});
2166 					this.addPostRenderCallback(callback, 1, 0, true);
2167 				} else {
2168 					app.activate(true);
2169 				}
2170 			}
2171 		}
2172 		this._evt.item = this._apps[appName];
2173 		this.notify(ZmAppEvent.ACTIVATE);
2174 	}
2175 	else if (this._activeApp && this._apps[this._activeApp]) {
2176 		this._apps[this._activeApp].stopAlert();
2177 	}
2178 };
2179 
2180 ZmZimbraMail.prototype._setSearchTabNewButtonProps =
2181 function(resultsController) {
2182 	var resultsApp;
2183 	if (resultsController.isZmCalViewController) {
2184 		//calendar search is different, no _currentSearch unfortunately.
2185 		resultsApp = appCtxt.getApp(ZmApp.CALENDAR);
2186 	}
2187 	else {
2188 		var currentSearch = resultsController._currentSearch;
2189 		var types = currentSearch && currentSearch.types;
2190 		var searchType = types && types.size() > 0 && types.get(0);
2191 		resultsApp = searchType && appCtxt.getApp(ZmItem.APP[searchType]);
2192 	}
2193 	if (resultsApp) {
2194 		appCtxt.getAppController().setNewButtonProps(resultsApp.getNewButtonProps());
2195 	}
2196 
2197 };
2198 
2199 /**
2200  * Gets the app chooser button.
2201  * 
2202  * @param	{String}	id		the id
2203  * @return	{ZmAppButton}	the button
2204  */
2205 ZmZimbraMail.prototype.getAppChooserButton =
2206 function(id) {
2207 	var chooser = this._components[ZmAppViewMgr.C_APP_CHOOSER];
2208 	return chooser && chooser.getButton(id);
2209 };
2210 
2211 /**
2212  * An app calls this once it has fully rendered, so that we may notify
2213  * any listeners.
2214  * 
2215  * @param	{String}	appName		the app name
2216  */
2217 ZmZimbraMail.prototype.appRendered =
2218 function(appName) {
2219 	var eventType = [appName, ZmAppEvent.POST_RENDER].join("_");
2220 	this.notify(eventType);
2221 
2222 	if (window._facadeCleanup) {
2223 		window._facadeCleanup();
2224 		window._facadeCleanup = null;
2225 	}
2226 };
2227 
2228 /**
2229  * Adds the application.
2230  * 
2231  * @param	{ZmApp}		app		the app
2232  */
2233 ZmZimbraMail.prototype.addApp = function(app) {
2234 	var appName = app.getName();
2235 	this._apps[appName] = app;
2236 	ZmApp.ENABLED_APPS[appName] = true;
2237 };
2238 
2239 // Private methods
2240 
2241 /**
2242  * Creates an app object, which doesn't necessarily do anything just yet.
2243  * 
2244  * @private
2245  */
2246 ZmZimbraMail.prototype._createApp =
2247 function(appName) {
2248 	if (!appName || this._apps[appName]) return;
2249 	DBG.println(AjxDebug.DBG1, "Creating app " + appName);
2250 	var appClass = eval(ZmApp.CLASS[appName]);
2251 	this.addApp(new appClass(this._shell));
2252 };
2253 
2254 /**
2255  * @private
2256  */
2257 ZmZimbraMail.prototype._setExternalLinks =
2258 function() {
2259     // bug: 41313 - admin console link
2260     var adminUrl;
2261     if (!appCtxt.isOffline &&
2262         (appCtxt.get(ZmSetting.IS_ADMIN) ||
2263          appCtxt.get(ZmSetting.IS_DELEGATED_ADMIN))) {
2264 
2265         adminUrl = appCtxt.get(ZmSetting.ADMIN_URL);
2266         if (!adminUrl) {
2267             adminUrl = ["https://", location.hostname, ":7071"].join("");
2268         }
2269     }
2270 	var el = document.getElementById("skin_container_links");
2271 	if (el) {
2272 		var data = {
2273 			showOfflineLink: (!appCtxt.isOffline && appCtxt.get(ZmSetting.SHOW_OFFLINE_LINK)),
2274 			helpIcon: (appCtxt.getSkinHint("helpButton", "hideIcon") ? null : "Help"),
2275 			logoutIcon: (appCtxt.getSkinHint("logoutButton", "hideIcon") ? null : "Logoff"),
2276 			logoutText: (appCtxt.isOffline ? ZmMsg.setup : ZmMsg.logOff),
2277 			adminUrl: adminUrl
2278 		};
2279 		el.innerHTML = AjxTemplate.expand("share.App#UserInfo", data);
2280 	}
2281 	
2282 	el = document.getElementById("skin_container_help_button");
2283 	if (el) {
2284 		this._helpButton = this.getHelpButton(DwtShell.getShell(window));
2285 		this._helpButton.reparentHtmlElement("skin_container_help_button");
2286 	}
2287 
2288     el = document.getElementById("skin_dropMenu");
2289     if (el) {
2290 		this._helpButton = this.getDropMenuOptions(DwtShell.getShell(window), el, adminUrl);
2291 		//this._helpButton.reparentHtmlElement("skin_dropMenu");
2292 	}
2293 };
2294 
2295 
2296 ZmZimbraMail.ONLINE_HELP_URL = "https://help.zimbra.com/?";
2297 ZmZimbraMail.NEW_FEATURES_URL = "https://www.zimbra.com/products/whats_new.html?";
2298 
2299 ZmZimbraMail.DEFAULT_CONTACT_ICON = appContextPath + "/img/large/ImgPerson_48.png?v=" + window.cacheKillerVersion;
2300 ZmZimbraMail.DEFAULT_CONTACT_ICON_SMALL = appContextPath + "/img/large/ImgPerson_32.png?v=" + window.cacheKillerVersion;
2301 
2302 /**
2303 * Adds a "help" submenu.
2304 *
2305 * @param {DwtComposite}		parent		the parent widget
2306 * @return {ZmActionMenu}	the menu
2307 */
2308 ZmZimbraMail.prototype.getDropMenuOptions =
2309 function(parent, parentElement, adminUrl) {
2310 
2311 	var button = new DwtLinkButton({parent: parent, className: DwtButton.LINK_BUTTON_CLASS, parentElement: parentElement, elementTag: "DIV"});
2312 	button.whatToShow = { };
2313 	button.setSize(Dwt.DEFAULT);
2314 	button.setAlign(DwtLabel.ALIGN_LEFT);
2315 	button.setText(ZmMsg.help);
2316 	button.setAttribute('aria-label', ZmMsg.userActions);
2317 	var menu = new ZmPopupMenu(button);
2318 
2319 	var supportedHelps = appCtxt.get(ZmSetting.SUPPORTED_HELPS);
2320 	var helpListener = new AjxListener(this, this._helpListener);
2321 	button.addSelectionListener(helpListener);
2322 
2323     var mi;
2324 	if (adminUrl) {
2325 	    mi = menu.createMenuItem("adminLink", {text: ZmMsg.adminLinkLabel});
2326 	    mi.addSelectionListener(new AjxListener(null, ZmZimbraMail.adminLinkCallback, adminUrl));
2327 	}
2328 
2329     mi = menu.createMenuItem("standardHtmlLink", {text: ZmMsg.htmlClient});
2330     mi.addSelectionListener(ZmZimbraMail.standardHtmlLinkCallback);
2331 
2332 	menu.createSeparator();
2333 
2334 	if (supportedHelps.indexOf("productHelp") !== -1) {
2335 		mi = menu.createMenuItem("documentation", {text: ZmMsg.productHelp});
2336 		mi.addSelectionListener(helpListener);
2337 	}
2338 
2339 	if (supportedHelps.indexOf("onlineHelp") !== -1) {
2340 		mi = menu.createMenuItem("onlinehelp", {text: ZmMsg.onlineHelp});
2341 		mi.addSelectionListener(new AjxListener(this, this._onlineHelpListener));
2342 	}
2343 
2344 
2345 	if (supportedHelps.indexOf("newFeatures") !== -1) {
2346 		mi = menu.createMenuItem("newFeatures", {text: ZmMsg.newFeatures});
2347 		mi.addSelectionListener(new AjxListener(this, this._newFeaturesListener));
2348 	}
2349 
2350 	mi = menu.createMenuItem("showCurrentShortcuts", {text: ZmMsg.shortcuts});
2351 	mi.addSelectionListener(this._showCurrentShortcuts.bind(this));
2352 
2353 	menu.createSeparator();
2354 
2355 	mi = menu.createMenuItem(ZmZimbraMail.HELP_MENU_ABOUT, {text: ZmMsg.about});
2356 	mi.addSelectionListener(new AjxListener(this, this._aboutListener));
2357 
2358     menu.createSeparator();
2359 
2360 	if (!appCtxt.isExternalAccount() && appCtxt.get(ZmSetting.WEBCLIENT_OFFLINE_ENABLED)) {
2361         mi = menu.createMenuItem("offlineSettings", {text: ZmMsg.offlineSettings});
2362         mi.addSelectionListener(new AjxListener(this, this._offlineSettingsListener));
2363     }
2364 
2365 	if (AjxEnv.isFirefox && (AjxEnv.browserVersion >= 23.0) && !appCtxt.isExternalAccount()) {
2366 		mi = menu.createMenuItem("socialfoxSettings", {text: ZmMsg.socialfoxEnableSidebar});
2367 		mi.addSelectionListener(this._socialfoxSettingsListener.bind(this));
2368 	}
2369 
2370 	if (appCtxt.get(ZmSetting.CHANGE_PASSWORD_ENABLED)) {
2371         mi = menu.createMenuItem("changePassword", {text: ZmMsg.changePassword});
2372         mi.addSelectionListener(new AjxListener(this, this._changePasswordListener));
2373 	}
2374 
2375     mi = menu.createMenuItem(ZmZimbraMail.HELP_MENU_LOGOFF, {text: ZmMsg.logOff});
2376 	mi.addSelectionListener(new AjxListener(null, ZmZimbraMail.logOff));
2377 
2378 	button.setMenu(menu);
2379 	this.setupHelpMenu(button);
2380 	return button;
2381 };
2382 
2383 ZmZimbraMail.HELP_MENU_ABOUT  = "about";
2384 ZmZimbraMail.HELP_MENU_LOGOFF = "logOff";
2385 
2386 
2387 ZmZimbraMail.prototype.setupHelpMenu = function(button) {
2388 	button = button || this._helpButton;
2389 	if (!button) return;
2390 
2391 	var menu = button.getMenu();
2392 	if (!menu) return;
2393 
2394 	var isOnline = !appCtxt.isWebClientOffline();
2395 	if (isOnline) {
2396 		menu.enableAll(true);
2397 	} else {
2398 		menu.enableAll(false);
2399 		var offlineEnabledIds = [ZmZimbraMail.HELP_MENU_ABOUT];
2400 		menu.enable(offlineEnabledIds, true);
2401 	}
2402 };
2403 
2404 ZmZimbraMail.prototype.getNewButton =
2405 function() {
2406 
2407 	var newButton = this._newButton;
2408 	if (!newButton) {
2409 		var buttonId = ZmId.getButtonId(null, ZmOperation.NEW_MENU);
2410 		var buttonParams = {
2411 			parent:		appCtxt.getShell(),
2412 			id:			buttonId,
2413 			posStyle:	DwtControl.ABSOLUTE_STYLE,
2414 			className:	"ZToolbarButton ZNewButton"
2415 		};
2416 		newButton = this._newButton = new DwtToolBarButton(buttonParams);
2417 		newButton.setText(ZmMsg._new);
2418 
2419 		ZmOperation.addNewMenu(newButton);
2420 
2421 		var selectionListener = this._newButtonListener.bind(this);
2422 		var listener = this._newDropDownListener.bind(this, selectionListener);
2423 		this._newDropDownListener = listener;
2424 		newButton.addSelectionListener(selectionListener);
2425 		newButton.addDropDownSelectionListener(listener);
2426 	}
2427 
2428 	return newButton;
2429 };
2430 
2431 
2432 
2433 /**
2434  * Creates the New menu's drop down menu the first time the drop down arrow is used,
2435  * then removes itself as a listener.
2436  *
2437  * @private
2438  */
2439 ZmZimbraMail.prototype._newDropDownListener =
2440 function(selectionListener, event) {
2441 
2442 	var newButton = this.getNewButton();
2443 	var menu = newButton.getMenu();
2444 	var items = menu.getItems();
2445 	for (var i = 0; i < menu.getItemCount(); i++) {
2446 		items[i].addSelectionListener(selectionListener);
2447 	}
2448 
2449 	var listener = this._newDropDownListener;
2450 	newButton.removeDropDownSelectionListener(listener);
2451 	//Called explicitly as its a selection listener. Refer DwtButton._dropDownCellMouseDownHdlr()
2452 	newButton.popup();
2453 
2454 	delete this._newDropDownListener;
2455 };
2456 
2457 /**
2458  * Create some new thing, via a dialog. If just the button has been pressed (rather than
2459  * a menu item), the action taken depends on the app.
2460  *
2461  * @param {DwtUiEvent}	ev		the ui event
2462  * @param {constant}	op		the operation ID
2463  * @param {Boolean}		newWin	<code>true</code> if in a separate window
2464  *
2465  * @private
2466  */
2467 ZmZimbraMail.prototype._newButtonListener =
2468 function(ev, op, params) {
2469 
2470 	if (!ev && !op) { return; }
2471 
2472 	op = op || ev.item.getData(ZmOperation.KEY_ID);
2473 	if (!op || op == ZmOperation.NEW_MENU) {
2474 		op = ZmController._defaultNewId;
2475 	}
2476 
2477 	var app = ZmApp.OPS_R[op];
2478 	if (app) {
2479 		params = params || {};
2480 		params.ev = ev;
2481 		appCtxt.getApp(app).handleOp(op, params);
2482 	} else {
2483 		var ctlr = appCtxt.getCurrentController();
2484 		if (ctlr) {
2485 			ctlr._newListener(ev, op);
2486 		}
2487 	}
2488 };
2489 
2490 /**
2491  * Set up the New button based on the current app.
2492  */
2493 ZmZimbraMail.prototype.setNewButtonProps =
2494 function(params) {
2495 	var newButton = this.getNewButton();
2496 	if (newButton) {
2497 		newButton.setText(params.text);
2498 		newButton.setToolTipContent(params.tooltip);
2499 		newButton.setImage(params.icon);
2500 		newButton.setEnabled(!params.disabled);
2501 		ZmController._defaultNewId = params.defaultId;
2502 		params.hidden ? newButton.setVisibility(false) : newButton.setVisibility(true);
2503 	}
2504 };
2505 
2506 
2507 /**
2508 * Adds a "help" submenu.
2509 *
2510 * @param {DwtComposite}		parent		the parent widget
2511 * @return {ZmActionMenu}	the menu
2512 */
2513 ZmZimbraMail.prototype.getHelpButton =
2514 function(parent) {
2515 
2516 	var button = new DwtLinkButton({parent: parent, className: DwtButton.LINK_BUTTON_CLASS});
2517 	button.dontStealFocus();
2518 	button.setSize(Dwt.DEFAULT);
2519 	button.setAlign(DwtLabel.ALIGN_LEFT);
2520 	button.setText(ZmMsg.help);
2521 	var menu = new ZmPopupMenu(button);
2522 
2523 	var helpListener = new AjxListener(this, this._helpListener);
2524 	button.addSelectionListener(helpListener);
2525 
2526 	var mi = menu.createMenuItem("documentation", {text: ZmMsg.productHelp});
2527 	mi.addSelectionListener(helpListener);
2528 
2529 	var mi = menu.createMenuItem("onlinehelp", {text: ZmMsg.onlineHelp});
2530 	mi.addSelectionListener(new AjxListener(this, this._onlineHelpListener));
2531 
2532 
2533 	mi = menu.createMenuItem("newFeatures", {text: ZmMsg.newFeatures});
2534 	mi.addSelectionListener(new AjxListener(this, this._newFeaturesListener));
2535 
2536 	mi = menu.createMenuItem("showCurrentShortcuts", {text: ZmMsg.shortcuts});
2537 	mi.addSelectionListener(this._showCurrentShortcuts.bind(this));
2538 
2539 	menu.createSeparator();
2540 
2541 	mi = menu.createMenuItem("about", {text: ZmMsg.about});
2542 	mi.addSelectionListener(new AjxListener(this, this._aboutListener));
2543 
2544 	button.setMenu(menu);
2545 	return button;
2546 };
2547 
2548 ZmZimbraMail.prototype._helpListener =
2549 function(ev) {
2550 	ZmZimbraMail.helpLinkCallback();
2551 };
2552 
2553 
2554 ZmZimbraMail.prototype._getVersion =
2555 function() {
2556 	return appCtxt.get(ZmSetting.CLIENT_VERSION);
2557 };
2558 
2559 
2560 ZmZimbraMail.prototype._getQueryParams =
2561 function() {
2562 
2563 	var appName = appCtxt.getCurrentAppName().toLowerCase();
2564 	var prod = appCtxt.isOffline ? "zd" : "zcs";
2565 	return ["utm_source=", appName, "&utm_medium=", prod, "&utm_content=", this._getVersion(), "&utm_campaign=help"].join("");
2566 };
2567 
2568 
2569 ZmZimbraMail.prototype._onlineHelpListener =
2570 function(ev) {
2571 	ZmZimbraMail.unloadHackCallback();
2572 	var url = [ZmZimbraMail.ONLINE_HELP_URL, this._getQueryParams()].join("");
2573 	window.open(url);
2574 };
2575 
2576 ZmZimbraMail.prototype._newFeaturesListener =
2577 function(ev) {
2578 	ZmZimbraMail.unloadHackCallback();
2579 	var url = [ZmZimbraMail.NEW_FEATURES_URL, this._getQueryParams()].join("");
2580 	window.open(url);
2581 };
2582 
2583 ZmZimbraMail.prototype._changePasswordListener =
2584 function(ev) {
2585     appCtxt.getChangePasswordWindow(ev);
2586 }
2587 
2588 ZmZimbraMail.prototype._aboutListener =
2589 function(ev) {
2590 	var dialog = appCtxt.getMsgDialog();
2591 	dialog.reset();
2592 	var version = this._getVersion();
2593 	var release = appCtxt.get(ZmSetting.CLIENT_RELEASE);
2594 	var aboutMsg = appCtxt.isOffline ? ZmMsg.aboutMessageZD : ZmMsg.aboutMessage;
2595 	dialog.setMessage(AjxMessageFormat.format(aboutMsg, [version, release, AjxDateUtil.getYearStr()]), DwtMessageDialog.INFO_STYLE, ZmMsg.about);
2596 	dialog.popup();
2597 
2598 };
2599 
2600 ZmZimbraMail.prototype._offlineSettingsListener =
2601 function(ev) {
2602     var dialog;
2603     if (AjxEnv.isOfflineSupported) {
2604         dialog = appCtxt.getOfflineSettingsDialog();
2605     } else {
2606         dialog = appCtxt.getMsgDialog();
2607         dialog.setMessage(ZmMsg.offlineSupportedBrowser, "", ZmMsg.offlineSettings);
2608     }
2609     dialog.popup();
2610 };
2611 
2612 ZmZimbraMail.prototype._socialfoxSettingsListener =
2613 function(ev) {
2614     var dialog = new ZmSocialfoxActivationDialog();
2615     dialog.popup();
2616 };
2617 
2618 
2619 ZmZimbraMail.prototype._initOfflineUserInfo =
2620 function() {
2621 	var htmlElId = this._userNameField.getHTMLElId();
2622 	this._userNameField.getHtmlElement().innerHTML = AjxTemplate.expand('share.App#NetworkStatus', {id:htmlElId});
2623 	this._userNameField.addClassName("BannerTextUserOffline");
2624 
2625 	var params = {
2626 		parent: this._userNameField,
2627 		parentElement: (htmlElId+"_networkStatusIcon")
2628 	};
2629 	this._networkStatusIcon = new DwtComposite(params);
2630 
2631 	var params1 = {
2632 		parent: this._userNameField,
2633 		parentElement: (htmlElId+"_networkStatusText")
2634 	};
2635 	this._networkStatusText = new DwtComposite(params1);
2636 
2637 	var topTreeEl = document.getElementById("skin_container_tree_top");
2638 	if (topTreeEl) {
2639 		Dwt.setSize(topTreeEl, Dwt.DEFAULT, "20");
2640 	}
2641 };
2642 
2643 ZmZimbraMail.prototype._offlineUpdateChannelPref =
2644 function(val) {
2645     try {
2646         netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
2647         var prefs =
2648             Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
2649         if (prefs) {
2650             prefs.setCharPref("app.update.channel", val);
2651         }
2652     } catch (ex) {
2653         DBG.println(AjxDebug.DBG1, "-----------Exception while setting update channel preference " + ex);
2654     }
2655 };
2656 
2657 /**
2658  * Sets the user info.
2659  *
2660  */
2661 ZmZimbraMail.prototype.setUserInfo = function() {
2662 
2663 	if (appCtxt.isOffline) {
2664 		return;
2665 	}
2666 
2667 	// username
2668 	var login = appCtxt.getLoggedInUsername();
2669 	var username = (appCtxt.get(ZmSetting.DISPLAY_NAME)) || login;
2670 	if (username) {
2671 		var el = this._userNameField.getHtmlElement();
2672         el.innerHTML =  AjxStringUtil.htmlEncode(AjxStringUtil.clipByLength(username, 24));
2673 		el.setAttribute('aria-label', username);
2674 		if (AjxEnv.isLinux) {	// bug fix #3355
2675 			el.style.lineHeight = "13px";
2676 		}
2677 	}
2678 
2679     this.setQuotaInfo(login, username);
2680 };
2681 
2682 ZmZimbraMail.prototype.setQuotaInfo = function(login, username) {
2683 
2684     var quota = appCtxt.get(ZmSetting.QUOTA);
2685 	var usedQuota = (appCtxt.get(ZmSetting.QUOTA_USED)) || 0;
2686 	var data = {
2687 		id: this._usedQuotaField._htmlElId,
2688 		login: login,
2689 		username: username,
2690 		quota: quota,
2691 		usedQuota: usedQuota,
2692 		size: (AjxUtil.formatSize(usedQuota, false, 1))
2693 	};
2694 
2695 	var quotaTemplateId;
2696 	if (data.quota) {
2697 		quotaTemplateId = 'UsedLimited';
2698 		data.limit = AjxUtil.formatSize(data.quota, false, 1);
2699 		data.percent = Math.min(Math.round((data.usedQuota / data.quota) * 100), 100);
2700 		data.desc = AjxMessageFormat.format(ZmMsg.usingDescLimited, [data.size, '(' + data.percent + '%)', data.limit]);
2701 	}
2702     else {
2703 		data.desc = AjxMessageFormat.format(ZmMsg.usingDescUnlimited, [data.size]);
2704 		quotaTemplateId = 'UsedUnlimited';
2705 	}
2706 
2707 	var el = this._usedQuotaField.getHtmlElement();
2708     el.innerHTML = AjxTemplate.expand('share.Quota#' + quotaTemplateId, data);
2709 	el.setAttribute('aria-label', data.desc);
2710 
2711 	// tooltip for username/quota fields
2712 	var html = AjxTemplate.expand('share.Quota#Tooltip', data);
2713 	this._components[ZmAppViewMgr.C_USER_INFO].setToolTipContent(html);
2714 	this._components[ZmAppViewMgr.C_QUOTA_INFO].setToolTipContent(html);
2715 };
2716 
2717 /**
2718  * If a user has been prompted and elects to stay on page, this timer automatically logs them off after an interval of time.
2719  * @param startTimer {boolean} true to start timer, false to cancel
2720  */
2721 ZmZimbraMail.setExitTimer = 
2722 function(startTimer) {
2723 	if (startTimer && ZmZimbraMail.stayOnPagePrompt) {
2724 		DBG.println(AjxDebug.DBG1, "user has clicked stay on page. scheduled exit timer at " + new Date().toLocaleString());
2725 		if (ZmZimbraMail._exitTimerId == -1) {
2726 			ZmZimbraMail._exitTimerId = AjxTimedAction.scheduleAction(ZmZimbraMail._exitTimer, ZmZimbraMail.STAYONPAGE_INTERVAL * 60 * 1000); //give user 2 minutes
2727 			if (AjxEnv.isFirefox) {
2728 				var msg = AjxMessageFormat.format(ZmMsg.appExitPrompt, [ZmZimbraMail.STAYONPAGE_INTERVAL]);
2729 				var msgDialog = appCtxt.getMsgDialog();
2730 				msgDialog.setMessage(msg, DwtMessageDialog.CRITICAL_STYLE); //Firefox 4+ doesn't allow custom stay on page message. Prompt user they have X minutes
2731 				//wait 2 seconds before popping up  so FF doesn't show dialog when leave page is clicked
2732 				setTimeout(function() { msgDialog.popup()}, 1000 * 2);
2733 			}
2734 		}
2735 
2736 	}
2737 	else if (!startTimer && ZmZimbraMail._exitTimerId) {
2738 		DBG.println(AjxDebug.DBG1, "canceling exit timer at " + new Date().toLocaleString());
2739 		AjxTimedAction.cancelAction(ZmZimbraMail._exitTimerId);
2740 		ZmZimbraMail._exitTimerId = -1;
2741 	}
2742 	
2743 };
2744 
2745 // Listeners
2746 
2747 /**
2748  * Logs off the application.
2749  * 
2750  */
2751 ZmZimbraMail.logOff =
2752 function(ev, relogin) {
2753 	if (appCtxt.isChildWindow) {
2754 		window.close();
2755 		return;
2756 	}
2757 	if (appCtxt.isWebClientOfflineSupported && (ev || relogin)) {
2758 		return ZmOffline.handleLogOff(ev, relogin);
2759     }
2760 
2761 	ZmZimbraMail._isLogOff = true;
2762 
2763 	// bug fix #36791 - reset the systray icon when returning to Account Setup
2764 	if (appCtxt.isOffline && AjxEnv.isWindows &&
2765 		appCtxt.get(ZmSetting.OFFLINE_SUPPORTS_DOCK_UPDATE))
2766 	{
2767 		window.platform.icon().imageSpec = "resource://webapp/icons/default/launcher.ico";
2768 		window.platform.icon().title = null;
2769 	}
2770     var urlParams = {
2771                 path:appContextPath,
2772                 qsArgs: {
2773                         loginOp: relogin ? 'relogin' : 'logout'
2774                     }
2775                 };
2776 	if (relogin) {
2777 		urlParams.qsArgs.username = appCtxt.getLoggedInUsername();
2778 	}
2779     if(appCtxt.isExternalAccount()) {
2780         var vAcctDomain = appCtxt.getUserDomain();
2781         urlParams.qsArgs.virtualacctdomain = vAcctDomain ? vAcctDomain : "";
2782     }
2783 	var url = AjxUtil.formatUrl(urlParams);
2784 	ZmZimbraMail.sendRedirect(url);	// will trigger onbeforeunload
2785 	if (AjxEnv.isFirefox) {
2786 		DBG.println(AjxDebug.DBG1, "calling setExitTimer from logoff "  + new Date().toLocaleString());
2787 		ZmZimbraMail.setExitTimer(true);	
2788 	}
2789 	
2790 
2791 };
2792 
2793 /**
2794  * Logs user off when session has expired and user has choosen to stay on page when prompted
2795  */
2796 ZmZimbraMail.exitSession =
2797 function() {
2798 	DBG.println(AjxDebug.DBG1, "exit timer called  " + new Date().toLocaleString());
2799 	ZmZimbraMail.logOff();
2800 };
2801 
2802 ZmZimbraMail.executeSessionTimer = 
2803 function() {
2804 	ZmZimbraMail.sessionTimerInvoked = true;
2805 	DBG.println(AjxDebug.DBG1, "session timer invoked  " + new Date().toLocaleString());
2806 	ZmZimbraMail.logOff();
2807 };
2808 
2809 
2810 /**
2811  * Return the confirmExitMethod that can be used for window.onbeforeunload
2812  *
2813  */
2814 ZmZimbraMail.getConfirmExitMethod =
2815 function(){
2816     return this._confirmExitMethod;
2817 }
2818 
2819 
2820 /**
2821  * @private
2822  */
2823 ZmZimbraMail._onClickLogOff =
2824 function() {
2825 	if (AjxEnv.isIE) {
2826 		// Don't the the default <a> handler process the event. It can bring up
2827 		// an unwanted "Are you sure you want to exit?" dialog.
2828 		var ev = DwtUiEvent.getEvent();
2829 		ev.returnValue = false;
2830 	}
2831 	ZmZimbraMail.logOff();
2832 };
2833 
2834 /**
2835  * @private
2836  */
2837 ZmZimbraMail.adminLinkCallback =
2838 function(url) {
2839 	ZmZimbraMail.unloadHackCallback();
2840 	var ac = window.parentAppCtxt || window.appCtxt;
2841 	window.open(url);
2842 };
2843 
2844 /**
2845  * @private
2846  */
2847 ZmZimbraMail.standardHtmlLinkCallback =
2848 function() {
2849 	var urlParams = {
2850 		path: appContextPath,
2851 		qsArgs: {
2852 			client: "standard"
2853 		}
2854 	};
2855 	var url = AjxUtil.formatUrl(urlParams);
2856 	ZmZimbraMail.sendRedirect(url);	// will trigger onbeforeunload
2857 };
2858 
2859 /**
2860  * @private
2861  */
2862 ZmZimbraMail.helpLinkCallback =
2863 function(helpurl) {
2864 	ZmZimbraMail.unloadHackCallback();
2865 
2866 	var ac = window.parentAppCtxt || window.appCtxt;
2867 	var url;
2868 	if (!ac.isOffline) {
2869 		try { url = helpurl || skin.hints.helpButton.url; } catch (e) { /* ignore */ }
2870 		url = url || ac.get(ZmSetting.HELP_URI);
2871 		var sep = url.match(/\?/) ? "&" : "?";
2872 		url = [url, sep, "locid=", AjxEnv.DEFAULT_LOCALE].join("");
2873 	} else {
2874 		url = ac.get(ZmSetting.HELP_URI).replace(/\/$/,"");
2875 		// bug fix #35098 - offline help is only available in en_US for now
2876 		url = [url, "help", "en_US", "Zimbra_Mail_Help.htm"].join("/");
2877 //		url = [url, "help", AjxEnv.DEFAULT_LOCALE, "Zimbra_Mail_Help.htm"].join("/");
2878 	}
2879 	window.open(url);
2880 };
2881 
2882 /**
2883  * Sends a redirect.
2884  * 
2885  * @param	{String}	locationStr		the redirect location
2886  */
2887 ZmZimbraMail.sendRedirect =
2888 function(locationStr) {
2889 	// not sure why IE doesn't allow this to process immediately, but since
2890 	// it does not, we'll set up a timed action.
2891 	if (AjxEnv.isIE) {
2892 		var act = new AjxTimedAction(null, ZmZimbraMail.redir, [locationStr]);
2893 		AjxTimedAction.scheduleAction(act, 1);
2894 	} else {
2895 		ZmZimbraMail.redir(locationStr);
2896 	}
2897 };
2898 
2899 /**
2900  * Redirect.
2901  * 
2902  * @param	{String}	locationStr		the redirect location
2903  */
2904 ZmZimbraMail.redir =
2905 function(locationStr){
2906 	// IE has a tendency to throw a mysterious error when the "are you sure" dialog pops up and the user presses "cancel".
2907 	// Pressing cancel, however, equals doing nothing, so we can just catch the exception and ignore it (bug #59853)
2908 	try {
2909 		window.location = locationStr;
2910 	} catch (e) {
2911 	}
2912 };
2913 
2914 /**
2915  * Sets the session timer.
2916  * 
2917  * @param	{Boolean}	bStartTimer		if <code>true</code>, start the timer
2918  */
2919 ZmZimbraMail.prototype.setSessionTimer =
2920 function(bStartTimer) {
2921 
2922 	// if no timeout value, user's client never times out from inactivity
2923 	var timeout = appCtxt.get(ZmSetting.IDLE_SESSION_TIMEOUT) * 1000;
2924 	if (timeout <= 0) {
2925 		return;
2926 	}
2927 
2928 	if (bStartTimer) {
2929 		DBG.println(AjxDebug.DBG3, "INACTIVITY TIMER SET (" + (new Date()).toLocaleString() + ")");
2930 		this._sessionTimerId = AjxTimedAction.scheduleAction(this._sessionTimer, timeout);
2931 
2932 		DwtEventManager.addListener(DwtEvent.ONMOUSEUP, ZmZimbraMail._userEventHdlr);
2933 		this._shell.setHandler(DwtEvent.ONMOUSEUP, ZmZimbraMail._userEventHdlr);
2934 		if (document.attachEvent)  {
2935 			document.attachEvent("onkeydown", ZmZimbraMail._userEventHdlr);
2936 		}
2937 		window.onkeydown = ZmZimbraMail._userEventHdlr;		
2938 	}
2939 	else {
2940 		DBG.println(AjxDebug.DBG3, "INACTIVITY TIMER CANCELED (" + (new Date()).toLocaleString() + ")");
2941 
2942 		AjxTimedAction.cancelAction(this._sessionTimerId);
2943 		this._sessionTimerId = -1;
2944 
2945 		DwtEventManager.removeListener(DwtEvent.ONMOUSEUP, ZmZimbraMail._userEventHdlr);
2946 		this._shell.clearHandler(DwtEvent.ONMOUSEUP);
2947 		if (document.detachEvent) {
2948 			document.detachEvent("onkeydown", ZmZimbraMail._userEventHdlr);
2949 		}	
2950 		window.onkeydown = null;
2951 	}
2952 };
2953 
2954 /**
2955  * Adds a child window.
2956  * 
2957  * @private
2958  */
2959 ZmZimbraMail.prototype.addChildWindow =
2960 function(childWin, childId) {
2961 	if (this._childWinList == null) {
2962 		this._childWinList = new AjxVector();
2963 	}
2964 
2965 	// NOTE: we now save childWin w/in Object so other params can be added to it.
2966 	// Otherwise, Safari breaks (see http://bugs.webkit.org/show_bug.cgi?id=7162)
2967 	var newWinObj = {win:childWin,childId:childId};
2968 	this._childWinList.add(newWinObj);
2969 
2970 	return newWinObj;
2971 };
2972 
2973 /**
2974  * Gets a child window.
2975  * 
2976  * @private
2977  */
2978 ZmZimbraMail.prototype.getChildWindow =
2979 function(childWin) {
2980 	var list = this._childWinList;
2981 	if (list && childWin) {
2982 		for (var i = 0; i < list.size(); i++) {
2983 			var winObj = list.get(i);
2984 			if (childWin === winObj.win || childWin.childId === winObj.childId) {
2985 				return winObj;
2986 			}
2987 		}
2988 	}
2989 	return null;
2990 };
2991 
2992 /**
2993  * Removes a child window.
2994  * 
2995  * @private
2996  */
2997 ZmZimbraMail.prototype.removeChildWindow =
2998 function(childWin) {
2999 	var list = this._childWinList;
3000 	if (list) {
3001 		for (var i = 0; i < list.size(); i++) {
3002 			var winObj = list.get(i);
3003 			if (childWin == winObj.win) {
3004 				// Bug 84426: We don't want our old window metadata to go away; if it's merely a refresh
3005 				// we want access to the parameters of our old window, so clear the actual window object,
3006 				// and leave the other parameters in winObj intact
3007 				winObj.win = null;
3008 				break;
3009 			}
3010 		}
3011 	}
3012 };
3013 
3014 /**
3015  * Checks for a certain type of exception, then hands off to standard
3016  * exception handler.
3017  *
3018  * @param {AjxException}	ex				the exception
3019  * @param {Object}	continuation		the original request params
3020  * 
3021  * @private
3022  */
3023 ZmZimbraMail.prototype._handleException =
3024 function(ex, continuation) {
3025 	var handled = false;
3026 	if (ex.code == ZmCsfeException.MAIL_NO_SUCH_FOLDER) {
3027 		// check for fault when getting folder perms
3028 		var organizerTypes = [ZmOrganizer.CALENDAR, ZmOrganizer.ADDRBOOK];
3029 		if (ex.data.itemId && ex.data.itemId.length) {
3030 			var itemId = ex.data.itemId[0];
3031 			var index = itemId.lastIndexOf(':');
3032 			var zid = itemId.substring(0, index);
3033 			var rid = itemId.substring(index + 1, itemId.length);
3034 			var ft = appCtxt.getFolderTree();
3035 			for (var type = 0; type < organizerTypes.length; type++) {
3036 				handled |= ft.handleNoSuchFolderError(organizerTypes[type], zid, rid, true);
3037 			}
3038 		}
3039 	}
3040     else if (appCtxt.isWebClientOffline() && ex.code === ZmCsfeException.EMPTY_RESPONSE) {
3041         handled = true;
3042     }
3043 	if (!handled) {
3044 		ZmController.prototype._handleException.apply(this, arguments);
3045 	}
3046 };
3047 
3048 /**
3049  * This method is called by the window.onbeforeunload handler
3050  * 
3051  * @private
3052  */
3053 ZmZimbraMail._confirmExitMethod =
3054 function() {
3055 
3056 	if (!ZmCsfeCommand.noAuth) {
3057 		appCtxt.accountList.saveImplicitPrefs();
3058 
3059 		if (appCtxt.get(ZmSetting.WARN_ON_EXIT) && !ZmZimbraMail._isOkToExit()) {
3060 			if (ZmZimbraMail.stayOnPagePrompt) {
3061 				DBG.println(AjxDebug.DBG1, "user has already been prompted. Forcing exit " + new Date().toLocaleString());
3062 				return;
3063 			}
3064 			
3065 			ZmZimbraMail._isLogOff = false;
3066 			DBG.println(AjxDebug.DBG1, "prompting to user to stay on page or leave " + new Date().toLocaleString());
3067 			var msg = (appCtxt.isOffline) ? ZmMsg.appExitWarningZD : ZmMsg.appExitWarning;
3068 			
3069 			if (ZmZimbraMail.sessionTimerInvoked) {
3070 				ZmZimbraMail.stayOnPagePrompt = true;
3071 				msg = AjxMessageFormat.format(msg + ZmMsg.appExitTimeWarning, [ZmZimbraMail.STAYONPAGE_INTERVAL]); //append time warning
3072 			}
3073 			if (!AjxEnv.isFirefox) {
3074 				DBG.println(AjxDebug.DBG1, "calling setExitTimer  "  + new Date().toLocaleString());
3075 				ZmZimbraMail.setExitTimer(true);
3076 			}
3077 			return msg;
3078 			
3079 		}
3080 
3081 		ZmZimbraMail._endSession();
3082 	}
3083     if (window.ZmDesktopAlert) {
3084         ZmDesktopAlert.closeNotification();
3085     }
3086 	ZmZimbraMail._endSessionDone = true;
3087 };
3088 
3089 /**
3090  * Returns true if there is no unsaved work. If that's the case, it also
3091  * cancels any pending poll. Typically called by onbeforeunload handling.
3092  * 
3093  * @private
3094  */
3095 ZmZimbraMail._isOkToExit =
3096 function() {
3097 	var appCtlr = window._zimbraMail;
3098 	if (!appCtlr) { return true; }
3099 	var okToExit = appCtlr._appViewMgr.isOkToUnload() && ZmZimbraMail._childWindowsOkToUnload();
3100 	if (okToExit && !AjxEnv.isPrism && appCtlr._pollRequest) {
3101 		appCtlr._requestMgr.cancelRequest(appCtlr._pollRequest);
3102 	}
3103 	return okToExit;
3104 };
3105 
3106 // returns true if no child windows are dirty
3107 ZmZimbraMail._childWindowsOkToUnload =
3108 function() {
3109 	var childWinList = window._zimbraMail ? window._zimbraMail._childWinList : null;
3110 	if (childWinList) {
3111 		for (var i = 0; i < childWinList.size(); i++) {
3112 			var childWin = childWinList.get(i);
3113 			if (childWin.win && childWin.win.ZmNewWindow && childWin.win.ZmNewWindow._confirmExitMethod()) {
3114 				return false;
3115 			}
3116 		}
3117 	}
3118 	return true;
3119 };
3120 
3121 ZmZimbraMail.handleNetworkStatusClick =
3122 function() {
3123 	var ac = window["appCtxt"].getAppController();
3124 
3125 	// if already offline, then ignore this click
3126 	if (!ac._isPrismOnline) { return; }
3127 
3128 	ac._isUserOnline = !ac._isUserOnline;
3129 	ac._updateNetworkStatus(ac._isUserOnline);
3130 };
3131 
3132 /**
3133  * @private
3134  */
3135 ZmZimbraMail.unloadHackCallback =
3136 function() {
3137 	window.onbeforeunload = null;
3138 	var f = function() { window.onbeforeunload = ZmZimbraMail._confirmExitMethod; };
3139 	AjxTimedAction.scheduleAction((new AjxTimedAction(null, f)), 3000);
3140 };
3141 
3142 /**
3143  * @private
3144  */
3145 ZmZimbraMail._userEventHdlr =
3146 function(ev) {
3147 	var zm = window._zimbraMail;
3148 	if (zm) {
3149 		// cancel old timer and start a new one
3150 		AjxTimedAction.cancelAction(zm._sessionTimerId);
3151 		var timeout = appCtxt.get(ZmSetting.IDLE_SESSION_TIMEOUT) * 1000;
3152 		if (timeout <= 0) {
3153 			return;
3154 		}
3155 		zm._sessionTimerId = AjxTimedAction.scheduleAction(zm._sessionTimer, timeout);
3156 	}
3157 	DBG.println(AjxDebug.DBG3, "INACTIVITY TIMER RESET (" + (new Date()).toLocaleString() + ")");
3158 };
3159 
3160 /**
3161  * @private
3162  */
3163 ZmZimbraMail.prototype._createBanner =
3164 function() {
3165 	var banner = new DwtComposite({parent:this._shell, posStyle:Dwt.ABSOLUTE_STYLE, id:ZmId.BANNER});
3166 	var logoUrl = appCtxt.getSkinHint("banner", "url") || appCtxt.get(ZmSetting.LOGO_URI);
3167 	var data = {url:logoUrl, isOffline:appCtxt.isOffline};
3168 	banner.getHtmlElement().innerHTML  = AjxTemplate.expand('share.App#Banner', data);
3169 	banner.getHtmlElement().style.height = '100%';
3170 	return banner;
3171 };
3172 
3173 /**
3174  * @private
3175  */
3176 ZmZimbraMail.prototype._createUserInfo =
3177 function(className, cid, id) {
3178 
3179 	var position = appCtxt.getSkinHint(cid, "position");
3180 	var posStyle = position || Dwt.ABSOLUTE_STYLE;
3181 	var ui = new DwtComposite({
3182 		parent:         this._shell,
3183 		className:      className,
3184 		posStyle:       posStyle,
3185 		id:             id,
3186 		isFocusable:    true
3187 	});
3188 	ui._setMouseEventHdlrs();
3189 	return ui;
3190 };
3191 
3192 /**
3193  * @private
3194  */
3195 ZmZimbraMail.prototype._createAppChooser =
3196 function() {
3197 
3198 	var buttons = [];
3199 	for (var id in ZmApp.CHOOSER_SORT) {
3200 		if (id == ZmAppChooser.SPACER || id == ZmAppChooser.B_HELP || id == ZmAppChooser.B_LOGOUT) {
3201 			continue;
3202 		}
3203 
3204 		if (this._isInternalApp(id) || this._isIframeApp(id)) {
3205 			buttons.push(id);
3206 		}
3207 	}
3208 	buttons.sort(function(a, b) {
3209 		return ZmZimbraMail.hashSortCompare(ZmApp.CHOOSER_SORT, a, b);
3210 	});
3211 
3212 	var appChooser = new ZmAppChooser({parent:this._shell, buttons:buttons, id:ZmId.APP_CHOOSER, refElementId:ZmId.SKIN_APP_CHOOSER});
3213 
3214 	var buttonListener = new AjxListener(this, this._appButtonListener);
3215 	appChooser.addSelectionListener(buttonListener);
3216 
3217 	return appChooser;
3218 };
3219 
3220 /**
3221  * @private
3222  */
3223 ZmZimbraMail.prototype._appButtonListener =
3224 function(ev) {
3225 	try {
3226 		var id = ev.item.getData(Dwt.KEY_ID);
3227 		DBG.println(AjxDebug.DBG1, "ZmZimbraMail button press: " + id);
3228 		if (id == ZmAppChooser.B_HELP) {
3229 			window.open(appCtxt.get(ZmSetting.HELP_URI));
3230 		} else if (id == ZmAppChooser.B_LOGOUT) {
3231 			ZmZimbraMail.logOff();
3232 		} else if (id && ZmApp.ENABLED_APPS[id] && (id != this._activeTabId)) {
3233 			this.activateApp(id);
3234 			if (appCtxt.zimletsPresent()) {
3235 				appCtxt.getZimletMgr().notifyZimlets("onSelectApp", id);
3236 			}
3237 		} else {
3238 			var isCloseButton = (DwtUiEvent.getTargetWithProp(ev, "id") == ev.item._getIconEl(DwtLabel.RIGHT));
3239 			if (isCloseButton) {
3240 				this._appViewMgr.popView(false, id);
3241 			}
3242 			else if (id != this._activeTabId) {
3243 				this._appViewMgr.pushView(id);
3244 			}
3245 		}
3246 	} catch (ex) {
3247 		this._handleException(ex);
3248 	}
3249 };
3250 
3251 /**
3252  * Gets the application chooser.
3253  * 
3254  * @return	{ZmAppChooser}	the chooser
3255  */
3256 ZmZimbraMail.prototype.getAppChooser =
3257 function() {
3258 	return this._appChooser;
3259 };
3260 
3261 /**
3262  * Sets the active tab.
3263  * 
3264  * @param	{String}	id		the tab id
3265  */
3266 ZmZimbraMail.prototype.setActiveTabId =
3267 function(id) {
3268 	this._activeTabId = id;
3269 	this._appChooser.setSelected(id);
3270 };
3271 
3272 /**
3273  * Displays a status message.
3274  * 
3275  * @param	{Hash}	params		a hash of parameters
3276  * @param {String}	params.msg		the message
3277  * @param {constant}	[params.level] ZmStatusView.LEVEL_INFO, ZmStatusView.LEVEL_WARNING, or ZmStatusView.LEVEL_CRITICAL
3278  * @param {constant}	[params.detail] 	the details
3279  * @param {constant}	[params.transitions]		the transitions
3280  * @param {constant}	[params.toast]		the toast control 
3281  * @param {boolean}     [force]        force any displayed toasts out of the way (dismiss them and run their dismissCallback). Enqueued messages that are not yet displayed will not be displayed
3282  * @param {AjxCallback}    [dismissCallback]    callback to run when the toast is dismissed (by another message using [force], or explicitly calling ZmStatusView.prototype.dismiss())
3283  * @param {AjxCallback}    [finishCallback]     callback to run when the toast finishes its transitions by itself (not when dismissed)
3284  */
3285 ZmZimbraMail.prototype.setStatusMsg =
3286 function(params) {
3287 	params = Dwt.getParams(arguments, ZmStatusView.MSG_PARAMS);
3288 	this.statusView.setStatusMsg(params);
3289 };
3290 
3291 /**
3292  * Dismisses the displayed status message, if any
3293  */
3294 
3295 ZmZimbraMail.prototype.dismissStatusMsg =
3296 function(all) {
3297 	this.statusView.dismissStatusMsg(all);
3298 };
3299 
3300 /**
3301  * Gets the key map name.
3302  * 
3303  * @return	{String}	the key map name
3304  */
3305 ZmZimbraMail.prototype.getKeyMapName =
3306 function() {
3307 	var ctlr = appCtxt.getCurrentController();
3308 	if (ctlr && ctlr.getKeyMapName) {
3309 		return ctlr.getKeyMapName();
3310 	}
3311 	return ZmKeyMap.MAP_GLOBAL;
3312 };
3313 
3314 /**
3315  * Handles the key action.
3316  * 
3317  * @param	{constant}		actionCode		the action code
3318  * @param	{Object}	    ev		        the event
3319  */
3320 ZmZimbraMail.prototype.handleKeyAction = function(actionCode, ev) {
3321 
3322     DwtMenu.closeActiveMenu();
3323 
3324 	var app = ZmApp.GOTO_ACTION_CODE_R[actionCode];
3325 	if (app) {
3326 		if (app == this.getActiveApp()) {
3327             return false;
3328         }
3329 		if (appCtxt.isWebClientOffline() && !AjxUtil.arrayContains(ZmOffline.SUPPORTED_APPS, app)) {
3330 			return false;
3331 		}
3332 		this.activateApp(app);
3333 		return true;
3334 	}
3335 
3336 	switch (actionCode) {
3337 
3338 		case ZmKeyMap.QUICK_REMINDER: {
3339             var account = appCtxt.multiAccounts && appCtxt.accountList.mainAccount;
3340             // calMgr.showQuickReminder uses an entire alternate search mechanism from ZmApptCache - setting params,
3341             // sending a search, etc.  Suppress for offline - lots of work for little gain to adapt this to offline modek
3342             if (appCtxt.get(ZmSetting.CALENDAR_ENABLED, null, account) && !appCtxt.isWebClientOffline()) {
3343                 var calMgr = appCtxt.getCalManager();
3344                 calMgr.showQuickReminder();
3345             }
3346 			break;
3347 		}
3348 
3349 		case ZmKeyMap.FOCUS_SEARCH_BOX: {
3350 			var stb = appCtxt.getSearchController().getSearchToolbar();
3351 			if (stb) {
3352 				var searchBox = stb.getSearchField();
3353 				appCtxt.getKeyboardMgr().grabFocus(searchBox);
3354 				if (ZmSearchAutocomplete) {
3355 					ZmSearchAutocomplete._ignoreNextKey = true;
3356 				}
3357 			}
3358 			break;
3359 		}
3360 
3361 		case ZmKeyMap.FOCUS_CONTENT_PANE: {
3362 			this.focusContentPane();
3363 			break;
3364 		}
3365 
3366 		case ZmKeyMap.FOCUS_TOOLBAR: {
3367 			this.focusToolbar();
3368 			break;
3369 		}
3370 
3371 		case ZmKeyMap.SHORTCUTS: {
3372 			this._showCurrentShortcuts();
3373 			break;
3374 		}
3375 
3376 		// this action needs to be last
3377 		case ZmKeyMap.CANCEL: {
3378 			// see if there's a current drag operation we can cancel
3379 			var handled = false;
3380 			var captureObj = (DwtMouseEventCapture.getId() == "DwtControl") ? DwtMouseEventCapture.getCaptureObj() : null;
3381 			var obj = captureObj && captureObj.targetObj;
3382 			if (obj && (obj._dragging == DwtControl._DRAGGING)) {
3383 				captureObj.release();
3384 				obj.__lastDestDwtObj = null;
3385 				obj._setDragProxyState(false);					// turn dnd icon red so user knows no drop is happening
3386 				DwtControl.__badDrop(obj, DwtShell.mouseEvent);	// shell's mouse ev should have latest info
3387 				handled = true;
3388 			}
3389 			if (handled) { break; }
3390 		}
3391 
3392 		default: {
3393             // Hand shortcut to current controller
3394 			var ctlr = appCtxt.getCurrentController();
3395 			return ctlr && ctlr.handleKeyAction ? ctlr.handleKeyAction(actionCode, ev) : false;
3396 		}
3397 	}
3398 
3399 	return true;
3400 };
3401 
3402 /**
3403  * Focuses on the content pane.
3404  * 
3405  */
3406 ZmZimbraMail.prototype.focusContentPane =
3407 function() {
3408 	// Set focus to the list view that's in the content pane. If there is no
3409 	// list view in the content pane, nothing happens. The list view will be
3410 	// found in the root tab group hierarchy.
3411 	var ctlr = appCtxt.getCurrentController();
3412 	var content = ctlr && ctlr._getDefaultFocusItem();
3413 	if (content) {
3414 		appCtxt.getKeyboardMgr().grabFocus(content);
3415 	}
3416 };
3417 
3418 /**
3419  * Focuses on the toolbar.
3420  * 
3421  */
3422 ZmZimbraMail.prototype.focusToolbar =
3423 function() {
3424 	// Set focus to the toolbar that's in the content pane.
3425 	var ctlr = appCtxt.getCurrentController();
3426 	var toolbar = ctlr && ctlr.getCurrentToolbar && ctlr.getCurrentToolbar();
3427 	if (toolbar) {
3428 		appCtxt.getKeyboardMgr().grabFocus(toolbar);
3429 	}
3430 };
3431 
3432 /**
3433  * Creates an "iframe view", which is a placeholder view for an app that's not
3434  * enabled but which has a tab. The app will have
3435  * a URL for its external content, which we put into an IFRAME.
3436  *
3437  * @param {constant}	appName	the name of app
3438  * 
3439  * @private
3440  */
3441 ZmZimbraMail.prototype._createAppIframeView =
3442 function(appName) {
3443 
3444 	var viewName = [appName, "iframe"].join("_"),
3445 		isSocial = (appName === ZmApp.SOCIAL),
3446 		params = { appName: appName },
3447 		appIframeView = this._appIframeView[appName];
3448 
3449 	if (!appIframeView) {
3450 		appIframeView = this._appIframeView[appName] = isSocial ? new ZmCommunityView(params) : new ZmAppIframeView(params);
3451 
3452 		var elements = {}, callbacks = {};
3453 		elements[ZmAppViewMgr.C_APP_CONTENT] = appIframeView;
3454 		callbacks[ZmAppViewMgr.CB_POST_SHOW] = this._displayAppIframeView.bind(this);
3455 
3456 		var hide = [ ZmAppViewMgr.C_TREE, ZmAppViewMgr.C_TREE_FOOTER, ZmAppViewMgr.C_TOOLBAR_TOP,
3457 			ZmAppViewMgr.C_NEW_BUTTON, ZmAppViewMgr.C_SASH ];
3458 
3459 		this._appViewMgr.createView({	viewId:			viewName,
3460 										appName:		appName,
3461 										controller:		this,
3462 										elements:		elements,
3463 										hide:			hide,
3464 										isTransient:	true,
3465 										isFullScreen:	true,
3466 										callbacks:		callbacks});
3467 	}
3468 
3469 	this._appViewMgr.pushView(viewName);
3470 	appIframeView.activate(true);
3471 };
3472 
3473 ZmZimbraMail.prototype._displayAppIframeView =
3474 function(appName) {
3475 	appCtxt.getApp(this._getDefaultStartAppName()).setOverviewPanelContent(false);
3476 };
3477 
3478 /**
3479  * Sets up Zimlet organizer type. This is run if we get zimlets in the
3480  * GetInfoResponse. Note that this will run before apps are instantiated,
3481  * which is necessary because they depend on knowing whether there are zimlets.
3482  * 
3483  * @private
3484  */
3485 ZmZimbraMail.prototype._postLoadZimlet =
3486 function() {
3487 	appCtxt.setZimletsPresent(true);
3488 };
3489 
3490 /**
3491  * @private
3492  */
3493 ZmZimbraMail.prototype._globalSelectionListener =
3494 function(ev) {
3495 	// bug 47514
3496 	if (this._waitDisallowed) {
3497 		this._waitDisallowed = false;
3498 		this.setInstantNotify(true);
3499 	}
3500 
3501 	if (!appCtxt.areZimletsLoaded()) { return; }
3502 
3503 	var item = ev.item;
3504 
3505 	// normalize action
3506 	var text = (item && item.getText) ? (item.getText() || item._toggleText) : null;
3507 	if (item && !text) {
3508 		text = item.getData(ZmOperation.KEY_ID) || item.getData(Dwt.KEY_ID);
3509 	}
3510 	if (text) {
3511 		var type;
3512 		if (item instanceof ZmAppButton) {
3513 			type = "app";
3514 		} else if (item instanceof DwtMenuItem) {
3515 			type = "menuitem";
3516 		} else if (item instanceof DwtButton) {
3517 			type = "button";
3518 		} else if (item instanceof DwtTreeItem) {
3519 			if (!item.getSelected()) { return; }
3520 			type = "treeitem";
3521 		} else {
3522 			type = item.toString();
3523 		}
3524 
3525 		var avm = appCtxt.getAppViewMgr();
3526 		var currentViewId = avm.getCurrentViewId();
3527 		var lastViewId = avm.getLastViewId();
3528 		var action = (AjxStringUtil.split((""+text), " ")).join("");
3529 		appCtxt.notifyZimlets("onAction", [type, action, currentViewId, lastViewId]);
3530 	}
3531 };
3532 
3533 ZmZimbraMail._folderTreeSashRelease =
3534 function(sash) {
3535 	var currentWidth = skin.getTreeWidth();
3536 	if (currentWidth) {
3537 		appCtxt.set(ZmSetting.FOLDER_TREE_SASH_WIDTH, currentWidth);
3538 	}
3539 };
3540 
3541 /**
3542  * @private
3543  */
3544 ZmZimbraMail._endSession =
3545 function() {
3546 	if (!AjxEnv.isPrism && navigator.onLine) {
3547 		// Let the server know that the session is ending.
3548 		var args = {
3549 			jsonObj: { EndSessionRequest: { _jsns: "urn:zimbraAccount" } },
3550 			asyncMode: !appCtxt.get("FORCE_CLEAR_COOKIES"),
3551 			emptyResponseOkay:	true
3552 		};
3553         var controller = appCtxt.getAppController();
3554 		controller && controller.sendRequest(args);
3555 	}
3556 };
3557 
3558 ZmZimbraMail.prototype.notify =
3559 function(eventType) {
3560 	this._evtMgr.notifyListeners(eventType, this._evt);
3561 };
3562 
3563 ZmZimbraMail.prototype._showCurrentShortcuts = function() {
3564 
3565 	var panel = appCtxt.getShortcutsPanel();
3566 	var curMap = this.getKeyMapName();
3567 	var km = appCtxt.getAppController().getKeyMapMgr();
3568 	var maps = km.getAncestors(curMap);
3569 	var inherits = (maps && maps.length > 0);
3570 	maps.unshift(curMap);
3571 	var maps2 = [];
3572 	if (inherits) {
3573 		if (maps.length > 1 && maps[maps.length - 1] == ZmKeyMap.MAP_GLOBAL) {
3574 			maps.pop();
3575 			maps2.push(ZmKeyMap.MAP_GLOBAL);
3576 		}
3577 	}
3578 
3579 	var col1 = {}, col2 = {};
3580 	col1.type = ZmShortcutList.TYPE_APP;
3581 	col1.maps = maps;
3582 	var colList = [col1];
3583 	if (maps2.length) {
3584 		col2.type = ZmShortcutList.TYPE_APP;
3585 		col2.maps = maps2;
3586 		colList.push(col2);
3587 	}
3588 	var col3 = {};
3589 	col3.type = ZmShortcutList.TYPE_SYS;
3590 	col3.maps = [];
3591 	var ctlr = appCtxt.getCurrentController();
3592 	var testMaps = ["list", "editor", "tabView"];
3593 	for (var i = 0; i < testMaps.length; i++) {
3594 		if (ctlr && ctlr.mapSupported(testMaps[i])) {
3595 			col3.maps.push(testMaps[i]);
3596 		}
3597 	}
3598 	col3.maps.push("button", "menu", "tree", "dialog", "toolbarHorizontal");
3599 	colList.push(col3);
3600 	panel.popup(colList);
3601 }
3602 
3603 // YUCK:
3604 ZmOrganizer.ZIMLET = "ZIMLET";
3605