1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 /**
 25  * @class
 26  * This class provides a central area for managing email recipient fields. It is not a control,
 27  * and does not exist within the widget hierarchy.
 28  * 
 29  * @param {hash}		params						a hash of params:
 30  * @param {function}	resetContainerSizeMethod	callback for when size needs to be adjusted
 31  * @param {function}	enableContainerInputs		callback for enabling/disabling input fields
 32  * @param {function}	reenter						callback to enable design mode
 33  * @param {AjxListener}	contactPopdownListener		listener called when contact picker pops down
 34  * @param {string}		contextId					ID of owner (used for autocomplete list)
 35  */
 36 ZmRecipients = function(params) {
 37 
 38 	this._divId			    = {};
 39 	this._buttonTdId	    = {};
 40 	this._fieldId		    = {};
 41 	this._using			    = {};
 42 	this._button		    = {};
 43 	this._field			    = {};
 44 	this._divEl			    = {};
 45 	this._addrInputField    = {};
 46 
 47     this._resetContainerSize = params.resetContainerSizeMethod;
 48     this._enableContainerInputs = params.enableContainerInputs;
 49     this._reenter = params.reenter;
 50     this._contactPopdownListener = params.contactPopdownListener;
 51 	this._contextId = params.contextId;
 52 
 53     this._bubbleOps = {};
 54     this._bubbleOps[AjxEmailAddress.TO]  = ZmOperation.MOVE_TO_TO;
 55     this._bubbleOps[AjxEmailAddress.CC]  = ZmOperation.MOVE_TO_CC;
 56     this._bubbleOps[AjxEmailAddress.BCC] = ZmOperation.MOVE_TO_BCC;
 57     this._opToField = {};
 58     this._opToField[ZmOperation.MOVE_TO_TO]  = AjxEmailAddress.TO;
 59     this._opToField[ZmOperation.MOVE_TO_CC] = AjxEmailAddress.CC;
 60     this._opToField[ZmOperation.MOVE_TO_BCC] = AjxEmailAddress.BCC;
 61 };
 62 
 63 ZmRecipients.OP = {};
 64 ZmRecipients.OP[AjxEmailAddress.TO]		= ZmId.CMP_TO;
 65 ZmRecipients.OP[AjxEmailAddress.CC]		= ZmId.CMP_CC;
 66 ZmRecipients.OP[AjxEmailAddress.BCC]	= ZmId.CMP_BCC;
 67 
 68 ZmRecipients.BAD = "_bad_addrs_";
 69 
 70 
 71 ZmRecipients.prototype.attachFromSelect =
 72 function(fromSelect) {
 73     this._fromSelect = fromSelect;
 74 }
 75 
 76 ZmRecipients.prototype.createRecipientIds =
 77 function(htmlElId, typeStr) {
 78     var ids = {};
 79     var components = ["row", "picker", "control", "cell"];
 80     for (var i = 0; i < components.length; i++) {
 81         ids[components[i]] = [htmlElId, typeStr, components[i]].join("_")
 82     }
 83     return ids;
 84 }
 85 
 86 
 87 ZmRecipients.prototype.createRecipientHtml =
 88 function(parent, viewId, htmlElId, fieldNames) {
 89 
 90     this._fieldNames = fieldNames;
 91 	var contactsEnabled = appCtxt.get(ZmSetting.CONTACTS_ENABLED);
 92 	var galEnabled = appCtxt.get(ZmSetting.GAL_ENABLED);
 93 
 94     	// init autocomplete list
 95     if (contactsEnabled || galEnabled || appCtxt.isOffline) {
 96 		var params = {
 97 			dataClass:		appCtxt.getAutocompleter(),
 98 			matchValue:		ZmAutocomplete.AC_VALUE_FULL,
 99 			keyUpCallback:	this._acKeyupHandler.bind(this),
100 			contextId:		this._contextId
101 		};
102 		this._acAddrSelectList = new ZmAutocompleteListView(params);
103 	}
104 
105 	var isPickerEnabled = contactsEnabled || galEnabled || appCtxt.multiAccounts;	
106 	
107 	this._pickerButton = {};
108 
109 	// process compose fields
110 	for (var i = 0; i < fieldNames.length; i++) {
111 		var type = fieldNames[i];
112 		var typeStr = AjxEmailAddress.TYPE_STRING[type];
113 
114 		// save identifiers
115         var ids = this.createRecipientIds(htmlElId, typeStr);
116 		this._divId[type] = ids.row;
117 		this._buttonTdId[type] = ids.picker;
118 		var inputId = this._fieldId[type] = ids.control;
119 		var label = AjxMessageFormat.format(ZmMsg.addressFieldLabel, ZmMsg[AjxEmailAddress.TYPE_STRING[this._fieldNames[i]]]);
120 
121 		// save field elements
122 		this._divEl[type] = document.getElementById(this._divId[type]);
123 		var aifId;
124 		var aifParams = {
125 			parent:								parent,
126 			autocompleteListView:				this._acAddrSelectList,
127 			bubbleAddedCallback:				this._bubblesChangedCallback.bind(this),
128 			bubbleRemovedCallback:				this._bubblesChangedCallback.bind(this),
129 			bubbleMenuCreatedCallback:			this._bubbleMenuCreated.bind(this),
130 			bubbleMenuResetOperationsCallback:	this._bubbleMenuResetOperations.bind(this),
131 			inputId:							inputId,
132 			label: 								label,
133 			type:								type
134 		}
135 		var aif = this._addrInputField[type] = new ZmAddressInputField(aifParams);
136 		aifId = aif._htmlElId;
137 		aif.reparentHtmlElement(ids.cell);
138 
139 		// save field control
140 		var fieldEl = this._field[type] = document.getElementById(this._fieldId[type]);
141 		if (fieldEl) {
142 			fieldEl.addrType = type;
143 			fieldEl.supportsAutoComplete = true;
144 		}
145 
146 		// create picker
147 		if (isPickerEnabled) {
148 
149 			// bug 78318 - if GAL enabled but not contacts, we need some things defined to handle GAL search
150 			if (!contactsEnabled) {
151 				appCtxt.getAppController()._createApp(ZmApp.CONTACTS);
152 			}
153 
154 			var pickerId = this._buttonTdId[type];
155 			var pickerEl = document.getElementById(pickerId);
156 			if (pickerEl) {
157 				var buttonId = ZmId.getButtonId(viewId, ZmRecipients.OP[type]);
158 				var button = this._pickerButton[type] = new DwtButton({parent:parent, id:buttonId});
159 				button.setText(pickerEl.innerHTML);
160 				button.replaceElement(pickerEl);
161 
162 				button.addSelectionListener(this.addressButtonListener.bind(this));
163 				button.addrType = type;
164 
165 				// autocomplete-related handlers
166 				// Enable this even if contacts are not enabled, to provide GAL autoComplete
167 				this._acAddrSelectList.handle(fieldEl, aifId);
168 
169 				this._button[type] = button;
170 			}
171 		} else {
172 			// Mark the field, so that it will be sized properly in ZmAddressInputField._resizeInput.
173 			// Otherwise, it is set to 30px wide, which makes it rather hard to type into.
174 			fieldEl.supportsAutoComplete = false;
175 		}
176 	}
177 };
178 
179 ZmRecipients.prototype.reset =
180 function() {
181 
182 	// reset To/CC/BCC fields
183 	for (var i = 0; i < this._fieldNames.length; i++) {
184 		var type = this._fieldNames[i];
185 		var textarea = this._field[type];
186 		textarea.value = "";
187 		var addrInput = this._addrInputField[type];
188 		if (addrInput) {
189 			addrInput.clear();
190 		}
191 	}
192 };
193 
194 ZmRecipients.prototype.resetPickerButtons =
195 function(account) {
196 	var ac = window.parentAppCtxt || window.appCtxt;
197 	var isEnabled = ac.get(ZmSetting.CONTACTS_ENABLED, null, account) ||
198 					ac.get(ZmSetting.GAL_ENABLED, null, account);
199 
200 	for (var i in this._pickerButton) {
201 		var button = this._pickerButton[i];
202 		button.setEnabled(isEnabled);
203 	}
204 };
205 
206 ZmRecipients.prototype.setup =
207 function() {
208     // reset To/Cc/Bcc fields
209     if (this._field[AjxEmailAddress.TO]) {
210         this._showAddressField(AjxEmailAddress.TO, true, true, true);
211     }
212     if (this._field[AjxEmailAddress.CC]) {
213         this._showAddressField(AjxEmailAddress.CC, true, true, true);
214     }
215     if (this._field[AjxEmailAddress.BCC]) {
216         this._showAddressField(AjxEmailAddress.BCC, false, true, true);
217     }
218 };
219 
220 ZmRecipients.prototype.getPicker =
221 function(type) {
222     return this._pickerButton[type];
223 };
224 
225 ZmRecipients.prototype.getField =
226 function(type) {
227     return document.getElementById(this._fieldId[type]);
228 };
229 
230 ZmRecipients.prototype.getUsing =
231 function(type) {
232     return this._using[type];
233 };
234 
235 ZmRecipients.prototype.getACAddrSelectList =
236 function() {
237     return this._acAddrSelectList;
238 };
239 
240 ZmRecipients.prototype.getTabGroupMember = function() {
241 	var tg = new DwtTabGroup('ZmRecipients');
242 
243 	for (var i = 0; i < ZmMailMsg.COMPOSE_ADDRS.length; i++) {
244 		var type = ZmMailMsg.COMPOSE_ADDRS[i];
245 		tg.addMember(this.getPicker(type));
246 		tg.addMember(this.getAddrInputField(type).getTabGroupMember());
247 	}
248 
249 	return tg;
250 };
251 
252 ZmRecipients.prototype.getAddrInputField =
253 function(type) {
254     return this._addrInputField[type];
255 };
256 
257 // Adds the given addresses to the form. We need to add each address separately in case it's a DL.
258 ZmRecipients.prototype.addAddresses =
259 function(type, addrVec, used) {
260 
261 	var addrAdded = false;
262 	used = used || {};
263 	var addrList = [];
264 	var addrs = AjxUtil.toArray(addrVec);
265 	if (addrs && addrs.length) {
266 		for (var i = 0, len = addrs.length; i < len; i++) {
267 			var addr = addrs[i];
268 			var email = addr.isAjxEmailAddress ? addr && addr.getAddress() : addr;
269 			if (!email) { continue; }
270 			email = email.toLowerCase();
271 			if (!used[email]) {
272 				this.setAddress(type, addr);	// add the bubble now
273 				used[email] = true;
274 				addrAdded = true;
275 			}
276 		}
277 	}
278 	return addrAdded;
279 };
280 
281 
282 /**
283  * Sets an address field.
284  *
285  * @param type	the address type
286  * @param addr	the address string
287  *
288  * XXX: if addr empty, check if should hide field
289  *
290  * @private
291  */
292 ZmRecipients.prototype.setAddress =
293 function(type, addr) {
294 
295 	addr = addr || "";
296 
297 	var addrStr = addr.isAjxEmailAddress ? addr.toString() : addr;
298 
299 	//show first, so focus works on IE.
300 	if (addrStr.length && !this._using[type]) {
301 		this._using[type] = true;
302 		this._showAddressField(type, true);
303 	}
304 
305 	var addrInput = this._addrInputField[type];
306 	if (!addrStr) {
307 		addrInput.clear();
308 	}
309 	else {
310 		if (addr.isAjxEmailAddress) {
311 			var match = {isDL: addr.isGroup && addr.canExpand, email: addrStr};
312 			addrInput.addBubble({address:addrStr, match:match, skipNotify:true, noFocus:true});
313 		}
314 		else {
315 			this._setAddrFieldValue(type, addrStr);
316 		}
317 	}
318 };
319 
320 
321 /**
322  * Gets the field values for each of the addr fields.
323  *
324  * @return	{Array}	an array of addresses
325  */
326 ZmRecipients.prototype.getRawAddrFields =
327 function() {
328 	var addrs = {};
329 	for (var i = 0; i < this._fieldNames.length; i++) {
330 		var type = this._fieldNames[i];
331 		if (this._using[type]) {
332 			addrs[type] = this.getAddrFieldValue(type);
333 		}
334 	}
335 	return addrs;
336 };
337 
338 // returns address fields that are currently visible
339 ZmRecipients.prototype.getAddrFields =
340 function() {
341 	var addrs = [];
342 	for (var i = 0; i < this._fieldNames.length; i++) {
343 		var type = this._fieldNames[i];
344 		if (this._using[type]) {
345 			addrs.push(this._field[type]);
346 		}
347 	}
348 	return addrs;
349 };
350 
351 
352 // Grab the addresses out of the form. Optionally, they can be returned broken
353 // out into good and bad addresses, with an aggregate list of the bad ones also
354 // returned. If the field is hidden, its contents are ignored.
355 ZmRecipients.prototype.collectAddrs =
356 function() {
357 
358 	var addrs = {};
359 	addrs[ZmRecipients.BAD] = new AjxVector();
360 	for (var i = 0; i < this._fieldNames.length; i++) {
361 		var type = this._fieldNames[i];
362 
363 		if (!this._field[type]) { //this check is in case we don't have all fields set up (might be configurable. Didn't look deeply).
364 			continue;
365 		}
366 
367 		var val = this.getAddrFieldValue(type);
368 		if (val.length == 0) { continue; }
369 		val = val.replace(/[; ,]+$/, "");	// ignore trailing (and possibly extra) separators
370 		var result = AjxEmailAddress.parseEmailString(val, type, false);
371 		if (result.all.size() == 0) { continue; }
372 		addrs.gotAddress = true;
373 		addrs[type] = result;
374 		if (result.bad.size()) {
375 			addrs[ZmRecipients.BAD].addList(result.bad);
376 			if (!addrs.badType) {
377 				addrs.badType = type;
378 			}
379 		}
380 	}
381 	return addrs;
382 };
383 
384 
385 ZmRecipients.prototype.getAddrFieldValue =
386 function(type) {
387 	var addrInput = this._addrInputField[type];
388 	return addrInput ? addrInput.getValue() : '';
389 };
390 
391 ZmRecipients.prototype.enableInputs =
392 function(bEnable) {
393 	// disable input elements so they dont bleed into top zindex'd view
394 	for (var i = 0; i < this._fieldNames.length; i++) {
395 		this._field[this._fieldNames[i]].disabled = !bEnable;
396 	}
397 };
398 
399 // Address buttons invoke contact picker
400 ZmRecipients.prototype.addressButtonListener =
401 function(ev, addrType) {
402 	if (appCtxt.isWebClientOffline()) return;
403 
404 	var obj = ev ? DwtControl.getTargetControl(ev) : null;
405 	if (this._enableContainerInputs) {
406 		this._enableContainerInputs(false);
407 	}
408 
409 	if (!this._contactPicker) {
410 		AjxDispatcher.require("ContactsCore");
411 		var buttonInfo = [];
412         for (var i = 0; i < this._fieldNames.length; i++) {
413             buttonInfo[i] = { id: this._fieldNames[i],
414                               label : ZmMsg[AjxEmailAddress.TYPE_STRING[this._fieldNames[i]]]};
415         }
416 		this._contactPicker = new ZmContactPicker(buttonInfo);
417 		this._contactPicker.registerCallback(DwtDialog.OK_BUTTON, this._contactPickerOkCallback, this);
418 		this._contactPicker.registerCallback(DwtDialog.CANCEL_BUTTON, this._contactPickerCancelCallback, this);
419 	}
420 
421 	var curType = obj ? obj.addrType : addrType;
422 	var addrList = {};
423 	for (var i = 0; i < this._fieldNames.length; i++) {
424 		var type = this._fieldNames[i];
425 		addrList[type] = this._addrInputField[type].getAddresses(true);
426 	}
427 	if (this._contactPopdownListener) {
428 		this._contactPicker.addPopdownListener(this._contactPopdownListener);
429 	}
430 	var str = (this._field[curType].value && !(addrList[curType] && addrList[curType].length))
431 		? this._field[curType].value : "";
432 
433 	var account;
434 	if (appCtxt.multiAccounts && this._fromSelect) {
435 		var addr = this._fromSelect.getSelectedOption().addr;
436 		account = appCtxt.accountList.getAccountByEmail(addr.address);
437 	}
438 	this._contactPicker.popup(curType, addrList, str, account);
439 };
440 
441 
442 
443 
444 // Private methods
445 
446 // Show address field
447 ZmRecipients.prototype._showAddressField =
448 function(type, show, skipNotify, skipFocus) {
449 	this._using[type] = show;
450 	Dwt.setVisible(this._divEl[type], show);
451 	this._setAddrFieldValue(type, "");	 // bug fix #750 and #3680
452 	this._field[type].noTab = !show;
453 	this._addrInputField[type].noTab = !show;
454 	if (this._pickerButton[type]) {
455 		this._pickerButton[type].noTab = !show;
456 	}
457 	if (this._resetContainerSize) {
458 		this._resetContainerSize();
459 	}
460 };
461 
462 ZmRecipients.prototype._acKeyupHandler =
463 function(ev, acListView, result, element) {
464 	var key = DwtKeyEvent.getCharCode(ev);
465 	// process any printable character or enter/backspace/delete keys
466 	if (result && element && (ev.inputLengthChanged ||
467 		(DwtKeyEvent.IS_RETURN[key] || key === DwtKeyEvent.KEY_BACKSPACE || key === DwtKeyEvent.KEY_DELETE ||
468 		(AjxEnv.isMac && key === DwtKeyEvent.KEY_COMMAND)))) // bug fix #24670
469 	{
470 		element.value = element.value && element.value.replace(/;([^\s])/g, function(all, group){return "; "+group}) || ""; // Change ";" to "; " if it is not succeeded by a whitespace
471 	}
472 };
473 
474 /**
475  * a callback that's called when bubbles are added or removed, since we need to resize the msg body in those cases.
476  */
477 ZmRecipients.prototype._bubblesChangedCallback =
478 function() {
479 	if (this._resetContainerSize) {
480 		this._resetContainerSize(); // body size might change due to change in size of address field (due to new bubbles).
481 	}
482 };
483 
484 ZmRecipients.prototype._bubbleMenuCreated =
485 function(addrInput, menu) {
486 
487 	this._bubbleActionMenu = menu;
488     if (this._fieldNames.length > 1) {
489         menu.addOp(ZmOperation.SEP);
490         var listener = new AjxListener(this, this._bubbleMove);
491 
492         for (var i = 0; i < this._fieldNames.length; i++) {
493             var type = this._fieldNames[i];
494             var op = this._bubbleOps[type];
495             menu.addOp(op);
496             menu.addSelectionListener(op, listener);
497         }
498     }
499 };
500 
501 ZmRecipients.prototype._bubbleMenuResetOperations =
502 function(addrInput, menu) {
503 	var sel = addrInput.getSelection();
504     for (var i = 0; i < this._fieldNames.length; i++) {
505         var type = this._fieldNames[i];
506 		var op = this._bubbleOps[type];
507 		menu.enable(op, sel.length > 0 && (type != addrInput.type));
508 	}
509 };
510 
511 ZmRecipients.prototype._bubbleMove =
512 function(ev) {
513 
514 	var sourceInput = ZmAddressInputField.menuContext.addrInput;
515 	var op = ev && ev.item && ev.item.getData(ZmOperation.KEY_ID);
516 	var type = this._opToField[op];
517 	var targetInput = this._addrInputField[type];
518 	if (sourceInput && targetInput) {
519 		var sel = sourceInput.getSelection();
520 		if (sel.length) {
521 			for (var i = 0; i < sel.length; i++) {
522 				var bubble = sel[i];
523 				this._showAddressField(type, true);
524 				targetInput.addBubble({bubble:bubble});
525 				sourceInput.removeBubble(bubble.id);
526 			}
527 		}
528 	}
529 };
530 
531 ZmRecipients.prototype._setAddrFieldValue =
532 function(type, value) {
533 
534 	var addrInput = this._addrInputField[type];
535 	if (addrInput) {
536 		addrInput.setValue(value, true);
537 	}
538 };
539 
540 // Generic routine for attaching an event handler to a field. Since "this" for the handlers is
541 // the incoming event, we need a way to get at ZmComposeView, so it's added to the event target.
542 ZmRecipients.prototype._setEventHandler =
543 function(id, event, addrType) {
544 	var field = document.getElementById(id);
545 	field._recipients = this;
546 	if (addrType) {
547 		field._addrType = addrType;
548 	}
549 	var lcEvent = event.toLowerCase();
550 	field[lcEvent] = ZmRecipients["_" + event];
551 };
552 
553 // set focus within tab group to element so tabbing works
554 ZmRecipients._onFocus =
555 function(ev) {
556 
557 	ev = DwtUiEvent.getEvent(ev);
558 	var element = DwtUiEvent.getTargetWithProp(ev, "id");
559 	if (!element) { return true; }
560 
561 	var kbMgr = appCtxt.getKeyboardMgr();
562 	if (kbMgr.__currTabGroup) {
563 		kbMgr.__currTabGroup.setFocusMember(element);
564 	}
565 };
566 
567 // Transfers addresses from the contact picker to the compose view.
568 ZmRecipients.prototype._contactPickerOkCallback =
569 function(addrs) {
570 
571 	if (this._enableContainerInputs) {
572 		this._enableContainerInputs(true);
573 	}
574 	for (var i = 0; i < this._fieldNames.length; i++) {
575 		var type = this._fieldNames[i];
576 		this.setAddress(type, "");
577         // If there was only one button, the picker will just return the list of selections,
578         // not a list per button type
579         var typeAddrs = (this._fieldNames.length == 1) ? addrs :  addrs[type];
580 		var addrVec = ZmRecipients.expandAddrs(typeAddrs);
581 		this.addAddresses(type, addrVec);
582 	}
583 
584 	// Still need this here since REMOVING stuff with the picker does not call removeBubble in the ZmAddressInputField.
585 	// Also - it's better to do it once than for every bubble in this case. user might add many addresses with the picker
586 	this._bubblesChangedCallback();
587 
588 	if (this._contactPopdownListener) {
589 		this._contactPicker.removePopdownListener(this._contactPopdownListener);
590 	}
591 	this._contactPicker.popdown();
592 	if (this._reenter) {
593 		this._reenter();
594 	}
595 };
596 
597 // Expands any addresses that are groups
598 ZmRecipients.expandAddrs =
599 function(addrs) {
600 	var addrsNew = [];
601 	var addrsArray = (addrs instanceof AjxVector) ? addrs.getArray() : addrs;
602 	if (addrsArray && addrsArray.length) {
603 		for (var i = 0; i < addrsArray.length; i++) {
604 			var addr = addrsArray[i];
605 			if (addr) {
606 				if (addr.isGroup && !(addr.__contact && addr.__contact.isDL)) {
607 					var members = addr.__contact ? addr.__contact.getGroupMembers().good.getArray() :
608 												   AjxEmailAddress.split(addr.address);
609 					addrsNew = addrsNew.concat(members);
610 				}
611 				else {
612 					addrsNew.push(addr);
613 				}
614 			}
615 		}
616 	}
617 	return AjxVector.fromArray(addrsNew);
618 };
619 
620 ZmRecipients.prototype._contactPickerCancelCallback =
621 function() {
622 	if (this._enableContainerInputs) {
623 		this._enableContainerInputs(true);
624 	}
625 	if (this._reenter) {
626 		this._reenter();
627 	}
628 };
629 
630 ZmRecipients.prototype._toggleBccField =
631 function(show) {
632 	var visible = AjxUtil.isBoolean(show) ? show : !Dwt.getVisible(this._divEl[AjxEmailAddress.BCC]);
633 	this._showAddressField(AjxEmailAddress.BCC, visible);
634 };
635