Files
ssh-access-tui/outlook-bulk-sender
redlineoandClaude Sonnet 5 e40d971de0 Fix crash opening bulk sender window (rows null during InitializeComponent)
Real stack trace from the operator's machine pinned it down: WinForms
fires DataGridView.CellValueChanged as a side effect of
DataGridViewColumn.HeaderText assignments inside InitializeComponent(),
i.e. before PreviewForm's constructor has had a chance to run RunScan()
and populate the `rows` field. UpdateSummaryLabel() then hit
rows.Count(...) on a null source. Nothing to do with Outlook/COM at
all - a pure WinForms initialization-order gotcha.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 10:26:29 +03:00
..
2026-07-23 12:38:13 +03:00
2026-07-23 12:38:13 +03:00

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}_(ssh_access|config_update).7z"
  EmailAddressBuilder.cs           login -> "login.with.dots@domain"
  SendJob.cs                       модель строки предпросмотра (Login/Email/Kind/ArchivePath/…/Status)
  ArchiveScanner.cs                сканирование папки, группировка по логину, конфликт "оба архива сразу"
  TemplateEngine.cs                подстановка {Login}/{Email}/{Date} в шаблон
  MailTemplates.cs                 два именованных шаблона (ssh_access / config_update)
  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)" — прежнее поведение без явного выбора аккаунта.
  • Шаблоны писем — тема и HTML-тело отдельно для ssh_access и config_update, с плейсхолдерами {Login}, {Email}, {Date}.
  • Тестовый режим по умолчанию — включён по умолчанию (письма сохраняются в Черновики, а не отправляются) до первого осознанного отключения оператором в окне предпросмотра.

Журнал отправок пишется в 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.