При массовой рассылке писем (например, счетов, актов сверки или расчетных листков) стандартная схема отправки нередко работает непозволительно медленно. Метод Подключиться() объекта ИнтернетПочта выполняет установку сетевого соединения, TLS-рукопожатие и авторизацию на SMTP-сервере, что может занимать от 1 до 3 секунд на каждое отдельное письмо. При сотнях контрагентов процедура растягивается на десятки минут.
Естественным шагом к оптимизации становится вынос вызова Подключиться() за пределы цикла отправки сообщений. Однако при таком подходе разработчики сталкиваются со скрытыми проблемами: часть писем пропадает без ошибок в блоке Попытка...Исключение, или сервер внезапно разрывает связь. Разберем по шагам, почему это происходит и как правильно организовать высокоскоростную и надежную рассылку.
Рассмотрим типовую оптимизированную схему, которую часто пытаются реализовать в обработках:
Профиль = Новый ИнтернетПочтовыйПрофиль;
// Заполнение настроек профиля (АдресСервераSMTP, Порт, ПользовательSMTP, ПарольSMTP и т.д.)
Почта = Новый ИнтернетПочта;
Почта.Подключиться(Профиль);
Для Каждого Стр Из ТаблицаПолучателей Цикл
ПочтовоеСообщение = Новый ИнтернетПочтовоеСообщение;
// Наполнение письма данными и вложениями
Попытка
Почта.Послать(ПочтовоеСообщение);
Исключение
ЗаписьЖурналаРегистрации("Ошибка отправки", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Почта.Отключиться();
На практике оказывается, что часть писем не доходит до адресатов, хотя в блок Исключение код ни разу не попадает. Выясним причину такого поведения.
Начиная с версии платформы 8.3.9, метод Послать() объекта ИнтернетПочта возвращает значение типа Соответствие. Исключение генерируется лишь при критических сбоях самого протокола или канала связи (например, полный обрыв сети или сброс сокета). Если же почтовый сервер отклонил конкретного получателя (превышение квоты, подозрение на спам, несуществующий почтовый ящик), метод отработает штатно, но вернет заполненное соответствие:
Если результат вызова метода Послать() не проверяется, 1С считает отправку успешной, скрывая ошибки отклонения сообщений почтовым сервером.
Второй скрытый фактор заключается в политиках безопасности почтовых серверов (включая Microsoft Exchange, Postfix, Exim и публичные сервисы):
DATA.Послать() программа формирует печатные формы, выполняет тяжелые запросы к базе или сохраняет большие файлы на диск, пауза между SMTP-командами может превысить таймаут ожидания сервера. В результате сервер закрывает соединение по неактивности.Для стабильной работы разделим процесс на подготовку и саму отправку, а также разобьем всю пачку сообщений на порции (пакеты по 20–30 писем) с обязательной проверкой возвращаемого соответствия.
Посмотрим на пример надежной реализации:
// Шаг 1. Предварительное формирование массива сообщений
МассивСообщений = Новый Массив;
Для Каждого ДанныеПолучателя Из ТаблицаДанных Цикл
Сообщение = Новый ИнтернетПочтовоеСообщение;
Сообщение.Тема = ДанныеПолучателя.Тема;
Сообщение.Получатели.Добавить(ДанныеПолучателя.Адрес);
// Добавление текстов и вложений выполняется ДО открытия сессии SMTP
МассивСообщений.Добавить(Сообщение);
КонецЦикла;
// Шаг 2. Пакетная отправка порциями
РазмерПакета = 25;
Индекс = 0;
ВсегоПисем = МассивСообщений.Количество();
Почта = Новый ИнтернетПочта;
Пока Индекс < ВсегоПисем Цикл
Попытка
Почта.Подключиться(Профиль);
Исключение
ЗаписьЖурналаРегистрации("ОтправкаПочты.Подключение", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
Прервать;
КонецПопытки;
НомерВПакете = 0;
Пока НомерВПакете < РазмерПакета И Индекс < ВсегоПисем Цикл
ТекущееПисьмо = МассивСообщений[Индекс];
Попытка
ОшибкиОтправки = Почта.Послать(ТекущееПисьмо);
Если ТипЗнч(ОшибкиОтправки) = Тип("Соответствие") И ОшибкиОтправки.Количество() > 0 Тогда
Для Каждого ОшибкаАдреса Из ОшибкиОтправки Цикл
ЗаписьЖурналаРегистрации("ОтправкаПочты.ОтказСервера",
УровеньЖурналаРегистрации.Предупреждение,,,
СтрШаблон("Адрес: %1, Ошибка: %2", ОшибкаАдреса.Ключ, ОшибкаАдреса.Значение));
КонецЦикла;
КонецЕсли;
Исключение
ЗаписьЖурналаРегистрации("ОтправкаПочты.СбойСессии", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
Прервать; // Выходим из пакета для переподключения
КонецПопытки;
Индекс = Индекс + 1;
НомерВПакете = НомерВПакете + 1;
КонецЦикла;
Если Почта.Подключена() Тогда
Почта.Отключиться();
КонецЕсли;
КонецЦикла;
Этот подход устраняет задержки на постоянные подключения, не дает почтовому серверу сбросить сессию по лимиту количества сообщений и защищает от разрыва связи при подготовке вложений.
Если конфигурация построена на базе Библиотеки стандартных подсистем (БСП), не обязательно писать собственный механизм управления сессиями. Проанализируем возможности подсистемы РаботаСПочтовымиСообщениями.
Функция РаботаСПочтовымиСообщениями.ОтправитьПисьма() уже содержит внутри алгоритмы автоматической разбивки на порции, повторных попыток подключения при сетевых сбоях и корректного логирования ошибок отправки по каждому получателю в регистры сведений.
Если требуется отправлять тысячи писем без замедления работы пользователей, воспользуемся следующими архитектурными практиками:
ФоновыеЗадания.Выполнить(). Каждый поток создает свой экземпляр ИнтернетПочта и параллельно отправляет свой пакет.