1/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2/* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6/**
7 * This file creates the class AccountConfig, which is a JS object that holds
8 * a configuration for a certain account. It is *not* created in the backend
9 * yet (use aw-createAccount.js for that), and it may be incomplete.
10 *
11 * Several AccountConfig objects may co-exist, e.g. for autoconfig.
12 * One AccountConfig object is used to prefill and read the widgets
13 * in the Wizard UI.
14 * When we autoconfigure, we autoconfig writes the values into a
15 * new object and returns that, and the caller can copy these
16 * values into the object used by the UI.
17 *
18 * See also
19 * <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat>
20 * for values stored.
21 */
22
23const EXPORTED_SYMBOLS = ["AccountConfig"];
24
25ChromeUtils.defineModuleGetter(
26  this,
27  "AccountCreationUtils",
28  "resource:///modules/accountcreation/AccountCreationUtils.jsm"
29);
30ChromeUtils.defineModuleGetter(
31  this,
32  "Sanitizer",
33  "resource:///modules/accountcreation/Sanitizer.jsm"
34);
35
36function AccountConfig() {
37  this.incoming = this.createNewIncoming();
38  this.incomingAlternatives = [];
39  this.outgoing = this.createNewOutgoing();
40  this.outgoingAlternatives = [];
41  this.identity = {
42    // displayed real name of user
43    realname: "%REALNAME%",
44    // email address of user, as shown in From of outgoing mails
45    emailAddress: "%EMAILADDRESS%",
46  };
47  this.inputFields = [];
48  this.domains = [];
49}
50AccountConfig.prototype = {
51  // @see createNewIncoming()
52  incoming: null,
53  // @see createNewOutgoing()
54  outgoing: null,
55  /**
56   * Other servers which can be used instead of |incoming|,
57   * in order of decreasing preference.
58   * (|incoming| itself should not be included here.)
59   * { Array of incoming/createNewIncoming() }
60   */
61  incomingAlternatives: null,
62  outgoingAlternatives: null,
63  // just an internal string to refer to this. Do not show to user.
64  id: null,
65  // who created the config.
66  // { one of kSource* }
67  source: null,
68  /**
69   * Used for telemetry purposes.
70   * - for kSourceXML, subSource is one of xml-from-{disk, db, isp-https, isp-http}.
71   * - for kSourceExchange, subSource is one of exchange-from-urlN[-guess].
72   */
73  subSource: null,
74  displayName: null,
75  // { Array of { varname (value without %), displayName, exampleValue } }
76  inputFields: null,
77  // email address domains for which this config is applicable
78  // { Array of Strings }
79  domains: null,
80
81  /**
82   * Factory function for incoming and incomingAlternatives
83   */
84  createNewIncoming() {
85    return {
86      // { String-enum: "pop3", "imap", "nntp", "exchange" }
87      type: null,
88      hostname: null,
89      // { Integer }
90      port: null,
91      // May be a placeholder (starts and ends with %). { String }
92      username: null,
93      password: null,
94      // { enum: 1 = plain, 2 = SSL/TLS, 3 = STARTTLS always, 0 = not inited }
95      // ('TLS when available' is insecure and not supported here)
96      socketType: 0,
97      /**
98       * true when the cert is invalid (and thus SSL useless), because it's
99       * 1) not from an accepted CA (including self-signed certs)
100       * 2) for a different hostname or
101       * 3) expired.
102       * May go back to false when user explicitly accepted the cert.
103       */
104      badCert: false,
105      /**
106       * How to log in to the server: plaintext or encrypted pw, GSSAPI etc.
107       * Defined by Ci.nsMsgAuthMethod
108       * Same as server pref "authMethod".
109       */
110      auth: 0,
111      /**
112       * Other auth methods that we think the server supports.
113       * They are ordered by descreasing preference.
114       * (|auth| itself is not included in |authAlternatives|)
115       * {Array of Ci.nsMsgAuthMethod} (same as .auth)
116       */
117      authAlternatives: null,
118      // in minutes { Integer }
119      checkInterval: 10,
120      loginAtStartup: true,
121      // POP3 only:
122      // Not yet implemented. { Boolean }
123      useGlobalInbox: false,
124      leaveMessagesOnServer: true,
125      daysToLeaveMessagesOnServer: 14,
126      deleteByAgeFromServer: true,
127      // When user hits delete, delete from local store and from server
128      deleteOnServerWhenLocalDelete: true,
129      downloadOnBiff: true,
130      // Override `addThisServer` for a specific incoming server
131      useGlobalPreferredServer: false,
132
133      // OAuth2 configuration, if needed.
134      oauthSettings: null,
135
136      // for Microsoft Exchange servers. Optional.
137      owaURL: null,
138      ewsURL: null,
139      easURL: null,
140      // for when an addon overrides the account type. Optional.
141      addonAccountType: null,
142    };
143  },
144  /**
145   * Factory function for outgoing and outgoingAlternatives
146   */
147  createNewOutgoing() {
148    return {
149      type: "smtp",
150      hostname: null,
151      port: null, // see incoming
152      username: null, // see incoming. may be null, if auth is 0.
153      password: null, // see incoming. may be null, if auth is 0.
154      socketType: 0, // see incoming
155      badCert: false, // see incoming
156      auth: 0, // see incoming
157      authAlternatives: null, // see incoming
158      addThisServer: true, // if we already have an SMTP server, add this
159      // if we already have an SMTP server, use it.
160      useGlobalPreferredServer: false,
161      // we should reuse an already configured SMTP server.
162      // nsISmtpServer.key
163      existingServerKey: null,
164      // user display value for existingServerKey
165      existingServerLabel: null,
166
167      // OAuth2 configuration, if needed.
168      oauthSettings: null,
169    };
170  },
171
172  /**
173   * The configuration needs an addon to handle the account type.
174   * The addon needs to be installed before the account can be created
175   * in the backend.
176   * You can choose one, if there are several addons in the list.
177   * (Optional)
178   *
179   * Array of:
180   * {
181   *   id: "owl@example.com" {string},
182   *
183   *   // already localized string
184   *   name: "Owl" {string},
185   *
186   *   // already localized string
187   *   description: "A third party addon that allows you to connect to Exchange servers" {string}
188   *
189   *   // Minimal version of the addon. Needed in case the addon is already installed,
190   *   // to verify that the installed version is sufficient.
191   *   // The XPI URL below must satisfy this.
192   *   // Must satisfy <https://developer.mozilla.org/en-US/docs/Mozilla/Toolkit_version_format>
193   *   minVersion: "0.2" {string}
194   *
195   *   xpiURL: "https://live.thunderbird.net/autoconfig/owl.xpi" {URL},
196   *   websiteURL: "https://www.beonex.com/owl/" {URL},
197   *   icon32: "https://www.beonex.com/owl/owl-32x32.png" {URL},
198   *
199   *   useType : {
200   *     // Type shown as radio button to user in the config result.
201   *     // Users won't understand OWA vs. EWS vs. EAS etc., so this is an abstraction
202   *     // from the end user perspective.
203   *     generalType: "exchange" {string},
204   *
205   *     // Protocol
206   *     // Independent of the addon
207   *     protocolType: "owa" {string},
208   *
209   *     // Account type in the Thunderbird backend.
210   *     // What nsIMsgAccount.type will be set to when creating the account.
211   *     // This is specific to the addon.
212   *     addonAccountType: "owl-owa" {string},
213   *   }
214   * }
215   */
216  addons: null,
217
218  /**
219   * Returns a deep copy of this object,
220   * i.e. modifying the copy will not affect the original object.
221   */
222  copy() {
223    // Workaround: deepCopy() fails to preserve base obj (instanceof)
224    let result = new AccountConfig();
225    for (let prop in this) {
226      result[prop] = AccountCreationUtils.deepCopy(this[prop]);
227    }
228
229    return result;
230  },
231
232  isComplete() {
233    return (
234      !!this.incoming.hostname &&
235      !!this.incoming.port &&
236      !!this.incoming.socketType &&
237      !!this.incoming.auth &&
238      !!this.incoming.username &&
239      (!!this.outgoing.existingServerKey ||
240        this.outgoing.useGlobalPreferredServer ||
241        (!!this.outgoing.hostname &&
242          !!this.outgoing.port &&
243          !!this.outgoing.socketType &&
244          !!this.outgoing.auth &&
245          !!this.outgoing.username))
246    );
247  },
248
249  toString() {
250    function sslToString(socketType) {
251      switch (socketType) {
252        case 0:
253          return "undefined";
254        case 1:
255          return "no SSL";
256        case 2:
257          return "SSL";
258        case 3:
259          return "STARTTLS";
260        default:
261          return "invalid";
262      }
263    }
264
265    function authToString(authMethod) {
266      switch (authMethod) {
267        case 0:
268          return "undefined";
269        case 1:
270          return "none";
271        case 2:
272          return "old plain";
273        case 3:
274          return "plain";
275        case 4:
276          return "encrypted";
277        case 5:
278          return "Kerberos";
279        case 6:
280          return "NTLM";
281        case 7:
282          return "external/SSL";
283        case 8:
284          return "any secure";
285        case 10:
286          return "OAuth2";
287        default:
288          return "invalid";
289      }
290    }
291
292    function passwordToString(password) {
293      return password ? "set" : "not set";
294    }
295
296    function configToString(config) {
297      return (
298        config.type +
299        ", " +
300        config.hostname +
301        ":" +
302        config.port +
303        ", " +
304        sslToString(config.socketType) +
305        ", auth: " +
306        authToString(config.auth) +
307        ", username: " +
308        (config.username || "(undefined)") +
309        ", password: " +
310        passwordToString(config.password)
311      );
312    }
313
314    let result = "Incoming: " + configToString(this.incoming) + "\nOutgoing: ";
315    if (
316      this.outgoing.useGlobalPreferredServer ||
317      this.incoming.useGlobalPreferredServer
318    ) {
319      result += "Use global server";
320    } else if (this.outgoing.existingServerKey) {
321      result += "Use existing server " + this.outgoing.existingServerKey;
322    } else {
323      result += configToString(this.outgoing);
324    }
325    for (let config of this.incomingAlternatives) {
326      result += "\nIncoming alt: " + configToString(config);
327    }
328    for (let config of this.outgoingAlternatives) {
329      result += "\nOutgoing alt: " + configToString(config);
330    }
331    return result;
332  },
333
334  /**
335   * Sort the config alternatives such that exchange is the last of the
336   * alternatives.
337   */
338  preferStandardProtocols() {
339    let alternatives = this.incomingAlternatives;
340    // Add default incoming as one alternative.
341    alternatives.unshift(this.incoming);
342    alternatives.sort((a, b) => {
343      if (a.type == "exchange") {
344        return 1;
345      }
346      if (b.type == "exchange") {
347        return -1;
348      }
349      return 0;
350    });
351    this.incomingAlternatives = alternatives;
352    this.incoming = alternatives.shift();
353  },
354};
355
356// enum consts
357
358// .source
359AccountConfig.kSourceUser = "user"; // user manually entered the config
360AccountConfig.kSourceXML = "xml"; // config from XML from ISP or Mozilla DB
361AccountConfig.kSourceGuess = "guess"; // guessConfig()
362AccountConfig.kSourceExchange = "exchange"; // from Microsoft Exchange AutoDiscover
363
364/**
365 * Some fields on the account config accept placeholders (when coming from XML).
366 *
367 * These are the predefined ones
368 * * %EMAILADDRESS% (full email address of the user, usually entered by user)
369 * * %EMAILLOCALPART% (email address, part before @)
370 * * %EMAILDOMAIN% (email address, part after @)
371 * * %REALNAME%
372 * as well as those defined in account.inputFields.*.varname, with % added
373 * before and after.
374 *
375 * These must replaced with real values, supplied by the user or app,
376 * before the account is created. This is done here. You call this function once
377 * you have all the data - gathered the standard vars mentioned above as well as
378 * all listed in account.inputFields, and pass them in here. This function will
379 * insert them in the fields, returning a fully filled-out account ready to be
380 * created.
381 *
382 * @param account {AccountConfig}
383 * The account data to be modified. It may or may not contain placeholders.
384 * After this function, it should not contain placeholders anymore.
385 * This object will be modified in-place.
386 *
387 * @param emailfull {String}
388 * Full email address of this account, e.g. "joe@example.com".
389 * Empty of incomplete email addresses will/may be rejected.
390 *
391 * @param realname {String}
392 * Real name of user, as will appear in From of outgoing messages
393 *
394 * @param password {String}
395 * The password for the incoming server and (if necessary) the outgoing server
396 */
397AccountConfig.replaceVariables = function(
398  account,
399  realname,
400  emailfull,
401  password
402) {
403  Sanitizer.nonemptystring(emailfull);
404  let emailsplit = emailfull.split("@");
405  AccountCreationUtils.assert(
406    emailsplit.length == 2,
407    "email address not in expected format: must contain exactly one @"
408  );
409  let emaillocal = Sanitizer.nonemptystring(emailsplit[0]);
410  let emaildomain = Sanitizer.hostname(emailsplit[1]);
411  Sanitizer.label(realname);
412  Sanitizer.nonemptystring(realname);
413
414  let otherVariables = {};
415  otherVariables.EMAILADDRESS = emailfull;
416  otherVariables.EMAILLOCALPART = emaillocal;
417  otherVariables.EMAILDOMAIN = emaildomain;
418  otherVariables.REALNAME = realname;
419
420  if (password) {
421    account.incoming.password = password;
422    account.outgoing.password = password; // set member only if auth required?
423  }
424  account.incoming.username = _replaceVariable(
425    account.incoming.username,
426    otherVariables
427  );
428  account.outgoing.username = _replaceVariable(
429    account.outgoing.username,
430    otherVariables
431  );
432  account.incoming.hostname = _replaceVariable(
433    account.incoming.hostname,
434    otherVariables
435  );
436  if (account.outgoing.hostname) {
437    // will be null if user picked existing server.
438    account.outgoing.hostname = _replaceVariable(
439      account.outgoing.hostname,
440      otherVariables
441    );
442  }
443  account.identity.realname = _replaceVariable(
444    account.identity.realname,
445    otherVariables
446  );
447  account.identity.emailAddress = _replaceVariable(
448    account.identity.emailAddress,
449    otherVariables
450  );
451  account.displayName = _replaceVariable(account.displayName, otherVariables);
452};
453
454function _replaceVariable(variable, values) {
455  let str = variable;
456  if (typeof str != "string") {
457    return str;
458  }
459
460  for (let varname in values) {
461    str = str.replace("%" + varname + "%", values[varname]);
462  }
463
464  return str;
465}
466