Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1c67669fd | ||
|
|
6e823954a3 |
No files matched your search
+117
-34
@@ -35,6 +35,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
private System.Windows.Forms.Label lblFromAccount;
|
||||
private System.Windows.Forms.ComboBox cboFromAccount;
|
||||
|
||||
private System.Windows.Forms.Panel pnlTemplateList;
|
||||
private System.Windows.Forms.Panel pnlTemplateButtons;
|
||||
private System.Windows.Forms.ListBox lstTemplates;
|
||||
private System.Windows.Forms.Button btnAddTemplate;
|
||||
private System.Windows.Forms.Button btnRemoveTemplate;
|
||||
@@ -44,6 +46,9 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
private System.Windows.Forms.Label lblTplFileSuffix;
|
||||
private System.Windows.Forms.TextBox txtTplFileSuffix;
|
||||
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.TextBox txtTplSubject;
|
||||
private System.Windows.Forms.Label lblTplBody;
|
||||
@@ -77,6 +82,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
this.lblFromAccount = new System.Windows.Forms.Label();
|
||||
this.cboFromAccount = new System.Windows.Forms.ComboBox();
|
||||
|
||||
this.pnlTemplateList = new System.Windows.Forms.Panel();
|
||||
this.pnlTemplateButtons = new System.Windows.Forms.Panel();
|
||||
this.lstTemplates = new System.Windows.Forms.ListBox();
|
||||
this.btnAddTemplate = new System.Windows.Forms.Button();
|
||||
this.btnRemoveTemplate = new System.Windows.Forms.Button();
|
||||
@@ -86,6 +93,9 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
this.lblTplFileSuffix = new System.Windows.Forms.Label();
|
||||
this.txtTplFileSuffix = new System.Windows.Forms.TextBox();
|
||||
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.txtTplSubject = new System.Windows.Forms.TextBox();
|
||||
this.lblTplBody = new System.Windows.Forms.Label();
|
||||
@@ -99,13 +109,20 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
this.tabControl.SuspendLayout();
|
||||
this.tabGeneral.SuspendLayout();
|
||||
this.tabTemplates.SuspendLayout();
|
||||
this.pnlTemplateList.SuspendLayout();
|
||||
this.pnlTemplateButtons.SuspendLayout();
|
||||
this.SuspendLayout();
|
||||
|
||||
//
|
||||
// tabControl
|
||||
//
|
||||
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)
|
||||
| System.Windows.Forms.AnchorStyles.Left)
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
@@ -149,50 +166,50 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
this.btnBrowseFolder.Click += new System.EventHandler(this.btnBrowseFolder_Click);
|
||||
|
||||
// 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.Text = "Домен электронной почты (login → login@домен):";
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.Text = "Файл подписи (.htm). Пусто = определять автоматически по умолчанию Outlook при каждой отправке:";
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.Text = "Обзор...";
|
||||
this.btnBrowseSignature.UseVisualStyleBackColor = true;
|
||||
this.btnBrowseSignature.Click += new System.EventHandler(this.btnBrowseSignature_Click);
|
||||
|
||||
// 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.Text = "Определить автоматически сейчас";
|
||||
this.btnAutoDetectSignature.UseVisualStyleBackColor = true;
|
||||
this.btnAutoDetectSignature.Click += new System.EventHandler(this.btnAutoDetectSignature_Click);
|
||||
|
||||
// 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.Text = "По умолчанию открывать предпросмотр в тестовом режиме (сохранять в Черновики)";
|
||||
this.chkTestModeDefault.UseVisualStyleBackColor = true;
|
||||
|
||||
// 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.Text = "Отправлять от (аккаунт Outlook текущей сессии):";
|
||||
|
||||
// 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.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
|
||||
@@ -202,44 +219,91 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
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.pnlTemplateList);
|
||||
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.lblTplLoginPattern);
|
||||
this.tabTemplates.Controls.Add(this.txtTplLoginPattern);
|
||||
this.tabTemplates.Controls.Add(this.lblTplLoginPatternHint);
|
||||
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);
|
||||
|
||||
// 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)
|
||||
// pnlTemplateList
|
||||
//
|
||||
// Left-column container for the template picker (buttons +
|
||||
// list). Its own outer Location/Size/Anchor still use fixed
|
||||
// pixel coordinates relative to the tab page, exactly like
|
||||
// before - that outer placement was never the problem. What
|
||||
// is Dock-based from here down is everything INSIDE this
|
||||
// panel, which is the part that actually broke on a real
|
||||
// machine: lstTemplates used to be a direct child of
|
||||
// tabTemplates with an explicit Size, so its bottom edge (and
|
||||
// therefore its scrollability/clickability once many
|
||||
// templates were added) depended on a hand-computed pixel
|
||||
// height that was only ever verified on the dev machine's own
|
||||
// DPI/font scale. Docking removes that computation entirely -
|
||||
// see pnlTemplateButtons/lstTemplates below.
|
||||
this.pnlTemplateList.Location = new System.Drawing.Point(15, 20);
|
||||
this.pnlTemplateList.Size = new System.Drawing.Size(190, 467);
|
||||
this.pnlTemplateList.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);
|
||||
// Dock order matters here: pnlTemplateButtons (Dock=Top) is
|
||||
// added FIRST, lstTemplates (Dock=Fill) SECOND, so the Fill
|
||||
// control's layout is computed against whatever client area
|
||||
// remains under the already-docked top strip.
|
||||
this.pnlTemplateList.Controls.Add(this.pnlTemplateButtons);
|
||||
this.pnlTemplateList.Controls.Add(this.lstTemplates);
|
||||
|
||||
// pnlTemplateButtons
|
||||
//
|
||||
// Fixed-height strip pinned to the top of pnlTemplateList.
|
||||
// btnAddTemplate/btnRemoveTemplate live inside it at a fixed
|
||||
// offset from ITS top-left corner (0,0)/(95,0) - a small,
|
||||
// never-resized container, so plain Location placement here
|
||||
// carries none of the earlier risk (that risk only existed
|
||||
// when the same fixed coordinates were relative to the whole,
|
||||
// variable-height tab page).
|
||||
this.pnlTemplateButtons.Dock = System.Windows.Forms.DockStyle.Top;
|
||||
this.pnlTemplateButtons.Size = new System.Drawing.Size(190, 35);
|
||||
this.pnlTemplateButtons.Controls.Add(this.btnAddTemplate);
|
||||
this.pnlTemplateButtons.Controls.Add(this.btnRemoveTemplate);
|
||||
|
||||
// btnAddTemplate
|
||||
this.btnAddTemplate.Location = new System.Drawing.Point(15, 355);
|
||||
this.btnAddTemplate.Location = new System.Drawing.Point(0, 0);
|
||||
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.Location = new System.Drawing.Point(95, 0);
|
||||
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);
|
||||
|
||||
// lstTemplates
|
||||
//
|
||||
// Dock=Fill: always occupies exactly whatever area of
|
||||
// pnlTemplateList is left below pnlTemplateButtons, computed
|
||||
// by the layout engine at every resize/DPI - not a hand-picked
|
||||
// pixel height that can end up too short (and therefore
|
||||
// effectively unscrollable/unclickable for items past the cut
|
||||
// -off) on a machine with different DPI/font scaling than the
|
||||
// one this was last tested on. IntegralHeight = false just
|
||||
// means the list doesn't force its height to a whole multiple
|
||||
// of the item height - irrelevant to whether Fill can "escape"
|
||||
// the panel, which it structurally cannot.
|
||||
this.lstTemplates.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.lstTemplates.IntegralHeight = false;
|
||||
this.lstTemplates.SelectedIndexChanged += new System.EventHandler(this.lstTemplates_SelectedIndexChanged);
|
||||
|
||||
// lblTplDisplayName
|
||||
this.lblTplDisplayName.Location = new System.Drawing.Point(220, 20);
|
||||
this.lblTplDisplayName.Size = new System.Drawing.Size(200, 20);
|
||||
@@ -267,25 +331,42 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
this.lblTplFileSuffixHint.Text = "Файл: {login}_<суффикс>.7z";
|
||||
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
|
||||
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.Text = "Тема письма:";
|
||||
|
||||
// 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.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.Location = new System.Drawing.Point(220, 312);
|
||||
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.Location = new System.Drawing.Point(220, 335);
|
||||
this.txtTplBody.Size = new System.Drawing.Size(410, 125);
|
||||
this.txtTplBody.Multiline = true;
|
||||
this.txtTplBody.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
|
||||
this.txtTplBody.AcceptsReturn = true;
|
||||
@@ -294,7 +375,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
| System.Windows.Forms.AnchorStyles.Right)));
|
||||
|
||||
// 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.Text = "Доступные плейсхолдеры: {Login}, {Email}, {Date}";
|
||||
this.lblTplHint.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
@@ -303,14 +384,14 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
//
|
||||
// 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.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);
|
||||
|
||||
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.Text = "Отмена";
|
||||
this.btnCancel.UseVisualStyleBackColor = true;
|
||||
@@ -318,7 +399,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
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.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.Text = "Сохранить";
|
||||
this.btnSave.UseVisualStyleBackColor = true;
|
||||
@@ -330,8 +411,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(684, 536);
|
||||
this.MinimumSize = new System.Drawing.Size(560, 420);
|
||||
this.ClientSize = new System.Drawing.Size(684, 616);
|
||||
this.MinimumSize = new System.Drawing.Size(560, 500);
|
||||
this.Controls.Add(this.tabControl);
|
||||
this.Controls.Add(this.btnResetDefaults);
|
||||
this.Controls.Add(this.btnCancel);
|
||||
@@ -346,6 +427,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
this.tabControl.ResumeLayout(false);
|
||||
this.tabGeneral.ResumeLayout(false);
|
||||
this.tabGeneral.PerformLayout();
|
||||
this.pnlTemplateButtons.ResumeLayout(false);
|
||||
this.pnlTemplateList.ResumeLayout(false);
|
||||
this.tabTemplates.ResumeLayout(false);
|
||||
this.tabTemplates.PerformLayout();
|
||||
this.ResumeLayout(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows.Forms;
|
||||
using BulkArchiveMailer.Core;
|
||||
using Outlook = Microsoft.Office.Interop.Outlook;
|
||||
@@ -194,6 +195,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
var t = templates[editedIndex];
|
||||
t.DisplayName = txtTplDisplayName.Text;
|
||||
t.FileSuffix = txtTplFileSuffix.Text.Trim();
|
||||
t.LoginPattern = txtTplLoginPattern.Text.Trim();
|
||||
t.Subject = txtTplSubject.Text;
|
||||
t.BodyHtml = txtTplBody.Text;
|
||||
}
|
||||
@@ -206,6 +208,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
|
||||
txtTplDisplayName.Enabled = hasSelection;
|
||||
txtTplFileSuffix.Enabled = hasSelection;
|
||||
txtTplLoginPattern.Enabled = hasSelection;
|
||||
txtTplSubject.Enabled = hasSelection;
|
||||
txtTplBody.Enabled = hasSelection;
|
||||
|
||||
@@ -214,6 +217,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
lstTemplates.Tag = -1;
|
||||
txtTplDisplayName.Text = "";
|
||||
txtTplFileSuffix.Text = "";
|
||||
txtTplLoginPattern.Text = "";
|
||||
txtTplSubject.Text = "";
|
||||
txtTplBody.Text = "";
|
||||
return;
|
||||
@@ -223,6 +227,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
lstTemplates.Tag = index;
|
||||
txtTplDisplayName.Text = t.DisplayName;
|
||||
txtTplFileSuffix.Text = t.FileSuffix;
|
||||
txtTplLoginPattern.Text = t.LoginPattern;
|
||||
txtTplSubject.Text = t.Subject;
|
||||
txtTplBody.Text = t.BodyHtml;
|
||||
}
|
||||
@@ -265,6 +270,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
{
|
||||
DisplayName = "Новый шаблон",
|
||||
FileSuffix = "",
|
||||
LoginPattern = "",
|
||||
Subject = "",
|
||||
BodyHtml = "",
|
||||
};
|
||||
@@ -446,6 +452,15 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
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 data = new AppSettingsData
|
||||
@@ -502,6 +517,41 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
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)
|
||||
{
|
||||
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")]
|
||||
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>
|
||||
@@ -95,6 +99,7 @@ namespace BulkArchiveMailer.Core
|
||||
FileSuffix = t?.FileSuffix ?? "",
|
||||
Subject = t?.Subject ?? "",
|
||||
BodyHtml = t?.BodyHtml ?? "",
|
||||
LoginPattern = t?.LoginPattern ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -115,6 +120,7 @@ namespace BulkArchiveMailer.Core
|
||||
FileSuffix = t?.FileSuffix ?? "",
|
||||
Subject = t?.Subject ?? "",
|
||||
BodyHtml = t?.BodyHtml ?? "",
|
||||
LoginPattern = t?.LoginPattern ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -380,6 +386,11 @@ namespace BulkArchiveMailer.Core
|
||||
FileSuffix = "ssh_access",
|
||||
Subject = legacy?.Templates?.SshAccess?.Subject ?? "",
|
||||
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
|
||||
{
|
||||
@@ -387,6 +398,7 @@ namespace BulkArchiveMailer.Core
|
||||
FileSuffix = "config_update",
|
||||
Subject = legacy?.Templates?.ConfigUpdate?.Subject ?? "",
|
||||
BodyHtml = legacy?.Templates?.ConfigUpdate?.BodyHtml ?? "",
|
||||
LoginPattern = "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace BulkArchiveMailer.Core
|
||||
{
|
||||
@@ -72,6 +73,25 @@ namespace BulkArchiveMailer.Core
|
||||
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))
|
||||
{
|
||||
list = new List<(string, string)>();
|
||||
@@ -151,5 +171,25 @@ namespace BulkArchiveMailer.Core
|
||||
|
||||
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 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": "Новый доступ (ключ + конфиг)",
|
||||
"fileSuffix": "ssh_access",
|
||||
"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": "Обновление конфига",
|
||||
"fileSuffix": "config_update",
|
||||
"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)
|
||||
EmailAddressBuilder.cs login -> "login.with.dots@domain"
|
||||
SendJob.cs модель строки предпросмотра (Login/Email/Kind/ArchivePath/…/Status)
|
||||
ArchiveScanner.cs сканирование папки, сопоставление суффикса с шаблоном, конфликт "оба архива сразу"
|
||||
ArchiveScanner.cs сканирование папки, сопоставление суффикса (и необязательного regex-фильтра логина) с шаблоном, конфликт "оба архива сразу"
|
||||
TemplateEngine.cs подстановка {Login}/{Email}/{Date} в шаблон
|
||||
MailTemplateDefinition.cs один шаблон (название, суффикс файла, тема, HTML-тело) - список произвольной длины
|
||||
SendLogStore.cs CSV-журнал отправок + дедуп по (Email, Kind, SHA-256 архива)
|
||||
@@ -96,13 +96,25 @@ Outlook в принципе не запустить.
|
||||
- **Шаблоны писем** — вкладка "Шаблоны" со списком произвольного числа
|
||||
шаблонов (кнопки "+ Добавить"/"Удалить"). Каждый шаблон — это название,
|
||||
суффикс имени файла (`{login}_<суффикс>.7z`, по которому шаблон подбирается
|
||||
для конкретного архива), тема и HTML-тело с плейсхолдерами `{Login}`,
|
||||
`{Email}`, `{Date}`. Суффикс обязателен и должен быть уникальным среди всех
|
||||
шаблонов — по нему, а не по фиксированному перечислению видов архива,
|
||||
`ArchiveScanner` теперь сопоставляет файл с шаблоном. Два исходных шаблона
|
||||
(`ssh_access`/`config_update`) — обычные равноправные записи в этом
|
||||
списке, без особой защиты от переименования/удаления; кнопка "Сбросить
|
||||
шаблоны по умолчанию" восстанавливает их, если они были случайно удалены.
|
||||
для конкретного архива), необязательный фильтр логина (regex) и тема с
|
||||
HTML-телом с плейсхолдерами `{Login}`, `{Email}`, `{Date}`. Суффикс
|
||||
обязателен и должен быть уникальным среди всех шаблонов — по нему, а не по
|
||||
фиксированному перечислению видов архива, `ArchiveScanner` сопоставляет
|
||||
файл с шаблоном. Два исходных шаблона (`ssh_access`/`config_update`) —
|
||||
обычные равноправные записи в этом списке, без особой защиты от
|
||||
переименования/удаления; кнопка "Сбросить шаблоны по умолчанию"
|
||||
восстанавливает их, если они были случайно удалены.
|
||||
- **Фильтр логина** — необязательное регулярное выражение (.NET regex,
|
||||
без учёта регистра). Пустое значение (по умолчанию, в том числе у
|
||||
шаблонов, мигрированных из конфига старого формата) означает "любой
|
||||
логин" — прежнее поведение, когда только суффикс решает, подходит ли
|
||||
файл под шаблон. Если поле заполнено, а логин из имени файла ему не
|
||||
соответствует, файл не считается подходящим под этот шаблон: строка в
|
||||
предпросмотре получает статус "Ошибка" с пояснением, какому шаблону и
|
||||
какому паттерну логин не соответствует, — автоматический подбор другого
|
||||
шаблона не выполняется. Некорректный regex (не компилируется) не даёт
|
||||
сохранить настройки — окно "Настройки" укажет, в каком именно шаблоне
|
||||
ошибка.
|
||||
- **Тестовый режим по умолчанию** — включён по умолчанию (письма сохраняются
|
||||
в Черновики, а не отправляются) до первого осознанного отключения
|
||||
оператором в окне предпросмотра.
|
||||
|
||||
Reference in new issue
Block a user