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;
}
}
}