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 /**
 25  * Creates a data source.
 26  * @class
 27  * This class represents a data source.
 28  * 
 29  * @param	{constant}	type	the account type (see <code>ZmAccount.TYPE_</code> constants)
 30  * @param	{String}	id		the id
 31  * 
 32  * @extends		ZmAccount
 33  */
 34 ZmDataSource = function(type, id) {
 35 	if (arguments.length == 0) { return; }
 36 	ZmAccount.call(this, type, id);
 37 	this.reset();
 38 };
 39 
 40 ZmDataSource.prototype = new ZmAccount;
 41 ZmDataSource.prototype.constructor = ZmDataSource;
 42 
 43 ZmDataSource.prototype.toString =
 44 function() {
 45 	return "ZmDataSource";
 46 };
 47 
 48 //
 49 // Constants
 50 //
 51 /**
 52  * Defines the "cleartext" connection type.
 53  */
 54 ZmDataSource.CONNECT_CLEAR = "cleartext";
 55 /**
 56  * Defines the "ssl" connection type.
 57  */
 58 ZmDataSource.CONNECT_SSL = "ssl";
 59 ZmDataSource.CONNECT_DEFAULT = ZmDataSource.CONNECT_CLEAR;
 60 
 61 ZmDataSource.POLL_NEVER = "0";
 62 
 63 // soap attribute to property maps
 64 
 65 ZmDataSource.DATASOURCE_ATTRS = {
 66 	// SOAP attr:		JS property
 67 	"id":				"id",
 68 	"name":				"name",
 69 	"isEnabled":		"enabled",
 70 	"emailAddress":		"email",
 71 	"host":				"mailServer",
 72 	"port":				"port",
 73 	"username":			"userName",
 74 	"password":			"password",
 75 	"l":				"folderId",
 76 	"connectionType":	"connectionType",
 77 	"pollingInterval":	"pollingInterval",
 78     "smtpEnabled":      "smtpEnabled",
 79 	"leaveOnServer":	"leaveOnServer" // POP only
 80 };
 81 
 82 ZmDataSource.IDENTITY_ATTRS = {
 83 	// SOAP attr:					JS property
 84 	"fromDisplay":					"sendFromDisplay",
 85 	"useAddressForForwardReply":	"setReplyTo",
 86 	"replyToAddress":				"setReplyToAddress",
 87 	"replyToDisplay":				"setReplyToDisplay",
 88 	"defaultSignature":				"signature",
 89 	"forwardReplySignature":		"replySignature"
 90 };
 91 
 92 //
 93 // Data
 94 //
 95 
 96 ZmDataSource.prototype.ELEMENT_NAME = "dsrc";
 97 
 98 // data source settings
 99 
100 ZmDataSource.prototype.enabled = true;
101 
102 // basic settings
103 
104 ZmDataSource.prototype.mailServer = "";
105 ZmDataSource.prototype.userName = "";
106 ZmDataSource.prototype.password = "";
107 ZmDataSource.prototype.folderId = ZmOrganizer.ID_INBOX;
108 
109 // advanced settings
110 
111 ZmDataSource.prototype.leaveOnServer = true;
112 ZmDataSource.prototype.connectionType = ZmDataSource.CONNECT_DEFAULT;
113 
114 //
115 // Public methods
116 //
117 
118 /** NOTE: Email is same as the identity's from address. */
119 ZmDataSource.prototype.setEmail =
120 function(email) {
121 	this.email = email;
122 };
123 
124 ZmDataSource.prototype.getEmail =
125 function() {
126 	var email = this.email != null ? this.email : this.identity.getField(ZmIdentity.SEND_FROM_ADDRESS); // bug: 23042
127 	if (!email) { // bug: 38175
128 		var provider = ZmDataSource.getProviderForAccount(this);
129 		var host = (provider && provider._host) || this.mailServer;
130 		email = "";
131         if (this.userName) {
132             if (this.userName.match(/@/)) email = this.userName; // bug: 48186
133             else if (host) email = [ this.userName, host].join("@");
134         }
135 	}
136 	return email;
137 };
138 
139 ZmDataSource.prototype.setFolderId =
140 function(folderId) {
141 	// TODO: Is there a better way to do this?
142 	//       I basically need to have the folder selector on the options
143 	//       page have a value of -1 but allow other code to see that and
144 	//       fill in the correct folder id. But I don't want it to
145 	//       overwrite that value once set.
146 	if (folderId == -1 && this.folderId != ZmOrganizer.ID_INBOX) { return; }
147 	this.folderId = folderId;
148 };
149 
150 ZmDataSource.prototype.getFolderId =
151 function() {
152 	return this.folderId;
153 };
154 
155 ZmDataSource.prototype.getIdentity =
156 function() {
157 	return this.identity;
158 };
159 
160 // operations
161 
162 ZmDataSource.prototype.create =
163 function(callback, errorCallback, batchCommand) {
164 	var soapDoc = AjxSoapDoc.create("CreateDataSourceRequest", "urn:zimbraMail");
165 	var dsrc = soapDoc.set(this.ELEMENT_NAME);
166 	for (var aname in ZmDataSource.DATASOURCE_ATTRS) {
167 		var pname = ZmDataSource.DATASOURCE_ATTRS[aname];
168 		var pvalue = pname == "folderId"
169 			? ZmOrganizer.normalizeId(this[pname])
170 			: this[pname];
171 		if (pname == "id" || (!pvalue && pname != "enabled" && pname != "leaveOnServer")) continue;
172 
173 		dsrc.setAttribute(aname, String(pvalue));
174 	}
175 	var identity = this.getIdentity();
176 	for (var aname in ZmDataSource.IDENTITY_ATTRS) {
177 		var pname = ZmDataSource.IDENTITY_ATTRS[aname];
178 		var pvalue = identity[pname];
179 		if (!pvalue) continue;
180 
181 		dsrc.setAttribute(aname, String(pvalue));
182 	}
183 
184 	var respCallback = new AjxCallback(this, this._handleCreateResponse, [callback]);
185 	if (batchCommand) {
186 		batchCommand.addNewRequestParams(soapDoc, respCallback, errorCallback);
187 		batchCommand.setSensitive(Boolean(this.password));
188 		return;
189 	}
190 
191 	var params = {
192 		soapDoc: soapDoc,
193 		sensitive: Boolean(this.password),
194 		asyncMode: Boolean(callback),
195 		callback: respCallback,
196 		errorCallback: errorCallback
197 	};
198 	return appCtxt.getAppController().sendRequest(params);
199 };
200 
201 ZmDataSource.prototype.save = function(callback, errorCallback, batchCommand, isIdentity) {
202 
203 	var soapDoc = AjxSoapDoc.create("ModifyDataSourceRequest", "urn:zimbraMail");
204 	var dsrc = soapDoc.set(this.ELEMENT_NAME);
205 	// NOTE: If this object is a proxy, we guarantee that the
206 	//       the id attribute is *always* set.
207 	dsrc.setAttribute("id", this.id);
208     if (!isIdentity) {
209         for (var aname in ZmDataSource.DATASOURCE_ATTRS) {
210             var pname = ZmDataSource.DATASOURCE_ATTRS[aname];
211             if (!this.hasOwnProperty(pname)) {
212                 continue;
213             }
214             var avalue = this[pname];
215             if (pname === "folderId") {
216                 avalue = ZmOrganizer.normalizeId(avalue);
217             }
218             // server sends us pollingInterval in ms, expects it back in seconds (!)
219             // since it is not a user-visible value, it's safer to not send it back at all
220             else if (pname === "pollingInterval") {
221                 continue;
222             }
223             dsrc.setAttribute(aname, String(avalue));
224         }
225     }
226 	var identity = this.getIdentity();
227 	for (var aname in ZmDataSource.IDENTITY_ATTRS) {
228 		var pname = ZmDataSource.IDENTITY_ATTRS[aname];
229 		if (!identity.hasOwnProperty(pname)) continue;
230 
231 		var avalue = identity[pname];
232 		dsrc.setAttribute(aname, String(avalue));
233 	}
234 
235 	var respCallback = new AjxCallback(this, this._handleSaveResponse, [callback]);
236 	if (batchCommand) {
237 		batchCommand.addNewRequestParams(soapDoc, respCallback, errorCallback);
238 		batchCommand.setSensitive(Boolean(this.password));
239 		return;
240 	}
241 
242 	var params = {
243 		soapDoc: soapDoc,
244 		sensitive: Boolean(this.password),
245 		asyncMode: Boolean(callback),
246 		callback: respCallback,
247 		errorCallback: errorCallback
248 	};
249 	return appCtxt.getAppController().sendRequest(params);
250 };
251 
252 ZmDataSource.prototype.doDelete =
253 function(callback, errorCallback, batchCommand) {
254 	var soapDoc = AjxSoapDoc.create("DeleteDataSourceRequest", "urn:zimbraMail");
255 	var dsrc = soapDoc.set(this.ELEMENT_NAME);
256 	dsrc.setAttribute("id", this.id);
257 
258 	var respCallback = new AjxCallback(this, this._handleDeleteResponse, [callback]);
259 	if (batchCommand) {
260 		batchCommand.addNewRequestParams(soapDoc, respCallback, errorCallback);
261 		return;
262 	}
263 
264 	var params = {
265 		soapDoc: soapDoc,
266 		asyncMode: Boolean(callback),
267 		callback: respCallback,
268 		errorCallback: errorCallback
269 	};
270 	return appCtxt.getAppController().sendRequest(params);
271 };
272 
273 /**
274  * Tests the data source connection.
275  * 
276  * @param	{AjxCallback}		callback		the callback
277  * @param	{AjxCallback}		errorCallback		the error callback
278  * @param	{ZmBatchCommand}		batchCommand		the batch command
279  * @param	{Boolean}	noBusyOverlay		if <code>true</code>, do not show busy overlay
280  * @return	{Object}	the response
281  */
282 ZmDataSource.prototype.testConnection =
283 function(callback, errorCallback, batchCommand, noBusyOverlay) {
284 	var soapDoc = AjxSoapDoc.create("TestDataSourceRequest", "urn:zimbraMail");
285 	var dsrc = soapDoc.set(this.ELEMENT_NAME);
286 
287 	var attrs = ["host", "port", "username", "password", "connectionType", "leaveOnServer"];
288 	for (var i = 0; i < attrs.length; i++) {
289 		var aname = attrs[i];
290 		var pname = ZmDataSource.DATASOURCE_ATTRS[aname];
291 		dsrc.setAttribute(aname, this[pname]);
292 	}
293 
294 	if (batchCommand) {
295 		batchCommand.addNewRequestParams(soapDoc, callback, errorCallback);
296 		batchCommand.setSensitive(true);
297 		return;
298 	}
299 
300 	var params = {
301 		soapDoc: soapDoc,
302 		sensitive: true,
303 		asyncMode: Boolean(callback),
304 		noBusyOverlay: noBusyOverlay,
305 		callback: callback,
306 		errorCallback: errorCallback
307 	};
308 	return appCtxt.getAppController().sendRequest(params);
309 };
310 
311 /**
312  * Gets the port.
313  * 
314  * @return	{int}	port
315  */
316 ZmDataSource.prototype.getPort =
317 function() {
318 	return this.port || this.getDefaultPort();
319 };
320 
321 ZmDataSource.prototype.isStatusOk = function() {
322 	return this.enabled && !this.failingSince;
323 };
324 
325 ZmDataSource.prototype.setFromJson =
326 function(obj) {
327 	// errors
328 	if (obj.failingSince) {
329 		this.failingSince = obj.failingSince;
330 		this.lastError = (obj.lastError && obj.lastError[0]._content) || ZmMsg.unknownError;
331 	}
332 	else {
333 		delete this.failingSince;
334 		delete this.lastError;
335 	}
336 	// data source fields
337 	for (var aname in ZmDataSource.DATASOURCE_ATTRS) {
338 		var avalue = obj[aname];
339 		if (avalue == null) continue;
340 		if (aname == "isEnabled" || aname == "leaveOnServer") {
341 			avalue = avalue == "1" || String(avalue).toLowerCase() == "true";
342 		}
343         // server sends us pollingInterval in ms, expects it back in seconds (!)
344         else if (aname === "pollingInterval") {
345             avalue = avalue / 1000;
346         }
347 		var pname = ZmDataSource.DATASOURCE_ATTRS[aname];
348 		this[pname] = avalue;
349 	}
350 
351 	// pseudo-identity fields
352 	var identity = this.getIdentity();
353 	for (var aname in ZmDataSource.IDENTITY_ATTRS) {
354 		var avalue = obj[aname];
355 		if (avalue == null) continue;
356 		if (aname == "useAddressForForwardReply") {
357 			avalue = avalue == "1" || String(avalue).toLowerCase() == "true";
358 		}
359 
360 		var pname = ZmDataSource.IDENTITY_ATTRS[aname];
361 		identity[pname] = avalue;
362 	}
363 	this._setupIdentity();
364 };
365 
366 ZmDataSource.prototype.reset = function() {
367 	// reset data source properties
368 	// NOTE: These have default values on the prototype object
369 	delete this.mailServer;
370 	delete this.userName;
371 	delete this.password;
372 	delete this.folderId;
373 	delete this.leaveOnServer;
374 	delete this.connectionType;
375 	delete this.pollingInterval;
376 	// other
377 	this.email = "";
378 	this.port = this.getDefaultPort();
379 
380 	// reset identity
381 	var identity = this.identity = new ZmIdentity();
382 	identity.id = this.id;
383 	identity.isFromDataSource = true;
384 	
385 	// saving the identity itself won't work; need to save the data source
386 	var self = this;
387 	identity.save = function(callback, errorCallback, batchCommand) {
388 		ZmDataSource.prototype.save.call(self, callback, errorCallback, batchCommand, true);
389 	};
390 };
391 
392 ZmDataSource.prototype.getProvider = function() {
393 	return ZmDataSource.getProviderForAccount(this);
394 };
395 
396 //
397 // Public functions
398 //
399 
400 // data source providers - provides default values
401 
402 /**
403  * Adds a data source provider. The registered providers are objects that
404  * specify default values for data sources. This can be used to show the
405  * user a list of known email providers (e.g. Yahoo! Mail) to pre-fill the
406  * account information.
407  *
408  * @param {Hash}	provider  a hash of provider information
409  * @param	{String}	provider.id		a unique identifier for this provider
410  * @param	{String}	provider.name	the name of this provider to display to the user
411  * @param	{String}	[provider.type]		the type (see <code>ZmAccount.TYPE_</code> constants)
412  * @param	{String}	[provider.connectionType]	the connection type (see <code>ZmDataSource.CONNECT_</code> constants)
413  * @param	{String}	[provider.host]	the server
414  * @param	{String}	[provider.pollingInterval]		the polling interval
415  * @param	{Boolean}	[provider.leaveOnServer]	if <code>true</code>, leave message on server (POP only)
416  */
417 ZmDataSource.addProvider = function(provider) {
418 	var providers = ZmDataSource.getProviders();
419 	providers[provider.id] = provider;
420 	// normalize values -- defensive programming
421 	if (provider.type) {
422 		provider.type = provider.type.toLowerCase() == "pop" ? ZmAccount.TYPE_POP : ZmAccount.TYPE_IMAP;
423 	}
424 	else {
425 		provider.type = ZmAccount.TYPE_POP;
426 	}
427 	if (provider.connectionType) {
428 		var isSsl = provider.connectionType.toLowerCase() == "ssl";
429 		provider.connectionType =  isSsl ? ZmDataSource.CONNECT_SSL : ZmDataSource.CONNECT_CLEAR;
430 	}
431 	else {
432 		provider.connectionType = ZmDataSource.CONNECT_CLEAR;
433 	}
434 	if (!provider.port) {
435 		var isPop = provider.type == ZmAccount.TYPE_POP;
436 		if (isSsl) {
437 			provider.port = isPop ? ZmPopAccount.PORT_SSL : ZmImapAccount.PORT_SSL;
438 		}
439 		else {
440 			provider.port = isPop ? ZmPopAccount.PORT_CLEAR : ZmImapAccount.PORT_CLEAR;
441 		}
442 	}
443 };
444 
445 /**
446  * Gets the providers.
447  * 
448  * @return	{Array}		an array of providers
449  */
450 ZmDataSource.getProviders =
451 function() {
452 	if (!ZmDataSource._providers) {
453 		ZmDataSource._providers = {};
454 	}
455 	return ZmDataSource._providers;
456 };
457 
458 /**
459  * Gets the provider.
460  * 
461  * @param	{ZmAccount}	account		the account
462  * @return	{Hash}		the provider or <code>null</code> for none
463  */
464 ZmDataSource.getProviderForAccount =
465 function(account) {
466 	return ZmDataSource.getProviderForHost(account.mailServer);
467 };
468 
469 /**
470  * Gets the provider.
471  * 
472  * @param	{String}	host		the host
473  * @return	{Hash}		the provider or <code>null</code> for none
474  */
475 ZmDataSource.getProviderForHost =
476 function(host) {
477 	var providers = ZmDataSource.getProviders();
478 	for (var id in providers) {
479 		hasProviders = true;
480 		var provider = providers[id];
481 		if (provider.host == host) {
482 			return provider;
483 		}
484 	}
485 	return null;
486 };
487 
488 /**
489  * Removes all providers.
490  */
491 ZmDataSource.removeAllProviders = function() {
492 	delete ZmDataSource._providers;
493 };
494 
495 //
496 // Protected methods
497 //
498 
499 
500 ZmDataSource.prototype._setupIdentity =
501 function() {
502 	this.identity.useWhenSentTo = true;
503 	this.identity.whenSentToAddresses = [ this.getEmail() ];
504 	this.identity.name = this.name;
505 };
506 
507 ZmDataSource.prototype._loadFromDom =
508 function(data) {
509 	this.setFromJson(data);
510 };
511 
512 ZmDataSource.prototype._handleCreateResponse =
513 function(callback, result) {
514 	var resp = result._data.CreateDataSourceResponse;
515 	this.id = resp[this.ELEMENT_NAME][0].id;
516 	this.identity.id = this.id;
517 	this._setupIdentity();
518 	delete this._new;
519 	delete this._dirty;
520 
521 	appCtxt.getDataSourceCollection().add(this);
522 
523 	var apps = [ZmApp.MAIL, ZmApp.PORTAL];
524 	for (var i=0; i<apps.length; i++) {
525 		var app = appCtxt.getApp(apps[i]);
526 		if (app) {
527 			var overviewId = app.getOverviewId();
528 			var treeView = appCtxt.getOverviewController().getTreeView(overviewId, ZmOrganizer.FOLDER);
529 			var fid = appCtxt.getActiveAccount().isMain ? this.folderId : ZmOrganizer.getSystemId(this.folderId);
530 			var treeItem = treeView ? treeView.getTreeItemById(fid) : null;
531 			if (treeItem) {
532 				// reset the icon in the tree view if POP account since the first time it
533 				// was created, we didnt know it was a data source
534 				if (this.type == ZmAccount.TYPE_POP && this.folderId != ZmFolder.ID_INBOX) {
535 					treeItem.setImage("POPAccount");
536 				}
537 				else if (this.type == ZmAccount.TYPE_IMAP) {
538 					// change imap folder to a tree header since folder is first created
539 					// without knowing its a datasource
540 					treeItem.dispose();
541 					var rootId = ZmOrganizer.getSystemId(ZmOrganizer.ID_ROOT);
542 					var parentNode = treeView.getTreeItemById(rootId);
543 					var organizer = appCtxt.getById(this.folderId);
544 					treeView._addNew(parentNode, organizer);
545 				}
546 			}
547 		}
548 	}
549 
550 	if (callback) {
551 		callback.run();
552 	}
553 };
554 
555 ZmDataSource.prototype._handleSaveResponse =
556 function(callback, result) {
557 	delete this._dirty;
558 
559 	var collection = appCtxt.getDataSourceCollection();
560 	// NOTE: By removing and adding it again, we make this proxy the
561 	//       base datasource object in the collection.
562 	collection.remove(this);
563 	collection.add(this);
564 
565 	if (callback) {
566 		callback.run();
567 	}
568 };
569 
570 ZmDataSource.prototype._handleDeleteResponse =
571 function(callback, result) {
572 	appCtxt.getDataSourceCollection().remove(this);
573 
574 	var overviewId = appCtxt.getApp(ZmApp.MAIL).getOverviewId();
575 	var treeView = appCtxt.getOverviewController().getTreeView(overviewId, ZmOrganizer.FOLDER);
576 	var fid = appCtxt.getActiveAccount().isMain ? this.folderId : ZmOrganizer.getSystemId(this.folderId);
577 	if(this.folderId == ZmAccountsPage.DOWNLOAD_TO_FOLDER && this._object_ && this._object_.folderId) {
578 		fid = this._object_.folderId;
579 	}	
580 	var treeItem = treeView ? treeView.getTreeItemById(fid) : null;
581 	if (treeItem) {
582 		if (this.type == ZmAccount.TYPE_POP && this.folderId != ZmFolder.ID_INBOX) {
583 			// reset icon since POP folder is no longer hooked up to a datasource
584 			treeItem.setImage("Folder");
585 		} else if (this.type == ZmAccount.TYPE_IMAP) {
586 			// reset the icon in the tree view if POP account since the first time it
587 			// was created, we didnt know it was a data source
588 			treeItem.dispose();
589 			var parentNode = treeView.getTreeItemById(ZmOrganizer.ID_ROOT);
590 			var organizer = appCtxt.getById(fid);
591 			if (organizer) {
592 				treeView._addNew(parentNode, organizer);
593 			}
594 		}
595 	}
596 
597 	if (callback) {
598 		callback.run();
599 	}
600 };
601