Logo
Englika

Как создать свой почтовый сервер (часть 2)

Как создать свой почтовый сервер (часть 2)

В первой части данной статьи мы говорили про теоретическую часть, как работает Postfix и Dovecot. Если вы ее пропустили, я настоятельно рекомендую с ней ознакомиться, чтобы понять основы. Иначе, если что-то не будет работать, вы не будете знать как это исправить.

Как я сказал в первой части данной статьи, я хочу описать здесь, как я настроил свой почтовый сервер, используя Postfix (MTA), Dovecot (MDA/MRA), Rspamd (анти-спам защита), и SPF/DKIM/DMARC. В итоге, у вас будет свой почтовый сервер, куда вы можете добавлять неограниченное количество доменов и почтовых ящиков.

Надеюсь, эта статья поможет вам все настроить, но конфиги сильно зависят от того, как именно вы хотите чтобы работал ваш почтовый сервер. Мне нужен почтовый сервер, чтобы управлять всеми почтовыми ящиками в различных доменах своих проектов (support@domain.com, и др.). Если вы хотите внести какие-либо изменения, ознакомьтесь с официальными документациями, или с первой частью данной статьи.

Схема базы данных

Я буду хранить все свои домены и почтовые ящики в PostgreSQL, чтобы иметь единый источник данных для Postfix и Dovecot. Они будут подключаться к базе данных напрямую.

Есть также возможность сгенерировать lmdb lookup-таблицу (или другого типа) для Postfix (или использовать dynamic recipient verification) и passwd-file для Dovecot, используя cron-скрипт. Процесс аутентификации будет происходить быстрее и в Postfix, и в Dovecot, но изменения в базе данных не будут доступны до тех пор, пока cron-скрипт не запустится в следующий раз.

Кстати, dynamic recipient verification позволяет Postfix делать запросы к Dovecot, используя тот же LMTP демон, чтобы проверить существование пользователя (Postfix пробует отправить тестовое письмо, но посылает команду QUIT после RCPT TO). Таким образом, Postfix может отменить почтовые письма для несуществующих пользователей, при этом не имея доступа к базе данных и не имея отдельную lookup-таблицу для этих целей. Это полезно, когда Postfix не должен иметь доступ к базе данных, или когда вы храните список пользователей в файле /etc/dovecot/users, который не может быть прочитан Postfix. Postfix также кеширует все запросы, чтобы последующие проверки производителись быстрее и хранит их в файле, который указан в address_verify_map. По умолчанию, несуществующие адреса истекают через 3 дня, подтвержденные адреса – через 31 день. См. параметры, начинающиеся с address_verify_. Используйте reject_unverified_recipient в smtpd_recipient_restrictions после permit_mynetworks, чтобы Postfix начал делать dynamic recipient verifications.

Вот схема PostgreSQL:

CREATE DATABASE mail;
-- \c mail

CREATE TABLE domains (
  id serial PRIMARY KEY,
  created_at timestamptz DEFAULT now() NOT NULL,
  name varchar(255) NOT NULL,
  UNIQUE (name)
);

CREATE TABLE mailboxes (
  id serial PRIMARY KEY,
  created_at timestamptz DEFAULT now() NOT NULL,
  domain_id int REFERENCES domains(id) ON DELETE CASCADE NOT NULL,
  name varchar(64) NOT NULL,
  password_hash text NOT NULL,
  UNIQUE (domain_id, name)
);

-- Change the passwords here and in the following files:
-- /etc/postfix/relay_domains.cf
-- /etc/postfix/relay_recipients.cf
-- /etc/dovecot/dovecot-sql.conf.ext
CREATE USER postfix WITH PASSWORD 'postfix';
CREATE USER dovecot WITH PASSWORD 'dovecot';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO postfix;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dovecot;

Ограничения выбраны не спонтанно. В соответствии с RFC 3696, максимальная длина локальной части в адресе должна быть 64 символа, домена – 255 символов.

Postfix

Postfix будет выполнять функцию простого relay и передавать письма в Dovecot. Таким образом, я использую класс адресов local только для root@localhost (используется для важных системных оповещений) и класс адресов relay для доменов, которые хранятся в базе данных.

apk add postfix postfix-pgsql

/etc/postfix/main.cf:

# -------------------------------------------------- 
# Defaults
# -------------------------------------------------- 
# Compatibility
compatibility_level = 3.8

# Local pathname information
queue_directory = /var/spool/postfix
command_directory = /usr/sbin
daemon_directory = /usr/libexec/postfix
data_directory = /var/lib/postfix

# Queue and process ownership
mail_owner = postfix

# Debugging control
debug_peer_level = 2
debugger_command =
	 PATH=/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin
	 ddd $daemon_directory/$process_name $process_id & sleep 5

# Install-time configuration information
sendmail_path = /usr/sbin/sendmail
newaliases_path = /usr/bin/newaliases
mailq_path = /usr/bin/mailq
setgid_group = postdrop
html_directory = no
manpage_directory = /usr/share/man
sample_directory = /etc/postfix
readme_directory = /usr/share/doc/postfix/readme
inet_protocols = ipv4
meta_directory = /etc/postfix
shlib_directory = /usr/lib/postfix
# -------------------------------------------------- 

# Must be a fully qualified domain
myhostname = mail.domain.com
mydomain = domain.com

# This domain will be appended to addresses without a domain
myorigin = $mydomain

# Use only ipv4 when making or accepting connections.
inet_protocols = ipv4

# The directory where unix-style mailboxes are kept.
# A trailing slash indicates that the Maildir storage format should be used.
mail_spool_directory = /var/mail/

# Local address class
local_recipient_maps = lmdb:/etc/postfix/local_recipients

# Relay address class
relay_domains = pgsql:/etc/postfix/relay_domains.cf
relay_recipient_maps = pgsql:/etc/postfix/relay_recipients.cf
transport_maps = $relay_domains

# What other systems can use this mail server to send emails.
# If `mynetworks` is set, `mynetworks_style` is ignored.
mynetworks_style = host
mynetworks =

# Select logging to a file
maillog_file = /var/log/postfix.log
maillog_file_permissions = 0644

# Restrict mail delivery to external commands and files
allow_mail_to_commands =
allow_mail_to_files =

# Max number of recipients for a single email
smtpd_recipient_limit = 10

# Max size of an email Postfix will accept
message_size_limit = 10240000

# Restrictions to prevent spam.
# Use very carefully: 
# `reject_unknown_client_hostname` because many legitimate domains don't have the PTR record.
# `reject_non_fqdn_helo_hostname` and `reject_unknown_helo_hostname` because many clients don't use a fully qualified hostname.
# Use `warn_if_reject <restriction>` for testing.
disable_vrfy_command = yes
smtpd_helo_required = yes
# Required for `reject_sender_login_mismatch` (used by the submission agent)
smtpd_sender_login_maps = pgsql:/etc/postfix/relay_recipients.cf
smtpd_client_restrictions =
smtpd_helo_restrictions =
    reject_invalid_helo_hostname
smtpd_sender_restrictions = 
    reject_non_fqdn_sender
    reject_unknown_sender_domain
smtpd_recipient_restrictions = 
    reject_non_fqdn_recipient
    reject_unknown_recipient_domain
    permit_mynetworks
    reject_unauth_destination
# Check quota status of the user on the IMAP server
    check_policy_service unix:/var/run/dovecot/quota-status
smtpd_data_restrictions =
    reject_unauth_pipelining

# SASL authentication.
# It's activated and used only by the submission agent (see master.cf).
# It's not used by other MTAs.
smtpd_sasl_auth_enable = no
smtpd_sasl_type = dovecot
smtpd_sasl_path = /var/run/dovecot/auth
# Make sure plaintext mechanisms are used only with SSL/TLS
smtpd_sasl_security_options = noanonymous, noplaintext
smtpd_sasl_tls_security_options = noanonymous

# TLS
smtpd_tls_security_level = may
# Use `may` for better compatibility with MTAs on the Internet (unfortunately)
smtp_tls_security_level = may
smtpd_tls_key_file = /etc/ssl/private/mail.domain.com.key
smtpd_tls_cert_file = /etc/ssl/certs/mail.domain.com.crt
smtpd_tls_dh1024_param_file = /etc/ssl/private/mail.domain.com.dh

# DKIM, Rspamd
smtpd_milters =
  unix:/var/run/opendkim/opendkim.sock
  unix:/var/run/rspamd/rspamd-proxy.sock
non_smtpd_milters = $smtpd_milters
milter_default_action = accept

/etc/postfix/local_recipients:

root -

Вы можете также создать свой почтовый ящик для domain.com, используя базу данных (см. выше) и создать алиас в /etc/postfix/aliases вот так root: name@domain.com, чтобы все системные письма были перенаправлены на name@domain.com.

/etc/postfix/relay_domains.cf:

hosts = localhost:5432
dbname = mail
user = postfix
password = postfix
query = 
    SELECT 'lmtp:unix:/var/run/dovecot/lmtp' 
    FROM domains WHERE name = '%s'

Если вы хотите подключиться к вашей базе данных по unix сокету, укажите hosts вот так hosts = unix:/var/run/postgresql.

Используйте lmtp:unix:private/lmtp-dovecot, если Postfix запущен в chroot окружении, чтобы открыть дополнительный LMTP сокет в /var/spool/postfix/private/lmtp-dovecot (см. ниже).

/etc/postfix/relay_recipients.cf:

hosts = localhost:5432
dbname = mail
user = postfix
password = postfix
query = 
    SELECT '%s' FROM mailboxes 
    INNER JOIN domains ON 
        domains.id = mailboxes.domain_id AND 
        domains.name = '%d' 
    WHERE mailboxes.name = '%u'

relay_recipients.cf используется в 2 местах: relay_recipient_maps и smtpd_sender_login_maps. Результат из lookup-таблицы указанный в relay_recipient_maps не используется и может быть любым (напр, мы можем вернуть SELECT 1), но lookup-таблица, указанная в smtpd_sender_login_maps должна возвращать список SASL логинов, разделенные запятой и/или пробелом/табуляцией. Нет никакого смысла создавать отдельную lookup-таблицу для smtpd_sender_login_maps, поэтому мы можем просто вернуть SELECT '%s' вместо SELECT 1 и использовать это в обоих местах.

Добавьте агент submission в master.cf файл:

submission inet n - n - - smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
  -o local_header_rewrite_clients=static:all
  -o smtpd_reject_unlisted_recipient=no
  -o smtpd_client_restrictions=
  -o smtpd_helo_restrictions=
  -o smtpd_sender_restrictions=reject_sender_login_mismatch
  -o smtpd_relay_restrictions=
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
  -o milter_macro_daemon_name=ORIGINATING

Dovecot

apk add dovecot dovecot-pgsql dovecot-lmtpd

Создайте единый UID и GID для всех пользователей.

addgroup -g 10000 virtual
adduser -u 10000 -G virtual -s /sbin/nologin -D virtual

Создайте директорию для хранения почтовых данных.

mkdir /srv/vmail
chown virtual:virtual /srv/vmail
chmod 770 /srv/vmail

Сгенерируйте самоподписную пару ключей (замените домен).

openssl req -new -newkey rsa:2048 -days 3650 \
    -nodes -x509 -subj /CN=mail.domain.com \
    -keyout /etc/ssl/private/mail.domain.com.key \
    -out /etc/ssl/certs/mail.domain.com.crt
chmod 400 /etc/ssl/private/mail.domain.com.key
chmod 444 /etc/ssl/certs/mail.domain.com.crt

Сгенерируйте Diffie-Hellman параметры.

openssl dhparam -out /etc/ssl/private/mail.domain.com.dh 2048
chmod 400 /etc/ssl/private/mail.domain.com.dh

Рекомендуемое количество битов – 2048.

Если вы хотите запустить почтовый сервер в docker контейнере, как я, сгенерируйте и сохраните все сертификаты на хост машине.

# List of used plugins
mail_plugins = $mail_plugins zlib quota

# List of supported protocols
protocols = lmtp imap

# Transmit a password in plain text
auth_mechanisms = plain login

# Disable plaintext mechanisms unless a SSL/TLS or local connection is used
disable_plaintext_auth = yes

# List of IPs or network ranges that considered secure even without a SSL/TLS connection.
# Use it for special clients such as webmailers, internal monitoring, Dovecot proxy servers.
login_trusted_networks =

# SSL/TLS is required for all client connections
ssl = required
ssl_cert = </etc/ssl/certs/mail.domain.com.crt
ssl_key = </etc/ssl/private/mail.domain.com.key
ssl_dh = </etc/ssl/private/mail.domain.com.dh

# Prefer server ciphers and their order over client's list.
ssl_prefer_server_ciphers = yes

# To transfer the entire lowercased email address to the authentication process
auth_username_format = %Lu

# Master users.
# They are able to log in under of any user, but not as themselves (name@domain.com*master).
# It's better to use it only temporarily for debugging or migration purposes.
# auth_master_user_separator = *
# passdb {
#     driver = passwd-file
#     args = /etc/dovecot/master-users
#     master = yes
#     # Verify whether a user exists before allowing the master user to log in.
#     # Otherwise, a new user can be created.
#     result_success = continue
# }

# Regular users
passdb {
    driver = sql
    args = /etc/dovecot/dovecot-sql.conf.ext
}
# Must be placed before the actual userdb lookup so that
# Dovecot can cancel if it already has all the required values.
userdb {
    driver = prefetch
}
userdb {
    driver = sql
    args = /etc/dovecot/dovecot-sql.conf.ext
}

mail_location = maildir:%h/Maildir

namespace inbox {
    # Hierarchy separator (use "/", not ".").
    # If you'll use the "." separator, and a username will contain a point,
    # the shared folders might be displayed incorrectly.
    separator = /

    # Prefix required to access this namespace (use "INBOX/", not "INBOX").
    # If you'll use the "INBOX" prefix, a problem will be occured when a user
    # sets up a regular folder named "shared" (as well as the "public" folder).
    prefix = INBOX/

    type = private
    hidden = no
    ignore_on_failure = no
    inbox = yes
    list = yes
    location =
    subscriptions = yes

    mailbox Archive {
        auto = subscribe
        special_use = \Archive
    }
    mailbox Drafts {
        auto = subscribe
        special_use = \Drafts
    }
    mailbox Junk {
        auto = subscribe
        special_use = \Junk
        autoexpunge = 30d
        autoexpunge_max_mails = 100000
    }
    mailbox Sent {
        auto = subscribe
        special_use = \Sent
    }
    mailbox Trash {
        auto = subscribe
        special_use = \Trash
        autoexpunge = 30d
        autoexpunge_max_mails = 100000
    }
}

# These settings are recommended with `autoexpunge`.
mailbox_list_index = yes
mail_always_cache_fields = date.save

# Activate the LMTP daemon.
service lmtp {
    # Open a unix socket in `/var/run/dovecot/lmtp`.
    unix_listener lmtp {
    }

    # If Postfix is running in a chroot environment, open an additional socket.
    # unix_listener /var/spool/postfix/private/lmtp-dovecot {
    #     user = postfix
    #     group = postfix
    # }
}

# Provide a SASL interface for Postfix via a unix socket. Postfix will pass 
# authentication queries to Dovecot via that interface and receives a yes/no response.
# SASL is used by the submission agent in Postfix.
service auth {
    # Open a unix socket in `/var/run/dovecot/auth-userdb`.
    # Dovecot uses this unix socket for its own purposes. Don't use it in Postfix.
    unix_listener auth-userdb {
    }

    # Open a unix socket in `/var/run/dovecot/auth`.
    # Used by Postfix.
    unix_listener auth {
      user = postfix
      group = postfix
      mode = 0666
    }

    # If Postfix is running in a chroot environment.
    # unix_listener /var/spool/postfix/private/auth {
    #     user = postfix
    #     group = postfix
    #     mode = 0666
    # }
}

protocol lmtp {
  # Enable sieves to move emails marked as spam to the junk IMAP folder.
  mail_plugins = $mail_plugins sieve
}

protocol imap {
    # 1. Enable the client and server agree on an on-the-fly compression of 
    #    the IMAP transmission in order to minimize the required bandwidth.
    # 2. Enable quota rules.
    # 3. Enable sieves to train Rspamd when a user moves an email to/from
    #    the junk IMAP folder.
    mail_plugins = $mail_plugins imap_zlib imap_quota imap_sieve
}

# Save and read all emails in a compressed state via a zlib library.
# It reduces space consumption by around 40%, relieves the hard drive from
# the strain of read operations, backup process runs faster, but it slightly
# increases the CPU usage.
# Make sure you've added `zlib` in `mail_plugins`.
plugin {
    zlib_save = gz
    zlib_save_level = 6
}

# Quota.
plugin {
    # Quotas for the individual mailbox of every user.
    quota = maildir:User quota

    quota_rule = *:storage=1G
    quota_rule1 = INBOX/Trash:storage=+10%%
    quota_rule2 = INBOX/Spam:storage=+20%%
    quota_rule3 = INBOX/Sent:ignore

    # Allow users to exceed their quota limit when saving the latest email
    quota_grace = 10%%

    quota_warning = storage=100%% quota-warning 100 %u
    quota_warning2 = storage=95%% quota-warning 95 %u
    quota_warning3 = storage=80%% quota-warning 80 %u
    quota_warning4 = -storage=100%% quota-warning -100 %u

    # Quotas for all mailboxes in one domain.
    # quota2 = dict:Domain quota:%d:file:/srv/vmail/dovecot-domain-quota

    # quota2_rule = *:storage=10G
    # quota2_rule1 = INBOX/Sent:ignore

    # quota2_warning = storage=100%% domain-quota-warning 100 %u
    # quota2_warning2 = storage=95%% domain-quota-warning 95 %u
    # quota2_warning3 = storage=80%% domain-quota-warning 80 %u
    # quota2_warning4 = -storage=100%% domain-quota-warning -100 %u
}
service quota-warning {
    executable = script /usr/local/bin/quota-warning.sh
    user = virtual
    unix_listener quota-warning {
        user = virtual
        group = virtual
    }
}

# The quota policy server for Postfix.
# It allows Postfix immediately reject an email if the quota has been exceeded.
service quota-status {
    executable = quota-status -p postfix
    # Open a unix socket in `/var/run/dovecot/quota-status`.
    unix_listener quota-status {
      user = virtual
      group = virtual
      mode = 0666
    }
    client_limit = 1
}
# Which feedback is returned to Postfix
plugin {
    quota_status_success = DUNNO
    quota_status_nouser = DUNNO
    quota_status_overquota = "552 5.2.2 Mailbox is full"
}

# Train Rspamd when a user moves an email from/to the junk IMAP folder.
# Do not forget to add the sieve plugin for lmtp and imap protocols.
plugin {
  sieve = file:%h/sieve;active=/home/virtual/sieve/active.sieve
}
plugin {
  sieve_plugins = sieve_imapsieve sieve_extprograms
  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment

  # The directory contains the scripts that are available for the pipe command.
  sieve_pipe_bin_dir = /home/virtual/sieve

  # When a user moves an email to Junk.
  imapsieve_mailbox1_name = INBOX/Junk
  imapsieve_mailbox1_causes = COPY
  imapsieve_mailbox1_before = file:/home/virtual/sieve/learn-spam.sieve

  # When a user moves an email from Junk.
  imapsieve_mailbox2_from = INBOX/Junk
  imapsieve_mailbox2_name = *
  imapsieve_mailbox2_causes = COPY
  imapsieve_mailbox2_before = file:/home/virtual/sieve/learn-ham.sieve
}

# The log file to use for error messages.
log_path = /var/log/dovecot.log

# Shows a username, IP address, SSL/TLS status, 
# executed IMAP command of the connected client.
verbose_proctitle = yes

# Logging detailed messages on the authentication process
# auth_verbose = yes

# Logging authentication prompts
# auth_debug = yes

# Logging how dovecot is looking for the user's mail directory, how it analyzes it, etc.
# mail_debug = yes

/etc/dovecot/master-users:

master:{PLAIN}secret::::::

Измените имя master и пароль secret вашего master-пользователя, запустите dovecot reload, и проверьте своего master-пользователя, запустив doveadm auth test name@domain.com*master. Теперь вы можете войти под любым пользователем вот так name@domain.com*master.

/etc/dovecot/dovecot-sql.conf.ext:

driver = pgsql
connect = host=localhost port=5432 dbname=mail user=dovecot password=dovecot
default_pass_scheme = ARGON2ID

password_query = \
  SELECT \
    mailboxes.name username, \
    domains.name domain, \
    mailboxes.password_hash password, \
    '/srv/vmail/%Ld/%Ln' userdb_home, \
    10000 userdb_uid, \
    10000 userdb_gid \
  FROM mailboxes \
  INNER JOIN domains ON \
    domains.id = mailboxes.domain_id AND \
    domains.name = '%Ld' \
  WHERE mailboxes.name = '%Ln'

user_query = \
  SELECT \
    '/srv/vmail/%Ld/%Ln' home, \
    10000 uid, \
    10000 gid \
  FROM mailboxes \
  INNER JOIN domains ON \
    domains.id = mailboxes.domain_id AND \
    domains.name = '%Ld' \
  WHERE mailboxes.name = '%Ln'

iterate_query = \
  SELECT \
    mailboxes.name username, \
    domains.name domain \
  FROM mailboxes \
  INNER JOIN domains ON domains.id = mailboxes.domain_id

Если вы хотите подключиться к базе данных через unix сокет, укажите connect вот так connect = host=/var/run/postgresql dbname=mail user=dovecot password=dovecot.

Примечания:

  • %Ld – это домен в нижнем регистре, и %Ln – это локальная часть почтового адреса в нижнем регистре.
  • Не удаляйте user_query, потому что, несмотря на то, что password_query поддерживает prefetching, user_query по-прежнему используется, когда было получено письмо или был произведен доступ к shared folder.
  • Если нужно изменить имя пользователя, также нужно одновременно изменить и его домашнюю директорию. Чтобы избежать этой проблемы, вы можете хранить домашнюю директорию в базе данных.
  • Рекомендуется возвращать uid, gid, home в SQL-запросах, вместо того, чтобы использовать mail_uid, mail_gid, и переменные %u, %n, %d в mail_location.
  • iterate_query используется командой doveadm purge или когда вы запускаете doveadm user '*', чтобы получить список всех пользователей в вашем скрипте.
  • Вы можете указать несколько хостов, чтобы обеспечить распределение нагрузки и отказоустойчивость.

/usr/local/bin/quota-warning.sh:

#!/bin/sh

PERCENT=$1
USER=$2

STATUS=""
if [[ $PERCENT = "-100" ]]; then
    STATUS="not overfull"
else
    STATUS="$PERCENT% full"
fi
    
cat << EOF | /usr/libexec/dovecot/dovecot-lda -d $USER -o "plugin/quota=maildir:User quota:noenforcing"
From: postmaster@domain.com
Subject: Quota warning

Your mailbox is now $STATUS.
EOF

noenforcing используется, чтобы доставить предупреждение о превышении квоты, даже если почтовый ящик полностью заполнен.

chmod 710 /usr/local/bin/quota-warning.sh

DKIM

apk add opendkim opendkim-utils

mkdir /run/opendkim
chown opendkim:mail /run/opendkim
chown -R opendkim:mail /etc/opendkim

/etc/opendkim/opendkim.conf:

BaseDirectory /run/opendkim

# Automatically re-start on failures.
AutoRestart yes

# The maximum automatic restart rate (limits the restarts to 10 in one hour).
AutoRestartRate 10/1h

# Select the canonicalization method(s) to be used when signing messages.
# Allows some reformatting of the header but not in the message body.
Canonicalization relaxed/simple

# Which mode(s) of operation are desired (s - signer, v - verifier).
Mode sv

# The signing algorithm used when generating signatures.
SignatureAlgorithm rsa-sha256

# Attempts to become the specified userid before starting operations.
UserID opendkim

# The socket that should be established by the filter to receive connections from sendmail.
# It opens a socket `/run/opendkim/opendkim.sock`. If Postfix is running in 
# a chroot environment, change it to `/var/spool/postfix/private/opendkim`.
Socket local:opendkim.sock

# Permissions mask to be used for file creation.
UMask 002

# The location of a file mapping key names to signing keys.
KeyTable refile:/etc/opendkim/key_table

# A table used to select one or more signatures to apply to a message based on 
# the address found in the From: header field.
SigningTable refile:/etc/opendkim/signing_table

# A set of "external" hosts that may send mail through the server as one of 
# the signing domains without credentials as such.
ExternalIgnoreList refile:/etc/opendkim/trusted_hosts

# A set internal hosts whose mail should be signed rather than verified.
InternalHosts refile:/etc/opendkim/trusted_hosts

# Enable logging.
Syslog yes

# Enable logging about successful signing or verification of messages.
SyslogSuccess yes

# Enable logging about the logic behind the filter's decision to either sign 
# a message or verify it.
LogWhy yes

/etc/opendkim/gen-dkim-key.sh:

#!/bin/sh

DOMAIN=$1
DIR="/etc/opendkim/keys/$DOMAIN"
SELECTOR="mail"

KEY_TABLE="/etc/opendkim/key_table"
SIGNING_TABLE="/etc/opendkim/signing_table"
TRUSTED_HOSTS="/etc/opendkim/trusted_hosts"

if [ ! -f "$TRUSTED_HOSTS" ]; then
cat > "$TRUSTED_HOSTS" <<- EOF
127.0.0.1
::1
localhost
EOF
fi

mkdir -p "$DIR"
opendkim-genkey -b 1024 -d "$DOMAIN" -s "$SELECTOR" -D "$DIR" -r

echo "$SELECTOR._domainkey.$DOMAIN $DOMAIN:$SELECTOR:$DIR/$SELECTOR.private" >> "$KEY_TABLE"
echo "*@$DOMAIN $SELECTOR._domainkey.$DOMAIN" >> "$SIGNING_TABLE"
echo "$DOMAIN" >> "$TRUSTED_HOSTS"

cat "$DIR/$SELECTOR.txt"

Rspamd

Rspamd – это высокопроизводительная система фильтрации спама. Она может общаться с Postfix, использя milter-протокол.

Помните, как Postfix получает письма по SMTP? smtpd демон получает письмо, отправляет его cleanup демону, который оставляет его в очереди incoming, и оповещает менеджер очередей qmgr (см. подробнее в первой части этой статьи). Перед тем, как отправить письмо cleanup демону, smtpd демон может передать его milter, который может сделать некоторые изменения с ним, после чего вернуть его назад smtpd демону. Например, milter может определить является ли письмо спамом, если это так, он добавляет в письмо новый заголовок X-Spam: true. Таким же способом работает opendkim milter. Вы отправляете письмо в Postfix, smtpd демон получает его, передает в opendkim milter, который добавляет DKIM-Signature в это письмо, и передает его обратно smtpd демону.

Rspamd имеет 4 workers:

  1. Proxy worker взаимодействует с MTA, используя milter протокол и перенаправляет письма normal worker. Он также может работать в режиме self scan и делать все самостоятельно без normal workers.
  2. Normal worker проверяет письма на спам. Он взаимодействует с fuzzy storage worker, чтобы получить хеши писем для определения является ли письмо спамом.
  3. Controller worker управляет статистикой и поддерживает набор комманд. Например, MDA (напр, Dovecot) может использовать его для того, чтобы сообщить rspamd является ли письмо спамом, когда пользователь перемещает письмо из/в IMAP папку со спамом. Он взаимодействует с fuzzy storage worker, чтобы сохранить хеши писем.
  4. Fuzzy storage worker хранит fuzzy hashes писем в базе данных. Например, в Redis.

Rspamd имеет множество модулей. Некоторые из них используются по-умолчанию, например, SPF, DKIM, RBL модули и др. Некоторые из них должны быть настроены вручную. См. rspamd конфиг, чтобы посмотреть какие модули используются.

apk add rspamd rspamd-client

Создайте директорию для rspamd.

mkdir /run/rspamd
chown rspamd:rspamd /run/rspamd

/etc/rspamd/local.d/classifier-bayes.conf:

# Store Bayesian statistics in Redis.
backend = "redis";

# Enable autolearning
autolearn = true;

/etc/rspamd/local.d/fuzzy_check.conf:

rule "local" {
  # Hashing algorithm.
  algorithm = "mumhash";
  
  # List of fuzzy hash workers used to check or train.
  servers = "0.0.0.0:11335";
  
  # The default symbol applied for a rule.
  symbol = "LOCAL_FUZZY_UNKNOWN";

  # Set of mime types to check with fuzzy.
  mime_types = ["*"];

  # Maximum global score for all maps combined.
  max_score = 20.0;

  # To allow learning for this fuzzy rule, set "no".
  read_only = no;

  # Ignore flags that are not listed in maps for this rule.
  skip_unknown = yes;

  # Whether to check the exact hash match for short texts where fuzzy algorithm
  # is not applicable.
  short_text_direct_hash = true;
  
  # Minimum length of text parts in words to perform fuzzy check.
  min_length = 64;

  # Symbol -> data for flag configuration.
  # `max_score` is a maximum score for this flag. `flag` is an ordinal flag number.
  fuzzy_map = {
    LOCAL_FUZZY_DENIED {
      max_score = 20.0;
      flag = 11;
    }
    LOCAL_FUZZY_PROB {
      max_score = 10.0;
      flag = 12;
    }
    LOCAL_FUZZY_WHITE {
      max_score = 2.0;
      flag = 13;
    }
  }
}

/etc/rspamd/local.d/fuzzy_group.conf:

# Max value for fuzzy hash when weight of symbol is exactly 1.0.
# If value is higher, then the score is still 1.0)
max_score = 12.0;

symbols = {
  "LOCAL_FUZZY_UNKNOWN" {
      weight = 5.0;
      description = "Generic fuzzy hash match";
  }
  "LOCAL_FUZZY_DENIED" {
      weight = 12.0;
      description = "Denied fuzzy hash";
  }
  "LOCAL_FUZZY_PROB" {
      weight = 5.0;
      description = "Probable fuzzy hash";
  }
  "LOCAL_FUZZY_WHITE" {
      weight = -2.1;
      description = "Whitelisted fuzzy hash";
  }
}

/etc/rspamd/local.d/logging.inc:

filename = "/var/log/rspamd.log";
level = "warning";

/etc/rspamd/local.d/redis.conf:

servers = "/var/run/redis/redis.sock";
password = "secret";

/etc/rspamd/local.d/replies.conf:

# Apply the given action to emails identified as replies.
action = "no action";

# The records will expire after this time period.
expire = 7d;

# String prefixed to keys in Redis.
key_prefix = "rr";

# The message.
message = "Message is reply to one we originated";

# Symbol yielded on emails identified as replies.
symbol = "REPLY";

# List of Redis servers to use.
backend = "redis";

/etc/rspamd/local.d/worker-controller.inc:

# The unix socket used by Dovecot to teach rspamd whether an email is spam.
bind_socket = "/run/rspamd/rspamd-controller.sock mode=0666";

/etc/rspamd/local.d/worker-fuzzy.inc:

# The fuzzy worker does not support a unix socket.
bind_socket = "*:11335";

# Enable the fuzzy worker.
# See https://github.com/rspamd/rspamd/issues/4677
count = 1;

# Store fuzzy hashes in Redis.
backend = "redis";

/etc/rspamd/local.d/worker-normal.inc:

# Disable the normal worker to free up system resources as it's not 
# necessary in the self-scan mode.
enabled = false;

/etc/rspamd/local.d/worker-proxy.inc:

# When the milter mode is enabled, the proxy communicates exclusively in 
# the milter protocol.
milter = yes;

# The unix socket to communicate with Postfix.
bind_socket = "/run/rspamd/rspamd-proxy.sock mode=0666";

upstream "local" {
  default = yes;

  # The proxy worker will handle all the spam filtering by itself without
  # normal workers.
  self_scan = yes;
}

/home/virtual/sieve/active.sieve:

require "fileinto";

if header :is "X-Spam" "Yes" {
  fileinto "INBOX/Junk";
}

/home/virtual/sieve/learn-ham.sieve:

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];

if environment :matches "imap.mailbox" "*" {
  set "mailbox" "${1}";
}

if string "${mailbox}" "INBOX/Trash" {
  stop;
}

if environment :matches "imap.user" "*" {
  set "username" "${1}";
}

pipe :copy "learn-ham.sh" [ "${username}" ];

/home/virtual/sieve/learn-ham.sh:

#!/bin/sh

HOST="/run/rspamd/rspamd-controller.sock"

rspamc -h "$HOST" learn_ham

/home/virtual/sieve/learn-spam.sieve:

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];

if environment :matches "imap.user" "*" {
  set "username" "${1}";
}

pipe :copy "learn-spam.sh" [ "${username}" ];

/home/virtual/sieve/learn-spam.sh:

#!/bin/sh

HOST="/run/rspamd/rspamd-controller.sock"

rspamc -h "$HOST" learn_spam

Сделайте bash-скрипты, которые используются в sieve выполняемыми.

chown -R virtual:virtual /home/virtual/sieve
find /home/virtual/sieve -type f -iname "*.sh" -exec chmod 710 {} \;

Скомпилируйте все sieve-скрипты.

sievec /home/virtual/sieve

Добавление нового домена

Например, вы хотите добавить новый домен domain.com.

Добавьте его в базу данных.

INSERT INTO domains (name) VALUES ('domain.com');

Создайте DKIM ключи для domain.com (см. содержание скрипта выше).

sh /etc/opendkim/gen-dkim-key.sh domain.com
# mail._domainkey	IN	TXT	( "v=DKIM1; k=rsa; s=email; "
# 	 "p=<key>" )  ; ----- DKIM key mail for domain.com

Добавьте DNS записи. Если вы мигрируйте из другого почтового сервера, тогда в начале создайте все почтовые ящики (см. ниже).

domain.com. IN MX 10 mail.server.com.
domain.com. IN TXT "v=spf1 redirect=_spf.server.com"
mail._domainkey.domain.com. IN TXT "v=DKIM1; h=sha256; k=rsa; s=email; p=<key>"
_dmarc.domain.com IN TXT "v=DMARC1; p=none"

Добавление нового почтового ящика

Например, вы хотите добавить почтовый ящик support@domain.com в существующий домен.

Сгенерируйте зашифрованный пароль.

doveadm pw -s ARGON2ID
# Enter new password: 
# Retype new password: 
# {ARGON2ID}$argon2id$<hash>

Добавьте новый почтовый ящик support в базу данных.

INSERT INTO mailboxes (domain_id, name, password_hash) 
    VALUES (1, 'support', '{ARGON2ID}$argon2id$<hash>');

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

Миграция

Вы можете использовать imapsync, чтобы перенести все письма из старого почтового сервера в новый, используя IMAP. Таким образом, вы можете перенести все свои письма, например, из Gmail в ваш почтовый сервер одной командой.

imapsync --host1 mail.source.com --user1 user --password1 "password" \
    --host2 mail.target.com --user2 user --password2 "password"

Вы можете создать bash-скрипт, чтобы вызвать эту команду для всех почтовых ящиков.

imapsync может не только просто скопировать все письма, но и синхронизировать их между 2 почтовыми серверами. Например, вы можете перенести все письма за несколько итераций. При этом на новом сервере не будет дубликатов писем.

Есть также онлайн версия imapsync, которая позволяет копировать/синхронизировать письма между 2 почтовыми ящиками (не забудьте временно поменять пароли).

Резервное копирование

Я не знаком с лучшими практиками создания бекапов, но мне нравится в начале продумать что может произойти не так, а затем как избежать этого.

Я хочу избежать 2 проблем:

  1. SSD/HDD, на котором хранятся почтовые данные был поврежден. Чтобы предотвратить потерю данных, мы можем куда-нибудь копировать все почтовые данные раз в сутки (напр, в облачное хранилище), и хранить только одну копию там.
  2. Кто-то (или наш скрипт), кто имеет доступ к серверу, случайно запустил неверную команду по ошибке (или не по ошибке), что привело к тому, что почтовые данные были изменены некорректно. Если вы копируете все данные куда-нибудь раз в сутки, но заметили, что данные были изменены некорректно только спустя несколько дней, вы можете потерять почтовые данные. Чтобы избежать этого, мы можем архивировать все почтовые данные раз в неделю, отправлять этот архив куда-нибудь (напр, в облачное хранилище), и хранить несколько бекапов (напр, 4 бекапа за последние 4 недели).

Реализация очень простая. В первом случае, вы можете использовать rsync или rclone. Например, вот так:

rclone sync "/srv/vmail" "google-cloud:my-bucket/mail"

Не забудьте создать rclone конфиг.

Во втором случае, просто создайте архив и отправьте его куда-нибудь.

tar -czf "/tmp/mail-$(date +%Y%m%d-%H%M).tar.gz" "/srv/vmail"

Тестирование

Все логи храняться в /var/log/postfix.log, /var/log/dovecot.log, и /var/log/rspamd.log. Проверьте логи и убедитесь, что все работает корректно.

Попробуйте отправить письмо локально вот так:

telnet localhost 25
# Trying ::1...
# Connected to localhost.
# Escape character is '^]'.
# 220 mail.domain.com ESMTP Postfix
ehlo localhost
# 250-mail.domain.com
# 250-PIPELINING
# 250-SIZE 10240000
# 250-ETRN
# 250-STARTTLS
# 250-ENHANCEDSTATUSCODES
# 250-8BITMIME
# 250-DSN
# 250-SMTPUTF8
# 250 CHUNKING
mail from: <support@domain.com>
# 250 2.1.0 Ok
rcpt to: <support@domain.com>
# 250 2.1.5 Ok
data
# 354 End data with <CR><LF>.<CR><LF>
# Subject: Who is that?
# 
# Hey!
# .
# 250 2.0.0 Ok: queued as CDB634C1D91
quit
# 221 2.0.0 Bye
# Connection closed by foreign host.

Проверьте различные случаи: когда указан некорректный домен, когда домен правильный, но пользователь не существует, когда все корректно, когда пользователь превысил квоту, когда пользователь перемещает письмо из/в IMAP папку со спамом, и т.д.

Отправьте письмо на:

  • check-auth@verifier.port25.com, чтобы убедиться, что SPF и DKIM работают корректно и чтобы посмотреть как ваш почтовый сервер отправляет письма (вы получите ответное письмо с отчетом).
  • abuse@mxtoolbox.com, чтобы убедиться, что ваш почтовый сервер не находится в каких-либо черных списках. Отчет также содержит информацию по SPF/DKIM/DMARC.
  • адрес, который отображается на странице mail-tester.com, чтобы посмотреть рекомендации, как увеличить шансы попадания ваших писем во входящие, а не в спам.

Проверьте здесь находится ли IP-адрес вашего сервера, email или домен в одном из черных списков. Это лучший инструмент, который я видел. Когда mxtoolbox.com и другие чекеры говорили, что IP-адрес сервера не находится ни в одной из таких баз, мои исходящие письма, отправленные с этого сервера, всегда попадали в спам в Gmail и Yandex. Только этот инструмент сказал, что данный IP находится в одном из черных списков. В данном случае смена IP-адреса решает проблему.

Вот и все. Надеюсь, что ваш почтовый сервер будет работать хорошо.

Похожие статьи

Как получить N строк для каждой группы в SQL

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

Как получить N строк для каждой группы в SQL