MailNews:Creating New Account Types

From MozillaWiki
Jump to: navigation, search
Warning signWarning: Some information here is specific to current trunk builds of Thunderbird and SeaMonkey, or, in some cases, rely on patches in the review process on Bugzilla. In particular, creating a new account type in this manner does not work on the 1.8 branch (Thunderbird 2 or SeaMonkey 1.1).

This page aims to provide a detailed guide on creating new account types from extensions for Thunderbird and SeaMonkey. This is still a work in progress, heavily dependent on my (jcranmer's) personal tree, where some critical components are merely patches in my tree and on bugzilla (where the latter may not be the most up-to-date).

The Account Manager

The best I can describe the account manager is that it is a hydra of components, managing several slightly different, but distinct, components. Primarily, these are servers, accounts, and identities. Folders, messages, message headers, databases, URLs, message DB views, the folder cache, and even more end up becoming part of this mess of topics to cover, the full scope of which is beyond the scope of this article. Needless to say, I will provide a sufficient overview to allow you to create a new account type.

For a diagram-based view of what is going on, see emre's diagrams on the subject. For other views, I invite you to run make documentation on the mozilla codebase to generate doxygen graphs and pages for the key components. This documentation can also be accessed at db48x's site, although I will warn you that he is using the experimental SVG output for doxygen which is quite obviously not quite production-ready.

Step 1: Account Types

The first thing to do when designing a new account type is to pick an internal name. This name should probably consist of the typical characters: alphanumerics, underscores, and dashes; this name will be used heavily as URI schemes and as portions of CIDs. For sake of simplicity, let's assume that the new account type is "acct" (don't actually pick this).

Defining account types requires that you implement a few interfaces. The most important ones are nsIMsgProtocolInfo, nsIMsgFolder, and nsIMsgIncomingServer, while other interfaces are important if you do crazier stuff, which you most likely will. Unfortunately, most of these interfaces involve a lot of repetitive code, while the nice implementations that do the magic for us are, naturally, native C++ code that JS-based implementations are unable to provide.

nsIMsgProtocolInfo

TODO: Investigate where nsIMsgProtocolInfo is used

Our first interface to implement is nsIMsgProtocolInfo, which defines basic parameters of the account type. A fair amount appears to be special casing for local folders. The contract ID is important; set it to be @mozilla.org/messenger/protocol/info;1?type=acct in our running example. Assuming you can generate the XPCOM overhead yourself, the following is an example of what to use:



function accountInfo() {
  this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
                          .getService(Ci.nsIPrefService)
                          .getBranch("acct.");
}
accountInfo.prototype = {

  // This variable represents the pref-based directory to store all server-
  // specific configurations, like News/ or ImapMail/
  get defaultLocalPath() {
    // Good idea to set this as a default pref in your extension.
    var pref = this._prefs.getComplexValue("root", Ci.nsIRelativeFilePref);
    return pref.file;
  },
  set defultLocalPath(path) {
    var newPref = Components.classes["@mozilla.org/pref-relativefile;1"]
                            .createInstance(Ci.nsIRelativeFilePref);
    newPref.relativeToKey = "ProfD";
    newPref.file = path;
    this._prefs.setComplexValue("root", Ci.nsIRelativeFilePref, newPref);
  },

  // Return the IID of the server for account wizard magic. You should only be
  // changing this value if you define your own incoming server for advanced
  // account wizard magic.
  get serverIID() {return Ci.nsIMsgIncomingServer;},
  getDefaultServerPort : function (isSecure) {
    return isSecure ? 443 : 80;
  },

  // True if you need a username
  get requiresUsername() {return false;},
  // True if the pretty name should be the email address.
  get preflightPrettyNameWithEmailAddress() {return false;},
  // True if you can delete this account type
  get canDelete() {return true;},
  get canLoginAtStartup() {return true;},
  // True if you can duplicate this account type
  get canDuplicate() {return true;},
  // Do we have an "inbox" for this account type?
  get canGetMessages() {return false;},
  // Can we use junk controls on these messages?
  get canGetIncomingMessages() {return false;},
  // Can we request new message notifications ("biff")?
  get defaultDoBiff() {return false;},
  get showComposeMsgLink() {return false;},
  get specialFoldersDeletionAllowed() {return false;}
};

All of the options--except the first three--are simple true/false boolean attributes that ask whether or not an account can do specific things. Full, up-to-date documentation is available at the IDL file; note that, in a few cases, the attribute name is a poor guideline for what it actually does. These defaults should be sufficient for most people, although a lot varies on what account type you are actually providing.

The first two are special attributes that require you to think somewhat before responding. First is the default local path, a directory under which all server-specific configuration servers are stored: this is essentially your Mail/ or ImapMail folders for your specific implementation. The catch is that this is actually pref-based, where the pref is de-facto of the form "mail.root.acct-rel"; the "rel" is there for backwards-compatibility reasons.

Second is the IID of the server. This is simply an interface that has special properties you can set with some account wizard magic; you can set this to return Ci.nsIMsgIncomingServer or even omit it if you do not actually need special properties.

The third non-boolean property is the default port, with an argument that tells you whether or not the port is secure. If you scrape from the web, you could set the port to return -1 or do an 80/443 combination like I do here. It's not terribly important.

nsIMsgIncomingServer

The second piece of the account type puzzle is the incoming server. Of all the parts related to account types, this one is the worst. Most of the attributes are really pref-based, although some key ones are not. A few of the attributes are merely helper functions, and some are not even used at all! It is expected that this class will be extremely pared in the post-TB 3 time frame as part of a general account manager overhaul.

TODO: Write me! Now!

nsIMsgFolder

The third and final piece of the core account type interfaces is the message folder. If you try to be too smart here, you can create refcount loops and leak horribly. This is also the core RDF object. In large part, this class works in tandem with nsIMsgIncomingServer through several attributes.

Step 2: Account Wizard Overlays

Warning signWarning: The account wizard may be going away soon, which will render this code obsolete. Until I get more information, I will not be updating this section.

After getting a basic account setup (or perhaps before, if you want to see results sooner), the next step is to overlay the account wizard, so that you don't force your users to either edit about:config manually or find another option in a menu somewhere.

Since the account wizard is shared between SeaMonkey and Thunderbird, there is no application-specific overlays that need to be accomplished.

Here is a sample overlay:

<overlay id="acct_overlay"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <radiogroup id="acctyperadio">
    <radio id="acctaccount" value="newpage, accnamepage"
           label="Account Type" accesskey="A"/>
  </radiogroup>
  <wizard id="AccountWizard">
    <script type="application/x-javascript" src="chrome://extension/content/account.js" />

   <wizardpage id="newpage" pageid="newpage" label="Page Name"
               onpageadvanced="return exitCallback()">
     <vbox flex="1">
       <!-- Enter your page in here. See the current account wizard pages
            if you want to see a model. -->
      </vbox>
    </wizardpage>
  </wizard>
</overlay>

TODO: Investigate the done page some more.

Note that we are, in fact, overlaying two elements here: the radiogroup for the account buttons, and the wizard element for the wizard pages. The single most important part of the overlay is the value attribute of your radio items. This value is a space-separated list of the pages to be visited in order. The account wizard will automatically add a "done" page to the list that displays the various set attributes of your account type for confirmation.

TODO: Dynamic page changes?

The second most important change here is the onpageadvanced attribute of the wizardpage items, since this is how you'll be actually adding the elements. If the function returns true, the page will be advanced; if false, the page will not advance (hopefully with an error dialog box as well).

TODO: Describe usage? Or move to the latter sections?

The javascript of these functions tends to be rather simple. Two main variables are manipulated: the account data (accessed via gCurrrentAccountData) and the page data (accessed via parent.GetPageData() and setPageData).

identitypage: Setting up an identity

serverpage: IMAP or POP server setups

loginpage: Specifying login

newsserver: Specifying news servers

accnamepage: Adding an account name

This page is the page that adds the pretty account name to your account. It is highly recommended that you use this page so that you can give users the ability to customize.

Page requirements
Attribute Optional Object Get or Set
wizardAutoGenerateUniqueHostname yes account get
server.hostname yes page get and set
accname.prettyName no page set
accname.userset no page set
server.servertype no page get
wizardAccountName yes account get
identity.email yes page get

wizardAccountName is used as the pre-filled value for the account name. If wizardAccountName is not defined, this page will use identity.email as the prefill instead (except for news servers, which uses its own hack).

If wizardAutoGenerateUniqueHostname is defined (apparently a hack designed for RSS accounts, according to the sources), then it will take the server's hostname and iterate over the hostname, appending successive integers, until it finds one that doesn't exist and makes that the new hostname.

The pretty name is set to value in the text field and accname.userset is set to true.

done: Finishing it all up

The done page is automatically added to all accounts. This page is a final confirmation of all settings, so that the user can make sure the values are correct before creating the account itself.

TODO: Adding other values? It looks ISP data does something here...

Page requirements
Attribute Optional Object
wizardHideIncoming yes account
identity.email yes page
login.username yes page
accname.prettyName yes page
server.hostname yes page
server.servertype yes page
server.smtphostname yes page
newsserver.name yes page

TODO: Finish this

Using ISP Data

Step 3: The Folder Pane and Folders

Step 4: Handling Subscription

Warning signWarning: Extension developers may be interested in bug 102699, which allows nsIDOMParser to parse text/html types. Until such bug is committed, however, extensions are forced to use their own workarounds to pragmatically access HTML scraped from the web.
Warning signWarning: The subscribe interface (specifically nsISubscribableServer) is undergoing drastic changes as a result of bug 457333.
To indicate that a server is subscribable, it needs to be able to query to nsISubscribableServer. The root folder must also return true for the attribute canSubscribe.

TODO: Hmm, can't hack in menu yet. Should probably just move to an instanceof check

Setting these values will allow you to open up the main subscribe dialog, used for NNTP and IMAP servers (but not RSS).

The subscribe dialog will first set your server's subscribeListener, which is the callback you will be using to display data. It will then call startPopulating in a synchronous manner. When this method is called, the server object will need to call addFolder on the listener. Once it is finished, the server will call onDonePopulating on the listener.

This first call to startPopulating is not the only way that the dialog asks the server for possible folders. If the Refresh button is clicked, the function will be called again with the forceToServer parameter set to true. If the server supports new folders, the same function will also be called with getOnlyNew set as well.

A final way of getting folders is by calling startPopulatingFromFolder. This is called every time a folder is expanded. Even if no list will be generated, the server must still call onDonePopulating at the end.

The Stop button, if pressed, will call stopPopulating.

The UI uses delimiter and showFullName for display purposes. delimiter is the character that will be used to split the folder names into a hierarchy. showFullName says whether or not the full name of the folder should be displayed or just the part following the last delimiter.

Whenever the user subscribes to or unsubscribes from a folder via the UI, setState is called. Most consumers should not need to deal with this method, as it exists purely for the benefit of those servers that need to handle search in subscribe.

When the dialog is closed, subscribeCleanup is first called. Then, the functions subscribe and unsubscribe, as appropriate are called for each groups whose subscription statuses have changed. Finally, commitSubscribeChanges is called. At the end, subscribeListener is nullified.

Supporting search in the subscribe dialog is more complicated. The server is expected to be able to query to nsITreeView, in addition to returning true for the attribute supportsSubscribeSearch. If the user types in the subscribe field, setSearchValue is called with the entered text and the tree view switches to using the server.

Reference implementation

subscribableServer.prototype = {
  // nsIMsgIncomingServer and other interfaces elided

  // Basic capabilities
  get subscribeListener() {return this._subscribeListener;},
  set subscribeListener(l) {this._subscribeListener = l;},
  get delimiter() {return '\t';},
  get showFullName() {return false;},
  get supportsSubscribeSearch() {return false;},
  // Subscribe utilities
  setSearchValue: function (value) {},
  setState: function (name, subscribed) {},

  // Subscription population
  _downloadMessages: function (window) {
    try {
      // Get the data somewhere
      let data = ...
      for (let name in data) 
        this._add(name, data[name]);
    } finally {
      this.stopPopulating(window);
    }
  },
  _loadGroupsFromCache: function() {
      // Get the data from a connection cache
      let data = ...
      for (let name in data) 
        this._add(name, data[name]);
     }
  },
  startPopulating: function (window, forceToServer, getOnlyNew) {
    this._lists = {};

    if (!forceToServer && !getOnlyNew) {
      this._loadGroupsFromCache();
    }

    // Make sure it's done asynchronously!
    this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    let closure = this;
    this._timer.initWithCallback({
        QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback]),
        notify: function() {closure._downloadMessages(window);}
      }, 0, this._timer.TYPE_ONE_SHOT);
    this._populating = true;
  },
  startPopulatingFromFolder: function (window, folderName) {},
  stopPopulating: function (window) {
    // Stop the connection if open...
    this.subscribeListener.onDonePopulating();
  },
  _add: function (name, subscribed) {
    this.subscribeListener.addTo(name, subscribed, true, true);
  },
 
  // Subscribe metafunctions
  subscribeCleanup: function () {
    this._timer = null;
    this._subscribed = {};
  },
  // Subscribe information
  subscribe: function (name) {
    // Subscribe...
    this._subscribed[name] = true;
  },
  unsubscribe: function (name) {
    // Unsubscribe...
    this._subscribed[name] = false;
  },
  commitSubscribeChanges: function () {
    for (let name in this._subscribed) {
      // Do something useful...
    }
    this._subscribed = null;
  }
};

Step 5: Configuration and the Account Manager

Step 6: Identities and Compose

Step 7: Filters and Search