diff options
25 files changed, 1919 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee34263 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +dot-thunderbird/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7d23b14 --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +EXTNAME  := bitmask-thunderbird +PREFIX   := . +FILES_TO_PACKAGE := chrome,chrome.manifest,install.rdf +RSA_FILE := META-INF/zigbert.rsa + +# the following variables are updated automatically +COMMIT   := $(shell git --no-pager log -1 --format=format:%h) +VERSION  := $(shell grep \<em:version\> $(PREFIX)/install.rdf | sed -e s/[^\>]\\\+\>// -e s/\<[^\>]\\\+\>//) +PKGNAME  := $(EXTNAME)-$(VERSION)-$(COMMIT).xpi +TARGET   := $(CURDIR)/build/$(PKGNAME) +TEMPDIR  := $(shell mktemp -d -u) + +# make sure CERTDIR and CERTNAME are defined for signing +USAGE    := "Usage: make CERTDIR=<certificate directory> CERTNAME=<certificate name> DEFAULTKEY=<key id>" +ifeq ($(MAKECMDGOALS),signed) +ifndef CERTDIR +  $(error $(USAGE)) +endif +ifndef CERTNAME +  $(error $(USAGE)) +endif +ifndef DEFAULTKEY +  $(error $(USAGE)) +endif +endif + +# make sure DEFAULTKEY was given to sign the calculated hashes +ifneq ($(MAKECMDGOALS),clean) +ifneq ($(MAKECMDGOALS),upload) +ifndef DEFAULTKEY +  $(error "Usage: make DEFAULTKEY=<key id>") +endif +endif +endif + +# main rule +all: clean $(TARGET) + +# main target: .xpi file + +$(TARGET): clean +	mkdir -p $(TEMPDIR) +	cp -r $(PREFIX)/{$(FILES_TO_PACKAGE)} $(TEMPDIR)/ +	(cd $(TEMPDIR) && zip -r $(TARGET) ./) +	rm -rf $(TEMPDIR) +	(cd build/ && sha512sum $(PKGNAME) > SHA512SUMS && gpg --default-key $(DEFAULTKEY) --sign SHA512SUMS) + +signed: clean +	mkdir -p $(TEMPDIR) +	cp -r $(PREFIX)/{$(FILES_TO_PACKAGE)} $(TEMPDIR)/ +	signtool -d $(CERTDIR) -k $(CERTNAME) $(TEMPDIR)/ +	(cd $(TEMPDIR) && zip $(TARGET) ./$(RSA_FILE) && zip -r -D $(TARGET) ./ -x ./$(RSA_FILE)) +	rm -rf $(TEMPDIR) +	(cd build/ && sha512sum $(PKGNAME) > SHA512SUMS && gpg --default-key $(DEFAULTKEY) --sign SHA512SUMS) + +clean: +	rm -f $(TARGET) build/* + +upload: +	scp build/* downloads.leap.se:~/public/thunderbird_extension/ + +.PHONY: all clean signed diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..be00062 --- /dev/null +++ b/README.rst @@ -0,0 +1,61 @@ +Bitmask Thunderbird Extension +============================= + +The Bitmask Thunderbird Extension provides: + +* A wizard for creating email accounts with IMAP/SMTP configuration targeted +  to localhost and the default Bitmask client ports. There are different ways to +  launch the wizard for configuring a Bitmask Account: +  - Clicking on the statusbar notification. +  - File -> New -> Bitmask account. +  - Edit -> Account Settings... -> Account Actions -> Add Bitmask Accont. +* Caching prevention: accounts are created with caching turned off and the +  UI is modified to prevent users from turning on caching for these +  accounts. + +Development/testing +------------------- + +For development/testing purposes you can create a text file in Thunderbird's +extension directory whose contents point to the repository dir: + +* The file must be created in the `~/.thunderbird/<profile>/extensions/` +  directory. +* The file name must be `bitmask-thunderbird@leap.se`. +* The file contents must be the path for the `src/` directory inside this +  repository. + +XPI Package +----------- + +To generate an unsigned XPI package, type the following inside the root of the +repository: + +  make DEFAULTKEY=<key id> + +This command will: + +* Generate a `.xpi` file inside the `build/` directory. +* Create a `build/SHA512SUMS` file containing the sha512 hash of the `.xpi` file. +* Sign that file with the given `DEFAULTKEY` and create a `build/SHA512SUMS.gpg` file. + +You can now use the generated `.xpi` file install the package as a normal +Thunderbird extension. + +Signed XPI package +------------------ + +To generate a signed XPI package you must first have a certificate and then do +the following: + +  make sign CERTDIR=<path to cert dir> CERTNAME=<cert name> DEFAULTKEY=<key id> + +This command will: + +* Generate a signed `.xpi` file inside the `build/` directory using the +  `CERTNAME` certificate contained in `CERTDIR`. +* Create a `build/SHA512SUMS` file containing the sha512 hash of the `.xpi` file. +* Sign that file with the given `DEFAULTKEY` and create a `build/SHA512SUMS.gpg` file. + +For more information about signed `.xpi` files, see: +https://developer.mozilla.org/en-US/docs/Signing_a_XPI diff --git a/changes/feature_3542-create-thunderbird-extension b/changes/feature_3542-create-thunderbird-extension new file mode 100644 index 0000000..1055474 --- /dev/null +++ b/changes/feature_3542-create-thunderbird-extension @@ -0,0 +1,2 @@ +  o Create a Thunderbird extension with a wizard for creating LEAP's Bitmask +    client-compliant accounts and with caching prevention. Closes #3542. diff --git a/chrome.manifest b/chrome.manifest new file mode 100644 index 0000000..bb14689 --- /dev/null +++ b/chrome.manifest @@ -0,0 +1,7 @@ +content bitmask                    chrome/content/ +skin    bitmask    classic/1.0     chrome/skin/ +locale  bitmask    en-US           chrome/locale/en-US/ +overlay chrome://messenger/content/messenger.xul chrome://bitmask/content/statusBar/statusBarOverlay.xul +overlay chrome://messenger/content/messenger.xul chrome://bitmask/content/accountWizard/bitmaskMessengerOverlay.xul +overlay chrome://messenger/content/AccountManager.xul chrome://bitmask/content/accountWizard/bitmaskAccountManagerOverlay.xul +overlay chrome://messenger/content/am-offline.xul chrome://bitmask/content/preventCaching/bitmaskAmOfflineOverlay.xul diff --git a/chrome/content/.gitignore b/chrome/content/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/chrome/content/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/chrome/content/accountWizard/accountWizard.js b/chrome/content/accountWizard/accountWizard.js new file mode 100644 index 0000000..b2b533f --- /dev/null +++ b/chrome/content/accountWizard/accountWizard.js @@ -0,0 +1,720 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/hostnameUtils.jsm"); + +/** + * This is the dialog opened by menu File | New account | Mail... . + * + * It gets the user's realname, email address and password, + * and automatically configures the LEAP account from that. + * + * Steps: + * - User enters realname, email address and password + * - verify the setup, by trying to login to the configured servers + * - let user verify and maybe edit the server names and ports + * - If user clicks OK, create the account + */ + + +// from http://xyfer.blogspot.com/2005/01/javascript-regexp-email-validator.html +var emailRE = /^[-_a-z0-9\'+*$^&%=~!?{}]+(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*@(?:[-a-z0-9.]+\.[a-z]{2,6}|\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/i; + +Cu.import("resource:///modules/gloda/log4moz.js"); +let gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard"); + +var gStringsBundle; +var gMessengerBundle; +var gBrandShortName; + +function e(elementID) +{ +  return document.getElementById(elementID); +}; + +function _hide(id) +{ +  e(id).hidden = true; +} + +function _show(id) +{ +  e(id).hidden = false; +} + +function _enable(id) +{ +  e(id).disabled = false; +} + +function _disable(id) +{ +  e(id).disabled = true; +} + +function setText(id, value) +{ +  var element = e(id); +  assert(element, "setText() on non-existant element ID"); + +  if (element.localName == "textbox" || element.localName == "label") { +    element.value = value; +  } else if (element.localName == "description") { +    element.textContent = value; +  } else { +    throw new NotReached("XUL element type not supported"); +  } +} + +function BitmaskAccountWizard() +{ +  this._init(); +} +BitmaskAccountWizard.prototype = +{ +  _init : function BitmaskAccountWizard__init() +  { +    gEmailWizardLogger.info("Initializing setup wizard"); +    this._abortable = null; +  }, + +  onLoad : function() +  { +    /** +     * this._currentConfig is the config we got either from the XML file or +     * from guessing or from the user. Unless it's from the user, it contains +     * placeholders like %EMAILLOCALPART% in username and other fields. +     * +     * The config here must retain these placeholders, to be able to +     * adapt when the user enters a different realname, or password or +     * email local part. (A change of the domain name will trigger a new +     * detection anyways.) +     * That means, before you actually use the config (e.g. to create an +     * account or to show it to the user), you need to run replaceVariables(). +     */ +    this._currentConfig = null; + +    let userFullname; +    try { +      let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo); +      userFullname = userInfo.fullname; +    } catch(e) { +      // nsIUserInfo may not be implemented on all platforms, and name might +      // not be avaialble even if it is. +    } + +    this._domain = ""; +    this._email = ""; +    this._realname = (userFullname) ? userFullname : ""; +    e("realname").value = this._realname; +    this._password = ""; +    this._okCallback = null; + +    if (window.arguments && window.arguments[0]) { +      if (window.arguments[0].msgWindow) { +        this._parentMsgWindow = window.arguments[0].msgWindow; +      } +      if (window.arguments[0].okCallback) { +        this._okCallback = window.arguments[0].okCallback; +      } +    } + +    gEmailWizardLogger.info("Email account setup dialog loaded."); + +    gStringsBundle = e("strings"); +    gMessengerBundle = e("bundle_messenger"); +    gBrandShortName = e("bundle_brand").getString("brandShortName"); + +    // admin-locked prefs hurray +    if (!Services.prefs.getBoolPref("signon.rememberSignons")) { +      let rememberPasswordE = e("remember_password"); +      rememberPasswordE.checked = false; +      rememberPasswordE.disabled = true; +    } + +    // First, unhide the main window areas, and store the width, +    // so that we don't resize wildly when we unhide areas. +    // switchToMode() will then hide the unneeded parts again. +    // We will add some leeway of 10px, in case some of the <description>s wrap, +    // e.g. outgoing username != incoming username. +    _show("status_area"); +    _show("result_area"); +    window.sizeToContent(); +    e("mastervbox").setAttribute("style", +        "min-width: " + document.width + "px; " + +        "min-height: " + (document.height + 10) + "px;"); + +    this.switchToMode("start"); +    e("realname").focus(); +  }, + +  /** +   * Changes the window configuration to the different modes we have. +   * Shows/hides various window parts and buttons. +   * @param modename {String-enum} +   *    "start" : Just the realname, email address, password fields +   *    "result" : We found a config and display it to the user. +   *       The user may create the account. +   */ +  switchToMode : function(modename) +  { +    if (modename == this._currentModename) { +      return; +    } +    this._currentModename = modename; +    gEmailWizardLogger.info("switching to UI mode " + modename) + +    //_show("initialSettings"); always visible +    //_show("cancel_button"); always visible +    if (modename == "start") { +      _hide("status_area"); +      _hide("result_area"); + +      _show("next_button"); +      _disable("next_button"); // will be enabled by code +      _hide("create_button"); +    } else if (modename == "result") { +      _show("status_area"); +      _show("result_area"); + +      _hide("next_button"); +      _show("create_button"); +      _enable("create_button"); +    } else { +      throw new NotReached("unknown mode"); +    } +    window.sizeToContent(); +  }, + +  /** +   * Start from beginning with possibly new email address. +   */ +  onStartOver : function() +  { +    if (this._abortable) { +      this.onStop(); +    } +    this.switchToMode("start"); +  }, + +  getConcreteConfig : function() +  { +    var result = this._currentConfig.copy(); +    replaceVariables(result, this._realname, this._email, this._password); +    result.rememberPassword = e("remember_password").checked && +                              !!this._password; +    return result; +  }, + +  /* +   * This checks if the email address is at least possibly valid, meaning it +   * has an '@' before the last char. +   */ +  validateEmailMinimally : function(emailAddr) +  { +    let atPos = emailAddr.lastIndexOf("@"); +    return atPos > 0 && atPos + 1 < emailAddr.length; +  }, + +  /* +   * This checks if the email address is syntactically valid, +   * as far as we can determine. We try hard to make full checks. +   * +   * OTOH, we have a very small chance of false negatives, +   * because the RFC822 address spec is insanely complicated, +   * but rarely needed, so when this here fails, we show an error message, +   * but don't stop the user from continuing. +   * In contrast, if validateEmailMinimally() fails, we stop the user. +   */ +  validateEmail : function(emailAddr) +  { +    return emailRE.test(emailAddr); +  }, + +  /** +   * onInputEmail and onInputRealname are called on input = keypresses, and +   * enable/disable the next button based on whether there's a semi-proper +   * e-mail address and non-blank realname to start with. +   * +   * A change to the email address also automatically restarts the +   * whole process. +   */ +  onInputEmail : function() +  { +    this._email = e("email").value; +    this.onStartOver(); +    this.checkStartDone(); +  }, +  onInputRealname : function() +  { +    this._realname = e("realname").value; +    this.checkStartDone(); +  }, + +  onInputPassword : function() +  { +    this._password = e("password").value; +  }, + +  /** +   * This does very little other than to check that a name was entered at all +   * Since this is such an insignificant test we should be using a very light +   * or even jovial warning. +   */ +  onBlurRealname : function() +  { +    let realnameEl = e("realname"); +    if (this._realname) { +      this.clearError("nameerror"); +      _show("nametext"); +      realnameEl.removeAttribute("error"); +    // bug 638790: don't show realname error until user enter an email address +    } else if (this.validateEmailMinimally(this._email)) { +      _hide("nametext"); +      this.setError("nameerror", "please_enter_name"); +      realnameEl.setAttribute("error", "true"); +    } +  }, + +  /** +   * This check is only done as an informative warning. +   * We don't want to block the person, if they've entered an email address +   * that doesn't conform to our regex. +   */ +  onBlurEmail : function() +  { +    if (!this._email) { +      return; +    } +    var emailEl = e("email"); +    if (this.validateEmail(this._email)) { +      this.clearError("emailerror"); +      emailEl.removeAttribute("error"); +      this.onBlurRealname(); +    } else { +      this.setError("emailerror", "double_check_email"); +      emailEl.setAttribute("error", "true"); +    } +  }, + +  /** +   * If the user just tabbed through the password input without entering +   * anything, set the type back to text so we don't wind up showing the +   * emptytext as bullet characters. +   */ +  onBlurPassword : function() +  { +    if (!this._password) { +      e("password").type = "text"; +    } +  }, + +  /** +   * @see onBlurPassword() +   */ +  onFocusPassword : function() +  { +    e("password").type = "password"; +  }, + +  /** +   * Check whether the user entered the minimum of information +   * needed to leave the "start" mode (entering of name, email, pw) +   * and is allowed to proceed to detection step. +   */ +  checkStartDone : function() +  { +    if (this.validateEmailMinimally(this._email) && +        this._realname) { +      this._domain = this._email.split("@")[1].toLowerCase(); +      _enable("next_button"); +    } else { +      _disable("next_button"); +    } +  }, + +  /** +   * When the [Continue] button is clicked, we move from the initial account +   * information stage to using that information to configure account details. +   */ +  onNext : function() +  { +    this.fillConfig(this._domain, this._email); +  }, + +  fillConfig : function(domain, email) +  { +    var config = new AccountConfig(); +    this._prefillConfig(config); +    config.source = AccountConfig.kSourceXML;  // TODO: change kSource type? +    config.incoming.hostname = IMAP_HOST; +    config.incoming.username = config.identity.emailAddress; +    config.outgoing.username = config.identity.emailAddress; +    config.incoming.type = "imap"; +    config.incoming.port = IMAP_PORT; +    // Values for socketType are: +    //   1 - plain +    //   2 - SSL / TLS +    //   3 - STARTTLS +    config.incoming.socketType = 1; +    config.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext; +    config.outgoing.hostname = SMTP_HOST; +    config.outgoing.socketType = 1; +    config.outgoing.port = SMTP_PORT; +    config.outgoing.auth = Ci.nsMsgAuthMethod.passwordCleartext; +    this.foundConfig(config); +  }, + +  /** +   * When findConfig() was successful, it calls this. +   * This displays the config to the user. +   */ +  foundConfig : function(config) +  { +    gEmailWizardLogger.info("foundConfig()"); +    assert(config instanceof AccountConfig, +        "BUG: Arg 'config' needs to be an AccountConfig object"); + +    this._haveValidConfigForDomain = this._email.split("@")[1];; + +    if (!this._realname || !this._email) { +      return; +    } +    this._foundConfig2(config); +  }, + +  // Continuation of foundConfig2() after custom fields. +  _foundConfig2 : function(config) +  { +    this.displayConfigResult(config); +  }, + + + + +  /////////////////////////////////////////////////////////////////// +  // status area + +  startSpinner : function(actionStrName) +  { +    e("status_area").setAttribute("status", "loading"); +    gEmailWizardLogger.warn("spinner start " + actionStrName); +    this._showStatusTitle(actionStrName); +  }, + +  stopSpinner : function(actionStrName) +  { +    e("status_area").setAttribute("status", "result"); +    _hide("stop_button"); +    this._showStatusTitle(actionStrName); +    gEmailWizardLogger.warn("all spinner stop " + actionStrName); +  }, + +  showErrorStatus : function(actionStrName) +  { +    e("status_area").setAttribute("status", "error"); +    gEmailWizardLogger.warn("status error " + actionStrName); +    this._showStatusTitle(actionStrName); +  }, + +  _showStatusTitle : function(msgName) +  { +    let msg = " "; // assure height. Do via min-height in CSS, for 2 lines? +    try { +      if (msgName) { +        msg = gStringsBundle.getFormattedString(msgName, [gBrandShortName]); +      } +    } catch(ex) { +      gEmailWizardLogger.error("missing string for " + msgName); +      msg = msgName + " (missing string in translation!)"; +    } + +    e("status_msg").textContent = msg; +    gEmailWizardLogger.info("status msg: " + msg); +  }, + + + +  ///////////////////////////////////////////////////////////////// +  // Result area + +  /** +   * Displays a (probed) config to the user, +   * in the result config details area. +   * +   * @param config {AccountConfig} The config to present to user +   */ +  displayConfigResult : function(config) +  { +    assert(config instanceof AccountConfig); +    this._currentConfig = config; +    var configFilledIn = this.getConcreteConfig(); + +    var unknownString = gStringsBundle.getString("resultUnknown"); + +    function _makeHostDisplayString(server, stringName) +    { +      let type = gStringsBundle.getString(sanitize.translate(server.type, +          { imap : "resultIMAP", pop3 : "resultPOP3", smtp : "resultSMTP" }), +          unknownString); +      let host = server.hostname + +          (isStandardPort(server.port) ? "" : ":" + server.port); +      let ssl = gStringsBundle.getString(sanitize.translate(server.socketType, +          { 1 : "resultNoEncryption", 2 : "resultSSL", 3 : "resultSTARTTLS" }), +          unknownString); +      let certStatus = gStringsBundle.getString(server.badCert ? +          "resultSSLCertWeak" : "resultSSLCertOK"); +      return gStringsBundle.getFormattedString(stringName, +          [ type, host, ssl, certStatus ]); +    }; + +    var incomingResult = unknownString; +    if (configFilledIn.incoming.hostname) { +      incomingResult = _makeHostDisplayString(configFilledIn.incoming, +          "resultIncoming"); +    } + +    var outgoingResult = unknownString; +    if (!config.outgoing.existingServerKey) { +      if (configFilledIn.outgoing.hostname) { +        outgoingResult = _makeHostDisplayString(configFilledIn.outgoing, +            "resultOutgoing"); +      } +    } else { +      outgoingResult = gStringsBundle.getString("resultOutgoingExisting"); +    } + +    var usernameResult; +    if (configFilledIn.incoming.username == configFilledIn.outgoing.username) { +      usernameResult = gStringsBundle.getFormattedString("resultUsernameBoth", +            [ configFilledIn.incoming.username || unknownString ]); +    } else { +      usernameResult = gStringsBundle.getFormattedString( +            "resultUsernameDifferent", +            [ configFilledIn.incoming.username || unknownString, +              configFilledIn.outgoing.username || unknownString ]); +    } + +    setText("result-incoming", incomingResult); +    setText("result-outgoing", outgoingResult); +    setText("result-username", usernameResult); + +    gEmailWizardLogger.info(debugObject(config, "config")); +    // IMAP / POP dropdown +    var lookForAltType = +        config.incoming.type == "imap" ? "pop3" : "imap"; +    var alternative = null; +    for (let i = 0; i < config.incomingAlternatives.length; i++) { +      let alt = config.incomingAlternatives[i]; +      if (alt.type == lookForAltType) { +        alternative = alt; +        break; +      } +    } +    if (alternative) { +      _show("result_imappop"); +      e("result_select_" + alternative.type).configIncoming = alternative; +      e("result_select_" + config.incoming.type).configIncoming = +          config.incoming; +      e("result_imappop").value = +          config.incoming.type == "imap" ? 1 : 2; +    } else { +      _hide("result_imappop"); +    } + +    this.switchToMode("result"); +  }, + +  onInputUsername : function() +  { +    this.onChangedManualEdit(); +  }, +  onInputHostname : function() +  { +    this.onChangedManualEdit(); +  }, + + + +  ///////////////////////////////////////////////////////////////// +  // UI helper functions + +  _prefillConfig : function(initialConfig) +  { +    var emailsplit = this._email.split("@"); +    assert(emailsplit.length > 1); +    var emaillocal = sanitize.nonemptystring(emailsplit[0]); +    initialConfig.incoming.username = emaillocal; +    initialConfig.outgoing.username = SMTP_USER; +    return initialConfig; +  }, + +  clearError : function(which) +  { +    _hide(which); +    _hide(which + "icon"); +    e(which).textContent = ""; +  }, + +  setError : function(which, msg_name) +  { +    try { +      _show(which); +      _show(which + "icon"); +      e(which).textContent = gStringsBundle.getString(msg_name); +      window.sizeToContent(); +    } catch (ex) { alertPrompt("missing error string", msg_name); } +  }, + + + +  ///////////////////////////////////////////////////////////////// +  // Finish & dialog close functions + +  onKeyDown : function(event) +  { +    let key = event.keyCode; +    if (key == 27) { // Escape key +      this.onCancel(); +      return true; +    } +    if (key == 13) { // OK key +      let buttons = [ +        { id: "next_button", action: makeCallback(this, this.onNext) }, +        { id: "create_button", action: makeCallback(this, this.onCreate) }, +      ]; +      for each (let button in buttons) { +        button.e = e(button.id); +        if (button.e.hidden || button.e.disabled) { +          continue; +        } +        button.action(); +        return true; +      } +    } +    return false; +  }, + +  onCancel : function() +  { +    window.close(); +    // The window onclose handler will call onWizardShutdown for us. +  }, + +  onWizardShutdown : function() +  { +    if (this._abortable) { +      this._abortable.cancel(new UserCancelledException()); +    } + +    if (this._okCallback) { +      this._okCallback(); +    } +    gEmailWizardLogger.info("Shutting down email config dialog"); +  }, + + +  onCreate : function() +  { +    try { +      gEmailWizardLogger.info("Create button clicked"); + +      this.validateAndFinish(); +    } catch (ex) { +      gEmailWizardLogger.error("Error creating account.  ex=" + ex + +                               ", stack=" + ex.stack); +      alertPrompt(gStringsBundle.getString("error_creating_account"), ex); +    } +  }, + +  // called by onCreate() +  validateAndFinish : function() +  { +    var configFilledIn = this.getConcreteConfig(); + +    if (checkIncomingServerAlreadyExists(configFilledIn)) { +      alertPrompt(gStringsBundle.getString("error_creating_account"), +                  gStringsBundle.getString("incoming_server_exists")); +      return; +    } + +    if (configFilledIn.outgoing.addThisServer) { +      let existingServer = checkOutgoingServerAlreadyExists(configFilledIn); +      if (existingServer) { +        configFilledIn.outgoing.addThisServer = false; +        configFilledIn.outgoing.existingServerKey = existingServer.key; +      } +    } + +    // TODO use a UI mode (switchToMode()) for verfication, too. +    // But we need to go back to the previous mode, because we might be in +    // "result" or "manual-edit-complete" mode. +    _disable("create_button"); +    // no stop button: backend has no ability to stop :-( +    var self = this; +    this.startSpinner("checking_password"); +    // logic function defined in verifyConfig.js +    verifyConfig( +      configFilledIn, +      // guess login config? +      configFilledIn.source != AccountConfig.kSourceXML, +      // TODO Instead, the following line would be correct, but I cannot use it, +      // because some other code doesn't adhere to the expectations/specs. +      // Find out what it was and fix it. +      //concreteConfig.source == AccountConfig.kSourceGuess, +      this._parentMsgWindow, +      function(successfulConfig) // success +      { +        self.stopSpinner(successfulConfig.incoming.password ? +                         "password_ok" : null); + +        // the auth might have changed, so we +        // should back-port it to the current config. +        self._currentConfig.incoming.auth = successfulConfig.incoming.auth; +        self._currentConfig.outgoing.auth = successfulConfig.outgoing.auth; +        self._currentConfig.incoming.username = successfulConfig.incoming.username; +        self._currentConfig.outgoing.username = successfulConfig.outgoing.username; +        self.finish(); +      }, +      function(e) // failed +      { +        self.showErrorStatus("config_unverifiable"); +        // TODO bug 555448: wrong error msg, there may be a 1000 other +        // reasons why this failed, and this is misleading users. +        self.setError("passworderror", "user_pass_invalid"); +        // TODO use switchToMode(), see above +        // give user something to proceed after fixing +        _enable("create_button"); +      }); +  }, + +  finish : function() +  { +    gEmailWizardLogger.info("creating account in backend"); +    createAccountInBackend(this.getConcreteConfig()); +    window.close(); +  }, +}; + +var gBitmaskAccountWizard = new BitmaskAccountWizard(); + +var _gStandardPorts = {}; +_gStandardPorts["imap"] = [ 143, 993 ]; +_gStandardPorts["pop3"] = [ 110, 995 ]; +_gStandardPorts["smtp"] = [ 587, 25, 465 ]; // order matters +var _gAllStandardPorts = _gStandardPorts["smtp"] +    .concat(_gStandardPorts["imap"]).concat(_gStandardPorts["pop3"]); + +function isStandardPort(port) +{ +  return _gAllStandardPorts.indexOf(port) != -1; +} + +function getStandardPorts(protocolType) +{ +  return _gStandardPorts[protocolType]; +} diff --git a/chrome/content/accountWizard/accountWizard.xul b/chrome/content/accountWizard/accountWizard.xul new file mode 100644 index 0000000..0d42c0d --- /dev/null +++ b/chrome/content/accountWizard/accountWizard.xul @@ -0,0 +1,223 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public +   - License, v. 2.0. If a copy of the MPL was not distributed with this +   - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/accountCreation.css" +                                              type="text/css"?> +<?xml-stylesheet href="chrome://bitmask/skin/accountWizard.css" +                                              type="text/css"?> + +<!DOCTYPE window [ +  <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +  %brandDTD; +  <!ENTITY % acDTD SYSTEM "chrome://messenger/locale/accountCreation.dtd"> +  %acDTD; +  <!ENTITY % awDTD SYSTEM "chrome://bitmask/locale/accountWizard.dtd"> +  %awDTD; +]> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" +        id="autoconfigWizard" +        windowtype="mail:autoconfig" +        title="&autoconfigWizard.title;" +        onload="gBitmaskAccountWizard.onLoad();" +        onkeypress="gBitmaskAccountWizard.onKeyDown(event)" +        onclose="gBitmaskAccountWizard.onWizardShutdown();" +        onunload="gBitmaskAccountWizard.onWizardShutdown();" +        > + +  <stringbundleset> +    <stringbundle id="bundle_brand" +          src="chrome://branding/locale/brand.properties"/> +    <stringbundle id="strings" +          src="chrome://messenger/locale/accountCreation.properties"/> +    <stringbundle id="utilstrings" +          src="chrome://messenger/locale/accountCreationUtil.properties"/> +    <stringbundle id="bundle_messenger" +          src="chrome://messenger/locale/messenger.properties"/> +  </stringbundleset> +  <script type="application/javascript" +          src="chrome://bitmask/content/serverConfig.js"/> +  <script type="application/javascript" +          src="chrome://bitmask/content/util.js"/> +  <script type="application/javascript" +          src="chrome://messenger/content/accountcreation/accountConfig.js"/> +  <script type="application/javascript" +          src="chrome://bitmask/content/accountWizard/accountWizard.js"/> +  <script type="application/javascript" +          src="chrome://messenger/content/accountcreation/sanitizeDatatypes.js"/> +  <script type="application/javascript" +          src="chrome://messenger/content/accountcreation/verifyConfig.js"/> +  <script type="application/javascript" +          src="chrome://bitmask/content/accountWizard/createInBackend.js"/> +  <script type="application/javascript" +          src="chrome://messenger/content/accountUtils.js" /> + +  <keyset id="mailKeys"> +    <key keycode="VK_ESCAPE" oncommand="window.close();"/> +  </keyset> + +  <tooltip id="optional-password"> +    <description>&password.text;</description> +  </tooltip> + +  <spacer id="fullwidth"/> + +  <vbox id="mastervbox" class="mastervbox" flex="1"> +    <description +      id="BitmaskAccountWizardTitle" +      class="bitmaskAccountWizardTitle">&bitmask.accountWizard.title;</description> +    <spacer flex="1" /> +    <grid id="initialSettings"> +      <columns> +        <column/> +        <column/> +        <column/> +      </columns> +      <rows> +        <row align="center"> +          <label accesskey="&name.accesskey;" +                 class="autoconfigLabel" +                 value="&name.label;" +                 control="realname"/> +          <textbox id="realname" +                   class="padded" +                   placeholder="&name.placeholder;" +                   oninput="gBitmaskAccountWizard.onInputRealname();" +                   onblur="gBitmaskAccountWizard.onBlurRealname();"/> +          <hbox> +            <description id="nametext" class="initialDesc">&name.text;</description> +            <image id="nameerroricon" +                   hidden="true" +                   class="warningicon"/> +            <description id="nameerror" class="errordescription" hidden="true"/> +          </hbox> +        </row> +        <row align="center"> +          <label accesskey="&email.accesskey;" +                 class="autoconfigLabel" +                 value="&email.label;" +                 control="email"/> +          <textbox id="email" +                   class="padded uri-element" +                   placeholder="&email.placeholder;" +                   oninput="gBitmaskAccountWizard.onInputEmail();" +                   onblur="gBitmaskAccountWizard.onBlurEmail();"/> +          <hbox> +            <image id="emailerroricon" +                   hidden="true" +                   class="warningicon"/> +            <description id="emailerror" class="errordescription" hidden="true"/> +          </hbox> +        </row> +        <row align="center"> +          <!-- this starts out as text so the emptytext shows, but then +               changes to type=password once it's not empty --> +          <label accesskey="&password.accesskey;" +                 class="autoconfigLabel" +                 value="&password.label;" +                 control="password" +                 tooltip="optional-password"/> +          <textbox id="password" +                   class="padded" +                   placeholder="&password.placeholder;" +                   type="text" +                   oninput="gBitmaskAccountWizard.onInputPassword();" +                   onfocus="gBitmaskAccountWizard.onFocusPassword();" +                   onblur="gBitmaskAccountWizard.onBlurPassword();"/> +          <hbox> +            <image id="passworderroricon" +                   hidden="true" +                   class="warningicon"/> +            <description id="passworderror" class="errordescription" hidden="true"/> +          </hbox> +        </row> +        <row align="center" pack="start"> +          <label class="autoconfigLabel"/> +          <checkbox id="remember_password" +                    label="&rememberPassword.label;" +                    accesskey="&rememberPassword.accesskey;" +                    checked="true"/> +        </row> +      </rows> +    </grid> +    <spacer flex="1" /> + +     <hbox id="status_area" flex="1"> +      <vbox id="status_img_before" pack="start"/> +      <description id="status_msg"> </description> +              <!-- Include 160 = nbsp, to make the element occupy the +                   full height, for at least one line. With a normal space, +                   it does not have sufficient height. --> +      <vbox id="status_img_after" pack="start"/> +    </hbox> + +    <groupbox id="result_area" hidden="true"> +      <radiogroup id="result_imappop" orient="horizontal"> +        <radio id="result_select_imap" label="&imapLong.label;" value="1" +               oncommand="gBitmaskAccountWizard.onResultIMAPOrPOP3();"/> +        <radio id="result_select_pop3" label="&pop3Long.label;" value="2" +               oncommand="gBitmaskAccountWizard.onResultIMAPOrPOP3();"/> +      </radiogroup> +      <grid> +        <columns> +          <column/> +          <column flex="1"/> +        </columns> +        <rows> +          <row align="center"> +            <label class="textbox-label" value="&incoming.label;" +                   control="result-incoming"/> +            <textbox id="result-incoming" disabled="true" flex="1"/> +          </row> +          <row align="center"> +            <label class="textbox-label" value="&outgoing.label;" +                   control="result-outgoing"/> +            <textbox id="result-outgoing" disabled="true" flex="1"/> +          </row> +          <row align="center"> +            <label class="textbox-label" value="&username.label;" +                   control="result-username"/> +            <textbox id="result-username" disabled="true" flex="1"/> +          </row> +        </rows> +      </grid> +    </groupbox> + +    <spacer flex="1" /> +    <hbox id="buttons_area"> +      <spacer flex="1"/> +      <hbox id="right_buttons_area" align="center" pack="end"> +        <button id="stop_button" +                label="&stop.label;" +                accesskey="&stop.accesskey;" +                hidden="true" +                oncommand="gBitmaskAccountWizard.onStop();"/> +        <button id="cancel_button" +                label="&cancel.label;" +                accesskey="&cancel.accesskey;" +                oncommand="gBitmaskAccountWizard.onCancel();"/> +        <button id="half-manual-test_button" +                label="&half-manual-test.label;" +                accesskey="&half-manual-test.accesskey;" +                hidden="true" +                oncommand="gBitmaskAccountWizard.onHalfManualTest();"/> +        <button id="next_button" +                label="&continue.label;" +                accesskey="&continue.accesskey;" +                hidden="false" +                oncommand="gBitmaskAccountWizard.onNext();"/> +        <button id="create_button" +                label="&doneAccount.label;" +                accesskey="&doneAccount.accesskey;" +                class="important-button" +                hidden="true" +                oncommand="gBitmaskAccountWizard.onCreate();"/> +      </hbox> +    </hbox> +  </vbox> + + +</window> diff --git a/chrome/content/accountWizard/bitmaskAccountManagerOverlay.xul b/chrome/content/accountWizard/bitmaskAccountManagerOverlay.xul new file mode 100644 index 0000000..ba68b5f --- /dev/null +++ b/chrome/content/accountWizard/bitmaskAccountManagerOverlay.xul @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!DOCTYPE overlay SYSTEM "chrome://bitmask/locale/bitmaskAccountManagerOverlay.dtd"> +<overlay id="bitmaskAccountManagerOverlay"  +         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +  <script type="application/javascript" +          src="chrome://bitmask/content/accountWizard/launchAccountWizard.js"/> +  <menupopup id="accountActionsDropdown"> +    <menuitem id="accountActionsAddBitmaskAccount" +              label="&addBitmaskAccountButton.label;" +              accesskey="&addBitmaskAccountButton.accesskey;" +              insertbefore="accountActionsDropdownSep1" +              oncommand="launchAccountWizard();" /> +  </menupopup> +</overlay> diff --git a/chrome/content/accountWizard/bitmaskMessengerOverlay.xul b/chrome/content/accountWizard/bitmaskMessengerOverlay.xul new file mode 100644 index 0000000..a767022 --- /dev/null +++ b/chrome/content/accountWizard/bitmaskMessengerOverlay.xul @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!DOCTYPE overlay SYSTEM "chrome://bitmask/locale/bitmaskMessengerOverlay.dtd"> +<overlay id="bitmaskMessengerOverlay"  +         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +  <script type="application/javascript" +          src="chrome://bitmask/content/accountWizard/launchAccountWizard.js"/> +  <menupopup id="menu_NewPopup"> +    <menuitem id="newBitmaskAccountMenuItem" +              label="&newBitmaskAccountCmd.label;" +              accesskey="&newBitmaskAccountCmd.accesskey;" +              oncommand="launchAccountWizard();" +              insertbefore="newPopupMenuSeparator" /> +  </menupopup> +</overlay> diff --git a/chrome/content/accountWizard/createInBackend.js b/chrome/content/accountWizard/createInBackend.js new file mode 100644 index 0000000..f15b882 --- /dev/null +++ b/chrome/content/accountWizard/createInBackend.js @@ -0,0 +1,330 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Takes an |AccountConfig| JS object and creates that account in the + * Thunderbird backend (which also writes it to prefs). + * + * @param config {AccountConfig} The account to create + * + * @return - the account created. + */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/iteratorUtils.jsm"); + +function createAccountInBackend(config) +{ +  // incoming server +  let inServer = MailServices.accounts.createIncomingServer( +      config.incoming.username, +      config.incoming.hostname, +      sanitize.enum(config.incoming.type, ["pop3", "imap", "nntp"])); +  inServer.port = config.incoming.port; +  inServer.authMethod = config.incoming.auth; +  inServer.password = config.incoming.password; +  if (config.rememberPassword && config.incoming.password.length) +    rememberPassword(inServer, config.incoming.password); + +  // SSL +  if (config.incoming.socketType == 1) // plain +    inServer.socketType = Ci.nsMsgSocketType.plain; +  else if (config.incoming.socketType == 2) // SSL / TLS +    inServer.socketType = Ci.nsMsgSocketType.SSL; +  else if (config.incoming.socketType == 3) // STARTTLS +    inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; +  //inServer.prettyName = config.displayName; +  inServer.prettyName = config.identity.emailAddress; + +  inServer.doBiff = true; +  inServer.biffMinutes = config.incoming.checkInterval; +  const loginAtStartupPrefTemplate = +    "mail.server.%serverkey%.login_at_startup"; +  var loginAtStartupPref = +    loginAtStartupPrefTemplate.replace("%serverkey%", inServer.key); +  Services.prefs.setBoolPref(loginAtStartupPref, +                             config.incoming.loginAtStartup); +  if (config.incoming.type == "pop3") +  { +    const leaveOnServerPrefTemplate = +      "mail.server.%serverkey%.leave_on_server"; +    const daysToLeaveOnServerPrefTemplate = +      "mail.server.%serverkey%.num_days_to_leave_on_server"; +    const deleteFromServerPrefTemplate = +      "mail.server.%serverkey%.delete_mail_left_on_server"; +    const deleteByAgeFromServerPrefTemplate = +      "mail.server.%serverkey%.delete_by_age_from_server"; +    const downloadOnBiffPrefTemplate = +      "mail.server.%serverkey%.download_on_biff"; +    var leaveOnServerPref = +      leaveOnServerPrefTemplate.replace("%serverkey%", inServer.key); +    var ageFromServerPref = +      deleteByAgeFromServerPrefTemplate.replace("%serverkey%", inServer.key); +    var daysToLeaveOnServerPref = +      daysToLeaveOnServerPrefTemplate.replace("%serverkey%", inServer.key); +    var deleteFromServerPref = +      deleteFromServerPrefTemplate.replace("%serverkey%", inServer.key); +    let downloadOnBiffPref = +      downloadOnBiffPrefTemplate.replace("%serverkey%", inServer.key); +    Services.prefs.setBoolPref(leaveOnServerPref, +                               config.incoming.leaveMessagesOnServer); +    Services.prefs.setIntPref(daysToLeaveOnServerPref, +                              config.incoming.daysToLeaveMessagesOnServer); +    Services.prefs.setBoolPref(deleteFromServerPref, +                               config.incoming.deleteOnServerWhenLocalDelete); +    Services.prefs.setBoolPref(ageFromServerPref, +                               config.incoming.deleteByAgeFromServer); +    Services.prefs.setBoolPref(downloadOnBiffPref, +                               config.incoming.downloadOnBiff); +  } +  inServer.valid = true; + +  let username = config.outgoing.auth > 1 ? config.outgoing.username : null; +  let outServer = MailServices.smtp.findServer(username, config.outgoing.hostname); +  assert(config.outgoing.addThisServer || +         config.outgoing.useGlobalPreferredServer || +         config.outgoing.existingServerKey, +         "No SMTP server: inconsistent flags"); + +  if (config.outgoing.addThisServer && !outServer) +  { +    outServer = MailServices.smtp.createServer(); +    outServer.hostname = config.outgoing.hostname; +    outServer.port = config.outgoing.port; +    outServer.authMethod = config.outgoing.auth; +    if (config.outgoing.auth > 1) +    { +      outServer.username = username; +      outServer.password = config.incoming.password; +      if (config.rememberPassword && config.incoming.password.length) +        rememberPassword(outServer, config.incoming.password); +    } + +    if (config.outgoing.socketType == 1) // no SSL +      outServer.socketType = Ci.nsMsgSocketType.plain; +    else if (config.outgoing.socketType == 2) // SSL / TLS +      outServer.socketType = Ci.nsMsgSocketType.SSL; +    else if (config.outgoing.socketType == 3) // STARTTLS +      outServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + +    // API problem: <http://mxr.mozilla.org/seamonkey/source/mailnews/compose/public/nsISmtpServer.idl#93> +    outServer.description = config.displayName; +    if (config.password) +      outServer.password = config.outgoing.password; + +    // If this is the first SMTP server, set it as default +    if (!MailServices.smtp.defaultServer || +        !MailServices.smtp.defaultServer.hostname) +      MailServices.smtp.defaultServer = outServer; +  } + +  // identity +  // TODO accounts without identity? +  let identity = MailServices.accounts.createIdentity(); +  identity.fullName = config.identity.realname; +  identity.email = config.identity.emailAddress; + +  // for new accounts, default to replies being positioned above the quote +  // if a default account is defined already, take its settings instead +  if (config.incoming.type == "imap" || config.incoming.type == "pop3") +  { +    identity.replyOnTop = 1; +    // identity.sigBottom = false; // don't set this until Bug 218346 is fixed + +    if (MailServices.accounts.accounts.length && +        MailServices.accounts.defaultAccount) +    { +      let defAccount = MailServices.accounts.defaultAccount; +      let defIdentity = defAccount.defaultIdentity; +      if (defAccount.incomingServer.canBeDefaultServer && +          defIdentity && defIdentity.valid) +      { +        identity.replyOnTop = defIdentity.replyOnTop; +        identity.sigBottom = defIdentity.sigBottom; +      } +    } +  } + +  // due to accepted conventions, news accounts should default to plain text +  if (config.incoming.type == "nntp") +    identity.composeHtml = false; + +  identity.valid = true; + +  if (config.outgoing.existingServerKey) +    identity.smtpServerKey = config.outgoing.existingServerKey; +  else if (!config.outgoing.useGlobalPreferredServer) +    identity.smtpServerKey = outServer.key; + +  // account and hook up +  // Note: Setting incomingServer will cause the AccountManager to refresh +  // itself, which could be a problem if we came from it and we haven't set +  // the identity (see bug 521955), so make sure everything else on the +  // account is set up before you set the incomingServer. +  let account = MailServices.accounts.createAccount(); +  account.addIdentity(identity); +  account.incomingServer = inServer; +  if (inServer.canBeDefaultServer && (!MailServices.accounts.defaultAccount || +                                      !MailServices.accounts.defaultAccount +                                       .incomingServer.canBeDefaultServer)) +    MailServices.accounts.defaultAccount = account; + +  verifyLocalFoldersAccount(MailServices.accounts); +  setFolders(identity, inServer); +  doNotCache(inServer); + +  // save +  MailServices.accounts.saveAccountInfo(); +  try { +    Services.prefs.savePrefFile(null); +  } catch (ex) { +    ddump("Could not write out prefs: " + ex); +  } +  return account; +} + +function setFolders(identity, server) +{ +  // TODO: support for local folders for global inbox (or use smart search +  // folder instead) + +  var baseURI = server.serverURI + "/"; + +  // Names will be localized in UI, not in folder names on server/disk +  // TODO allow to override these names in the XML config file, +  // in case e.g. Google or AOL use different names? +  // Workaround: Let user fix it :) +  var fccName = "Sent"; +  var draftName = "Drafts"; +  var templatesName = "Templates"; + +  identity.draftFolder = baseURI + draftName; +  identity.stationeryFolder = baseURI + templatesName; +  identity.fccFolder = baseURI + fccName; + +  identity.fccFolderPickerMode = 0; +  identity.draftsFolderPickerMode = 0; +  identity.tmplFolderPickerMode = 0; +} + +function doNotCache(inServer) +{ +  // make sure account is marked to not download +  inServerQI = inServer.QueryInterface( +    Components.interfaces.nsIImapIncomingServer); +  inServerQI.offlineDownload = false; +  // and remove offline flag from all folders +  var allFolders = inServer.rootFolder.descendants; +  for (let folder in fixIterator(allFolders, Components.interfaces.nsIMsgFolder)) +    folder.clearFlag(Components.interfaces.nsMsgFolderFlags.Offline); +} + +function rememberPassword(server, password) +{ +  if (server instanceof Components.interfaces.nsIMsgIncomingServer) +    var passwordURI = server.localStoreType + "://" + server.hostName; +  else if (server instanceof Components.interfaces.nsISmtpServer) +    var passwordURI = "smtp://" + server.hostname; +  else +    throw new NotReached("Server type not supported"); + +  let login = Cc["@mozilla.org/login-manager/loginInfo;1"] +              .createInstance(Ci.nsILoginInfo); +  login.init(passwordURI, null, passwordURI, server.username, password, "", ""); +  try { +    Services.logins.addLogin(login); +  } catch (e if e.message.contains("This login already exists")) { +    // TODO modify +  } +} + +/** + * Check whether the user's setup already has an incoming server + * which matches (hostname, port, username) the primary one + * in the config. + * (We also check the email address as username.) + * + * @param config {AccountConfig} filled in (no placeholders) + * @return {nsIMsgIncomingServer} If it already exists, the server + *     object is returned. + *     If it's a new server, |null| is returned. + */ +function checkIncomingServerAlreadyExists(config) +{ +  assert(config instanceof AccountConfig); +  let incoming = config.incoming; +  let existing = MailServices.accounts.findRealServer(incoming.username, +        incoming.hostname, +        sanitize.enum(incoming.type, ["pop3", "imap", "nntp"]), +        incoming.port); + +  // if username does not have an '@', also check the e-mail +  // address form of the name. +  if (!existing && !incoming.username.contains("@")) +    existing = MailServices.accounts.findRealServer(config.identity.emailAddress, +          incoming.hostname, +          sanitize.enum(incoming.type, ["pop3", "imap", "nntp"]), +          incoming.port); +  return existing; +}; + +/** + * Check whether the user's setup already has an outgoing server + * which matches (hostname, port, username) the primary one + * in the config. + * + * @param config {AccountConfig} filled in (no placeholders) + * @return {nsISmtpServer} If it already exists, the server + *     object is returned. + *     If it's a new server, |null| is returned. + */ +function checkOutgoingServerAlreadyExists(config) +{ +  assert(config instanceof AccountConfig); +  let smtpServers = MailServices.smtp.servers; +  while (smtpServers.hasMoreElements()) +  { +    let existingServer = smtpServers.getNext() +        .QueryInterface(Ci.nsISmtpServer); +    // TODO check username with full email address, too, like for incoming +    if (existingServer.hostname == config.outgoing.hostname && +        existingServer.port == config.outgoing.port && +        existingServer.username == config.outgoing.username) +      return existingServer; +  } +  return null; +}; + +/** + * Check if there already is a "Local Folders". If not, create it. + * Copied from AccountWizard.js with minor updates. + */ +function verifyLocalFoldersAccount(am)  +{ +  let localMailServer; +  try { +    localMailServer = am.localFoldersServer; +  } +  catch (ex) { +    localMailServer = null; +  } + +  try { +    if (!localMailServer)  +    { +      // creates a copy of the identity you pass in +      am.createLocalMailAccount(); +      try { +        localMailServer = am.localFoldersServer; +      } +      catch (ex) { +        ddump("Error! we should have found the local mail server " + +              "after we created it."); +      } +    } +  } +  catch (ex) { ddump("Error in verifyLocalFoldersAccount " + ex); } +} diff --git a/chrome/content/accountWizard/launchAccountWizard.js b/chrome/content/accountWizard/launchAccountWizard.js new file mode 100644 index 0000000..bda743f --- /dev/null +++ b/chrome/content/accountWizard/launchAccountWizard.js @@ -0,0 +1,64 @@ +/** + * statusBar.js + * Copyright (C) 2013 LEAP + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along + */ + + +Components.utils.import("resource:///modules/mailServices.js"); + + +/***************************************************************************** + * Wizard functions. + ****************************************************************************/ + +/** + * Launch the wizard to configure a new LEAP account. + */ +function launchAccountWizard() +{ +  msgNewBitmaskMailAccount(MailServices.mailSession.topmostMsgWindow, null, null); +} + +/** + * Open the New Mail Account Wizard, or focus it if it's already open. + * + * @param msgWindow a msgWindow for us to use to verify the accounts. + * @param okCallback an optional callback for us to call back to if + *                   everything's okay. + * @param extraData an optional param that allows us to pass data in and + *                  out.  Used in the upcoming AccountProvisioner add-on. + * @see msgOpenAccountWizard below for the previous implementation. + */ +function msgNewBitmaskMailAccount(msgWindow, okCallback, extraData) +{ +  if (!msgWindow) +    throw new Error("msgNewBitmaskMailAccount must be given a msgWindow."); +  let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] +                     .getService() +                     .QueryInterface(Components.interfaces.nsIWindowMediator); +  let existingWindow = wm.getMostRecentWindow("mail:bitmaskautoconfig"); +  if (existingWindow) +    existingWindow.focus(); +  else +    // disabling modal for the time being, see 688273 REMOVEME +    window.openDialog("chrome://bitmask/content/accountWizard/accountWizard.xul", +                      "AccountSetup", "chrome,titlebar,centerscreen", +                      {msgWindow:msgWindow, +                       okCallback:function () { updatePanel(); }, +                       extraData:extraData}); + +} + diff --git a/chrome/content/preventCaching/bitmaskAmOfflineOverlay.js b/chrome/content/preventCaching/bitmaskAmOfflineOverlay.js new file mode 100644 index 0000000..c88b2ab --- /dev/null +++ b/chrome/content/preventCaching/bitmaskAmOfflineOverlay.js @@ -0,0 +1,67 @@ +/** + * preventCachingOverlay.js + * Copyright (C) 2013 LEAP + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along + */ + + +// The following variable will hold the current account being edited after +// onPreInit() is called. +var currentAccount = null; + + +// The following function disables UI items that would allow a user to turn +// on offline caching for a LEAP account. It acts on am-offline.xul items that +// can be accessed in Thunderbird by choosing: +// +//   Edit -> Account Settings... -> Synchronization and Storage. +function disableOfflineCaching() +{ +  var disabled; +  // for now, we consider a LEAP account every account whose incoming server +  // has a hostname equal to IMAP_HOST and port equal to IMAP_PORT. +  if (currentAccount.incomingServer.port == IMAP_PORT && +      currentAccount.incomingServer.hostName == IMAP_HOST) +    disabled = true; +  else +    disabled = false; +  // The "Keep messsages for this account on this computer" checkbox. +  document.getElementById("offline.folders").disabled = disabled; +  // The "Advanced..." button. +  document.getElementById("selectImapFoldersButton").disabled = disabled; +} + + +// Clone 'am-offline.js' onPreInit() so we can store the current account. +var oldOnPreInit = onPreInit.bind({}); + +// Store current account and call old onPreInit(). +var onPreInit = function(account, accountValues) +{ +  currentAccount = account; +  oldOnPreInit(account, accountValues); +} + + +// Clone 'am-offline.js' onInit() so we can disable offline caching. +var oldOnInit = onInit.bind({}); + +// Call old onInit() and disable offline caching. +var onInit = function(aPageId, aServerId) +{ +  oldOnInit(aPageId, aServerId); +  disableOfflineCaching(); +} + diff --git a/chrome/content/preventCaching/bitmaskAmOfflineOverlay.xul b/chrome/content/preventCaching/bitmaskAmOfflineOverlay.xul new file mode 100644 index 0000000..a5b5039 --- /dev/null +++ b/chrome/content/preventCaching/bitmaskAmOfflineOverlay.xul @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<overlay id="bitmaskPreventCachingOverlay"  +         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +  <script type="application/javascript" +          src="chrome://bitmask/content/serverConfig.js"/> +  <script type="application/javascript" +          src="chrome://bitmask/content/preventCaching/bitmaskAmOfflineOverlay.js"/> +</overlay> diff --git a/chrome/content/serverConfig.js b/chrome/content/serverConfig.js new file mode 100644 index 0000000..470ceb3 --- /dev/null +++ b/chrome/content/serverConfig.js @@ -0,0 +1,28 @@ +/** + * serverConfig.js + * Copyright (C) 2013 LEAP + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along + */ + + +// IMAP configuration +var IMAP_HOST = "localhost"; +var IMAP_PORT = 1984; + + +// SMTP configuration +var SMTP_HOST = "localhost"; +var SMTP_PORT = 2013; +var SMTP_USER = ""; diff --git a/chrome/content/statusBar/statusBarOverlay.js b/chrome/content/statusBar/statusBarOverlay.js new file mode 100644 index 0000000..31ec16a --- /dev/null +++ b/chrome/content/statusBar/statusBarOverlay.js @@ -0,0 +1,94 @@ +/** + * statusBar.js + * Copyright (C) 2013 LEAP + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along + */ + + +Components.utils.import("resource:///modules/mailServices.js"); + +var accountNotConfigured = getStringBundle( +    "chrome://bitmask/locale/statusBar.properties") +        .GetStringFromName("account_not_configured"); +var accountConfigured = getStringBundle( +    "chrome://bitmask/locale/statusBar.properties") +        .GetStringFromName("account_configured"); + + +/***************************************************************************** + * Schedule initialization and update functions. + ****************************************************************************/ + +// run startUp() once when window loads +window.addEventListener("load", function(e) {  +	starUp();  +}, false); + +// run updatePanel() periodically +window.setInterval( +	function() { +		updatePanel();  +	}, 10000); // update every ten seconds + + +/***************************************************************************** + * GUI maintenance functions. + ****************************************************************************/ + +function starUp() { +    updatePanel(); +    if (!isBitmaskAccountConfigured()) { +        launchAccountWizard(); +    } +} + +/** + * Update the status bar panel with information about bitmask accounts. + */ +function updatePanel() { +    var statusBarPanel = document.getElementById("bitmask-status-bar"); +    if (isBitmaskAccountConfigured()) +        statusBarPanel.label = accountConfigured; +    else +        statusBarPanel.label = accountNotConfigured; +} + +/** + * Handle a click on the status bar panel. For now, just launch the new + * account wizard if there's no account configured. + */ +function handleStatusBarClick() { +    if (!isBitmaskAccountConfigured()) +        launchAccountWizard(); +} + + +/***************************************************************************** + * Account management functions + ****************************************************************************/ + +/** + * Return true if there exists an account with incoming server hostname equal + * to IMAP_HOST and port equal to IMAP_PORT. + * + * TODO: also verify for SMTP configuration? + */ +function isBitmaskAccountConfigured() { +    var accountManager = Cc["@mozilla.org/messenger/account-manager;1"] +                         .getService(Ci.nsIMsgAccountManager); +    var existing = accountManager.findRealServer( +        "", IMAP_HOST, "imap", IMAP_PORT); +    return !!existing; +} diff --git a/chrome/content/statusBar/statusBarOverlay.xul b/chrome/content/statusBar/statusBarOverlay.xul new file mode 100644 index 0000000..006ccb9 --- /dev/null +++ b/chrome/content/statusBar/statusBarOverlay.xul @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<overlay id="bitmaskStatusBarOverlay" +         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +  <script type="application/javascript" +          src="chrome://bitmask/content/util.js"/> +  <script type="application/javascript" +          src="chrome://bitmask/content/serverConfig.js"/> +  <script type="application/javascript" +          src="chrome://bitmask/content/accountWizard/bitmaskAccountManagerOverlay.js"/> +  <script type="application/javascript" +          src="chrome://bitmask/content/statusBar/statusBarOverlay.js"/> +  <statusbar id="status-bar"> +    <statusbarpanel id="bitmask-status-bar" +                    label="Bitmask" +                    onclick="handleStatusBarClick();" /> +  </statusbar> +</overlay> diff --git a/chrome/content/util.js b/chrome/content/util.js new file mode 100644 index 0000000..1fab8d3 --- /dev/null +++ b/chrome/content/util.js @@ -0,0 +1,164 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/** + * Some common, generic functions + */ + +try { +  var Cc = Components.classes; +  var Ci = Components.interfaces; +} catch (e) { ddump(e); } // if already declared, as in xpcshell-tests +try { +  var Cu = Components.utils; +} catch (e) { ddump(e); } + +Cu.import("resource:///modules/errUtils.js"); + +/** + * Create a subtype + */ +function extend(child, supertype) +{ +  child.prototype.__proto__ = supertype.prototype; +} + +function assert(test, errorMsg) +{ +  if (!test) +    throw new NotReached(errorMsg ? errorMsg : +          "Programming bug. Assertion failed, see log."); +} + +function makeCallback(obj, func) +{ +  return function() +  { +    return func.apply(obj, arguments); +  } +} + +/** + * @param bundleURI {String}   chrome URL to properties file + * @return nsIStringBundle + */ +function getStringBundle(bundleURI) +{ +  try { +    return Cc["@mozilla.org/intl/stringbundle;1"] +           .getService(Ci.nsIStringBundleService) +           .createBundle(bundleURI); +  } catch (e) { +    throw new Exception("Failed to get stringbundle URI <" + bundleURI + +                        ">. Error: " + e); +  } +} + +function Exception(msg) +{ +  this._message = msg; + +  // get stack +  try { +    not.found.here += 1; // force a native exception ... +  } catch (e) { +    this.stack = e.stack; // ... to get the current stack +  } +} +Exception.prototype = +{ +  get message() +  { +    return this._message; +  }, +  toString : function() +  { +    return this._message; +  } +} + +function NotReached(msg) +{ +  Exception.call(this, msg); +  logException(this); +} +extend(NotReached, Exception); + + +function deepCopy(org) +{ +  if (typeof(org) == "undefined") +    return undefined; +  if (org == null) +    return null; +  if (typeof(org) == "string") +    return org; +  if (typeof(org) == "number") +    return org; +  if (typeof(org) == "boolean") +    return org == true; +  if (typeof(org) == "function") +    return org; +  if (typeof(org) != "object") +    throw "can't copy objects of type " + typeof(org) + " yet"; + +  //TODO still instanceof org != instanceof copy +  //var result = new org.constructor(); +  var result = new Object(); +  if (typeof(org.length) != "undefined") +    var result = new Array(); +  for (var prop in org) +    result[prop] = deepCopy(org[prop]); +  return result; +} + +let kDebug = false; +function ddump(text) +{ +  if (kDebug) +    dump(text + "\n"); +} + +function debugObject(obj, name, maxDepth, curDepth) +{ +  if (curDepth == undefined) +    curDepth = 0; +  if (maxDepth != undefined && curDepth > maxDepth) +    return ""; + +  var result = ""; +  var i = 0; +  for (let prop in obj) +  { +    i++; +    try { +      if (typeof(obj[prop]) == "object") +      { +        if (obj[prop] && obj[prop].length != undefined) +          result += name + "." + prop + "=[probably array, length " + +                obj[prop].length + "]\n"; +        else +          result += name + "." + prop + "=[" + typeof(obj[prop]) + "]\n"; +        result += debugObject(obj[prop], name + "." + prop, +                              maxDepth, curDepth + 1); +      } +      else if (typeof(obj[prop]) == "function") +        result += name + "." + prop + "=[function]\n"; +      else +        result += name + "." + prop + "=" + obj[prop] + "\n"; +    } catch (e) { +      result += name + "." + prop + "-> Exception(" + e + ")\n"; +    } +  } +  if (!i) +    result += name + " is empty\n"; +  return result; +} + +function alertPrompt(alertTitle, alertMsg) +{ +  Cc["@mozilla.org/embedcomp/prompt-service;1"] +      .getService(Ci.nsIPromptService) +      .alert(window, alertTitle, alertMsg); +} diff --git a/chrome/locale/en-US/accountWizard.dtd b/chrome/locale/en-US/accountWizard.dtd new file mode 100644 index 0000000..a0221ee --- /dev/null +++ b/chrome/locale/en-US/accountWizard.dtd @@ -0,0 +1 @@ +<!ENTITY bitmask.accountWizard.title       "Configure Bitmask Email Account"> diff --git a/chrome/locale/en-US/accountWizard.properties b/chrome/locale/en-US/accountWizard.properties new file mode 100644 index 0000000..51532d0 --- /dev/null +++ b/chrome/locale/en-US/accountWizard.properties @@ -0,0 +1,2 @@ +# verifyConfig.js +cannot_login.error=Unable to log in at server. Probably wrong configuration, username or password. diff --git a/chrome/locale/en-US/bitmaskAccountManagerOverlay.dtd b/chrome/locale/en-US/bitmaskAccountManagerOverlay.dtd new file mode 100644 index 0000000..b87fbc3 --- /dev/null +++ b/chrome/locale/en-US/bitmaskAccountManagerOverlay.dtd @@ -0,0 +1,2 @@ +<!ENTITY addBitmaskAccountButton.label       "Add Bitmask Account..."> +<!ENTITY addBitmaskAccountButton.accesskey   "B"> diff --git a/chrome/locale/en-US/bitmaskMessengerOverlay.dtd b/chrome/locale/en-US/bitmaskMessengerOverlay.dtd new file mode 100644 index 0000000..1e815cd --- /dev/null +++ b/chrome/locale/en-US/bitmaskMessengerOverlay.dtd @@ -0,0 +1,2 @@ +<!ENTITY newBitmaskAccountCmd.label       "Bitmask Account..."> +<!ENTITY newBitmaskAccountCmd.accesskey   "B"> diff --git a/chrome/locale/en-US/statusBar.properties b/chrome/locale/en-US/statusBar.properties new file mode 100644 index 0000000..7715c71 --- /dev/null +++ b/chrome/locale/en-US/statusBar.properties @@ -0,0 +1,3 @@ +# statusBar.js +account_not_configured=Click to config Bitmask account +account_configured=Bitmask account is configured! diff --git a/chrome/skin/accountWizard.css b/chrome/skin/accountWizard.css new file mode 100644 index 0000000..3bf6846 --- /dev/null +++ b/chrome/skin/accountWizard.css @@ -0,0 +1,10 @@ +@import url("chrome://messenger/skin/"); + +@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); + + +.bitmaskAccountWizardTitle { +  font-size: 150%; +  font-weight: bold; +} + diff --git a/install.rdf b/install.rdf new file mode 100644 index 0000000..f0a5398 --- /dev/null +++ b/install.rdf @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" +     xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + +  <Description about="urn:mozilla:install-manifest"> +    <em:id>bitmask-thunderbird@leap.se</em:id> +    <em:name>Bitmask Thunderbird Extension</em:name> +    <em:version>0.0.1</em:version> +    <em:creator>LEAP developers</em:creator> +    +    <em:targetApplication> +      <Description> +        <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id> +        <em:minVersion>24.0</em:minVersion> +        <em:maxVersion>24.0.*</em:maxVersion> +      </Description> +    </em:targetApplication> +    +  </Description>       +</RDF>  | 
