Mail 26.900.0: общие подписи ломают старый API, а тяжёлые вложения переезжают на Диск
Модуль mail получил один из самых жирных релизов за последнее время: 523 файла, +39 182/−3 131 строк. Внутри — три большие фичи (общие подписи, автоперенос тяжёлых вложений на Диск, избранное с AI-классификацией писем), переработанный IMAP-синк истории и три новые таблицы в БД. Если вы кастомизировали работу с подписями или наследовались от почтовых контроллеров — читайте внимательно, там breaking changes.
Что сломается
Контроллер UserSignature сменил контракт. У getAction / updateAction / deleteAction параметр больше не объект \Bitrix\Mail\Internals\Entity\UserSignature, а простой int $userSignatureId. Метод checkAccess(UserSignature) удалён. Внутри контроллер теперь работает через SharedSignatureService и при обращении лениво домигрирует подписи пользователя (SignatureMigrator::migrateUser).
Controller\Base похудел. Удалены protected-методы init(), convertArrayKeysToCamel() и toCamelCase(). Вместе с init() пропала и Binder-регистрация автоподстановки сущности UserSignature по id в параметры действий. Если ваши контроллеры наследуются от Bitrix\Mail\Controller\Base и дёргали эти методы — они упадут.
UserSignatureTable::getList() теперь адаптер, а не таблетка. По возможности он отвечает из новой единой модели (b_mail_shared_signature), отдавая ответ в легаси-формате с полями ID/USER_ID/SENDER/SIGNATURE, в остальных случаях уходит в легаси-таблицу b_mail_user_signature. Самое коварное: во время миграции чтение по конкретному ID может вернуть строку из любого из двух пространств идентификаторов — это прямо описано в docblock. Прямое чтение легаси вынесено в новый getLegacyList(), а вот ручной UserSignatureTable::query() по-прежнему ходит только в старую таблицу. Учитывайте, если строите свои выборки.
Сигнатуры IMAP-хелпера расширены. У Imap::syncMessages($mailboxID, $dirPath, $UIDs, $isRecovered = false, ...) появились три новых булевых параметра: $ignoreSyncFrom = true, $stopOnEmptyChunk = true, $failOnLocalError = false. При $stopOnEmptyChunk = false пустой чанк пропускается (continue) вместо обрыва всего прохода (break). syncMessage() получил bool $storeRetryState = false, bool $reuseCachedMessage = false, а removeExistingMessagesFromSynchronizationList() — bool $onlyCompleted = false.
Загрузчик цепочек переписан. У Helper\Message\MessageThreadLoader удалены loadAfterThreadMessageIds(), loadBeforeThreadMessageIds(), loadFullThreadMessageIds() — вместо них loadThreadBranchMessageIds(), selectAncestorIds(int) и selectDescendantIds(int). У MessageSearch::getMessageThread() добавились int $offset = 0, bool $withBodies = true, а дефолт лимита стал константой THREAD_PAGE_SIZE_DEFAULT.
Мелкие изменения сигнатур. Конструкторы DirSortingHelper, MailboxDirectoryHelper и метод Mailbox::getDirsHelper() получили ?int $userId = null (под персональную сортировку папок). MessageFilter::__construct — ?int $userId = null и bool $withAttachmentsStack = false. MailboxConnector::getMailboxData() — второй параметр bool $withCrmFilterState = true.
Deprecated
Bitrix\Mail\Internals\UserSignatureTable— помечен@deprecatedс честной формулировкой: «Kept for the core, CRM and mobile versions that still read signatures from this entity directly». Уберут, когда минимальная поддерживаемая версия main начнёт получать подписи через событиеOnMailSenderListBuild. Замена —\Bitrix\Mail\Service\SharedSignature\SharedSignatureService: запись идёт только туда, легаси-таблица через адаптер не пишется.Bitrix\Mail\Internals\UserSignatureAdapterResult— «Lives exactly as long as the UserSignatureTable adapter does». Редко увидишь deprecated с таким самосознанием.
Новое
Общие подписи
Подписи наконец перестали быть личной запиской пользователя: появилась область видимости (SCOPE_SHARED / SCOPE_OWNER) и назначение на пользователей, отделы, ящики и отправителей.
Ядро связки — Service\SharedSignature\SharedSignatureService (CRUD и права: canManageSharedScope, фильтры видимости). Рядом: AssignmentResolver, AssignmentTargetDirectory, SignatureResolver (подбирает подпись под отправителя), SignatureChoiceStorage (запоминает выбор пользователя) и SignatureMigrator со степпером SignatureMigrationStepper — фоновая порционная миграция из b_mail_user_signature.
Примерно так это теперь читается из кода (пример мой, не ядро):
<?php declare(strict_types=1);
use Bitrix\Main\Loader;
use Bitrix\Mail\Service\SharedSignature\SharedSignatureService;
Loader::requireModule('mail');
// статическая проверка прав ТЕКУЩЕГО пользователя (лицензия + доступ к управлению ящиками)
if (SharedSignatureService::canManageSharedScope()) {
// сервис в контейнере не регистрируется — ядро везде создаёт его напрямую
$service = new SharedSignatureService();
// CRUD общих подписей — только через сервис,
// UserSignatureTable больше не пишется напрямую
}
Контроллеров тоже два новых: Controller\SharedSignature и Controller\Signature — единый CRUD, отвечающий в старом camelCase-формате, «который клиенты уже понимают». Доступ охраняет фильтр Controller\ActionFilter\SharedSignatureAccess: без права управления ящиками (MailAccess::hasCurrentUserAccessToMailboxManagement()) — 403. На фронте — новый JS-редактор install/js/mail/signature/editor/ с тремя транспортами: user/shared/unified.
Большие вложения через Диск
Тяжёлые вложения автоматически переезжают на Диск, а в письмо вставляется публичная ссылка. Архитектура образцовая: интерфейс Integration\Disk\LargeAttachmentStorageInterface, реализации RealLargeAttachmentStorage / StubLargeAttachmentStorage, фабрика LargeAttachmentStorageFactory. Логика — в Internal\Service\LargeAttachment\LargeAttachmentService и AttachmentSizeGuard, валидация отправки — Public\Service\LargeAttachment\SendContractValidator. Контроллер Controller\LargeAttachment умеет convert / deleteUploaded / finalizeReplacement.
Фича гейтится двумя флагами: Feature::isLargeAttachmentDiskUploadAvailable() и LicenseManager::isLargeAttachmentAutoUploadEnabled().
Избранное и AI-классификация писем
Новая таблетка Internals\MailMessageMarkTable хранит метки писем: CODE_FAVORITES = 1, CODE_CLASSIFICATION_URGENT = 2, CODE_CLASSIFICATION_RISKY = 3, CODE_CLASSIFICATION_LOST = 4. Отдельная деталь: SHARED_USER_ID = 0 — метка на весь ящик, а не на конкретного пользователя.
Сервисы: Internal\Service\FavoritesService и Internal\Service\Message\ClassificationService с enum ClassificationLabel (Urgent/Risky/Lost). У Controller\Message появился экшен setFavoriteStateAction, у MessageFilter — фильтры addIsFavorite / addClassification / addUnanswered.
Классификацией может рулить и AI-ассистент — для него добавлены инструменты SetEmailClassificationTool и RemoveEmailClassificationTool в Integration\AiAssistant.
CRM IMAP-фильтр: один писатель вместо зоопарка
Helper\Mailbox\CrmImapFilter объявлен «единственным писателем строк фильтра 'crm_imap'» — а эти строки и есть фактический гейт обработки CRM-почты. Контексты записи описаны enum-ом CrmImapFilterContext.
Для починки уже разъехавшихся фильтров есть Helper::repairCrmImapFilterAgent(int $lastMailboxId = 0, int $packSize = 100) — самоперепланирующийся one-shot агент, который порционно приводит фильтры всех активных IMAP-ящиков в соответствие с флагом CrmFlag::Connect и удаляется по завершении. Регистрируется через install/migrations/agents.php.
Синк истории по датам
IMAP-синхронизация истории научилась работать периодами: Imap::syncDirHistoryByPeriod(), маркеры покрытия истории (HISTORY_SYNC_COVERAGE_PROPERTY, build/read/writeHistoryCoverageMarker, isHistorySyncRequired) и retry-механика (collectRetryUids, buildRetryExternalId, findRetryMessageId). На низком уровне появился Bitrix\Mail\Imap::getUidsSince() — обёртка над IMAP SEARCH SINCE.
Нижняя граница периода синка теперь считается в одном месте — SyncPeriodBoundary::dayStartUtcMinusDays(), единый источник для тарифа, очистки и рантайма. Включается всё флагом Feature::isHistorySyncByDateSearchEnabled().
Прочее
MailboxSettings::saveFolderCustomOrderAction()+Feature::isFolderManualSortingAvailable()— ручная персональная сортировка папок.Controller\Attachment::downloadArchiveAction(int $messageId)+ArchiveService/ListingService— скачать все вложения письма одним архивом.Integration\AI\Context\Subject— копилот генерирует тему письма; включается флагамиSettings::isMailSubjectCopilotEnabledInGlobalSettings()/isMailCrmSubjectCopilotEnabledInGlobalSettings().Helper\Message::sanitizeHtmlForMessageView()— санитайзер HTML для просмотра письма в sandbox-iframe.Integration\UI\EntitySelector\MailboxProvider— провайдер почтовых ящиков для entity-selector.Notification::cancelDeferredPushForReadMessages()и deferred-push методы вImapCommands\SyncInternalManager— отмена отложенных пушей по уже прочитанным письмам.Integration\Crm\Permissions::canReadEntity()иQuoteTrimmer::stripQuotedPlain().
БД
db.diff пуст, но это обманка: схема расширена через install/migrations/tables.php.
b_mail_shared_signature— новая таблица подписей: ID, CREATED_BY, OWNER_ID, SCOPE varchar(16) default 'shared', SIGNATURE text, DATE_CREATE/DATE_MODIFY. ПолеLEGACY_IDс уникальным индексом связывает строку с мигрированной легаси-записью.b_mail_shared_signature_assignment— назначения подписей: SIGNATURE_ID, TARGET_TYPE varchar(20), TARGET_ID, TARGET_VALUE, IS_FLAT char(1).b_mail_message_mark— метки писем с составным PK MAILBOX_ID+MESSAGE_ID+CODE+USER_ID; USER_ID=0 — метка на весь ящик.- В
b_mail_msg_attachmentдобавлена колонкаEXTERNAL_LINK_ID+ индексIX_MAIL_MSG_ATTACH_EXTLINK— связь вложения с публичной ссылкой Диска.
Мелочи и находки
StubLargeAttachmentStorageчестно признаётся в комментарии: «Fallback storage used until the disk contract (MR 7258) is merged». Фича больших вложений выпущена раньше, чем готова принимающая сторона в модуле disk — до мержа MR 7258 все операции возвращают контролируемую ошибку «диск недоступен».- В
install/migrations/agents.php— редкий для ядра комментарий, объясняющий, почему агент ищется по маске имени: самоперепланирующийся агент носит курсор в собственном имени, и точная проверка вAddAgent()его бы просто не заметила. Message::neutralizeViewportUnits()заменяет все viewport-единицы (vh, vw, vmin, svh, dvh и прочие) в стилях письма на0px, чтобы вёрстка не «взрывалась» внутри iframe. Попутно вызываетсяIni::adjustPcreBacktrackLimit(strlen($html) * 2)— на всякий пожарный.- Docblock адаптера
UserSignatureTable— целое эссе о жизни с двумя независимыми пространствами ID во время миграции: чтение по номеру «отвечает той строкой, которую встретит первой», и разрешать неоднозначность — забота вызывающего. Imap::HISTORY_SYNC_BOUNDARY_MARGIN = 172800— запас в два дня с пояснением: «one for the RFC 3501 date truncation, one for the server-side timezone interpretation». IMAP SEARCH SINCE оперирует датами без времени и в таймзоне сервера — Битрикс подстелил соломки.- Комментарий у
syncMessagesпро$stopOnEmptyChunkпредупреждает: вызывающие, передающие весь список UID за раз, обязаны ставитьfalse— иначе хвост останется несинхронизированным, а период будет помечен покрытым, и хвост больше никогда не будет получен. - PNG-картинки (в компонентах и JS-расширениях модуля) заменены на WebP: 13 webp добавлено, ни одного нового png.
Итого
Severity: notable. Три крупные пользовательские фичи, три новые таблицы — и при этом реальные риски для кастомизаций: смена параметров экшенов UserSignature, удалённые protected-методы Controller\Base и превращение UserSignatureTable::getList() в адаптер. Ядро мигрирует данные само и формат ответов контроллеров сохраняет, но если ваш код читает подписи напрямую или наследует почтовые контроллеры — проверяйте до обновления, а не после.
Устаревшие и удалённые API в этой версии
| Символ | Статус | Чем заменять |
|---|---|---|
Bitrix\Mail\Internals\UserSignatureTable
|
Deprecated |
Bitrix\Mail\Service\SharedSignature\SharedSignatureService
|
Bitrix\Mail\Internals\UserSignatureAdapterResult
|
Deprecated | — |
Bitrix\Mail\Controller\UserSignature::checkAccess()
|
Удалено | — |
Bitrix\Mail\Controller\Base::init()
|
Удалено | — |
Bitrix\Mail\Controller\Base::convertArrayKeysToCamel()
|
Удалено | — |
Bitrix\Mail\Controller\Base::toCamelCase()
|
Удалено | — |
Bitrix\Mail\Helper\Message\MessageThreadLoader::loadAfterThreadMessageIds()
|
Удалено |
Bitrix\Mail\Helper\Message\MessageThreadLoader::selectDescendantIds()
|
Bitrix\Mail\Helper\Message\MessageThreadLoader::loadBeforeThreadMessageIds()
|
Удалено |
Bitrix\Mail\Helper\Message\MessageThreadLoader::selectAncestorIds()
|
Bitrix\Mail\Helper\Message\MessageThreadLoader::loadFullThreadMessageIds()
|
Удалено |
Bitrix\Mail\Helper\Message\MessageThreadLoader::loadThreadBranchMessageIds()
|