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>
ArchiveKind was a hardcoded two-value enum (ssh_access/config_update),
wired through FilenameParser's regex, MailTemplateSet's two fixed
slots, and SettingsForm's two fixed tabs. Operators wanted arbitrary
custom templates (own file-name suffix, subject, body) without a code
change per new archive kind ssh-access-tui might start producing.
Replaced with MailTemplateDefinition (DisplayName/FileSuffix/Subject/
BodyHtml) in an open-ended list: FilenameParser now accepts any
"{login}_{suffix}.7z" and ArchiveScanner matches the suffix against
the configured list; SendJob.Kind/SendLogEntry.Kind became plain
strings; SettingsForm gained a template list (+/- buttons, editor
panel) replacing the two static tabs. ssh_access/config_update are now
just the two default entries in that list, not special-cased.
Handles the two backward-compat edges that matter for a tool already
in real use: old config.json's object-shaped "templates" migrates to
the new list shape transparently on first load (DataContractJsonSerializer
turned out to silently produce an empty list rather than throw on the
old shape, so detection is structural, not exception-based), and old
send-log.csv rows with PascalCase Kind values (SshAccess/ConfigUpdate)
still dedupe correctly against the new snake_case suffixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
An operator hit "Значение не может быть неопределенным. Имя параметра:
source" (ArgumentNullException, ParamName=source) opening the bulk
sender window on a real Outlook install. A full read-through of every
LINQ call reachable from that path found nothing that could receive a
null source, and this dev machine has no Outlook to reproduce against
- so the next occurrence needs a real stack trace to diagnose instead
of guessing from ex.Message alone.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
First live build against real Visual Studio/VSTO tooling surfaced two
issues in the hand-written AddIn scaffold: ThisAddIn derived from the
wrong base type (OutlookAddIn is an interface, not the concrete
OutlookAddInBase class), and VSTO Application projects cannot build
without a signed ClickOnce manifest, so SignManifests is now on with a
throwaway self-signed test certificate. Both Core/Core.Tests/AddIn now
build clean and Core.Tests passes.
Also: mail always sent from Outlook's default account with no way to
pick a different one (e.g. a shared mailbox) - added a "From account"
setting (Session.Accounts dropdown in SettingsForm, SendUsingAccount in
MailSender). And added deploy/ with both a local ClickOnce publish
script (real install/uninstall via Windows Apps, no manual registry
editing) and a manual registry sideload fallback for scripted rollouts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>