Почта кажется простой ровно до того момента, пока вы не решаете ее перенести.
На бумаге все красиво:
«Ну там же просто ящики. Скопировал письма, поменял 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 дня как страховку.
Где чаще всего ломается
- Права 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
- Apache на BitrixVM слушает не тот порт
Симптом:
/var/www/html/index.php not found
Лечение:
httpd -S
ss -tulpn | egrep ':8887|:8888'
И проксировать nginx туда, где реально живут vhost сайтов.
- 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
}
- DKIM есть в DNS, но письма не подписываются
Проверка:
postconf -n | egrep 'milter|smtpd_milters|non_smtpd_milters'
systemctl status opendkim --no-pager
journalctl -u opendkim -n 100 --no-pager
- DMARC положили не туда
Неправильно:
example.ru TXT "v=DMARC1; ..."
Правильно:
_dmarc.example.ru TXT "v=DMARC1; ..."
- SPF сделали два раза
Нельзя:
example.ru TXT "v=spf1 ..."
example.ru TXT "v=spf1 ..."
Должна быть одна SPF-запись.
- Сначала поменяли 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