diff options
author | drebs <drebs@leap.se> | 2013-10-03 14:24:48 -0300 |
---|---|---|
committer | drebs <drebs@leap.se> | 2013-10-04 14:25:08 -0300 |
commit | 3c23f4aa7eeabff0382f7789cfb7ad0c3090615a (patch) | |
tree | b7970f3d7fef0ceabb4293f135c97f257fe6baef |
Initial import.
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> |