Files
ssh-access-tui/outlook-bulk-sender/BulkArchiveMailer.AddIn/UI/SettingsForm.cs
T
redlineoandClaude Sonnet 5 264ed74095 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>
2026-07-24 09:51:52 +03:00

249 lines
10 KiB
C#

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);
}
/// <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 ?? "";
txtDomain.Text = settings.EmailDomain ?? "";
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;
txtCfgSubject.Text = templates.ConfigUpdate.Subject;
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())
{
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 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 });
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();
}
}
}