Merge pull request #200 from doktoil-makresh/master

Support for password expiration, managed in PostFix Admin
pull/215/head
David Goodwin 6 years ago committed by GitHub
commit 69e234f668
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,48 @@
*Description
This extension adds support for password expiration.
It is designed to have expiration on users passwords. An email is sent when the password is expiring in 30 days, then 14 days, then 7 days.
It is strongly inspired by https://abridge2devnull.com/posts/2014/09/29/dovecot-user-password-expiration-notifications-updated-4122015/, and adapted to fit with Postfix Admin & Roundcube's password plugin
Expiration unit is day
Expiration value for domain is set through Postfix Admin GUI
*Installation
Perform the following changes:
**Changes in MySQL/MariaDB mailbox table (as defined in $CONF['database_tables'] from config.inc.php):
You are invited to backup your DB first, and ensure the table name is correct.
Execute the attached SQL script (password_expiration.sql) that will add the required columns. The expiration value for existing users will be set to 90 days. If you want a different value, edit line 2 in the script and replace 90 by the required value.
**Changes in Postfix Admin :
To enable password expiration, add the following to your config.inc.php file:
$CONF['password_expiration_enabled'] = 'YES';
All my tests are performed using $CONF['encrypt'] = 'md5crypt';
**If you are using Roundcube's password plugin, you should also adapt the $config['password_query'] value.
I recommend to use:
$config['password_query'] = 'UPDATE mailbox SET password=%c, modified = now(), password_expiry = now() + interval 90 day';
of cource you may adapt to the expected expiration value
All my tests are performed using $config['password_algorithm'] = 'md5-crypt';
**Changes in Dovecot (adapt if you use another LDA)
Edit dovecot-mysql.conf file, and replace the user_query (and only this one) by this query:
password_query = SELECT username as user, password, concat('/var/vmail/', maildir) as userdb_var, concat('maildir:/var/vmail/', maildir) as userdb_mail, 20001 as userdb_uid, 20001 as userdb_gid, m.domain FROM mailbox m, domain d where d.domain = m.domain and m.username = '%u' AND m.active = '1' AND (m.pw_expires_on > now() or d.password_expiration_value = 0)
Of course you may require to adapt the uid, gid, maildir and table to your setup
**Changes in system
You need to have a script running on a daily basis to check password expiration and send emails 30, 14 and 7 days before password expiration (script attached: check_mailpass_expiration.sh).
Edit the script to adapt the variables to your setup.
This script is using postfixadmin.my.cnf to read credentials. Edit this file to enter a DB user that is allowed to access (read-write) your database. This file should be protected from any user (chmod 400).

@ -0,0 +1,20 @@
#!/bin/bash
#Adapt to your setup
POSTFIX_DB="postfix_test"
MYSQL_CREDENTIALS_FILE="postfixadmin.my.cnf"
REPLY_ADDRESS=noreply@example.com
# Change this list to change notification times and when ...
for INTERVAL in 30 14 7
do
LOWER=$(( $INTERVAL - 1 ))
QUERY="SELECT username,password_expiry FROM mailbox WHERE password_expiry > now() + interval $LOWER DAY AND password_expiry < NOW() + interval $INTERVAL DAY"
mysql --defaults-extra-file="$MYSQL_CREDENTIALS_FILE" "$POSTFIX_DB" -B -e "$QUERY" | while read -a RESULT ; do
echo -e "Dear User, \n Your password will expire on ${RESULT[1]}" | mail -s "Password 30 days before expiration notication" -r $REPLY_ADDRESS ${RESULT[0]}
done
done

@ -516,6 +516,16 @@ $CONF['show_undeliverable']='YES';
$CONF['show_undeliverable_color']='tomato'; $CONF['show_undeliverable_color']='tomato';
// mails to these domains will never be flagged as undeliverable // mails to these domains will never be flagged as undeliverable
$CONF['show_undeliverable_exceptions']=array("unixmail.domain.ext","exchangeserver.domain.ext"); $CONF['show_undeliverable_exceptions']=array("unixmail.domain.ext","exchangeserver.domain.ext");
// show mailboxes with expired password
$CONF['show_expired']='YES';
$CONF['show_expired_color']='orange';
// show vacation enabled mailboxes
$CONF['show_vacation']='YES';
$CONF['show_vacation_color']='turquoise';
// show disabled accounts
$CONF['show_disabled']='YES';
$CONF['show_disabled_color']='grey';
// show POP/IMAP mailboxes
$CONF['show_popimap']='YES'; $CONF['show_popimap']='YES';
$CONF['show_popimap_color']='darkgrey'; $CONF['show_popimap_color']='darkgrey';
// you can assign special colors to some domains. To do this, // you can assign special colors to some domains. To do this,
@ -661,6 +671,11 @@ $CONF['theme_custom_css'] = '';
// change to boolean true to enable xmlrpc // change to boolean true to enable xmlrpc
$CONF['xmlrpc_enabled'] = false; $CONF['xmlrpc_enabled'] = false;
//Account expiration info
//If you want to display the password expiracy status of the accounts (read-only)
//More details in README.password_expiration
$CONF['password_expiration_enable'] = 'YES';
// If you want to keep most settings at default values and/or want to ensure // If you want to keep most settings at default values and/or want to ensure
// that future updates work without problems, you can use a separate config // that future updates work without problems, you can use a separate config
// file (config.local.php) instead of editing this file and override some // file (config.local.php) instead of editing this file and override some

@ -260,6 +260,18 @@ function check_domain($domain) {
return ''; return '';
} }
/**
* Get password expiration value for a domain
* @param string $domain - a string that may be a domain
* @return int password expiration value for this domain (DAYS, or zero if not enabled)
*/
function get_password_expiration_value ($domain) {
$table_domain = table_by_key('domain');
$query = "SELECT password_expiry FROM $table_domain WHERE domain='$domain'";
$result = db_query ($query);
$password_expiration_value = db_array ($result['result']);
return $password_expiration_value[0];
}
/** /**
* check_email * check_email
@ -1871,7 +1883,7 @@ function db_delete($table, $where, $delete, $additionalwhere='') {
* @param array $timestamp (optional) - array of fields to set to now() - default: array('created', 'modified') * @param array $timestamp (optional) - array of fields to set to now() - default: array('created', 'modified')
* @return int - number of inserted rows * @return int - number of inserted rows
*/ */
function db_insert($table, $values, $timestamp = array('created', 'modified')) { function db_insert ($table, $values, $timestamp = array('created', 'modified'), $timestamp_expiration = array('password_expiry') ) {
$table = table_by_key($table); $table = table_by_key($table);
foreach (array_keys($values) as $key) { foreach (array_keys($values) as $key) {
@ -1886,6 +1898,19 @@ function db_insert($table, $values, $timestamp = array('created', 'modified')) {
} }
} }
global $CONF;
if ($CONF['password_expiration_enabled'] == 'YES') {
if ($table == 'mailbox') {
$domain_dirty = $values['domain'];
$domain = substr($domain_dirty, 1, -1); // really the update to the mailbox password_expiry should be based on a trigger, or a query like :
// .... NOW() + INTERVAL domain.password_expiry DAY
$password_expiration_value = get_password_expiration_value($domain);
foreach($timestamp_expiration as $key) {
$values[$key] = "now() + interval " . $password_expiration_value . " day";
}
}
}
$sql_values = "(" . implode(",", escape_string(array_keys($values))).") VALUES (".implode(",", $values).")"; $sql_values = "(" . implode(",", escape_string(array_keys($values))).") VALUES (".implode(",", $values).")";
$result = db_query("INSERT INTO $table $sql_values"); $result = db_query("INSERT INTO $table $sql_values");
@ -1934,6 +1959,19 @@ function db_update_q($table, $where, $values, $timestamp = array('modified')) {
} }
} }
global $CONF;
if ($CONF['password_expiration_enabled'] == 'YES') {
$where_type = explode('=',$where);
$email = ($where_type[1]);
$domain_dirty = explode('@',$email)[1];
$domain = substr($domain_dirty, 0, -1);
if ($table == 'mailbox') {
$password_expiration_value = get_password_expiration_value($domain);
$key = 'password_expiry';
$sql_values[$key] = $key . " = now() + interval " . $password_expiration_value . " day";
}
}
$sql="UPDATE $table SET " . implode(",", $sql_values) . " WHERE $where"; $sql="UPDATE $table SET " . implode(",", $sql_values) . " WHERE $where";
$result = db_query($sql); $result = db_query($sql);
@ -2190,6 +2228,36 @@ function gen_show_status($show_alias) {
} }
} }
// Vacation CHECK
if ( $CONF['show_vacation'] == 'YES' ) {
$stat_result = db_query ("SELECT * FROM ". $CONF['database_tables']['vacation'] ." WHERE email = '" . $show_alias . "' AND active = 1");
if ($stat_result['rows'] == 1) {
$stat_string .= "<span style='background-color:" . $CONF['show_vacation_color'] . "'>" . $CONF['show_status_text'] . "</span>&nbsp;";
} else {
$stat_string .= $CONF['show_status_text'] . "&nbsp;";
}
}
// Disabled CHECK
if ( $CONF['show_disabled'] == 'YES' ) {
$stat_result = db_query ("SELECT * FROM ". $CONF['database_tables']['mailbox'] ." WHERE username = '" . $show_alias . "' AND active = 0");
if ($stat_result['rows'] == 1) {
$stat_string .= "<span style='background-color:" . $CONF['show_disabled_color'] . "'>" . $CONF['show_status_text'] . "</span>&nbsp;";
} else {
$stat_string .= $CONF['show_status_text'] . "&nbsp;";
}
}
// Expired CHECK
if ( $CONF['show_expired'] == 'YES' ) {
$stat_result = db_query ("SELECT * FROM ". $CONF['database_tables']['mailbox'] ." WHERE username = '" . $show_alias . "' AND password_expiry <= now()");
if ($stat_result['rows'] == 1) {
$stat_string .= "<span style='background-color:" . $CONF['show_expired_color'] . "'>" . $CONF['show_status_text'] . "</span>&nbsp;";
} else {
$stat_string .= $CONF['show_status_text'] . "&nbsp;";
}
}
// POP/IMAP CHECK // POP/IMAP CHECK
if ($CONF['show_popimap'] == 'YES') { if ($CONF['show_popimap'] == 'YES') {
$stat_delimiter = ""; $stat_delimiter = "";

@ -348,6 +348,10 @@ $PALANG['broadcast_mailboxes_only'] = 'Only send to mailboxes';
$PALANG['broadcast_to_domains'] = 'Send to domains:'; $PALANG['broadcast_to_domains'] = 'Send to domains:';
$PALANG['pStatus_undeliverable'] = 'maybe UNDELIVERABLE '; $PALANG['pStatus_undeliverable'] = 'maybe UNDELIVERABLE ';
$PALANG['pStatus_disabled'] = 'Account disabled ';
$PALANG['pStatus_expired'] = 'Password expired ';
$PALANG['pStatus_vacation'] = 'Vacation enabled ';
$PALANG['pStatus_custom'] = 'Delivers to '; $PALANG['pStatus_custom'] = 'Delivers to ';
$PALANG['pStatus_popimap'] = 'POP/IMAP '; $PALANG['pStatus_popimap'] = 'POP/IMAP ';
@ -407,6 +411,7 @@ $PALANG['pFetchmail_desc_returned_text'] = 'Text message from last polling';
$PALANG['dateformat_pgsql'] = 'YYYY-mm-dd'; # translators: rearrange to your local date format, but make sure it's a valid PostgreSQL date format $PALANG['dateformat_pgsql'] = 'YYYY-mm-dd'; # translators: rearrange to your local date format, but make sure it's a valid PostgreSQL date format
$PALANG['dateformat_mysql'] = '%Y-%m-%d'; # translators: rearrange to your local date format, but make sure it's a valid MySQL date format $PALANG['dateformat_mysql'] = '%Y-%m-%d'; # translators: rearrange to your local date format, but make sure it's a valid MySQL date format
$PALANG['password_expiration'] = 'Pass expires';
$PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh $PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh
/* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */ /* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */

@ -342,6 +342,9 @@ $PALANG['pBroadcast_error_empty'] = 'Les champs "Nom", "Sujet" et "Message" ne p
$PALANG['broadcast_mailboxes_only'] = 'Only send to mailboxes'; # XXX $PALANG['broadcast_mailboxes_only'] = 'Only send to mailboxes'; # XXX
$PALANG['broadcast_to_domains'] = 'Send to domains:'; # XXX $PALANG['broadcast_to_domains'] = 'Send to domains:'; # XXX
$PALANG['pStatus_undeliverable'] = 'Non délivrable '; $PALANG['pStatus_undeliverable'] = 'Non délivrable ';
$PALANG['pStatus_vacation'] = 'Répondeur activé ';
$PALANG['pStatus_disabled'] = 'Compte désactivé ';
$PALANG['pStatus_expired'] = 'Mot de passe expiré ';
$PALANG['pStatus_custom'] = 'Délivré à '; $PALANG['pStatus_custom'] = 'Délivré à ';
$PALANG['pStatus_popimap'] = 'POP/IMAP '; $PALANG['pStatus_popimap'] = 'POP/IMAP ';
$PALANG['password_too_short'] = 'Mot de passe trop court. - %s caractères minimum'; $PALANG['password_too_short'] = 'Mot de passe trop court. - %s caractères minimum';
@ -398,6 +401,7 @@ $PALANG['pFetchmail_desc_date'] = 'Date de la dernière vérification/changement
$PALANG['pFetchmail_desc_returned_text'] = 'Message de la dernière vérification'; $PALANG['pFetchmail_desc_returned_text'] = 'Message de la dernière vérification';
$PALANG['dateformat_pgsql'] = 'dd-mm-YYYY'; $PALANG['dateformat_pgsql'] = 'dd-mm-YYYY';
$PALANG['dateformat_mysql'] = '%d-%m-%Y'; $PALANG['dateformat_mysql'] = '%d-%m-%Y';
$PALANG['password_expiration'] = 'Expiration du mot de passe';
$PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh $PALANG['please_keep_this_as_last_entry'] = ''; # needed for language-check.sh
/* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */ /* vim: set expandtab ft=php softtabstop=3 tabstop=3 shiftwidth=3: */

@ -94,6 +94,7 @@ class DomainHandler extends PFAHandler {
'default_aliases' => pacol($this->new, $this->new, 0, 'bool', 'pAdminCreate_domain_defaultaliases', '' , 1,'', /*not in db*/ 1 ), 'default_aliases' => pacol($this->new, $this->new, 0, 'bool', 'pAdminCreate_domain_defaultaliases', '' , 1,'', /*not in db*/ 1 ),
'created' => pacol(0, 0, 0, 'ts', 'created' , '' ), 'created' => pacol(0, 0, 0, 'ts', 'created' , '' ),
'modified' => pacol(0, 0, $super, 'ts', 'last_modified' , '' ), 'modified' => pacol(0, 0, $super, 'ts', 'last_modified' , '' ),
'password_expiry' => pacol($super, $super, $super, 'num', 'password_expiration' , 'password_expiration_desc', ''),
'_can_edit' => pacol(0, 0, 1, 'int', '' , '' , 0 , '_can_edit' => pacol(0, 0, 1, 'int', '' , '' , 0 ,
/*options*/ '', /*options*/ '',
/*not_in_db*/ 0, /*not_in_db*/ 0,

@ -49,6 +49,7 @@ class MailboxHandler extends PFAHandler {
'token_validity' => pacol(1, 0, 0, 'ts', '' , '', date("Y-m-d H:i:s",time())), 'token_validity' => pacol(1, 0, 0, 'ts', '' , '', date("Y-m-d H:i:s",time())),
'created' => pacol(0, 0, 1, 'ts', 'created' , '' ), 'created' => pacol(0, 0, 1, 'ts', 'created' , '' ),
'modified' => pacol(0, 0, 1, 'ts', 'last_modified' , '' ), 'modified' => pacol(0, 0, 1, 'ts', 'last_modified' , '' ),
'password_expiry' => pacol(0, 0, 1, 'ts', 'password_expiration' , '' ),
# TODO: add virtual 'notified' column and allow to display who received a vacation response? # TODO: add virtual 'notified' column and allow to display who received a vacation response?
); );

@ -0,0 +1,3 @@
ALTER TABLE mailbox ADD COLUMN password_expiry TIMESTAMP DEFAULT now() not null;
UPDATE mailbox set password_expiry = now() + interval 90 day;
ALTER TABLE domain ADD COLUMN password_expiry int DEFAULT 0;

@ -0,0 +1,3 @@
[client]
user=postfix_read_write_account
password=strong_password

@ -165,6 +165,7 @@ $tAlias = $handler->result();
# #
$display_mailbox_aliases = Config::bool('alias_control_admin'); $display_mailbox_aliases = Config::bool('alias_control_admin');
$password_expiration = Config::bool('password_expiration');
# build the sql query # build the sql query
$sql_select = "SELECT $table_mailbox.* "; $sql_select = "SELECT $table_mailbox.* ";
@ -190,6 +191,10 @@ if ($display_mailbox_aliases) {
$sql_join .= " LEFT JOIN $table_alias ON $table_mailbox.username=$table_alias.address "; $sql_join .= " LEFT JOIN $table_alias ON $table_mailbox.username=$table_alias.address ";
} }
if ($password_expiration) {
$sql_select .= ", $table_mailbox.password_expiry as password_expiration ";
}
if (Config::bool('vacation_control_admin')) { if (Config::bool('vacation_control_admin')) {
$table_vacation = table_by_key('vacation'); $table_vacation = table_by_key('vacation');
$sql_select .= ", $table_vacation.active AS v_active "; $sql_select .= ", $table_vacation.active AS v_active ";

@ -88,11 +88,11 @@ function _upgrade_filter_function($name) {
return preg_match('/upgrade_[\d]+(_mysql|_pgsql|_sqlite|_mysql_pgsql)?$/', $name) == 1; return preg_match('/upgrade_[\d]+(_mysql|_pgsql|_sqlite|_mysql_pgsql)?$/', $name) == 1;
} }
function _db_add_field($table, $field, $fieldtype, $after) { function _db_add_field($table, $field, $fieldtype, $after = '') {
global $CONF; global $CONF;
$query = "ALTER TABLE " . table_by_key($table) . " ADD COLUMN $field $fieldtype"; $query = "ALTER TABLE " . table_by_key($table) . " ADD COLUMN $field $fieldtype";
if ($CONF['database_type'] == 'mysql') { if ($CONF['database_type'] == 'mysql' && !empty($after)) {
$query .= " AFTER $after "; # PgSQL does not support to specify where to add the column, MySQL does $query .= " AFTER $after "; # PgSQL does not support to specify where to add the column, MySQL does
} }
@ -1760,3 +1760,8 @@ function upgrade_1841_sqlite() {
_db_add_field($table, 'token_validity', '{DATETIME}', 'token'); _db_add_field($table, 'token_validity', '{DATETIME}', 'token');
} }
} }
function upgrade_1842() {
_db_add_field('mailbox', 'password_expiry', "{DATETIME}"); // when a specific mailbox password expires
_db_add_field('domain', 'password_expiry', 'int DEFAULT 0'); // expiry applied to mailboxes within that domain
}

@ -71,6 +71,16 @@
{if $CONF.show_undeliverable===YES} {if $CONF.show_undeliverable===YES}
&nbsp;<span style='background-color:{$CONF.show_undeliverable_color};'>{$CONF.show_status_text}</span>={$PALANG.pStatus_undeliverable} &nbsp;<span style='background-color:{$CONF.show_undeliverable_color};'>{$CONF.show_status_text}</span>={$PALANG.pStatus_undeliverable}
{/if} {/if}
{if $CONF.show_vacation===YES}
&nbsp;<span style='background-color:{$CONF.show_vacation_color};'>{$CONF.show_status_text}</span>={$PALANG.pStatus_vacation}
{/if}
{if $CONF.show_disabled===YES}
&nbsp;<span style='background-color:{$CONF.show_disabled_color};'>{$CONF.show_status_text}</span>={$PALANG.pStatus_disabled}
{/if}
{if $CONF.show_expired===YES}
&nbsp;<span style='background-color:{$CONF.show_expired_color};'>{$CONF.show_status_text}</span>={$PALANG.pStatus_expired}
{/if}
{if $CONF.show_popimap===YES} {if $CONF.show_popimap===YES}
&nbsp;<span style='background-color:{$CONF.show_popimap_color};'>{$CONF.show_status_text}</span>={$PALANG.pStatus_popimap} &nbsp;<span style='background-color:{$CONF.show_popimap_color};'>{$CONF.show_status_text}</span>={$PALANG.pStatus_popimap}
{/if} {/if}

@ -13,6 +13,9 @@
<td>{$PALANG.name}</td> <td>{$PALANG.name}</td>
{if $CONF.quota===YES}<td>{$PALANG.pOverview_mailbox_quota}</td>{/if} {if $CONF.quota===YES}<td>{$PALANG.pOverview_mailbox_quota}</td>{/if}
<td>{$PALANG.last_modified}</td> <td>{$PALANG.last_modified}</td>
{if $CONF.password_expiration===YES}
<td>{$PALANG.password_expiration}</td>
{/if}
<td>{$PALANG.active}</td> <td>{$PALANG.active}</td>
{assign var="colspan" value="`$colspan-6`"} {assign var="colspan" value="`$colspan-6`"}
<td colspan="{$colspan}">&nbsp;</td> <td colspan="{$colspan}">&nbsp;</td>
@ -74,6 +77,9 @@
</td> </td>
{/if} {/if}
<td>{$item.modified}</td> <td>{$item.modified}</td>
{if $CONF.password_expiration===YES}
<td>{$item.password_expiration}</td>
{/if}
<td><a href="{#url_editactive#}mailbox&amp;id={$item.username|escape:"url"}&amp;active={if ($item.active==0)}1{else}0{/if}&amp;token={$smarty.session.PFA_token|escape:"url"}" <td><a href="{#url_editactive#}mailbox&amp;id={$item.username|escape:"url"}&amp;active={if ($item.active==0)}1{else}0{/if}&amp;token={$smarty.session.PFA_token|escape:"url"}"
>{if $item.active==1}{$PALANG.YES}{else}{$PALANG.NO}{/if}</a></td> >{if $item.active==1}{$PALANG.YES}{else}{$PALANG.NO}{/if}</a></td>
{if $CONF.vacation_control_admin===YES && $CONF.vacation===YES} {if $CONF.vacation_control_admin===YES && $CONF.vacation===YES}

Loading…
Cancel
Save