Generalize mail templates from a fixed pair to an operator-managed list

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>
This commit is contained in:
redlineoandClaude Sonnet 5 committed 2026-07-24 11:26:31 +03:00
1 parent cb3b273741
commit 804b9610ca
16 files changed
+904 -288

No files matched your search

@@ -16,8 +16,7 @@ namespace BulkArchiveMailer.AddIn.UI
private System.Windows.Forms.TabControl tabControl;
private System.Windows.Forms.TabPage tabGeneral;
private System.Windows.Forms.TabPage tabSshAccess;
private System.Windows.Forms.TabPage tabConfigUpdate;
private System.Windows.Forms.TabPage tabTemplates;
private System.Windows.Forms.Label lblArchiveFolder;
private System.Windows.Forms.TextBox txtArchiveFolder;
@@ -36,17 +35,20 @@ namespace BulkArchiveMailer.AddIn.UI
private System.Windows.Forms.Label lblFromAccount;
private System.Windows.Forms.ComboBox cboFromAccount;
private System.Windows.Forms.Label lblSshSubject;
private System.Windows.Forms.TextBox txtSshSubject;
private System.Windows.Forms.Label lblSshBody;
private System.Windows.Forms.TextBox txtSshBody;
private System.Windows.Forms.Label lblSshHint;
private System.Windows.Forms.ListBox lstTemplates;
private System.Windows.Forms.Button btnAddTemplate;
private System.Windows.Forms.Button btnRemoveTemplate;
private System.Windows.Forms.Label lblCfgSubject;
private System.Windows.Forms.TextBox txtCfgSubject;
private System.Windows.Forms.Label lblCfgBody;
private System.Windows.Forms.TextBox txtCfgBody;
private System.Windows.Forms.Label lblCfgHint;
private System.Windows.Forms.Label lblTplDisplayName;
private System.Windows.Forms.TextBox txtTplDisplayName;
private System.Windows.Forms.Label lblTplFileSuffix;
private System.Windows.Forms.TextBox txtTplFileSuffix;
private System.Windows.Forms.Label lblTplFileSuffixHint;
private System.Windows.Forms.Label lblTplSubject;
private System.Windows.Forms.TextBox txtTplSubject;
private System.Windows.Forms.Label lblTplBody;
private System.Windows.Forms.TextBox txtTplBody;
private System.Windows.Forms.Label lblTplHint;
private System.Windows.Forms.Button btnSave;
private System.Windows.Forms.Button btnCancel;
@@ -56,8 +58,7 @@ namespace BulkArchiveMailer.AddIn.UI
{
this.tabControl = new System.Windows.Forms.TabControl();
this.tabGeneral = new System.Windows.Forms.TabPage();
this.tabSshAccess = new System.Windows.Forms.TabPage();
this.tabConfigUpdate = new System.Windows.Forms.TabPage();
this.tabTemplates = new System.Windows.Forms.TabPage();
this.lblArchiveFolder = new System.Windows.Forms.Label();
this.txtArchiveFolder = new System.Windows.Forms.TextBox();
@@ -76,17 +77,20 @@ namespace BulkArchiveMailer.AddIn.UI
this.lblFromAccount = new System.Windows.Forms.Label();
this.cboFromAccount = new System.Windows.Forms.ComboBox();
this.lblSshSubject = new System.Windows.Forms.Label();
this.txtSshSubject = new System.Windows.Forms.TextBox();
this.lblSshBody = new System.Windows.Forms.Label();
this.txtSshBody = new System.Windows.Forms.TextBox();
this.lblSshHint = new System.Windows.Forms.Label();
this.lstTemplates = new System.Windows.Forms.ListBox();
this.btnAddTemplate = new System.Windows.Forms.Button();
this.btnRemoveTemplate = new System.Windows.Forms.Button();
this.lblCfgSubject = new System.Windows.Forms.Label();
this.txtCfgSubject = new System.Windows.Forms.TextBox();
this.lblCfgBody = new System.Windows.Forms.Label();
this.txtCfgBody = new System.Windows.Forms.TextBox();
this.lblCfgHint = new System.Windows.Forms.Label();
this.lblTplDisplayName = new System.Windows.Forms.Label();
this.txtTplDisplayName = new System.Windows.Forms.TextBox();
this.lblTplFileSuffix = new System.Windows.Forms.Label();
this.txtTplFileSuffix = new System.Windows.Forms.TextBox();
this.lblTplFileSuffixHint = new System.Windows.Forms.Label();
this.lblTplSubject = new System.Windows.Forms.Label();
this.txtTplSubject = new System.Windows.Forms.TextBox();
this.lblTplBody = new System.Windows.Forms.Label();
this.txtTplBody = new System.Windows.Forms.TextBox();
this.lblTplHint = new System.Windows.Forms.Label();
this.btnSave = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
@@ -94,8 +98,7 @@ namespace BulkArchiveMailer.AddIn.UI
this.tabControl.SuspendLayout();
this.tabGeneral.SuspendLayout();
this.tabSshAccess.SuspendLayout();
this.tabConfigUpdate.SuspendLayout();
this.tabTemplates.SuspendLayout();
this.SuspendLayout();
//
@@ -108,8 +111,7 @@ namespace BulkArchiveMailer.AddIn.UI
| System.Windows.Forms.AnchorStyles.Right)));
this.tabControl.TabPages.AddRange(new System.Windows.Forms.TabPage[] {
this.tabGeneral,
this.tabSshAccess,
this.tabConfigUpdate});
this.tabTemplates});
//
// tabGeneral
@@ -195,97 +197,115 @@ namespace BulkArchiveMailer.AddIn.UI
this.cboFromAccount.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
//
// tabSshAccess
// tabTemplates
//
this.tabSshAccess.Text = "Шаблон: новый доступ";
this.tabSshAccess.Padding = new System.Windows.Forms.Padding(12);
this.tabSshAccess.UseVisualStyleBackColor = true;
this.tabSshAccess.Controls.Add(this.lblSshSubject);
this.tabSshAccess.Controls.Add(this.txtSshSubject);
this.tabSshAccess.Controls.Add(this.lblSshBody);
this.tabSshAccess.Controls.Add(this.txtSshBody);
this.tabSshAccess.Controls.Add(this.lblSshHint);
this.tabTemplates.Text = "Шаблоны";
this.tabTemplates.Padding = new System.Windows.Forms.Padding(12);
this.tabTemplates.UseVisualStyleBackColor = true;
this.tabTemplates.Controls.Add(this.lstTemplates);
this.tabTemplates.Controls.Add(this.btnAddTemplate);
this.tabTemplates.Controls.Add(this.btnRemoveTemplate);
this.tabTemplates.Controls.Add(this.lblTplDisplayName);
this.tabTemplates.Controls.Add(this.txtTplDisplayName);
this.tabTemplates.Controls.Add(this.lblTplFileSuffix);
this.tabTemplates.Controls.Add(this.txtTplFileSuffix);
this.tabTemplates.Controls.Add(this.lblTplFileSuffixHint);
this.tabTemplates.Controls.Add(this.lblTplSubject);
this.tabTemplates.Controls.Add(this.txtTplSubject);
this.tabTemplates.Controls.Add(this.lblTplBody);
this.tabTemplates.Controls.Add(this.txtTplBody);
this.tabTemplates.Controls.Add(this.lblTplHint);
// lblSshSubject
this.lblSshSubject.Location = new System.Drawing.Point(15, 20);
this.lblSshSubject.Size = new System.Drawing.Size(200, 20);
this.lblSshSubject.Text = "Тема письма:";
// lstTemplates
this.lstTemplates.Location = new System.Drawing.Point(15, 20);
this.lstTemplates.Size = new System.Drawing.Size(190, 329);
this.lstTemplates.IntegralHeight = false;
this.lstTemplates.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)));
this.lstTemplates.SelectedIndexChanged += new System.EventHandler(this.lstTemplates_SelectedIndexChanged);
// txtSshSubject
this.txtSshSubject.Location = new System.Drawing.Point(15, 43);
this.txtSshSubject.Size = new System.Drawing.Size(610, 23);
// btnAddTemplate
this.btnAddTemplate.Location = new System.Drawing.Point(15, 355);
this.btnAddTemplate.Size = new System.Drawing.Size(90, 27);
this.btnAddTemplate.Text = "+ Добавить";
this.btnAddTemplate.UseVisualStyleBackColor = true;
this.btnAddTemplate.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnAddTemplate.Click += new System.EventHandler(this.btnAddTemplate_Click);
// lblSshBody
this.lblSshBody.Location = new System.Drawing.Point(15, 78);
this.lblSshBody.Size = new System.Drawing.Size(200, 20);
this.lblSshBody.Text = "Тело письма (HTML):";
// btnRemoveTemplate
this.btnRemoveTemplate.Location = new System.Drawing.Point(115, 355);
this.btnRemoveTemplate.Size = new System.Drawing.Size(90, 27);
this.btnRemoveTemplate.Text = "Удалить";
this.btnRemoveTemplate.UseVisualStyleBackColor = true;
this.btnRemoveTemplate.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnRemoveTemplate.Click += new System.EventHandler(this.btnRemoveTemplate_Click);
// txtSshBody
this.txtSshBody.Location = new System.Drawing.Point(15, 101);
this.txtSshBody.Size = new System.Drawing.Size(610, 300);
this.txtSshBody.Multiline = true;
this.txtSshBody.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtSshBody.AcceptsReturn = true;
this.txtSshBody.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
// lblTplDisplayName
this.lblTplDisplayName.Location = new System.Drawing.Point(220, 20);
this.lblTplDisplayName.Size = new System.Drawing.Size(200, 20);
this.lblTplDisplayName.Text = "Название:";
// txtTplDisplayName
this.txtTplDisplayName.Location = new System.Drawing.Point(220, 43);
this.txtTplDisplayName.Size = new System.Drawing.Size(410, 23);
this.txtTplDisplayName.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right));
this.txtTplDisplayName.Leave += new System.EventHandler(this.txtTplDisplayName_Leave);
// lblTplFileSuffix
this.lblTplFileSuffix.Location = new System.Drawing.Point(220, 78);
this.lblTplFileSuffix.Size = new System.Drawing.Size(200, 20);
this.lblTplFileSuffix.Text = "Суффикс файла:";
// txtTplFileSuffix
this.txtTplFileSuffix.Location = new System.Drawing.Point(220, 101);
this.txtTplFileSuffix.Size = new System.Drawing.Size(200, 23);
// lblTplFileSuffixHint
this.lblTplFileSuffixHint.Location = new System.Drawing.Point(220, 127);
this.lblTplFileSuffixHint.Size = new System.Drawing.Size(410, 20);
this.lblTplFileSuffixHint.Text = "Файл: {login}_<суффикс>.7z";
this.lblTplFileSuffixHint.ForeColor = System.Drawing.SystemColors.GrayText;
// lblTplSubject
this.lblTplSubject.Location = new System.Drawing.Point(220, 160);
this.lblTplSubject.Size = new System.Drawing.Size(200, 20);
this.lblTplSubject.Text = "Тема письма:";
// txtTplSubject
this.txtTplSubject.Location = new System.Drawing.Point(220, 183);
this.txtTplSubject.Size = new System.Drawing.Size(410, 23);
this.txtTplSubject.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right));
// lblTplBody
this.lblTplBody.Location = new System.Drawing.Point(220, 218);
this.lblTplBody.Size = new System.Drawing.Size(200, 20);
this.lblTplBody.Text = "Тело письма (HTML):";
// txtTplBody
this.txtTplBody.Location = new System.Drawing.Point(220, 241);
this.txtTplBody.Size = new System.Drawing.Size(410, 160);
this.txtTplBody.Multiline = true;
this.txtTplBody.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtTplBody.AcceptsReturn = true;
this.txtTplBody.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
// lblSshHint
this.lblSshHint.Location = new System.Drawing.Point(15, 408);
this.lblSshHint.Size = new System.Drawing.Size(610, 20);
this.lblSshHint.Text = "Доступные плейсхолдеры: {Login}, {Email}, {Date}";
this.lblSshHint.ForeColor = System.Drawing.SystemColors.GrayText;
this.lblSshHint.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
//
// tabConfigUpdate
//
this.tabConfigUpdate.Text = "Шаблон: обновление конфига";
this.tabConfigUpdate.Padding = new System.Windows.Forms.Padding(12);
this.tabConfigUpdate.UseVisualStyleBackColor = true;
this.tabConfigUpdate.Controls.Add(this.lblCfgSubject);
this.tabConfigUpdate.Controls.Add(this.txtCfgSubject);
this.tabConfigUpdate.Controls.Add(this.lblCfgBody);
this.tabConfigUpdate.Controls.Add(this.txtCfgBody);
this.tabConfigUpdate.Controls.Add(this.lblCfgHint);
// lblCfgSubject
this.lblCfgSubject.Location = new System.Drawing.Point(15, 20);
this.lblCfgSubject.Size = new System.Drawing.Size(200, 20);
this.lblCfgSubject.Text = "Тема письма:";
// txtCfgSubject
this.txtCfgSubject.Location = new System.Drawing.Point(15, 43);
this.txtCfgSubject.Size = new System.Drawing.Size(610, 23);
// lblCfgBody
this.lblCfgBody.Location = new System.Drawing.Point(15, 78);
this.lblCfgBody.Size = new System.Drawing.Size(200, 20);
this.lblCfgBody.Text = "Тело письма (HTML):";
// txtCfgBody
this.txtCfgBody.Location = new System.Drawing.Point(15, 101);
this.txtCfgBody.Size = new System.Drawing.Size(610, 300);
this.txtCfgBody.Multiline = true;
this.txtCfgBody.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtCfgBody.AcceptsReturn = true;
this.txtCfgBody.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
// lblCfgHint
this.lblCfgHint.Location = new System.Drawing.Point(15, 408);
this.lblCfgHint.Size = new System.Drawing.Size(610, 20);
this.lblCfgHint.Text = "Доступные плейсхолдеры: {Login}, {Email}, {Date}";
this.lblCfgHint.ForeColor = System.Drawing.SystemColors.GrayText;
this.lblCfgHint.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
// lblTplHint
this.lblTplHint.Location = new System.Drawing.Point(220, 408);
this.lblTplHint.Size = new System.Drawing.Size(410, 20);
this.lblTplHint.Text = "Доступные плейсхолдеры: {Login}, {Email}, {Date}";
this.lblTplHint.ForeColor = System.Drawing.SystemColors.GrayText;
this.lblTplHint.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
//
// btnSave / btnCancel / btnResetDefaults
//
this.btnResetDefaults.Location = new System.Drawing.Point(12, 494);
this.btnResetDefaults.Size = new System.Drawing.Size(220, 30);
this.btnResetDefaults.Text = "Сбросить домен/шаблоны по умолчанию";
this.btnResetDefaults.Text = "Сбросить шаблоны по умолчанию";
this.btnResetDefaults.UseVisualStyleBackColor = true;
this.btnResetDefaults.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnResetDefaults.Click += new System.EventHandler(this.btnResetDefaults_Click);
@@ -326,10 +346,8 @@ namespace BulkArchiveMailer.AddIn.UI
this.tabControl.ResumeLayout(false);
this.tabGeneral.ResumeLayout(false);
this.tabGeneral.PerformLayout();
this.tabSshAccess.ResumeLayout(false);
this.tabSshAccess.PerformLayout();
this.tabConfigUpdate.ResumeLayout(false);
this.tabConfigUpdate.PerformLayout();
this.tabTemplates.ResumeLayout(false);
this.tabTemplates.PerformLayout();
this.ResumeLayout(false);
}
}