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>
249 lines
10 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|