Добавить в корзинуПозвонить
Найти в Дзене
АйТи для Бизнеса

Как перенести корпоративную почту на свой VDS и не устроить себе маленький цифровой Чернобыль

Почта кажется простой ровно до того момента, пока вы не решаете ее перенести. На бумаге все красиво: «Ну там же просто ящики. Скопировал письма, поменял MX и готово». Ага. Конечно. Потом выясняется, что у одного ящика 200 тысяч писем, второй требует пароль приложения, третий хранит папки с названиями, от которых Dovecot начинает смотреть в пустоту, а DNS живет по своим законам и делает вид, что он тут главный. Почтовая миграция - это не “перекинуть письма”. Это операция на живом бизнесе. Если сделать криво, люди не смогут отправлять счета, заявки уйдут в никуда, менеджеры начнут пересылать клиентам скриншоты ошибок, а директор внезапно вспомнит все ваши прошлые грехи. Ниже - нормальный практический гайд, как перенести почту со старого провайдера на свой VDS с Postfix, Dovecot, Roundcube, SPF, DKIM, DMARC и минимальным простоем. Без магии. Без паники. С руками. Дадим бонус в конце. Будет такая схема: Почтовый домен: example.ru
Почтовый сервер: mail.example.ru
IP сервера: 203.0.113.10
Оглавление

Почта кажется простой ровно до того момента, пока вы не решаете ее перенести.

На бумаге все красиво:

«Ну там же просто ящики. Скопировал письма, поменял MX и готово».

Ага. Конечно.

Потом выясняется, что у одного ящика 200 тысяч писем, второй требует пароль приложения, третий хранит папки с названиями, от которых Dovecot начинает смотреть в пустоту, а DNS живет по своим законам и делает вид, что он тут главный.

Почтовая миграция - это не “перекинуть письма”. Это операция на живом бизнесе.

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

Ниже - нормальный практический гайд, как перенести почту со старого провайдера на свой VDS с Postfix, Dovecot, Roundcube, SPF, DKIM, DMARC и минимальным простоем.

Без магии. Без паники. С руками. Дадим бонус в конце.

Что мы строим

Будет такая схема:

Почтовый домен: example.ru
Почтовый сервер: mail.example.ru
IP сервера: 203.0.113.10

Пользователи:
info@example.ru
order@example.ru
buh@example.ru
manager@example.ru

Что за что отвечает:

Postfix - принимает и отправляет почту
Dovecot - хранит ящики и дает IMAP-доступ
Roundcube - веб-интерфейс для пользователей
imapsync - переносит письма со старого сервера
OpenDKIM - подписывает исходящие письма DKIM
DNS - говорит миру, куда слать почту

Веб-морда:

https://mail.example.ru

Письма при этом отправляются не от mail.example.ru, а нормально:

info@example.ru
order@example.ru
buh@example.ru

mail.example.ru - это технический хост. Не надо путать его с почтовым доменом.

Главная мысль перед началом

Не трогайте MX первым шагом.

Сначала:

поднять новый сервер
создать ящики
перенести письма
проверить вход
проверить отправку
проверить SPF/DKIM/DMARC/PTR
сделать финальный sync
и только потом переключать MX

Кто сначала меняет MX, а потом начинает разбираться с Dovecot, тот не DevOps, а организатор квеста для бухгалтерии.

Подготовка переменных

В гайде используем такие значения. Подставьте свои:

DOMAIN="example.ru"
MAIL_HOST="mail.example.ru"
SERVER_IP="203.0.113.10"

Проверяем ОС:

hostname -f
cat /etc/os-release

Проверяем, что уже слушает сервер:

ss -tulpn | egrep ':25|:465|:587|:993|:143|:80|:443|:8887|:8888' || true

Если на сервере уже живут сайты, особенно на BitrixVM, не надо героически править nginx и Apache вслепую.

Сначала бэкап конфигов:

tar -czf /root/before-mail-migration-$(date +%F_%H-%M-%S).tar.gz \
/etc/nginx \
/etc/httpd \
/etc/postfix \
/etc/dovecot 2>/dev/null

Потом уже работаем.

Установка базовых пакетов

Для CentOS Stream 9 / RHEL-like:

dnf install -y epel-release

dnf install -y \
postfix \
dovecot \
dovecot-pigeonhole \
cyrus-sasl \
cyrus-sasl-plain \
openssl \
s-nail \
telnet \
wget \
tar \
unzip \
certbot \
python3-certbot-nginx

Для imapsync:

dnf install -y imapsync
imapsync --version

Создаем системного пользователя для почты

Письма будем хранить в /var/vmail.

groupadd -g 5000 vmail 2>/dev/null || true
useradd -g vmail -u 5000 vmail -d /var/vmail -m 2>/dev/null || true

mkdir -p /var/vmail
chown -R vmail:vmail /var/vmail
chmod 770 /var/vmail

Настройка Dovecot

Нам нужна простая схема:

/etc/dovecot/users - логины и хэши паролей
/var/vmail/example.ru/user/Maildir - почта пользователя

Создаем файл пользователей:

touch /etc/dovecot/users
chown root:dovecot /etc/dovecot/users
chmod 640 /etc/dovecot/users

Конфиг авторизации:

cat > /etc/dovecot/conf.d/auth-passwdfile.conf.ext <<'EOF'
passdb {
driver = passwd-file
args = scheme=SHA512-CRYPT username_format=%u /etc/dovecot/users
}

userdb {
driver = static
args = uid=vmail gid=vmail home=/var/vmail/%d/%n
}
EOF

Подключаем его в /etc/dovecot/conf.d/10-auth.conf:

sed -i 's/^!include auth-system.conf.ext/#!include auth-system.conf.ext/' /etc/dovecot/conf.d/10-auth.conf
grep -q 'auth-passwdfile.conf.ext' /etc/dovecot/conf.d/10-auth.conf || \
echo '!include auth-passwdfile.conf.ext' >> /etc/dovecot/conf.d/10-auth.conf

Настраиваем mail location:

cat > /etc/dovecot/conf.d/10-mail.conf <<'EOF'
mail_location = maildir:/var/vmail/%d/%n/Maildir

namespace inbox {
inbox = yes
}
EOF

Включаем IMAP и IMAPS:

cat > /etc/dovecot/conf.d/10-master.conf <<'EOF'
service imap-login {
inet_listener imap {
port = 143
}

inet_listener imaps {
port = 993
ssl = yes
}
}

service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}

service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
EOF

Пока сертификата может еще не быть, поэтому SSL подключим после выпуска Let's Encrypt.

Запускаем Dovecot:

systemctl enable --now dovecot
systemctl restart dovecot

Настройка Postfix

Создаем файлы виртуальных доменов и ящиков:

cat > /etc/postfix/vdomains <<EOF
example.ru OK
EOF

touch /etc/postfix/vmailbox

postmap /etc/postfix/vdomains
postmap /etc/postfix/vmailbox

Основные параметры Postfix:

postconf -e "myhostname = mail.example.ru"
postconf -e "mydomain = example.ru"
postconf -e "myorigin = \$mydomain"
postconf -e "mydestination = localhost"
postconf -e "inet_interfaces = all"
postconf -e "inet_protocols = ipv4"
postconf -e "mynetworks = 127.0.0.0/8"

postconf -e "virtual_mailbox_domains = hash:/etc/postfix/vdomains"
postconf -e "virtual_mailbox_maps = hash:/etc/postfix/vmailbox"
postconf -e "virtual_mailbox_base = /var/vmail"
postconf -e "virtual_uid_maps = static:5000"
postconf -e "virtual_gid_maps = static:5000"
postconf -e "virtual_transport = virtual"

postconf -e "smtpd_recipient_restrictions = permit_mynetworks,permit_sasl_authenticated,reject_unauth_destination"

Включаем SMTP auth через Dovecot:

postconf -e "smtpd_sasl_type = dovecot"
postconf -e "smtpd_sasl_path = private/auth"
postconf -e "smtpd_sasl_auth_enable = yes"
postconf -e "smtpd_tls_auth_only = yes"

Добавляем 587 и 465:

cp /etc/postfix/master.cf /etc/postfix/master.cf.bak.$(date +%F_%H-%M-%S)

grep -q "^submission inet" /etc/postfix/master.cf || cat >> /etc/postfix/master.cf <<'EOF'

submission inet n - n - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING

smtps inet n - n - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
-o milter_macro_daemon_name=ORIGINATING
EOF

Перезапуск:

postfix check
systemctl enable --now postfix
systemctl restart postfix

Проверяем порты:

ss -tulpn | egrep ':25|:465|:587|:993|:143'

Нужно увидеть:

25 postfix
465 postfix
587 postfix
143 dovecot
993 dovecot

Создание ящиков

Пример создания одного ящика:

EMAIL="info@example.ru"
LOCAL="info"
PASS='StrongPassword123!'

HASH=$(openssl passwd -6 "$PASS")

echo "$EMAIL:$HASH" >> /etc/dovecot/users
echo "$EMAIL example.ru/$LOCAL/" >> /etc/postfix/vmailbox

mkdir -p /var/vmail/example.ru/$LOCAL/Maildir/{cur,new,tmp}
chown -R vmail:vmail /var/vmail/example.ru/$LOCAL

postmap /etc/postfix/vmailbox

chown root:dovecot /etc/dovecot/users
chmod 640 /etc/dovecot/users

systemctl reload dovecot
systemctl reload postfix

Проверяем авторизацию:

doveadm auth test info@example.ru 'StrongPassword123!'

Если видите auth succeeded, живем.

Если нет, значит пароль не тот, файл не тот или права опять решили устроить саботаж.

CSV для массового импорта

Сделайте файл:

mkdir -p /root/mail-migration
nano /root/mail-migration/mailboxes.csv

Формат:

email,app_password,new_password,maxage
info@example.ru,OLD_APP_PASSWORD,NewPassword123!,30
order@example.ru,OLD_APP_PASSWORD,NewPassword123!,30
buh@example.ru,OLD_APP_PASSWORD,NewPassword123!,90

Где:

email - ящик
app_password - пароль приложения от старого почтового сервера
new_password - новый пароль на вашем сервере
maxage - сколько дней почты переносить

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

Скрипт создания ящиков:

cat > /root/mail-migration/01-create-mailboxes.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

CSV="/root/mail-migration/mailboxes.csv"
DOMAIN="example.ru"

if [ ! -f "$CSV" ]; then
echo "ERROR: CSV not found: $CSV"
exit 1
fi

tail -n +2 "$CSV" | while IFS=',' read -r email app_password new_password maxage; do
email=$(echo "$email" | tr -d '\r' | xargs)
new_password=$(echo "$new_password" | tr -d '\r')
localpart="${email%@*}"

[ -z "$email" ] && continue

echo "Creating mailbox: $email"

HASH=$(openssl passwd -6 "$new_password")

grep -v "^$email:" /etc/dovecot/users > /tmp/dovecot_users.tmp || true
mv /tmp/dovecot_users.tmp /etc/dovecot/users
echo "$email:$HASH" >> /etc/dovecot/users

grep -v "^$email " /etc/postfix/vmailbox > /tmp/vmailbox.tmp || true
mv /tmp/vmailbox.tmp /etc/postfix/vmailbox
echo "$email $DOMAIN/$localpart/" >> /etc/postfix/vmailbox

mkdir -p "/var/vmail/$DOMAIN/$localpart/Maildir/"{cur,new,tmp}
chown -R vmail:vmail "/var/vmail/$DOMAIN/$localpart"
done

postmap /etc/postfix/vmailbox

chown root:dovecot /etc/dovecot/users
chmod 640 /etc/dovecot/users

systemctl reload dovecot
systemctl reload postfix

echo "DONE"
EOF

chmod +x /root/mail-migration/01-create-mailboxes.sh
/root/mail-migration/01-create-mailboxes.sh

Миграция писем через imapsync

Сначала тест одного ящика:

imapsync \
--host1 imap.old-provider.example --port1 993 --ssl1 \
--user1 info@example.ru --password1 'OLD_APP_PASSWORD' \
--host2 127.0.0.1 --port2 143 \
--user2 info@example.ru --password2 'NewPassword123!' \
--automap \
--syncinternaldates \
--noauthmd5 \
--maxage 30 \
--nofoldersizes \
--skipsize \
--usecache

Для массового sync:

mkdir -p /root/mail-migration/logs

cat > /root/mail-migration/03-sync-mailboxes.sh <<'EOF'
#!/usr/bin/env bash
set -u

CSV="/root/mail-migration/mailboxes.csv"
LOGDIR="/root/mail-migration/logs"
OLD_IMAP_HOST="imap.old-provider.example"
REPORT="$LOGDIR/sync-report-$(date +%F_%H-%M-%S).txt"

mkdir -p "$LOGDIR"

echo "SYNC REPORT: $(date)" | tee "$REPORT"

tail -n +2 "$CSV" | while IFS=',' read -r email app_password new_password maxage; do
email=$(echo "$email" | tr -d '\r' | xargs)
app_password=$(echo "$app_password" | tr -d '\r')
new_password=$(echo "$new_password" | tr -d '\r')
maxage=$(echo "$maxage" | tr -d '\r' | xargs)

[ -z "$email" ] && continue
[ -z "$maxage" ] && maxage=30

safe_email=$(echo "$email" | sed 's/@/_/g; s/\./_/g')
logfile="$LOGDIR/${safe_email}_maxage_${maxage}_$(date +%F_%H-%M-%S).log"

echo "==========================================" | tee -a "$REPORT"
echo "SYNC: $email" | tee -a "$REPORT"
echo "MAXAGE: $maxage days" | tee -a "$REPORT"
echo "LOG: $logfile" | tee -a "$REPORT"
echo "START: $(date)" | tee -a "$REPORT"

imapsync \
--host1 "$OLD_IMAP_HOST" --port1 993 --ssl1 \
--user1 "$email" --password1 "$app_password" \
--host2 127.0.0.1 --port2 143 \
--user2 "$email" --password2 "$new_password" \
--automap \
--regextrans2 's/\./_/g' \
--regextrans2 's/:/_/g' \
--regextrans2 's/\\/_/g' \
--regextrans2 's/\[/_/g' \
--regextrans2 's/\]/_/g' \
--regextrans2 's/\*/_/g' \
--regextrans2 's/\?/_/g' \
--regextrans2 's#/+$##' \
--syncinternaldates \
--noauthmd5 \
--maxage "$maxage" \
--nofoldersizes \
--skipsize \
--usecache \
--errorsmax 50 \
2>&1 | tee "$logfile"

code=${PIPESTATUS[0]}

if [ "$code" -eq 0 ]; then
echo "OK: $email" | tee -a "$REPORT"
else
echo "FAIL: $email, exit code: $code" | tee -a "$REPORT"
fi

echo "END: $(date)" | tee -a "$REPORT"
done

echo "DONE: $(date)" | tee -a "$REPORT"
EOF

chmod +x /root/mail-migration/03-sync-mailboxes.sh
/root/mail-migration/03-sync-mailboxes.sh

Почему столько regextrans2?

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

Особенно веселая ошибка:

Invalid mailbox name: Ends with hierarchy separator

Лечится строкой:

--regextrans2 's#/+$##'

То есть убираем хвостовой /, если имя папки после преобразования стало кривым.

Проверка результата:

for d in /var/vmail/example.ru/*; do
echo "$(basename "$d"): $(find "$d/Maildir" -type f 2>/dev/null | grep -E '/new/|/cur/' | wc -l)"
done

Веб-морда Roundcube

Пользователям нужна веб-почта. Иначе они будут спрашивать, где «сайт с письмами».

Ставим PHP-модули:

dnf install -y \
php \
php-cli \
php-mbstring \
php-xml \
php-imap \
php-intl \
php-json \
php-pdo \
php-mysqlnd \
php-gd \
php-zip \
php-sqlite3 \
php-process

Скачиваем Roundcube:

mkdir -p /var/www
cd /var/www

wget -O roundcube.tar.gz https://github.com/roundcube/roundcubemail/releases/download/1.6.11/roundcubemail-1.6.11-complete.tar.gz

tar -xzf roundcube.tar.gz
rm -rf /var/www/roundcube
mv roundcubemail-1.6.11 roundcube

mkdir -p /var/www/roundcube/db
chown -R bitrix:bitrix /var/www/roundcube 2>/dev/null || chown -R apache:apache /var/www/roundcube

Конфиг Roundcube:

cat > /var/www/roundcube/config/config.inc.php <<'EOF'
<?php

$config['db_dsnw'] = 'sqlite:////var/www/roundcube/db/roundcube.sqlite?mode=0646';

$config['imap_host'] = 'tls://127.0.0.1:143';
$config['imap_conn_options'] = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
),
);

$config['smtp_host'] = 'tls://mail.example.ru:587';
$config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p';

$config['support_url'] = '';
$config['product_name'] = 'Company Mail';
$config['des_key'] = 'CHANGE_THIS_DES_KEY';
$config['plugins'] = array('archive', 'zipdownload');
$config['skin'] = 'elastic';

$config['enable_installer'] = false;
$config['default_charset'] = 'UTF-8';
EOF

KEY=$(openssl rand -base64 24)
sed -i "s/CHANGE_THIS_DES_KEY/$KEY/" /var/www/roundcube/config/config.inc.php

Права, если Apache работает от bitrix:

chown -R bitrix:bitrix /var/www/roundcube

find /var/www/roundcube -type d -exec chmod 755 {} \;
find /var/www/roundcube -type f -exec chmod 644 {} \;

chmod -R 775 /var/www/roundcube/temp
chmod -R 775 /var/www/roundcube/logs
chmod -R 775 /var/www/roundcube/db

chown -R bitrix:bitrix /var/www/roundcube/temp
chown -R bitrix:bitrix /var/www/roundcube/logs
chown -R bitrix:bitrix /var/www/roundcube/db

Вот тут обычно и начинается классика жанра.

Roundcube показывает:

Oops... something went wrong!

А в логах:

Unable to create file /var/www/roundcube/db/roundcube.sqlite because Permission denied

Это не «Roundcube плохой». Это права. Как обычно.

Проверяем пользователя Apache:

ps aux | grep '[h]ttpd' | head

И даем владельца именно ему.

BitrixVM и отдельная боль с портами

На BitrixVM часто схема такая:

nginx снаружи: 80/443
Apache внутри: 127.0.0.1:8887 или 8888

Проверяем:

ss -tulpn | egrep ':80|:443|:8887|:8888'
httpd -S

Если сайты Bitrix сидят на 127.0.0.1:8887, то Roundcube тоже надо добавлять в этот контур, а не выдумывать отдельную реальность.

Apache vhost:

cat > /etc/httpd/bx/conf/bx_ext_mail.example.ru.conf <<'EOF'
<VirtualHost 127.0.0.1:8887>
ServerName mail.example.ru

DocumentRoot /var/www/roundcube
DirectoryIndex index.php index.html

<Directory "/var/www/roundcube">
Options FollowSymLinks
AllowOverride All
Require all granted
</Directory>

<Directory "/var/www/roundcube/config">
Require all denied
</Directory>

<Directory "/var/www/roundcube/temp">
Require all denied
</Directory>

<Directory "/var/www/roundcube/logs">
Require all denied
</Directory>

<Directory "/var/www/roundcube/SQL">
Require all denied
</Directory>

ErrorLog /var/log/httpd/mail.example.ru_error.log
CustomLog /var/log/httpd/mail.example.ru_access.log combined
</VirtualHost>
EOF

nginx vhost:

cat > /etc/nginx/bx/site_avaliable/bx_ext_mail.example.ru.conf <<'EOF'
server {
listen 80;
server_name mail.example.ru;

root /var/www/roundcube;
index index.php index.html;

include /etc/nginx/bx/conf/letsencrypt-challenge-tokens.conf;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
proxy_pass http://127.0.0.1:8887;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header HTTPS off;
proxy_set_header X-Forwarded-Proto http;
}

location ~ ^/(README|INSTALL|LICENSE|CHANGELOG|UPGRADING)$ {
deny all;
}

location ~ ^/(config|temp|logs|SQL)/ {
deny all;
}
}
EOF

ln -sf /etc/nginx/bx/site_avaliable/bx_ext_mail.example.ru.conf /etc/nginx/bx/site_enabled/bx_ext_mail.example.ru.conf

httpd -t
nginx -t

systemctl reload httpd
systemctl reload nginx

Проверка:

curl -I -H "Host: mail.example.ru" http://127.0.0.1:8887/
curl -I http://mail.example.ru

Нужно получить 200 OK.

Если видите /var/www/html/index.php not found, значит Apache не попал в нужный vhost.

Не лечите это молитвой. Смотрите httpd -S.

SSL для веба, IMAP и SMTP

Сначала DNS:

mail.example.ru A 203.0.113.10

Проверка:

dig +short mail.example.ru A

Выпускаем сертификат:

certbot --nginx -d mail.example.ru

Подключаем сертификат к Dovecot:

cat > /etc/dovecot/conf.d/10-ssl.conf <<'EOF'
ssl = required
ssl_cert = </etc/letsencrypt/live/mail.example.ru/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.example.ru/privkey.pem
EOF

systemctl restart dovecot

Проверка IMAPS:

openssl s_client -connect mail.example.ru:993 -servername mail.example.ru </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

Подключаем сертификат к Postfix:

postconf -e "smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.ru/fullchain.pem"
postconf -e "smtpd_tls_key_file = /etc/letsencrypt/live/mail.example.ru/privkey.pem"
postconf -e "smtpd_use_tls = yes"
postconf -e "smtpd_tls_security_level = may"
postconf -e "smtp_tls_security_level = may"

systemctl restart postfix

Проверка SMTP TLS:

openssl s_client -connect mail.example.ru:465 -servername mail.example.ru </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

openssl s_client -starttls smtp -connect mail.example.ru:587 -servername mail.example.ru </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates

Firewall

Открываем нужные порты:

systemctl enable --now firewalld || true

firewall-cmd --permanent --add-service=http || true
firewall-cmd --permanent --add-service=https || true
firewall-cmd --permanent --add-service=smtp || true
firewall-cmd --permanent --add-service=smtps || true
firewall-cmd --permanent --add-service=imap || true
firewall-cmd --permanent --add-service=imaps || true

firewall-cmd --permanent --add-port=587/tcp || true
firewall-cmd --reload || true

firewall-cmd --list-all

Проверяем:

ss -tulpn | egrep ':25|:465|:587|:993|:143|:80|:443'

DNS: A, MX, SPF, DKIM, DMARC, PTR

Вот тут люди особенно любят ломать доставку.

Правильная логика:

mail.example.ru - технический сервер
example.ru - почтовый домен

DNS:

mail.example.ru. A 203.0.113.10
example.ru. MX 10 mail.example.ru.

SPF:

example.ru. TXT "v=spf1 mx ip4:203.0.113.10 ~all"

Если используются сервисы рассылок, добавляем их include, но аккуратно:

example.ru. TXT "v=spf1 mx ip4:203.0.113.10 include:spf.some-service.example ~all"

Важно: SPF-запись должна быть одна. Не две. Не три. Одна.

DKIM через OpenDKIM

Установка:

dnf install -y opendkim opendkim-tools

Ключ:

mkdir -p /etc/opendkim/keys/example.ru

opendkim-genkey -b 2048 -d example.ru -D /etc/opendkim/keys/example.ru -s mail

chown -R opendkim:opendkim /etc/opendkim/keys
chmod 600 /etc/opendkim/keys/example.ru/mail.private

Конфиг:

cat > /etc/opendkim.conf <<'EOF'
Syslog yes
SyslogSuccess yes
LogWhy yes
UMask 002
Mode sv
Canonicalization relaxed/simple
Socket inet:8891@localhost
PidFile /run/opendkim/opendkim.pid
UserID opendkim:opendkim
KeyTable /etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
ExternalIgnoreList /etc/opendkim/TrustedHosts
InternalHosts /etc/opendkim/TrustedHosts
EOF

TrustedHosts:

cat > /etc/opendkim/TrustedHosts <<'EOF'
127.0.0.1
localhost
mail.example.ru
example.ru
203.0.113.10
EOF

KeyTable:

cat > /etc/opendkim/KeyTable <<'EOF'
mail._domainkey.example.ru example.ru:mail:/etc/opendkim/keys/example.ru/mail.private
EOF

SigningTable:

cat > /etc/opendkim/SigningTable <<'EOF'
*@example.ru mail._domainkey.example.ru
EOF

Подключаем к Postfix:

postconf -e "milter_default_action = accept"
postconf -e "milter_protocol = 6"
postconf -e "smtpd_milters = inet:localhost:8891"
postconf -e "non_smtpd_milters = inet:localhost:8891"

systemctl enable --now opendkim
systemctl restart opendkim postfix

Проверка:

systemctl is-active opendkim
ss -tulpn | grep 8891
postconf -n | egrep 'milter|smtpd_milters|non_smtpd_milters'

Получить DNS TXT:

cat /etc/opendkim/keys/example.ru/mail.txt

В DNS добавляем:

mail._domainkey.example.ru. TXT "v=DKIM1; k=rsa; p=ВАШ_ДЛИННЫЙ_КЛЮЧ"

Если DNS разбивает длинный DKIM на две строки в ответе dig, это нормально.

Проверка:

dig @ns1.your-dns-provider.example mail._domainkey.example.ru TXT

DMARC

На старте ставим не reject, а none.

Потому что p=reject до проверки всех источников отправки - это не безопасность, а хороший способ самому себе отстрелить рассылки, счета и уведомления.

Запись:

_dmarc.example.ru. TXT "v=DMARC1; p=none; rua=mailto:postmaster@example.ru; ruf=mailto:postmaster@example.ru; fo=1; adkim=s; aspf=s"

Потом, когда убедились, что все легальные отправители проходят SPF/DKIM:

p=quarantine

И только потом:

p=reject

PTR / rDNS

PTR делается не в DNS домена. Он делается у хостера, которому принадлежит IP.

Нужно:

203.0.113.10 -> mail.example.ru.

Проверка:

dig -x 203.0.113.10 +short

Нужно получить:

mail.example.ru.

Связка должна быть симметричной:

A: mail.example.ru -> 203.0.113.10
PTR: 203.0.113.10 -> mail.example.ru

Без PTR часть почтовых систем будет смотреть на ваш сервер как на странного человека в подъезде с коробкой проводов.

Проверка отправки

Заходим в Roundcube:

https://mail.example.ru

Логин:

info@example.ru

Пароль:

новый пароль

Отправляем письмо на внешний ящик.

Лог:

tail -f /var/log/maillog

Норма:

sasl_username=info@example.ru
status=sent

В исходнике полученного письма должны быть:

spf=pass
dkim=pass
DKIM-Signature: d=example.ru; s=mail;

Если отправка падает после STARTTLS, проверьте Roundcube:

$config['smtp_host'] = 'tls://mail.example.ru:587';

Не 127.0.0.1, если сертификат выписан на mail.example.ru.

Вот такая мелочь, а нервов съедает как полноценный понедельник.

Переключение MX

Только теперь меняем MX.

Было:

example.ru. MX 10 старый-почтовый-сервер.example.

Стало:

example.ru. MX 10 mail.example.ru.

Проверка авторитетных DNS:

dig @ns1.your-dns-provider.example example.ru MX +short
dig @ns2.your-dns-provider.example example.ru MX +short

Нужно:

10 mail.example.ru.

Публичный резолвер может еще какое-то время показывать старый MX. Это кеш. Он пройдет.

После переключения MX

Смотрим входящие:

tail -f /var/log/maillog

Отправляем письмо с внешнего ящика на:

info@example.ru

В логе должно быть:

postfix/smtpd: connect from ...
postfix/virtual: to=<info@example.ru>, relay=virtual, status=sent

Проверяем Maildir:

find /var/vmail/example.ru/info/Maildir -type f | grep -E '/new/|/cur/' | wc -l

И Roundcube.

Контрольный sync после MX

Через 30-60 минут после переключения MX делаем еще один sync со старого сервера:

cd /root/mail-migration
./03-sync-mailboxes.sh

Потом:

LASTREPORT=$(ls -t /root/mail-migration/logs/sync-report-*.txt | head -1)
echo "$LASTREPORT"
grep "FAIL:" "$LASTREPORT"
cat "$LASTREPORT"

Если FAIL: пустой, выдохнули.

Старого провайдера не выключаем сразу. Держим 2-3 дня как страховку.

Где чаще всего ломается

  1. Права Roundcube

Симптом:

Oops... something went wrong

Лог:

Unable to create file roundcube.sqlite because Permission denied

Лечение:

ps aux | grep '[h]ttpd' | head
chown -R bitrix:bitrix /var/www/roundcube
chmod -R 775 /var/www/roundcube/temp /var/www/roundcube/logs /var/www/roundcube/db
  1. Apache на BitrixVM слушает не тот порт

Симптом:

/var/www/html/index.php not found

Лечение:

httpd -S
ss -tulpn | egrep ':8887|:8888'

И проксировать nginx туда, где реально живут vhost сайтов.

  1. Dovecot на 993 без SSL

Симптом:

Could not find certificate from stdin

Проверка:

dovecot -n | egrep -i 'imaps|ssl|cert|key' -A3 -B3

Нужно:

inet_listener imaps {
port = 993
ssl = yes
}
  1. DKIM есть в DNS, но письма не подписываются

Проверка:

postconf -n | egrep 'milter|smtpd_milters|non_smtpd_milters'
systemctl status opendkim --no-pager
journalctl -u opendkim -n 100 --no-pager
  1. DMARC положили не туда

Неправильно:

example.ru TXT "v=DMARC1; ..."

Правильно:

_dmarc.example.ru TXT "v=DMARC1; ..."
  1. SPF сделали два раза

Нельзя:

example.ru TXT "v=spf1 ..."
example.ru TXT "v=spf1 ..."

Должна быть одна SPF-запись.

  1. Сначала поменяли MX, потом начали настраивать

Это уже не DevOps, это реалити-шоу.

Мини-администрирование ящиков без панели

Если не хочется ставить PostfixAdmin и переводить все на MySQL, можно жить на скриптах.

Список ящиков:

cat /etc/postfix/vmailbox

Размеры:

du -sh /var/vmail/example.ru/*

Смена пароля:

EMAIL="info@example.ru"
NEWPASS='NewStrongPassword123!'

HASH=$(openssl passwd -6 "$NEWPASS")

sed -i "s#^$EMAIL:.*#$EMAIL:$HASH#" /etc/dovecot/users

chown root:dovecot /etc/dovecot/users
chmod 640 /etc/dovecot/users

systemctl reload dovecot

Создать ящик:

EMAIL="newuser@example.ru"
LOCAL="newuser"
PASS='StrongPassword123!'

HASH=$(openssl passwd -6 "$PASS")

echo "$EMAIL:$HASH" >> /etc/dovecot/users
echo "$EMAIL example.ru/$LOCAL/" >> /etc/postfix/vmailbox

mkdir -p /var/vmail/example.ru/$LOCAL/Maildir/{cur,new,tmp}
chown -R vmail:vmail /var/vmail/example.ru/$LOCAL

postmap /etc/postfix/vmailbox

chown root:dovecot /etc/dovecot/users
chmod 640 /etc/dovecot/users

systemctl reload postfix
systemctl reload dovecot

PostfixAdmin, Modoboa, Mailcow и прочие взрослые панели - штуки хорошие. Но не на уже работающий боевой сервер с сайтами, где все и так держится на связке nginx, Apache, PHP, BitrixVM и вашей вере в аккуратность.

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

Финальный чек-лист

Перед тем как сказать «готово», проверяем:

dig +short mail.example.ru A
dig +short example.ru MX
dig +short example.ru TXT
dig +short mail._domainkey.example.ru TXT
dig +short _dmarc.example.ru TXT
dig -x 203.0.113.10 +short

ss -tulpn | egrep ':25|:465|:587|:993|:143|:80|:443'

systemctl is-active postfix
systemctl is-active dovecot
systemctl is-active opendkim

postqueue -p

В идеале:

mail.example.ru A -> IP сервера
example.ru MX -> mail.example.ru
SPF есть
DKIM есть
DMARC есть
PTR -> mail.example.ru
25/465/587/143/993 открыты
Postfix active
Dovecot active
OpenDKIM active
очередь Postfix пустая

И только после этого можно с чистой совестью закрыть терминал.

Хотя нет.

Через час все равно зайдите и проверьте логи.

Потому что почта - это не сервис.

Это отдельная форма воспитания характера. Терпеливым бонус: https://github.com/s4ps4n/mmig/ - тут и скрипты и этот гайд и прочее полезное.

Мы в Дзене: https://dzen.ru/it-uu.ru

Мы в Максе: max.ru/id323416670_biz

Наш сайт: https://it-uu.ru

АйТи для бизнеса