1 /*
  2  * ***** BEGIN LICENSE BLOCK *****
  3  * Zimbra Collaboration Suite Web Client
  4  * Copyright (C) 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) 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016 Synacor, Inc. All Rights Reserved.
 21  * ***** END LICENSE BLOCK *****
 22  */
 23 
 24 ZmSignaturesPage = function(parent, section, controller) {
 25 
 26 	ZmPreferencesPage.call(this, parent, section, controller);
 27 
 28 	this._minEntries = appCtxt.get(ZmSetting.SIGNATURES_MIN);	// added for Comcast
 29 	this._maxEntries = appCtxt.get(ZmSetting.SIGNATURES_MAX);
 30 
 31 	this.addControlListener(this._resetSize.bind(this));
 32 };
 33 
 34 ZmSignaturesPage.prototype = new ZmPreferencesPage;
 35 ZmSignaturesPage.prototype.constructor = ZmSignaturesPage;
 36 
 37 ZmSignaturesPage.prototype.toString = function() {
 38 	return "ZmSignaturesPage";
 39 };
 40 
 41 //
 42 // Constants
 43 //
 44 
 45 ZmSignaturesPage.SIGNATURE_TEMPLATE = "prefs.Pages#SignatureSplitView";
 46 
 47 ZmSignaturesPage.SIG_TYPES = [ZmIdentity.SIGNATURE, ZmIdentity.REPLY_SIGNATURE];
 48 
 49 //
 50 // Public methods
 51 //
 52 
 53 ZmSignaturesPage.prototype.showMe =
 54 function() {
 55 
 56 	ZmPreferencesPage.prototype.showMe.call(this);
 57 
 58 	// bug #41719 & #94845 - always update size on display
 59 	this._resetSize();
 60 
 61 	if (!this._firstTime) {
 62 		this._firstTime = true;
 63 	}
 64 
 65 	// bug fix #31849 - reset the signature html editor when in multi-account mode
 66 	// since the view gets re-rendered whenever the account changes
 67 	if (appCtxt.multiAccounts) {
 68 		this._signatureEditor = null;
 69 	}
 70 };
 71 
 72 ZmSignaturesPage.prototype._rehashByName =
 73 function() {
 74 	this._byName = {};
 75 	for (var id in this._signatures) {
 76 		var signature = this._signatures[id];
 77         if (!signature._new) {         // avoid messing with existing signatures with same names
 78 		    this._byName[signature.name] = signature;
 79         }
 80 	}
 81 };
 82 
 83 ZmSignaturesPage.prototype.getNewSignatures =
 84 function(onlyValid) {
 85 	var list = [];
 86 	this._rehashByName();
 87 	for (var id in this._signatures) {
 88 		var signature = this._signatures[id],
 89 			isEmpty = signature._autoAdded && !(AjxStringUtil._NON_WHITESPACE.test(signature.getValue()) || AjxStringUtil._NON_WHITESPACE.test(signature.name));
 90 		if (signature._new && !isEmpty && !(onlyValid && this._isInvalidSig(signature, true))) {
 91 			list.push(signature);
 92 		}
 93 	}
 94 	return list;
 95 };
 96 
 97 ZmSignaturesPage.prototype.getDeletedSignatures =
 98 function() {
 99 	return AjxUtil.values(this._deletedSignatures);
100 };
101 
102 ZmSignaturesPage.prototype.getModifiedSignatures =
103 function() {
104 
105 	var array = [];
106 	for (var id in this._signatures) {
107 		var signature = this._signatures[id];
108 		if (signature._new) {
109 			continue;
110 		}
111 
112 		if (this._hasChanged(signature)) {
113 			array.push(signature);
114 		}
115 	}
116 	return array;
117 };
118 
119 ZmSignaturesPage.SIG_FIELDS = [ZmIdentity.SIGNATURE, ZmIdentity.REPLY_SIGNATURE];
120 
121 // returns a hash representing the current usage, based on form selects
122 ZmSignaturesPage.prototype._getUsage =
123 function(newOnly) {
124 
125 	var usage = {};
126 	var foundOne;
127 	for (var identityId in this._sigSelect) {
128 		usage[identityId] = {};
129 		for (var j = 0; j < ZmSignaturesPage.SIG_FIELDS.length; j++) {
130 			var field = ZmSignaturesPage.SIG_FIELDS[j];
131 			var select = this._sigSelect[identityId] && this._sigSelect[identityId][field];
132 			if (select) {
133 				var sigId = select.getValue();
134 				if (newOnly && this._newSigId[sigId]) {
135 					return true;
136 				}
137 				else {
138 					usage[identityId][field] = sigId;
139 				}
140 			}
141 		}
142 	}
143 	return newOnly ? false : usage;
144 };
145 
146 // returns a hash representing the current usage, based on identity data
147 ZmSignaturesPage.prototype._getUsageFromIdentities =
148 function() {
149 
150 	var usage = {};
151 	var collection = appCtxt.getIdentityCollection();
152 	var identities = collection && collection.getIdentities();
153 	for (var i = 0, len = identities.length; i < len; i++) {
154 		var identity = identities[i];
155 		usage[identity.id] = {};
156 		for (var j = 0; j < ZmSignaturesPage.SIG_FIELDS.length; j++) {
157 			var field = ZmSignaturesPage.SIG_FIELDS[j];
158 			usage[identity.id][field] = identity.getField(field) || "";
159 		}
160 	}
161 	return usage;
162 };
163 
164 /**
165  * Returns a list of usage changes. Each item in the list details the identity,
166  * which type of signature, and the ID of the new signature.
167  */
168 ZmSignaturesPage.prototype.getChangedUsage =
169 function() {
170 
171 	var list = [];
172 	var usage = this._getUsage();
173 	for (var identityId in usage) {
174 		var u1 = this._origUsage[identityId];
175 		var u2 = usage[identityId];
176         if (u1 && u2){
177 		    for (var j = 0; j < ZmSignaturesPage.SIG_FIELDS.length; j++) {
178 			    var field = ZmSignaturesPage.SIG_FIELDS[j];
179 			    var savedSigId = (u1[field]) || ((field === ZmIdentity.REPLY_SIGNATURE) ? ZmIdentity.SIG_ID_NONE : u1[field]);
180 			    var curSigId = this._newSigId[u2[field]] || u2[field];
181 			    if (savedSigId !== curSigId) {
182 				    list.push({identity:identityId, sig:field, value:curSigId});
183 		        }
184 		    }
185         }
186 	}
187 	return list;
188 };
189 
190 ZmSignaturesPage.prototype.reset =
191 function(useDefaults) {
192 	this._updateSignature();
193 	ZmPreferencesPage.prototype.reset.apply(this, arguments);
194 	this._populateSignatures(true);
195 };
196 
197 ZmSignaturesPage.prototype.resetOnAccountChange =
198 function() {
199 	ZmPreferencesPage.prototype.resetOnAccountChange.apply(this, arguments);
200 	this._selSignature = null;
201 	this._firstTime = false;
202 };
203 
204 ZmSignaturesPage.prototype.isDirty =
205 function() {
206 
207 	this._updateSignature();
208 
209 	var printSigs = function(sig) {
210 		if (AjxUtil.isArray(sig)) {
211 			return AjxUtil.map(sig, printSigs).join("\n");
212 		}
213 		return [sig.name, " (", ((sig._orig && sig._orig.value !== sig.value) ? (sig._orig.value+" changed to ") : ""), sig.value, ")"].join("");
214 	}
215 
216 	var printUsages = function(usage) {
217 		if (AjxUtil.isArray(usage)) {
218 			return AjxUtil.map(usage, printUsages).join("\n");
219 		}
220 		return ["identityId: ", usage.identity, ", type: ", usage.sig, ", signatureId: ", usage.value].join("");
221 	}
222 
223 	if (this.getNewSignatures(false).length > 0) {
224 		AjxDebug.println(AjxDebug.PREFS, "Dirty preferences:\nNew signatures:\n" + printSigs(this.getNewSignatures(false)));
225 		return true;
226 	}
227 	if (this.getDeletedSignatures().length > 0) {
228 		AjxDebug.println(AjxDebug.PREFS, "Dirty preferences:\nDeleted signatures:\n" + printSigs(this.getDeletedSignatures()));
229 		return true;
230 	}
231 	if (this.getModifiedSignatures().length > 0) {
232 		AjxDebug.println(AjxDebug.PREFS, "Dirty preferences:\nModified signatures:\n" + printSigs(this.getModifiedSignatures()));
233 		return true;
234 	}
235 	if (this.getChangedUsage().length > 0) {
236 		AjxDebug.println(AjxDebug.PREFS, "Dirty preferences:\nSignature usage changed:\n" + printUsages(this.getChangedUsage()));
237 		return true;
238 	}
239 };
240 
241 ZmSignaturesPage.prototype.validate =
242 function() {
243 	this._updateSignature();
244 	this._rehashByName();
245 
246 	for (var id in this._signatures) {
247 		var error = this._isInvalidSig(this._signatures[id]);
248 		if (error) {
249 			this._errorMsg = error;
250 			return false;
251 		}
252 	}
253 	return true;
254 };
255 
256 // The 'strict' parameter will make the function return true if a signature is not
257 // saveable, even if it can be safely ignored. Without it, the function returns an error
258 // only if there's bad user input.
259 ZmSignaturesPage.prototype._isInvalidSig =
260 function(signature, strict) {
261 
262 	var hasName = AjxStringUtil._NON_WHITESPACE.test(signature.name);
263 	var hasContact = Boolean(signature.contactId);
264 	var hasValue = AjxStringUtil._NON_WHITESPACE.test(signature.getValue()) || hasContact;
265 	if (!hasName && !hasValue) {
266 		this._deleteSignature(signature);
267 		if (strict) {
268 			return true;
269 		}
270 	}
271 	else if (!hasName || !hasValue) {
272 		return !hasName ? ZmMsg.signatureNameMissingRequired : ZmMsg.signatureValueMissingRequired;
273 	}
274 	else if (strict && !hasValue) {
275 		return true;
276 	}
277 	if (hasName && this._byName[signature.name]) {
278         // If its a new signature with a in-use name or a existing signature whose name the user is trying to edit to a existing name value
279 		if (signature._new || (this._byName[signature.name].id !== signature.id)) {
280             return AjxMessageFormat.format(ZmMsg.signatureNameDuplicate, AjxStringUtil.htmlEncode(signature.name));
281 		}
282 	}
283 	var sigValue = signature.value;
284 	var maxLength = appCtxt.get(ZmSetting.SIGNATURE_MAX_LENGTH);
285 	if (maxLength > 0 && sigValue.length > maxLength) {
286 		return AjxMessageFormat.format((signature.contentType === ZmMimeTable.TEXT_HTML)
287 			? ZmMsg.errorHtmlSignatureTooLong
288 			: ZmMsg.errorSignatureTooLong, maxLength);
289 	}
290 
291 	return false;
292 };
293 
294 ZmSignaturesPage.prototype.getErrorMessage =
295 function() {
296 	return this._errorMsg;
297 };
298 
299 ZmSignaturesPage.prototype.addCommand =
300 function(batchCommand) {
301 
302 	// delete signatures
303 	var deletedSigs = this.getDeletedSignatures();
304 	for (var i = 0; i < deletedSigs.length; i++) {
305 		var signature = deletedSigs[i];
306 		var callback = this._handleDeleteResponse.bind(this, signature);
307 		signature.doDelete(callback, null, batchCommand);
308 	}
309 
310 	// modify signatures
311 	var modifiedSigs = this.getModifiedSignatures();
312 	for (var i = 0; i < modifiedSigs.length; i++) {
313 		var signature = modifiedSigs[i];
314 		var comps = this._signatures[signature._htmlElId];
315 		var callback = this._handleModifyResponse.bind(this, signature);
316 		var errorCallback = this._handleModifyError.bind(this, signature);
317 		signature.save(callback, errorCallback, batchCommand);
318 	}
319 
320 	// add signatures
321 	var newSigs = this.getNewSignatures(true);
322 	for (var i = 0; i < newSigs.length; i++) {
323 		var signature = newSigs[i];
324 		signature._id = signature.id; // Clearing existing dummy id
325 		signature.id = null;
326 		var callback = this._handleNewResponse.bind(this, signature);
327 		signature.create(callback, null, batchCommand);
328 	}
329 
330 	// signature usage
331 	var sigChanges = this.getChangedUsage();
332 	if (sigChanges.length) {
333 		var collection = appCtxt.getIdentityCollection();
334 		if (collection) {
335 			for (var i = 0; i < sigChanges.length; i++) {
336 				var usage = sigChanges[i];
337 				var identity = collection.getById(usage.identity);
338 				// don't save usage of new signature just yet
339 				if (identity && !this._isTempId[usage.value]) {
340 					identity.setField(usage.sig, usage.value);
341 					identity.save(null, null, batchCommand);
342 				}
343 			}
344 		}
345 	}
346 };
347 
348 ZmSignaturesPage.prototype.getPostSaveCallback =
349 function() {
350 	return this._postSave.bind(this);
351 };
352 
353 // if a new sig has been assigned to an identity, we need to save again since
354 // we only now know the sig's ID
355 ZmSignaturesPage.prototype._postSave =
356 function() {
357 
358 	var newUsage = this._getUsage(true);
359 	if (newUsage) {
360 		var respCallback = this._handleResponsePostSave.bind(this);
361 		this._controller.save(respCallback, true);
362 	}
363 };
364 
365 ZmSignaturesPage.prototype._handleResponsePostSave =
366 function() {
367 
368 	this._newSigId = {};	// clear this to prevent request loop
369 	this._resetOperations();
370 	this._origUsage = this._getUsage();	// form selects and data in identities should be in sync now
371 };
372 
373 ZmSignaturesPage.prototype.setContact =
374 function(contact) {
375 	if (this._selSignature) {
376 		this._selSignature.contactId = contact.id;
377 	}
378 	this._vcardField.value = contact.getFileAs() || contact.getFileAsNoName();
379 };
380 
381 //
382 // Protected methods
383 //
384 
385 ZmSignaturesPage.prototype._initialize =
386 function(container) {
387 
388 	container.getHtmlElement().innerHTML = AjxTemplate.expand(ZmSignaturesPage.SIGNATURE_TEMPLATE, {id:this._htmlElId});
389 
390 	// Signature list
391 	var listEl = document.getElementById(this._htmlElId + "_SIG_LIST");
392 	var list = new ZmSignatureListView(this);
393 	this._replaceControlElement(listEl, list);
394 	list.setMultiSelect(false);
395 	list.addSelectionListener(this._selectionListener.bind(this));
396 	list.setUI(null, true); // renders headers and empty list
397 	this._sigList = list;
398 
399 	// Signature ADD
400 	var addEl = document.getElementById(this._htmlElId + "_SIG_NEW");
401 	var button = new DwtButton(this);
402 	button.setText(ZmMsg.newSignature);
403 	button.addSelectionListener(this._handleAddButton.bind(this));
404 	this._replaceControlElement(addEl, button);
405 	this._sigAddBtn = button;
406 
407 	// Signature DELETE
408 	var deleteEl = document.getElementById(this._htmlElId + "_SIG_DELETE");
409 	var button = new DwtButton(this);
410 	button.setText(ZmMsg.del);
411 	button.addSelectionListener(this._handleDeleteButton.bind(this));
412 	this._replaceControlElement(deleteEl, button);
413 	this._deleteBtn = button;
414 
415 	// vCard INPUT
416 	this._vcardField = document.getElementById(this._htmlElId + "_SIG_VCARD");
417 
418 	// vCard BROWSE
419 	var el = document.getElementById(this._htmlElId + "_SIG_VCARD_BROWSE");
420 	var button = new DwtButton(this);
421 	button.setText(ZmMsg.browse);
422 	button.addSelectionListener(this._handleVcardBrowseButton.bind(this));
423 	this._replaceControlElement(el, button);
424 	this._vcardBrowseBtn = button;
425 
426 	// vCard CLEAR
427 	var el = document.getElementById(this._htmlElId + "_SIG_VCARD_CLEAR");
428 	var button = new DwtButton(this);
429 	button.setText(ZmMsg.clear);
430 	button.addSelectionListener(this._handleVcardClearButton.bind(this));
431 	this._replaceControlElement(el, button);
432 	this._vcardClearBtn = button;
433 
434 	// Signature Name
435 	var nameEl = document.getElementById(this._htmlElId + "_SIG_NAME");
436 	var params = {
437 		parent:             this,
438 		type:               DwtInputField.STRING,
439 		required:           false,
440 		validationStyle:    DwtInputField.CONTINUAL_VALIDATION,
441 	};
442 	var input = this._sigName = new DwtInputField(params);
443 	input.setValidationCallback(this._updateName.bind(this));
444 	this._replaceControlElement(nameEl, input);
445 
446 	// Signature FORMAT
447 	var formatEl = document.getElementById(this._htmlElId + "_SIG_FORMAT");
448 	if (formatEl && appCtxt.get(ZmSetting.HTML_COMPOSE_ENABLED)) {
449 		var select = new DwtSelect(this);
450 		select.setToolTipContent(ZmMsg.formatTooltip);
451 		select.addOption(ZmMsg.formatAsText, 1 , true);
452 		select.addOption(ZmMsg.formatAsHtml, 0, false);
453 		select.addChangeListener(this._handleFormatSelect.bind(this));
454 		this._replaceControlElement(formatEl, select);
455 		this._sigFormat = select;
456 	}
457 
458 	// Signature CONTENT - editor added by ZmPref.regenerateSignatureEditor
459 
460 	// Signature use by identity
461 	var collection = appCtxt.getIdentityCollection();
462 	if (collection) {
463 		collection.addChangeListener(this._identityChangeListener.bind(this));
464 	}
465 
466 	this._initialized = true;
467 };
468 
469 // generate usage selects based on identity data
470 ZmSignaturesPage.prototype._resetUsageSelects =
471 function(addSigs) {
472 
473 	this._clearUsageSelects();
474 
475 	var table = document.getElementById(this._htmlElId + "_SIG_TABLE");
476 	this._sigSelect = {};
477 	var signatures;
478 	if (addSigs) {
479 		signatures = appCtxt.getSignatureCollection().getSignatures(true);
480 	}
481 	var ic = appCtxt.getIdentityCollection();
482 	var identities = ic && ic.getIdentities(true);
483 	if (identities && identities.length) {
484 		for (var i = 0, len = identities.length; i < len; i++) {
485 			this._addUsageSelects(identities[i], table, signatures);
486 		}
487 	}
488 };
489 
490 ZmSignaturesPage.prototype._clearUsageSelects =
491 function() {
492 
493 	var table = document.getElementById(this._htmlElId + "_SIG_TABLE");
494 	while (table.rows.length > 1) {
495 		table.deleteRow(-1);
496 	}
497 	for (var id in this._sigSelect) {
498 		for (var field in this._sigSelect[id]) {
499 			var select = this._sigSelect[id][field];
500 			if (select) {
501 				select.dispose();
502 			}
503 		}
504 	}
505 };
506 
507 ZmSignaturesPage.prototype._addUsageSelects =
508 function(identity, table, signatures, index) {
509 
510 	table = table || document.getElementById(this._htmlElId + "_SIG_TABLE");
511 	index = (index != null) ? index : -1;
512 	var row = table.insertRow(index);
513 	row.id = identity.id + "_row";
514 	var name = identity.getField(ZmIdentity.NAME);
515 	if (name === ZmIdentity.DEFAULT_NAME) {
516 		name = ZmMsg.accountDefault;
517 	}
518 	var cell = row.insertCell(-1);
519 	cell.className = "ZOptionsLabel";
520 	var id = identity.id + "_name";
521 	cell.innerHTML = "<span id='" + id + "'>" + AjxStringUtil.htmlEncode(name) + ":</span>";
522 
523 	this._sigSelect[identity.id] = {};
524 	for (var i = 0; i < ZmSignaturesPage.SIG_FIELDS.length; i++) {
525 		this._addUsageSelect(row, identity, signatures, ZmSignaturesPage.SIG_FIELDS[i]);
526 	}
527 };
528 
529 ZmSignaturesPage.prototype._addUsageSelect =
530 function(row, identity, signatures, sigType) {
531 
532 	var select = this._sigSelect[identity.id][sigType] = new DwtSelect(this);
533 	var curSigId = identity.getField(sigType);
534 	var noSigId = (sigType === ZmIdentity.REPLY_SIGNATURE) ? ZmIdentity.SIG_ID_NONE : "";
535 	this._addUsageSelectOption(select, {name:ZmMsg.noSignature, id:noSigId}, sigType, identity);
536 	if (signatures) {
537 		for (var i = 0, len = signatures.length; i < len; i++) {
538 			this._addUsageSelectOption(select, signatures[i], sigType, identity);
539 		}
540 	}
541 	var cell = row.insertCell(-1);
542 	select.reparentHtmlElement(cell);
543 };
544 
545 // Bug 86217, Don't apply any default to the Reply Signature if it is not set.
546 ZmSignaturesPage.prototype._addUsageSelectOption =
547 function(select, signature, sigType, identity) {
548 
549 	var curSigId = identity.getField(sigType);
550 	DBG.println(AjxDebug.DBG3, "Adding " + sigType + " option for " + identity.name + ": " + signature.name + " / " + signature.id + " (" + (curSigId === signature.id) + ")");
551 	// a new signature starts with an empty name; use a space so that option gets added
552 	select.addOption(signature.name || ' ', (curSigId === signature.id), signature.id);
553 };
554 
555 // handles addition, removal, or rename of a signature within the form
556 ZmSignaturesPage.prototype._updateUsageSelects =
557 function(signature, action) {
558 
559 	if (!this._initialized) {
560 		return;
561 	}
562 
563 	for (var id in this._sigSelect) {
564 		for (var sigType in this._sigSelect[id]) {
565 			var select = this._sigSelect[id][sigType];
566 			if (select) {
567 				var hasOption = !!(select.getOptionWithValue(signature.id));
568 				var collection = appCtxt.getIdentityCollection();
569 				var identity = collection && collection.getById(id);
570 				if (action === ZmEvent.E_CREATE && !hasOption && identity) {
571 					this._addUsageSelectOption(select, signature, sigType, identity);
572 				}
573 				else if (action === ZmEvent.E_DELETE && hasOption && identity) {
574 					select.removeOptionWithValue(signature.id);
575 					var curSigId = identity.getField(sigType);
576 					if (curSigId === signature.id) {
577 						var noSigId = (sigType === ZmIdentity.REPLY_SIGNATURE) ? ZmIdentity.SIG_ID_NONE : "";
578 						select.setSelectedValue(noSigId);
579 					}
580 				}
581 				else if (action === ZmEvent.E_MODIFY && hasOption) {
582 					select.rename(signature.id, signature.name);
583 				}
584 			}
585 		}
586 	}
587 };
588 
589 ZmSignaturesPage.prototype._identityChangeListener =
590 function(ev) {
591 
592 	var identity = ev.getDetail("item");
593 	if (!identity) {
594 		return;
595 	}
596 	
597 	var collection = appCtxt.getIdentityCollection();
598 	var id = identity.id;
599 	var signatures = appCtxt.getSignatureCollection().getSignatures(true);
600 	var table = document.getElementById(this._htmlElId + "_SIG_TABLE");
601 	if (ev.event === ZmEvent.E_CREATE) {
602 		var index = collection.getSortIndex(identity);
603 		this._addUsageSelects(identity, table, signatures, index);
604 		for (var i = 0; i < ZmSignaturesPage.SIG_FIELDS.length; i++) {
605 			var field = ZmSignaturesPage.SIG_FIELDS[i];
606 			var select = this._sigSelect[id] && this._sigSelect[id][field];
607 			if (select) {
608 				this._origUsage[id] = this._origUsage[id] || {};
609 				this._origUsage[id][field] = select.getValue();
610 			}
611 		}
612 	}
613 	else if (ev.event === ZmEvent.E_DELETE) {
614 		var row = document.getElementById(id + "_row");
615 		if (row) {
616 			table.deleteRow(row.rowIndex);
617 		}
618 		delete this._origUsage[id];
619 	}
620 	else if (ev.event === ZmEvent.E_MODIFY) {
621 		var row = document.getElementById(id + "_row");
622 		if (row) {
623 			var index = collection.getSortIndex(identity);
624 			if (index === row.rowIndex - 1) {	// header row doesn't count
625 				var span = document.getElementById(id + "_name");
626 				if (span) {
627 					span.innerHTML = identity.name + ":";
628 				}
629 			}
630 			else {
631 				table.deleteRow(row.rowIndex);
632 				this._addUsageSelects(identity, table, signatures, index);
633 			}
634 		}
635 	}
636 };
637 
638 // Sets the height of the editor and the list
639 ZmSignaturesPage.prototype._resetSize =
640 function() {
641 	if (!this._sigEditor || !this._sigList) {
642 		return;
643 	}
644 
645 	// resize editor and list to fit appropriately -- in order to get this
646 	// right, we size them to a minimum, and then apply the sizes of their
647 	// containing table cells
648 	AjxUtil.foreach([this._sigEditor, this._sigList], function(ctrl) {
649 		ctrl.setSize(0, 0);
650 
651 		var bounds = Dwt.getInsetBounds(ctrl.getHtmlElement().parentNode);
652 		ctrl.setSize(bounds.width, bounds.height);
653 	});
654 };
655 
656 ZmSignaturesPage.prototype._setupCustom =
657 function(id, setup, value) {
658 	if (id === ZmSetting.SIGNATURES) {
659 		// create container control
660 		var container = new DwtComposite(this);
661 		this.setFormObject(id, container);
662 
663 		// create radio group for defaults
664 		this._defaultRadioGroup = new DwtRadioButtonGroup();
665 
666 		this._initialize(container);
667 
668 		return container;
669 	}
670 
671 	return ZmPreferencesPage.prototype._setupCustom.apply(this, arguments);
672 };
673 
674 ZmSignaturesPage.prototype._selectionListener =
675 function(ev) {
676 
677 	this._updateSignature();
678 
679 	var signature = this._sigList.getSelection()[0];
680 	if (signature) {
681 		this._resetSignature(this._signatures[signature.id]);
682 	}
683 	this._resetOperations();
684 };
685 
686 ZmSignaturesPage.prototype._insertImagesListener =
687 function(ev) {
688 	AjxDispatcher.require("BriefcaseCore");
689 	appCtxt.getApp(ZmApp.BRIEFCASE)._createDeferredFolders();
690 	var callback = this._sigEditor._imageUploaded.bind(this._sigEditor);
691 	var cFolder = appCtxt.getById(ZmOrganizer.ID_BRIEFCASE);
692 	var dialog = appCtxt.getUploadDialog();
693 	dialog.popup(null, cFolder, callback, ZmMsg.uploadImage, null, true);
694 };
695 
696 // Updates name and format of selected sig based on form fields
697 ZmSignaturesPage.prototype._updateSignature =
698 function(select) {
699 
700 	if (!this._selSignature) {
701 		return;
702 	}
703 
704 	var sig = this._selSignature;
705 	var newName = AjxStringUtil.trim(this._sigName.getValue());
706 	var isNameModified = (newName !== sig.name);
707 
708 	sig.name = newName;
709 
710 	var isText = this._sigFormat ? this._sigFormat.getValue() : true;
711 	sig.setContentType(isText ? ZmMimeTable.TEXT_PLAIN : ZmMimeTable.TEXT_HTML);
712 
713 	sig.value = this._sigEditor.getContent(false, true);
714 
715 	if (isNameModified) {
716 		this._sigList.redrawItem(sig);
717 		this._updateUsageSelects(sig, ZmEvent.E_MODIFY);
718 	}
719 };
720 
721 ZmSignaturesPage.prototype._populateSignatures =
722 function(reset) {
723 
724 	this._signatures = {};
725 	this._deletedSignatures = {};
726 	this._origUsage = {};
727 	this._isTempId = {};
728 	this._newSigId = {};
729 	this._byName = {};
730 
731 	this._selSignature = null;
732 	this._sigList.removeAll(true);
733 	this._sigList._resetList();
734 	this._resetUsageSelects();	// signature options will be added via _addSignature
735 
736 	var signatures = appCtxt.getSignatureCollection().getSignatures(true);
737 	var count = Math.min(signatures.length, this._maxEntries);
738 	for (var i = 0; i < count; i++) {
739 		var signature = signatures[i];
740 		this._addSignature(signature, true, reset);
741 	}
742 	for (var i = count; i < this._minEntries; i++) {
743 		this._addNewSignature(true, true);  // autoAdded
744 	}
745 
746 	var selectSig = this._sigList.getList().get(0);
747 	this._sigList.setSelection(selectSig);
748 
749 	this._origUsage = this._getUsageFromIdentities();
750 };
751 
752 ZmSignaturesPage.prototype._getNewSignature =
753 function() {
754 	var signature = new ZmSignature(null);
755 	signature.id = Dwt.getNextId();
756 	signature.name = '';
757 	signature._new = true;
758 	this._isTempId[signature.id] = true;
759 	return signature;
760 };
761 
762 ZmSignaturesPage.prototype._addNewSignature =
763 function(skipControls, autoAdded) {
764 	// add new signature
765 	var signature = this._getNewSignature();
766     var sigEditor = this._sigEditor;
767     if (sigEditor) {
768         sigEditor.setContent('');
769     }
770     signature._autoAdded = autoAdded;
771 	signature = this._addSignature(signature, skipControls);
772 	setTimeout(this._sigName.focus.bind(this._sigName), 100);
773 
774 	return signature;
775 };
776 
777 ZmSignaturesPage.prototype._addSignature =
778 function(signature, skipControls, reset, index, skipNotify) {
779 
780 	if (!signature._new) {
781 		if (reset) {
782 			this._restoreFromOrig(signature);
783 		} else if (!signature._orig) {
784 			this._setOrig(signature);
785 		}
786 	}
787 
788 	this._signatures[signature.id] = signature;
789 
790 	if (this._sigList.getItemIndex(signature) === null) {
791 		this._sigList.addItem(signature, index);
792 		if (!skipNotify) {
793 			this._updateUsageSelects(signature, ZmEvent.E_CREATE);
794 		}
795 	}
796 
797 	if (!skipControls) {
798 		this._resetSignature(signature); // initialize state
799 	}
800 
801 	this._resetOperations();
802 	if (signature.name) {
803 		this._byName[signature.name] = signature;
804 	}
805 
806 	return signature;
807 };
808 
809 ZmSignaturesPage.prototype._fixSignatureInlineImages_onTimer =
810 function(msg) {
811 	// first time the editor is initialized, idoc.getElementsByTagName("img") is empty
812 	// Instead of waiting for 500ms, trying to add this callback. Risky but works.
813 	if (!this._firstTimeFixImages) {
814 		this._sigEditor.addOnContentInitializedListener(this._fixSignatureInlineImages.bind(this));
815 	}
816 	else {
817 		this._fixSignatureInlineImages();
818 	}
819 };
820 
821 ZmSignaturesPage.prototype._fixSignatureInlineImages =
822 function() {
823 	var idoc = this._sigEditor.getIframeDoc();
824 	if (idoc) {
825 		if (!this._firstTimeFixImages) {
826 			this._firstTimeFixImages = true;
827 			this._sigEditor.clearOnContentInitializedListeners();
828 		}
829 
830 		var images = idoc.getElementsByTagName("img");
831 		var path = appCtxt.get(ZmSetting.REST_URL) + ZmFolder.SEP;
832 		var img;
833 		for (var i = 0; i < images.length; i++) {
834 			img = images[i];
835 			var dfsrc = img.getAttribute("dfsrc");
836 			if (dfsrc && dfsrc.indexOf("doc:") === 0) {
837 				var url = [path, dfsrc.substring(4)].join('');
838 				img.src = AjxStringUtil.fixCrossDomainReference(url, false, true);
839 			}
840 		}
841 	}
842 };
843 
844 ZmSignaturesPage.prototype._restoreSignatureInlineImages =
845 function() {
846 	var idoc = this._sigEditor.getIframeDoc();
847 	if (idoc) {
848 		var images = idoc.getElementsByTagName("img");
849 		var img;
850 		for (var i = 0; i < images.length; i++) {
851 			img = images[i];
852 			var dfsrc = img.getAttribute("dfsrc");
853 			if (dfsrc && dfsrc.substring(0, 4) === "doc:") {
854 				img.removeAttribute("src");
855 			}
856 		}
857 	}
858 };
859 
860 ZmSignaturesPage.prototype._resetSignature =
861 function(signature, clear) {
862 	this._selSignature = signature;
863 	if (!signature) {
864 		return;
865 	}
866 
867 	this._sigList.setSelection(signature, true);
868 	this._sigName.setValue(signature.name, true);
869 	if (this._sigFormat) {
870 		this._sigFormat.setSelectedValue(signature.getContentType() === ZmMimeTable.TEXT_PLAIN);
871 	}
872 	var vcardName = "";
873 	if (signature.contactId) {
874 		var contactsApp = appCtxt.getApp(ZmApp.CONTACTS);
875 		var contact = contactsApp && contactsApp.getContactList().getById(signature.contactId);
876 		if (contact) {
877 			vcardName = contact.getFileAs() || contact.getFileAsNoName();
878 		}
879 	}
880 	this._vcardField.value = vcardName;
881 
882     this._sigEditor.clear();
883 	var editorMode = (appCtxt.get(ZmSetting.HTML_COMPOSE_ENABLED) && signature.getContentType() === ZmMimeTable.TEXT_HTML)
884 		? Dwt.HTML : Dwt.TEXT;
885 	var htmlModeInited = this._sigEditor.isHtmlModeInited();
886 	if (editorMode !== this._sigEditor.getMode()) {
887 		this._sigEditor.setMode(editorMode);
888 		this._resetSize();
889 	}
890 	this._sigEditor.setContent(signature.getValue(editorMode === Dwt.HTML ? ZmMimeTable.TEXT_HTML : ZmMimeTable.TEXT_PLAIN));
891 	if (editorMode === Dwt.HTML) {
892 		this._fixSignatureInlineImages_onTimer();
893 	}
894 };
895 
896 ZmSignaturesPage.prototype._resetOperations =
897 function() {
898 	if (this._sigAddBtn) {
899 		var hasEmptyNewSig = false;
900 		for (var id in this._signatures) {
901 			var signature = this._signatures[id];
902 			if (signature._new && !signature.name) {
903 				hasEmptyNewSig = true;
904 				break;
905 			}
906 		}
907 		this._sigAddBtn.setEnabled(!hasEmptyNewSig && this._sigList.size() < this._maxEntries);
908 	}
909 };
910 
911 ZmSignaturesPage.prototype._setFormat =
912 function(isText) {
913 	this._sigEditor.setMode(isText ? Dwt.TEXT : Dwt.HTML, true);
914 	this._selSignature.setContentType(isText ? ZmMimeTable.TEXT_PLAIN : ZmMimeTable.TEXT_HTML);
915 	this._resetSize();
916 };
917 
918 ZmSignaturesPage.prototype._formatOkCallback =
919 function(isText) {
920 	this._formatWarningDialog.popdown();
921 	this._setFormat(isText);
922 };
923 
924 ZmSignaturesPage.prototype._formatCancelCallback =
925 function(isText) {
926 	this._formatWarningDialog.popdown();
927 	// reset the option
928 	this._sigFormat.setSelectedValue(!isText);
929 };
930 
931 
932 // buttons
933 ZmSignaturesPage.prototype._handleFormatSelect =
934 function(ev) {
935 	var isText = this._sigFormat ? this._sigFormat.getValue() : true;
936 	var currentIsText = this._sigEditor.getMode() === Dwt.TEXT;
937 	if (isText === currentIsText) {
938 		return;
939 	}
940 
941 	var content = this._sigEditor.getContent();
942 	var contentIsEmpty = (content === "<html><body><br></body></html>" || content === "");
943 
944 	if (!contentIsEmpty && isText) {
945 		if (!this._formatWarningDialog) {
946 			this._formatWarningDialog = new DwtMessageDialog({parent : appCtxt.getShell(), buttons : [DwtDialog.OK_BUTTON, DwtDialog.CANCEL_BUTTON]});
947 		}
948 		var dialog = this._formatWarningDialog;
949 		dialog.registerCallback(DwtDialog.OK_BUTTON, this._formatOkCallback, this, [isText]);
950 		dialog.registerCallback(DwtDialog.CANCEL_BUTTON, this._formatCancelCallback, this, [isText]);
951 		dialog.setMessage(ZmMsg.switchToText, DwtMessageDialog.WARNING_STYLE);
952 		dialog.popup();
953 		return;
954 	}
955 	this._setFormat(isText);
956 
957 };
958 
959 ZmSignaturesPage.prototype._handleAddButton =
960 function(ev) {
961 	this._updateSignature();
962 	this._addNewSignature();
963 };
964 
965 ZmSignaturesPage.prototype._deleteSignature =
966 function(signature, skipNotify) {
967 	signature = signature || this._selSignature;
968 	if (this._selSignature && !skipNotify) {
969 		this._sigName.clear();
970 	}
971 	this._sigList.removeItem(signature);
972 	if (!skipNotify) {
973 		this._updateUsageSelects(signature, ZmEvent.E_DELETE);
974 	}
975 	delete this._signatures[signature.id];
976 	if (!signature._new) {
977 		this._deletedSignatures[signature.id] = signature;
978 	}
979 	delete this._byName[signature.name];
980 };
981 
982 ZmSignaturesPage.prototype._handleDeleteButton =
983 function(evt) {
984     var sigEditor = this._sigEditor;
985 	this._deleteSignature();
986 	this._selSignature = null;
987 
988     if (sigEditor) {
989         sigEditor.setContent('');
990     }
991 	if (this._sigList.size() > 0) {
992 		var sel = this._sigList.getList().get(0);
993 		if (sel) {
994 			this._sigList.setSelection(sel);
995 		}
996 	}
997 	else {
998 		for (var i = 0; i < this._minEntries; i++) {
999 			this._addNewSignature(false, true); //autoAdded
1000 		}
1001 	}
1002 	this._resetOperations();
1003 };
1004 
1005 // saving
1006 
1007 ZmSignaturesPage.prototype._handleDeleteResponse =
1008 function(signature, resp) {
1009 	delete this._deletedSignatures[signature.id];
1010 };
1011 
1012 ZmSignaturesPage.prototype._handleModifyResponse =
1013 function(signature, resp) {
1014 	delete this._byName[signature._orig.name];
1015 	this._byName[signature.name] = signature;
1016 	this._setOrig(signature);
1017 };
1018 
1019 ZmSignaturesPage.prototype._handleModifyError =
1020 function(signature) {
1021 	this._restoreFromOrig(signature);
1022 	if (this._selSignature.id === signature.id) {
1023 		this._resetSignature(signature);
1024 	}
1025 	return true;
1026 };
1027 
1028 ZmSignaturesPage.prototype._handleNewResponse =
1029 function(signature, resp) {
1030 	var id = signature.id;
1031 	signature.id = signature._id;
1032 
1033 	// delete and add so that ID of row in list view is updated
1034 	var index = this._sigList.getItemIndex(signature);
1035 	this._deleteSignature(signature, true);
1036 	signature.id = id;
1037 	this._addSignature(signature, false, false, index, true);
1038 
1039 	this._newSigId[signature._id] = signature.id;
1040 	delete signature._new;
1041 
1042 	this._setOrig(signature);
1043 };
1044 
1045 ZmSignaturesPage.prototype._handleVcardBrowseButton =
1046 function(ev) {
1047 
1048 	var query;
1049 	if (!this._vcardPicker) {
1050 		AjxDispatcher.require(["ContactsCore", "Contacts"]);
1051 		this._vcardPicker = new ZmVcardPicker({sigPage:this});
1052 		var user = appCtxt.getUsername();
1053 		query = user.substr(0, user.indexOf('@'));
1054 	}
1055 	this._vcardPicker.popup(query);
1056 };
1057 
1058 ZmSignaturesPage.prototype._handleVcardClearButton =
1059 function(ev) {
1060 	this._vcardField.value = "";
1061 	if (this._selSignature) {
1062 		this._selSignature.contactId = null;
1063 	}
1064 };
1065 
1066 // validation
1067 
1068 ZmSignaturesPage.prototype._updateName =
1069 function(field, isValid) {
1070 
1071 	var signature = this._selSignature;
1072 	if (!signature) {
1073 		return;
1074 	}
1075 
1076 	if (signature.name !== field.getValue()) {
1077 		signature.name = field.getValue();
1078 		this._sigList.redrawItem(signature);
1079 		this._sigList.setSelection(signature, true);
1080 		this._resetOperations();
1081 		this._updateUsageSelects(signature, ZmEvent.E_MODIFY);
1082 	}
1083 };
1084 
1085 ZmSignaturesPage.prototype._hasChanged =
1086 function(signature) {
1087 
1088 	var o = signature._orig;
1089 	return (o.name !== signature.name ||
1090 			o.contactId !== signature.contactId ||
1091 			o.contentType !== signature.getContentType() ||
1092 			!AjxStringUtil.equalsHtmlPlatformIndependent(AjxStringUtil.htmlEncode(o.value), AjxStringUtil.htmlEncode(signature.getValue())));
1093 };
1094 
1095 ZmSignaturesPage.prototype._setOrig =
1096 function(signature) {
1097 	signature._orig = {
1098 		name:			signature.name,
1099 		contactId:		signature.contactId,
1100 		value:			signature.getValue(),
1101 		contentType:	signature.getContentType()
1102 	};
1103 };
1104 
1105 ZmSignaturesPage.prototype._restoreFromOrig =
1106 function(signature) {
1107 	var o = signature._orig;
1108 	signature.name = o.name;
1109 	signature.contactId = o.contactId;
1110 	signature.value = o.value;
1111 	signature.setContentType(o.contentType);
1112 };
1113 
1114 //
1115 // Classes
1116 //
1117 
1118 //ZmSignatureListView:  Signatures List
1119 
1120 ZmSignatureListView = function(parent) {
1121 	if (arguments.length === 0) { return; }
1122 
1123 	DwtListView.call(this, {parent:parent, className:"ZmSignatureListView"});
1124 };
1125 
1126 ZmSignatureListView.prototype = new DwtListView;
1127 ZmSignatureListView.prototype.constructor = ZmSignatureListView;
1128 
1129 ZmSignatureListView.prototype.toString =
1130 function() {
1131 	return "ZmSignatureListView";
1132 };
1133 
1134 ZmSignatureListView.prototype._getCellContents =
1135 function(html, idx, signature, field, colIdx, params) {
1136 	html[idx++] = signature.name ? AjxStringUtil.htmlEncode(signature.name, true) : ZmMsg.signatureNameHint;
1137 	return idx;
1138 };
1139 
1140 ZmSignatureListView.prototype._getItemId =
1141 function(signature) {
1142 	return (signature && signature.id) ? signature.id : Dwt.getNextId();
1143 };
1144 
1145 ZmSignatureListView.prototype.setSignatures =
1146 function(signatures) {
1147 	this._resetList();
1148 	this.addItems(signatures);
1149 	var list = this.getList();
1150 	if (list && list.size() > 0) {
1151 		this.setSelection(list.get(0));
1152 	}
1153 };
1154 
1155 
1156 // ZmVcardPicker
1157 
1158 ZmVcardPicker = function(params) {
1159 
1160 	params = params || {};
1161 	params.parent = appCtxt.getShell();
1162 	params.title = ZmMsg.selectContact;
1163 	DwtDialog.call(this, params);
1164 
1165 	this._sigPage = params.sigPage;
1166 	this.setButtonListener(DwtDialog.OK_BUTTON, new AjxListener(this, this._okButtonListener));
1167 };
1168 
1169 ZmVcardPicker.prototype = new DwtDialog;
1170 ZmVcardPicker.prototype.constructor = ZmVcardPicker;
1171 
1172 ZmVcardPicker.prototype.popup =
1173 function(query, account) {
1174 
1175 	if (!this._initialized) {
1176 		this._initialize();
1177 	}
1178 	this._contactSearch.reset(query, account);
1179 	if (query) {
1180 		this._contactSearch.search();
1181 	}
1182 
1183 	DwtDialog.prototype.popup.call(this);
1184 };
1185 
1186 ZmVcardPicker.prototype._initialize =
1187 function(account) {
1188 
1189 	var options = {preamble: ZmMsg.vcardContactSearch};
1190 	this._contactSearch = new ZmContactSearch({options:options});
1191 	this.setView(this._contactSearch);
1192 	this._initialized = true;
1193 };
1194 
1195 ZmVcardPicker.prototype._okButtonListener =
1196 function(ev) {
1197 
1198 	var data = this._contactSearch.getContacts();
1199 	var contact = data && data[0];
1200 	if (contact) {
1201 		this._sigPage.setContact(contact);
1202 	}
1203 
1204 	this.popdown();
1205 };
1206