Sunday, October 2. 2011SquirrelMail Change Password Plugin
A couple of years ago I've shared my expierience about how to setup virtual mail hosting with Postfix and Dovecot. Some time ago I needed to make that accessible via Web. So, a webmail.
After trying a couple of engines I've decided in favour of SquirrelMail. This worked like a charm with SSL, etc. so the full mailing functionality was available just out of the box. The one thing I was still missing was a possibility to change password. There are already several plugins to achieve the goal, for instance when logging in over LDAP, but nothing could really fit for me. That's because the maildir setup I'm using is non standard. Luckily, the SquirrelMail API is simple and just well documented, so I was able to write a custom change password plugin very fast. First creating the plugin directory within the SquirrelMail tree:
BASH: user@host:/www$ mkdir plugins/chpassThen, we need a file named setup.php inside the plugin dir: PHP: <?php function squirrelmail_plugin_init_chpass() { global $squirrelmail_plugin_hooks; $squirrelmail_plugin_hooks['optpage_register_block']['chpass'] = 'plugin_chpass_optpage_register_block'; } function plugin_chpass_optpage_register_block() { global $optpage_blocks; $optpage_blocks[] = array( 'name' => _("Change Password"), 'url' => '../plugins/chpass/options.php', 'desc' => _("Change your current mail account password"), 'js' => false ); } Here, the function squirrelmail_plugin_init_chpass registers a block handle. The block will be shown inside the options page. The next step is to create the actual block handler, which would visualize the change password page with a form. It also will handle that form. See options.php: PHP: <?php define('SM_PATH','../../'); /* SquirrelMail required files. */ require_once(SM_PATH . 'include/validate.php'); require_once(SM_PATH . 'functions/strings.php'); require_once(SM_PATH . 'functions/page_header.php'); require_once(SM_PATH . 'functions/display_messages.php'); require_once(SM_PATH . 'include/load_prefs.php'); displayPageHeader($color, 'None'); $error = NULL; $dovecotpw = "/usr/sbin/dovecotpw"; $base_mail_path = "/my/vdomains/path"; $save_path = "/my/new_pass_tmp"; if (isset($_POST['changepw'])) { $oldpw = $_POST['oldpw']; $newpw = $_POST['newpw']; $newpwrep = $_POST['newpwrep']; $email = $_SESSION['username']; $tmp = explode("@", $email); $name = $tmp[0]; $domain = $tmp[1]; $tmp = plugin_chpass_get_user_hash_data($name, $domain, $base_mail_path); // XXX catch error tmp is empty $hash = plugin_chpass_gen_dovecot_hash($dovecotpw, $tmp['schema'], $oldpw); if ($hash == '{' . $tmp['schema'] . '}' . $tmp['hash']) { if ($newpw == $newpwrep && !empty($newpw) && !empty($newpwrep)) { $gen_hash = plugin_chpass_gen_dovecot_hash( $dovecotpw, $tmp['schema'], $newpw ); $line = plugin_chpass_produce_hash_line( $email, $gen_hash, $base_mail_path ); $fname = "$save_path/" . md5(uniqid()); while(file_exists($fname)) { $fname = "$save_path/" . md5(uniqid()); } file_put_contents($fname, $line); echo "<b>password changed and will be soon active, ", "<a target=\"_top\" href=\"/src/signout.php\">", "logout</a> and login in a minute.</b>"; } else { if (empty($newpw)) { $error = "new password is empty"; } else if (empty($newpwrep)) { $error = "new password repetition is empty"; } else { $error = "passwords do not match"; } } } else { $error = "Old password didn't match"; } } if (!isset($_POST['changepw']) || !empty($error)) { if (!empty($error)) { echo "<b>$error</b><br/>"; } ?> <form action="<?php echo $SERVER['PHP_SELF']; ?>" method="post"> <table> <tr><td>old password</td><td><input type="password" name="oldpw" value="" /></td></tr> <tr><td>new password</td><td><input type="password" name="newpw" value="" /></td></tr> <tr><td>repeat new password</td><td><input type="password" name="newpwrep" value="" /></td></tr> <tr><td colspan="2"><input type="submit" name="changepw" value="Change current password"></td></tr> </table> </form> <?php } function plugin_chpass_gen_dovecot_hash($dovecotpw, $schema, $password) { $cmd = "$dovecotpw -s " . escapeshellarg($schema) . " -p " . escapeshellarg($password); exec($cmd, $out); return $out[0]; } function plugin_chpass_get_user_hash_data($user, $domain, $base_mail_path) { $retval = array('hash' => '', 'schema' => ''); /* * line looks like * mail@example.com:{CRAM-MD5}09146fe849a4e6634bee8cd4b3dedda200a076c8a22db1f95766b888397cc58d:1042:1033:::: */ $all = file("$base_mail_path/$domain/etc/passwd"); foreach($all as $line) { $data = explode(":", trim($line)); if ("#" == $data[0][0]) { continue; } if ("$user@$domain" == $data[0]) { if(preg_match(',\{(.*)\}(.*),', $data[1], $m)) { $retval['hash'] = $m[2]; $retval['schema'] = $m[1]; break; } } } return $retval; } function plugin_chpass_produce_hash_line($email, $gen_hash, $base_mail_path) { $retval = ''; $tmp = explode('@', $email); $all = file("$base_mail_path/{$tmp[1]}/etc/passwd"); foreach($all as $line) { $data = explode(":", trim($line)); if ("#" == $data[0][0]) { continue; } if ($data[0] == $email) { $data[1] = $gen_hash; $retval = implode(':', $data); } } return $retval; } As filesystem is used for the hosting setup, writing the configuration directly from the web would not be really secure way to go. Therefore the new config file line is saved in some temporary location first only readable for the webserver user. Otherwise we would need to make configuration files world readable or something even worst. Writing to a temporary location we also ensure there is no posibility to access important system data. The final step would be then to create a cronjob to actually update the vmail configuration. That's it could look like: BASH: #!/bin/sh NEW_PASS_PATH=/my/new_pass_tmp BASE_MAIL_PATH=/my/vdomains/path ls -1tr "$NEW_PASS_PATH" | while read FNAME do PASS_FNAME="$NEW_PASS_PATH/$FNAME" PASS_LINE=`cat "$PASS_FNAME"` PASS_EMAIL=`echo "$PASS_LINE" | cut -d: -f1` PASS_DOMAIN=`echo "$PASS_EMAIL" | cut -d@ -f2` #echo $EMAIL $DOMAIN TMP=`tempfile` CONF_FNAME="$BASE_MAIL_PATH/$PASS_DOMAIN/etc/passwd" cat "$CONF_FNAME" | while read CONF_LINE do CONF_EMAIL=`echo "$CONF_LINE" | cut -d: -f1` if [ "$PASS_EMAIL" = "$CONF_EMAIL" ] then echo "$PASS_LINE" >> "$TMP" else echo "$CONF_LINE" >> "$TMP" fi done # move to the config and set perms install -m 644 -g vmail -o vmail "$TMP" "$CONF_FNAME" rm "$PASS_FNAME" rm "$TMP" done Just let it run every minute as a privileged user and that's it. After changing a password user will be kicked out almost immediately and a login with the new password will be forced. Some time ago I thougt about moving the config setup into the DB. But as there was no real need to do this, nothing happened. With the configuration stored in the DB the cronjob step can be of course avoided. Despite the SquirrelMail API is PHP4 style, which is weird, it works wery well and stable. That's what one needs for a webmail service, don't you agree? Ah, of course, we should activate the new plugin. Just invoke ./configure within your SquirrelMail webroot, choose plugins (8 in my case) and then the number your plugin is listed under. Go into your account and enjoy. Now there is the end of the story. Thanks SquirrelMail for the excellent work, and thank you for attention. Bye. Trackbacks
Trackback specific URI for this entry
No Trackbacks
|
CategoriesQuicksearchArchives |