using System; using System.Runtime.InteropServices; using BulkArchiveMailer.Core; using Outlook = Microsoft.Office.Interop.Outlook; namespace BulkArchiveMailer.AddIn { /// Per-job result of a send attempt, returned by MailSender so the caller can log + report it without any of this class's own exceptions ever escaping. public sealed class MailSendOutcome { public SendJob Job { get; set; } public bool Success { get; set; } public string ErrorMessage { get; set; } public bool WasDraft { get; set; } } /// /// Creates and sends (or, in test mode, saves as a Draft) one Outlook /// mail item per SendJob via Interop. This is the only file that /// actually creates/sends a MailItem - everything else builds up to /// calling SendOne. /// /// SendOne never throws: every failure is caught and reported back as /// a failed MailSendOutcome, so a caller looping over a batch can /// always move on to the next job without a try/catch of its own. /// public sealed class MailSender { private readonly Outlook.Application application; private readonly string signatureHtml; private readonly string fromAccountSmtpAddress; /// Live Outlook.Application (from ThisAddIn.Application) used to create mail items. /// /// HTML of the operator's default signature, already read from /// disk by SignatureReader (Outlook does not auto-insert a /// signature on programmatically created items - it must be /// appended to HTMLBody by hand). May be null/empty if no /// signature is configured; the mail is then sent without one. /// /// /// SmtpAddress of the Outlook account (from Session.Accounts) to /// send from, e.g. a shared mailbox distinct from the operator's /// own primary account (AppSettingsData.FromAccountSmtpAddress). /// Null/empty means "use whatever account Outlook picks by /// default" - the previous, unconfigurable behavior. /// public MailSender(Outlook.Application application, string signatureHtml, string fromAccountSmtpAddress = null) { this.application = application ?? throw new ArgumentNullException(nameof(application)); this.signatureHtml = signatureHtml ?? string.Empty; this.fromAccountSmtpAddress = fromAccountSmtpAddress ?? string.Empty; } /// Row to send. Caller is responsible for only passing selectable (Ready/AlreadySent), checked rows - this method does not re-validate Status. /// When true, the mail item is Save()'d (left in Drafts) instead of Send()'d - the safety net for trying the tool out before trusting it with real recipients. public MailSendOutcome SendOne(SendJob job, bool testMode) { if (job == null) { throw new ArgumentNullException(nameof(job)); } Outlook.MailItem mailItem = null; try { mailItem = (Outlook.MailItem)application.CreateItem(Outlook.OlItemType.olMailItem); mailItem.To = job.Email; mailItem.Subject = job.Subject; if (!string.IsNullOrEmpty(fromAccountSmtpAddress)) { var account = TryFindAccountBySmtpAddress(fromAccountSmtpAddress); if (account != null) { try { mailItem.SendUsingAccount = account; } finally { Marshal.ReleaseComObject(account); } } // Account no longer present in this Outlook session // (removed/renamed since it was configured) - fall // through and let Outlook use its own default account, // same as when the setting is empty. } var body = job.BodyHtml ?? string.Empty; if (!string.IsNullOrEmpty(signatureHtml)) { body += "

" + signatureHtml; } mailItem.HTMLBody = body; if (!string.IsNullOrEmpty(job.ArchivePath)) { mailItem.Attachments.Add(job.ArchivePath, Outlook.OlAttachmentType.olByValue, Type.Missing, Type.Missing); } if (testMode) { mailItem.Save(); } else { mailItem.Send(); } return new MailSendOutcome { Job = job, Success = true, WasDraft = testMode }; } catch (Exception ex) { return new MailSendOutcome { Job = job, Success = false, ErrorMessage = ex.Message, WasDraft = testMode }; } finally { if (mailItem != null) { Marshal.ReleaseComObject(mailItem); } } } /// /// Looks up a live Outlook.Account in the current session by /// SmtpAddress (case-insensitive). Returns null - never throws - /// if the account isn't found or Session/Accounts can't be read /// for any reason, so a stale/renamed configured account degrades /// to "use Outlook's default" instead of failing the send. /// /// Ownership: the returned Account (if any) is a fresh RCW the /// caller is responsible for releasing via /// Marshal.ReleaseComObject once done with it, matching the /// release-what-you-touch convention used throughout this class /// and in SignatureReader. /// private Outlook.Account TryFindAccountBySmtpAddress(string smtpAddress) { Outlook.Accounts accounts = null; try { accounts = application.Session.Accounts; foreach (Outlook.Account candidate in accounts) { bool isMatch; try { isMatch = string.Equals(candidate.SmtpAddress, smtpAddress, StringComparison.OrdinalIgnoreCase); } catch { isMatch = false; } if (isMatch) { return candidate; // ownership passed to the caller } Marshal.ReleaseComObject(candidate); } return null; } catch { return null; } finally { if (accounts != null) { Marshal.ReleaseComObject(accounts); } // application.Session (the NameSpace) is a long-lived // object owned by Outlook itself - like `application`, it // is never released here. } } } }