Fix hidden add/remove template buttons, add per-template login filter
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>
This commit is contained in:
1 parent
804b9610ca
commit
08f1d671b6
8 files changed
+385
-50
No files matched your search
@@ -44,6 +44,9 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
private System.Windows.Forms.Label lblTplFileSuffix;
|
private System.Windows.Forms.Label lblTplFileSuffix;
|
||||||
private System.Windows.Forms.TextBox txtTplFileSuffix;
|
private System.Windows.Forms.TextBox txtTplFileSuffix;
|
||||||
private System.Windows.Forms.Label lblTplFileSuffixHint;
|
private System.Windows.Forms.Label lblTplFileSuffixHint;
|
||||||
|
private System.Windows.Forms.Label lblTplLoginPattern;
|
||||||
|
private System.Windows.Forms.TextBox txtTplLoginPattern;
|
||||||
|
private System.Windows.Forms.Label lblTplLoginPatternHint;
|
||||||
private System.Windows.Forms.Label lblTplSubject;
|
private System.Windows.Forms.Label lblTplSubject;
|
||||||
private System.Windows.Forms.TextBox txtTplSubject;
|
private System.Windows.Forms.TextBox txtTplSubject;
|
||||||
private System.Windows.Forms.Label lblTplBody;
|
private System.Windows.Forms.Label lblTplBody;
|
||||||
@@ -86,6 +89,9 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
this.lblTplFileSuffix = new System.Windows.Forms.Label();
|
this.lblTplFileSuffix = new System.Windows.Forms.Label();
|
||||||
this.txtTplFileSuffix = new System.Windows.Forms.TextBox();
|
this.txtTplFileSuffix = new System.Windows.Forms.TextBox();
|
||||||
this.lblTplFileSuffixHint = new System.Windows.Forms.Label();
|
this.lblTplFileSuffixHint = new System.Windows.Forms.Label();
|
||||||
|
this.lblTplLoginPattern = new System.Windows.Forms.Label();
|
||||||
|
this.txtTplLoginPattern = new System.Windows.Forms.TextBox();
|
||||||
|
this.lblTplLoginPatternHint = new System.Windows.Forms.Label();
|
||||||
this.lblTplSubject = new System.Windows.Forms.Label();
|
this.lblTplSubject = new System.Windows.Forms.Label();
|
||||||
this.txtTplSubject = new System.Windows.Forms.TextBox();
|
this.txtTplSubject = new System.Windows.Forms.TextBox();
|
||||||
this.lblTplBody = new System.Windows.Forms.Label();
|
this.lblTplBody = new System.Windows.Forms.Label();
|
||||||
@@ -105,7 +111,12 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
// tabControl
|
// tabControl
|
||||||
//
|
//
|
||||||
this.tabControl.Location = new System.Drawing.Point(12, 12);
|
this.tabControl.Location = new System.Drawing.Point(12, 12);
|
||||||
this.tabControl.Size = new System.Drawing.Size(660, 470);
|
// Height deliberately generous (+80px versus the original 470)
|
||||||
|
// rather than packed to the computed minimum - see the long
|
||||||
|
// remark on lstTemplates/btnAddTemplate below for why "just
|
||||||
|
// enough" turned out not to be enough in practice on a real
|
||||||
|
// machine (DPI/font-scaling dependent tab-strip height).
|
||||||
|
this.tabControl.Size = new System.Drawing.Size(660, 550);
|
||||||
this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
this.tabControl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||||
| System.Windows.Forms.AnchorStyles.Left)
|
| System.Windows.Forms.AnchorStyles.Left)
|
||||||
| System.Windows.Forms.AnchorStyles.Right)));
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
@@ -149,50 +160,50 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
this.btnBrowseFolder.Click += new System.EventHandler(this.btnBrowseFolder_Click);
|
this.btnBrowseFolder.Click += new System.EventHandler(this.btnBrowseFolder_Click);
|
||||||
|
|
||||||
// lblDomain
|
// lblDomain
|
||||||
this.lblDomain.Location = new System.Drawing.Point(15, 85);
|
this.lblDomain.Location = new System.Drawing.Point(15, 99);
|
||||||
this.lblDomain.Size = new System.Drawing.Size(400, 20);
|
this.lblDomain.Size = new System.Drawing.Size(400, 20);
|
||||||
this.lblDomain.Text = "Домен электронной почты (login → login@домен):";
|
this.lblDomain.Text = "Домен электронной почты (login → login@домен):";
|
||||||
|
|
||||||
// txtDomain
|
// txtDomain
|
||||||
this.txtDomain.Location = new System.Drawing.Point(15, 108);
|
this.txtDomain.Location = new System.Drawing.Point(15, 122);
|
||||||
this.txtDomain.Size = new System.Drawing.Size(300, 23);
|
this.txtDomain.Size = new System.Drawing.Size(300, 23);
|
||||||
|
|
||||||
// lblSignaturePath
|
// lblSignaturePath
|
||||||
this.lblSignaturePath.Location = new System.Drawing.Point(15, 150);
|
this.lblSignaturePath.Location = new System.Drawing.Point(15, 178);
|
||||||
this.lblSignaturePath.Size = new System.Drawing.Size(610, 20);
|
this.lblSignaturePath.Size = new System.Drawing.Size(610, 20);
|
||||||
this.lblSignaturePath.Text = "Файл подписи (.htm). Пусто = определять автоматически по умолчанию Outlook при каждой отправке:";
|
this.lblSignaturePath.Text = "Файл подписи (.htm). Пусто = определять автоматически по умолчанию Outlook при каждой отправке:";
|
||||||
|
|
||||||
// txtSignaturePath
|
// txtSignaturePath
|
||||||
this.txtSignaturePath.Location = new System.Drawing.Point(15, 173);
|
this.txtSignaturePath.Location = new System.Drawing.Point(15, 201);
|
||||||
this.txtSignaturePath.Size = new System.Drawing.Size(500, 23);
|
this.txtSignaturePath.Size = new System.Drawing.Size(500, 23);
|
||||||
|
|
||||||
// btnBrowseSignature
|
// btnBrowseSignature
|
||||||
this.btnBrowseSignature.Location = new System.Drawing.Point(525, 171);
|
this.btnBrowseSignature.Location = new System.Drawing.Point(525, 199);
|
||||||
this.btnBrowseSignature.Size = new System.Drawing.Size(100, 25);
|
this.btnBrowseSignature.Size = new System.Drawing.Size(100, 25);
|
||||||
this.btnBrowseSignature.Text = "Обзор...";
|
this.btnBrowseSignature.Text = "Обзор...";
|
||||||
this.btnBrowseSignature.UseVisualStyleBackColor = true;
|
this.btnBrowseSignature.UseVisualStyleBackColor = true;
|
||||||
this.btnBrowseSignature.Click += new System.EventHandler(this.btnBrowseSignature_Click);
|
this.btnBrowseSignature.Click += new System.EventHandler(this.btnBrowseSignature_Click);
|
||||||
|
|
||||||
// btnAutoDetectSignature
|
// btnAutoDetectSignature
|
||||||
this.btnAutoDetectSignature.Location = new System.Drawing.Point(15, 205);
|
this.btnAutoDetectSignature.Location = new System.Drawing.Point(15, 247);
|
||||||
this.btnAutoDetectSignature.Size = new System.Drawing.Size(280, 25);
|
this.btnAutoDetectSignature.Size = new System.Drawing.Size(280, 25);
|
||||||
this.btnAutoDetectSignature.Text = "Определить автоматически сейчас";
|
this.btnAutoDetectSignature.Text = "Определить автоматически сейчас";
|
||||||
this.btnAutoDetectSignature.UseVisualStyleBackColor = true;
|
this.btnAutoDetectSignature.UseVisualStyleBackColor = true;
|
||||||
this.btnAutoDetectSignature.Click += new System.EventHandler(this.btnAutoDetectSignature_Click);
|
this.btnAutoDetectSignature.Click += new System.EventHandler(this.btnAutoDetectSignature_Click);
|
||||||
|
|
||||||
// chkTestModeDefault
|
// chkTestModeDefault
|
||||||
this.chkTestModeDefault.Location = new System.Drawing.Point(15, 250);
|
this.chkTestModeDefault.Location = new System.Drawing.Point(15, 306);
|
||||||
this.chkTestModeDefault.Size = new System.Drawing.Size(610, 24);
|
this.chkTestModeDefault.Size = new System.Drawing.Size(610, 24);
|
||||||
this.chkTestModeDefault.Text = "По умолчанию открывать предпросмотр в тестовом режиме (сохранять в Черновики)";
|
this.chkTestModeDefault.Text = "По умолчанию открывать предпросмотр в тестовом режиме (сохранять в Черновики)";
|
||||||
this.chkTestModeDefault.UseVisualStyleBackColor = true;
|
this.chkTestModeDefault.UseVisualStyleBackColor = true;
|
||||||
|
|
||||||
// lblFromAccount
|
// lblFromAccount
|
||||||
this.lblFromAccount.Location = new System.Drawing.Point(15, 284);
|
this.lblFromAccount.Location = new System.Drawing.Point(15, 378);
|
||||||
this.lblFromAccount.Size = new System.Drawing.Size(400, 20);
|
this.lblFromAccount.Size = new System.Drawing.Size(400, 20);
|
||||||
this.lblFromAccount.Text = "Отправлять от (аккаунт Outlook текущей сессии):";
|
this.lblFromAccount.Text = "Отправлять от (аккаунт Outlook текущей сессии):";
|
||||||
|
|
||||||
// cboFromAccount
|
// cboFromAccount
|
||||||
this.cboFromAccount.Location = new System.Drawing.Point(15, 307);
|
this.cboFromAccount.Location = new System.Drawing.Point(15, 401);
|
||||||
this.cboFromAccount.Size = new System.Drawing.Size(500, 23);
|
this.cboFromAccount.Size = new System.Drawing.Size(500, 23);
|
||||||
this.cboFromAccount.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
this.cboFromAccount.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||||
|
|
||||||
@@ -202,44 +213,69 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
this.tabTemplates.Text = "Шаблоны";
|
this.tabTemplates.Text = "Шаблоны";
|
||||||
this.tabTemplates.Padding = new System.Windows.Forms.Padding(12);
|
this.tabTemplates.Padding = new System.Windows.Forms.Padding(12);
|
||||||
this.tabTemplates.UseVisualStyleBackColor = true;
|
this.tabTemplates.UseVisualStyleBackColor = true;
|
||||||
this.tabTemplates.Controls.Add(this.lstTemplates);
|
|
||||||
this.tabTemplates.Controls.Add(this.btnAddTemplate);
|
this.tabTemplates.Controls.Add(this.btnAddTemplate);
|
||||||
this.tabTemplates.Controls.Add(this.btnRemoveTemplate);
|
this.tabTemplates.Controls.Add(this.btnRemoveTemplate);
|
||||||
|
this.tabTemplates.Controls.Add(this.lstTemplates);
|
||||||
this.tabTemplates.Controls.Add(this.lblTplDisplayName);
|
this.tabTemplates.Controls.Add(this.lblTplDisplayName);
|
||||||
this.tabTemplates.Controls.Add(this.txtTplDisplayName);
|
this.tabTemplates.Controls.Add(this.txtTplDisplayName);
|
||||||
this.tabTemplates.Controls.Add(this.lblTplFileSuffix);
|
this.tabTemplates.Controls.Add(this.lblTplFileSuffix);
|
||||||
this.tabTemplates.Controls.Add(this.txtTplFileSuffix);
|
this.tabTemplates.Controls.Add(this.txtTplFileSuffix);
|
||||||
this.tabTemplates.Controls.Add(this.lblTplFileSuffixHint);
|
this.tabTemplates.Controls.Add(this.lblTplFileSuffixHint);
|
||||||
|
this.tabTemplates.Controls.Add(this.lblTplLoginPattern);
|
||||||
|
this.tabTemplates.Controls.Add(this.txtTplLoginPattern);
|
||||||
|
this.tabTemplates.Controls.Add(this.lblTplLoginPatternHint);
|
||||||
this.tabTemplates.Controls.Add(this.lblTplSubject);
|
this.tabTemplates.Controls.Add(this.lblTplSubject);
|
||||||
this.tabTemplates.Controls.Add(this.txtTplSubject);
|
this.tabTemplates.Controls.Add(this.txtTplSubject);
|
||||||
this.tabTemplates.Controls.Add(this.lblTplBody);
|
this.tabTemplates.Controls.Add(this.lblTplBody);
|
||||||
this.tabTemplates.Controls.Add(this.txtTplBody);
|
this.tabTemplates.Controls.Add(this.txtTplBody);
|
||||||
this.tabTemplates.Controls.Add(this.lblTplHint);
|
this.tabTemplates.Controls.Add(this.lblTplHint);
|
||||||
|
|
||||||
|
// btnAddTemplate / btnRemoveTemplate
|
||||||
|
//
|
||||||
|
// Deliberately placed ABOVE lstTemplates (not below it) so
|
||||||
|
// their visibility never depends on how much vertical room is
|
||||||
|
// left over at the bottom of the tab page after the list -
|
||||||
|
// that computation (tab-strip height under the operator's
|
||||||
|
// actual DPI/font scale, which this code cannot know at design
|
||||||
|
// time) is exactly what went wrong before: lstTemplates ran all
|
||||||
|
// the way to the tab page's real bottom edge with only ~6px to
|
||||||
|
// spare for the buttons beneath it, which was enough on some
|
||||||
|
// machines and not on others. Anchored Top|Left (fixed
|
||||||
|
// distance from the tab page's top-left corner) rather than
|
||||||
|
// Bottom, they are visible as long as the tab page itself is
|
||||||
|
// visible at all - not merely as long as it is "tall enough".
|
||||||
|
this.btnAddTemplate.Location = new System.Drawing.Point(15, 20);
|
||||||
|
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.Top | System.Windows.Forms.AnchorStyles.Left)));
|
||||||
|
this.btnAddTemplate.Click += new System.EventHandler(this.btnAddTemplate_Click);
|
||||||
|
|
||||||
|
// btnRemoveTemplate
|
||||||
|
this.btnRemoveTemplate.Location = new System.Drawing.Point(115, 20);
|
||||||
|
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.Top | System.Windows.Forms.AnchorStyles.Left)));
|
||||||
|
this.btnRemoveTemplate.Click += new System.EventHandler(this.btnRemoveTemplate_Click);
|
||||||
|
|
||||||
// lstTemplates
|
// lstTemplates
|
||||||
this.lstTemplates.Location = new System.Drawing.Point(15, 20);
|
//
|
||||||
this.lstTemplates.Size = new System.Drawing.Size(190, 329);
|
// Starts below the two buttons now (y=55, was y=20) and still
|
||||||
|
// stretches down to the bottom of the tab page (Anchor
|
||||||
|
// Top|Bottom|Left) - only its OWN bottom edge is at risk of
|
||||||
|
// being visually tight against the tab page's real bottom
|
||||||
|
// edge if the assumed content height is off, which just means
|
||||||
|
// slightly less of the list is visible at once (it already has
|
||||||
|
// a scrollbar - IntegralHeight = false - so this degrades
|
||||||
|
// gracefully) rather than an entire control disappearing.
|
||||||
|
this.lstTemplates.Location = new System.Drawing.Point(15, 55);
|
||||||
|
this.lstTemplates.Size = new System.Drawing.Size(190, 432);
|
||||||
this.lstTemplates.IntegralHeight = false;
|
this.lstTemplates.IntegralHeight = false;
|
||||||
this.lstTemplates.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
this.lstTemplates.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||||
| System.Windows.Forms.AnchorStyles.Left)));
|
| System.Windows.Forms.AnchorStyles.Left)));
|
||||||
this.lstTemplates.SelectedIndexChanged += new System.EventHandler(this.lstTemplates_SelectedIndexChanged);
|
this.lstTemplates.SelectedIndexChanged += new System.EventHandler(this.lstTemplates_SelectedIndexChanged);
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// lblTplDisplayName
|
// lblTplDisplayName
|
||||||
this.lblTplDisplayName.Location = new System.Drawing.Point(220, 20);
|
this.lblTplDisplayName.Location = new System.Drawing.Point(220, 20);
|
||||||
this.lblTplDisplayName.Size = new System.Drawing.Size(200, 20);
|
this.lblTplDisplayName.Size = new System.Drawing.Size(200, 20);
|
||||||
@@ -267,25 +303,42 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
this.lblTplFileSuffixHint.Text = "Файл: {login}_<суффикс>.7z";
|
this.lblTplFileSuffixHint.Text = "Файл: {login}_<суффикс>.7z";
|
||||||
this.lblTplFileSuffixHint.ForeColor = System.Drawing.SystemColors.GrayText;
|
this.lblTplFileSuffixHint.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||||
|
|
||||||
|
// lblTplLoginPattern
|
||||||
|
this.lblTplLoginPattern.Location = new System.Drawing.Point(220, 160);
|
||||||
|
this.lblTplLoginPattern.Size = new System.Drawing.Size(410, 20);
|
||||||
|
this.lblTplLoginPattern.Text = "Фильтр логина (regex, необязательно):";
|
||||||
|
|
||||||
|
// txtTplLoginPattern
|
||||||
|
this.txtTplLoginPattern.Location = new System.Drawing.Point(220, 183);
|
||||||
|
this.txtTplLoginPattern.Size = new System.Drawing.Size(410, 23);
|
||||||
|
this.txtTplLoginPattern.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||||
|
| System.Windows.Forms.AnchorStyles.Right));
|
||||||
|
|
||||||
|
// lblTplLoginPatternHint
|
||||||
|
this.lblTplLoginPatternHint.Location = new System.Drawing.Point(220, 209);
|
||||||
|
this.lblTplLoginPatternHint.Size = new System.Drawing.Size(410, 32);
|
||||||
|
this.lblTplLoginPatternHint.Text = "Пусто = любой логин. Пример: ^a-.* — только логины, начинающиеся с \"a-\".";
|
||||||
|
this.lblTplLoginPatternHint.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||||
|
|
||||||
// lblTplSubject
|
// lblTplSubject
|
||||||
this.lblTplSubject.Location = new System.Drawing.Point(220, 160);
|
this.lblTplSubject.Location = new System.Drawing.Point(220, 254);
|
||||||
this.lblTplSubject.Size = new System.Drawing.Size(200, 20);
|
this.lblTplSubject.Size = new System.Drawing.Size(200, 20);
|
||||||
this.lblTplSubject.Text = "Тема письма:";
|
this.lblTplSubject.Text = "Тема письма:";
|
||||||
|
|
||||||
// txtTplSubject
|
// txtTplSubject
|
||||||
this.txtTplSubject.Location = new System.Drawing.Point(220, 183);
|
this.txtTplSubject.Location = new System.Drawing.Point(220, 277);
|
||||||
this.txtTplSubject.Size = new System.Drawing.Size(410, 23);
|
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)
|
this.txtTplSubject.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||||
| System.Windows.Forms.AnchorStyles.Right));
|
| System.Windows.Forms.AnchorStyles.Right));
|
||||||
|
|
||||||
// lblTplBody
|
// lblTplBody
|
||||||
this.lblTplBody.Location = new System.Drawing.Point(220, 218);
|
this.lblTplBody.Location = new System.Drawing.Point(220, 312);
|
||||||
this.lblTplBody.Size = new System.Drawing.Size(200, 20);
|
this.lblTplBody.Size = new System.Drawing.Size(200, 20);
|
||||||
this.lblTplBody.Text = "Тело письма (HTML):";
|
this.lblTplBody.Text = "Тело письма (HTML):";
|
||||||
|
|
||||||
// txtTplBody
|
// txtTplBody
|
||||||
this.txtTplBody.Location = new System.Drawing.Point(220, 241);
|
this.txtTplBody.Location = new System.Drawing.Point(220, 335);
|
||||||
this.txtTplBody.Size = new System.Drawing.Size(410, 160);
|
this.txtTplBody.Size = new System.Drawing.Size(410, 125);
|
||||||
this.txtTplBody.Multiline = true;
|
this.txtTplBody.Multiline = true;
|
||||||
this.txtTplBody.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
|
this.txtTplBody.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
|
||||||
this.txtTplBody.AcceptsReturn = true;
|
this.txtTplBody.AcceptsReturn = true;
|
||||||
@@ -294,7 +347,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
| System.Windows.Forms.AnchorStyles.Right)));
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
|
||||||
// lblTplHint
|
// lblTplHint
|
||||||
this.lblTplHint.Location = new System.Drawing.Point(220, 408);
|
this.lblTplHint.Location = new System.Drawing.Point(220, 467);
|
||||||
this.lblTplHint.Size = new System.Drawing.Size(410, 20);
|
this.lblTplHint.Size = new System.Drawing.Size(410, 20);
|
||||||
this.lblTplHint.Text = "Доступные плейсхолдеры: {Login}, {Email}, {Date}";
|
this.lblTplHint.Text = "Доступные плейсхолдеры: {Login}, {Email}, {Date}";
|
||||||
this.lblTplHint.ForeColor = System.Drawing.SystemColors.GrayText;
|
this.lblTplHint.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||||
@@ -303,14 +356,14 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
//
|
//
|
||||||
// btnSave / btnCancel / btnResetDefaults
|
// btnSave / btnCancel / btnResetDefaults
|
||||||
//
|
//
|
||||||
this.btnResetDefaults.Location = new System.Drawing.Point(12, 494);
|
this.btnResetDefaults.Location = new System.Drawing.Point(12, 574);
|
||||||
this.btnResetDefaults.Size = new System.Drawing.Size(220, 30);
|
this.btnResetDefaults.Size = new System.Drawing.Size(220, 30);
|
||||||
this.btnResetDefaults.Text = "Сбросить шаблоны по умолчанию";
|
this.btnResetDefaults.Text = "Сбросить шаблоны по умолчанию";
|
||||||
this.btnResetDefaults.UseVisualStyleBackColor = true;
|
this.btnResetDefaults.UseVisualStyleBackColor = true;
|
||||||
this.btnResetDefaults.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
|
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);
|
this.btnResetDefaults.Click += new System.EventHandler(this.btnResetDefaults_Click);
|
||||||
|
|
||||||
this.btnCancel.Location = new System.Drawing.Point(479, 494);
|
this.btnCancel.Location = new System.Drawing.Point(479, 574);
|
||||||
this.btnCancel.Size = new System.Drawing.Size(90, 30);
|
this.btnCancel.Size = new System.Drawing.Size(90, 30);
|
||||||
this.btnCancel.Text = "Отмена";
|
this.btnCancel.Text = "Отмена";
|
||||||
this.btnCancel.UseVisualStyleBackColor = true;
|
this.btnCancel.UseVisualStyleBackColor = true;
|
||||||
@@ -318,7 +371,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
this.btnCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
|
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
|
||||||
|
|
||||||
this.btnSave.Location = new System.Drawing.Point(575, 494);
|
this.btnSave.Location = new System.Drawing.Point(575, 574);
|
||||||
this.btnSave.Size = new System.Drawing.Size(97, 30);
|
this.btnSave.Size = new System.Drawing.Size(97, 30);
|
||||||
this.btnSave.Text = "Сохранить";
|
this.btnSave.Text = "Сохранить";
|
||||||
this.btnSave.UseVisualStyleBackColor = true;
|
this.btnSave.UseVisualStyleBackColor = true;
|
||||||
@@ -330,8 +383,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
//
|
//
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
this.ClientSize = new System.Drawing.Size(684, 536);
|
this.ClientSize = new System.Drawing.Size(684, 616);
|
||||||
this.MinimumSize = new System.Drawing.Size(560, 420);
|
this.MinimumSize = new System.Drawing.Size(560, 500);
|
||||||
this.Controls.Add(this.tabControl);
|
this.Controls.Add(this.tabControl);
|
||||||
this.Controls.Add(this.btnResetDefaults);
|
this.Controls.Add(this.btnResetDefaults);
|
||||||
this.Controls.Add(this.btnCancel);
|
this.Controls.Add(this.btnCancel);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using BulkArchiveMailer.Core;
|
using BulkArchiveMailer.Core;
|
||||||
using Outlook = Microsoft.Office.Interop.Outlook;
|
using Outlook = Microsoft.Office.Interop.Outlook;
|
||||||
@@ -194,6 +195,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
var t = templates[editedIndex];
|
var t = templates[editedIndex];
|
||||||
t.DisplayName = txtTplDisplayName.Text;
|
t.DisplayName = txtTplDisplayName.Text;
|
||||||
t.FileSuffix = txtTplFileSuffix.Text.Trim();
|
t.FileSuffix = txtTplFileSuffix.Text.Trim();
|
||||||
|
t.LoginPattern = txtTplLoginPattern.Text.Trim();
|
||||||
t.Subject = txtTplSubject.Text;
|
t.Subject = txtTplSubject.Text;
|
||||||
t.BodyHtml = txtTplBody.Text;
|
t.BodyHtml = txtTplBody.Text;
|
||||||
}
|
}
|
||||||
@@ -206,6 +208,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
|
|
||||||
txtTplDisplayName.Enabled = hasSelection;
|
txtTplDisplayName.Enabled = hasSelection;
|
||||||
txtTplFileSuffix.Enabled = hasSelection;
|
txtTplFileSuffix.Enabled = hasSelection;
|
||||||
|
txtTplLoginPattern.Enabled = hasSelection;
|
||||||
txtTplSubject.Enabled = hasSelection;
|
txtTplSubject.Enabled = hasSelection;
|
||||||
txtTplBody.Enabled = hasSelection;
|
txtTplBody.Enabled = hasSelection;
|
||||||
|
|
||||||
@@ -214,6 +217,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
lstTemplates.Tag = -1;
|
lstTemplates.Tag = -1;
|
||||||
txtTplDisplayName.Text = "";
|
txtTplDisplayName.Text = "";
|
||||||
txtTplFileSuffix.Text = "";
|
txtTplFileSuffix.Text = "";
|
||||||
|
txtTplLoginPattern.Text = "";
|
||||||
txtTplSubject.Text = "";
|
txtTplSubject.Text = "";
|
||||||
txtTplBody.Text = "";
|
txtTplBody.Text = "";
|
||||||
return;
|
return;
|
||||||
@@ -223,6 +227,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
lstTemplates.Tag = index;
|
lstTemplates.Tag = index;
|
||||||
txtTplDisplayName.Text = t.DisplayName;
|
txtTplDisplayName.Text = t.DisplayName;
|
||||||
txtTplFileSuffix.Text = t.FileSuffix;
|
txtTplFileSuffix.Text = t.FileSuffix;
|
||||||
|
txtTplLoginPattern.Text = t.LoginPattern;
|
||||||
txtTplSubject.Text = t.Subject;
|
txtTplSubject.Text = t.Subject;
|
||||||
txtTplBody.Text = t.BodyHtml;
|
txtTplBody.Text = t.BodyHtml;
|
||||||
}
|
}
|
||||||
@@ -265,6 +270,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
{
|
{
|
||||||
DisplayName = "Новый шаблон",
|
DisplayName = "Новый шаблон",
|
||||||
FileSuffix = "",
|
FileSuffix = "",
|
||||||
|
LoginPattern = "",
|
||||||
Subject = "",
|
Subject = "",
|
||||||
BodyHtml = "",
|
BodyHtml = "",
|
||||||
};
|
};
|
||||||
@@ -446,6 +452,15 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var loginPatternError = ValidateTemplateLoginPatterns();
|
||||||
|
if (loginPatternError != null)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, loginPatternError, "BulkArchiveMailer", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
tabControl.SelectedTab = tabTemplates;
|
||||||
|
lstTemplates.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var selectedAccount = cboFromAccount.SelectedItem as FromAccountItem;
|
var selectedAccount = cboFromAccount.SelectedItem as FromAccountItem;
|
||||||
|
|
||||||
var data = new AppSettingsData
|
var data = new AppSettingsData
|
||||||
@@ -502,6 +517,41 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Every template's LoginPattern (if non-empty) must compile as a
|
||||||
|
/// .NET regular expression - ArchiveScanner treats an uncompilable
|
||||||
|
/// pattern defensively as "never matches" rather than throwing, but
|
||||||
|
/// that would silently turn a typo'd filter into "this template
|
||||||
|
/// never applies to anything", which is worth catching here instead
|
||||||
|
/// while the operator still has the exact field in front of them.
|
||||||
|
/// Returns null if every configured pattern is either empty or
|
||||||
|
/// compiles, otherwise a message naming the offending template and
|
||||||
|
/// the underlying regex error.
|
||||||
|
/// </summary>
|
||||||
|
private string ValidateTemplateLoginPatterns()
|
||||||
|
{
|
||||||
|
for (var i = 0; i < templates.Count; i++)
|
||||||
|
{
|
||||||
|
var pattern = templates[i].LoginPattern;
|
||||||
|
if (string.IsNullOrEmpty(pattern))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = new Regex(pattern);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
var name = string.IsNullOrWhiteSpace(templates[i].DisplayName) ? $"#{i + 1}" : templates[i].DisplayName;
|
||||||
|
return $"У шаблона \"{name}\" некорректное регулярное выражение в поле \"Фильтр логина\" (\"{pattern}\"): {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void btnCancel_Click(object sender, EventArgs e)
|
private void btnCancel_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
DialogResult = DialogResult.Cancel;
|
DialogResult = DialogResult.Cancel;
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using BulkArchiveMailer.Core;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BulkArchiveMailer.Core.Tests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ArchiveScanner.Scan touches the real file system (Directory.Exists/
|
||||||
|
/// EnumerateFiles), so these tests set up a throwaway folder under
|
||||||
|
/// Path.GetTempPath() per test and delete it afterwards (IDisposable),
|
||||||
|
/// rather than mocking - the existing precedent in this test project
|
||||||
|
/// (SendLogStoreTests-equivalent file-based tests) already treats this
|
||||||
|
/// as an acceptable pattern for .NET file-system-touching code.
|
||||||
|
/// </summary>
|
||||||
|
public class ArchiveScannerTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string tempDir;
|
||||||
|
|
||||||
|
public ArchiveScannerTests()
|
||||||
|
{
|
||||||
|
tempDir = Path.Combine(Path.GetTempPath(), "BulkArchiveMailerTests_" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Directory.Exists(tempDir))
|
||||||
|
{
|
||||||
|
Directory.Delete(tempDir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Best-effort cleanup only - never fail a passing test over
|
||||||
|
// a locked/leftover temp file.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteArchive(string path) => File.WriteAllBytes(path, new byte[] { 1, 2, 3 });
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_LoginMatchesTemplateLoginPattern_IsReady()
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(tempDir, "a-makarov_ssh_access.7z");
|
||||||
|
WriteArchive(filePath);
|
||||||
|
|
||||||
|
var templates = new List<MailTemplateDefinition>
|
||||||
|
{
|
||||||
|
new MailTemplateDefinition
|
||||||
|
{
|
||||||
|
DisplayName = "SSH-доступ (только a-*)",
|
||||||
|
FileSuffix = "ssh_access",
|
||||||
|
Subject = "Доступ — {Login}",
|
||||||
|
BodyHtml = "<p>{Login}</p>",
|
||||||
|
LoginPattern = "^a-.*",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = ArchiveScanner.Scan(tempDir, "redlab.lo", templates);
|
||||||
|
|
||||||
|
var job = Assert.Single(result.Jobs);
|
||||||
|
Assert.Equal(SendStatus.Ready, job.Status);
|
||||||
|
Assert.Equal("a-makarov", job.Login);
|
||||||
|
Assert.Equal("a.makarov@redlab.lo", job.Email);
|
||||||
|
Assert.Empty(result.UnrecognizedFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_LoginDoesNotMatchTemplateLoginPattern_IsErrorInUnrecognized()
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(tempDir, "b-petrov_ssh_access.7z");
|
||||||
|
WriteArchive(filePath);
|
||||||
|
|
||||||
|
var templates = new List<MailTemplateDefinition>
|
||||||
|
{
|
||||||
|
new MailTemplateDefinition
|
||||||
|
{
|
||||||
|
DisplayName = "SSH-доступ (только a-*)",
|
||||||
|
FileSuffix = "ssh_access",
|
||||||
|
Subject = "Доступ — {Login}",
|
||||||
|
BodyHtml = "<p>{Login}</p>",
|
||||||
|
LoginPattern = "^a-.*",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = ArchiveScanner.Scan(tempDir, "redlab.lo", templates);
|
||||||
|
|
||||||
|
Assert.Empty(result.Jobs);
|
||||||
|
var row = Assert.Single(result.UnrecognizedFiles);
|
||||||
|
Assert.Equal(SendStatus.Error, row.Status);
|
||||||
|
Assert.Contains("b-petrov", row.ErrorMessage);
|
||||||
|
Assert.Contains("SSH-доступ (только a-*)", row.ErrorMessage);
|
||||||
|
Assert.Contains("^a-.*", row.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_EmptyLoginPattern_MatchesAnyLogin_UnchangedLegacyBehavior()
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(tempDir, "z-unrelated_ssh_access.7z");
|
||||||
|
WriteArchive(filePath);
|
||||||
|
|
||||||
|
var templates = new List<MailTemplateDefinition>
|
||||||
|
{
|
||||||
|
new MailTemplateDefinition
|
||||||
|
{
|
||||||
|
DisplayName = "SSH-доступ",
|
||||||
|
FileSuffix = "ssh_access",
|
||||||
|
Subject = "Доступ — {Login}",
|
||||||
|
BodyHtml = "<p>{Login}</p>",
|
||||||
|
LoginPattern = "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = ArchiveScanner.Scan(tempDir, "redlab.lo", templates);
|
||||||
|
|
||||||
|
var job = Assert.Single(result.Jobs);
|
||||||
|
Assert.Equal(SendStatus.Ready, job.Status);
|
||||||
|
Assert.Equal("z-unrelated", job.Login);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Scan_InvalidLoginPatternRegex_TreatedAsNoMatchRatherThanThrowing()
|
||||||
|
{
|
||||||
|
var filePath = Path.Combine(tempDir, "a-makarov_ssh_access.7z");
|
||||||
|
WriteArchive(filePath);
|
||||||
|
|
||||||
|
var templates = new List<MailTemplateDefinition>
|
||||||
|
{
|
||||||
|
new MailTemplateDefinition
|
||||||
|
{
|
||||||
|
DisplayName = "SSH-доступ (битый regex)",
|
||||||
|
FileSuffix = "ssh_access",
|
||||||
|
Subject = "Доступ — {Login}",
|
||||||
|
BodyHtml = "<p>{Login}</p>",
|
||||||
|
// Unbalanced group - not a valid regex.
|
||||||
|
LoginPattern = "(a-",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = ArchiveScanner.Scan(tempDir, "redlab.lo", templates);
|
||||||
|
|
||||||
|
Assert.Empty(result.Jobs);
|
||||||
|
var row = Assert.Single(result.UnrecognizedFiles);
|
||||||
|
Assert.Equal(SendStatus.Error, row.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,10 @@ namespace BulkArchiveMailer.Core
|
|||||||
|
|
||||||
[DataMember(Name = "bodyHtml")]
|
[DataMember(Name = "bodyHtml")]
|
||||||
public string BodyHtml { get; set; } = "";
|
public string BodyHtml { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>See MailTemplateDefinition.LoginPattern. Absent from any config.json written before v1.2.0 - DataContractJsonSerializer leaves this null (not "") for those, same caveat as every other string member here; ToTemplateList/SetTemplates and every call site normalize via `?? ""`.</summary>
|
||||||
|
[DataMember(Name = "loginPattern")]
|
||||||
|
public string LoginPattern { get; set; } = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Shape of %APPDATA%\BulkArchiveMailer\config.json (and of the embedded config.default.json that seeds it).</summary>
|
/// <summary>Shape of %APPDATA%\BulkArchiveMailer\config.json (and of the embedded config.default.json that seeds it).</summary>
|
||||||
@@ -95,6 +99,7 @@ namespace BulkArchiveMailer.Core
|
|||||||
FileSuffix = t?.FileSuffix ?? "",
|
FileSuffix = t?.FileSuffix ?? "",
|
||||||
Subject = t?.Subject ?? "",
|
Subject = t?.Subject ?? "",
|
||||||
BodyHtml = t?.BodyHtml ?? "",
|
BodyHtml = t?.BodyHtml ?? "",
|
||||||
|
LoginPattern = t?.LoginPattern ?? "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +120,7 @@ namespace BulkArchiveMailer.Core
|
|||||||
FileSuffix = t?.FileSuffix ?? "",
|
FileSuffix = t?.FileSuffix ?? "",
|
||||||
Subject = t?.Subject ?? "",
|
Subject = t?.Subject ?? "",
|
||||||
BodyHtml = t?.BodyHtml ?? "",
|
BodyHtml = t?.BodyHtml ?? "",
|
||||||
|
LoginPattern = t?.LoginPattern ?? "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,6 +386,11 @@ namespace BulkArchiveMailer.Core
|
|||||||
FileSuffix = "ssh_access",
|
FileSuffix = "ssh_access",
|
||||||
Subject = legacy?.Templates?.SshAccess?.Subject ?? "",
|
Subject = legacy?.Templates?.SshAccess?.Subject ?? "",
|
||||||
BodyHtml = legacy?.Templates?.SshAccess?.BodyHtml ?? "",
|
BodyHtml = legacy?.Templates?.SshAccess?.BodyHtml ?? "",
|
||||||
|
// The legacy shape never had a per-template login
|
||||||
|
// filter - migrated entries keep the old
|
||||||
|
// unrestricted "suffix alone decides the match"
|
||||||
|
// behavior.
|
||||||
|
LoginPattern = "",
|
||||||
},
|
},
|
||||||
new MailTemplateData
|
new MailTemplateData
|
||||||
{
|
{
|
||||||
@@ -387,6 +398,7 @@ namespace BulkArchiveMailer.Core
|
|||||||
FileSuffix = "config_update",
|
FileSuffix = "config_update",
|
||||||
Subject = legacy?.Templates?.ConfigUpdate?.Subject ?? "",
|
Subject = legacy?.Templates?.ConfigUpdate?.Subject ?? "",
|
||||||
BodyHtml = legacy?.Templates?.ConfigUpdate?.BodyHtml ?? "",
|
BodyHtml = legacy?.Templates?.ConfigUpdate?.BodyHtml ?? "",
|
||||||
|
LoginPattern = "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace BulkArchiveMailer.Core
|
namespace BulkArchiveMailer.Core
|
||||||
{
|
{
|
||||||
@@ -72,6 +73,25 @@ namespace BulkArchiveMailer.Core
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(matchedTemplate.LoginPattern) && !LoginMatchesPattern(parsed.Login, matchedTemplate.LoginPattern))
|
||||||
|
{
|
||||||
|
// Suffix matched, but the operator scoped this template
|
||||||
|
// to a subset of logins (e.g. a per-team/per-suffix
|
||||||
|
// custom template) and this file's login falls outside
|
||||||
|
// it - surfaced next to the other per-file
|
||||||
|
// "recognized but does not qualify" case (suffix
|
||||||
|
// mismatch above) rather than silently picking a
|
||||||
|
// different template or falling through to
|
||||||
|
// byLogin/Ready.
|
||||||
|
result.UnrecognizedFiles.Add(new SendJob
|
||||||
|
{
|
||||||
|
ArchivePath = filePath,
|
||||||
|
Status = SendStatus.Error,
|
||||||
|
ErrorMessage = $"Логин \"{parsed.Login}\" не соответствует фильтру шаблона \"{matchedTemplate.DisplayName}\" ({matchedTemplate.LoginPattern}): {fileName}",
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!byLogin.TryGetValue(parsed.Login, out var list))
|
if (!byLogin.TryGetValue(parsed.Login, out var list))
|
||||||
{
|
{
|
||||||
list = new List<(string, string)>();
|
list = new List<(string, string)>();
|
||||||
@@ -151,5 +171,25 @@ namespace BulkArchiveMailer.Core
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether login satisfies the template's LoginPattern regex
|
||||||
|
/// (case-insensitive). An invalid pattern (e.g. hand-edited
|
||||||
|
/// config.json with broken regex syntax) is treated as "does not
|
||||||
|
/// match" rather than throwing and aborting the whole folder scan -
|
||||||
|
/// SettingsForm.btnSave_Click is where a bad pattern is meant to be
|
||||||
|
/// caught, before it ever reaches disk.
|
||||||
|
/// </summary>
|
||||||
|
private static bool LoginMatchesPattern(string login, string pattern)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Regex.IsMatch(login, pattern, RegexOptions.IgnoreCase);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,5 +30,19 @@ namespace BulkArchiveMailer.Core
|
|||||||
public string Subject { get; set; } = "";
|
public string Subject { get; set; } = "";
|
||||||
|
|
||||||
public string BodyHtml { get; set; } = "";
|
public string BodyHtml { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional regular expression the parsed login (from
|
||||||
|
/// "{login}_{FileSuffix}.7z") must match (case-insensitively) for
|
||||||
|
/// this template to actually apply, checked by ArchiveScanner AFTER
|
||||||
|
/// the file's suffix has already matched this template. Empty (the
|
||||||
|
/// default) means "no restriction" - the original, pre-v1.2.0
|
||||||
|
/// behavior where suffix alone decides the match. Validated for
|
||||||
|
/// compilability by SettingsForm at save time; ArchiveScanner
|
||||||
|
/// itself treats an uncompilable pattern defensively as "does not
|
||||||
|
/// match" rather than throwing, in case an operator hand-edits
|
||||||
|
/// config.json with a broken regex.
|
||||||
|
/// </summary>
|
||||||
|
public string LoginPattern { get; set; } = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,13 +9,15 @@
|
|||||||
"displayName": "Новый доступ (ключ + конфиг)",
|
"displayName": "Новый доступ (ключ + конфиг)",
|
||||||
"fileSuffix": "ssh_access",
|
"fileSuffix": "ssh_access",
|
||||||
"subject": "Доступ к SSH jump-хосту — {Login}",
|
"subject": "Доступ к SSH jump-хосту — {Login}",
|
||||||
"bodyHtml": "<p>Здравствуйте!</p><p>Во вложении — архив с вашим персональным SSH-ключом, готовым клиентским конфигом и краткой инструкцией по подключению (<b>{Login}_ssh_access.7z</b>).</p><p>Пароль от архива будет отправлен отдельным сообщением — не пересылайте архив вместе с паролем.</p><p>Если по какой-то причине не получается распаковать архив или подключиться — напишите в ответ на это письмо.</p>"
|
"bodyHtml": "<p>Здравствуйте!</p><p>Во вложении — архив с вашим персональным SSH-ключом, готовым клиентским конфигом и краткой инструкцией по подключению (<b>{Login}_ssh_access.7z</b>).</p><p>Пароль от архива будет отправлен отдельным сообщением — не пересылайте архив вместе с паролем.</p><p>Если по какой-то причине не получается распаковать архив или подключиться — напишите в ответ на это письмо.</p>",
|
||||||
|
"loginPattern": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"displayName": "Обновление конфига",
|
"displayName": "Обновление конфига",
|
||||||
"fileSuffix": "config_update",
|
"fileSuffix": "config_update",
|
||||||
"subject": "Обновление конфигурации доступа — {Login}",
|
"subject": "Обновление конфигурации доступа — {Login}",
|
||||||
"bodyHtml": "<p>Здравствуйте!</p><p>Список доступных вам через jump-хост сервисов изменился. Во вложении — обновлённый клиентский конфиг и инструкция (<b>{Login}_config_update.7z</b>). Ваш существующий SSH-ключ по-прежнему действителен, переустанавливать его не нужно — достаточно заменить конфиг.</p><p>Пароль от архива будет отправлен отдельным сообщением — не пересылайте архив вместе с паролем.</p>"
|
"bodyHtml": "<p>Здравствуйте!</p><p>Список доступных вам через jump-хост сервисов изменился. Во вложении — обновлённый клиентский конфиг и инструкция (<b>{Login}_config_update.7z</b>). Ваш существующий SSH-ключ по-прежнему действителен, переустанавливать его не нужно — достаточно заменить конфиг.</p><p>Пароль от архива будет отправлен отдельным сообщением — не пересылайте архив вместе с паролем.</p>",
|
||||||
|
"loginPattern": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ BulkArchiveMailer.Core/ net48, class library, БЕЗ ссылок н
|
|||||||
FilenameParser.cs парсинг "{login}_{суффикс}.7z" (суффикс - любой, сверяется с шаблонами в ArchiveScanner)
|
FilenameParser.cs парсинг "{login}_{суффикс}.7z" (суффикс - любой, сверяется с шаблонами в ArchiveScanner)
|
||||||
EmailAddressBuilder.cs login -> "login.with.dots@domain"
|
EmailAddressBuilder.cs login -> "login.with.dots@domain"
|
||||||
SendJob.cs модель строки предпросмотра (Login/Email/Kind/ArchivePath/…/Status)
|
SendJob.cs модель строки предпросмотра (Login/Email/Kind/ArchivePath/…/Status)
|
||||||
ArchiveScanner.cs сканирование папки, сопоставление суффикса с шаблоном, конфликт "оба архива сразу"
|
ArchiveScanner.cs сканирование папки, сопоставление суффикса (и необязательного regex-фильтра логина) с шаблоном, конфликт "оба архива сразу"
|
||||||
TemplateEngine.cs подстановка {Login}/{Email}/{Date} в шаблон
|
TemplateEngine.cs подстановка {Login}/{Email}/{Date} в шаблон
|
||||||
MailTemplateDefinition.cs один шаблон (название, суффикс файла, тема, HTML-тело) - список произвольной длины
|
MailTemplateDefinition.cs один шаблон (название, суффикс файла, тема, HTML-тело) - список произвольной длины
|
||||||
SendLogStore.cs CSV-журнал отправок + дедуп по (Email, Kind, SHA-256 архива)
|
SendLogStore.cs CSV-журнал отправок + дедуп по (Email, Kind, SHA-256 архива)
|
||||||
@@ -96,13 +96,25 @@ Outlook в принципе не запустить.
|
|||||||
- **Шаблоны писем** — вкладка "Шаблоны" со списком произвольного числа
|
- **Шаблоны писем** — вкладка "Шаблоны" со списком произвольного числа
|
||||||
шаблонов (кнопки "+ Добавить"/"Удалить"). Каждый шаблон — это название,
|
шаблонов (кнопки "+ Добавить"/"Удалить"). Каждый шаблон — это название,
|
||||||
суффикс имени файла (`{login}_<суффикс>.7z`, по которому шаблон подбирается
|
суффикс имени файла (`{login}_<суффикс>.7z`, по которому шаблон подбирается
|
||||||
для конкретного архива), тема и HTML-тело с плейсхолдерами `{Login}`,
|
для конкретного архива), необязательный фильтр логина (regex) и тема с
|
||||||
`{Email}`, `{Date}`. Суффикс обязателен и должен быть уникальным среди всех
|
HTML-телом с плейсхолдерами `{Login}`, `{Email}`, `{Date}`. Суффикс
|
||||||
шаблонов — по нему, а не по фиксированному перечислению видов архива,
|
обязателен и должен быть уникальным среди всех шаблонов — по нему, а не по
|
||||||
`ArchiveScanner` теперь сопоставляет файл с шаблоном. Два исходных шаблона
|
фиксированному перечислению видов архива, `ArchiveScanner` сопоставляет
|
||||||
(`ssh_access`/`config_update`) — обычные равноправные записи в этом
|
файл с шаблоном. Два исходных шаблона (`ssh_access`/`config_update`) —
|
||||||
списке, без особой защиты от переименования/удаления; кнопка "Сбросить
|
обычные равноправные записи в этом списке, без особой защиты от
|
||||||
шаблоны по умолчанию" восстанавливает их, если они были случайно удалены.
|
переименования/удаления; кнопка "Сбросить шаблоны по умолчанию"
|
||||||
|
восстанавливает их, если они были случайно удалены.
|
||||||
|
- **Фильтр логина** — необязательное регулярное выражение (.NET regex,
|
||||||
|
без учёта регистра). Пустое значение (по умолчанию, в том числе у
|
||||||
|
шаблонов, мигрированных из конфига старого формата) означает "любой
|
||||||
|
логин" — прежнее поведение, когда только суффикс решает, подходит ли
|
||||||
|
файл под шаблон. Если поле заполнено, а логин из имени файла ему не
|
||||||
|
соответствует, файл не считается подходящим под этот шаблон: строка в
|
||||||
|
предпросмотре получает статус "Ошибка" с пояснением, какому шаблону и
|
||||||
|
какому паттерну логин не соответствует, — автоматический подбор другого
|
||||||
|
шаблона не выполняется. Некорректный regex (не компилируется) не даёт
|
||||||
|
сохранить настройки — окно "Настройки" укажет, в каком именно шаблоне
|
||||||
|
ошибка.
|
||||||
- **Тестовый режим по умолчанию** — включён по умолчанию (письма сохраняются
|
- **Тестовый режим по умолчанию** — включён по умолчанию (письма сохраняются
|
||||||
в Черновики, а не отправляются) до первого осознанного отключения
|
в Черновики, а не отправляются) до первого осознанного отключения
|
||||||
оператором в окне предпросмотра.
|
оператором в окне предпросмотра.
|
||||||
|
|||||||
Reference in new issue
Block a user