Легковесный почтовый сервер с хранением учетных данных в базе Postgresql


Open Source компоненты почтового сервера Администрирование серверов баз данных Postgresql
conf dovecot exim gita gita-dev.ru mailaccount postgresql
 
 

* В этом блоге я описываю свою повседневную рабочую практику, поэтому все статьи в блоге написаны лично мной и при копировании их на свой сайт пожалуйста указывайте ссылку на страницу откуда вы скопировали.
* Если какая-то статья вам помогла, то вы можете дать мне немного денег вместо простого спасибо (ссылка на форму поддержки проекта внизу страницы), если вы что-то не поняли или у вас что-то не получается, то вы можете нанять меня и я вам все подробно расскажу (расценки и ссылки в конце статьи).


(последние правки 3 недели, 6 дней)

До этого я рассказывал исключительно о монстроидальном комплексе совместной работы под названием Zimbra и вы наверное поняли, что запустить его на небольшом VPS у вас не получится, так как он потребляет просто огромное количество системных ресурсов. Но, что же делать, если требуется собственный почтовый сервер и вы сильно ограничены в системных ресурсах? Правильный ответ, это собрать его самому из Opensource-компонентов и полученный результат удивит вас своей легковесностью.

Начнем с создания хранилища для учетных данных пользователей почтового сервера и так как у меня планируется интеграция почтового сервера с моим сайтом на Django, то и схему данных для почтового сервера я опишу в виде модели данных для приложения core:

# Домены обслуживаемые сервером
class MailDomains(models.Model):
  domain = models.CharField(max_length=255,verbose_name="Обслуживаемый почтовый домен", unique = True)

  class Meta:
    verbose_name = "Почтовые домены обслуживаемые сервером"
    verbose_name_plural = "Почтовые домены обслуживаемые сервером"
    ordering = ("domain",)

# Почтовые аккаунты
class MailAccount(models.Model):
  login = models.CharField(max_length=255,verbose_name="Логин пользователя", unique = False)
  password = models.CharField(max_length=255,verbose_name="Пароль пользователя", unique = False)
  domain = models.ForeignKey(MailDomains,on_delete=models.CASCADE,verbose_name="Почтовый домен")   

  class Meta:
    verbose_name = "Почтовые домены обслуживаемые сервером"
    verbose_name_plural = "Почтовые домены обслуживаемые сервером"
    ordering = ("login",)
    unique_together = (("login", "domain"),)

На первом этапе такой структуры вполне достаточно, а после миграции в базе создаются две таблицы:

CREATE TABLE public.core_maildomains
(
  id integer NOT NULL DEFAULT nextval('core_maildomains_id_seq'::regclass),
  domain character varying(255) COLLATE pg_catalog."default" NOT NULL,
  CONSTRAINT core_maildomains_pkey PRIMARY KEY (id),
  CONSTRAINT core_maildomains_domain_key UNIQUE (domain)
)
WITH (
  OIDS = FALSE
)
TABLESPACE pg_default;

ALTER TABLE public.core_maildomains
  OWNER to web_portal;

-- Index: core_maildomains_domain_1deec3a4_like

-- DROP INDEX public.core_maildomains_domain_1deec3a4_like;

CREATE INDEX core_maildomains_domain_1deec3a4_like
  ON public.core_maildomains USING btree
  (domain COLLATE pg_catalog."default" varchar_pattern_ops)
  TABLESPACE pg_default;

CREATE TABLE public.core_mailaccount
(
  id integer NOT NULL DEFAULT nextval('core_mailaccount_id_seq'::regclass),
  login character varying(255) COLLATE pg_catalog."default" NOT NULL,
  password character varying(255) COLLATE pg_catalog."default" NOT NULL,
  domain_id integer NOT NULL,
  CONSTRAINT core_mailaccount_pkey PRIMARY KEY (id),
  CONSTRAINT core_mailaccount_login_domain_id_32957517_uniq UNIQUE (login, domain_id),
  CONSTRAINT core_mailaccount_domain_id_11dd627f_fk_core_maildomains_id FOREIGN KEY (domain_id)
    REFERENCES public.core_maildomains (id) MATCH SIMPLE
    ON UPDATE NO ACTION
    ON DELETE NO ACTION
    DEFERRABLE INITIALLY DEFERRED
)
WITH (
  OIDS = FALSE
)
TABLESPACE pg_default;

ALTER TABLE public.core_mailaccount
  OWNER to web_portal;

-- Index: core_mailaccount_domain_id_11dd627f

-- DROP INDEX public.core_mailaccount_domain_id_11dd627f;

CREATE INDEX core_mailaccount_domain_id_11dd627f
  ON public.core_mailaccount USING btree
  (domain_id)
  TABLESPACE pg_default;

Эти таблицы будут использоваться компонентами почтового сервера для авторизации пользователей и начнем мы с IMAP-сервера Dovecot.

Настройка IMAP-сервера Dovecot

Устанавливаем необходимые для работы пакеты:

# aptitude install dovecot-imapd dovecot-pgsql dovecot-sieve

Настройка IMAP-сервера Dovecot в простейшем случае сводится к следующей последовательности:

  • Отключаем PAM-авторизацию
  • Настраиваем авторизацию с использованием Postgresq
  • Настраиваем шифрование трафика

Sieve-фильтры и подобное украшательство мы рассмотрим чуть позже, а сейчас наша задача просто настроить работоспособный IMAP-сервер. Для работы IMAP-сервера нам потребуется знать id и git пользователя dovecot и создать каталог для хранения почты в формате maildir, а начнем мы с получения сведений о системном пользователе dovecot (используется команда id):

# id dovecot 
uid=109(dovecot) gid=115(dovecot) группы=115(dovecot)

Я предпочитаю хранить почту пользователей в каталоге /home/mailboxes (ну тут на вкус и цвет как говорится):

# mkdir /home/mailboxes
# chown -R dovecot:dovecot /home/mailboxes
# usermod -G mail dovecot

Не забывайте устанавливать владельца на каталоги. После того как мы подготовили инфраструктуру, мы начинаем править конфигурационные файлы Dovecot.

В файле /conf.d/10-auth.conf измените следующие параметры:

disable_plaintext_auth = no
auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@
auth_username_format = %Lu
auth_mechanisms = plain login digest-md5 cram-md5
##!include auth-master.conf.ext
#!include auth-system.conf.ext
!include auth-sql.conf.ext

Мы настроили базовые параметры авторизации, разрешили передачу паролей в виде PLAIN-TEXT (на этапе тестирования), отключили системную авторизацию и подключили конфигурационный файл в котором опишем использование SQL-базы данных для авторизации.

В файле /conf.d/10-mail.conf соответственно изменяем:

mail_location = maildir:~/Maildir
first_valid_uid = 109 
last_valid_uid = 109
first_valid_gid = 115 
last_valid_gid = 115

Мы настроили ограничения по uid и gid с которыми может работать наш IMAP-сервер и задали расположение каталогов maildir.

Файл /dovecot-sql.conf.ext содержит основную информацию о взаимодействии с базой данных:

driver = pgsql
connect = host=127.0.0.1 dbname=web_portal_db user=web_portal password=xxxPasswordxxx
default_pass_scheme = PLAIN
user_query = \ 
   SELECT '/home/mailboxes/' || login || '@' ||domain AS home, '109' AS uid, '115' AS gid\ 
      FROM core_mailaccount INNER JOIN core_maildomains ON (core_mailaccount.domain_id = core_maildomains.id) \ 
      WHERE login='%n' AND domain='%d';
password_query = \ 
   SELECT login || '@' || domain AS user, password, '/home/mailboxes/' || login || '@' ||domain AS userdb_home,\ 
      '109' AS userdb_uid, '115' AS userdb_gid FROM core_mailaccount \ 
      INNER JOIN core_maildomains ON (core_mailaccount.domain_id = core_maildomains.id) \ 
      WHERE login='%n' AND domain='%d';

И в конце концов разрешаем нашему IMAP-серверу принимать входящие подключения /dovecot.conf:

listen = *, ::

Теперь вы можете подключаться к IMAP-серверу при помощи почтового клиента.

Средства диагностики и отладки

Запрос системной информации о IMAP-пользователе:

# doveadm user anton@gita-dev.ru 
field  value 
uid    109 
gid    115 
home   /home/mailboxes/anton@gita-dev.ru 
mail   mbox:~/mail:INBOX=/var/mail/anton@gita-dev.ru

Тестирование авторизации:

# doveadm auth test -x service=imap -x rip=127.0.0.1 anton@gita-dev.ru 
Password: 
passdb: anton@gita-dev.ru auth succeeded 
extra fields: 
 user=anton 
 original_user=anton@gita-dev.ru

Ну и естественно по всем нестандартным ситуациям смотрите логи системы и IMAP-сервера.

Настройка почтового MTA сервера EXIM

Можно конечно использовать любой другой почтовый MTA сервер, но на мой взгляд единственным конкурентом EXIM является Postfix и он не намного легче в конфигурировании, а может даже и сложнее.

Начинаем как обычно с установки необходимых пакетов:

# aptitude install exim4-daemon-heavy

Сконфигурировать его необходимо аналогично EXIM, с использованием авторизации и запретом всех возможных RELAY (только авторизованные пользователи могут отправлять почту на сторонние необслуживаемые домены). В Debian и в Ubuntu настройка Exim реализована довольно необычно и в самом простом случае вы можете использовать псевдографический конфигуратор:

# dpkg-reconfigure exim4-config

Все, что вы настроили в графическом режиме (можно построить довольно тривиальные почтовые решения) будет записано в файл параметров update-exim4.conf.conf, но я честно говоря вообще никогда не встречал людей которые бы использовали такой подход к конфигурированию exim и обычно поддерживается один обычный конфигурационный файл, поэтому я смело удаляю все содержимое conf.d и переписываю демон запускающий exim, так как он при каждом запуске перестраивает конфигурацию согласно /etc/exim4/update-exim4.conf.conf (Exim в Debian 9 это старый init-скрипт и поэтому будьте аккуратнее).

В файле /etc/init.d/exim4 убираем все, что связано с regenerate exim4.conf:

case "$1" in 
 start) 
   log_daemon_msg "Starting MTA" 
   # regenerate exim4.conf 
   start_exim 
   log_end_msg 0 
   warn_paniclog 
   ;; 
 stop) 
   log_daemon_msg "Stopping MTA" 
   stop_exim 
   log_end_msg 0 
   warn_paniclog 
   ;; 
 restart) 
   # check whether newly generated config would work 
   log_daemon_msg "Stopping MTA for restart" 
   stop_exim 
   isconfigvalid 
   log_end_msg 0 
   sleep 2 
   log_daemon_msg "Restarting MTA" 
   start_exim 
   log_end_msg 0 
   warn_paniclog 
   ;; 
 reload|force-reload) 
   log_daemon_msg "Reloading $NAME configuration files" 
   log_end_msg 0 
   warn_paniclog 
   ;; 
 status) 
   status 
   ;; 
 force-stop) 
   kill_all_exims $2 
   ;; 
 *) 
   echo "Usage: $0 {start|stop|restart|reload|status|force-stop}" 
   exit 1 
   ;; 
esac 
 
exit 0

И теперь мы можем приступать к созданию нашей собственной конфигурации (при обновлении пакета имейте в виду, что мы внесли кардинальные изменения в систему запуска и конфигурирования MTA).

Инструкций по настройке EXIM в интернете полно и наша установка довольно типовая, а главное максимально простая, так как она пока без особых наворотов (алиасы, антивирус, антиспам и т.п.), просто представляю вашему вниманию работающий конфигурационный файл /etc/exim4/exim4.conf:

hide pgsql_servers = 127.0.0.1/web_portal_db/web_portal/xxxPasswordxxx 
 
domainlist local_domains = ${lookup pgsql{SELECT domain FROM public.core_maildomains WHERE domain='${domain}';}} 
domainlist relay_to_domains = ${lookup pgsql{SELECT domain FROM public.core_maildomains;}} 
hostlist relay_from_hosts = 127.0.0.1/8 
 
host_lookup = * 
rfc1413_hosts = * 
rfc1413_query_timeout = 0s 
allow_domain_literals = false 
 
smtp_banner = mail.gita-dev.ru ESMTP EXIM 
local_interfaces = 127.0.0.1:94.177.204.179 
qualify_domain = mail.gita-dev.ru 
qualify_recipient = mail.gita-dev.ru 
primary_hostname = mail.gita-dev.ru 
 
exim_user = dovecot 
exim_group = dovecot 
dsn_from = Mail Delivery System <Mailer-Daemon@$qualify_domain> 
 
# Списки доступа 
acl_smtp_mail = acl_check_mail 
acl_smtp_rcpt = acl_check_rcpt 
acl_smtp_data = acl_check_data 
 
begin acl 
 
acl_check_mail: 
 
 deny   condition = ${if eq{$sender_helo_name}{} {1}} 
      message = Say HELO first 
  
 warn condition = ${if eq{$sender_host_name}{} {1}} 
      set acl_m_greylistreasons = Host $sender_host_address lacks reverse DNS\n$acl_m_greylistreasons 
  
 accept 
 
acl_check_rcpt: 
 
 deny   message      = Restricted characters in address 
         domains      = +local_domains 
         local_parts  = ^[.] : ^.*[@%!/|] 
 
 deny   message      = Restricted characters in address 
         domains      = !+local_domains 
         local_parts  = ^[./|] : ^.*[@%!] : ^.*/\\.\\./ 
 
 accept local_parts  = postmaster 
         domains      = +local_domains 
 
 require verify       = sender 
 
 accept hosts        = +relay_from_hosts 
         control      = submission 
 
 accept authenticated = * 
         control      = submission 
 
 require message = relay not permitted 
         domains = +local_domains : +relay_to_domains 
 
 require verify = recipient 
 
 deny   message      = "HELO/EHLO required by SMTP RFC" 
         condition    = ${if eq{$sender_helo_name}{}{yes}{no}} 
 
 deny   condition    = ${if match{$sender_helo_name}{\N^\d+$\N}{yes}{no}} 
         hosts        = !127.0.0.1:!localhost:* 
         message      = "There can not be only numbers in HELO!" 
 
 deny   condition    = ${if eq{$sender_address}{}{yes}{no}} 
         hosts        = +relay_from_hosts 
         message      = "Your message have not return address" 
 
 deny   message  = "The use of IP is forbidden in HELO!" 
         hosts    = *:!+relay_from_hosts 
         condition = ${if eq{$sender_helo_name}\ 
                     {$sender_host_address}{true}{false}} 
 
 deny   condition = ${if eq{$sender_helo_name}\ 
                     {$interface_address}{yes}{no}} 
         hosts    = !127.0.0.1 : !localhost : * 
         message  = "The use of my IP is forbidden!" 
 
 accept 
 
acl_check_data: 
 
 warn   condition = ${if !def:h_Message-ID: {1}} 
         set acl_m_greylistreasons = Message lacks Message-Id: header. Consult RFC2822.\n$acl_m_greylistreasons 
 
 accept condition = ${if >={$message_size}{100000} {1}} 
          add_header = X-Spam-Note: SpamAssassin run bypassed due to message size 
 
 warn   spam      = nobody/defer_ok 
          add_header = X-Spam-Flag: YES 
  
 accept condition = ${if !def:spam_score_int {1}} 
          add_header = X-Spam-Note: SpamAssassin invocation failed 
 
 warn   add_header = X-Spam-Score: $spam_score ($spam_bar)\n\ 
                       X-Spam-Report: $spam_report 
 
 deny      condition = ${if >{$spam_score_int}{100} {1}} 
          message  = Your message scored $spam_score SpamAssassin point. Report follows:\n\ 
                     $spam_report 
 
 accept 
 
begin routers 
 
dnslookup: 
  driver = dnslookup 
  domains = !+local_domains 
  transport = remote_smtp 
  ignore_target_hosts = 0.0.0.0 : 127.0.0.0/8 
  no_more 
 
dovecot_user: 
  driver = accept 
  condition = ${if eq{} {${lookup pgsql{SELECT login FROM core_mailaccount INNER JOIN core_maildomains ON (core_mailaccount.domain_id 
= core_maildomains.id) WHERE login='${local_part}' AND domain='${domain}';}}}{no}{yes}} 
  transport = dovecot_delivery 
 
begin transports 
 
remote_smtp: 
     driver = smtp 
     interface = 94.177.204.179 
 
dovecot_delivery: 
   driver = pipe 
   command = /usr/lib/dovecot/dovecot-lda -d $local_part@$domain -f $sender_address 
   message_prefix = 
   message_suffix = 
   delivery_date_add 
   envelope_to_add 
   return_path_add 
   log_output 
   user = dovecot 
 
address_pipe: 
   driver = pipe 
   return_output 
 
address_reply: 
   driver = autoreply 
 
begin retry 
 
*                     *          F,2h,15m; G,16h,1h,1.5; F,4d,6h 
 
begin rewrite 
 
begin authenticators 
 
auth_plain: 
 driver = dovecot 
 public_name = PLAIN 
 server_socket = /var/run/dovecot/auth-client 
 server_set_id = $auth1 
 
auth_login: 
 driver = dovecot 
 public_name = LOGIN 
 server_socket = /var/run/dovecot/auth-client 
 server_set_id = $auth1 
 
auth_cram_md5: 
 driver = dovecot 
 public_name = CRAM-MD5 
 server_socket = /var/run/dovecot/auth-client 
 server_set_id = $auth1

Единственное, что я кардинально поменял, это пользователя от имени которого работает Exim, и в моем случае они с Dovecot работают от имени одного и того же пользователя dovecot (в противном случае были проблемы с доставкой почты при помощи /usr/lib/dovecot/dovecot-lda).

Средства диагностики и отладки

Если вам потребуется вывести все текущие параметры с которыми работает EXIM, то вы можете использовать команду:

# exim -bP

На этом этапе простейший почтовый сервер собран и в дальнейшем мы его немного усовершенствуем.

Моя официальная страница на FaceBook
Мой микроблог в твиттер

Как вы наверное понимаете, бесплатно сейчас работать никто не будет и если ответ на ваш вопрос потребует больше трех минут времени и вам требуется полноценная консультация, то расценки на мои услуги представленны ниже.


Есть вопросы?
Спрашивайте и я обязательно вам отвечу!

* Поля обязательные для заполнения .