intro | fun | stuff | photos

Integrating IMP webmail with Active Directory and Exchange

This page describes the process I went through to build a Horde/IMP webmail system that integrated well with Exchange 2003 and Active Directory. It is not a detailed howto but a guide that assumes some familiarity with Linux systems, web servers and PHP.

At the time of writing, my employer has predominantly a Windows 2003 server environment, with Windows 2000 desktops throughout the organisation. As the foundation we have Active Directory providing AAA and directory services and Exchange 2003 handling email.

Exchange 2003 has a built-in webmail server that functions adequately within a local network environment. It is a rather hefty program that tries to emulate everything from the dedicated client and cram it all into a web interface. The two concerns I had over using it to provide external access to email (ie. via the internet) were security and bloat. I looked to IMP as an alternative means of accessing corporate email outside the company and decided that it could work quite well.

The feature requirements were, most basically, to be able to read and send email via an externally accessible web site using a simple interface that would not confuse users or introduce a lengthy learning curve. What I ended up with was that plus integration with the Exchange global address book, proper recording of sent mail, spell checking, folder management and personal identity details sourced directly from Active Directory.

The system is based on Debian GNU/Linux. I did a base install of Debian 3.0r3, skipping task and package selection and opting to install packages as necessary once development was underway. The apt sources were changed to use the testing tree and a dist upgrade followed. As part of this it upgraded itself to the 2.4 kernel. I upgraded it from there to 2.6 for better usb and network driver support. I had problems with kernel modules not loading until I realised that modutils also had to be upgraded with the change to 2.6. With the new version of modutils installed kernel modules loaded as they ought and the basic system seemed to be operating well.

Apache-SSL was installed using apt, and as part of it a site certificate was created. This later was replaced with a new certificate generated along with a certificate signing request that was processed by CAcert, an aspiring root CA that provides free signed certificates. Given that at this point this is merely a test system I could not justify spending some dollars on a browser-recognised CA. If the system goes live for more than our department then it will be necessary to fork out the dough.

With Apache running the next step was to begin with Horde. The Debian testing distribution's version of IMP is already out of date. Instead I obtained the 3.2 version of IMP, and 2.2.7 Horde from www.horde.org. This also required updating PEAR and PHP to later versions than those included in the Debian testing pool. Horde includes test.php, which reports on which dependencies have been satisfied. I should also note that Horde, IMP and Turba were installed according to the included instructions. As they are related applications there are some options to be enabled to make them work better together.

Horde resides in /var/www/horde, and IMP is in /var/www/horde/imp. There is an option within Horde to allow IMP to handle all authentication, to avoid having to login once into Horde and then again into IMP to retrieve email. This option was enabled.

A change was also made to the Apache configuration. The document root was set to the IMP directory and a separate /horde alias established to point to the next level up. This was solely so that users could go to http://server/ instead of http://server/horde/imp/ to login.

DocumentRoot /var/www/horde/imp
Alias /horde /var/www/horde

Users are not permitted to select from options of different servers, or enter their own server details. This is done in IMP's conf.php in the Mail Server Settings section. The server details were configured in imp/config/servers.php.

$servers['ascham'] = array(
'name' => 'Company',
'server' => 'localhost',
'protocol' => 'imap',
'port' => 143,
'folders' => '',
'namespace' => '',
'maildomain' => 'company.com.au',
'realm' => '',
'preferred' => ''
);

Somewhere in the Horde config is the option to set the default language and disable the ability to change it. Once this is done there are just username and password fields on the login screen (and a submit button). A final touch is to edit the Horde registry.php to change the logon screen from "Welcome to Horde" to something more appropriate.

'name' => _("Company Webmail")

As can be seen above, the server IMP knows about is running on the same machine as IMP itself. This is because an IMAP proxy is running on the webmail box that talks to the real server. Of course, IMAP services must be enabled on the real server as well. IMP, being a stateless web client, needs to create a new IMAP connection every time a client requests some action on a mailbox. This is slow and load intensive. An IMAP proxy holds a connection open for an extended period of time to the real server, and so IMP needs only to authenticate locally to the IMAP proxy to pick up the same connection it has been using until now. This takes some load off the real mail server and is a bit quicker.

In IMP's prefs.php, and this is important for proper integration with Outlook, there is a section called Personal Information preferences. In the sent mail folder section, 'value' => 'sent-mail' should be changed to 'value' => 'Sent Items', which is what Outlook uses. Similarly, the trash folder should be changed from 'trash' to 'Deleted Items'.

In that same file there are some more changes to make that are required to integrate with the Exchange global address list. These are related to further when Turba is configured.

$_prefs['search_sources'] = array(
'value' => "company_staff\tcompany_altstaff",
'locked' => false,
'shared' => true,
'type' => 'implicit'
);

$_prefs['search_fields'] = array(
'value' => "company_staff\tname\tcompany_altstaff\tname",
'locked' => false,
'shared' => false,
'type' => 'implicit'
);

Lastly, conf.php now includes a file with some php that provides some site specific data.

if(!function_exists('imp_expand_fromaddress')) {
function imp_expand_fromaddress($imp) {
$ldapServer = 'ldap-host';
$ldapPort = '389';

$authUser = 'domain\\' . Auth::getAuth();
$authPass = Auth::getCredential('password');

$searchBase = 'ou=Staff,dc=internal,dc=company,dc=com,dc=au';
$altBase = 'cn=Users,dc=internal,dc=company,dc=com,dc=au';

$name = '';

$ds = ldap_connect($ldapServer, $ldapPort);
if($ds) {
$bind = ldap_bind($ds, $authUser, $authPass);
if($bind) {
$searchResult = ldap_search($ds, $searchBase, 'sAMAccountName=' . $imp['user']);
$info = ldap_get_entries($ds, $searchResult);
if(count($info) > 1) {
$name = $info[0]['mail'][0];
} else {
// check for staff not in ou=Staff
$searchResult = ldap_search($ds, $altBase, 'sAMAccountName=' . $imp['user']);
$info = ldap_get_entries($ds, $searchResult);
if(count($info) > 1) {
$name = $info[0]['mail'][0];
}
}

}
ldap_close($ds);
}

if(!strlen($name)) {
$name = $imp['user'];
}

return $name;
}
}

if(!function_exists('imp_set_fullname')) {
function imp_set_fullname($imp) {
$ldapServer = 'ldap-host';
$ldapPort = '389';

$authUser = 'domain\\' . Auth::getAuth();
$authPass = Auth::getCredential('password');

$searchBase = 'ou=Staff,dc=internal,dc=company,dc=com,dc=au';
$altBase = 'cn=Users,dc=internal,dc=company,dc=com,dc=au';

$name = '';

$ds = ldap_connect($ldapServer, $ldapPort);
if($ds) {
$bind = ldap_bind($ds, $authUser, $authPass);
if($bind) {
$searchResult = ldap_search($ds, $searchBase, 'sAMAccountName=' . $imp['user']);

$info = ldap_get_entries($ds, $searchResult);

if(count($info) > 1) {
$name = $info[0]['displayName'][0];
} else {
$searchResult = ldap_search($ds, $altBase, 'sAMAccountName=' . $imp['user']);
if(count($info) > 1) {
$name = $info[0]['displayName'][0];
}
}
if(!strlen($name)) {
$name = $info[0]['cn'][0];
}
}
ldap_close($ds);
}

if(!$name) {
$name = $imp['user'];
}

return $name;
}
}

This code speaks to ldap-host, a name pointing to an Active Directory server, and retrieves user details from the LDAP database (specifically, a user's email address and full name). AD does not permit this to be done as an anonymous bind, so instead we bind with the user credentials that our email user has logged in with. Alternatively we could hard code authentication information into this script, or permit users to configure their own details. This adds an extra burdon on the user and requires us to record the user's preferences somewhere (typically a mysql database).

Turba 1.2.2 was installed to allow users to access the Exchange address book. Turba's config/sources.php was the only file that really needed to be modified. This was modified to get user credentials (like in the code above) and bind to the Active Directory server with a root like 'ou=Staff,dc=internal,dc=company,dc=com,dc=au'

$did = 'domain\\' . Auth::getAuth();
$dpass = Auth::getCredential('password');

$cfgSources['company_staff'] = array(
'title' => 'Company Staff',
'type' => 'ldap',
'params' => array(
'server' => 'ldap-host',
'port' => 389,
'root' => 'ou=Staff,dc=internal,dc=company,dc=com,dc=au',
'bind_dn' => $did,
'bind_password' => $dpass,
'dn' => array('cn'),
'objectclass' => '*',
'version' => 3
),
'map' => array(
'__key' => 'dn',
'name' => 'cn',
'email' => 'mail',
),
'search' => array(
'name',
'email',
),
'strict' => array(
'dn'
),
'public' => true,
'readonly' => true,
'admin' => array(),
'export' => true
);

With this done I was able to search the address book directly with Turba, and within IMP when composing new mail. The limitation I have with it currently is that there are two main organisational units, referred to here loosely as company_staff and company_altstaff - so I have two stanzas in Turba's sources (only one is shown above). This also means that users are forced to choose which address book to search through when they wish to find an email address. While this suits my environment fine, it may not be ideal for those elsewhere who may have many OUs to deal with. There may be a way to configure Turba to account for this without delving too deeply into its internals but I have not found it.

If you have read this page and found it useful or think of something that should be added to it then I would like to hear from you. Please send me an email if you have something to say.