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:
redlineoandClaude Sonnet 5 committed 2026-07-24 09:51:52 +03:00
1 parent 7f0e0fdb23
commit 264ed74095
20 files changed
+756 -67

No files matched your search

@@ -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 });