INBOX-3: SMTP send + Gmail threading (POST /api/requests/{id}/send)
- IGmailMailSender (mockable) + MailKit GmailMailSender (smtp.gmail.com:587 STARTTLS, App
Password). SendReplyService builds a threaded reply: In-Reply-To = original Message-Id,
References = stored chain + original (deduped), Subject = 'Re: ' (no double-prefix); sends,
then sets Status=Answered + AnsweredAt + stores sent body in DraftReply. Never auto-sends.
- POST /api/requests/{id}/send {body}: 200 updated RequestDto; 404 unknown; 503 MailNotConfigured;
502 MailAuthFailed (UI hint: re-enter App Password). No entity change (no migration).
- Tests (fake SMTP): threading headers, Answered transition, no-double-Re, not-configured +
auth-failure keep status; BuildMimeMessage header mapping. Live send gated on App Password.
This commit is contained in:
43
GerbilManagerWebAPI/Inbox/GmailMailSender.cs
Normal file
43
GerbilManagerWebAPI/Inbox/GmailMailSender.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using MailKit.Net.Smtp;
|
||||
using MailKit.Security;
|
||||
using MimeKit;
|
||||
|
||||
namespace GerbilManagerWebAPI.Inbox
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Builds the reply MimeMessage with In-Reply-To / References / Subject set for
|
||||
/// Gmail threading. Public so the threading headers are unit-testable without SMTP.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user