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:
1 parent
cb3b273741
commit
804b9610ca
16 files changed
+904
-288
No files matched your search
@@ -86,7 +86,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
var logPath = Path.Combine(settings.ArchiveFolder, "send-log.csv");
|
||||
sendLog = new SendLogStore(logPath);
|
||||
|
||||
var templates = settings.ToTemplateSet();
|
||||
var templates = settings.ToTemplateList();
|
||||
var scan = ArchiveScanner.Scan(settings.ArchiveFolder, settings.EmailDomain, templates, sendLog);
|
||||
|
||||
rows = new BindingList<PreviewRow>(scan.AllRows.Select(job => new PreviewRow(job)).ToList());
|
||||
@@ -277,7 +277,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
TimestampUtc = DateTime.UtcNow,
|
||||
Login = row.Job.Login,
|
||||
Email = row.Job.Email,
|
||||
Kind = row.Job.Kind ?? default,
|
||||
Kind = row.Job.Kind ?? "",
|
||||
ArchivePath = row.Job.ArchivePath ?? "",
|
||||
FileSha256 = row.Job.FileSha256 ?? "",
|
||||
Status = outcome.Success ? (testMode ? SendLogStatus.Draft : SendLogStatus.Sent) : SendLogStatus.Failed,
|
||||
|
||||
+127
-109
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows.Forms;
|
||||
using BulkArchiveMailer.Core;
|
||||
using Outlook = Microsoft.Office.Interop.Outlook;
|
||||
@@ -7,13 +9,31 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Archive folder, email domain, signature override, test-mode default,
|
||||
/// and the two editable mail templates. Saves straight to
|
||||
/// and an arbitrary-length, operator-editable list of mail templates
|
||||
/// (master-detail: lstTemplates on the left, editor fields for the
|
||||
/// selected one on the right). Saves straight to
|
||||
/// %APPDATA%\BulkArchiveMailer\config.json via AppSettings.
|
||||
/// </summary>
|
||||
public partial class SettingsForm : Form
|
||||
{
|
||||
private readonly Outlook.Application outlookApplication;
|
||||
|
||||
/// <summary>
|
||||
/// In-memory working copy of the template list, edited freely
|
||||
/// (add/remove/rename/reorder-by-selection) until btnSave_Click
|
||||
/// persists it via AppSettings.Save - nothing here touches disk
|
||||
/// until then.
|
||||
/// </summary>
|
||||
private List<MailTemplateDefinition> templates = new List<MailTemplateDefinition>();
|
||||
|
||||
/// <summary>
|
||||
/// Guards against SyncEditorToSelectedTemplate/LoadSelectedTemplateIntoEditor
|
||||
/// re-entrancy while lstTemplates.SelectedIndex is being changed
|
||||
/// programmatically (as opposed to by the operator clicking in the
|
||||
/// list) - the standard pattern for a WinForms master-detail list.
|
||||
/// </summary>
|
||||
private bool suppressSelectionSync;
|
||||
|
||||
public SettingsForm(Outlook.Application outlookApplication)
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -61,11 +81,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
|
||||
PopulateFromAccountCombo(settings.FromAccountSmtpAddress ?? "");
|
||||
|
||||
var templates = settings.ToTemplateSet();
|
||||
txtSshSubject.Text = templates.SshAccess.Subject;
|
||||
txtSshBody.Text = templates.SshAccess.BodyHtml;
|
||||
txtCfgSubject.Text = templates.ConfigUpdate.Subject;
|
||||
txtCfgBody.Text = templates.ConfigUpdate.BodyHtml;
|
||||
templates = settings.ToTemplateList();
|
||||
PopulateTemplateList(selectFirst: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -111,6 +128,211 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
cboFromAccount.SelectedIndex = selectedIndex;
|
||||
}
|
||||
|
||||
// --- Template list master-detail -----------------------------------
|
||||
|
||||
/// <summary>Rebuilds lstTemplates' items from the in-memory `templates` list, preserving (or resetting to the first item) the current selection.</summary>
|
||||
private void PopulateTemplateList(bool selectFirst)
|
||||
{
|
||||
suppressSelectionSync = true;
|
||||
try
|
||||
{
|
||||
lstTemplates.Items.Clear();
|
||||
foreach (var t in templates)
|
||||
{
|
||||
lstTemplates.Items.Add(TemplateListLabel(t));
|
||||
}
|
||||
|
||||
if (lstTemplates.Items.Count > 0)
|
||||
{
|
||||
lstTemplates.SelectedIndex = selectFirst ? 0 : Math.Min(lstTemplates.Items.Count - 1, lstTemplates.SelectedIndex);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
suppressSelectionSync = false;
|
||||
}
|
||||
|
||||
LoadSelectedTemplateIntoEditor();
|
||||
}
|
||||
|
||||
private static string TemplateListLabel(MailTemplateDefinition t) =>
|
||||
string.IsNullOrWhiteSpace(t.DisplayName) ? "(без названия)" : t.DisplayName;
|
||||
|
||||
/// <summary>
|
||||
/// Rewrites lstTemplates' visible label for the currently-edited
|
||||
/// index from the in-memory list (call right after
|
||||
/// SyncEditorToSelectedTemplate so the list reflects a renamed
|
||||
/// template without a full PopulateTemplateList rebuild, which
|
||||
/// would also disturb the current selection/scroll position).
|
||||
/// </summary>
|
||||
private void RefreshEditedTemplateLabel()
|
||||
{
|
||||
if (!(lstTemplates.Tag is int editedIndex) || editedIndex < 0 || editedIndex >= templates.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
suppressSelectionSync = true;
|
||||
try
|
||||
{
|
||||
lstTemplates.Items[editedIndex] = TemplateListLabel(templates[editedIndex]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
suppressSelectionSync = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Writes the editor fields' current values back into the in-memory list entry that was selected when editing started (tracked via lstTemplates.Tag, see LoadSelectedTemplateIntoEditor). Safe to call with nothing selected/no prior selection.</summary>
|
||||
private void SyncEditorToSelectedTemplate()
|
||||
{
|
||||
if (!(lstTemplates.Tag is int editedIndex) || editedIndex < 0 || editedIndex >= templates.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var t = templates[editedIndex];
|
||||
t.DisplayName = txtTplDisplayName.Text;
|
||||
t.FileSuffix = txtTplFileSuffix.Text.Trim();
|
||||
t.Subject = txtTplSubject.Text;
|
||||
t.BodyHtml = txtTplBody.Text;
|
||||
}
|
||||
|
||||
/// <summary>Loads the newly selected list item's values into the editor fields, and records which index the editor now belongs to (lstTemplates.Tag) so a later SyncEditorToSelectedTemplate knows where to write back.</summary>
|
||||
private void LoadSelectedTemplateIntoEditor()
|
||||
{
|
||||
var index = lstTemplates.SelectedIndex;
|
||||
var hasSelection = index >= 0 && index < templates.Count;
|
||||
|
||||
txtTplDisplayName.Enabled = hasSelection;
|
||||
txtTplFileSuffix.Enabled = hasSelection;
|
||||
txtTplSubject.Enabled = hasSelection;
|
||||
txtTplBody.Enabled = hasSelection;
|
||||
|
||||
if (!hasSelection)
|
||||
{
|
||||
lstTemplates.Tag = -1;
|
||||
txtTplDisplayName.Text = "";
|
||||
txtTplFileSuffix.Text = "";
|
||||
txtTplSubject.Text = "";
|
||||
txtTplBody.Text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
var t = templates[index];
|
||||
lstTemplates.Tag = index;
|
||||
txtTplDisplayName.Text = t.DisplayName;
|
||||
txtTplFileSuffix.Text = t.FileSuffix;
|
||||
txtTplSubject.Text = t.Subject;
|
||||
txtTplBody.Text = t.BodyHtml;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps lstTemplates' visible label in sync as soon as the
|
||||
/// operator finishes editing the name (focus leaves the field),
|
||||
/// without waiting for a selection change or Save. Leave (not
|
||||
/// TextChanged) deliberately: TextChanged would fire re-entrantly
|
||||
/// while LoadSelectedTemplateIntoEditor is itself still in the
|
||||
/// middle of setting the other fields for a newly selected
|
||||
/// template, which could write stale values back.
|
||||
/// </summary>
|
||||
private void txtTplDisplayName_Leave(object sender, EventArgs e)
|
||||
{
|
||||
SyncEditorToSelectedTemplate();
|
||||
RefreshEditedTemplateLabel();
|
||||
}
|
||||
|
||||
private void lstTemplates_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (suppressSelectionSync)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Save whatever is currently in the editor back to the
|
||||
// template it belonged to BEFORE loading the newly selected
|
||||
// one - otherwise unsaved edits to the previous selection
|
||||
// would be lost the moment the operator clicks another item.
|
||||
SyncEditorToSelectedTemplate();
|
||||
LoadSelectedTemplateIntoEditor();
|
||||
}
|
||||
|
||||
private void btnAddTemplate_Click(object sender, EventArgs e)
|
||||
{
|
||||
SyncEditorToSelectedTemplate();
|
||||
|
||||
var newTemplate = new MailTemplateDefinition
|
||||
{
|
||||
DisplayName = "Новый шаблон",
|
||||
FileSuffix = "",
|
||||
Subject = "",
|
||||
BodyHtml = "",
|
||||
};
|
||||
templates.Add(newTemplate);
|
||||
|
||||
PopulateTemplateList(selectFirst: false);
|
||||
suppressSelectionSync = true;
|
||||
try
|
||||
{
|
||||
lstTemplates.SelectedIndex = templates.Count - 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
suppressSelectionSync = false;
|
||||
}
|
||||
|
||||
LoadSelectedTemplateIntoEditor();
|
||||
txtTplDisplayName.Focus();
|
||||
txtTplDisplayName.SelectAll();
|
||||
}
|
||||
|
||||
private void btnRemoveTemplate_Click(object sender, EventArgs e)
|
||||
{
|
||||
var index = lstTemplates.SelectedIndex;
|
||||
if (index < 0 || index >= templates.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var label = TemplateListLabel(templates[index]);
|
||||
var confirm = MessageBox.Show(
|
||||
this,
|
||||
"Удалить шаблон \"" + label + "\"? Несохранённые правки в этом окне будут потеряны только после нажатия \"Сохранить\".",
|
||||
"Подтверждение",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question);
|
||||
if (confirm != DialogResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
templates.RemoveAt(index);
|
||||
|
||||
// The editor currently holds this now-removed template's stale
|
||||
// values - clear its "which index does the editor belong to"
|
||||
// marker so a later SyncEditorToSelectedTemplate (e.g. from the
|
||||
// SelectedIndexChanged this RemoveAt is about to trigger) does
|
||||
// not write those stale values into whatever template happens
|
||||
// to occupy this index next.
|
||||
lstTemplates.Tag = -1;
|
||||
|
||||
var nextIndex = templates.Count == 0 ? -1 : Math.Min(index, templates.Count - 1);
|
||||
suppressSelectionSync = true;
|
||||
try
|
||||
{
|
||||
lstTemplates.Items.RemoveAt(index);
|
||||
lstTemplates.SelectedIndex = nextIndex;
|
||||
}
|
||||
finally
|
||||
{
|
||||
suppressSelectionSync = false;
|
||||
}
|
||||
|
||||
LoadSelectedTemplateIntoEditor();
|
||||
}
|
||||
|
||||
// --- General tab handlers --------------------------------------
|
||||
|
||||
private void btnBrowseFolder_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (var dialog = new FolderBrowserDialog())
|
||||
@@ -168,7 +390,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
{
|
||||
var confirm = MessageBox.Show(
|
||||
this,
|
||||
"Заменить домен и оба шаблона писем значениями по умолчанию? Папка с архивами и файл подписи не изменятся. Несохранённые правки в этом окне будут потеряны.",
|
||||
"Заменить домен и весь список шаблонов писем значениями по умолчанию (два исходных шаблона)? Папка с архивами и файл подписи не изменятся. Несохранённые правки в этом окне будут потеряны.",
|
||||
"Подтверждение",
|
||||
MessageBoxButtons.YesNo,
|
||||
MessageBoxIcon.Question);
|
||||
@@ -180,11 +402,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
var defaults = AppSettings.LoadDefaults();
|
||||
txtDomain.Text = defaults.EmailDomain;
|
||||
|
||||
var templates = defaults.ToTemplateSet();
|
||||
txtSshSubject.Text = templates.SshAccess.Subject;
|
||||
txtSshBody.Text = templates.SshAccess.BodyHtml;
|
||||
txtCfgSubject.Text = templates.ConfigUpdate.Subject;
|
||||
txtCfgBody.Text = templates.ConfigUpdate.BodyHtml;
|
||||
templates = defaults.ToTemplateList();
|
||||
PopulateTemplateList(selectFirst: true);
|
||||
}
|
||||
|
||||
private void btnSave_Click(object sender, EventArgs e)
|
||||
@@ -212,6 +431,21 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure whatever is currently in the editor (possibly never
|
||||
// committed by a selection change, e.g. the operator edited one
|
||||
// template and clicked Save directly) is captured before
|
||||
// validating/saving the list.
|
||||
SyncEditorToSelectedTemplate();
|
||||
|
||||
var suffixError = ValidateTemplateSuffixes();
|
||||
if (suffixError != null)
|
||||
{
|
||||
MessageBox.Show(this, suffixError, "BulkArchiveMailer", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
tabControl.SelectedTab = tabTemplates;
|
||||
lstTemplates.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedAccount = cboFromAccount.SelectedItem as FromAccountItem;
|
||||
|
||||
var data = new AppSettingsData
|
||||
@@ -222,8 +456,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
TestModeDefault = chkTestModeDefault.Checked,
|
||||
FromAccountSmtpAddress = selectedAccount?.SmtpAddress ?? "",
|
||||
};
|
||||
data.SetTemplate(ArchiveKind.SshAccess, new MailTemplate { Subject = txtSshSubject.Text, BodyHtml = txtSshBody.Text });
|
||||
data.SetTemplate(ArchiveKind.ConfigUpdate, new MailTemplate { Subject = txtCfgSubject.Text, BodyHtml = txtCfgBody.Text });
|
||||
data.SetTemplates(templates);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -239,6 +472,36 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
Close();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every template's FileSuffix must be non-empty, and unique
|
||||
/// case-insensitively among all configured templates (it is
|
||||
/// ArchiveScanner's only way to match a file to a template).
|
||||
/// Returns null if valid, otherwise a message describing exactly
|
||||
/// which template/duplicate is the problem.
|
||||
/// </summary>
|
||||
private string ValidateTemplateSuffixes()
|
||||
{
|
||||
for (var i = 0; i < templates.Count; i++)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(templates[i].FileSuffix))
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(templates[i].DisplayName) ? $"#{i + 1}" : templates[i].DisplayName;
|
||||
return $"У шаблона \"{name}\" не заполнен суффикс файла — заполните поле \"Суффикс файла\" (например, \"ssh_access\").";
|
||||
}
|
||||
}
|
||||
|
||||
var duplicateGroup = templates
|
||||
.GroupBy(t => t.FileSuffix.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(g => g.Count() > 1);
|
||||
if (duplicateGroup != null)
|
||||
{
|
||||
var names = string.Join(", ", duplicateGroup.Select(t => string.IsNullOrWhiteSpace(t.DisplayName) ? "(без названия)" : t.DisplayName));
|
||||
return $"Суффикс файла \"{duplicateGroup.Key}\" используется в нескольких шаблонах ({names}) — суффикс должен быть уникальным.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void btnCancel_Click(object sender, EventArgs e)
|
||||
{
|
||||
DialogResult = DialogResult.Cancel;
|
||||
|
||||
Reference in new issue
Block a user