add ProxyJump function

This commit is contained in:
redlineo committed 2026-07-23 12:38:13 +03:00
1 parent 56e20e5464
commit 7f0e0fdb23
48 files changed
+3875 -44

No files matched your search

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">17.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{F0475AB7-87AA-4E6B-9BF9-1779AD59912E}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>BulkArchiveMailer.AddIn</RootNamespace>
<AssemblyName>BulkArchiveMailer.AddIn</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{BAA0C2D2-18E2-41B9-852F-F413020CAA33};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<LangVersion>7.3</LangVersion>
<!-- VSTO / Office add-in specifics -->
<OfficeApplication>Outlook</OfficeApplication>
<NoStandardLibraries>false</NoStandardLibraries>
<VstoAddInTemplateVersion>3</VstoAddInTemplateVersion>
<GenerateManifests>true</GenerateManifests>
<VSTOFrameworkVer40>true</VSTOFrameworkVer40>
<!--
Signing is deliberately OFF: this is an internal tool, sideloaded by
hand (see docs/BUILD.md) rather than published/ClickOnce/GPO
deployed, so there is no certificate to manage here. On first load
Outlook will show an "unable to verify publisher" style prompt for
the unsigned manifest - that's expected; docs/BUILD.md covers how to
either accept it or add a Trusted Location. Turning signing on for a
wider rollout is documented there as a follow-up, not implemented
here - a judgment call, since fabricating a certificate in an
environment with no Visual Studio/dotnet SDK to actually sign
anything would just be decoration.
-->
<SignAssembly>false</SignAssembly>
<SignManifests>false</SignManifests>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="Microsoft.CSharp" />
<!--
The Microsoft.Office.* / Microsoft.VisualStudio.Tools.Applications.*
references below are resolved from the GAC by the "Office/SharePoint
development" Visual Studio workload (Office Developer Tools + VSTO
Runtime), same as any project created from the built-in "Outlook VSTO
Add-in" template - no HintPath/NuGet package needed for them. If VS
reports these as missing, the workload component isn't installed
(see docs/BUILD.md).
-->
<Reference Include="Microsoft.Office.Interop.Outlook, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="office, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="stdole, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<Reference Include="Microsoft.Office.Tools, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
</Reference>
<Reference Include="Microsoft.Office.Tools.Common, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
</Reference>
<Reference Include="Microsoft.Office.Tools.Common.v4.0.Utilities, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
</Reference>
<Reference Include="Microsoft.Office.Tools.Outlook, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
</Reference>
<Reference Include="Microsoft.Office.Tools.v4.0.Framework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
</Reference>
<Reference Include="Microsoft.VisualStudio.Tools.Applications.Runtime, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
</Reference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BulkArchiveMailer.Core\BulkArchiveMailer.Core.csproj">
<Project>{96DFBF08-8BCA-4A4E-BDF8-3F013965E30E}</Project>
<Name>BulkArchiveMailer.Core</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="Globals.cs" />
<Compile Include="MailSender.cs" />
<Compile Include="RibbonController.cs" />
<Compile Include="SignatureReader.cs" />
<Compile Include="ThisAddIn.cs" />
<Compile Include="ThisAddIn.Designer.cs">
<DependentUpon>ThisAddIn.cs</DependentUpon>
</Compile>
<Compile Include="UI\SettingsForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\SettingsForm.Designer.cs">
<DependentUpon>SettingsForm.cs</DependentUpon>
</Compile>
<Compile Include="UI\PreviewForm.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="UI\PreviewForm.Designer.cs">
<DependentUpon>PreviewForm.cs</DependentUpon>
</Compile>
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Ribbon.xml" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\OfficeTools\Microsoft.VisualStudio.Tools.Office.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\OfficeTools\Microsoft.VisualStudio.Tools.Office.targets" Condition="false" />
<!--
To modify your build process, add your task inside one of the targets
below and uncomment it. Other similar extension points exist, see
Microsoft.Common.targets.
<Target Name="BeforeBuild"> </Target>
<Target Name="AfterBuild"> </Target>
-->
</Project>
@@ -0,0 +1,14 @@
namespace BulkArchiveMailer.AddIn
{
/// <summary>
/// Static access point to the running add-in instance, matching the
/// shape Visual Studio normally generates for VSTO projects (hand
/// written here for the same reason as ThisAddIn.Designer.cs - see
/// its header comment).
/// </summary>
internal static class Globals
{
internal static ThisAddIn ThisAddIn =>
(ThisAddIn)Microsoft.Office.Tools.Outlook.OutlookAddIn.GetInstance(typeof(ThisAddIn));
}
}
@@ -0,0 +1,99 @@
using System;
using System.Runtime.InteropServices;
using BulkArchiveMailer.Core;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace BulkArchiveMailer.AddIn
{
/// <summary>Per-job result of a send attempt, returned by MailSender so the caller can log + report it without any of this class's own exceptions ever escaping.</summary>
public sealed class MailSendOutcome
{
public SendJob Job { get; set; }
public bool Success { get; set; }
public string ErrorMessage { get; set; }
public bool WasDraft { get; set; }
}
/// <summary>
/// Creates and sends (or, in test mode, saves as a Draft) one Outlook
/// mail item per SendJob via Interop. This is the only file that
/// actually creates/sends a MailItem - everything else builds up to
/// calling SendOne.
///
/// SendOne never throws: every failure is caught and reported back as
/// a failed MailSendOutcome, so a caller looping over a batch can
/// always move on to the next job without a try/catch of its own.
/// </summary>
public sealed class MailSender
{
private readonly Outlook.Application application;
private readonly string signatureHtml;
/// <param name="application">Live Outlook.Application (from ThisAddIn.Application) used to create mail items.</param>
/// <param name="signatureHtml">
/// HTML of the operator's default signature, already read from
/// disk by SignatureReader (Outlook does not auto-insert a
/// signature on programmatically created items - it must be
/// appended to HTMLBody by hand). May be null/empty if no
/// signature is configured; the mail is then sent without one.
/// </param>
public MailSender(Outlook.Application application, string signatureHtml)
{
this.application = application ?? throw new ArgumentNullException(nameof(application));
this.signatureHtml = signatureHtml ?? string.Empty;
}
/// <param name="job">Row to send. Caller is responsible for only passing selectable (Ready/AlreadySent), checked rows - this method does not re-validate Status.</param>
/// <param name="testMode">When true, the mail item is Save()'d (left in Drafts) instead of Send()'d - the safety net for trying the tool out before trusting it with real recipients.</param>
public MailSendOutcome SendOne(SendJob job, bool testMode)
{
if (job == null)
{
throw new ArgumentNullException(nameof(job));
}
Outlook.MailItem mailItem = null;
try
{
mailItem = (Outlook.MailItem)application.CreateItem(Outlook.OlItemType.olMailItem);
mailItem.To = job.Email;
mailItem.Subject = job.Subject;
var body = job.BodyHtml ?? string.Empty;
if (!string.IsNullOrEmpty(signatureHtml))
{
body += "<br><br>" + signatureHtml;
}
mailItem.HTMLBody = body;
if (!string.IsNullOrEmpty(job.ArchivePath))
{
mailItem.Attachments.Add(job.ArchivePath, Outlook.OlAttachmentType.olByValue, Type.Missing, Type.Missing);
}
if (testMode)
{
mailItem.Save();
}
else
{
mailItem.Send();
}
return new MailSendOutcome { Job = job, Success = true, WasDraft = testMode };
}
catch (Exception ex)
{
return new MailSendOutcome { Job = job, Success = false, ErrorMessage = ex.Message, WasDraft = testMode };
}
finally
{
if (mailItem != null)
{
Marshal.ReleaseComObject(mailItem);
}
}
}
}
}
@@ -0,0 +1,19 @@
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("BulkArchiveMailer.AddIn")]
[assembly: AssemblyDescription("VSTO Outlook add-in: sends ssh-access-tui's per-user .7z archives via a ribbon button.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("BulkArchiveMailer.AddIn")]
[assembly: AssemblyCopyright("")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("ru")]
[assembly: ComVisible(false)]
[assembly: Guid("7a1c9e2a-4f9a-4d6a-9b8e-1c2d3e4f5a6b")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<customUI xmlns="http://schemas.microsoft.com/office/2009/07/customui" onLoad="Ribbon_Load">
<ribbon>
<tabs>
<!-- TabMail = the Home tab of the main Outlook Explorer (mail list) window. -->
<tab idMso="TabMail">
<group id="BulkArchiveMailerGroup" label="ssh-access-tui">
<button id="OpenBulkSenderButton"
label="Массовая рассылка"
size="large"
onAction="OnOpenBulkSenderButtonClick"
imageMso="MailMerge"
screentip="Массовая рассылка архивов доступа"
supertip="Сканирует папку с .7z-архивами, сгенерированными ssh-access-tui, и рассылает их получателям с индивидуальным письмом." />
</group>
</tab>
</tabs>
</ribbon>
</customUI>
@@ -0,0 +1,95 @@
using System;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using BulkArchiveMailer.AddIn.UI;
using BulkArchiveMailer.Core;
using Office = Microsoft.Office.Core;
namespace BulkArchiveMailer.AddIn
{
/// <summary>
/// Backs the single "Массовая рассылка" ribbon button declared in
/// Ribbon.xml (raw Ribbon XML + callbacks approach, not the Ribbon
/// Designer visual item - keeps everything in one small readable
/// file). Wired up via ThisAddIn.CreateRibbonExtensibilityObject().
/// </summary>
[System.Runtime.InteropServices.ComVisible(true)]
public class RibbonController : Office.IRibbonExtensibility
{
private Office.IRibbonUI ribbon;
public string GetCustomUI(string ribbonId)
{
return GetEmbeddedResourceText("BulkArchiveMailer.AddIn.Ribbon.xml");
}
public void Ribbon_Load(Office.IRibbonUI ribbonUi)
{
ribbon = ribbonUi;
}
/// <summary>
/// The button's onAction callback. Opens SettingsForm first if the
/// archive folder is not configured yet (or does not exist);
/// otherwise jumps straight to PreviewForm. Kept deliberately
/// simple - see README.md for the reasoning.
/// </summary>
public void OnOpenBulkSenderButtonClick(Office.IRibbonControl control)
{
try
{
var settings = AppSettings.Load();
var outlookApplication = Globals.ThisAddIn.OutlookApplication;
if (string.IsNullOrWhiteSpace(settings.ArchiveFolder) || !Directory.Exists(settings.ArchiveFolder))
{
using (var settingsForm = new SettingsForm(outlookApplication))
{
settingsForm.ShowDialog();
}
return;
}
using (var previewForm = new PreviewForm(outlookApplication))
{
previewForm.ShowDialog();
}
}
catch (Exception ex)
{
MessageBox.Show(
"Не удалось открыть окно массовой рассылки: " + ex.Message,
"BulkArchiveMailer",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
private static string GetEmbeddedResourceText(string resourceName)
{
var asm = Assembly.GetExecutingAssembly();
foreach (var name in asm.GetManifestResourceNames())
{
if (string.Equals(name, resourceName, StringComparison.OrdinalIgnoreCase))
{
using (var stream = asm.GetManifestResourceStream(name))
{
if (stream == null)
{
return null;
}
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
}
return null;
}
}
}
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Win32;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace BulkArchiveMailer.AddIn
{
/// <summary>
/// Reads the user's default "new message" Outlook signature HTML from
/// disk. Outlook Interop has no API that exposes "the currently
/// selected default signature" - the only reliable way to get at it
/// is: look up its *name* in the registry
/// (HKCU\Software\Microsoft\Office\&lt;version&gt;\Common\MailSettings,
/// value "NewSignature"), then load the matching .htm file from
/// %APPDATA%\Microsoft\Signatures\&lt;name&gt;.htm ourselves.
///
/// This is the only file in the solution that touches the Windows
/// registry - deliberately kept out of BulkArchiveMailer.Core.
/// </summary>
public static class SignatureReader
{
/// <summary>
/// Best-effort auto-detection. Never throws - returns null if the
/// registry value, the version-specific key, or the .htm file is
/// missing for any reason, so the caller can fall back to letting
/// the operator pick a signature file manually instead of crashing
/// the add-in.
/// </summary>
public static string TryAutoDetectSignatureHtml(Outlook.Application application)
{
try
{
var signatureName = TryReadSignatureName(application);
if (string.IsNullOrEmpty(signatureName))
{
return null;
}
var path = ResolveSignaturePath(signatureName);
return File.Exists(path) ? File.ReadAllText(path) : null;
}
catch
{
return null;
}
}
/// <summary>Same lookup, but returns the resolved .htm path instead of its contents - useful for SettingsForm to show/prefill the path.</summary>
public static string TryAutoDetectSignaturePath(Outlook.Application application)
{
try
{
var signatureName = TryReadSignatureName(application);
if (string.IsNullOrEmpty(signatureName))
{
return null;
}
var path = ResolveSignaturePath(signatureName);
return File.Exists(path) ? path : null;
}
catch
{
return null;
}
}
public static string ResolveSignaturePath(string signatureName)
{
var signaturesFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Microsoft",
"Signatures");
return Path.Combine(signaturesFolder, signatureName + ".htm");
}
private static string TryReadSignatureName(Outlook.Application application)
{
var candidateVersions = new List<string>();
try
{
// application.Version looks like "16.0" and lines up
// exactly with the Office registry hive layout, so prefer
// it over the fixed fallback list below.
var version = application?.Version;
if (!string.IsNullOrEmpty(version))
{
candidateVersions.Add(version);
}
}
catch
{
// Interop call failed for some reason - fall through to
// the fixed list.
}
foreach (var fallback in new[] { "16.0", "15.0", "14.0" })
{
if (!candidateVersions.Contains(fallback))
{
candidateVersions.Add(fallback);
}
}
foreach (var version in candidateVersions)
{
using (var key = Registry.CurrentUser.OpenSubKey(
$@"Software\Microsoft\Office\{version}\Common\MailSettings"))
{
var value = key?.GetValue("NewSignature") as string;
if (!string.IsNullOrEmpty(value))
{
return value;
}
}
}
return null;
}
}
}
@@ -0,0 +1,24 @@
namespace BulkArchiveMailer.AddIn
{
/// <summary>
/// Designer-generated half of ThisAddIn. Normally Visual Studio
/// regenerates this file's contents automatically the first time the
/// project is opened/built (it derives from
/// Microsoft.Office.Tools.Outlook.OutlookAddIn and wires Startup/
/// Shutdown) - it is hand-written here only because this environment
/// has no VSTO tooling to run the code generator. Do not hand-edit
/// once opened in VS if the IDE regenerates it differently.
/// </summary>
partial class ThisAddIn : Microsoft.Office.Tools.Outlook.OutlookAddIn
{
/// <summary>
/// Required method for Designer support - do not modify the
/// contents of this method with the code editor.
/// </summary>
private void InternalStartup()
{
this.Startup += new System.EventHandler(ThisAddIn_Startup);
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
}
}
}
@@ -0,0 +1,32 @@
using System;
using Office = Microsoft.Office.Core;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace BulkArchiveMailer.AddIn
{
public partial class ThisAddIn
{
/// <summary>Convenience alias for the inherited Application property, so RibbonController/forms don't need to know the base class's exact property name.</summary>
internal Outlook.Application OutlookApplication => this.Application;
private void ThisAddIn_Startup(object sender, EventArgs e)
{
// Deliberately does nothing eagerly: no settings/folder/registry
// access happens until the operator clicks the ribbon button,
// so add-in load never slows down or blocks Outlook startup.
}
private void ThisAddIn_Shutdown(object sender, EventArgs e)
{
// Outlook typically no longer raises this reliably before
// process exit; nothing here needs cleanup anyway (no open
// handles/timers are kept alive between ribbon clicks).
}
/// <summary>Wires up RibbonController (raw Ribbon XML + callbacks, see Ribbon.xml) as this add-in's ribbon UI.</summary>
protected override Office.IRibbonExtensibility CreateRibbonExtensibilityObject()
{
return new RibbonController();
}
}
}
@@ -0,0 +1,283 @@
namespace BulkArchiveMailer.AddIn.UI
{
partial class PreviewForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && components != null)
{
components.Dispose();
}
base.Dispose(disposing);
}
private System.Windows.Forms.Label lblPasswordReminder;
private System.Windows.Forms.Label lblSignatureWarning;
private System.Windows.Forms.DataGridView grid;
private System.Windows.Forms.DataGridViewCheckBoxColumn colSelect;
private System.Windows.Forms.DataGridViewTextBoxColumn colLogin;
private System.Windows.Forms.DataGridViewTextBoxColumn colEmail;
private System.Windows.Forms.DataGridViewTextBoxColumn colKind;
private System.Windows.Forms.DataGridViewTextBoxColumn colFile;
private System.Windows.Forms.DataGridViewTextBoxColumn colStatus;
private System.Windows.Forms.DataGridViewTextBoxColumn colComment;
private System.Windows.Forms.Label lblSummary;
private System.Windows.Forms.CheckBox chkTestMode;
private System.Windows.Forms.Button btnCheckAllReady;
private System.Windows.Forms.Button btnUncheckAll;
private System.Windows.Forms.Button btnRescan;
private System.Windows.Forms.ProgressBar progressSend;
private System.Windows.Forms.Label lblProgressStatus;
private System.Windows.Forms.Button btnSend;
private System.Windows.Forms.Button btnClose;
private void InitializeComponent()
{
this.lblPasswordReminder = new System.Windows.Forms.Label();
this.lblSignatureWarning = new System.Windows.Forms.Label();
this.grid = new System.Windows.Forms.DataGridView();
this.colSelect = new System.Windows.Forms.DataGridViewCheckBoxColumn();
this.colLogin = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colEmail = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colKind = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colFile = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colStatus = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.colComment = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.lblSummary = new System.Windows.Forms.Label();
this.chkTestMode = new System.Windows.Forms.CheckBox();
this.btnCheckAllReady = new System.Windows.Forms.Button();
this.btnUncheckAll = new System.Windows.Forms.Button();
this.btnRescan = new System.Windows.Forms.Button();
this.progressSend = new System.Windows.Forms.ProgressBar();
this.lblProgressStatus = new System.Windows.Forms.Label();
this.btnSend = new System.Windows.Forms.Button();
this.btnClose = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.grid)).BeginInit();
this.SuspendLayout();
//
// lblPasswordReminder
//
this.lblPasswordReminder.Location = new System.Drawing.Point(12, 10);
this.lblPasswordReminder.Size = new System.Drawing.Size(900, 20);
this.lblPasswordReminder.Text = "Напоминание: пароль от архивов сообщается получателям отдельно (вручную, не через это письмо и не через этот инструмент).";
this.lblPasswordReminder.ForeColor = System.Drawing.Color.DarkRed;
this.lblPasswordReminder.Font = new System.Drawing.Font(this.lblPasswordReminder.Font, System.Drawing.FontStyle.Bold);
this.lblPasswordReminder.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)));
//
// lblSignatureWarning
//
this.lblSignatureWarning.Location = new System.Drawing.Point(12, 32);
this.lblSignatureWarning.Size = new System.Drawing.Size(900, 20);
this.lblSignatureWarning.Text = "Подпись не найдена — письма будут отправлены без подписи. Настройте .htm-файл вручную в Настройках.";
this.lblSignatureWarning.ForeColor = System.Drawing.Color.DarkOrange;
this.lblSignatureWarning.Visible = false;
this.lblSignatureWarning.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)));
//
// grid
//
this.grid.Location = new System.Drawing.Point(12, 58);
this.grid.Size = new System.Drawing.Size(900, 420);
this.grid.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)));
this.grid.AllowUserToAddRows = false;
this.grid.AllowUserToDeleteRows = false;
this.grid.AllowUserToResizeRows = false;
this.grid.RowHeadersVisible = false;
this.grid.AutoGenerateColumns = false;
this.grid.SelectionMode = System.Windows.Forms.DataGridViewSelectionMode.FullRowSelect;
this.grid.MultiSelect = false;
this.grid.EditMode = System.Windows.Forms.DataGridViewEditMode.EditOnEnter;
this.grid.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.colSelect,
this.colLogin,
this.colEmail,
this.colKind,
this.colFile,
this.colStatus,
this.colComment});
this.grid.CurrentCellDirtyStateChanged += new System.EventHandler(this.grid_CurrentCellDirtyStateChanged);
this.grid.CellValueChanged += new System.Windows.Forms.DataGridViewCellEventHandler(this.grid_CellValueChanged);
this.grid.DataBindingComplete += new System.Windows.Forms.DataGridViewBindingCompleteEventHandler(this.grid_DataBindingComplete);
// colSelect
this.colSelect.Name = "colSelect";
this.colSelect.HeaderText = "Отправить";
this.colSelect.DataPropertyName = "IsChecked";
this.colSelect.Width = 70;
this.colSelect.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
// colLogin
this.colLogin.Name = "colLogin";
this.colLogin.HeaderText = "Логин";
this.colLogin.DataPropertyName = "Login";
this.colLogin.ReadOnly = true;
this.colLogin.Width = 100;
this.colLogin.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
// colEmail
this.colEmail.Name = "colEmail";
this.colEmail.HeaderText = "Получатель";
this.colEmail.DataPropertyName = "Email";
this.colEmail.ReadOnly = true;
this.colEmail.Width = 190;
this.colEmail.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
// colKind
this.colKind.Name = "colKind";
this.colKind.HeaderText = "Вид архива";
this.colKind.DataPropertyName = "KindDisplay";
this.colKind.ReadOnly = true;
this.colKind.Width = 170;
this.colKind.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
// colFile
this.colFile.Name = "colFile";
this.colFile.HeaderText = "Файл";
this.colFile.DataPropertyName = "FileName";
this.colFile.ReadOnly = true;
this.colFile.Width = 190;
this.colFile.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
// colStatus
this.colStatus.Name = "colStatus";
this.colStatus.HeaderText = "Статус";
this.colStatus.DataPropertyName = "StatusDisplay";
this.colStatus.ReadOnly = true;
this.colStatus.Width = 120;
this.colStatus.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
// colComment
this.colComment.Name = "colComment";
this.colComment.HeaderText = "Комментарий";
this.colComment.DataPropertyName = "Comment";
this.colComment.ReadOnly = true;
this.colComment.AutoSizeMode = System.Windows.Forms.DataGridViewAutoSizeColumnMode.Fill;
this.colComment.SortMode = System.Windows.Forms.DataGridViewColumnSortMode.NotSortable;
//
// lblSummary
//
this.lblSummary.Location = new System.Drawing.Point(12, 486);
this.lblSummary.Size = new System.Drawing.Size(500, 20);
this.lblSummary.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
//
// chkTestMode
//
this.chkTestMode.Location = new System.Drawing.Point(12, 510);
this.chkTestMode.Size = new System.Drawing.Size(360, 24);
this.chkTestMode.Text = "Тестовый режим (сохранять в Черновики, не отправлять)";
this.chkTestMode.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.chkTestMode.UseVisualStyleBackColor = true;
//
// btnRescan
//
this.btnRescan.Location = new System.Drawing.Point(12, 540);
this.btnRescan.Size = new System.Drawing.Size(110, 27);
this.btnRescan.Text = "Пересканировать";
this.btnRescan.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnRescan.UseVisualStyleBackColor = true;
this.btnRescan.Click += new System.EventHandler(this.btnRescan_Click);
//
// btnCheckAllReady
//
this.btnCheckAllReady.Location = new System.Drawing.Point(130, 540);
this.btnCheckAllReady.Size = new System.Drawing.Size(150, 27);
this.btnCheckAllReady.Text = "Отметить все готовые";
this.btnCheckAllReady.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnCheckAllReady.UseVisualStyleBackColor = true;
this.btnCheckAllReady.Click += new System.EventHandler(this.btnCheckAllReady_Click);
//
// btnUncheckAll
//
this.btnUncheckAll.Location = new System.Drawing.Point(288, 540);
this.btnUncheckAll.Size = new System.Drawing.Size(120, 27);
this.btnUncheckAll.Text = "Снять все отметки";
this.btnUncheckAll.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnUncheckAll.UseVisualStyleBackColor = true;
this.btnUncheckAll.Click += new System.EventHandler(this.btnUncheckAll_Click);
//
// progressSend
//
this.progressSend.Location = new System.Drawing.Point(430, 543);
this.progressSend.Size = new System.Drawing.Size(300, 20);
this.progressSend.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
//
// lblProgressStatus
//
this.lblProgressStatus.Location = new System.Drawing.Point(430, 565);
this.lblProgressStatus.Size = new System.Drawing.Size(482, 20);
this.lblProgressStatus.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
//
// btnClose
//
this.btnClose.Location = new System.Drawing.Point(720, 540);
this.btnClose.Size = new System.Drawing.Size(90, 30);
this.btnClose.Text = "Закрыть";
this.btnClose.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnClose.UseVisualStyleBackColor = true;
this.btnClose.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
//
// btnSend
//
this.btnSend.Location = new System.Drawing.Point(816, 540);
this.btnSend.Size = new System.Drawing.Size(96, 30);
this.btnSend.Text = "Отправить";
this.btnSend.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnSend.Font = new System.Drawing.Font(this.btnSend.Font, System.Drawing.FontStyle.Bold);
this.btnSend.UseVisualStyleBackColor = true;
this.btnSend.Click += new System.EventHandler(this.btnSend_Click);
//
// PreviewForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(924, 582);
this.MinimumSize = new System.Drawing.Size(760, 420);
this.Controls.Add(this.lblPasswordReminder);
this.Controls.Add(this.lblSignatureWarning);
this.Controls.Add(this.grid);
this.Controls.Add(this.lblSummary);
this.Controls.Add(this.chkTestMode);
this.Controls.Add(this.btnRescan);
this.Controls.Add(this.btnCheckAllReady);
this.Controls.Add(this.btnUncheckAll);
this.Controls.Add(this.progressSend);
this.Controls.Add(this.lblProgressStatus);
this.Controls.Add(this.btnClose);
this.Controls.Add(this.btnSend);
this.Text = "BulkArchiveMailer — предпросмотр и рассылка";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
this.MaximizeBox = true;
this.CancelButton = this.btnClose;
((System.ComponentModel.ISupportInitialize)(this.grid)).EndInit();
this.ResumeLayout(false);
}
}
}
@@ -0,0 +1,340 @@
using System;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using BulkArchiveMailer.Core;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace BulkArchiveMailer.AddIn.UI
{
/// <summary>
/// Scans the configured archive folder, shows one row per login (plus
/// unrecognized files) in a grid, and sends the checked rows through
/// MailSender. This is the only form that actually touches Outlook
/// COM (via the Outlook.Application passed into its constructor) or
/// writes to the send log.
/// </summary>
public partial class PreviewForm : Form
{
private readonly Outlook.Application outlookApplication;
private AppSettingsData settings;
private SendLogStore sendLog;
private string signatureHtml;
private BindingList<PreviewRow> rows;
public PreviewForm(Outlook.Application outlookApplication)
{
InitializeComponent();
this.outlookApplication = outlookApplication ?? throw new ArgumentNullException(nameof(outlookApplication));
Load += PreviewForm_Load;
}
private void PreviewForm_Load(object sender, EventArgs e)
{
try
{
settings = AppSettings.Load();
}
catch (Exception ex)
{
MessageBox.Show(this, "Не удалось прочитать настройки: " + ex.Message, "BulkArchiveMailer", MessageBoxButtons.OK, MessageBoxIcon.Error);
Close();
return;
}
chkTestMode.Checked = settings.TestModeDefault;
signatureHtml = ResolveSignatureHtml();
lblSignatureWarning.Visible = string.IsNullOrEmpty(signatureHtml);
RunScan();
}
private string ResolveSignatureHtml()
{
try
{
if (!string.IsNullOrWhiteSpace(settings.SignatureHtmlPath))
{
return File.Exists(settings.SignatureHtmlPath) ? File.ReadAllText(settings.SignatureHtmlPath) : null;
}
return SignatureReader.TryAutoDetectSignatureHtml(outlookApplication);
}
catch
{
return null; // never block preview over a signature problem
}
}
private void RunScan()
{
if (string.IsNullOrWhiteSpace(settings.ArchiveFolder) || !Directory.Exists(settings.ArchiveFolder))
{
MessageBox.Show(
this,
"Папка с архивами не настроена или не существует:\n" + settings.ArchiveFolder,
"BulkArchiveMailer",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
Close();
return;
}
var logPath = Path.Combine(settings.ArchiveFolder, "send-log.csv");
sendLog = new SendLogStore(logPath);
var templates = settings.ToTemplateSet();
var scan = ArchiveScanner.Scan(settings.ArchiveFolder, settings.EmailDomain, templates, sendLog);
rows = new BindingList<PreviewRow>(scan.AllRows.Select(job => new PreviewRow(job)).ToList());
grid.DataSource = rows;
UpdateSummaryLabel();
}
private void ApplyRowStyles()
{
foreach (DataGridViewRow row in grid.Rows)
{
if (!(row.DataBoundItem is PreviewRow previewRow))
{
continue;
}
row.Cells[colSelect.Index].ReadOnly = !previewRow.Job.IsSelectable;
switch (previewRow.Job.Status)
{
case SendStatus.Error:
row.DefaultCellStyle.BackColor = System.Drawing.Color.MistyRose;
break;
case SendStatus.Skipped:
row.DefaultCellStyle.BackColor = System.Drawing.Color.WhiteSmoke;
row.DefaultCellStyle.ForeColor = System.Drawing.Color.Gray;
break;
case SendStatus.AlreadySent:
row.DefaultCellStyle.BackColor = System.Drawing.Color.LightYellow;
break;
default:
row.DefaultCellStyle.BackColor = grid.DefaultCellStyle.BackColor;
row.DefaultCellStyle.ForeColor = grid.DefaultCellStyle.ForeColor;
break;
}
}
}
private void UpdateSummaryLabel()
{
var ready = rows.Count(r => r.Job.Status == SendStatus.Ready);
var already = rows.Count(r => r.Job.Status == SendStatus.AlreadySent);
var errors = rows.Count(r => r.Job.Status == SendStatus.Error);
var skipped = rows.Count(r => r.Job.Status == SendStatus.Skipped);
var checkedCount = rows.Count(r => r.IsChecked);
lblSummary.Text = $"Готово: {ready} Уже отправлено: {already} Ошибки: {errors} Пропущено: {skipped} | Отмечено к отправке: {checkedCount}";
}
private void grid_DataBindingComplete(object sender, DataGridViewBindingCompleteEventArgs e)
{
ApplyRowStyles();
UpdateSummaryLabel();
}
private void grid_CurrentCellDirtyStateChanged(object sender, EventArgs e)
{
if (grid.IsCurrentCellDirty && grid.CurrentCell is DataGridViewCheckBoxCell)
{
grid.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
}
private void grid_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
UpdateSummaryLabel();
}
private void btnRescan_Click(object sender, EventArgs e)
{
RunScan();
}
private void btnCheckAllReady_Click(object sender, EventArgs e)
{
foreach (var row in rows)
{
if (row.Job.IsSelectable)
{
row.IsChecked = true;
}
}
grid.Refresh();
UpdateSummaryLabel();
}
private void btnUncheckAll_Click(object sender, EventArgs e)
{
foreach (var row in rows)
{
row.IsChecked = false;
}
grid.Refresh();
UpdateSummaryLabel();
}
private void btnClose_Click(object sender, EventArgs e)
{
Close();
}
private void btnSend_Click(object sender, EventArgs e)
{
var toSend = rows.Where(r => r.IsChecked && r.Job.IsSelectable).ToList();
if (toSend.Count == 0)
{
MessageBox.Show(this, "Не выбрано ни одной строки для отправки.", "BulkArchiveMailer", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
var testMode = chkTestMode.Checked;
var confirmText = testMode
? $"Сохранить {toSend.Count} писем в Черновики (тестовый режим, реальная отправка НЕ произойдёт)?"
: $"Отправить {toSend.Count} писем? Это реальная отправка получателям.";
if (MessageBox.Show(this, confirmText, "Подтверждение", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
{
return;
}
btnSend.Enabled = false;
btnRescan.Enabled = false;
progressSend.Minimum = 0;
progressSend.Maximum = toSend.Count;
progressSend.Value = 0;
var mailSender = new MailSender(outlookApplication, signatureHtml);
var sentCount = 0;
var failedCount = 0;
foreach (var row in toSend)
{
lblProgressStatus.Text = $"Обработка: {row.Job.Email} ({row.Job.KindDisplay})...";
// No async/await here on purpose: Outlook COM objects are
// STA-thread-affine, and MailSender.SendOne must run on
// this UI thread. DoEvents keeps the form responsive
// (progress bar, ability to see the current item) during
// what is otherwise a synchronous per-item loop.
Application.DoEvents();
var outcome = mailSender.SendOne(row.Job, testMode);
try
{
sendLog.Append(new SendLogEntry
{
TimestampUtc = DateTime.UtcNow,
Login = row.Job.Login,
Email = row.Job.Email,
Kind = row.Job.Kind ?? default,
ArchivePath = row.Job.ArchivePath ?? "",
FileSha256 = row.Job.FileSha256 ?? "",
Status = outcome.Success ? (testMode ? SendLogStatus.Draft : SendLogStatus.Sent) : SendLogStatus.Failed,
Error = outcome.Success ? "" : outcome.ErrorMessage,
});
}
catch (Exception logEx)
{
// A logging failure must not be confused with a send
// failure, but it also must not silently disappear -
// surface it in the comment column.
row.ResultText = (outcome.Success ? "Отправлено, но" : "Ошибка отправки, и") + " не удалось записать в журнал: " + logEx.Message;
row.IsChecked = false;
grid.Refresh();
continue;
}
if (outcome.Success)
{
sentCount++;
row.ResultText = testMode ? "Сохранено в Черновики" : "Отправлено";
}
else
{
failedCount++;
row.ResultText = "Ошибка: " + outcome.ErrorMessage;
}
row.IsChecked = false;
progressSend.Value++;
grid.Refresh();
}
btnSend.Enabled = true;
btnRescan.Enabled = true;
lblProgressStatus.Text = "Готово.";
UpdateSummaryLabel();
MessageBox.Show(
this,
$"Отправлено/сохранено: {sentCount}\nОшибок: {failedCount}\n\nЖурнал: {sendLog.CsvPath}",
"Массовая рассылка — итог",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
/// <summary>
/// UI-only wrapper around a Core SendJob: adds the checkbox state
/// and post-send result text, neither of which belong in the
/// portable Core model.
/// </summary>
private sealed class PreviewRow
{
public PreviewRow(SendJob job)
{
Job = job;
// AlreadySent starts unchecked by default (per spec) but
// remains selectable if the operator wants to resend it
// anyway (e.g. recipient claims they never got it).
IsChecked = job.Status == SendStatus.Ready;
}
public SendJob Job { get; }
public bool IsChecked { get; set; }
public string ResultText { get; set; } = "";
public string Login => Job.Login;
public string Email => Job.Email;
public string KindDisplay => Job.KindDisplay;
public string FileName => Job.FileName;
public string StatusDisplay => ToRussian(Job.Status);
public string Comment => !string.IsNullOrEmpty(ResultText) ? ResultText : (Job.ErrorMessage ?? "");
private static string ToRussian(SendStatus status)
{
switch (status)
{
case SendStatus.Ready:
return "Готово";
case SendStatus.AlreadySent:
return "Уже отправлено";
case SendStatus.Error:
return "Ошибка";
case SendStatus.Skipped:
return "Пропущено";
default:
return status.ToString();
}
}
}
}
}
@@ -0,0 +1,318 @@
namespace BulkArchiveMailer.AddIn.UI
{
partial class SettingsForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && components != null)
{
components.Dispose();
}
base.Dispose(disposing);
}
private System.Windows.Forms.TabControl tabControl;
private System.Windows.Forms.TabPage tabGeneral;
private System.Windows.Forms.TabPage tabSshAccess;
private System.Windows.Forms.TabPage tabConfigUpdate;
private System.Windows.Forms.Label lblArchiveFolder;
private System.Windows.Forms.TextBox txtArchiveFolder;
private System.Windows.Forms.Button btnBrowseFolder;
private System.Windows.Forms.Label lblDomain;
private System.Windows.Forms.TextBox txtDomain;
private System.Windows.Forms.Label lblSignaturePath;
private System.Windows.Forms.TextBox txtSignaturePath;
private System.Windows.Forms.Button btnBrowseSignature;
private System.Windows.Forms.Button btnAutoDetectSignature;
private System.Windows.Forms.CheckBox chkTestModeDefault;
private System.Windows.Forms.Label lblSshSubject;
private System.Windows.Forms.TextBox txtSshSubject;
private System.Windows.Forms.Label lblSshBody;
private System.Windows.Forms.TextBox txtSshBody;
private System.Windows.Forms.Label lblSshHint;
private System.Windows.Forms.Label lblCfgSubject;
private System.Windows.Forms.TextBox txtCfgSubject;
private System.Windows.Forms.Label lblCfgBody;
private System.Windows.Forms.TextBox txtCfgBody;
private System.Windows.Forms.Label lblCfgHint;
private System.Windows.Forms.Button btnSave;
private System.Windows.Forms.Button btnCancel;
private System.Windows.Forms.Button btnResetDefaults;
private void InitializeComponent()
{
this.tabControl = new System.Windows.Forms.TabControl();
this.tabGeneral = new System.Windows.Forms.TabPage();
this.tabSshAccess = new System.Windows.Forms.TabPage();
this.tabConfigUpdate = new System.Windows.Forms.TabPage();
this.lblArchiveFolder = new System.Windows.Forms.Label();
this.txtArchiveFolder = new System.Windows.Forms.TextBox();
this.btnBrowseFolder = new System.Windows.Forms.Button();
this.lblDomain = new System.Windows.Forms.Label();
this.txtDomain = new System.Windows.Forms.TextBox();
this.lblSignaturePath = new System.Windows.Forms.Label();
this.txtSignaturePath = new System.Windows.Forms.TextBox();
this.btnBrowseSignature = new System.Windows.Forms.Button();
this.btnAutoDetectSignature = new System.Windows.Forms.Button();
this.chkTestModeDefault = new System.Windows.Forms.CheckBox();
this.lblSshSubject = new System.Windows.Forms.Label();
this.txtSshSubject = new System.Windows.Forms.TextBox();
this.lblSshBody = new System.Windows.Forms.Label();
this.txtSshBody = new System.Windows.Forms.TextBox();
this.lblSshHint = new System.Windows.Forms.Label();
this.lblCfgSubject = new System.Windows.Forms.Label();
this.txtCfgSubject = new System.Windows.Forms.TextBox();
this.lblCfgBody = new System.Windows.Forms.Label();
this.txtCfgBody = new System.Windows.Forms.TextBox();
this.lblCfgHint = new System.Windows.Forms.Label();
this.btnSave = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.btnResetDefaults = new System.Windows.Forms.Button();
this.tabControl.SuspendLayout();
this.tabGeneral.SuspendLayout();
this.tabSshAccess.SuspendLayout();
this.tabConfigUpdate.SuspendLayout();
this.SuspendLayout();
//
// tabControl
//
this.tabControl.Location = new System.Drawing.Point(12, 12);
this.tabControl.Size = new System.Drawing.Size(660, 470);
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)));
this.tabControl.TabPages.AddRange(new System.Windows.Forms.TabPage[] {
this.tabGeneral,
this.tabSshAccess,
this.tabConfigUpdate});
//
// tabGeneral
//
this.tabGeneral.Text = "Общие";
this.tabGeneral.Padding = new System.Windows.Forms.Padding(12);
this.tabGeneral.UseVisualStyleBackColor = true;
this.tabGeneral.Controls.Add(this.lblArchiveFolder);
this.tabGeneral.Controls.Add(this.txtArchiveFolder);
this.tabGeneral.Controls.Add(this.btnBrowseFolder);
this.tabGeneral.Controls.Add(this.lblDomain);
this.tabGeneral.Controls.Add(this.txtDomain);
this.tabGeneral.Controls.Add(this.lblSignaturePath);
this.tabGeneral.Controls.Add(this.txtSignaturePath);
this.tabGeneral.Controls.Add(this.btnBrowseSignature);
this.tabGeneral.Controls.Add(this.btnAutoDetectSignature);
this.tabGeneral.Controls.Add(this.chkTestModeDefault);
// lblArchiveFolder
this.lblArchiveFolder.Location = new System.Drawing.Point(15, 20);
this.lblArchiveFolder.Size = new System.Drawing.Size(400, 20);
this.lblArchiveFolder.Text = "Папка с архивами ssh-access-tui (общий выходной каталог):";
// txtArchiveFolder
this.txtArchiveFolder.Location = new System.Drawing.Point(15, 43);
this.txtArchiveFolder.Size = new System.Drawing.Size(500, 23);
// btnBrowseFolder
this.btnBrowseFolder.Location = new System.Drawing.Point(525, 41);
this.btnBrowseFolder.Size = new System.Drawing.Size(100, 25);
this.btnBrowseFolder.Text = "Обзор...";
this.btnBrowseFolder.UseVisualStyleBackColor = true;
this.btnBrowseFolder.Click += new System.EventHandler(this.btnBrowseFolder_Click);
// lblDomain
this.lblDomain.Location = new System.Drawing.Point(15, 85);
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.Size = new System.Drawing.Size(300, 23);
// lblSignaturePath
this.lblSignaturePath.Location = new System.Drawing.Point(15, 150);
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.Size = new System.Drawing.Size(500, 23);
// btnBrowseSignature
this.btnBrowseSignature.Location = new System.Drawing.Point(525, 171);
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.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.Size = new System.Drawing.Size(610, 24);
this.chkTestModeDefault.Text = "По умолчанию открывать предпросмотр в тестовом режиме (сохранять в Черновики)";
this.chkTestModeDefault.UseVisualStyleBackColor = true;
//
// tabSshAccess
//
this.tabSshAccess.Text = "Шаблон: новый доступ";
this.tabSshAccess.Padding = new System.Windows.Forms.Padding(12);
this.tabSshAccess.UseVisualStyleBackColor = true;
this.tabSshAccess.Controls.Add(this.lblSshSubject);
this.tabSshAccess.Controls.Add(this.txtSshSubject);
this.tabSshAccess.Controls.Add(this.lblSshBody);
this.tabSshAccess.Controls.Add(this.txtSshBody);
this.tabSshAccess.Controls.Add(this.lblSshHint);
// lblSshSubject
this.lblSshSubject.Location = new System.Drawing.Point(15, 20);
this.lblSshSubject.Size = new System.Drawing.Size(200, 20);
this.lblSshSubject.Text = "Тема письма:";
// txtSshSubject
this.txtSshSubject.Location = new System.Drawing.Point(15, 43);
this.txtSshSubject.Size = new System.Drawing.Size(610, 23);
// lblSshBody
this.lblSshBody.Location = new System.Drawing.Point(15, 78);
this.lblSshBody.Size = new System.Drawing.Size(200, 20);
this.lblSshBody.Text = "Тело письма (HTML):";
// txtSshBody
this.txtSshBody.Location = new System.Drawing.Point(15, 101);
this.txtSshBody.Size = new System.Drawing.Size(610, 300);
this.txtSshBody.Multiline = true;
this.txtSshBody.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtSshBody.AcceptsReturn = true;
this.txtSshBody.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
// lblSshHint
this.lblSshHint.Location = new System.Drawing.Point(15, 408);
this.lblSshHint.Size = new System.Drawing.Size(610, 20);
this.lblSshHint.Text = "Доступные плейсхолдеры: {Login}, {Email}, {Date}";
this.lblSshHint.ForeColor = System.Drawing.SystemColors.GrayText;
this.lblSshHint.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
//
// tabConfigUpdate
//
this.tabConfigUpdate.Text = "Шаблон: обновление конфига";
this.tabConfigUpdate.Padding = new System.Windows.Forms.Padding(12);
this.tabConfigUpdate.UseVisualStyleBackColor = true;
this.tabConfigUpdate.Controls.Add(this.lblCfgSubject);
this.tabConfigUpdate.Controls.Add(this.txtCfgSubject);
this.tabConfigUpdate.Controls.Add(this.lblCfgBody);
this.tabConfigUpdate.Controls.Add(this.txtCfgBody);
this.tabConfigUpdate.Controls.Add(this.lblCfgHint);
// lblCfgSubject
this.lblCfgSubject.Location = new System.Drawing.Point(15, 20);
this.lblCfgSubject.Size = new System.Drawing.Size(200, 20);
this.lblCfgSubject.Text = "Тема письма:";
// txtCfgSubject
this.txtCfgSubject.Location = new System.Drawing.Point(15, 43);
this.txtCfgSubject.Size = new System.Drawing.Size(610, 23);
// lblCfgBody
this.lblCfgBody.Location = new System.Drawing.Point(15, 78);
this.lblCfgBody.Size = new System.Drawing.Size(200, 20);
this.lblCfgBody.Text = "Тело письма (HTML):";
// txtCfgBody
this.txtCfgBody.Location = new System.Drawing.Point(15, 101);
this.txtCfgBody.Size = new System.Drawing.Size(610, 300);
this.txtCfgBody.Multiline = true;
this.txtCfgBody.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtCfgBody.AcceptsReturn = true;
this.txtCfgBody.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
// lblCfgHint
this.lblCfgHint.Location = new System.Drawing.Point(15, 408);
this.lblCfgHint.Size = new System.Drawing.Size(610, 20);
this.lblCfgHint.Text = "Доступные плейсхолдеры: {Login}, {Email}, {Date}";
this.lblCfgHint.ForeColor = System.Drawing.SystemColors.GrayText;
this.lblCfgHint.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
//
// btnSave / btnCancel / btnResetDefaults
//
this.btnResetDefaults.Location = new System.Drawing.Point(12, 494);
this.btnResetDefaults.Size = new System.Drawing.Size(220, 30);
this.btnResetDefaults.Text = "Сбросить домен/шаблоны по умолчанию";
this.btnResetDefaults.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.Size = new System.Drawing.Size(90, 30);
this.btnCancel.Text = "Отмена";
this.btnCancel.UseVisualStyleBackColor = true;
this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
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.Size = new System.Drawing.Size(97, 30);
this.btnSave.Text = "Сохранить";
this.btnSave.UseVisualStyleBackColor = true;
this.btnSave.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// SettingsForm
//
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.Controls.Add(this.tabControl);
this.Controls.Add(this.btnResetDefaults);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.btnSave);
this.Text = "BulkArchiveMailer — настройки";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
this.MaximizeBox = true;
this.AcceptButton = null;
this.CancelButton = this.btnCancel;
this.tabControl.ResumeLayout(false);
this.tabGeneral.ResumeLayout(false);
this.tabGeneral.PerformLayout();
this.tabSshAccess.ResumeLayout(false);
this.tabSshAccess.PerformLayout();
this.tabConfigUpdate.ResumeLayout(false);
this.tabConfigUpdate.PerformLayout();
this.ResumeLayout(false);
}
}
}
@@ -0,0 +1,190 @@
using System;
using System.Windows.Forms;
using BulkArchiveMailer.Core;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace BulkArchiveMailer.AddIn.UI
{
/// <summary>
/// Archive folder, email domain, signature override, test-mode default,
/// and the two editable mail templates. Saves straight to
/// %APPDATA%\BulkArchiveMailer\config.json via AppSettings.
/// </summary>
public partial class SettingsForm : Form
{
private readonly Outlook.Application outlookApplication;
public SettingsForm(Outlook.Application outlookApplication)
{
InitializeComponent();
this.outlookApplication = outlookApplication;
Load += SettingsForm_Load;
}
private void SettingsForm_Load(object sender, EventArgs e)
{
AppSettingsData settings;
try
{
settings = AppSettings.Load();
}
catch (Exception ex)
{
MessageBox.Show(
"Не удалось прочитать config.json (" + AppSettings.ConfigFilePath + "):\n" + ex.Message +
"\n\nБудут показаны пустые настройки — сохранение перезапишет файл заново.",
"BulkArchiveMailer",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
settings = new AppSettingsData();
}
PopulateFrom(settings);
}
private void PopulateFrom(AppSettingsData settings)
{
txtArchiveFolder.Text = settings.ArchiveFolder ?? "";
txtDomain.Text = settings.EmailDomain ?? "";
txtSignaturePath.Text = settings.SignatureHtmlPath ?? "";
chkTestModeDefault.Checked = settings.TestModeDefault;
var templates = settings.ToTemplateSet();
txtSshSubject.Text = templates.SshAccess.Subject;
txtSshBody.Text = templates.SshAccess.BodyHtml;
txtCfgSubject.Text = templates.ConfigUpdate.Subject;
txtCfgBody.Text = templates.ConfigUpdate.BodyHtml;
}
private void btnBrowseFolder_Click(object sender, EventArgs e)
{
using (var dialog = new FolderBrowserDialog())
{
dialog.Description = "Папка с архивами, которые ssh-access-tui выкладывает для рассылки";
if (System.IO.Directory.Exists(txtArchiveFolder.Text))
{
dialog.SelectedPath = txtArchiveFolder.Text;
}
if (dialog.ShowDialog(this) == DialogResult.OK)
{
txtArchiveFolder.Text = dialog.SelectedPath;
}
}
}
private void btnBrowseSignature_Click(object sender, EventArgs e)
{
using (var dialog = new OpenFileDialog())
{
dialog.Title = "Выберите файл подписи";
dialog.Filter = "HTML-подпись (*.htm;*.html)|*.htm;*.html|Все файлы (*.*)|*.*";
if (dialog.ShowDialog(this) == DialogResult.OK)
{
txtSignaturePath.Text = dialog.FileName;
}
}
}
private void btnAutoDetectSignature_Click(object sender, EventArgs e)
{
if (outlookApplication == null)
{
MessageBox.Show(this, "Outlook.Application недоступен в этом окне.", "BulkArchiveMailer", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var path = SignatureReader.TryAutoDetectSignaturePath(outlookApplication);
if (string.IsNullOrEmpty(path))
{
MessageBox.Show(
this,
"Не удалось определить подпись автоматически (не настроена подпись по умолчанию для новых писем, либо файл не найден). Выберите файл вручную кнопкой \"Обзор...\".",
"BulkArchiveMailer",
MessageBoxButtons.OK,
MessageBoxIcon.Information);
return;
}
txtSignaturePath.Text = path;
}
private void btnResetDefaults_Click(object sender, EventArgs e)
{
var confirm = MessageBox.Show(
this,
"Заменить домен и оба шаблона писем значениями по умолчанию? Папка с архивами и файл подписи не изменятся. Несохранённые правки в этом окне будут потеряны.",
"Подтверждение",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
if (confirm != DialogResult.Yes)
{
return;
}
var defaults = AppSettings.LoadDefaults();
txtDomain.Text = defaults.EmailDomain;
var templates = defaults.ToTemplateSet();
txtSshSubject.Text = templates.SshAccess.Subject;
txtSshBody.Text = templates.SshAccess.BodyHtml;
txtCfgSubject.Text = templates.ConfigUpdate.Subject;
txtCfgBody.Text = templates.ConfigUpdate.BodyHtml;
}
private void btnSave_Click(object sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(txtDomain.Text))
{
MessageBox.Show(this, "Домен электронной почты не может быть пустым.", "BulkArchiveMailer", MessageBoxButtons.OK, MessageBoxIcon.Warning);
tabControl.SelectedTab = tabGeneral;
txtDomain.Focus();
return;
}
if (!string.IsNullOrWhiteSpace(txtArchiveFolder.Text) && !System.IO.Directory.Exists(txtArchiveFolder.Text))
{
var proceed = MessageBox.Show(
this,
"Указанная папка с архивами не существует:\n" + txtArchiveFolder.Text + "\n\nВсё равно сохранить?",
"BulkArchiveMailer",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (proceed != DialogResult.Yes)
{
tabControl.SelectedTab = tabGeneral;
return;
}
}
var data = new AppSettingsData
{
ArchiveFolder = txtArchiveFolder.Text.Trim(),
EmailDomain = txtDomain.Text.Trim(),
SignatureHtmlPath = txtSignaturePath.Text.Trim(),
TestModeDefault = chkTestModeDefault.Checked,
};
data.SetTemplate(ArchiveKind.SshAccess, new MailTemplate { Subject = txtSshSubject.Text, BodyHtml = txtSshBody.Text });
data.SetTemplate(ArchiveKind.ConfigUpdate, new MailTemplate { Subject = txtCfgSubject.Text, BodyHtml = txtCfgBody.Text });
try
{
AppSettings.Save(data);
}
catch (Exception ex)
{
MessageBox.Show(this, "Не удалось сохранить настройки: " + ex.Message, "BulkArchiveMailer", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
DialogResult = DialogResult.OK;
Close();
}
private void btnCancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Office.Tools.Common.v4.0.Utilities" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>