The add/remove template buttons sat directly below the fixed-height list with only 6px of margin to the tab's bottom edge - enough on this dev machine but not on the operator's real one (different DPI/font scale), so the buttons rendered off the visible area. Moved them above the list instead (visibility no longer depends on how much space is left at the bottom) and gave the whole "Шаблоны" tab substantially more vertical room throughout. Also added an optional per-template login regex filter (MailTemplateDefinition.LoginPattern): once a file's suffix matches a template, its login must also match that template's pattern (if any) or the row surfaces as a clear Error in the preview instead of silently going through - addresses "непонятно что будет работать". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
149 lines
14 KiB
Markdown
149 lines
14 KiB
Markdown
# BulkArchiveMailer (outlook-bulk-sender)
|
||
|
||
VSTO-плагин классического Outlook (Win32/C2R, **не** "новый Outlook" — тот
|
||
не поддерживает VSTO и локальный доступ к файловой системе) для массовой
|
||
рассылки зашифрованных `.7z`-архивов, которые генерирует
|
||
[`ssh-access-tui`](../README.md) для каждого пользователя jump-хоста.
|
||
|
||
## Как это связано с ssh-access-tui
|
||
|
||
`ssh-access-tui` (см. `internal/keymgr/pack.go` и
|
||
`internal/keymgr/configonly.go` в корне репозитория) складывает в общий
|
||
выходной каталог по одному архиву на пользователя:
|
||
|
||
- `{login}_ssh_access.7z` — новая пара ключей + конфиг + инструкция (выдаётся
|
||
при первом доступе или полной переиздаче ключа).
|
||
- `{login}_config_update.7z` — только обновлённый конфиг + инструкция, без
|
||
ключа (выдаётся, когда у пользователя поменялся список разрешённых
|
||
сервисов, а сам ключ остался прежним).
|
||
|
||
`ssh-access-tui` только *генерирует* эти архивы — раздача их получателям по
|
||
почте раньше делалась вручную. BulkArchiveMailer автоматизирует именно эту
|
||
последнюю часть: сканирует папку, по имени файла восстанавливает логин и
|
||
адрес получателя, подставляет письмо по шаблону, показывает предпросмотр со
|
||
статусами и рассылает.
|
||
|
||
BulkArchiveMailer **не порождает архивы и не знает пароль от них** — пароль
|
||
общий на партию экспорта и сообщается получателям отдельно, вне этого
|
||
инструмента (см. баннер-напоминание в окне предпросмотра). Инструмент только
|
||
прикладывает уже готовый файл архива к письму.
|
||
|
||
## Как логин превращается в адрес
|
||
|
||
Правило простое и единственное: в логине из имени файла (например,
|
||
`a-makarov`) каждый дефис заменяется на точку, и добавляется домен —
|
||
получаем `a.makarov@<домен>`. Отдельной таблицы соответствий нет и не
|
||
планируется.
|
||
|
||
**Известное ограничение:** если у реального логина дефис не является
|
||
разделителем инициала/фамилии (то есть входит в исходное системное имя как
|
||
есть, а не как искусственный разделитель), адрес получится неправильным.
|
||
Механизм переопределения (per-user override) сознательно не реализован —
|
||
такие логины считаются исключением, которое разбирается вручную (переслать
|
||
письмо оператором с указанием правильного адреса).
|
||
|
||
## Структура решения
|
||
|
||
```
|
||
BulkArchiveMailer.sln
|
||
BulkArchiveMailer.Core/ net48, class library, БЕЗ ссылок на Outlook/COM/реестр — можно
|
||
собрать и протестировать без Windows/Office.
|
||
FilenameParser.cs парсинг "{login}_{суффикс}.7z" (суффикс - любой, сверяется с шаблонами в ArchiveScanner)
|
||
EmailAddressBuilder.cs login -> "login.with.dots@domain"
|
||
SendJob.cs модель строки предпросмотра (Login/Email/Kind/ArchivePath/…/Status)
|
||
ArchiveScanner.cs сканирование папки, сопоставление суффикса (и необязательного regex-фильтра логина) с шаблоном, конфликт "оба архива сразу"
|
||
TemplateEngine.cs подстановка {Login}/{Email}/{Date} в шаблон
|
||
MailTemplateDefinition.cs один шаблон (название, суффикс файла, тема, HTML-тело) - список произвольной длины
|
||
SendLogStore.cs CSV-журнал отправок + дедуп по (Email, Kind, SHA-256 архива)
|
||
AppSettings.cs %APPDATA%\BulkArchiveMailer\config.json, засев из config.default.json, миграция старого формата шаблонов
|
||
config.default.json шаблоны писем (список) и домен по умолчанию (встроен как Embedded Resource)
|
||
BulkArchiveMailer.Core.Tests/ xUnit-заготовка (один smoke-тест) — полное покрытие добавляется отдельно
|
||
BulkArchiveMailer.AddIn/ VSTO-плагин, net48, Windows-only, ссылается на Core
|
||
ThisAddIn.cs / .Designer.cs запуск/остановка плагина
|
||
Ribbon.xml + RibbonController.cs одна кнопка ленты "Массовая рассылка"
|
||
SignatureReader.cs чтение подписи Outlook из %APPDATA%\Microsoft\Signatures через реестр
|
||
MailSender.cs создание/отправка Outlook.MailItem, тестовый режим (Черновики)
|
||
UI/SettingsForm.cs папка, домен, подпись, тестовый режим по умолчанию, список шаблонов (добавление/удаление)
|
||
UI/PreviewForm.cs таблица предпросмотра, баннер про пароль, отправка, итоговый журнал
|
||
docs/BUILD.md как собрать и подключить плагин в Visual Studio на Windows
|
||
```
|
||
|
||
Почему граница именно такая: всё, что в принципе можно проверить без
|
||
Windows/Office (парсинг имён, вывод email, шаблоны, дедуп-журнал,
|
||
JSON-настройки), живёт в `BulkArchiveMailer.Core` и покрывается обычными
|
||
xUnit-тестами. Всё, что обязано трогать Outlook (COM-объекты писем, реестр
|
||
для подписи, лента) — в `BulkArchiveMailer.AddIn`, который без установленного
|
||
Outlook в принципе не запустить.
|
||
|
||
## Настройка
|
||
|
||
При первом запуске плагин создаёт `%APPDATA%\BulkArchiveMailer\config.json`
|
||
из встроенного `config.default.json`. Дальше всё редактируется через окно
|
||
"Настройки" (открывается кнопкой ленты, если папка с архивами ещё не
|
||
настроена или не существует):
|
||
|
||
- **Папка с архивами** — тот самый общий выходной каталог `ssh-access-tui`.
|
||
- **Домен электронной почты** — по умолчанию `redlab.lo` (реальный домен
|
||
организации), обычное редактируемое поле, а не зашитая логика.
|
||
- **Файл подписи (.htm)** — можно оставить пустым (тогда при каждой рассылке
|
||
подпись определяется автоматически по умолчанию Outlook для новых писем)
|
||
либо указать конкретный файл вручную — например, если у оператора настроена
|
||
не та подпись, которую нужно вставлять в такие письма.
|
||
- **Отправлять от** — выпадающий список аккаунтов текущей сессии Outlook
|
||
(`Session.Accounts`), полезно, если у оператора несколько ящиков (например,
|
||
отдельный служебный/shared-ящик именно для этой рассылки). Пункт
|
||
"(по умолчанию Outlook)" — прежнее поведение без явного выбора аккаунта.
|
||
- **Шаблоны писем** — вкладка "Шаблоны" со списком произвольного числа
|
||
шаблонов (кнопки "+ Добавить"/"Удалить"). Каждый шаблон — это название,
|
||
суффикс имени файла (`{login}_<суффикс>.7z`, по которому шаблон подбирается
|
||
для конкретного архива), необязательный фильтр логина (regex) и тема с
|
||
HTML-телом с плейсхолдерами `{Login}`, `{Email}`, `{Date}`. Суффикс
|
||
обязателен и должен быть уникальным среди всех шаблонов — по нему, а не по
|
||
фиксированному перечислению видов архива, `ArchiveScanner` сопоставляет
|
||
файл с шаблоном. Два исходных шаблона (`ssh_access`/`config_update`) —
|
||
обычные равноправные записи в этом списке, без особой защиты от
|
||
переименования/удаления; кнопка "Сбросить шаблоны по умолчанию"
|
||
восстанавливает их, если они были случайно удалены.
|
||
- **Фильтр логина** — необязательное регулярное выражение (.NET regex,
|
||
без учёта регистра). Пустое значение (по умолчанию, в том числе у
|
||
шаблонов, мигрированных из конфига старого формата) означает "любой
|
||
логин" — прежнее поведение, когда только суффикс решает, подходит ли
|
||
файл под шаблон. Если поле заполнено, а логин из имени файла ему не
|
||
соответствует, файл не считается подходящим под этот шаблон: строка в
|
||
предпросмотре получает статус "Ошибка" с пояснением, какому шаблону и
|
||
какому паттерну логин не соответствует, — автоматический подбор другого
|
||
шаблона не выполняется. Некорректный regex (не компилируется) не даёт
|
||
сохранить настройки — окно "Настройки" укажет, в каком именно шаблоне
|
||
ошибка.
|
||
- **Тестовый режим по умолчанию** — включён по умолчанию (письма сохраняются
|
||
в Черновики, а не отправляются) до первого осознанного отключения
|
||
оператором в окне предпросмотра.
|
||
|
||
Журнал отправок пишется в `send-log.csv` прямо в папке с архивами
|
||
(`TimestampUtc,Login,Email,Kind,ArchivePath,FileSha256,Status,Error`). Строка
|
||
считается "уже отправлено" (`AlreadySent` в предпросмотре), только если для
|
||
той же пары (email, вид архива) в журнале есть строка `Status=Sent` с тем же
|
||
SHA-256 файла — то есть если архив перегенерировали (новый ключ или новый
|
||
список сервисов), это будет новый хэш, и строка снова станет `Ready`.
|
||
|
||
## Поведение, которое нельзя менять не глядя
|
||
|
||
- Файл с "неправильным" именем не блокирует остальную рассылку — он просто
|
||
попадает в список нераспознанных со статусом "Пропущено".
|
||
- Если для одного логина одновременно лежат оба вида архива — это ошибка
|
||
данных выше по конвейеру (в норме такого не должно случаться), и строка
|
||
помечается как `Error`, заблокирована для отправки, без автоматического
|
||
выбора одного из двух файлов.
|
||
- Одна неудачная отправка не прерывает пачку — журналируется, и цикл идёт
|
||
дальше.
|
||
- Пароль от архива этот инструмент никогда не формирует, не хранит и не
|
||
отправляет — только статичное напоминание в интерфейсе, что пароль нужно
|
||
сообщить получателю отдельно.
|
||
|
||
## Статус сборки
|
||
|
||
**Ничего из этого решения не скомпилировано и не запущено** — код написан в
|
||
среде без Windows, Visual Studio, Office и .NET SDK. Первую сборку и
|
||
проверку нужно сделать на машине с Windows + Visual Studio (workload
|
||
"Office/SharePoint development") — см. [docs/BUILD.md](docs/BUILD.md).
|