using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; namespace GerbilManagerWebAPI.Inbox { /// /// Live Gmail sender over SMTP (App Password auth, STARTTLS). Builds a correctly-threaded /// reply so it lands in the same Gmail conversation. Gated on Julian's App Password /// (tests use a fake IGmailMailSender + assert BuildMimeMessage). /// public sealed class GmailMailSender : IGmailMailSender { public async Task SendAsync(MailConnection connection, OutgoingMail mail, CancellationToken ct = default) { var message = BuildMimeMessage(connection.GmailAddress, mail); using var client = new SmtpClient(); await client.ConnectAsync("smtp.gmail.com", 587, SecureSocketOptions.StartTls, ct); await client.AuthenticateAsync(connection.GmailAddress, connection.AppPassword, ct); await client.SendAsync(message, ct); await client.DisconnectAsync(true, ct); } /// Builds the reply MimeMessage with In-Reply-To / References / Subject set for /// Gmail threading. Public so the threading headers are unit-testable without SMTP. public static MimeMessage BuildMimeMessage(string fromAddress, OutgoingMail mail) { var message = new MimeMessage(); message.From.Add(MailboxAddress.Parse(fromAddress)); message.To.Add(MailboxAddress.Parse(mail.To)); message.Subject = mail.Subject; message.Body = new TextPart("plain") { Text = mail.Body }; if (!string.IsNullOrWhiteSpace(mail.InReplyTo)) message.InReplyTo = mail.InReplyTo; if (!string.IsNullOrWhiteSpace(mail.References)) foreach (var id in mail.References.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) message.References.Add(id); return message; } } }