Продолжаю потихоньку совершенствовать свою почтовую систему, параллельно разрабатывая веб-интерфейс для управления всем этим хозяйством. Этот пост по сути носит лишь отчётный характер и не предназначен для внедрения кем бы то ни было в практику. С другой стороны, знающие люди могут заинтересоваться описанными здесь идеями и реализовать нечто подобное у себя.
Месяца два назад аккуратно поэкспериментировал с таблицей virtual_alias_maps и обнаружил, что в случае, если запрос для одного псевдонима возвращает два адреса, Postfix отправляет письмо на оба адреса.
Таким образом можно делать рассылки штатными средствами самого Postfix. Можно добавить в таблицу псевдонимов несколько записей с одинаковым псевдонимом, но с разными получателями и получить простейший список рассылки.
Потом я подумал о том, что между такой рассылкой и копированием входящей почты нескольким адресатам фактически нет никакой разницы. Разница только лишь в том, куда подставлена эта карта - в recipient_bcc_maps или в virtual_alias_maps. Потом я подумал о том, что обе функции можно объединить в одной таблице, если учитывать существование адреса в таблице пользователей и статус этого пользователя - включен или отключен. Если пользователь включен, то он должен получить своё письмо в свой ящик, а вместе с ним все те, кто подписан на его входящую почту - это поведение для recipient_bcc_maps. Если пользователь отключен или он не существует, то почту должны получить лишь те, кто подписан на входящую почту этого адреса - это поведение virtual_alias_maps. Добавим в таблицу ещё дополнительную колонку, которая позволит отличать подписку на исходящую почту и входящую почту и получим совмещённую таблицу алиасов, подписок, пересылок и скрытых копий входящей и исходящей почты.
Вот и запрос для создания соответствующей таблицы:
CREATE TABLE `subscription` ( `id` int(10) unsigned NOT NULL auto_increment, `direction` varchar(1) NOT NULL default 'I', `email` varchar(255) character set latin1 NOT NULL, `recipient` varchar(255) character set latin1 NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `subscription` (`direction`,`email`,`recipient`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
Запрос для создания таблицы пользователей/ящиков у меня выглядит так:
CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL auto_increment, `active` varchar(1) default 'Y', `email` varchar(255) character set latin1 NOT NULL, `password` varchar(255) character set latin1 NOT NULL default '', `surname` varchar(255) NOT NULL default '', `name` varchar(255) NOT NULL default '', `patronym` varchar(255) NOT NULL default '', `lastip` varchar(32) default NULL, `lasttime` datetime default NULL, `bytes` bigint(20) default NULL, `messages` int(11) default NULL, `max_bytes` bigint(20) unsigned NOT NULL default '1073741824', `max_messages` int(10) unsigned NOT NULL default '1000', `ad_login` varchar(255) character set latin1 NOT NULL default '', `phones` varchar(255) NOT NULL default '', `position` varchar(255) NOT NULL default '', `department` varchar(255) NOT NULL default '', PRIMARY KEY (`id`), UNIQUE KEY `email` (`email`), UNIQUE KEY `fullname` (`surname`,`name`,`patronym`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
И, наконец, запрос для создания таблицы доменов/транспортов выглядит так:
CREATE TABLE `domain` ( `id` int(10) unsigned NOT NULL auto_increment, `domain` varchar(255) character set latin1 NOT NULL, `transport` varchar(255) character set latin1 NOT NULL default 'dovecot:', PRIMARY KEY (`id`), UNIQUE KEY `domain` (`domain`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
В файле /etc/postfix/main.cf имеется 5 карт для работы с этими таблицами:
transport_maps = mysql:/etc/postfix/mysql/transport.cf virtual_mailbox_maps = mysql:/etc/postfix/mysql/user.cf virtual_alias_maps = mysql:/etc/postfix/mysql/subscription.cf sender_bcc_maps = mysql:/etc/postfix/mysql/sender_bcc.cf recipient_bcc_maps = mysql:/etc/postfix/mysql/recipient_bcc.cf
Ещё одна карта используется для отказа в приёме писем для тех адресатов, у которых переполнился ящик:
check_recipient_access mysql:/etc/postfix/mysql/quota.cf
Ещё одна карта используется для аутентификации POP-before-SMTP (и для IMAP-before-SMTP):
mysql:/etc/postfix/mysql/pop-before-smtp.cf
Вот сами эти карты. Первая - /etc/postfix/mysql/transport.cf:
user = postfix password = postfix_password dbname = mail hosts = 127.0.0.1 query = SELECT transport FROM domain WHERE domain='%s'
Вторая - /etc/postfix/mysql/user.cf:
user = postfix
password = postfix_password
dbname = mail
hosts = 127.0.0.1
query = SELECT CONCAT(SUBSTRING_INDEX(email, '@', -1), '/', SUBSTRING_INDEX(email, '@', 1), '/')
FROM user
WHERE email='%s'
Третья - /etc/postfix/mysql/subscription.cf:
user = postfix
password = postfix_password
dbname = mail
hosts = 127.0.0.1
query = SELECT recipient
FROM subscription
WHERE direction='I'
AND email='%s'
AND email NOT IN (SELECT email
FROM user
WHERE email='%s'
AND active='Y')
Четвёртая - /etc/postfix/mysql/sender_bcc.cf:
user = postfix
password = postfix_password
dbname = mail
hosts = 127.0.0.1
query = SELECT recipient
FROM subscription
WHERE direction='O'
AND email='%s'
AND email IN (SELECT email
FROM user
WHERE email='%s'
AND active='Y')
Пятая - /etc/postfix/mysql/recipient_bcc.cf:
user = postfix
password = postfix_password
dbname = mail
hosts = 127.0.0.1
query = SELECT recipient
FROM subscription
WHERE direction='I'
AND email='%s'
AND email IN (SELECT email
FROM user
WHERE email='%s'
AND active='Y')
Шестая - /etc/postfix/mysql/quota.cf:
user = postfix
password = postfix_password
dbname = mail
hosts = 127.0.0.1
query = SELECT '452 Mailbox is over quota'
FROM user
WHERE email = '%s'
AND ((bytes >= max_bytes AND max_bytes > 0)
OR (messages >= max_messages AND max_messages > 0))
Седьмая - /etc/postfix/mysql/pop-before-smtp.cf:
user = postfix
password = postfix_password
dbname = mail
hosts = 127.0.0.1
query = SELECT DISTINCT lastip
FROM user
WHERE lastip='%s'
AND ADDTIME(lasttime, '0:1:0') > NOW()
Для реализации POP-before-SMTP и IMAP-before-SMTP сделаны два скрипта, которые вызываются сразу после аутентификации в Dovecot.
Первый - /etc/dovecot/pop-before-smtp.sh:
#!/bin/sh echo "UPDATE user SET lasttime=NOW(), lastip='$IP' WHERE email='$USER';" | mysql -udovecot -pdovecot_password -h127.0.0.1 mail exec /usr/lib/dovecot/pop3 "$@"
И второй - /etc/dovecot/imap-before-smtp.sh:
#!/bin/sh echo "UPDATE user SET lasttime=NOW(), lastip='$IP' WHERE email='$USER';" | mysql -udovecot -pdovecot_password -h127.0.0.1 mail exec /usr/lib/dovecot/imap "$@"
В Dovecot используются два файла, позволяющие брать информацию из БД MySQL.
Первый - /etc/dovecot/dovecot-mysql.conf:
driver = mysql
connect = host=127.0.0.1 dbname=mail user=dovecot password=dovecot_password
default_pass_scheme = CRYPT
password_query = SELECT password FROM user WHERE email='%u'
user_query = SELECT CONCAT(SUBSTRING_INDEX(email, '@', -1), '/', SUBSTRING_INDEX(email, '@', 1), '/'), \
999 AS uid, \
999 AS gid, \
CONCAT('*:bytes=', max_bytes, ':messages=', max_messages) AS quota_rule \
FROM user \
WHERE email = '%u'
Второй - /etc/dovecto/dovecot-dict-mysql.conf:
connect = host=127.0.0.1 dbname=mail user=dovecot password=dovecot_password
map {
pattern = priv/quota/storage
table = user
username_field = email
value_field = bytes
}
map {
pattern = priv/quota/messages
table = user
username_field = email
value_field = messages
}
Файл конфигурации самого Dovecot /etc/dovecot/dovecot.conf у меня выглядит вот так:
protocols = pop3 imap
disable_plaintext_auth = no
log_timestamp = "%Y-%m-%d %H:%M:%S "
mail_location = maildir:/var/mail/virtual/%Ld/%Ln
first_valid_uid = 999
first_valid_gid = 999
dict {
quotadict = mysql:/etc/dovecot/dovecot-dict-mysql.conf
}
plugin {
quota = dict:user::proxy::quotadict
quota_rule = *:storage=1G:messages=1000
}
protocol pop3 {
mail_plugins = quota
mail_executable = /etc/dovecot/pop-update-lastlog.sh
}
protocol imap {
mail_plugins = quota imap_quota
mail_executable = /etc/dovecot/imap-update-lastlog.sh
}
protocol lda {
postmaster_address = postmaster@mydomain.ru
mail_plugins = quota
quota_full_tempfail = yes
rejection_reason = Your message to <%t> was automatically rejected:%n%r
quota_exceeded_message = 422: No space to store your message
}
auth_default_realm = mydomain.ru
auth default {
mechanisms = plain login
passdb sql {
args = /etc/dovecot/dovecot-mysql.conf
}
userdb sql {
args = /etc/dovecot/dovecot-mysql.conf
}
socket listen {
client {
path = /var/spool/postfix/private/auth
mode = 0660
user = postfix
group = postfix
}
master {
path = /var/run/dovecot/auth-master
mode = 0660
user = vmail
group = vmail
}
}
}
Для ограничения доступа к таблицам из postfix и dovecot были созданы два пользователя, которые имеют минимально необходимые им для работы права доступа к соответствующей БД MySQL:
insert into user(host, user, password)
values('localhost', 'postfix', PASSWORD('postfix_password'));
insert into tables_priv(host, db, user, table_name, table_priv, column_priv)
values('localhost', 'mail', 'postfix', 'subscription', '', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'subscription', 'direction', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'subscription', 'email', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'subscription', 'recipient', 'select');
insert into tables_priv(host, db, user, table_name, table_priv, column_priv)
values('localhost', 'mail', 'postfix', 'domain', '', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'domain', 'domain', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'domain', 'transport', 'select');
insert into tables_priv(host, db, user, table_name, table_priv, column_priv)
values('localhost', 'mail', 'postfix', 'user', '', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'user', 'active', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'user', 'email', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'user', 'password', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'user', 'bytes', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'user', 'max_bytes', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'user', 'messages', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'user', 'max_messages', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'user', 'lasttime', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'postfix', 'user', 'lastip', 'select');
insert into user(host, user, password)
values('localhost', 'dovecot', PASSWORD('dovecot_password'));
insert into tables_priv(host, db, user, table_name, table_priv, column_priv)
values('localhost', 'mail', 'dovecot', 'user', '', 'select,update');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'email', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'password', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'bytes', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'max_bytes', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'messages', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'max_messages', 'select');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'bytes', 'update');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'messages', 'update');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'lasttime', 'update');
insert into columns_priv(host, db, user, table_name, column_name, column_priv)
values('localhost', 'mail', 'dovecot', 'user', 'lastip', 'update');
flush privileges;
Естественно, во всём тексте статьи, для облегчения восприятия, реальные пароли заменены на их заменители - postfix_password и dovecot_password.
На интерфейс для управления почтовым сервером (по понятным причинам, реальные данные пришлось забрызгать спреем) можно посмотреть на нижеследующих снимках.
Управление доменами и транспортами:
Редактирование одного домена и его транспорта:
Управление пользователями/ящиками (совмещены две разные картинки):
Редактирование одного пользователя/ящика:
Заведение нового пользователя/ящика:
Управление всеми подписками на входящие (скоро переделаю для управления подписками на исходящие тоже):
Страница синхронизации информации почтовой системы с порталом MS SharePoint Services 3.0:
В процессе написания веб-приложения сам собой зародился простенький фреймворк в процедурном стиле, в который входит несколько функций для более удобного доступа к БД, функции для работы с шаблонами, виджеты таблиц и форм редактирования объектов БД, функции для ведения веб-сессий в БД (стандартный механизм сессий для PHP мне не подошёл, сессии сейчас в приложении никак не используются), и каркас приложения, который вызывает функции обработки форм и формирования HTML-блоков. Аутентификацию на базе модуля сессий делал, но пока что она не имеет смысла, т.к. нет механизмов авторизации, то есть программа может отличать пользователей друг от друга, но у всех пользователей, включая неаутентифицированных, пока что равные права. Сейчас ограничение доступа осуществляется средствами веб-сервера, в котором каталог с приложением просто запаролен.
Планов по дальнейшему развитию системы ещё очень много. Однако, дело это не быстрое и не известно, хватит ли терпения всё это реализовать.