Initial import.
authordrebs <drebs@leap.se>
Thu, 3 Oct 2013 17:24:48 +0000 (14:24 -0300)
committerdrebs <drebs@leap.se>
Fri, 4 Oct 2013 17:25:08 +0000 (14:25 -0300)
25 files changed:
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.rst [new file with mode: 0644]
changes/feature_3542-create-thunderbird-extension [new file with mode: 0644]
chrome.manifest [new file with mode: 0644]
chrome/content/.gitignore [new file with mode: 0644]
chrome/content/accountWizard/accountWizard.js [new file with mode: 0644]
chrome/content/accountWizard/accountWizard.xul [new file with mode: 0644]
chrome/content/accountWizard/bitmaskAccountManagerOverlay.xul [new file with mode: 0644]
chrome/content/accountWizard/bitmaskMessengerOverlay.xul [new file with mode: 0644]
chrome/content/accountWizard/createInBackend.js [new file with mode: 0644]
chrome/content/accountWizard/launchAccountWizard.js [new file with mode: 0644]
chrome/content/preventCaching/bitmaskAmOfflineOverlay.js [new file with mode: 0644]
chrome/content/preventCaching/bitmaskAmOfflineOverlay.xul [new file with mode: 0644]
chrome/content/serverConfig.js [new file with mode: 0644]
chrome/content/statusBar/statusBarOverlay.js [new file with mode: 0644]
chrome/content/statusBar/statusBarOverlay.xul [new file with mode: 0644]
chrome/content/util.js [new file with mode: 0644]
chrome/locale/en-US/accountWizard.dtd [new file with mode: 0644]
chrome/locale/en-US/accountWizard.properties [new file with mode: 0644]
chrome/locale/en-US/bitmaskAccountManagerOverlay.dtd [new file with mode: 0644]
chrome/locale/en-US/bitmaskMessengerOverlay.dtd [new file with mode: 0644]
chrome/locale/en-US/statusBar.properties [new file with mode: 0644]
chrome/skin/accountWizard.css [new file with mode: 0644]
install.rdf [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..ee34263
--- /dev/null
@@ -0,0 +1,2 @@
+build/
+dot-thunderbird/
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 (file)
index 0000000..be00062
--- /dev/null
@@ -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 (file)
index 0000000..1055474
--- /dev/null
@@ -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 (file)
index 0000000..bb14689
--- /dev/null
@@ -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 (file)
index 0000000..3fec32c
--- /dev/null
@@ -0,0 +1 @@
+tmp/
diff --git a/chrome/content/accountWizard/accountWizard.js b/chrome/content/accountWizard/accountWizard.js
new file mode 100644 (file)
index 0000000..b2b533f
--- /dev/null
@@ -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 (file)
index 0000000..0d42c0d
--- /dev/null
@@ -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">&#160;</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 (file)
index 0000000..ba68b5f
--- /dev/null
@@ -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 (file)
index 0000000..a767022
--- /dev/null
@@ -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 (file)
index 0000000..f15b882
--- /dev/null
@@ -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 (file)
index 0000000..bda743f
--- /dev/null
@@ -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 (file)
index 0000000..c88b2ab
--- /dev/null
@@ -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 (file)
index 0000000..a5b5039
--- /dev/null
@@ -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 (file)
index 0000000..470ceb3
--- /dev/null
@@ -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 (file)
index 0000000..31ec16a
--- /dev/null
@@ -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 (file)
index 0000000..006ccb9
--- /dev/null
@@ -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 (file)
index 0000000..1fab8d3
--- /dev/null
@@ -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 (file)
index 0000000..a0221ee
--- /dev/null
@@ -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 (file)
index 0000000..51532d0
--- /dev/null
@@ -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 (file)
index 0000000..b87fbc3
--- /dev/null
@@ -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 (file)
index 0000000..1e815cd
--- /dev/null
@@ -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 (file)
index 0000000..7715c71
--- /dev/null
@@ -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 (file)
index 0000000..3bf6846
--- /dev/null
@@ -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 (file)
index 0000000..f0a5398
--- /dev/null
@@ -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>