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
6const EXPORTED_SYMBOLS = ["CreateInBackend"];
7
8ChromeUtils.defineModuleGetter(
9  this,
10  "AccountConfig",
11  "resource:///modules/accountcreation/AccountConfig.jsm"
12);
13ChromeUtils.defineModuleGetter(
14  this,
15  "AccountCreationUtils",
16  "resource:///modules/accountcreation/AccountCreationUtils.jsm"
17);
18
19const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
20const { MailServices } = ChromeUtils.import(
21  "resource:///modules/MailServices.jsm"
22);
23
24/* eslint-disable complexity */
25/**
26 * Takes an |AccountConfig| JS object and creates that account in the
27 * Thunderbird backend (which also writes it to prefs).
28 *
29 * @param {AccountConfig} config - The account to create
30 * @return {nsIMsgAccount} - the newly created account
31 */
32function createAccountInBackend(config) {
33  let uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(
34    Ci.nsIUUIDGenerator
35  );
36  // incoming server
37  let inServer = MailServices.accounts.createIncomingServer(
38    config.incoming.username,
39    config.incoming.hostname,
40    config.incoming.type
41  );
42  inServer.port = config.incoming.port;
43  inServer.authMethod = config.incoming.auth;
44  inServer.password = config.incoming.password;
45  // This new CLIENTID is for the outgoing server, and will be applied to the
46  // incoming only if the incoming username and hostname match the outgoing.
47  // We must generate this unconditionally because we cannot determine whether
48  // the outgoing server has clientid enabled yet or not, and we need to do it
49  // here in order to populate the incoming server if the outgoing matches.
50  let newOutgoingClientid = uuidGen
51    .generateUUID()
52    .toString()
53    .replace(/[{}]/g, "");
54  // Grab the base domain of both incoming and outgoing hostname in order to
55  // compare the two to detect if the base domain is the same.
56  let incomingBaseDomain;
57  let outgoingBaseDomain;
58  try {
59    incomingBaseDomain = Services.eTLD.getBaseDomainFromHost(
60      config.incoming.hostname
61    );
62  } catch (e) {
63    incomingBaseDomain = config.incoming.hostname;
64  }
65  try {
66    outgoingBaseDomain = Services.eTLD.getBaseDomainFromHost(
67      config.outgoing.hostname
68    );
69  } catch (e) {
70    outgoingBaseDomain = config.outgoing.hostname;
71  }
72  if (
73    config.incoming.username == config.outgoing.username &&
74    incomingBaseDomain == outgoingBaseDomain
75  ) {
76    inServer.clientid = newOutgoingClientid;
77  } else {
78    // If the username/hostname are different then generate a new CLIENTID.
79    inServer.clientid = uuidGen
80      .generateUUID()
81      .toString()
82      .replace(/[{}]/g, "");
83  }
84
85  if (config.rememberPassword && config.incoming.password) {
86    rememberPassword(inServer, config.incoming.password);
87  }
88
89  if (inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
90    inServer.setCharValue("oauth2.scope", config.incoming.oauthSettings.scope);
91    inServer.setCharValue(
92      "oauth2.issuer",
93      config.incoming.oauthSettings.issuer
94    );
95  }
96
97  // SSL
98  if (config.incoming.socketType == 1) {
99    // plain
100    inServer.socketType = Ci.nsMsgSocketType.plain;
101  } else if (config.incoming.socketType == 2) {
102    // SSL / TLS
103    inServer.socketType = Ci.nsMsgSocketType.SSL;
104  } else if (config.incoming.socketType == 3) {
105    // STARTTLS
106    inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
107  }
108
109  // If we already have an account with an identical name, generate a unique
110  // name for the new account to avoid duplicates.
111  inServer.prettyName = checkAccountNameAlreadyExists(
112    config.identity.emailAddress
113  )
114    ? generateUniqueAccountName(config)
115    : config.identity.emailAddress;
116
117  inServer.doBiff = true;
118  inServer.biffMinutes = config.incoming.checkInterval;
119  inServer.setBoolValue("login_at_startup", config.incoming.loginAtStartup);
120  if (config.incoming.type == "pop3") {
121    inServer.setBoolValue(
122      "leave_on_server",
123      config.incoming.leaveMessagesOnServer
124    );
125    inServer.setIntValue(
126      "num_days_to_leave_on_server",
127      config.incoming.daysToLeaveMessagesOnServer
128    );
129    inServer.setBoolValue(
130      "delete_mail_left_on_server",
131      config.incoming.deleteOnServerWhenLocalDelete
132    );
133    inServer.setBoolValue(
134      "delete_by_age_from_server",
135      config.incoming.deleteByAgeFromServer
136    );
137    inServer.setBoolValue("download_on_biff", config.incoming.downloadOnBiff);
138  }
139  if (config.incoming.owaURL) {
140    inServer.setUnicharValue("owa_url", config.incoming.owaURL);
141  }
142  if (config.incoming.ewsURL) {
143    inServer.setUnicharValue("ews_url", config.incoming.ewsURL);
144  }
145  if (config.incoming.easURL) {
146    inServer.setUnicharValue("eas_url", config.incoming.easURL);
147  }
148  inServer.valid = true;
149
150  let username =
151    config.outgoing.auth != Ci.nsMsgAuthMethod.none
152      ? config.outgoing.username
153      : null;
154  let outServer = MailServices.smtp.findServer(
155    username,
156    config.outgoing.hostname
157  );
158  AccountCreationUtils.assert(
159    config.outgoing.addThisServer ||
160      config.outgoing.useGlobalPreferredServer ||
161      config.outgoing.existingServerKey,
162    "No SMTP server: inconsistent flags"
163  );
164
165  if (
166    config.outgoing.addThisServer &&
167    !outServer &&
168    !config.incoming.useGlobalPreferredServer
169  ) {
170    outServer = MailServices.smtp.createServer();
171    outServer.hostname = config.outgoing.hostname;
172    outServer.port = config.outgoing.port;
173    outServer.authMethod = config.outgoing.auth;
174    // Populate the clientid if it is enabled for this outgoing server.
175    if (outServer.clientidEnabled) {
176      outServer.clientid = newOutgoingClientid;
177    }
178    if (config.outgoing.auth != Ci.nsMsgAuthMethod.none) {
179      outServer.username = username;
180      outServer.password = config.outgoing.password;
181      if (config.rememberPassword && config.outgoing.password) {
182        rememberPassword(outServer, config.outgoing.password);
183      }
184    }
185
186    if (outServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
187      let prefBranch = "mail.smtpserver." + outServer.key + ".";
188      Services.prefs.setCharPref(
189        prefBranch + "oauth2.scope",
190        config.outgoing.oauthSettings.scope
191      );
192      Services.prefs.setCharPref(
193        prefBranch + "oauth2.issuer",
194        config.outgoing.oauthSettings.issuer
195      );
196    }
197
198    if (config.outgoing.socketType == 1) {
199      // no SSL
200      outServer.socketType = Ci.nsMsgSocketType.plain;
201    } else if (config.outgoing.socketType == 2) {
202      // SSL / TLS
203      outServer.socketType = Ci.nsMsgSocketType.SSL;
204    } else if (config.outgoing.socketType == 3) {
205      // STARTTLS
206      outServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
207    }
208
209    outServer.description = config.displayName;
210
211    // If this is the first SMTP server, set it as default
212    if (
213      !MailServices.smtp.defaultServer ||
214      !MailServices.smtp.defaultServer.hostname
215    ) {
216      MailServices.smtp.defaultServer = outServer;
217    }
218  }
219
220  // identity
221  // TODO accounts without identity?
222  let identity = MailServices.accounts.createIdentity();
223  identity.fullName = config.identity.realname;
224  identity.email = config.identity.emailAddress;
225
226  // for new accounts, default to replies being positioned above the quote
227  // if a default account is defined already, take its settings instead
228  if (config.incoming.type == "imap" || config.incoming.type == "pop3") {
229    identity.replyOnTop = 1;
230    // identity.sigBottom = false; // don't set this until Bug 218346 is fixed
231
232    if (
233      MailServices.accounts.accounts.length &&
234      MailServices.accounts.defaultAccount
235    ) {
236      let defAccount = MailServices.accounts.defaultAccount;
237      let defIdentity = defAccount.defaultIdentity;
238      if (
239        defAccount.incomingServer.canBeDefaultServer &&
240        defIdentity &&
241        defIdentity.valid
242      ) {
243        identity.replyOnTop = defIdentity.replyOnTop;
244        identity.sigBottom = defIdentity.sigBottom;
245      }
246    }
247  }
248
249  // due to accepted conventions, news accounts should default to plain text
250  if (config.incoming.type == "nntp") {
251    identity.composeHtml = false;
252  }
253
254  identity.valid = true;
255
256  if (
257    !config.outgoing.useGlobalPreferredServer &&
258    !config.incoming.useGlobalPreferredServer
259  ) {
260    if (config.outgoing.existingServerKey) {
261      identity.smtpServerKey = config.outgoing.existingServerKey;
262    } else {
263      identity.smtpServerKey = outServer.key;
264    }
265  }
266
267  // account and hook up
268  // Note: Setting incomingServer will cause the AccountManager to refresh
269  // itself, which could be a problem if we came from it and we haven't set
270  // the identity (see bug 521955), so make sure everything else on the
271  // account is set up before you set the incomingServer.
272  let account = MailServices.accounts.createAccount();
273  account.addIdentity(identity);
274  account.incomingServer = inServer;
275  if (
276    inServer.canBeDefaultServer &&
277    (!MailServices.accounts.defaultAccount ||
278      !MailServices.accounts.defaultAccount.incomingServer.canBeDefaultServer)
279  ) {
280    MailServices.accounts.defaultAccount = account;
281  }
282
283  verifyLocalFoldersAccount(MailServices.accounts);
284  setFolders(identity, inServer);
285
286  // save
287  MailServices.accounts.saveAccountInfo();
288  try {
289    Services.prefs.savePrefFile(null);
290  } catch (ex) {
291    AccountCreationUtils.ddump("Could not write out prefs: " + ex);
292  }
293  return account;
294}
295/* eslint-enable complexity */
296
297function setFolders(identity, server) {
298  // TODO: support for local folders for global inbox (or use smart search
299  // folder instead)
300
301  var baseURI = server.serverURI + "/";
302
303  // Names will be localized in UI, not in folder names on server/disk
304  // TODO allow to override these names in the XML config file,
305  // in case e.g. Google or AOL use different names?
306  // Workaround: Let user fix it :)
307  var fccName = "Sent";
308  var draftName = "Drafts";
309  var templatesName = "Templates";
310
311  identity.draftFolder = baseURI + draftName;
312  identity.stationeryFolder = baseURI + templatesName;
313  identity.fccFolder = baseURI + fccName;
314
315  identity.fccFolderPickerMode = 0;
316  identity.draftsFolderPickerMode = 0;
317  identity.tmplFolderPickerMode = 0;
318}
319
320function rememberPassword(server, password) {
321  let passwordURI;
322  if (server instanceof Ci.nsIMsgIncomingServer) {
323    passwordURI = server.localStoreType + "://" + server.hostName;
324  } else if (server instanceof Ci.nsISmtpServer) {
325    passwordURI = "smtp://" + server.hostname;
326  } else {
327    throw new AccountCreationUtils.NotReached("Server type not supported");
328  }
329
330  let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
331    Ci.nsILoginInfo
332  );
333  login.init(passwordURI, null, passwordURI, server.username, password, "", "");
334  try {
335    Services.logins.addLogin(login);
336  } catch (e) {
337    if (e.message.includes("This login already exists")) {
338      // TODO modify
339    } else {
340      throw e;
341    }
342  }
343}
344
345/**
346 * Check whether the user's setup already has an incoming server
347 * which matches (hostname, port, username) the primary one
348 * in the config.
349 * (We also check the email address as username.)
350 *
351 * @param config {AccountConfig} filled in (no placeholders)
352 * @return {nsIMsgIncomingServer} If it already exists, the server
353 *     object is returned.
354 *     If it's a new server, |null| is returned.
355 */
356function checkIncomingServerAlreadyExists(config) {
357  AccountCreationUtils.assert(config instanceof AccountConfig);
358  let incoming = config.incoming;
359  let existing = MailServices.accounts.findRealServer(
360    incoming.username,
361    incoming.hostname,
362    incoming.type,
363    incoming.port
364  );
365
366  // if username does not have an '@', also check the e-mail
367  // address form of the name.
368  if (!existing && !incoming.username.includes("@")) {
369    existing = MailServices.accounts.findRealServer(
370      config.identity.emailAddress,
371      incoming.hostname,
372      incoming.type,
373      incoming.port
374    );
375  }
376  return existing;
377}
378
379/**
380 * Check whether the user's setup already has an outgoing server
381 * which matches (hostname, port, username) the primary one
382 * in the config.
383 *
384 * @param config {AccountConfig} filled in (no placeholders)
385 * @return {nsISmtpServer} If it already exists, the server
386 *     object is returned.
387 *     If it's a new server, |null| is returned.
388 */
389function checkOutgoingServerAlreadyExists(config) {
390  AccountCreationUtils.assert(config instanceof AccountConfig);
391  for (let existingServer of MailServices.smtp.servers) {
392    // TODO check username with full email address, too, like for incoming
393    if (
394      existingServer.hostname == config.outgoing.hostname &&
395      existingServer.port == config.outgoing.port &&
396      existingServer.username == config.outgoing.username
397    ) {
398      return existingServer;
399    }
400  }
401  return null;
402}
403
404/**
405 * Check whether the user's setup already has an account with the same email
406 * address. This might happen if the user uses the same email for different
407 * protocols (eg. IMAP and POP3).
408 *
409 * @param {string} name - The name or email address of the new account.
410 * @returns {boolean} True if an account with the same name is found.
411 */
412function checkAccountNameAlreadyExists(name) {
413  return MailServices.accounts.accounts.some(
414    a => a.incomingServer.prettyName == name
415  );
416}
417
418/**
419 * Generate a unique account name by appending the incoming protocol type, and
420 * a counter if necessary.
421 *
422 * @param {AccountConfig} config - The config data of the account being created.
423 * @returns {string} - The unique account name.
424 */
425function generateUniqueAccountName(config) {
426  // Generate a potential unique name. e.g. "foo@bar.com (POP3)".
427  let name = `${
428    config.identity.emailAddress
429  } (${config.incoming.type.toUpperCase()})`;
430
431  // If this name already exists, append a counter until we find a unique name.
432  if (checkAccountNameAlreadyExists(name)) {
433    let counter = 2;
434    while (checkAccountNameAlreadyExists(`${name}_${counter}`)) {
435      counter++;
436    }
437    // e.g. "foo@bar.com (POP3)_1".
438    name = `${name}_${counter}`;
439  }
440
441  return name;
442}
443
444/**
445 * Check if there already is a "Local Folders". If not, create it.
446 * Copied from AccountWizard.js with minor updates.
447 */
448function verifyLocalFoldersAccount(am) {
449  let localMailServer;
450  try {
451    localMailServer = am.localFoldersServer;
452  } catch (ex) {
453    localMailServer = null;
454  }
455
456  try {
457    if (!localMailServer) {
458      // creates a copy of the identity you pass in
459      am.createLocalMailAccount();
460      try {
461        localMailServer = am.localFoldersServer;
462      } catch (ex) {
463        AccountCreationUtils.ddump(
464          "Error! we should have found the local mail server " +
465            "after we created it."
466        );
467      }
468    }
469  } catch (ex) {
470    AccountCreationUtils.ddump("Error in verifyLocalFoldersAccount " + ex);
471  }
472}
473
474var CreateInBackend = {
475  checkIncomingServerAlreadyExists,
476  checkOutgoingServerAlreadyExists,
477  createAccountInBackend,
478};
479