Что на самом деле значит «удалить письмо»
Менеджер получает запрос об удалении персональных данных, открывает вебмейл, находит письмо, жмёт «Удалить» и отчитывается: исполнено. На диске в этот момент не изменилось почти ничего.
Одно входящее письмо к моменту запроса существует не в одном экземпляре, а в шести-восьми независимых копиях. Оригинал лежит в maildir получателя. Копия — в папке Sent отправителя, если он тоже ваш пользователь. Заголовки и куски тела закешированы в dovecot.index.cache. Полный текст письма проиндексирован в FTS-бэкенде (Xapian или Solr) и живёт там отдельным блобом. Адреса и Message-ID записаны в mail.log. Если письмо ещё не доставлено — оно в активной очереди Postfix. Спам-движок мог отложить копию в карантин. И, наконец, всё перечисленное вчера ночью уехало в бэкап, а оттуда — в офсайтовое хранилище.
Клик по «Удалить» трогает ровно одну из этих поверхностей — ящик получателя, да и то лишь перекладывает письмо в Корзину, а не стирает. Удаление ПДн — это операция по всей поверхности хранения, а не действие в одном ящике. Ниже — как эта поверхность выглядит и как её реально зачистить.
Юридическая рамка за 60 секунд
Два режима, одна инженерная задача.
GDPR, статья 17 — «право на стирание». Субъект вправе требовать удаления своих данных, оператор обязан ответить в течение одного месяца (ст. 12(3)), с продлением до трёх при обоснованной сложности. 152-ФЗ, ст. 21 — оператор обязан прекратить неправомерную обработку и уничтожить ПДн в установленный срок, а требование Роскомнадзора исполняется в предписанный период.
Что удалять обязательно — переписку, контакты, вложения, метаданные, где субъект идентифицируем. Что можно и нужно удержать на законном основании: бухгалтерские и налоговые записи в пределах сроков хранения, данные под активным правовым требованием (спор, претензия), антифрод-логи в рамках объявленного retention. Право на стирание не абсолютно — оно уступает другому законному основанию обработки.
Ваши союзники здесь — принципы минимизации (ст. 5(1)(c)) и ограничения хранения (ст. 5(1)(e)). Чем меньше вы храните и чем короче, тем меньше поверхность стирания. Практический вывод: сначала фиксируем scope — что удаляем и что законно удерживаем, с обоснованием, — и только потом трогаем диск.
Инвентаризация: где живёт почта субъекта
Карта поверхностей с реальными путями (на стеке evilmail — Dovecot + Postfix + rspamd):
- Maildir:
/var/mail/vhosts/{domain}/{user}/Maildir/{cur,new,tmp}— сами письма, по файлу на сообщение. - Индексы Dovecot:
dovecot.index,dovecot.index.cache,dovecot.list.indexв каталоге ящика. В.cacheлежат закешированные заголовки и куски тела. - FTS-бэкенд: Xapian в
/var/lib/dovecot/либо коллекцияdovecotв Solr. Полнотекстовый индекс держит тело письма отдельно от maildir. - Логи MTA:
/var/log/mail.log*, плюс journald. - Очередь Postfix:
/var/spool/postfix/{active,deferred,hold}— если письмо ещё в пути.
Две поверхности забывают почти всегда. Первая — почта субъекта в чужих ящиках: он писал вашим пользователям, значит его адрес и имя лежат в их INBOX и в их Sent. Вторая — внешние интеграции: CRM, тикет-система, рассылочный движок, отдельный поисковый индекс. Удаление из maildir без зачистки этих двух — это неисполненный запрос.
Поиск: находим все следы
Сначала соберите полный набор идентификаторов. Один субъект — это основной адрес, все алиасы, старые адреса, отображаемое имя в кириллице И латинице (Иван Петров / Ivan Petrov), плюс Message-ID цепочки, если речь о конкретной переписке. Искать надо по From, по To/Cc, в теле и в заголовках (Return-Path, Reply-To).
# найти во всех папках одного ящика (or здесь — инфиксный оператор Dovecot)
doveadm search -u [email protected] mailbox '*' \
from [email protected] or to [email protected]
# обойти всех виртуальных пользователей сервера
doveadm user '*' | while read u; do
hits=$(doveadm search -u "$u" mailbox '*' \
from [email protected] or to [email protected] | wc -l)
[ "$hits" -gt 0 ] && echo "== $u : $hits"
done
# сырой поиск по дереву maildir (тело + заголовки), два алфавита
grep -rliE 'subject@example\.com|Ivan Petrov|Иван Петров' /var/mail/vhosts/
# логи и очередь
grep -i '[email protected]' /var/log/mail.log*
mailq | grep -i [email protected]grep -rl по maildir ловит то, что doveadm search может пропустить из-за расхождения индекса с диском — например, письма, попавшие мимо LDA. Прогоняйте оба.
Удаление в живой системе
Сначала разберитесь, какое у вас хранилище (mail_location) — от этого зависит вся процедура. На evilmail это maildir:~/Maildir, и здесь важная деталь: на Maildir `doveadm expunge` сразу удаляет файл сообщения (unlink) — каждое письмо это отдельный файл, отдельная команда purge не нужна и на Maildir она ничего не делает. doveadm purge осмыслен только на mdbox/sdbox: там expunge лишь помечает сообщение (refcount), а физически данные вырезает purge, схлопывая dbox-файлы. Не перепутайте: на Maildir ждать «purge» после expunge — значит ничего не сделать.
Но и на Maildir есть нюанс: unlink убирает имя файла, а блоки с телом письма остаются на диске до перезаписи. Гарантию физического стирания даёт только шифрование тома (LUKS/dm-crypt) или очистка на уровне ФС — на уровне почтового сервера этого не решить.
# удалить из INBOX получателя и из Sent отправителя (Maildir: файл уходит сразу)
doveadm expunge -u [email protected] mailbox INBOX from [email protected]
doveadm expunge -u [email protected] mailbox Sent to [email protected]
# ТОЛЬКО для mdbox/sdbox: физически вырезать данные из dbox-файлов
# doveadm purge -u [email protected]
# пересборка FTS, иначе тело письма остаётся в поисковом индексе
doveadm fts rescan -u [email protected]
doveadm fts optimize -u [email protected]Три подводных камня. Первый — FTS: без rescan/optimize тело удалённого письма продолжает всплывать в поиске и, что важнее, физически лежит в индексе Xapian. Второй — `.cache`: закешированные заголовки инвалидируются при экспандже, но чтобы гарантированно пересобрать индекс каталога, прогоните doveadm index -u [email protected] '*'. Третий — hardlink и дедупликация: при включённом single-instance storage (sis, например для вложений) или на бэкапах с дедупом одна inode держит данные живыми, даже когда «последняя» видимая копия удалена. Проверьте счётчик ссылок перед тем, как считать письмо стёртым:
find /var/mail/vhosts -samefile /path/to/message | wc -lНе забудьте карантин (rspamc / каталог amavis) и очередь — застрявшее письмо удаляется через postsuper -d <queue_id>.
Самое сложное: бэкапы и логи
Здесь начинается отдельная война, и здесь ломается большинство «исполненных» запросов.
Правильно устроенные бэкапы — append-only и immutable: borg с append-only, restic с WORM-хранилищем, S3 с Object Lock. Это защита от шифровальщика, и это же делает точечное вырезание одного письма невозможным без цепочки restore → scrub → re-backup, которая рвёт целостность и историю снапшотов. Резать снапшот скальпелем — плохая идея.
Два легитимных пути.
Путь 1 — документированное исключение (обычно правильный). Данные субъекта в бэкапах помечаются к удалению при следующей ротации и НЕ восстанавливаются в прод ни при каком restore. В ответе субъекту прямо указывается срок хранения бэкапов, по истечении которого копия исчезнет физически. GDPR такой подход допускает: живая обработка прекращена немедленно, бэкап гасится по объявленному retention.
# не вырезаем точечно — управляем retention всего репозитория
restic snapshots
restic forget --keep-within 30d --pruneПуть 2 — дождаться ротации при коротком retention: если цикл бэкапов 7–14 дней, часто проще дождаться естественного истечения снапшотов, чем городить restore-scrub. Главное — зафиксировать дату, к которой копия гарантированно исчезнет.
Логи — та же логика. PII в mail.log и journald гасится ротацией и вакуумом, а не редактированием файлов:
journalctl --vacuum-time=30d
# в logrotate — сжатие + короткий maxage; при необходимости — анонимизация адресов/IPДоказательство и аудит
Исполнение без доказательства = неисполнение. Регулятор и субъект вправе спросить «покажите». Фиксируйте:
- Журнал операции: кто, когда, какие команды выполнил, сколько сообщений затронуто. Вывод
doveadm expungeдаёт число. - Контрольный повторный поиск после чистки, возвращающий ноль совпадений в живой системе — тот же
doveadm searchиgrep, что на этапе инвентаризации. - Зафиксированный retention бэкапов с датой, к которой копия исчезнет.
- Письменный ответ субъекту с датой исполнения и явным упоминанием законно удержанных данных.
Сам аудит-журнал храните как минимально необходимый набор — факт операции и идентификаторы, без содержимого писем. Аудит удаления ПДн не должен становиться новой копией ПДн.
Чеклист исполнения запроса
- 1.Подтвердить личность заявителя — иначе вы удаляете чужие данные по чужой просьбе.
- 2.Собрать все идентификаторы: основной адрес, алиасы, старые адреса, имя в двух алфавитах.
- 3.Определить scope: что удаляем и что законно удерживаем (бухгалтерия, спор, антифрод) — с обоснованием.
- 4.
doveadm search+grep -rlпо всем ящикам, папкам, логам и очереди. - 5.
doveadm expungeв INBOX/Sent/shared (на Maildir файл уходит сразу; на mdbox — затемpurge). - 6.
doveadm fts rescan+optimize— вычистить тело из поискового индекса. - 7.Карантин rspamd/amavis, застрявшая очередь (
postsuper -d).
Privacy by design: почему TTL решает половину проблемы
Каждый пункт выше — это работа, потому что данные лежат долго и в множестве копий. Убери одно из двух — и запрос на стирание схлопывается.
Именно на этом построена временная почта evilmail: ящики и сообщения имеют TTL и истекают автоматически, долгого хранения нет по умолчанию, минимизация встроена на входе. Когда данные субъекта живут часы, а не годы, поверхность стирания падает на порядок: нет разросшихся Sent в чужих ящиках, нет писем в квартальных бэкапах, FTS-индекс короткий и самоочищается.
Лучший запрос на удаление — тот, который нечего исполнять, потому что данных уже нет.


