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:
2026-06-06 09:50:24 +02:00
parent dcf849cfa7
commit 68366ad8c9
7 changed files with 287 additions and 0 deletions

View File

@@ -27,6 +27,9 @@ namespace GerbilManagerWebAPI.Dtos
bool BackgroundPollEnabled,
bool HasAppPassword);
/// <summary>Body of the (edited) reply to send for POST /api/requests/{id}/send.</summary>
public record SendReplyInput(string Body);
/// <summary>Mail settings write shape. AppPassword is write-only; null/omitted leaves it unchanged,
/// empty string clears it.</summary>
public record MailSettingsInput(