Fix first real VSTO build, add sender account selection, polish deploy
First live build against real Visual Studio/VSTO tooling surfaced two issues in the hand-written AddIn scaffold: ThisAddIn derived from the wrong base type (OutlookAddIn is an interface, not the concrete OutlookAddInBase class), and VSTO Application projects cannot build without a signed ClickOnce manifest, so SignManifests is now on with a throwaway self-signed test certificate. Both Core/Core.Tests/AddIn now build clean and Core.Tests passes. Also: mail always sent from Outlook's default account with no way to pick a different one (e.g. a shared mailbox) - added a "From account" setting (Session.Accounts dropdown in SettingsForm, SendUsingAccount in MailSender). And added deploy/ with both a local ClickOnce publish script (real install/uninstall via Windows Apps, no manual registry editing) and a manual registry sideload fallback for scripted rollouts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7f0e0fdb23
commit
264ed74095
20 files changed
+756
-67
No files matched your search
@@ -12,7 +12,7 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>BulkArchiveMailer.AddIn</RootNamespace>
|
||||
<AssemblyName>BulkArchiveMailer.AddIn</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<TargetFrameworkVersion>v4.8.1</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{BAA0C2D2-18E2-41B9-852F-F413020CAA33};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<LangVersion>7.3</LangVersion>
|
||||
@@ -25,19 +25,47 @@
|
||||
<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.
|
||||
Same MSBuild properties Visual Studio's Publish → Options dialog
|
||||
writes here - without them ClickOnce falls back to the bare
|
||||
assembly name ("BulkArchiveMailer.AddIn", from TargetName - see
|
||||
Microsoft.VisualStudio.Tools.Office.targets'
|
||||
"<ProductName Condition=... >$(TargetName)</ProductName>") both in
|
||||
the install dialog and in Settings → Apps, which reads as
|
||||
unfinished/unbranded to an operator installing this for the first
|
||||
time. Property name is ProductName, NOT Product - Product is not a
|
||||
recognized ClickOnce/VSTO manifest property and is silently ignored,
|
||||
confirmed by grepping the generated .vsto after a real -t:Publish
|
||||
(see deploy/publish-clickonce.ps1).
|
||||
-->
|
||||
<ProductName>BulkArchiveMailer — массовая рассылка архивов</ProductName>
|
||||
<PublisherName>ssh-access-tui</PublisherName>
|
||||
|
||||
<!--
|
||||
Corrected after the first real build against actual VSTO/MSBuild
|
||||
tooling: unlike a plain ClickOnce EXE, a VSTO Office add-in's
|
||||
"VisualStudioForApplicationsBuild" build step (Microsoft.VisualStudio.
|
||||
Tools.Office.targets) unconditionally generates and signs a ClickOnce
|
||||
application/deployment manifest even for local Debug builds -
|
||||
SignManifests=false makes VerifyClickOnceSigningSettings fail the
|
||||
build outright ("Cannot build because the ClickOnce manifest signing
|
||||
option is not selected"). There is no way to opt out of this for an
|
||||
Application-type VSTO project, so a throwaway self-signed "test"
|
||||
certificate (BulkArchiveMailer.AddIn_TemporaryKey.pfx, no password,
|
||||
not from a trusted CA) is checked into this folder purely to satisfy
|
||||
that requirement - the same thing VS itself generates automatically
|
||||
the first time you tick "Sign the ClickOnce manifests" in the
|
||||
project's Signing property page. It does NOT make the add-in
|
||||
"trusted": Outlook still shows an "unable to verify publisher"-style
|
||||
prompt on first load because the certificate isn't from a trusted
|
||||
root, same as before this fix - docs/BUILD.md still covers how to
|
||||
accept that prompt or add a Trusted Location. Assembly strong-name
|
||||
signing (SignAssembly) is a separate, unrelated concept and stays
|
||||
off - nothing requires it.
|
||||
-->
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<SignManifests>false</SignManifests>
|
||||
<SignManifests>true</SignManifests>
|
||||
<ManifestKeyFile>BulkArchiveMailer.AddIn_TemporaryKey.pfx</ManifestKeyFile>
|
||||
<ManifestCertificateThumbprint>9A9AF6F0E6EF09093910BD0FA2CCFFEC81F00E25</ManifestCertificateThumbprint>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
@@ -95,6 +123,19 @@
|
||||
<Reference Include="Microsoft.Office.Tools.Outlook, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
</Reference>
|
||||
<!--
|
||||
Microsoft.Office.Tools.Outlook.dll only exposes the OutlookAddIn
|
||||
*interface* (FormRegion-related members). The real concrete base
|
||||
class ThisAddIn derives from, Microsoft.Office.Tools.Outlook.
|
||||
OutlookAddInBase, lives in this "Utilities" assembly instead - same
|
||||
as the built-in "Outlook VSTO Add-in" project template, which also
|
||||
references it with Private=True (runtime-only helper, not part of
|
||||
the public design-time surface).
|
||||
-->
|
||||
<Reference Include="Microsoft.Office.Tools.Outlook.v4.0.Utilities, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Office.Tools.v4.0.Framework, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
</Reference>
|
||||
@@ -111,6 +152,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="Globals.cs" />
|
||||
<Compile Include="MailSender.cs" />
|
||||
<Compile Include="OutlookAccounts.cs" />
|
||||
<Compile Include="RibbonController.cs" />
|
||||
<Compile Include="SignatureReader.cs" />
|
||||
<Compile Include="ThisAddIn.cs" />
|
||||
@@ -136,6 +178,15 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config" />
|
||||
<!--
|
||||
Test certificate for ClickOnce manifest signing only (see SignManifests
|
||||
comment above) - no password. VSTO's ManageCertificateStore build task
|
||||
picks up *.pfx files listed here and imports them into the current
|
||||
user's certificate store (CurrentUser\My) if a certificate matching
|
||||
ManifestCertificateThumbprint isn't already present, so a fresh clone
|
||||
builds without any manual "import the cert" step.
|
||||
-->
|
||||
<None Include="BulkArchiveMailer.AddIn_TemporaryKey.pfx" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(VSToolsPath)\OfficeTools\Microsoft.VisualStudio.Tools.Office.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
|
||||
Binary file not shown.
@@ -5,10 +5,50 @@ namespace BulkArchiveMailer.AddIn
|
||||
/// shape Visual Studio normally generates for VSTO projects (hand
|
||||
/// written here for the same reason as ThisAddIn.Designer.cs - see
|
||||
/// its header comment).
|
||||
///
|
||||
/// <c>Microsoft.Office.Tools.Outlook.OutlookAddIn</c> has no static
|
||||
/// "GetInstance" helper (it is just the FormRegion-related interface
|
||||
/// implemented by the real base class, OutlookAddInBase) - the
|
||||
/// running instance is instead captured directly by
|
||||
/// ThisAddIn.Initialize() into <see cref="ThisAddIn"/> below, exactly
|
||||
/// like the real VSTO-generated Globals class does via its
|
||||
/// <c>Globals.ThisAddIn = this;</c> assignment.
|
||||
/// </summary>
|
||||
internal static class Globals
|
||||
{
|
||||
internal static ThisAddIn ThisAddIn =>
|
||||
(ThisAddIn)Microsoft.Office.Tools.Outlook.OutlookAddIn.GetInstance(typeof(ThisAddIn));
|
||||
private static ThisAddIn _thisAddIn;
|
||||
private static Microsoft.Office.Tools.Outlook.Factory _factory;
|
||||
|
||||
internal static ThisAddIn ThisAddIn
|
||||
{
|
||||
get { return _thisAddIn; }
|
||||
set
|
||||
{
|
||||
if (_thisAddIn == null)
|
||||
{
|
||||
_thisAddIn = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new System.NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static Microsoft.Office.Tools.Outlook.Factory Factory
|
||||
{
|
||||
get { return _factory; }
|
||||
set
|
||||
{
|
||||
if (_factory == null)
|
||||
{
|
||||
_factory = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new System.NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ namespace BulkArchiveMailer.AddIn
|
||||
{
|
||||
private readonly Outlook.Application application;
|
||||
private readonly string signatureHtml;
|
||||
private readonly string fromAccountSmtpAddress;
|
||||
|
||||
/// <param name="application">Live Outlook.Application (from ThisAddIn.Application) used to create mail items.</param>
|
||||
/// <param name="signatureHtml">
|
||||
@@ -37,10 +38,18 @@ namespace BulkArchiveMailer.AddIn
|
||||
/// 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)
|
||||
/// <param name="fromAccountSmtpAddress">
|
||||
/// SmtpAddress of the Outlook account (from Session.Accounts) to
|
||||
/// send from, e.g. a shared mailbox distinct from the operator's
|
||||
/// own primary account (AppSettingsData.FromAccountSmtpAddress).
|
||||
/// Null/empty means "use whatever account Outlook picks by
|
||||
/// default" - the previous, unconfigurable behavior.
|
||||
/// </param>
|
||||
public MailSender(Outlook.Application application, string signatureHtml, string fromAccountSmtpAddress = null)
|
||||
{
|
||||
this.application = application ?? throw new ArgumentNullException(nameof(application));
|
||||
this.signatureHtml = signatureHtml ?? string.Empty;
|
||||
this.fromAccountSmtpAddress = fromAccountSmtpAddress ?? 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>
|
||||
@@ -59,6 +68,26 @@ namespace BulkArchiveMailer.AddIn
|
||||
mailItem.To = job.Email;
|
||||
mailItem.Subject = job.Subject;
|
||||
|
||||
if (!string.IsNullOrEmpty(fromAccountSmtpAddress))
|
||||
{
|
||||
var account = TryFindAccountBySmtpAddress(fromAccountSmtpAddress);
|
||||
if (account != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
mailItem.SendUsingAccount = account;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ReleaseComObject(account);
|
||||
}
|
||||
}
|
||||
// Account no longer present in this Outlook session
|
||||
// (removed/renamed since it was configured) - fall
|
||||
// through and let Outlook use its own default account,
|
||||
// same as when the setting is empty.
|
||||
}
|
||||
|
||||
var body = job.BodyHtml ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(signatureHtml))
|
||||
{
|
||||
@@ -95,5 +124,63 @@ namespace BulkArchiveMailer.AddIn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a live Outlook.Account in the current session by
|
||||
/// SmtpAddress (case-insensitive). Returns null - never throws -
|
||||
/// if the account isn't found or Session/Accounts can't be read
|
||||
/// for any reason, so a stale/renamed configured account degrades
|
||||
/// to "use Outlook's default" instead of failing the send.
|
||||
///
|
||||
/// Ownership: the returned Account (if any) is a fresh RCW the
|
||||
/// caller is responsible for releasing via
|
||||
/// Marshal.ReleaseComObject once done with it, matching the
|
||||
/// release-what-you-touch convention used throughout this class
|
||||
/// and in SignatureReader.
|
||||
/// </summary>
|
||||
private Outlook.Account TryFindAccountBySmtpAddress(string smtpAddress)
|
||||
{
|
||||
Outlook.Accounts accounts = null;
|
||||
try
|
||||
{
|
||||
accounts = application.Session.Accounts;
|
||||
foreach (Outlook.Account candidate in accounts)
|
||||
{
|
||||
bool isMatch;
|
||||
try
|
||||
{
|
||||
isMatch = string.Equals(candidate.SmtpAddress, smtpAddress, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch
|
||||
{
|
||||
isMatch = false;
|
||||
}
|
||||
|
||||
if (isMatch)
|
||||
{
|
||||
return candidate; // ownership passed to the caller
|
||||
}
|
||||
|
||||
Marshal.ReleaseComObject(candidate);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (accounts != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(accounts);
|
||||
}
|
||||
|
||||
// application.Session (the NameSpace) is a long-lived
|
||||
// object owned by Outlook itself - like `application`, it
|
||||
// is never released here.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using Outlook = Microsoft.Office.Interop.Outlook;
|
||||
|
||||
namespace BulkArchiveMailer.AddIn
|
||||
{
|
||||
/// <summary>Read-only snapshot of one Outlook.Account, taken once while the COM object was still alive - safe to hold onto and display after the Account RCW itself has been released.</summary>
|
||||
public sealed class OutlookAccountInfo
|
||||
{
|
||||
public string DisplayName { get; set; } = "";
|
||||
public string SmtpAddress { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates Outlook.Application.Session.Accounts for UI purposes
|
||||
/// (the "Отправлять от:" picker in SettingsForm) without leaking any
|
||||
/// COM references - mirrors the release-what-you-touch convention used
|
||||
/// in MailSender/SignatureReader. Never throws: on any failure it
|
||||
/// returns an empty list so a picker can simply fall back to "default
|
||||
/// account only" instead of crashing the Settings window.
|
||||
/// </summary>
|
||||
public static class OutlookAccounts
|
||||
{
|
||||
public static List<OutlookAccountInfo> ListAccounts(Outlook.Application application)
|
||||
{
|
||||
var result = new List<OutlookAccountInfo>();
|
||||
if (application == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
Outlook.Accounts accounts = null;
|
||||
try
|
||||
{
|
||||
accounts = application.Session.Accounts;
|
||||
foreach (Outlook.Account account in accounts)
|
||||
{
|
||||
try
|
||||
{
|
||||
result.Add(new OutlookAccountInfo
|
||||
{
|
||||
DisplayName = account.DisplayName ?? "",
|
||||
SmtpAddress = account.SmtpAddress ?? "",
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip an individual account we couldn't read
|
||||
// rather than losing the rest of the list.
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ReleaseComObject(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<OutlookAccountInfo>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (accounts != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(accounts);
|
||||
}
|
||||
|
||||
// application.Session (NameSpace) is a long-lived object
|
||||
// owned by Outlook itself - never released here, same as
|
||||
// in MailSender.
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
-11
@@ -1,24 +1,68 @@
|
||||
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.
|
||||
/// Designer-generated half of ThisAddIn. Normally Visual Studio's VSTO
|
||||
/// project system regenerates this file automatically from
|
||||
/// ThisAddIn.Designer.xml the first time the project is opened/edited
|
||||
/// in the IDE (host-item "blueprint" mechanism) - it is hand-written
|
||||
/// here only because this environment had no VSTO tooling to run that
|
||||
/// generator, and later corrected to match the real base class after
|
||||
/// the first build against the actual VSTO assemblies (see git log).
|
||||
///
|
||||
/// The real base class is NOT the <c>Microsoft.Office.Tools.Outlook.OutlookAddIn</c>
|
||||
/// interface (that only declares the FormRegion-related members) but
|
||||
/// <c>Microsoft.Office.Tools.Outlook.OutlookAddInBase</c> - a concrete
|
||||
/// abstract class living in Microsoft.Office.Tools.Outlook.v4.0.Utilities.dll
|
||||
/// (referenced with SpecificVersion=False/Private=True in the .csproj,
|
||||
/// same as the built-in "Outlook VSTO Add-in" project template), which
|
||||
/// itself derives from Microsoft.Office.Tools.AddInBase and already
|
||||
/// implements OutlookAddIn/AddIn/IBindableComponent/IComponent/
|
||||
/// IDisposable, plus exposes the protected virtual
|
||||
/// CreateRibbonExtensibilityObject() overridden in ThisAddIn.cs.
|
||||
///
|
||||
/// Do not hand-edit once opened in VS if the IDE regenerates it
|
||||
/// differently.
|
||||
/// </summary>
|
||||
partial class ThisAddIn : Microsoft.Office.Tools.Outlook.OutlookAddIn
|
||||
[Microsoft.VisualStudio.Tools.Applications.Runtime.StartupObjectAttribute(0)]
|
||||
[global::System.Security.Permissions.PermissionSetAttribute(global::System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")]
|
||||
public sealed partial class ThisAddIn : Microsoft.Office.Tools.Outlook.OutlookAddInBase
|
||||
{
|
||||
/// <summary>Populated from the host item in <see cref="Initialize"/>; exposed to the rest of the add-in via the OutlookApplication alias in ThisAddIn.cs.</summary>
|
||||
internal Microsoft.Office.Interop.Outlook.Application Application;
|
||||
|
||||
/// <summary>
|
||||
/// Required constructor for VSTO add-in support - do not modify
|
||||
/// the contents of this method with the code editor. The VSTO
|
||||
/// runtime instantiates ThisAddIn through this constructor via
|
||||
/// reflection (see the StartupObjectAttribute above).
|
||||
/// </summary>
|
||||
public ThisAddIn(Microsoft.Office.Tools.Outlook.Factory factory, System.IServiceProvider serviceProvider)
|
||||
: base(factory, serviceProvider, "AddIn", "ThisAddIn")
|
||||
{
|
||||
Globals.Factory = factory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify the
|
||||
/// contents of this method with the code editor.
|
||||
/// </summary>
|
||||
private void InternalStartup()
|
||||
protected override void Initialize()
|
||||
{
|
||||
this.Startup += new System.EventHandler(ThisAddIn_Startup);
|
||||
this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
|
||||
base.Initialize();
|
||||
this.Application = this.GetHostItem<Microsoft.Office.Interop.Outlook.Application>(
|
||||
typeof(Microsoft.Office.Interop.Outlook.Application), "Application");
|
||||
Globals.ThisAddIn = this;
|
||||
global::System.Windows.Forms.Application.EnableVisualStyles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify the
|
||||
/// contents of this method with the code editor.
|
||||
/// </summary>
|
||||
protected override void FinishInitialization()
|
||||
{
|
||||
this.InternalStartup();
|
||||
this.OnStartup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,5 +28,20 @@ namespace BulkArchiveMailer.AddIn
|
||||
{
|
||||
return new RibbonController();
|
||||
}
|
||||
|
||||
#region VSTO generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify the
|
||||
/// contents of this method with the code editor. Called from
|
||||
/// ThisAddIn.Designer.cs's FinishInitialization() override.
|
||||
/// </summary>
|
||||
private void InternalStartup()
|
||||
{
|
||||
this.Startup += new EventHandler(ThisAddIn_Startup);
|
||||
this.Shutdown += new EventHandler(ThisAddIn_Shutdown);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -215,7 +215,7 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
progressSend.Maximum = toSend.Count;
|
||||
progressSend.Value = 0;
|
||||
|
||||
var mailSender = new MailSender(outlookApplication, signatureHtml);
|
||||
var mailSender = new MailSender(outlookApplication, signatureHtml, settings.FromAccountSmtpAddress);
|
||||
var sentCount = 0;
|
||||
var failedCount = 0;
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
|
||||
private System.Windows.Forms.CheckBox chkTestModeDefault;
|
||||
|
||||
private System.Windows.Forms.Label lblFromAccount;
|
||||
private System.Windows.Forms.ComboBox cboFromAccount;
|
||||
|
||||
private System.Windows.Forms.Label lblSshSubject;
|
||||
private System.Windows.Forms.TextBox txtSshSubject;
|
||||
private System.Windows.Forms.Label lblSshBody;
|
||||
@@ -70,6 +73,9 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
|
||||
this.chkTestModeDefault = new System.Windows.Forms.CheckBox();
|
||||
|
||||
this.lblFromAccount = new System.Windows.Forms.Label();
|
||||
this.cboFromAccount = new System.Windows.Forms.ComboBox();
|
||||
|
||||
this.lblSshSubject = new System.Windows.Forms.Label();
|
||||
this.txtSshSubject = new System.Windows.Forms.TextBox();
|
||||
this.lblSshBody = new System.Windows.Forms.Label();
|
||||
@@ -121,6 +127,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
this.tabGeneral.Controls.Add(this.btnBrowseSignature);
|
||||
this.tabGeneral.Controls.Add(this.btnAutoDetectSignature);
|
||||
this.tabGeneral.Controls.Add(this.chkTestModeDefault);
|
||||
this.tabGeneral.Controls.Add(this.lblFromAccount);
|
||||
this.tabGeneral.Controls.Add(this.cboFromAccount);
|
||||
|
||||
// lblArchiveFolder
|
||||
this.lblArchiveFolder.Location = new System.Drawing.Point(15, 20);
|
||||
@@ -176,6 +184,16 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
this.chkTestModeDefault.Text = "По умолчанию открывать предпросмотр в тестовом режиме (сохранять в Черновики)";
|
||||
this.chkTestModeDefault.UseVisualStyleBackColor = true;
|
||||
|
||||
// lblFromAccount
|
||||
this.lblFromAccount.Location = new System.Drawing.Point(15, 284);
|
||||
this.lblFromAccount.Size = new System.Drawing.Size(400, 20);
|
||||
this.lblFromAccount.Text = "Отправлять от (аккаунт Outlook текущей сессии):";
|
||||
|
||||
// cboFromAccount
|
||||
this.cboFromAccount.Location = new System.Drawing.Point(15, 307);
|
||||
this.cboFromAccount.Size = new System.Drawing.Size(500, 23);
|
||||
this.cboFromAccount.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
|
||||
//
|
||||
// tabSshAccess
|
||||
//
|
||||
|
||||
@@ -42,6 +42,16 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
PopulateFrom(settings);
|
||||
}
|
||||
|
||||
/// <summary>ComboBox item: displays a friendly label but carries the SmtpAddress that actually gets saved to config.json. The first item (SmtpAddress == "") represents "let Outlook pick its default account".</summary>
|
||||
private sealed class FromAccountItem
|
||||
{
|
||||
public string Label { get; set; } = "";
|
||||
public string SmtpAddress { get; set; } = "";
|
||||
public override string ToString() => Label;
|
||||
}
|
||||
|
||||
private const string DefaultAccountLabel = "(по умолчанию Outlook)";
|
||||
|
||||
private void PopulateFrom(AppSettingsData settings)
|
||||
{
|
||||
txtArchiveFolder.Text = settings.ArchiveFolder ?? "";
|
||||
@@ -49,6 +59,8 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
txtSignaturePath.Text = settings.SignatureHtmlPath ?? "";
|
||||
chkTestModeDefault.Checked = settings.TestModeDefault;
|
||||
|
||||
PopulateFromAccountCombo(settings.FromAccountSmtpAddress ?? "");
|
||||
|
||||
var templates = settings.ToTemplateSet();
|
||||
txtSshSubject.Text = templates.SshAccess.Subject;
|
||||
txtSshBody.Text = templates.SshAccess.BodyHtml;
|
||||
@@ -56,6 +68,49 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
txtCfgBody.Text = templates.ConfigUpdate.BodyHtml;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills cboFromAccount with "(по умолчанию Outlook)" plus one
|
||||
/// entry per real Outlook.Account in the current session, then
|
||||
/// selects the item matching savedSmtpAddress. If that address no
|
||||
/// longer belongs to any account in this session (removed/renamed
|
||||
/// since it was configured, or Outlook.Application is unavailable
|
||||
/// e.g. under a design-time host), it just falls back to the
|
||||
/// default item instead of throwing - never blocks opening the
|
||||
/// Settings window over this.
|
||||
/// </summary>
|
||||
private void PopulateFromAccountCombo(string savedSmtpAddress)
|
||||
{
|
||||
cboFromAccount.Items.Clear();
|
||||
cboFromAccount.Items.Add(new FromAccountItem { Label = DefaultAccountLabel, SmtpAddress = "" });
|
||||
|
||||
if (outlookApplication != null)
|
||||
{
|
||||
foreach (var account in OutlookAccounts.ListAccounts(outlookApplication))
|
||||
{
|
||||
var label = string.IsNullOrEmpty(account.SmtpAddress)
|
||||
? account.DisplayName
|
||||
: account.DisplayName + " <" + account.SmtpAddress + ">";
|
||||
cboFromAccount.Items.Add(new FromAccountItem { Label = label, SmtpAddress = account.SmtpAddress });
|
||||
}
|
||||
}
|
||||
|
||||
var selectedIndex = 0;
|
||||
if (!string.IsNullOrEmpty(savedSmtpAddress))
|
||||
{
|
||||
for (var i = 0; i < cboFromAccount.Items.Count; i++)
|
||||
{
|
||||
var item = (FromAccountItem)cboFromAccount.Items[i];
|
||||
if (string.Equals(item.SmtpAddress, savedSmtpAddress, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cboFromAccount.SelectedIndex = selectedIndex;
|
||||
}
|
||||
|
||||
private void btnBrowseFolder_Click(object sender, EventArgs e)
|
||||
{
|
||||
using (var dialog = new FolderBrowserDialog())
|
||||
@@ -157,12 +212,15 @@ namespace BulkArchiveMailer.AddIn.UI
|
||||
}
|
||||
}
|
||||
|
||||
var selectedAccount = cboFromAccount.SelectedItem as FromAccountItem;
|
||||
|
||||
var data = new AppSettingsData
|
||||
{
|
||||
ArchiveFolder = txtArchiveFolder.Text.Trim(),
|
||||
EmailDomain = txtDomain.Text.Trim(),
|
||||
SignatureHtmlPath = txtSignaturePath.Text.Trim(),
|
||||
TestModeDefault = chkTestModeDefault.Checked,
|
||||
FromAccountSmtpAddress = selectedAccount?.SmtpAddress ?? "",
|
||||
};
|
||||
data.SetTemplate(ArchiveKind.SshAccess, new MailTemplate { Subject = txtSshSubject.Text, BodyHtml = txtSshBody.Text });
|
||||
data.SetTemplate(ArchiveKind.ConfigUpdate, new MailTemplate { Subject = txtCfgSubject.Text, BodyHtml = txtCfgBody.Text });
|
||||
|
||||
Reference in new issue
Block a user