add ProxyJump function
This commit is contained in:
1 parent
56e20e5464
commit
7f0e0fdb23
48 files changed
+3875
-44
No files matched your search
@@ -0,0 +1,340 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user