Локальное тестирование сообщений и почты
- Назначение
- Как это устроено
- Шаг 1. Почтовый сервер в докере
- Шаг 2. Папки в ящике
- Шаг 3. Тип сообщения в конфигурации
- Шаг 4. Отправка письма из ERP
- Шаг 5. Приём письма от клиента
- Шаг 6. Письмо без маркера
- Вариант без докера: свой ящик на mail.ufanet.ru
- Как письмо находит свой процесс
- Частые грабли
- Уборка
Назначение
Подсистема сообщений — это переписка, привязанная к процессу: письма к клиенту и от клиента, звонки, заметки. Проверять правки в ней на продовых ящиках нельзя, а без ящика вообще ничего не проверишь: и приём, и отправка идут через настоящий почтовый сервер.
Здесь описано, как за десять минут поднять свой почтовый сервер в докере, подключить его к локальной ERP и прогнать оба направления — отправку письма и приём ответа от клиента. Ни одного реального ящика и ни одного реального пароля при этом не потребуется.
Если нужен именно настоящий почтовый сервер компании — см. раздел Вариант без докера: свой ящик на mail.ufanet.ru. Начните с чтения предупреждений в нём: локальная ERP умеет достучаться до продовых ящиков, и одно неверное нажатие удаляет письмо из ящика живого сотрудника.
Инструкция рассчитана на то, что вы уже умеете запускать ERP локально и заходить в неё браузером.
Как это устроено
Три вещи, которые надо понять до первой команды, иначе настройка кажется магией.
Тип сообщения — это почтовый ящик. В конфигурации ERP каждый ящик описан блоком
messageType.<id>.*: класс-обработчик, адрес, параметры IMAP и SMTP. Для писем класс всегда
ru.bgcrm.dao.message.MessageTypeEmail. Один блок — один ящик, и <id> этого блока лежит потом
в каждой строке таблицы n_message в поле type_id.
Приём и отправка — разные протоколы, но одна задача планировщика. Класс
ru.bgcrm.worker.MessageExchange по расписанию обходит все настроенные типы и у каждого вызывает
process(), а тот делает два дела подряд: readBox() забирает новые письма по IMAP и
sendMessages() отправляет накопившиеся исходящие по SMTP. Без планировщика письма из ERP
никуда не уйдут — они просто лежат в базе.
Исходящее письмо — это строка в n_message с пустым to_dt. Когда вы жмёте «Отправить» в
карточке процесса, письмо только записывается в базу: direction=2, to_dt IS NULL. Реально его
отправит следующий проход MessageExchange, и он же проставит to_dt. Поэтому «письмо не
отправилось» чаще всего означает «обмен для этого типа не настроен», а не ошибку в коде.
| Что | Где |
|---|---|
Строки сообщений |
таблица |
Разбор письма, ошибки IMAP/SMTP |
|
Настройки ящиков |
активная глобальная конфигурация, блоки |
Расписание обмена |
та же конфигурация, блоки |
Шаг 1. Почтовый сервер в докере
Берём GreenMail — SMTP и IMAP в одном контейнере, без регистрации и смс.
docker run -d --name greenmail-erp -p 3025:3025 -p 3143:3143 \
-e GREENMAIL_OPTS="-Dgreenmail.setup.test.smtp -Dgreenmail.setup.test.imap \
-Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose" \
greenmail/standalone:2.1.0
Порты: 3025 — SMTP, 3143 — IMAP. Проверка авторизации выключена, поэтому логин и пароль подойдут любые, а ящик заводится сам при первом обращении.
| Контейнер поднимает ещё и свой веб-API на 8080 внутри себя, но наружу этот порт мы не публикуем. |
Шаг 2. Папки в ящике
ERP не создаёт папки самостоятельно. Если папки для обработанных или пропущенных писем нет, MessageTypeEmail
молча выходит из разбора: ни ошибки, ни письма. Это самая частая причина «ничего не работает»,
поэтому папки создаём сразу.
import imaplib
M = imaplib.IMAP4('127.0.0.1', 3143)
M.login('erp@localhost', 'test')
for f in ('CRM_PROCESSED', 'CRM_SKIPPED', 'CRM_SENT'):
print(f, M.create(f))
M.logout()
Ожидаемый ответ на каждую папку — ('OK', [b’CREATE completed.']).
Шаг 3. Тип сообщения в конфигурации
Открываем в ERP Администрирование → Конфигурация, редактируем активную конфигурацию («Основной») и дописываем блок в конец.
messageType.900.title=GreenMail (локальный тест)
messageType.900.class=ru.bgcrm.dao.message.MessageTypeEmail
messageType.900.email=erp@localhost
messageType.900.host=localhost
messageType.900.port=3143
messageType.900.login=erp@localhost
messageType.900.pswd=test
messageType.900.folderIn=INBOX
messageType.900.folderProcessed=CRM_PROCESSED
messageType.900.folderSkipped=CRM_SKIPPED
messageType.900.folderSent=CRM_SENT
messageType.900.mail.transport.protocol=smtp
messageType.900.mail.smtp.host=localhost
messageType.900.mail.smtp.port=3025
Не берите маленький <id>. В локальную конфигурацию подключается боевой конфиг ящиков
(include.20), где заняты номера с 1 по 53. Если написать messageType.1.*, ваш блок
перекроется настоящим crm@ufanet.ru, и вы будете отлаживать чужой ящик. Берите номер из третьей
сотни — 900 свободен.
|
Комментарий к сохранению обязателен, он попадёт в историю конфигураций. Перезапускать приложение не нужно: конфигурация перечитывается сразу.
Проверяем, что тип виден. В браузере на любой странице ERP откройте консоль и выполните:
await (await fetch('/user/message.do?action=messageTypes&types=EMAIL&responseType=json',
{method:'POST'})).json();
В ответе, в data.list, должен быть ваш тип с id: 900.
Шаг 4. Отправка письма из ERP
Чтобы исходящие реально уходили, нужна задача обмена. Дописываем в ту же конфигурацию, что и тип сообщения — в активную:
scheduler.task.900.class=ru.bgcrm.worker.MessageExchange
scheduler.task.900.messageTypeIds=900
Параметр messageTypeIds ограничивает обмен вашим ящиком. Без него задача полезет во все типы из
боевого конфига, то есть в реальные ящики Уфанета — так делать нельзя. Полей с расписанием
(hours, minutes) не ставим: пустое расписание означает «каждую минуту», а планировщик просыпается
раз в 60 секунд.
Почему все конфигурации с планировщиком «неактивны»
Открыв список конфигураций, легко решить, что планировщик вообще не работает: у конфигураций «Планировщик 3.0» и «Планировщик 3.0 [Активный]» галка Активный снята, и на проде тоже. Это не поломка, просто слово выбрано неудачно.
«Активный» здесь означает «корневой», а не «включённый». При старте приложение берёт
одну конфигурацию с active=1 (ConfigDAO.getActiveGlobalConfig(), WHERE active=true),
а все остальные подмешивает к ней строками include.<id>=1. Поэтому у всех неглавных конфигураций
галка снята — иначе система не поняла бы, с какой начинать. Слово «[Активный]» в названии
конфигурации 30 дописано руками, к флагу отношения не имеет.
Строки include.<id>=1 берутся из двух мест сразу:
-
из самой корневой конфигурации в базе;
-
из файла
bgcrm_<логин>.propertiesв корне проекта (на сервере —bgcrm.propertiesрядом с инстансом). Настройки файла и базы сливаются в одну общую мапу.
Так это устроено в продакшене: конфигурации с задачами планировщика нет в списке include корневой
конфигурации — она подключается из файла инстанса на erp0 двумя строками:
scheduler.start=1
include.30=1
На erp2 в том же файле стоит scheduler.start=0. Поэтому почту разбирает только erp0, хотя
конфигурация с задачами одна на всех.
Локально в bgcrm_<логин>.properties обе строки закомментированы. При этом сам планировщик у вас
работает: у параметра scheduler.start значение по умолчанию — true, поток стартует и раз в
минуту просыпается. Просто задач у него нет, потому что ни одна конфигурация с scheduler.task.*
не подключена.
Отсюда и рецепт этой инструкции: кладите задачу в активную конфигурацию, тогда вопрос с
include не возникает вовсе.
Не подключайте локально include.30. Это 54 продовые задачи разом, включая обмен по 51
реальному ящику Уфанета и рассылки клиентам.
|
Что задача доехала, видно по трём строкам в app/log/all.log:
[Scheduler] Class: ru.bgcrm.worker.MessageExchange; dm: []; dw: []; hours: []; minutes: []
[Scheduler] Running scheduled task: ru.bgcrm.worker.MessageExchange@5f2040b2
[MessageExchange] Message types: [900]
Первая печатается при каждом перечитывании конфигурации и означает «задача найдена и разобрана»;
пустые hours/minutes — то самое «каждую минуту». Вторая — что задача запущена. Третья — что она
ограничена вашим ящиком. Нет первой строки — задача лежит не в той конфигурации.
Теперь письмо. Заведите процесс типа, у которого включена вкладка сообщений
(processShowMessages=1 в конфигурации типа), откройте вкладку Сообщения, нажмите Создать,
выберите тип «GreenMail (локальный тест)», заполните получателя и текст, сохраните.
Через минуту письмо должно оказаться в GreenMail:
Письмо придёт не совсем таким, каким вы его написали, — ERP дописывает три вещи:
-
маркер процесса в теме:
Исходящее из ERPпревращается вИсходящее из ERP [erp@localhost#122], где 122 — код процесса. По нему потом опознаётся ответ клиента, поэтому тему в ответе менять нельзя; -
подпись в конце текста («Сообщение подготовлено системой Avantys ERP…»), её можно переопределить параметрами
signExpressionиsignFooterтипа сообщения; -
вложение
History.txt— вся переписка по процессу одним файлом.
Копия отправленного письма при этом складывается в папку из folderSent.
Как понять, что обмен вообще запустился: в app/log/all.log за одну минуту появляются три строки —
Reload tasks config, Running scheduled task: ru.bgcrm.worker.MessageExchange и
Starting EMail daemon, box: erp@localhost. Строка про ящик должна быть одна. Если их
десятки и там чужие адреса — вы забыли messageTypeIds, и стенд полез в продовые ящики.
Что смотреть, если письма нет:
-
в
n_messageу строки осталсяto_dt IS NULL— обмен не отработал: проверьте, что задача планировщика добавлена в активную конфигурацию и что в логе есть строкаStarting EMail daemon, box: erp@localhost; -
в логе есть ошибка SMTP — проверьте порт 3025 и что контейнер жив (
docker ps); -
to_dtпроставился, а письма в GreenMail нет — смотрите адрес получателя, письмо ушло в другой ящик.
| Перезапускать приложение после добавления задачи не нужно. Планировщик просыпается раз в минуту и сам перечитывает список задач — правка конфигурации подхватывается на ближайшем проходе. |
Шаг 5. Приём письма от клиента
Отправляем «ответ клиента» — обычным письмом на адрес ящика, с маркером процесса в теме.
Дальше есть два пути.
По расписанию. Задача обмена из шага 4 через минуту сама заберёт письмо, привяжет его к
процессу и переложит в CRM_PROCESSED.
Вручную, не дожидаясь. Удобно, когда отлаживаешь и не хочешь ждать. Действие
newMessageLoad выполняет тот же разбор, что и планировщик, но прямо сейчас. В messageId
подставьте Message-ID из вывода скрипта выше, вместе с угловыми скобками:
await fetch('/user/message.do?action=newMessageLoad&typeId=900&messageId='
+ encodeURIComponent('<178716204369.24189.12026963957311248591@example.org>')
+ '&responseType=json', {method:'POST'});
Проверяем результат в бд:
SELECT id, type_id, direction, process_id, subject, from_dt, processed
FROM n_message ORDER BY id DESC LIMIT 5;
У входящего письма должно быть direction=1, processed=1 и process_id вашего процесса.
В карточке процесса письмо появится на вкладке Сообщения.
Шаг 6. Письмо без маркера
Отправьте в ящик письмо с обычной темой, без [erp@localhost#…] — так выглядит клиент, который
написал новое письмо вместо ответа.
Такое письмо в процесс не попадёт: оно останется во входящих и появится в оснастке Сообщения в главном меню, в очереди неразобранного. Откройте его там, найдите контрагента режимом поиска и привяжите к процессу руками — это и есть работа оператора первой линии.
Прогнать этот сценарий стоит обязательно: «маркер потерялся» — вторая по частоте причина вопросов «почему письмо не дошло до процесса». Почему так устроено, разобрано ниже, в разделе Как письмо находит свой процесс.
Вариант без докера: свой ящик на mail.ufanet.ru
Докер нужен не всегда. Почтовый сервер компании доступен с рабочего пк, и стенд можно собрать на нём — тогда проверяется только та инфраструктура, что работает на проде.
| Хост | Порт | Что это |
|---|---|---|
|
143 |
IMAP, приём |
|
25, 587 |
SMTP, отправка |
|
25 |
внутренний релей, с пк недоступен |
В боевой конфигурации в качестве SMTP указан mail.core.ufanet.ru — он виден только с серверов.
Локально в блоке своего типа пишите mail.smtp.host=mail.ufanet.ru, иначе исходящие будут
копиться в базе, а в логе будет таймаут.
Так делать нельзя
В локальную конфигурацию подключается боевой конфиг ящиков (include.20): 52 настоящих ящика
Уфанета с рабочими паролями, и mail.ufanet.ru с пк открывается. То есть локальная ERP
прямо сейчас может работать с живой корпоративной почтой. Отсюда правило: готовые типы 1–53
не трогаем.
Опасен здесь не просмотр, а привязка письма к процессу. Когда вы открываете письмо из оснастки
и привязываете его, MessageTypeEmail.processMessage копирует письмо в папку обработанных,
ставит на оригинал флаг DELETED и делает expunge — письмо исчезает из ящика живого
сотрудника, и вернуть его нельзя. Сам список писем в оснастке безопасен: папка открывается
только на чтение.
Так можно: свой ящик и отдельная папка
Идея простая — дать ERP не весь ящик, а одну служебную папку, куда вы сами кладёте тестовые письма. Тогда рабочая переписка недосягаема для стенда даже при ошибке в конфигурации.
-
В своём почтовом клиенте создайте три папки:
ERP_TEST,ERP_TEST_PROCESSED,ERP_TEST_SKIPPED. -
Добавьте в активную конфигурацию блок со своим адресом и свободным номером:
messageType.900.title=Мой ящик (локальный тест) messageType.900.class=ru.bgcrm.dao.message.MessageTypeEmail messageType.900.email=ivanov_i@ufanet.ru messageType.900.host=mail.ufanet.ru messageType.900.login=ivanov_i messageType.900.pswd=<пароль от своей почты> messageType.900.folderIn=ERP_TEST messageType.900.folderProcessed=ERP_TEST_PROCESSED messageType.900.folderSkipped=ERP_TEST_SKIPPED messageType.900.folderSent=ERP_TEST_PROCESSED messageType.900.mail.transport.protocol=smtp messageType.900.mail.smtp.host=mail.ufanet.ru messageType.900.mail.smtp.user=ivanov_i messageType.900.mail.smtp.pswd=<пароль от своей почты> -
Задачу обмена ставьте так же, как в шаге 4, обязательно с
messageTypeIds=900.
Что выбрать
| GreenMail | Свой ящик | |
|---|---|---|
Настройка |
одна команда докера |
папки в почтовом клиенте + пароль в конфиге |
Пароли |
не нужны |
свой рабочий, открытым текстом |
Отправка |
всегда работает |
работает, письмо реально уходит |
Риск задеть чужую почту |
нет |
есть, если ошибиться в |
Когда брать |
обычная разработка и отладка |
проверка настоящих IMAP/SMTP, кодировок, вложений |
По умолчанию берите GreenMail. Свой ящик нужен, когда проверяете именно поведение настоящего почтового сервера: кодировки тем, вложения, автоответы, разбор писем от реальных клиентов.
Как письмо находит свой процесс
Всё держится на одной строке в теме письма и одном регулярном выражении.
Метка ставится при отправке
Отправляя письмо из карточки процесса, ERP смотрит на тему и, если метки там ещё нет, дописывает её в конец:
Исходящее из ERP → Исходящее из ERP [erp@localhost#122]
^^^^^^^^^^^^^ ^^^
адрес ящика код процесса
Дальше вся надежда на то, что клиент нажмёт «Ответить», а его почтовый клиент сохранит тему,
добавив к ней Re:.
Метка читается при приёме
У каждого типа сообщения при создании компилируется своя регулярка — из его собственного адреса:
pattern = Pattern.compile(mailConfig.getEmail().replaceAll("\\.", "\\\\.") + "#(\\d+)");
Для ящика erp@localhost это erp@localhost#(\d+). Обмен перебирает письма в папке и на каждом
вызывает getProcessId(subject) — обычный find() по теме, из группы захвата берётся число.
Квадратные скобки в выражение не входят, они нужны только для читаемости.
Нашлось число — работает processMessage:
-
достаёт процесс по этому коду из базы (нет такого — ошибка в лог, письмо в папку пропущенных);
-
пишет строку в
n_message, гдеprocess_id= найденное число,direction=1,processed=1, автор — системный пользователь; -
перекладывает письмо в папку обработанных;
-
поднимает событие
ProcessMessageAddedEvent.
Типа процесса в маршрутизации нет вообще
Это главное, что стоит уложить в голове. Письмо адресуется конкретному процессу по его коду, а
не типу. Связки «ящик ul@ufanet.ru → тип процесса 9384» в системе не существует. Тип узнаётся
уже потом, из самого процесса, и нужен только для одного — понять, какой скрипт-обработчик дёрнуть
при событии.
Так что на вопрос «как письмо попадает к нужному типу процесса» ответ такой: оно к нему и не попадает. Оно попадает к процессу, а тип у процесса уже есть.
Если метки нет
Система не знает ничего и в процесс письмо не кладёт. Оно остаётся во входящих и всплывает в
оснастке Сообщения в главном меню — очереди неразобранного. Там его открывает сотрудник, ищет
контрагента или договор режимами поиска (messageType.<id>.search.N.class — по email, по телефону,
по номеру договора) и привязывает руками: либо к существующему процессу по коду, либо к новому,
который тут же создаёт.
Вот здесь и появляется тип процесса — сотрудник выбирает его сам. Это единственное место во всей цепочке, где тип вообще фигурирует как решение.
Есть и третий путь: если у ящика стоит processMessageWithEvent=true, на письмо без метки
поднимается событие MessageReceivedEvent, и разбирает его прикладной код. На проде так настроен
один ящик, bgcrm@ufanet.ru.
Где это ломается
Механика на одной строке в теме хрупкая и ломается предсказуемо. Прежде чем искать баг, проверьте эти четыре случая — скорее всего вы в одном из них.
- Ответ ушёл в другой ящик
-
Каждый тип узнаёт только собственный адрес. Письмо отправили с
ul@ufanet.ru, клиент ответил наzapros@ufanet.ru— регулярка второго ящика метку первого не увидит. - Клиент написал новым письмом, а не ответом
-
Метки нет — процесса нет, письмо уходит в неразобранное.
- Тему переписали
-
Некоторые почтовые клиенты и корпоративные шлюзы режут или переоформляют тему, метка исчезает вместе с ней. Поэтому в стандартной подписи ERP есть просьба тему не менять.
- Процесс удалили
-
Код в теме есть, процесса по нему нет — письмо уезжает в папку пропущенных с ошибкой в логе.
Заголовки In-Reply-To и References, по которым почтовые системы обычно связывают
переписку, не используются вообще.
|
Частые грабли
- Ящик настроен, но писем нет
-
Не созданы папки из шага 2.
MessageTypeEmailвыходит без ошибки, если нетfolderProcessedилиfolderSkipped. - Тип сообщения в списке есть, но с чужим названием
-
Занятый
<id>: боевой конфиг ящиков перекрыл ваш блок. Возьмите номер из третьей сотни. - Письмо лежит в базе и не уходит
-
Нет задачи
MessageExchangeили она ограничена другимиmessageTypeIds. Проверьте по логу:Starting EMail daemon, box: …. - Добавил
scheduler.task, но ничего не запускается -
Задача попала в неактивную конфигурацию. Кладите её в ту, у которой стоит галка Активный, — остальные подключаются только через
include.<id>=1. Подробно — в разделе Почему все конфигурации с планировщиком «неактивны». - Все конфигурации с планировщиком неактивны
-
Так и задумано: «Активный» означает «корневой», а не «включённый». Конфигурация с задачами подключается к корневой строкой
include.<id>=1из файлаbgcrm.propertiesинстанса. Там же живётscheduler.start, которым планировщик включён на erp0 и выключен на erp2. - Ответ клиента не привязался к процессу
-
В теме нет маркера или он от другого ящика. Тип узнаёт только собственный адрес.
- Правки в коде не подхватились
-
Изменения в Kotlin/Java требуют перезапуска приложения, изменения конфигурации — нет.
Уборка
docker rm -f greenmail-erp
И уберите из конфигурации блоки messageType.900. и scheduler.task.900. — иначе при каждом
запуске ERP будет минуту за минутой стучаться в несуществующий почтовый сервер и писать ошибки в лог.
| Никогда не прописывайте в локальную конфигурацию реальный ящик Уфанета. Локальный инстанс с продовым ящиком начнёт вычитывать из него письма и перекладывать их в служебные папки — то есть уводить рабочую почту у живых сотрудников. |