Миграция ящиков через imapsync без простоя: bulk, дельты и переключение MX
Перенос сотен ящиков с Gmail Workspace, Yandex или старого cPanel на собственный Dovecot без единой минуты простоя. Двухфазная модель imapsync: долгий bulk при живом источнике, дельты по cron, короткая догоняющая синхронизация после cutover MX. С реальными командами, DNS-записями и чек-листом.
EvilMail Team27 июля 2026 г.13 мин чтения
Почему «слить всё за выходные» ломается — и что делает imapsync другим
Пока вы копируете 400 ГБ почты со старого сервера, старый сервер продолжает принимать новые письма. Каждую минуту, что идёт rsync или ручной экспорт, дельта между источником и приёмником растёт. К моменту, когда вы «закончили», у вас на руках снимок недельной давности, а пользователи за это время получили ещё пару тысяч писем, которых на новом сервере нет. Классический сценарий «остановили всех в пятницу вечером, перелили, в понедельник включили» разваливается ровно на этом: либо вы реально останавливаете входящую почту на выходные (простой и отбитые письма), либо теряете всё, что пришло во время копирования.
imapsync решает это тем, что он идемпотентен. Он не «копирует папку», он сравнивает каждое сообщение на источнике и приёмнике по паре заголовков (Message-Id, Date, Subject, From) плюс размеру тела, и переносит только то, чего на приёмнике ещё нет. Запустили второй раз — перенеслась только разница. Третий — ещё меньше. Именно поэтому его гоняют не один раз, а тремя проходами:
Фаза 1, bulk — долгий первый слив всей истории, идёт днями при полностью работающем старом сервере.
Фаза 1, дельты — та же команда по cron каждые несколько часов, добирает свежие письма и держит новый сервер «горячим».
Фаза 2, финальная догоняющая — короткий прогон сразу после переключения MX, забирает последнее split-brain окно.
Сразу закрою главный миф. UID на приёмнике будут другими. Это нормально и неизбежно — UID назначает целевой сервер, imapsync их не переносит и не должен. Он сохраняет флаги (\Seen, \Answered, \Flagged), внутренние даты (INTERNALDATE) и структуру папок. Клиенты просто переиндексируют ящик при первом подключении. Никто из пользователей этого не замечает.
Подготовка: инвентаризация, доступы и провайдер-специфика
До первого запуска соберите три вещи: список ящиков с рабочими учётными данными, объём каждого и особые папки. Объём с IMAP-стороны снимается через QUOTA или грубо через imapsync --justfolders --justfoldersizes, с Maildir-источника — банальным du -sh. Это нужно, чтобы посчитать окно на bulk: при реальной (заthrottленной провайдером) скорости 15–40 сообщений/с ящик на 80 тыс. писем переносится от получаса до полутора часов, а сотня таких ящиков в четыре потока — это уже двое-трое суток.
Провайдерские грабли, на которых спотыкаются чаще всего:
Gmail / Google Workspace. При включённой 2FA обычный пароль не работает — нужен App Password (пароль приложения). Обязателен флаг --gmail1: он включает правильный маппинг лейблов в папки и дедупликацию через [Gmail]/All Mail (иначе каждое письмо приедет столько раз, во скольких лейблах оно висит). Лимит примерно 15 одновременных IMAP-сессий на аккаунт — параллелить агрессивнее -j 8 бессмысленно, начнутся обрывы и [ALERT] Too many simultaneous connections.
Yandex / Mail.ru. Включить доступ по IMAP в настройках ящика и завести пароль приложения — с 2022 года без него IMAP-логин отбивается.
Office 365 / Exchange Online. Basic auth отключён. Либо OAuth2 (--oauth2 --oauthaccesstoken1), либо ничего. Планируйте это заранее, это не тумблер на пять минут.
На стороне приёмника (Dovecot на стеке evilmail.pro) ящики создаются заранее — imapsync не регистрирует пользователей. Проверьте, что схема пароля PLAIN, uid/gid 5000, а квота на целевом ящике не меньше исходного объёма, иначе на середине bulk словите Over quota. Maildir лежит в /var/mail/vhosts/{domain}/{user}/.
Первый прогон: всегда `--dry`, потом bulk
Никогда не запускайте боевой прогон вслепую. Первый вызов — всегда с --dry: он логинится в оба ящика, перечисляет папки, считает сообщения и байты, но ничего не пишет.
В отчёте смотрите три вещи: обе стороны залогинились без ошибок, количество папок и сообщений выглядит вменяемо, нет NO/BAD от сервера. Если тут падает логин — дальше идти незачем.
Что здесь важно. --useheader 'Message-Id' задаёт заголовок для сравнения сообщений — надёжнее, чем полагаться только на размер тела. --usecache пишет локальный кэш уже перенесённых пар UID, чтобы повторный прогон не перечитывал всё заново. --automap сам сопоставляет системные папки (Sent/Отправленные, Drafts/Черновики) между разными провайдерами. --noexpunge1 — страховка: imapsync не тронет источник, что бы ни случилось. --exclude выкидывает служебные папки Gmail (спам, корзину, «Важное»); имена этих папок у Gmail всегда идут через префикс [Gmail]/, поэтому и регэксп такой, а не голое ^Spam$.
Отдельно про параллелизм, потому что это ошибка №1. imapsync однопоточен внутри ящика. Гнать «в 50 потоков» один ящик нельзя — такого режима просто нет. Параллелизм даётся на уровне процессов: несколько ящиков одновременно, каждый своим процессом. Внутри ящика — один линейный поток, и это правильно, потому что IMAP-сервер источника всё равно ограничит вас по сессиям.
Массовый перенос: файл аккаунтов + parallel
Складываете все ящики в текстовый файл, по строке на ящик, с разделителем ;:
Дальше есть два пути. Штатный sync_loop_unix.sh из поставки imapsync читает file.txt того же формата и гонит ящики последовательно — просто и надёжно, но медленно. Для скорости берём GNU parallel:
Оборвался процесс, упала сеть, перезагрузился сервер — просто перезапускаете ту же команду. Благодаря --usecache уже перенесённые сообщения пропускаются, прогон продолжается почти с места обрыва. Это и есть та самая идемпотентность, ради которой всё затевалось.
Дельта-синхронизация: держим приёмник горячим до cutover
Bulk закончился, но MX всё ещё смотрит на старый сервер, туда продолжает капать свежая почта. Пока не переключились — гоняем ту же самую команду по cron, например раз в 2–3 часа:
Каждый прогон дельты переносит только новые письма — при --usecache это секунды на ящик вместо часов. Флаги удаления на этом этапе не ставим: никаких --delete1, --delete2, --expunge1. Задача дельты — добирать, а не синхронизировать удаления. Если пользователь удалил письмо на старом сервере, а вы его уже перенесли — пусть остаётся на новом до финала, это безопаснее, чем случайно снести нужное.
Одна тонкость про UIDVALIDITY. Это счётчик валидности UID-пространства папки на источнике. Если он меняется (пересоздали ящик, провайдер переехал на бэкенде), кэш imapsync инвалидируется, и следующий прогон снова станет полным — долгим. На стабильных источниках это не проблема, но если src ведёт себя странно и дельты внезапно тащат всю историю заново — первым делом проверяйте, не сменился ли UIDVALIDITY.
Критерий готовности прост: когда каждый прогон дельты приносит единицы сообщений вместо тысяч, приёмник «прогрет» и можно планировать cutover.
Cutover: переключение MX без простоя
За 24–48 часов до часа X срезаем TTL MX-записи до 300 секунд. Это делается заранее, чтобы к моменту переключения старое значение TTL уже выветрилось из кэшей резолверов и новый MX разошёлся за пять минут, а не за сутки.
В час X меняем MX и SPF на новый сервер:
dns
; было: MX на старый провайдер
; стало:
old.com. 300 IN MX 10 mail.evilmail.pro.
old.com. 300 IN TXT "v=spf1 include:evilmail.pro ~all"
selector._domainkey.old.com. 300 IN TXT "v=DKIM1; k=rsa; p=MIGfMA0..."
_dmarc.old.com. 300 IN TXT "v=DMARC1; p=quarantine; rua=mailto:[email protected]"
Старый сервер не выключаем. Пока DNS расходится по миру, наступает split-brain окно: часть отправителей уже видит новый MX и шлёт на evilmail, часть ещё держит старый в кэше и шлёт туда. Если убить старый сервер прямо на cutover — вся почта от «отставших» резолверов отобьётся или зависнет. Поэтому старый принимающий узел живёт как ни в чём не бывало, а мы просто продолжаем добирать с него дельту.
После того как MX устоялся (обычно 1–2 часа при TTL 300), запускаем финальную догоняющую синхронизацию — тот же imapsync, забирающий последние письма, что упали на старый сервер в split-brain окне. Вот здесь, и только здесь, после подтверждения полноты, можно добавить --delete1, если по политике нужно вычистить источник.
Финализация и откат
Полноту переноса проверяем по хвосту лога imapsync — там в конце печатается сводка:
text
Host1 Nb messages in all folders : 84213
Host2 Nb messages in all folders : 84213
Total bytes transferred : 0 (0.000 KiB) # на финале дельта нулевая
Совпали Host1 и Host2 по всем ящикам — количественно перенос полный. Дальше spot-check: открываете пару крупных папок в веб-клиенте нового сервера, проверяете, что вложения на месте и флаги прочитанности сохранились. И контрольный признак, что cutover сработал — новая почта идёт только на приёмник:
bash
tail -f /var/log/mail.log # на evilmail: видим входящие; на старом — тишина
Старый сервер держим живым минимум две недели. Причины две: отложенная доставка (некоторые отправители ретраят зависшие письма несколько суток) и откат. План отката тривиален именно благодаря тому, что источник цел: возвращаете MX обратно — и все письма по-прежнему там, вы ничего не потеряли, потому что на дельтах не включали удаления.
Чек-лист перед объявлением «готово»
Все ящики созданы на Dovecot, квоты ≥ исходного объёма, схема PLAIN, uid/gid 5000
--dry прогон прошёл чисто по каждому ящику, логины на обеих сторонах ОК
Bulk завершён, Host1 == Host2 в логах всех ящиков
Дельты по cron приносят единицы сообщений (источник прогрет)
TTL MX срезан до 300 за 24–48 ч до cutover
MX, SPF, DKIM (selector), DMARC переставлены на evilmail.pro
Старый сервер продолжает принимать почту (не выключен)
Финальная догоняющая синхронизация выполнена после устаканивания DNS
tail -f /var/log/mail.log подтверждает: новая почта только на приёмнике
Старый сервер оставлен живым на 2 недели, план отката (MX назад) зафиксирован
Вся суть в одном: копирование идёт при работающем источнике, а простоя нет, потому что после переключения MX остаётся догнать не сотни гигабайт, а несколько часов дельты. imapsync делает это ровно потому, что его можно запускать сколько угодно раз, и каждый следующий прогон дешевле предыдущего.