MailNews:Creating New Account Types
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).
Contents
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
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.
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...
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
TODO: Hmm, can't hack in menu yet. Should probably just move to an instanceof check
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; } };