Tag Archives: mcd

Easy Thunderbird Account Management Using MCD

Ford Thunderbird Console
From myoldpostcards

Continuing my series on Mission Control Desktop, this post covers some enhancements I made for configuring email accounts in Thunderbird.  The mail.* branch is one of the largest group of preferences you can manipulate using autoconfig.  Setting up the right combination of mail.accountmail.server, mail.identity and mail.smtpserver is just a little tricky.

With the exception of the special “local folders” account, an email account in Thunderbird has four components: an account, a server, one or more identities, and an smtp server.  There are a few preference strings gluing each of these elements together. Each account you create is differentiated from the others by a unique label, defaulting to account<num>, with incrementing numbers. You can also use your own, more meaningful labels, such as mail.account.work-email and mail.account.gmail.

Element Branch
Account mail.account.accountN Glues POP/IMAP, SMTP servers and identities together
Server mail.server.serverN Settings for the IMAP or POP server
Identity mail.identity.idN Name, email address, drafts & stationery folders
SMTP Server mail.smtpserver.smtpN Settings for the SMTP server
Glue settings
Accounts list mail.accountmanager.accounts Comma-separated list of mail.account labels
Default account mail.accountmanager.defaultaccount The label of the default mail account
SMTP servers mail.smtpservers Comma-separated list of mail.smtpserver server labels
Default SMTP mail.smtp.defaultserver The label of the default SMTP server

Here is a quick example:

// Identity
defaultPref("mail.identity.id1.fullName", "Dean Brundage");
defaultPref("mail.identity.id1.draft_folder", "imap://dean.brundage@example.com@mail.example.com/Drafts");
defaultPref("mail.identity.id1.smtpServer", "smtp1");
 
// IMAP server settings
defaultPref("mail.server.server1.type", "imap");
defaultPref("mail.server.server1.hostname", "mail.example.com");
// etc
 
// SMTP server settings
defaultPref("mail.smtpserver.smtp1.auth_method", 1);
defaultPref("mail.smtpserver.smtp1.hostname", "smtp.example.com");
// etc
 
// Glue it all together
defaultPref("mail.account.account1.identities", "id1");
defaultPref("mail.account.account1.server", "server1");
 
defaultPref("mail.accountmanager.accounts", "account1");
defaultPref("mail.accountmanager.defaultaccount", "account1");
 
defaultPref("mail.smtp.defaultserver", "smtp1");
defaultPref("mail.smtpservers", "smtp1");

As you add more email accounts the code can get unmanageable when you try to remember to twiddle the right branches so all the accounts, their identities and smtp servers show up. I would like to present an alternative.

This code builds upon the PreferenceFactory prototype covered earlier.  I put together an object prototype for an account, containing a server, one or more identities, and an smtp server.  There is also a singleton for the account manager.  Now you can instantiate an object and call preference setters on it.  The code is more readable and less error prone.

   // Create an email account
   var workAccount = AccountManager.newAccount({ isDefault: true,
                                                 label: "work",
                                                 type: "imap" });
 
   // Lock Preferences on the IMAP server
   workAccount.server.setPrefs({ hostname: "mail.example.com",
                                 type: "imap" }, "lock" );
   // Default Preferences
   workAccount.server.setPrefs({ check_new_mail: true,
                                 name: "Corporate eMail" });
 
  // Lock Preferences on the SMTP server
  workAccount.smtpServer.setPrefs({ auth_method: 1,    /* User/pass */
                                    username: "dean.brundage@example.com" }, "lock" );
  // Default Preferences
  workAccount.smtpServer.setPrefs({ description: "Corporate SMTP server",
                                     hostname: "mail.example.com"});
 
  // And now a personal account
  var myGmailAccount = AccountManager.newAccount({ label: "gmail", type: "imap" });
  // Go about setting preferences on myGmailAccount as before

Download the source

That block of code replaces the tedious series of defaultPref and lockPref required to set up an email account.  It produces preference settings like these:

defaultPref("mail.accountmanager.accounts", "work-account1,gmail-account2");
defaultPref("mail.accountmanager.defaultaccount", "work-account1");
 
defaultPref("mail.account.work-account1.identities", "work-id1");
defaultPref("mail.account.work-account1.server", "work-server1");
lockPref("mail.server.work-server1.type", "imap");
 
defaultPref("mail.server.gmail-server2.type", "imap");
// etc
 
defaultPref("mail.smtp.defaultserver", "work-smtp1");
defaultPref("mail.smtpservers", "work-smtp1");

Download the source

/**
   By Dean Brundage
   Originally published here:
     http://blog.deanandadie.net/2010/06/easy-thunderbird-account-management-using-mcd/
*/
 
 
/* "Inheritance" helper
   http://www.sitepoint.com/blogs/2006/01/17/javascript-inheritance/
   copy all of parent's prototype functions to descendant
*/
function copyPrototype(parent, descendant)
{  var sConstructor = parent.toString(); 
   var aMatch = sConstructor.match( /\s*function (.*)\(/ ); 
   if( aMatch != null )
   {  descendant.prototype[aMatch[1]] = parent;  } 
   for (var m in parent.prototype)
   {  descendant.prototype[m] = parent.prototype[m];  } 
};
 
 
/* "Base" for the Mail.xxxx objects
 
   This requires the PreferenceFactory prototype covered in a previous post:
     http://blog.deanandadie.net/2010/05/manufacturing-user-preferences-for-mcd/
*/
function Mail()
{  this.PreferenceFactory();  // super
   this.addPrefBranch("mail");
   return this;
}
 
copyPrototype( PreferenceFactory, Mail );
 
 
Mail.prototype.joinIds = function(collection,separator)
{  if( ! separator )
      separator = ",";
   if( collection )
   {  ret = collection[0].id
      for( i = 1; i < collection.length; i++ )
         ret = ret + separator + collection[i].id;
   }
   return ret;
};
 
 
/* A thunderbird email "account" consists of
      Mail.Server -- A mail server (IMAP or POP)
      Mail.SMTPServer -- A SMTP server
      One or more Mail.Identities -- Email address and name
 
   A thunderbird local folders "account" has only one Mail.Server, nothing else
      Yup, local folders are "servers"
*/
 
/* Object to manage accounts
      I recommend managing mail accounts with the AccountManager unless you
      know what you are doing.
   var myAccount = AccountManager.newAccount();
   myAccount.doStuff();
 
*/
var AccountManager = new Mail();
 
AccountManager.accounts = [];
AccountManager.addPrefBranch("accountmanager");
 
/* Create a new account
   Alerts Thunderbird to the presence of the new account
   See Mail.Account for valid options
*/
AccountManager.newAccount = function(opts)
{  if( ! opts )
      opts = { };
 
   acct = new Mail.Account(opts);
 
   if( opts.isDefault )
   {  acct.useSMTPServer( new Mail.SMTPServer(opts) );
      this.setDefaultAccount(acct,opts.lockLevel);
   } else
   {  acct.useSMTPServer( Mail.SMTPServer.defaultServer );
   }
 
   this.accounts.push(acct);
   this.setPref( "accounts", this.joinIds(this.accounts) );
   return acct;
}
 
 
AccountManager.setDefaultAccount = function(account,lockLevel)
{  
   this.setPref("defaultaccount", account.id, lockLevel );
 
   /* It's possible to create an account before setting the default
      SMTP server.  Clean them up if this new account is the default
      (Expect Mail.Account to inform Mail.SMTPServer of the new default)
   */
   for( i = 0; i < this.accounts.length; i++ )
      this.accounts[i].useSMTPServer(Mail.SMTPServer.defaultServer);
}
 
 
AccountManager.setLocalFolders = function(folders,lockLevel)
{  this.setPref("localfoldersserver", folders.id, lockLevel);
}
 
 
/* Creates a new generic mail account (identity, server, smtp)
   Arguments
     opts: A hash of options. Valid options are:
           "isDefault": true | false (default false)
           "label": string - A unique label for this account, server & identity
           "lockLevel": "default" | "lock" | "pref" (default "default")
           "type": imap | pop | localFolder
 
   This prototype creates an object representing an email account
   It exposes some objects:
      myAccount.directoryServer  // The (optional) LDAP2Server object
      myAccount.identities       // An array of identites
      myAccount.server           // The IMAP/POP/LocalFolder Mail.Server object
      myAccount.smtpServer       // The Mail.SMTPServer object
 
   (The LDAP2Server object is detailed in a separate post on http://blog.deanandadie.net/)
*/
Mail.Account = function(opts)
{  if( ! opts )
     opts = { };
 
   if( ! opts.type )
      throw("What's my type?");
 
   this.Mail();  // "super"
 
   this.id = "account" + ++Mail.Account.count;
   if( opts.label )
   {  this.id = opts.label + "-" + this.id;  }
 
   // Our preference branch is mail.account.accountN
   this.addPrefBranch( [ "account", this.id ] );
 
   this.identities = [];
 
   this.server = new Mail.Server(opts);  // Need this either way
 
   if( opts.type.match(/^^imap$|^pop$/i) )
   {
      this.server.setPref("type", opts.type, opts.lockLevel);
 
      // Important that addIdentity be before the SMTP server is set up
      this.addIdentity( new Mail.Identity(opts), opts.lockLevel );
 
   } else if( opts.type.match(/^localFolders?$/i) )
   {  this.folders = this.server;
      AccountManager.setLocalFolders(this.folders);
      this.folders.setPref("type", "none", opts.lockLevel );
 
   } else
   {  throw("unrecognized Mail.Account type: " + opts.type);  }
 
   this.useServer(this.server, opts.lockLevel);
 
}
 
copyPrototype( Mail, Mail.Account );
 
Mail.Account.count = 0;                                   // Fake class variable
 
 
// Expects a Mail.Identity object and, optionally, the preference locking level
Mail.Account.prototype.addIdentity = function(identity,lockLevel)
{  if( identity )
   {  if( ! this.identities.contains(identity) )
      {  this.identities.push(identity);
         this.setPref("identities", this.joinIds(this.identities), lockLevel );
      }
   }
}
 
 
/* Helper to set the SMTP server
   Expects a Mail.SMTPServer object & optionally, an options hash
   Valid options:
     lockLevel: lock | default | pref
     force: true | false
*/
Mail.Account.prototype.useDirectory = function(directory, opts)
{  if( ! opts )
      opts = { };
   if( directory )
   {  if( opts.force || typeof(this.directoryServer) == "undefined" )
      {  this.directoryServer = directory;
         for( i = 0; i < this.identities.length; i++ )
            this.identities[i].useDirectory( this.directoryServer, opts );
      }
   }
}
 
/* Helper to set the SMTP server
   Expects a Mail.SMTPServer object & optionally, an options hash
   Valid options:
     lockLevel: lock | default | pref
     force: true | false
*/
Mail.Account.prototype.useSMTPServer = function(smtp,opts)
{  if( ! opts )
      opts = { };
   if( smtp )
   {  if( opts.force || typeof(this.smtpServer) == "undefined" )
      {  this.smtpServer = smtp;
         for( i = 0; i < this.identities.length; i++ )
            this.identities[i].useSMTPServer( this.smtpServer, opts );
      }
   }
}
 
 
// Expects a Mail.Server object and, optionally, the preference locking level
Mail.Account.prototype.useServer = function(server,lockLevel)
{  if( server )
      this.setPref("server", server.id, lockLevel );
}
 
 
/* Creates a new generic mail server
   Arguments
     opts: A hash of options. Valid options are:
           label: string - A unique label for this account, server & identity
*/
Mail.Server = function(opts)
{  if( ! opts )
     opts = { };
 
   this.Mail();
 
   this.id = "server" + ++Mail.Server.count;
   if( opts.label )
   {  this.id = opts.label + "-" + this.id;  }
 
   // Our preference branch is mail.server.serverN
   this.addPrefBranch( [ "server", this.id ] );
}
 
copyPrototype( Mail, Mail.Server );
 
Mail.Server.count = 0;  // Fake class variable
 
 
/* Creates a new mail identity. (name, email address, etc)
   Arguments
     opts: A hash of options. Valid options are:
           "label": string - A unique label for this account, server & identity
*/
Mail.Identity = function(opts)
{  if( ! opts )
     opts = { };
 
   this.Mail();
 
   this.id = "id" + ++Mail.Identity.count;
   if( opts.label )
   {  this.id = opts.label + "-" + this.id;  }
 
   // Our preference branch is mail.identity.idN
   this.addPrefBranch( [ "identity", this.id ] );
   this.hasSMTPServer = false;
};
 
copyPrototype(Mail, Mail.Identity);
 
 
Mail.Identity.count = 0;  // Fake class variable
 
 
/* Expects a LDAP2Server object and a hash of options
   Valid options:
      lockLevel: lock | default | pref
*/
Mail.Identity.prototype.useDirectory = function(ldap2Server,opts)
{  if( ! opts )
      opts = { };
 
   if( ldap2Server )
   {  this.setPref("directoryServer", "ldap_2.servers." + ldap2Server.id, opts.lockLevel);
      this.setPref("overrideGlobal_Pref", true, opts.lockLevel);
   }
};
 
 
/* Expects a SMTPServer object and a hash of options
   Valid options:
      force: true | false
      lockLevel: lock | default | pref
*/
Mail.Identity.prototype.useSMTPServer = function(smtpServer,opts)
{  if( ! opts )
      opts = { }
   if( smtpServer )
   {  if( opts.force || ! this.hasSMTPServer )
      {  this.setPref("smtpServer", smtpServer.id, opts.lockLevel);
         this.hasSMTPServer = true;
      }
   }
};
 
 
/* Creates a new smtp server
   Artuments:
     opts: A hash of options. Valid options are:
       isDefault: true | false (default)
       label: A unique label for this smtp server
   Throws an error if there is already a default smtp server
*/
Mail.SMTPServer = function(opts)
{  if( ! opts )
     opts = { };
 
   this.Mail();
 
   this.id = "smtp" + ++Mail.SMTPServer.count;
   if( opts.label )
   {  this.id = opts.label + "-" + this.id;  }
 
   // Our perference branch is mail.smtpserver.smtpN
   this.addPrefBranch( [ "smtpserver", this.id ] );
 
   if( opts.isDefault && Mail.SMTPServer.defaultServer )
      throw("Default smtp server already configured to " + Mail.SMTPServer.defaultServer);
 
   if( opts.isDefault )
      this.setDefault();
 
   // Register this SMTP server
   if( ! Mail.SMTPServer.servers.contains(this) )
      Mail.SMTPServer.servers.push(this);
 
   // Update mail.smtpservers to include the new one
   defaultPref("mail.smtpservers",  this.joinIds(Mail.SMTPServer.servers) );
 
   return this;
};
 
copyPrototype(Mail, Mail.SMTPServer);
 
// Some "class" variables
Mail.SMTPServer.count = 0;
Mail.SMTPServer.defaultServer = undefined;
Mail.SMTPServer.servers = [];
 
 
Mail.SMTPServer.prototype.setDefault = function()
{  Mail.SMTPServer.defaultServer = this;
   defaultPref("mail.smtp.defaultserver", Mail.SMTPServer.defaultServer.id );
}

Reading Local Files With Javascript

Security Conscious Javascript

Normally, javascript does not have access to local files.  Rightfully so because almost every web server should be untrusted and allowing anybody to read your files is a large security risk.  Mission Control Desktop, however is a javascript application with access to XPCOM, Mozilla’s component object model. This article is an example of how to use it to read local files.

Solaris Printing Configuration

I use MCD to configure thousands of users’ browser and mail clients.  In Solaris, printer preferences are set according to the Mozilla Postscript subsystem in a .printers file in the home directory.  Firefox and Thunderbird do not support this file interface so I use autoconfig to read the file and set printer preferences accordingly.

I wrote an object prototype to read and parse $HOME/.printers which the autoconfig uses to set the print.printer_list preference.  The prototype searches the file for a string indicating the list of printers to use:

# $HOME/.printers
# Set the default printer to riemann
_default riemann
# Display a chooser for three printers, ignoring all others.
_all newton,poincare,riemann

The line beginning with _all is quite important in my environment with 1,700 printers configured through LDAP.  If Thunderbird or Firefox were to try to load the entire printer list, it would become overwhelmed and crash.

extract_allPrinters utility

First, a look at the utility function I use to find the right line.

if( typeof(FileInterface) == "undefined" )  // Barf if the object prototype is undefined
   throw("autoconfig constructed improperly: need classes/file_interface.js before extract_all_printers.js");
 
function extract_allPrinters(path)
{  if( typeof(path) == "undefined" )
      path = env_home + "/.printers";
 
   // Regular expression to search .printers for _all
   re = /^\s*_all\s+(.+)\s*/i;
   lines = new FileInterface(path).grep(re);
 
   if( typeof(lines) != "undefined" )
   {  // Take the first match
      allPrinters = re.exec( lines[0] )[1].replace(/,\s*/g, " ");
   }
   return allPrinters;
}
 
/*
   *snip*
*/
 
// Tell FF &amp; TB which printers to offer a print dialogue for
defaultPref("print.printer_list", extract_allPrinters() );

The first lines check that the FileInterface object prototype is defined.  This is necessary because my autoconfig script broken into component scripts in the filesystem.  Besides making it easier to maintain, I can share functionality between Firefox and Thunderbird while excluding application-specific parts from the wrong autoconfig.

This function uses a prototype called FileInterface (explained later) to grep .printers for lines beginning with _all. If grep returns a match, the function takes the first one. print.printer_list takes a space-separated list of printers while _all is comma-separated, so the final thing to do is replace the commas with spaces.

Later on in the script I call defaultPref to set the printer list.

The FileInterface object prototype

nsILocalFile is the XPCOM interface used to access files on the client-side filesystem.  This object prototype provides only enough functionality to search files, but remains extensible should I need to create or modify.

// All you need is a (string) path
function FileInterface(path)
{  if( typeof(path) == "undefined" )
     throw("Need a path for FileInterface()");
   this.path = path;
}
 
/* Emulate the functionality of unix grep
   Takes a RegExp as it's only argument
   Returns an array of lines matching the RegExp
     E.G.: passwd = new FileInterface("/etc/passwd");
           lines = passwd.grep(/brundage/);
           // lines[0] = brundage:x:1002:1002::/home/brundage:/bin/zsh
*/
FileInterface.prototype.grep = function(re)
{  matches = undefined;
   if( re )
   {  line = {};
      matches = [];
      this.initIStream();
      do
      {  hasMore = this.iStream.readLine(line);
         if( re.test(line.value) )
            matches.push(line.value);
      } while(hasMore);
      this.iStream.close();
   }
   return matches;
}
 
/* Initializes a nsILineInputStream for higher-level functions
   Takes three arguments, assigning read-only defaults to undefined arguments.
   For possible values see: https://developer.mozilla.org/en/NsIFileInputStream
*/
FileInterface.prototype.initIStream = function(ioFlags,perm,behaviorFlags)
{  if( ! ioFlags )
     ioFlags = -1;  // Default mode (PR_READONLY)
   if( ! perm )
     perm = -1;  // Default mode (0)
   if( ! behaviorFlags )
     behaviorFlags = 0;
 
   // Initializing the stream requires an nsILocalFile.  Make one out of the path attribute.
   this.initLocalFile();
 
   // Get the nsIFileInputStream instance from the global Components variable
   this.iStream =  Components.classes["@mozilla.org/network/file-input-stream;1"].createInstance(Components.interfaces.nsIFileInputStream);
   if( ! this.iStream )  // Bad Things
      throw("network/file-input-stream component does not exist");
 
   // Point the stream at the iLocalFile
   this.iStream.init(this.iLocalFile, ioFlags, perm, behaviorFlags);
   // Transform iStream into a nsILineInputStream
   this.iStream.QueryInterface(Components.interfaces.nsILineInputStream);
}
 
/* Initialize an nsILocalFile instance with the path attribute of this object
   Required for streams
*/
FileInterface.prototype.initLocalFile = function()
{  this.iLocalFile = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsILocalFile);
   if( ! this.iLocalFile )  // Bad Things
     throw("file/local component does not exist");
 
   this.iLocalFile.initWithPath(this.path);
}

There you have it

I have only encountered this single situation that requires access to the local filesystem.  Can you think of others?

LDAP Queries in Mission Control Desktop

Previously, we saw that Mozilla MCD can inspect a user’s environment using getEnv().  It can also retrieve information from an LDAP directory.  I use this feature to inform Firefox and Thunderbird of the user in detail.  The corporate directory knows the user’s full name, mail server and authentication credentials.  autoconfig takes this and, among other useful things, constructs an email account for Thunderbird without user intervention.

The javascript API to deal with LDAP is a bit hackish, however it is all we have.  The first task is to define a function called processLDAPValues() which accepts a queryResults string as its only argument.  Inside processLDAPValues you extract return data from queryResults.

Instead of invoking processLDAPValues() directly, you call getLDAPAttributes() which in turn gets you to your function.  To illustrate, here is the code I use to query the corporate directory server and save the values for later use.

var userInfo = new Object();  // This will hold LDAP results
 
userInfo.envUser = getenv("LOGNAME");   // Unix UID
userInfo.envHome = getenv("HOME");      // User home directory
 
var ldapHost = "ldap.example.com";
var ldapBase = "dc=example,dc=com";
 
if( userInfo.envUser )
{  var ldapFilter = "uid=" + userInfo.envUser;  }
else
{  throw("Couldn't get UID from the environment");  }
 
// LDAP attributes to retrieve from the server
var ldapAttrs = new Array( "cn", "email", "employeenumber", "givenname", "mailhost", "sn", "uid" );
 
// Define how to process LDAP results before we make the call
function processLDAPValues(queryResults)
{  if( queryResults )
   {  // Build the userInfo object for later use
      for( var attr in ldapAttrs )
      {  userInfo[ ldapAttrs[attr] ] = getLDAPValue( queryResults, ldapAttrs[attr] );  }
   } else
   {  throw( "No LDAP results" );  }
}
 
// Call upon LDAP for the values in ldapAttrs array
// Uses the previous processLDAPValues()
getLDAPAttributes( ldapHost, ldapBase, ldapFilter, ldapAttrs.join(",") );

The first thing I do is create a userInfo object that will hold LDAP results for use later in the autoconfig.  To that object I add attributes for the user’s login name and home directory.

The next bit sets variables to contain the directory server’s hostname, base DN and the LDAP filter to use in the search. It’s a good idea to throw an error if there is no $LOGNAME.  (In a later post I will show how to enhance autoconfig error reporting.)

The ldapAttrs array names the attributes I want to return from LDAP.  Change this array to suit your environment.  The last line of code joins the array together with commas and feeds it to getLDAPAttributes along with the hostname, base DN and filter.  getLDAPAttributes is defined in MOZILLA_HOME/defaults/autoconfig/prefcalls.js and does the work to perform the query, then call your predefined processLDAPValues() function.

The example autoconfig script at developer.mozilla.org set preferences inside processLDAPValues, however this is a bad convention.  There are many preferences that require user information and separating pref() calls away from the main block of preference setting can be confusing.  As you can see here I simply run through the array of attributes I’m interested in and get the result from the LDAP query for that attribute, assigning it to the userInfo object.

Later on in the script I ask the userInfo object for those stored LDAP attributes. To set the hostname of the user’s mail server, for example, I call

// IMAP server name from corporate LDAP directory
defaultPref("mail.server.server1.hostname", userInfo.mailhost );

LDAP directories are a great resource. What attributes could you store in your corporate server?

Setting User Preferences with Mission Control Desktop

A challenge with software

Managing software for thousands of users presents a formidable challenge to the system administrator. Publishing corporate policy, using standard environments and providing clear end-user documentation helps. However, it doesn’t beat automatically doing it right.  This is the power of MCD autoconfig.

In an earlier post, I introduced MCD as a way to configure Mozilla products (Firefox, Thunderbird, Prism, etc) and provided background on building them with autoconfig support.  This post covers how to get started with the standard javascript API.  In forthcoming posts I’ll detail the useful enhancements I built using this API.

The environment

First, a quick rehash of my world.  I work in a Solaris shop with over 33,000 users.  Supported users log into a shared Sun Ray server or their personal workstation which mounts a shared NFS directory.  That directory houses the software I support along with about 700 other programs.

While this post is unix-centric, other operating environments that launch managed software can make use of MCD. See the introductory post for  a little more detail.

Mozilla Preference System

Firefox and Thunderbird use a simple preference tree to store all configuration options. Leaves of the tree are strings that store the option’s value.  For example, the preference browser.startup.homepage is a string containing the URL(s) of Firefox’s homepage.  mail.forward_message_mode contains an integer indicating how Thunderbird should forward email messages (inline or as an attachment).  You can find every available setting and their values in the config editor for Thunderbird and about:config in Firefox.

When a user changes his or her preferences the changed values are stored in a file called prefs.js in their home directory. On my MacOS laptop, this is $HOME/Library/Application Support/Firefox/Profiles/kzssiknu.default.

The Application Programming Interface

The autoconfig acts on preferences through a javascript API defined in the file MOZILLA_LIB_DIR/defaults/autoconfig/prefcalls.js.  I will talk about the most useful in this post and cover the LDAP parts of the API later.

// Used most often
function defaultPref(prefName, value);
function lockPref(prefName, value);
// Sometimes used
function displayError(funcname, message);
function getenv(name);

defaultPref() and lockPref()

These two functions perform the bulk of work in an autoconfig script.  A default preference setting may be overridden by the user, but a locked preference may not.

getenv() and displayError()

getenv() acts as you would expect from its name. This function returns the value of an environment variable.  I use it to get $USER and $HOME. displayError() pops up an error message.  It is useful for debugging, but a user should rarely see it.

Putting it all together

We now have some basic building blocks to configure thousands of users. Here is a look at a simple autoconfig.js file for Firefox.  These settings will apply to every user.

// Catch errors
try
{
   // Set  downloads directory to a folder on the user's desktop
   var download_dir = getenv("HOME") + "/Desktop/Downloads";
   defaultPref("browser.download.defaultFolder", download_dir);
   defaultPref("browser.download.dir", download_dir);
   defaultPref("browser.download.downloadDir", download_dir);
   defaultPref("browser.download.folderList", 2);kk
 
   // Length of Time to Remember Visited Pages For (Days) - 30
   defaultPref("browser.history_expire_days", 30);
 
   // Automatically Add 'www.' and '.com' to the Location if a Web Page is Not Found - Enabled
   defaultPref("browser.fixup.alternate.enabled", true);
 
   // Lock the cache size to 60MB for shared performance
   lockPref("browser.cache.memory.capacity", 60000);
   lockPref("browser.cache.disk.capacity", 60000);
 
   // fix memory usage with lots of tabs
   lockPref("browser.sessionhistory.max_total_viewers", 2);
 
   // Set animated images to loop once
   defaultPref("image.animation_mode", "once");
 
} catch(e)
{
   displayError("autoconfig.js failed", e);
}

Learning preference strings

Finding the right preference setting or combination of preferences that change the program’s behavior sometimes presents a challenge.  I use the About:config entries page on the Mozilla Zine knowledge base.  That page also has a pointer to three more resources.

Another technique is to watch your own prefs.js file for changes when you twiddle settings.  Save a copy of prefs.js before flipping a preference.  Immediately quit the application after the change and diff the two files.

[Edit: I wrote up an entire post on this subject.]

Using LDAP and more fun things to do

In following posts I will cover retrieving information from LDAP, better error reporting, reading local files like $HOME/.printers, and simplifying Thunderbird email account management. Stay tuned.

An Introduction To Mission Control Desktop

What is MCD?

MCD (aka AutoConfig) is a script used to programmatically configure Mozilla products such as Firefox and Thunderbird in the enterprise for multiple users.  Part of my job is to ensure 33,385 people have the right settings to check their email and browse the web.  Centralizing their set up with autoconfig removes the burden from the user.

Why write about it?

Documentation on MCD is old, but not exactly out of date.  The basics of autoconfig have not changed since the age of the Netscape browser.  From trolling newsgroups, IRC and Google, I know many people use MCD, but share little about the subject. (Some do.)  Over the course of my work I wrote object prototypes, extended error reporting and generally tried to make using this bit of javascript easier. I want to reach out to the community and give a little back.

A quick run-through

Most people think of javascript as a browser technology. But, MCD has access to XPCOM, a bridge between C++ libraries and javascript, which gives the developer power to poke at Mozilla internals.  When Thunderbird (Firefox, Seamonkey, etc) launches, it executes a javascript script that makes use of a configuration API.

The autoconfig sets preferences exactly as a user would using about:config. It can also render preferences immutable, locking them down according to corporate policy.  When I inherited the script it was  simply a long string of preference directives with a little LDAP voodoo.

defaultPref("news.server_change_xaction", 0 );
defaultPref("mail.migration.copyMailFiles", false);
defaultPref("network.cookie.disableCookieForMailNews", false);
   lockPref("mail.remember_password", false);
defaultPref("javascript.allow.mailnews", false);
defaultPref("mail.addr_book.lastnamefirst", 1);
defaultPref("mail.toolbars.showbutton.file", false);
defaultPref("mail.toolbars.showbutton.junk", true);
defaultPref("mail.forward_message_mode", 2);
defaultPref("mailnews.wraplength", 72);
defaultPref("mail.wrap_long_lines", true);
defaultPref("mail.collect_email_address_outgoing", true);
defaultPref("mail.collect_email_address_incoming", true);

Not the easiest thing to grok.

After Thunderbird executes the autoconfig it starts up normally, applying saved user preferences. defaultPref settings are overridden by user preferences, but lockPref are not.

If you want to turn on a proxy server and force SSL in Firefox for every user it becomes easy to do:

// Set http proxy to your.server.domain
lockPref("network.proxy.http", "your.server.domain");
// Require and lock SSL
lockPref("network.proxy.ssl", true);

Details, implementation details

There are a number of things required to get MCD working.

Build *zilla (Firefox, Thunderbird, etc) with support

Your Mozilla product needs to be built with pref extension support. Add this to your .mozconfig file:

ac_add_options --enable-extensions=pref

To utilize LDAP (you do want to use LDAP, don’t you?) check the configure script for:

MOZ_LDAP_XPCOM=1

You can check about:buildconfig in Firefox to see if your build is good to go. While MCD documentation is sparse, the Mozilla Developer’s Center has plenty of build instructions.

Software distribution

I work in a Solaris world. Servers and desktops mount a shared NFS directory from a network of servers housing some 735 programs, including Firefox & Thunderbird.  The directory is mounted read-only so average users are not tempted to twiddle with the software.  Although I wrote this paper from a unix perspective the implementation will work in a Linux, Windows, or MacOS environment.  Mounting a shared software repository makes the system robust, however MCD works in a network of stand-alone desktops.

Breaking .cfg “encryption”

*zilla products first read a javascript configuration file in the lib directory called, for example, firefox.cfg.  In the beginning-time, Mozilla developers chose to ROT-7 encode the file, obscuring its contents from users.  When Netscape 7 came out, they did away with ROT-7 in favor of ROT-13. Many Firefox and Thunderbird .cfg files are still encoded this way using moz_byteshift.pl.

The rotary encoding is controlled by a setting in $MOZ_LIB_DIR/greprefs/all.js. At packaging time I patch this file, setting encoding to 0.

// ROT-encoding is bad, mmmkay?
pref("general.config.obscure_value", 0);  // for MCD .cfg files

This tells *zilla not to ROT-decode the .cfg file.

This shadowy file mojo likely came from the day of stand-alone workstations where users had root access and the software maintainers wanted to have just a little control over Netscape preferences.  Hiding the configuration file’s location gives you the illusion of control.

Now, the .cfg file is on a read-only mounted partition and nobody on the system has super-user level access. There is little danger of a user skirting corporate policy by turning off autoconfig.

Pointing *zilla at the autoconfig

When Firefox starts up it checks for and executes javascript a .cfg file giving it the autoconfig script’s path.

// $MOZ_LIB_DIR/firefox.cfg
// the output from the obscuration is still more readable than MORK!
lockPref("autoadmin.global_config_url","file:///path/to/firefox,v3.0.17/share/autoconfig.js");
lockPref("autoadmin.offline_failover", true);
lockPref("autoadmin.refresh_interval", 60);

I left the MORK comment line in there to remind me how far we’ve come already.

These directives:

  • Set the autoconfig url
  • Tell *zilla to automatically fail over to offline mode if online browsing fails
  • Re-fetch the autoconfig file every 60 minutes

Any URL *zilla understands is a valid value for autoadmin.global_config_url meaning you could house the autoconfig script on a web server.

Away you go

Now your Mozilla product will read and execute the javascript autoconfig script you indicated.  There you can set or lock application preferences using a specialized XPCOM API.  I will cover the API in a following post.