lstTemplates was a direct child of tabTemplates with an explicit pixel Size, computed once against this dev machine's tab-strip height under its own DPI/font scale. On the operator's real machine, adding enough templates pushed rows past the control's actual bottom edge - maximizing the window didn't help, confirming it wasn't a screen-space problem but a stale size assumption baked into the designer file (the same root cause as the earlier add/remove-button visibility bug). Moved the button strip + list into their own panel (pnlTemplateList), with the buttons Dock=Top and lstTemplates Dock=Fill - the list's bounds are now computed by the layout engine against the real container at runtime, with no absolute size to be wrong about, regardless of DPI/font scale/window size. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
BulkArchiveMailer (outlook-bulk-sender)
VSTO-плагин классического Outlook (Win32/C2R, не "новый Outlook" — тот
не поддерживает VSTO и локальный доступ к файловой системе) для массовой
рассылки зашифрованных .7z-архивов, которые генерирует
ssh-access-tui для каждого пользователя 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.