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