1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 2010, 2011, 2012, 2013, 2014, 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) 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * Creates a preferences page for displaying notifications.
 26  *
 27  * @param {DwtControl}	parent			the containing widget
 28  * @param {Object}	section			the page
 29  * @param {ZmPrefController}	controller		the prefs controller
 30  *
 31  * @extends		ZmPreferencesPage
 32  */
 33 ZmNotificationsPage = function(parent, section, controller) {
 34 	if (arguments.length == 0) return;
 35 	ZmPreferencesPage.apply(this, arguments);
 36 };
 37 ZmNotificationsPage.prototype = new ZmPreferencesPage;
 38 ZmNotificationsPage.prototype.constructor = ZmNotificationsPage;
 39 
 40 ZmNotificationsPage.prototype.toString = function() {
 41 	return "ZmNotificationsPage";
 42 };
 43 
 44 //
 45 // Constants
 46 //
 47 
 48 // device email data
 49 
 50 ZmNotificationsPage.REGIONS = {};
 51 ZmNotificationsPage.CARRIERS = {};
 52 
 53 //
 54 // DwtControl methods
 55 //
 56 
 57 ZmNotificationsPage.prototype.getTabGroupMember = function() {
 58 	return this._form.getTabGroupMember();
 59 };
 60 
 61 ZmNotificationsPage.prototype._getValidatedDevice = function() {
 62 	var acct = appCtxt.multiAccounts && appCtxt.getActiveAccount();
 63 	return appCtxt.get(ZmSetting.CAL_DEVICE_EMAIL_REMINDERS_ADDRESS, null, acct);
 64 };
 65 
 66 ZmNotificationsPage.prototype._getEmailAddress = function() {
 67 	var acct = appCtxt.multiAccounts && appCtxt.getActiveAccount();
 68 	return appCtxt.get(ZmSetting.CAL_EMAIL_REMINDERS_ADDRESS, null, acct);
 69 };
 70 
 71 
 72 //
 73 // ZmPreferencesPage methods
 74 //
 75 
 76 ZmNotificationsPage.prototype.showMe = function() {
 77 	// default processing
 78 	var initialize = !this.hasRendered;
 79 	ZmPreferencesPage.prototype.showMe.apply(this, arguments);
 80 
 81 	// setup controls
 82 	var status = this._getValidatedDevice() ?
 83 			ZmNotificationsPageForm.CONFIRMED : ZmNotificationsPageForm.UNCONFIRMED;
 84 	this._form.setValue("DEVICE_EMAIL_CODE_STATUS_VALUE", status);
 85 	this._form.setValue("EMAIL", this._getEmailAddress());
 86 	this._form.update();
 87 
 88 	// load SMS data, if needed
 89 	if (initialize && this._form.getControl("DEVICE_EMAIL_REGION") != null) {
 90 		var locid = window.appRequestLocaleId ? "&locid=" + window.appRequestLocaleId : "";
 91 		var includes = [
 92 			[   appContextPath,
 93 				"/res/ZmSMS.js",
 94 				"?v=",cacheKillerVersion,
 95 				locid,
 96 				appDevMode ? "&debug=1" : ""
 97 			].join("")
 98 		];
 99 		var baseurl = null;
100 		var callback = new AjxCallback(this, this._smsDataLoaded);
101 		var proxy = null;
102 		AjxInclude(includes, baseurl, callback, proxy);
103 	}
104 };
105 
106 /**
107  * <strong>Note:</strong>
108  * Only the email field is a preference that the user can set directly.
109  *
110  * @private
111  */
112 ZmNotificationsPage.prototype.isDirty = function() {
113 	return this._form.getValue("EMAIL") != this._getEmailAddress();
114 };
115 
116 /**
117  * <strong>Note:</strong>
118  * Only the email field is a preference that the user can set directly.
119  *
120  * @private
121  */
122 ZmNotificationsPage.prototype.validate = function() {
123 	return this._form.isValid("EMAIL");
124 };
125 
126 ZmNotificationsPage.prototype.setFormValue = function(id, value, setup, control) {
127 	value = ZmPreferencesPage.prototype.setFormValue.apply(this, arguments);
128 	if (id == ZmSetting.CAL_EMAIL_REMINDERS_ADDRESS) {
129 		this._form.setValue("EMAIL", value);
130 		this._form.update();
131 	}
132 	return value;
133 };
134 
135 ZmNotificationsPage.prototype.getFormValue = function(id, setup, control) {
136 	if (id == ZmSetting.CAL_EMAIL_REMINDERS_ADDRESS) {
137 		var value = this._form.getValue("EMAIL");
138 		return setup && setup.valueFunction ? setup.valueFunction(value) : value;
139 	}
140 	return ZmPreferencesPage.prototype.getFormValue.apply(this, arguments);
141 };
142 
143 /**
144  * This class compleletely overrides the ZmPreferencePage's page
145  * creation in order to use DwtForm for easier page creation. By
146  * using DwtForm, we can get automatic control creation, tab group
147  * ordering, and interactivity dependencies.
148  *
149  * @private
150  */
151 ZmNotificationsPage.prototype._createPageTemplate = function() {
152 	DBG.println(AjxDebug.DBG2, "rendering preferences page " + this._section.id);
153 	this._cleanup();
154 	this.setVisible(false); // hide until ready
155 };
156 
157 ZmNotificationsPage.prototype._cleanup =
158 		function() {
159 			this.setContent("");
160 		}
161 
162 ZmNotificationsPage.prototype._createControls = function() {
163 
164 	//cleanup
165 	this._cleanup();
166 
167 	// the following part is to do stuff that ZmPreferencesPage does (setting the origValue of the preference),
168 	// but simplified for this case (since we are not using all the stuff from ZmPreferencesPage, we are using DwtForm instead)
169 	var email = this._getEmailAddress();
170 	var settings = appCtxt.getSettings();
171 	var emailPrefId = ZmSetting.CAL_EMAIL_REMINDERS_ADDRESS;
172 	var pref = settings.getSetting(emailPrefId);
173 	// save the current value (for checking later if it changed)
174 	pref.origValue = email;
175 
176 	// create form controls
177 	this._form = this._setupCustomForm();
178 	this._form.reparentHtmlElement(this.getContentHtmlElement());
179 
180 	// make it look like email notification pref control is present
181 	// NOTE: This is needed so that the default pref saving works.
182 	// NOTE: This is used in conjunction with set/getFormValue.
183 	if (this._form.getControl("EMAIL")) {
184 		this._prefPresent = {};
185 		this._prefPresent[emailPrefId] = true;
186 	}
187 
188 	// finish setup
189 	this.setVisible(true);
190 	this.hasRendered = true;
191 };
192 
193 ZmNotificationsPage.prototype._resetPageListener = function() {
194 	ZmPreferencesPage.prototype._resetPageListener.apply(this, arguments);
195 	this._form.reset();
196 };
197 
198 //
199 // Protected methods
200 //
201 
202 ZmNotificationsPage.prototype._setupCustomForm = function() {
203 	return new ZmNotificationsPageForm({parent:this, sectionTemplate:this._section.templateId, id:"ZmNotificationsPage"});
204 };
205 
206 ZmNotificationsPage.prototype._smsDataLoaded = function() {
207 	if (!window.ZmSMS) return;
208 	this._form.setSMSData(window.ZmSMS);
209 };
210 
211 //
212 // Classes
213 //
214 
215 ZmNotificationsPageForm = function(params) {
216 	if (arguments.length == 0) return;
217 	params.form = this._getFormParams(params.sectionTemplate);
218 	DwtForm.apply(this, arguments);
219 
220     // hide DwtSelect options which overflow the container
221 	var select = this.getControl("DEVICE_EMAIL_CARRIER");
222 	if (select) {
223 		select.dynamicButtonWidth();
224 	}
225 
226 	this._regionSelectionListener = new AjxListener(this, this._handleRegionSelection);
227 
228 	// listen to CAL_DEVICE_EMAIL_REMINDERS_ADDRESS changes
229 	// NOTE: We can't actually listen to changes in this value because
230 	// NOTE: ZmSetting doesn't notify the listeners when the value is
231 	// NOTE: set *if* it has an actual LDAP name.
232 };
233 ZmNotificationsPageForm.prototype = new DwtForm;
234 ZmNotificationsPageForm.prototype.constructor = ZmNotificationsPageForm;
235 
236 ZmNotificationsPageForm.prototype.toString = function() {
237 	return "ZmNotificationsPageForm";
238 };
239 
240 // Constants: special region/carrier ids
241 
242 ZmNotificationsPageForm.CUSTOM = "Custom";
243 ZmNotificationsPageForm.UNKNOWN = "Unknown";
244 
245 // Constants: code status values; also doubles as element class names
246 
247 ZmNotificationsPageForm.CONFIRMED = "DeviceCodeConfirmed";
248 ZmNotificationsPageForm.PENDING = "DeviceCodePending";
249 ZmNotificationsPageForm.UNCONFIRMED = "DeviceCodeUnconfirmed";
250 
251 // Constants: private
252 
253 ZmNotificationsPageForm.__letters2NumbersMap = {
254 	a:2,b:2,c:2,d:3,e:3,f:3,
255 	g:4,h:4,i:4,	j:5,k:5,l:5,m:6,n:6,o:6,
256 	p:7,q:7,r:7,s:7,t:8,u:8,v:8,w:9,x:9,y:9,z:9
257 };
258 
259 // Public
260 
261 ZmNotificationsPageForm.prototype.setSMSData = function(data) {
262 	this._smsData = data;
263 
264 	// setup regions control
265 	var regionMap = this._defineRegions(data);
266 	var button = this.getControl("DEVICE_EMAIL_REGION");
267 	var menu = this.__createRegionsMenu(button, regionMap);
268 	if (menu == null) {
269 		// always have at least one region
270 		menu = new DwtMenu({parent:button});
271 		this.__createRegionMenuItem(menu, ZmNotificationsPageForm.UNKNOWN, ZmMsg.unknown);
272 	}
273 	menu.addSelectionListener(this._regionSelectionListener);
274 	button.setMenu(menu);
275 
276 	// set default region
277 	this.setRegion(data.defaultRegionId);
278 };
279 
280 ZmNotificationsPageForm.prototype.setRegion = function(regionId) {
281 	// decorate the region button
282 	var region = ZmNotificationsPage.REGIONS[regionId] || { id:ZmNotificationsPageForm.UNKNOWN, label:ZmMsg.unknown };
283 	var button = this.getControl("DEVICE_EMAIL_REGION");
284 	button.setText(region.label);
285 	button.setImage(region.image);
286 	button.setData(Dwt.KEY_ID, region.id);
287 
288 	// update the form
289 	this.setCarriers(regionId);
290 	this.setValue("DEFAULT_EMAIL_REGION", regionId);
291 	if (regionId == this._smsData.defaultRegionId && this._smsData.defaultCarrierId) {
292 		this.setValue("DEVICE_EMAIL_CARRIER", this._smsData.defaultCarrierId);
293 	}
294 	this.update();
295 };
296 
297 ZmNotificationsPageForm.prototype.setCarriers = function(regionId) {
298 	var select = this.getControl("DEVICE_EMAIL_CARRIER");
299 	select.clearOptions();
300 	var region = ZmNotificationsPage.REGIONS[regionId] || {};
301 	var carriers = ZmNotificationsPageForm.__getRegionCarriers(region, true);
302 	carriers.sort(ZmNotificationsPageForm.__byLabel);
303 	for (var i = 0; i < carriers.length; i++) {
304 		var carrier = carriers[i];
305 		var image = carrier.image || carrier.region.image;
306 		select.addOption({displayValue:carrier.label, value:carrier.id, image:image});
307 	}
308 	select.addOption({displayValue:ZmMsg.custom, value:ZmNotificationsPageForm.CUSTOM, image:null});
309 };
310 
311 ZmNotificationsPageForm.prototype.isCustom = function() {
312 	// user selected "Custom"
313 	var carrierId = this.getValue("DEVICE_EMAIL_CARRIER");
314 	if (carrierId == ZmNotificationsPageForm.CUSTOM) return true;
315 
316 	// any entry w/o an email pattern is also custom
317 	var carrier = ZmNotificationsPage.CARRIERS[carrierId];
318 	var hasPattern = Boolean(carrier && carrier.pattern);
319 	return !hasPattern;
320 };
321 
322 ZmNotificationsPageForm.prototype.getEmailAddress = function() {
323 	if (this.isCustom()) {
324 		var number = this.getValue("DEVICE_EMAIL_CUSTOM_NUMBER");
325 		var address = this.getValue("DEVICE_EMAIL_CUSTOM_ADDRESS");
326 		return number && address ? [number,address].join("@") : "";
327 	}
328 
329 	var phone = ZmNotificationsPageForm.normalizePhoneNumber(this.getValue("DEVICE_EMAIL_PHONE"));
330 	var carrier = ZmNotificationsPage.CARRIERS[this.getValue("DEVICE_EMAIL_CARRIER")];
331 	return phone ? AjxMessageFormat.format(carrier.pattern, [phone]) : "";
332 };
333 
334 ZmNotificationsPageForm.prototype.getCodeStatus = function() {
335 	// is there anything to do?
336 	var control = this.getControl("DEVICE_EMAIL_CODE_STATUS");
337 	if (!control) return "";
338 
339 	// remove other status class names
340 	var controlEl = control.getHtmlElement();
341 	Dwt.delClass(controlEl, ZmNotificationsPageForm.CONFIRMED);
342 	Dwt.delClass(controlEl, ZmNotificationsPageForm.PENDING);
343 	Dwt.delClass(controlEl, ZmNotificationsPageForm.UNCONFIRMED);
344 
345 	// add appropriate class name
346 	var status = this.get("DEVICE_EMAIL_CODE_STATUS_VALUE");
347 	Dwt.addClass(controlEl, status);
348 
349 	// format status text
350 	if (status == ZmNotificationsPageForm.CONFIRMED) {
351 		var email = this._getValidatedDevice();
352 		if (email) {
353 			var pattern = ZmMsg.deviceEmailNotificationsVerificationStatusConfirmed;
354 			return AjxMessageFormat.format(pattern, [email]);
355 		}
356 		// default back to unconfirmed
357 		Dwt.delClass(controlEl, status, ZmNotificationsPageForm.UNCONFIRMED);
358 	}
359 	return status == ZmNotificationsPageForm.PENDING ?
360 			ZmMsg.deviceEmailNotificationsVerificationStatusPending :
361 			ZmMsg.deviceEmailNotificationsVerificationStatusUnconfirmed
362 			;
363 };
364 
365 ZmNotificationsPageForm.prototype.getPhoneHint = function() {
366 	var carrierId = this.getValue("DEVICE_EMAIL_CARRIER");
367 	var carrier = ZmNotificationsPage.CARRIERS[carrierId];
368 	var carrierHint = carrier && carrier.hint;
369 	var email = this.getEmailAddress();
370 	var emailHint = email && AjxMessageFormat.format(ZmMsg.deviceEmailNotificationsPhoneNumber, [email]);
371 	if (carrierHint || emailHint) {
372 		if (!carrierHint) return emailHint;
373 		if (!emailHint) return carrierHint;
374 		return AjxMessageFormat.format(ZmMsg.deviceEmailNotificationsCarrierEmailHint, [emailHint,carrierHint]);
375 	}
376 	return "";
377 };
378 
379 ZmNotificationsPageForm.normalizePhoneNumber = function(phone) {
380 	if (phone) {
381 		phone = phone.replace(/[a-z]/gi, ZmNotificationsPageForm.__letters2Numbers);
382 		phone = phone.replace(/[^+#\*0-9]/g, "");
383 	}
384 	return phone;
385 };
386 
387 // Protected
388 
389 ZmNotificationsPageForm.prototype._getFormParams = function(templateId) {
390 	return {
391 		template: templateId,
392 		items: [
393 			// default pref page controls
394 			{ id: "REVERT_PAGE", type: "DwtButton", label: ZmMsg.restorePage,
395 				onclick: "this.parent._resetPageListener()"
396 			},
397 			// email
398 			{ id: "EMAIL", type: "DwtInputField", hint: ZmMsg.exampleEmailAddr, cols:100 },
399 			// device email (aka SMS)
400 			{ id: "DEVICE_EMAIL_REGION", type: "DwtButton",
401 				enabled: "this._smsData",
402 				onclick: this._handleRegionClick
403 			},
404 			{ id: "DEVICE_EMAIL_CARRIER", type: "DwtSelect", value: ZmNotificationsPageForm.CUSTOM,
405 				enabled: this._isCarrierEnabled
406 			},
407 			{ id: "DEVICE_EMAIL_PHONE", type: "DwtInputField",
408 				hint: ZmMsg.deviceEmailNotificationsPhoneHint,
409 				visible: "!this.isCustom()",
410 				onchange: this._handleCarrierChange
411 			},
412 			{ id: "DEVICE_EMAIL_PHONE_HINT", type: "DwtText",
413 				getter: this.getPhoneHint,
414 				visible: "get('DEVICE_EMAIL_PHONE_HINT')" // NOTE: only show if there's a value
415 			},
416 			{ id: "DEVICE_EMAIL_PHONE_SEND_CODE", type: "DwtButton",
417 				label: ZmMsg.deviceEmailNotificationsVerificationCodeSend,
418 				visible: "!this.isCustom()",
419 				enabled: "get('DEVICE_EMAIL_PHONE')",
420 				onclick: this._handleSendCode
421 			},
422 			{ id: "DEVICE_EMAIL_CUSTOM_NUMBER", type: "DwtInputField",
423 				visible: "this.isCustom()"
424 			},
425 			{ id: "DEVICE_EMAIL_CUSTOM_ADDRESS", type: "DwtInputField",
426 				visible: "this.isCustom()"
427 			},
428 			{ id: "DEVICE_EMAIL_CUSTOM_SEND_CODE", type: "DwtButton",
429 				label: ZmMsg.deviceEmailNotificationsVerificationCodeSend,
430 				visible: "this.isCustom()",
431 				enabled: "get('DEVICE_EMAIL_CUSTOM_NUMBER') && get('DEVICE_EMAIL_CUSTOM_ADDRESS')",
432 				onclick: this._handleSendCode
433 			},
434 			{ id: "DEVICE_EMAIL_CODE", type: "DwtInputField",
435 				hint: ZmMsg.deviceEmailNotificationsVerificationCodeHint
436 			},
437 			{ id: "DEVICE_EMAIL_CODE_VALIDATE", type: "DwtButton",
438 				label: ZmMsg.deviceEmailNotificationsVerificationCodeValidate,
439 				enabled: "get('DEVICE_EMAIL_CODE') && this.getEmailAddress()",
440 				onclick: this._handleValidateCode
441 			},
442 			{ id: "DEVICE_EMAIL_CODE_INVALIDATE", type: "DwtButton",
443 				label: ZmMsg.deviceEmailNotificationsVerificationCodeInvalidate,
444 				visible: "this._getValidatedDevice()",
445 				onclick: this._handleInvalidateDevice
446 			},
447 			{ id: "DEVICE_EMAIL_CODE_STATUS", type: "DwtText",
448 				className: "DeviceCode", getter: this.getCodeStatus
449 			},
450 			// NOTE: This holds the current code status
451 			{ id: "DEVICE_EMAIL_CODE_STATUS_VALUE", value: ZmNotificationsPageForm.UNCONFIRMED }
452 		]
453 	};
454 };
455 
456 ZmNotificationsPageForm.prototype._isCarrierEnabled = function() {
457 	var control = this.getControl('DEVICE_EMAIL_CARRIER');
458 	return control && control.getOptionCount() > 0;
459 };
460 
461 ZmNotificationsPageForm.prototype._defineRegions = function(data) {
462 	// define regions
463 	var regionMap = {};
464 	for (var id in data) {
465 		// do we care about this entry?
466 		if (!id.match(/^region_/)) continue;
467 
468 		// take apart message key
469 		var parts = id.split(/[_\.]/);
470 		var prop = parts[parts.length - 1];
471 
472 		// set region info
473 		var regions = regionMap;
474 		var regionIds = parts[1].split("/"), region;
475 		for (var i = 0; i < regionIds.length; i++) {
476 			var regionId = regionIds[i];
477 			if (!regions[regionId]) {
478 				regions[regionId] = ZmNotificationsPage.REGIONS[regionId] = { id: regionId };
479 			}
480 			region = regions[regionId];
481 			if (i < regionIds.length - 1 && !region.regions) {
482 				region.regions = {};
483 			}
484 			regions = region.regions;
485 		}
486 
487 		// store property
488 		region[prop] = data[id];
489 	}
490 
491 	// define carriers
492 	this._defineCarriers(data);
493 
494 	return regionMap;
495 };
496 
497 ZmNotificationsPageForm.prototype._defineCarriers = function(data) {
498 	for (var id in data) {
499 		// do we care about this entry?
500 		if (!id.match(/^carrier_/)) continue;
501 
502 		// take apart message key
503 		var s = id.split(/[_\.]/);
504 		var prop = s[s.length - 1];
505 
506 		// set carrier info
507 		var carrierId = s.slice(1, 3).join("_");
508 		var carrier = ZmNotificationsPage.CARRIERS[carrierId];
509 		if (!carrier) {
510 			var regionId = s[1];
511 			var region = ZmNotificationsPage.REGIONS[regionId];
512 			if (!region.carriers) {
513 				region.carriers = {};
514 			}
515 			carrier = region.carriers[carrierId] = ZmNotificationsPage.CARRIERS[carrierId] = {
516 				id: carrierId, region: region
517 			};
518 		}
519 
520 		// store property
521 		carrier[prop] = data[id];
522 	}
523 	return ZmNotificationsPage.CARRIERS;
524 };
525 
526 ZmNotificationsPageForm.prototype._handleRegionClick = function() {
527 	var button = this.getControl("DEVICE_EMAIL_REGION");
528 	var menu = button.getMenu();
529 	if (menu.isPoppedUp()) {
530 		menu.popdown();
531 	}
532 	else {
533 		button.popup();
534 	}
535 };
536 
537 ZmNotificationsPageForm.prototype._handleCarrierChange = function() {
538 	var controlId = this.isCustom() ? "DEVICE_EMAIL_NUMBER" : "DEVICE_EMAIL_PHONE";
539 	var control = this.getControl(controlId);
540 	if (control && control.focus) {
541 		control.focus();
542 	}
543 };
544 
545 ZmNotificationsPageForm.prototype._handleSendCode = function() {
546 	var params = {
547 		jsonObj: {
548 			SendVerificationCodeRequest: {
549 				_jsns: "urn:zimbraMail",
550 				a: this.getEmailAddress()
551 			}
552 		},
553 		asyncMode: true,
554 		callback: new AjxCallback(this, this._handleSendCodeResponse)
555 	};
556 	appCtxt.getAppController().sendRequest(params);
557 };
558 
559 ZmNotificationsPageForm.prototype._handleSendCodeResponse = function(resp) {
560 	appCtxt.setStatusMsg(ZmMsg.deviceEmailNotificationsVerificationCodeSendSuccess);
561 
562 	this.setValue("DEVICE_EMAIL_CODE_STATUS_VALUE", ZmNotificationsPageForm.PENDING);
563 	this.update();
564 
565 	var dialog = appCtxt.getMsgDialog();
566 	var email = appCtxt.get(ZmSetting.USERNAME);
567 	var message = AjxMessageFormat.format(ZmMsg.deviceEmailNotificationsVerificationCodeSendNote, [email]);
568 	dialog.setMessage(message);
569 	dialog.popup();
570 };
571 
572 ZmNotificationsPageForm.prototype._handleRegionSelection = function(event) {
573 	var regionId = event.item.getData(Dwt.KEY_ID);
574 	this.setRegion(regionId);
575 };
576 
577 ZmNotificationsPageForm.prototype._handleValidateCode = function() {
578 	var params = {
579 		jsonObj: {
580 			VerifyCodeRequest: {
581 				_jsns: "urn:zimbraMail",
582 				a: this.getEmailAddress(),
583 				code: this.getValue("DEVICE_EMAIL_CODE")
584 			}
585 		},
586 		asyncMode: true,
587 		callback: new AjxCallback(this, this._handleValidateCodeResponse)
588 	};
589 	appCtxt.getAppController().sendRequest(params);
590 };
591 
592 ZmNotificationsPageForm.prototype._handleValidateCodeResponse = function(resp) {
593 	var success = AjxUtil.get(resp.getResponse(), "VerifyCodeResponse", "success") == "1";
594 	var params = {
595 		msg: success ?
596 				ZmMsg.deviceEmailNotificationsVerificationCodeValidateSuccess :
597 				ZmMsg.deviceEmailNotificationsVerificationCodeValidateFailure,
598 		level: success ? ZmStatusView.LEVEL_INFO : ZmStatusView.LEVEL_CRITICAL
599 	};
600 	appCtxt.setStatusMsg(params);
601 
602 	// NOTE: Since the preference values only come in at launch time,
603 	// NOTE: manually set the confirmed email address so that we can
604 	// NOTE: display the correct confirmed code status text
605 	if (success) {
606 		appCtxt.set(ZmSetting.CAL_DEVICE_EMAIL_REMINDERS_ADDRESS, this.getEmailAddress());
607 	}
608 
609 	var status = success ? ZmNotificationsPageForm.CONFIRMED : ZmNotificationsPageForm.UNCONFIRMED;
610 	this.setValue("DEVICE_EMAIL_CODE_STATUS_VALUE", status);
611 	this.update();
612 };
613 
614 
615 ZmNotificationsPageForm.prototype._getValidatedDevice = function() {
616 	return this.parent._getValidatedDevice();
617 };
618 
619 
620 ZmNotificationsPageForm.prototype._handleInvalidateDevice = function() {
621 	var params = {
622 		jsonObj: {
623 			InvalidateReminderDeviceRequest: {
624 				_jsns: "urn:zimbraMail",
625 				a: this._getValidatedDevice()
626 			}
627 		},
628 		asyncMode: true,
629 		callback: new AjxCallback(this, this._handleInvalidateDeviceResponse)
630 	};
631 	appCtxt.getAppController().sendRequest(params);
632 };
633 
634 ZmNotificationsPageForm.prototype._handleInvalidateDeviceResponse = function(resp) {
635 	var dummy = resp.getResponse(); //to get the exception thrown if there was some unexpected error.
636 	var params = {
637 		msg: ZmMsg.deviceEmailNotificationsVerificationCodeInvalidateSuccess,
638 		level: ZmStatusView.LEVEL_INFO
639 	};
640 	appCtxt.setStatusMsg(params);
641 
642 	appCtxt.set(ZmSetting.CAL_DEVICE_EMAIL_REMINDERS_ADDRESS, null);
643 	this.setValue("DEVICE_EMAIL_CODE_STATUS_VALUE", ZmNotificationsPageForm.UNCONFIRMED);
644 	this.update();
645 };
646 
647 
648 // Private
649 
650 ZmNotificationsPageForm.prototype.__createRegionsMenu = function(parent, regionMap, parentRegion) {
651 	var regions = AjxUtil.values(regionMap, ZmNotificationsPageForm.__acceptRegion);
652 	if (regions.length == 0) return null;
653 
654 	var menu = new DwtMenu({parent:parent});
655 
656 	regions.sort(ZmNotificationsPageForm.__byLabel);
657 	for (var i = 0; i < regions.length; i++) {
658 		var region = regions[i];
659 		// add entry for this region
660 		var image = parent instanceof DwtMenuItem ? region.image : null;
661 		var menuItem = this.__createRegionMenuItem(menu, region.id, region.label, image);
662 		// add sub-regions
663 		var subMenu = region.regions && this.__createRegionsMenu(menuItem, region.regions, region);
664 		if (subMenu && subMenu.getItemCount() > 0) {
665 			subMenu.addSelectionListener(this._regionSelectionListener);
666 			menuItem.setMenu(subMenu);
667 		}
668 	}
669 
670 	// NOTE: Since only the call to this method *within* this method
671 	// NOTE: passes in a parentRegion, we'll only add a general entry for
672 	// NOTE: the parent region at a sub-level and never at the top level.
673 	if (parentRegion) {
674 		var hasCarriers = parentRegion.carriers && parentRegion.carriers.length > 0;
675 		var menuItemCount = menu.getItemCount();
676 		if (hasCarriers || menuItemCount > 1) {
677 			this.__createRegionMenuItem(menu, parentRegion.id, parentRegion.label, parentRegion.image, 0);
678 			if (menuItemCount > 1) {
679 				new DwtMenuItem({parent:menu,style:DwtMenuItem.SEPARATOR_STYLE,index:1});
680 			}
681 		}
682 	}
683 
684 	return menu;
685 };
686 
687 ZmNotificationsPageForm.prototype.__createRegionMenuItem = function(parent, id, label, image, index) {
688 	var menuItem = new DwtMenuItem({parent:parent,index:index});
689 	menuItem.setText(label);
690 	menuItem.setImage(image);
691 	menuItem.setData(Dwt.KEY_ID, id);
692 	return menuItem;
693 };
694 
695 ZmNotificationsPageForm.__acceptRegion = function(regionId, regionMap) {
696 	var region = regionMap[regionId];
697 	var hasRegions = false;
698 	for (var id in region.regions) {
699 		if (ZmNotificationsPageForm.__acceptRegion(id, region.regions)) {
700 			hasRegions = true;
701 			break;
702 		}
703 	}
704 	var hasCarriers = ZmNotificationsPageForm.__getRegionCarriers(region).length > 0;
705 	return hasRegions || hasCarriers;
706 };
707 
708 ZmNotificationsPageForm.__getRegionCarriers = function(region, recurse) {
709 	if (region.carriers && !(region.carriers instanceof Array)) {
710 		region.carriers = AjxUtil.values(region.carriers);
711 		region.carriers.sort(ZmNotificationsPageForm.__byLabel);
712 	}
713 	var carriers = region.carriers || [];
714 	if (recurse && region.regions) {
715 		for (var regionId in region.regions) {
716 			carriers = carriers.concat(ZmNotificationsPageForm.__getRegionCarriers(region.regions[regionId], true));
717 		}
718 	}
719 	return carriers;
720 };
721 
722 ZmNotificationsPageForm.__byLabel = AjxCallback.simpleClosure(AjxUtil.byStringProp, window, "label");
723 
724 ZmNotificationsPageForm.__letters2Numbers = function($0) {
725 	return ZmNotificationsPageForm.__letters2NumbersMap[$0.toLowerCase()];
726 };
727