Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42815aa2a | ||
|
|
f1c67669fd | ||
|
|
6e823954a3 |
No files matched your search
+129
-34
@@ -35,6 +35,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
private System.Windows.Forms.Label lblFromAccount;
|
private System.Windows.Forms.Label lblFromAccount;
|
||||||
private System.Windows.Forms.ComboBox cboFromAccount;
|
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.ListBox lstTemplates;
|
||||||
private System.Windows.Forms.Button btnAddTemplate;
|
private System.Windows.Forms.Button btnAddTemplate;
|
||||||
private System.Windows.Forms.Button btnRemoveTemplate;
|
private System.Windows.Forms.Button btnRemoveTemplate;
|
||||||
@@ -44,6 +46,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;
|
||||||
@@ -77,6 +82,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
this.lblFromAccount = new System.Windows.Forms.Label();
|
this.lblFromAccount = new System.Windows.Forms.Label();
|
||||||
this.cboFromAccount = new System.Windows.Forms.ComboBox();
|
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.lstTemplates = new System.Windows.Forms.ListBox();
|
||||||
this.btnAddTemplate = new System.Windows.Forms.Button();
|
this.btnAddTemplate = new System.Windows.Forms.Button();
|
||||||
this.btnRemoveTemplate = 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.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();
|
||||||
@@ -99,13 +109,20 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
this.tabControl.SuspendLayout();
|
this.tabControl.SuspendLayout();
|
||||||
this.tabGeneral.SuspendLayout();
|
this.tabGeneral.SuspendLayout();
|
||||||
this.tabTemplates.SuspendLayout();
|
this.tabTemplates.SuspendLayout();
|
||||||
|
this.pnlTemplateList.SuspendLayout();
|
||||||
|
this.pnlTemplateButtons.SuspendLayout();
|
||||||
this.SuspendLayout();
|
this.SuspendLayout();
|
||||||
|
|
||||||
//
|
//
|
||||||
// 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 +166,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 +219,103 @@ 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.pnlTemplateList);
|
||||||
this.tabTemplates.Controls.Add(this.btnAddTemplate);
|
|
||||||
this.tabTemplates.Controls.Add(this.btnRemoveTemplate);
|
|
||||||
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);
|
||||||
|
|
||||||
// lstTemplates
|
// pnlTemplateList
|
||||||
this.lstTemplates.Location = new System.Drawing.Point(15, 20);
|
//
|
||||||
this.lstTemplates.Size = new System.Drawing.Size(190, 329);
|
// Left-column container for the template picker (buttons +
|
||||||
this.lstTemplates.IntegralHeight = false;
|
// list). Its own outer Location/Size/Anchor still use fixed
|
||||||
this.lstTemplates.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
// 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)));
|
| System.Windows.Forms.AnchorStyles.Left)));
|
||||||
this.lstTemplates.SelectedIndexChanged += new System.EventHandler(this.lstTemplates_SelectedIndexChanged);
|
// Dock order matters here, and it is the OPPOSITE of what looks
|
||||||
|
// intuitive: WinForms lays out docked children in the REVERSE
|
||||||
|
// of their Controls.Add() order - the LAST control added is
|
||||||
|
// docked FIRST (closest to the parent's edge), and each
|
||||||
|
// earlier-added control is then laid out against whatever
|
||||||
|
// client area is left over. So lstTemplates (Dock=Fill) must be
|
||||||
|
// added FIRST, and pnlTemplateButtons (Dock=Top) must be added
|
||||||
|
// SECOND/last, so that the Top strip is actually the one that
|
||||||
|
// gets docked last (i.e. first in effect) and carves its 35px
|
||||||
|
// band out of the top; Fill then only gets whatever remains
|
||||||
|
// below it. Adding them in the other order (as this used to be)
|
||||||
|
// makes Fill get laid out first, against the FULL panel client
|
||||||
|
// area (nothing has carved out the top strip yet), so it
|
||||||
|
// expands to cover the entire panel and visually overlaps
|
||||||
|
// pnlTemplateButtons - exactly the "buttons overlap the list"
|
||||||
|
// bug reported on the real machine.
|
||||||
|
this.pnlTemplateList.Controls.Add(this.lstTemplates);
|
||||||
|
this.pnlTemplateList.Controls.Add(this.pnlTemplateButtons);
|
||||||
|
|
||||||
|
// 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
|
// 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.Size = new System.Drawing.Size(90, 27);
|
||||||
this.btnAddTemplate.Text = "+ Добавить";
|
this.btnAddTemplate.Text = "+ Добавить";
|
||||||
this.btnAddTemplate.UseVisualStyleBackColor = true;
|
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);
|
this.btnAddTemplate.Click += new System.EventHandler(this.btnAddTemplate_Click);
|
||||||
|
|
||||||
// btnRemoveTemplate
|
// 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.Size = new System.Drawing.Size(90, 27);
|
||||||
this.btnRemoveTemplate.Text = "Удалить";
|
this.btnRemoveTemplate.Text = "Удалить";
|
||||||
this.btnRemoveTemplate.UseVisualStyleBackColor = true;
|
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);
|
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
|
// 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 +343,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 +387,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 +396,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 +411,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 +423,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);
|
||||||
@@ -346,6 +439,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
|||||||
this.tabControl.ResumeLayout(false);
|
this.tabControl.ResumeLayout(false);
|
||||||
this.tabGeneral.ResumeLayout(false);
|
this.tabGeneral.ResumeLayout(false);
|
||||||
this.tabGeneral.PerformLayout();
|
this.tabGeneral.PerformLayout();
|
||||||
|
this.pnlTemplateButtons.ResumeLayout(false);
|
||||||
|
this.pnlTemplateList.ResumeLayout(false);
|
||||||
this.tabTemplates.ResumeLayout(false);
|
this.tabTemplates.ResumeLayout(false);
|
||||||
this.tabTemplates.PerformLayout();
|
this.tabTemplates.PerformLayout();
|
||||||
this.ResumeLayout(false);
|
this.ResumeLayout(false);
|
||||||
|
|||||||
@@ -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