mail · Почта 26.1100.0 Заметное

Mail 26.1100.0: smtpd.php без токена отвечает 404, а ящики удалённых пользователей удаляет агент

5 мин чтения
Узнавайте о ломающих изменениях Битрикса раньше, чем они доедут до прода: свежие разборы — в канале @bxmax_updates

Mail 26.1100.0 вышел следом за 26.1000.0. Вендор пишет только о повышенной стабильности подключённых ящиков. В коде за этим стоят три вещи: инфраструктура «поколений источника» для переезда ящика на другой IMAP-сервер без потери истории (по умолчанию выключена), переделанное удаление ящиков и писем и защита встроенного SMTP-сервера от запуска по HTTP и от второго экземпляра.

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

Что сломается

Проверьте, как вы запускаете smtpd.php

HTTP-запрос к smtpd.php без переменной окружения BITRIX_SMTPD_COMMAND_TOKEN теперь получает 404. Раньше файл запускал CSMTPServer::Run() при любом вызове. Страница admin/mail_smtpd_manager.php передаёт токен процессу через окружение сама, а запуск из CLI без токена работает: Run(false) сам записывает команду start. Если же сервер поднимал крон или скрипт запросом по URL, после обновления этот запрос получит 404, и сервер не стартует. Найти такие места:

        crontab -l 2>/dev/null | grep -n smtpd
grep -rn "smtpd.php" /etc/cron* /etc/systemd/system 2>/dev/null

    

Второй экземпляр запустить больше нельзя. Сервер берёт flock на <upload_dir>/.smtpd.lock и проверяет, что это обычный файл с теми же dev и ino. Сторож в кроне, который перезапускал сервер на всякий случай, лишнего процесса уже не поднимет.

Команды start и stop теперь хранятся в b_option (smtpd_command, smtpd_stop_token) и читаются с мастера (StartUsingMasterOnly). Раньше стоп передавался флагом кеша smtpd_stop. Если ваш код останавливал сервер через этот флаг, теперь сервер на него не реагирует.

Поменялись и сигнатуры. CSMTPServer::Run($commandToken = false) завершается exit, Start() возвращает bool, добавлены SetCommand(), GetCommand(), ReportStartupError() и CSMTPServerHost::Shutdown().

Не ждите, что ящики пользователя исчезнут вместе с ним

CMail::onUserDelete() больше не удаляет ящики удалённого пользователя сразу. MailboxDeletionRequestService::request() деактивирует ящик, ставит агент Bitrix\Mail\Helper::deleteMailboxAgent(id) с интервалом 60 секунд, чистит счётчик и данные синхронизации. Тот же сервис использует MailboxConnector.

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

CMailbox::Delete() тоже переделан:

  • сначала снимает агенты синхронизации ящика и ставит SYNC_LOCK;
  • до 30 секунд ждёт блокировку операций миграции (MailboxDeletion::takeMailboxOver());
  • удаляет физические данные всех поколений до обхода писем и ещё раз после него.

Проверьте код вокруг CMailMessage::Delete()

Сигнатура теперь CMailMessage::Delete($id, ?int $mailboxId = null, bool $entityOptionsClearedByCaller = false, bool $withPlacements = true). С параметрами по умолчанию метод удаляет и строки b_mail_message_uid этого письма во всех поколениях, очереди выгрузки и удаления, отпечатки и журнал сопоставления (Helper::deleteMessagePlacements()). В 26.1000.0 Delete() этих строк не трогал.

Если после Delete() ваш код сам чистил b_mail_message_uid или, наоборот, рассчитывал найти там строку письма, поправьте его. Новый параметр стоит последним намеренно: по docblock, все позиционные вызовы передают вторым аргументом ящик.

Учтите вторую копию пароля у новых IMAP-ящиков

CMailBox::Add для IMAP-ящика сразу создаёт первое поколение (G1) в b_mail_mailbox_source_generation с копией SERVER, PORT, USE_TLS, LOGIN и PASSWORD, а затем выставляет b_mail_mailbox.ACTIVE_GENERATION_ID (BackfillService::onMailboxAdded()). Флаг source_generations_enabled на это не влияет, он решает только, работают ли поколения при синхронизации. Выходит, что у каждого нового IMAP-ящика реквизиты с паролем лежат в двух таблицах. Настройку шифрования пароля поколения MailboxSourceGenerationTable::passwordCryptoEnabled() берёт у b_mail_mailbox: шифрование в ORM включается по имени таблицы, и без этого пароль поколения молча остался бы на старом шифровании модуля.

Добавьте GenerationScope в переопределения

Необязательный параметр ?GenerationScope появился:

  • в статических методах MailboxDirectory: fetchAll*, fetchOne* (кроме fetchOneById()), fetchTrashAndSpamHash(), countSyncDirs(), countMessagesSyncDirs(), getMinSyncTime(), resetDirsTypes(), updateSyncDirs();
  • в MailMessageUidTable::getFirstLocalUID(), getLastLocalUID() и getLocalUID();
  • в конструкторах MailboxDirectoryStorage и MailboxDirectoryHelper;
  • в Helper::getLastDeletedOldMessageInternaldate(), SettingsReader::read(), SyncFlagsUpdater::update() и TypesAssigner::assign() (SettingsReader, SyncFlagsUpdater и TypesAssigner объявлены final, наследовать их нельзя).

Другие необязательные параметры получили MailboxDirectory::invalidateCache(?int $mailboxId = null, ?int $generationId = null), Helper\Mailbox::listMessages(..., bool $withGenerationScope = true), MailboxDirectoryHelper::syncChildren($parent, ?Mailbox $engine = null), Mail\Imap::getUidsSince(..., ?int $minimumUid = null) и MailsFoldersManager::syncMovedMessages(..., $generationId = 0).

Вызывающий код все эти изменения переживёт. Наследникам придётся поправить сигнатуры. PHP 8 не пропустит переопределение без нового параметра, и класс упадёт с fatal error о несовместимой сигнатуре прямо при загрузке. Конструкторы PHP так не проверяет, там сверьте, что вы передаёте родителю. Исправленное переопределение выглядит так:

        <?php declare(strict_types=1);

namespace Vendor\Mailext;

use Bitrix\Mail\Internal\Service\SourceGeneration\GenerationScope;
use Bitrix\Mail\MailboxDirectory;

class AuditedMailboxDirectory extends MailboxDirectory
{
    // до 26.1100.0 было fetchAllSyncDirs($mailboxId)
    public static function fetchAllSyncDirs($mailboxId, ?GenerationScope $scope = null)
    {
        $dirs = parent::fetchAllSyncDirs($mailboxId, $scope);
        // своя логика поверх списка папок

        return $dirs;
    }
}

    

У Helper\Mailbox\Imap появилось protected-поле $resyncWalkIncomplete = true, а resyncDirInternal() может вернуть 'incompleteList'. Если обход папки шёл по неполному списку, письма в «переезжающем» состоянии больше не объявляются удалёнными на сервере. Комментарий в lib/helper/mailbox/imap.php объясняет, почему признак передаётся полем: новый параметр сломал бы объявление resyncMessages() у наследника. Из-за осторожного значения по умолчанию свой обход папки, который вызывает resyncMessages() и не выставляет $resyncWalkIncomplete = false, по своему списку ничего не удалит.

Controller\Message (lib/controller/message.php) теперь ищет строку UID только в активном поколении ящика. ID строки из архивного поколения больше ни на что не разрешается.

Новое

Переезд ящика на другой сервер лежит за флагом

Около 40 классов в lib/Internal/Service/SourceGeneration/* готовят переезд ящика на другой IMAP-сервер без потери истории. Включает их опция source_generations_enabled, по умолчанию N (у модуля появился default_option.php). Опция source_generations_migration_stopped работает аварийным тормозом: останавливает новые миграции и синхронизацию подготовленных поколений.

  • MigrationService::run() за один ограниченный проход двигает операцию по стадиям MigrationStage: INITIALIZING_G1, G1_READY, PREPARING_G2, SHADOW_MATCHING, MATCHING, TAIL_APPEND, AWAITING_SWITCH_AUTHORIZATION, READY_TO_SWITCH, SWITCHED. Если папка меняет UIDVALIDITY, срабатывает откат RESETTING_IMPORT.
  • Письма сопоставляют Matching\MessageMatcher, FingerprintBuilder и CanonicalMessageNormalizer. В базе хранятся только SHA-256 отпечатки Message-ID, содержимого и ссылки мигратора.
  • Письма, которые внешний сервис не перенёс, модуль дописывает в новый ящик сам через IMAP APPEND (TailAppendService, TailMessageBuilder). Если сервер не вернул APPENDUID, письмо находят по одноразовой метке в заголовке X-Bitrix-Mail-Tail-Mark (TailMarkService).
  • Переключает ящик MailboxMigrationSwitcher: меняет указатель поколения, пишет проекцию IMAP и SMTP в b_mail_mailbox и отправляет старый адрес в историю через новый AddressHistory::registerWithinTransaction().
  • GenerationScope фильтрует физические строки по GENERATION_ID. Строки с неявным 0 считаются строками активного поколения.

Для внешнего мигратора модуль оставил контракты в lib/integration/MailService/*: ReadyMailboxReceiver::accept(ReadyMailbox), MailboxSwitchAuthorizationReceiver::accept(), MailboxMigrationCancelReceiver::accept() и MigrationStatusProvider. Методы accept() приёмников внутри модуля никто не вызывает, а MigrationStatusProvider модуль использует сам: через него статус читают MigrationActionGuard, Controller\Migration и агент миграций. Свой идентификатор письма мигратор может передать в заголовке X-Bitrix-Mail-Migrator-Reference (Imap::MIGRATOR_REFERENCE_HEADER).

Статус переезда отдаёт Controller\Migration: getStatusAction(int $mailboxId) и getStatusesAction(array $mailboxIds), только POST с CSRF и не больше 100 ящиков в пакете. На фронте его читает расширение mail.migration-state.

Пока миграция ящика видна как активная, MigrationActionGuard блокирует действия с ним в ajax.php клиента, MailboxConnecting, MessageActions и MessageSender. REST mail.message.* в это время отвечает ошибкой MAILBOX_MIGRATION_ACTION_BLOCKED. Если ваша интеграция шлёт письма через REST, научите её обрабатывать этот код. Если статус прочитать не удалось, REST отвечает MAILBOX_MIGRATION_STATUS_UNAVAILABLE, его тоже стоит обработать. isBlocked() в интерфейсе в этом случае возвращает true.

Фоновая часть:

  • агент Internal\Agent\MigrationOperationAgent::run() раз в 60 секунд, пакет 20, бюджет 60 секунд;
  • агент DeletedMailboxCleanupAgent::run(0) раз в час, курсор по ID ящиков хранится в имени агента;
  • очередь Messenger mail_migration_notification и уведомление Integration\Im\Notification::notifyUserAboutMailboxMigration();
  • степпер Update\SourceGenerationBackfillStepper, который раздаёт G1 существующим ящикам. Он выключен опцией source_generations_backfill_enabled и без неё завершается на первом же шаге.

Ссылки на большие вложения выбирают шлюз

Появился интерфейс Integration\Disk\MailAttachmentLinkGateway с двумя реализациями. DiskMailAttachmentLinkGateway вызывает команды диска CanCreateMailAttachmentLinkCommand и CreateMailAttachmentLinkCommand. LegacyMailAttachmentLinkGateway создаёт обычную ручную ссылку, это временный класс, чтобы mail мог выйти раньше disk 26.1200.0. LargeAttachmentStorageFactory выбирает шлюз через class_exists() при каждом вызове, рядом появился DTO Dto\LargeAttachmentTokenPayload. Как большие вложения уходят на Диск, мы разбирали в mail 26.900.0.

Массовое подключение фильтрует сотрудников по правам

Провайдер entity-selector mail-massconnect-user (Integration\UI\EntitySelector\MassConnectUserProvider) подменяет user. Клиентские опции он игнорирует и подставляет свой набор SAFE_OPTIONS, а выбор ограничивает сотрудниками отделов, на которые у текущего пользователя есть право редактирования ящиков (MAIL_MAILBOX_LIST_ITEM_EDIT). В форме массового подключения теперь можно выбирать отдельных сотрудников.

Прочее

Helper\Message\MessageSender::__construct() принимает зависимости: интерфейс MessageSenderSourceProvider (по умолчанию DefaultMessageSenderSourceProvider) и замыкания транспорта, цитаты, санитайзера и резолвера хоста.

Кроме того, появились CAllMailMessage::completeClosureChain(), Helper\Mailbox::getLastSyncRefusal(), isSyncAllowedByLicense() и syncUnderHeldMailboxLock(), Integration\Attachment::materialize*() и hasAllExpectedAttachments(), Internal\Service\Attachment\FileNameNormalizer и ActiveMessageLocator.

БД

В install/index.php в InstallDB() сразу после RegisterModule("mail") вызывается MigrationOperationSchema::clearCache(). Он сбрасывает закешированный ответ на вопрос, готова ли схема b_mail_migration_operation. Отрицательный ответ хранится 300 секунд, положительный сутки, плюс WeakMap на соединение. InstallDB() работает при установке модуля, а как кеш сбрасывается при обновлении, из кода релиза не видно.

Саму схему меняет install/migrations/tables.php:

  • b_mail_mailbox: ACTIVE_GENERATION_ID int NOT NULL DEFAULT 0 и индекс IX_B_MAIL_MAILBOX_ACTIVE_GEN;
  • колонка GENERATION_ID int NOT NULL DEFAULT 0 в b_mail_message_uid, b_mail_message_upload_queue, b_mail_message_delete_queue и b_mail_mailbox_dir с составными индексами, среди них покрывающий IX_MAIL_MESSAGE_UID_GEN_LIST для списка, страницы папки и счётчиков;
  • b_mail_message: индекс IX_MAIL_MESSAGE_MBX_ID (MAILBOX_ID, ID) для keyset-обхода;
  • индекс IX_MAIL_MSG_UID_DIR_START_DATE из 26.1000.0 переименован в IX_MAIL_MESSAGE_UID_DIR_START_DATE;
  • b_mail_message_delete_queue.PK потерял unsigned.

Новые таблицы:

  • b_mail_mailbox_source_generation: поколения с копией IMAP- и SMTP-реквизитов, статусы PREPARING, ACTIVE, ARCHIVED;
  • b_mail_migration_operation: очередь и состояние операций;
  • b_mail_message_fingerprint;
  • b_mail_source_generation_match: журнал решений MATCHED, NEW, AMBIGUOUS;
  • b_mail_source_generation_tail_mark: хранит только MARK_HASH, UNIQUE-ключи по хешу и по паре (GENERATION_ID, MESSAGE_ID).

В install/migrations/agents.php добавлен MigrationOperationAgent::run(). DeletedMailboxCleanupAgent добавляется, только если агента нет по маске имени ...::run(%. Новые опции: source_generations_enabled, source_generations_migration_stopped, source_generations_backfill_enabled, smtpd_command, smtpd_stop_token.

Мелочи и находки

  • Docblock степпера бэкфилла объясняет, почему тот выключен. bindClass() даёт агенту интервал в одну секунду, и портал получал непрерывный поток пакетных апдейтов. Степпер, которого не пустили, завершается сам и заодно убирает свой агент и опцию. Там же ссылка на внутреннюю базу знаний .ai/knowledge-base/notes/source-generation-indexes.md и задачу «P1.T3», а в Helper\Mailbox попадаются метки внутренних документов TPL-01, ALG-01, DATA-01.
  • TailMarkService и TailMessageBuilder прямо описывают слабость заголовка отправки X-Bitrix-Mail-Message-UID. В нём открытый и угадываемый ID строки, его читает любой проход синхронизации в любой папке, и обработка переписывает названную строку. Поэтому в восстановленных письмах он запрещён (FORBIDDEN_HEADERS). Остаточный риск тоже записан. Тот, у кого есть доступ к ящику, может подложить письмо с той же меткой, но только пока метка жива, а живёт она три дня.
  • Комментарий к TailMarkService::HEADER гласит «Not an X-Bitrix-Mail-* name on purpose», а значение константы при этом X-Bitrix-Mail-Tail-Mark.
  • TailMessageBuilder принудительно кодирует части в base64. Ядро берёт кодировку из опций портала, по умолчанию 8bit, а IMAP APPEND отправляет литерал, не спрашивая сервер, примет ли он 8-битные байты.
  • GenerationScope::forMailbox() читает указатель поколения сырым SQL, потому что ORM-карта другого релиза может не знать колонку. В BackfillService::onMailboxDeleted() та же мысль записана короче: файлы обновления приезжают на установку раньше, чем её база.
  • Helper::deleteMessagePlacements() собирает ID отдельным запросом вместо подзапроса. Колонка строковая, а сравнение строки с числом, по комментарию, на одной СУБД работает, а на другой даёт ошибку.
  • MailboxDirectorySettingsService::getSettings() и loadChildren() получили bool $mayEditMailbox. Контроллер его передаёт, но внутри параметр пока не используется.
  • Controller\Migration добавляет фильтр Intranet\ActionFilter\IntranetUser без Loader::includeModule('intranet'). Controller\Draft и Controller\MailTemplate добавляют тот же фильтр только после такой проверки.

Новая версия вышла — разбор уже в Telegram

В канале @bxmax_updates публикуем каждый свежий разбор: что сломается, что устарело и что появилось в API. Узнаете до того, как обновите прод.

Подписаться

Читайте дальше

mail 26.1200.0 Заметное Свежее

Mail 26.1200.0: отложенная отправка, отмена отправки и автоответ за выключенными опциями

Mail 26.1200.0 идёт следом за [26.1100.0](/bitrix-updates/mail/mail-26-1100-0) и по объёму тянет на несколько релизов: отложенная отправка, отмена отправки, автоответ «в отпуске», группировка писем в переписки, картинки в подписях и генерация темы через AI. Всё это выключено опциями модуля, и после…

2 мин
mail 26.1000.0 Заметное Свежее

Mail 26.1000.0: FIELD_BCC без конвертных получателей, один адрес на портал и транзакции в MailboxTable

Mail 26.1000.0 вышел следом за [26.900.0](/bitrix-updates/mail/mail-26-900-0) и снова большой. Много кода спрятано за выключенными флагами: серверные черновики, метки, новая форма письма, AI-классификация входящих, макросы в подписях. Сразу после обновления заработают запрет повторного подключения а…

3 мин
mail 26.900.0 Заметное

Mail 26.900.0: общие подписи ломают старый API, а тяжёлые вложения переезжают на Диск

Модуль mail получил один из самых жирных релизов за последнее время: 523 файла, +39 182/−3 131 строк. Внутри — три большие фичи (общие подписи, автоперенос тяжёлых вложений на Диск, избранное с AI-классификацией писем), переработанный IMAP-синк истории и три новые таблицы в БД. Если вы кастомизирова…

4 мин
mail 26.700.0 Заметное

mail 26.700.0: статистика писем для AI-ассистента и предупреждение о провайдерах

Модуль научился считать входящие по ящику или сотруднику за период и отдавать это AI-ассистенту как инструмент. Плюс появился слой ограничений по почтовым провайдерам. Основной риск обновления — не удаления, а вставка аргументов в середину конструкторов двух DTO.

3 мин
Мы используем файлы cookie для улучшения работы сайта. Продолжая использовать сайт, вы соглашаетесь с нашей политикой конфиденциальности.
AI Домовой

AI Домовой История

на связи

пишет…
Нет истории чатов
AI Домовой

Нужна авторизация

Войдите, чтобы задавать вопросы AI Домовому.

Войти