using GerbilManagerWebAPI.Models; using MailKit.Security; namespace GerbilManagerWebAPI.Inbox { /// /// Sends a (human-edited) reply to a Request via SMTP, threaded into the original Gmail /// conversation, and marks the request Answered. Only ever called on the explicit /// POST /api/requests/{id}/send — never auto-sends. /// public sealed class SendReplyService { private readonly ApplicationContext _db; private readonly IGmailMailSender _sender; private readonly MailSettingsService _settings; public SendReplyService(ApplicationContext db, IGmailMailSender sender, MailSettingsService settings) { _db = db; _sender = sender; _settings = settings; } public async Task SendAsync(Request request, string body, CancellationToken ct = default) { var settings = await _settings.GetAsync(ct); var password = _settings.DecryptPassword(settings); if (string.IsNullOrWhiteSpace(settings.GmailAddress) || string.IsNullOrWhiteSpace(password)) return SendOutcome.Fail("MailNotConfigured"); var mail = new OutgoingMail( To: request.FromAddress, Subject: ReplySubject(request.Subject), Body: body, InReplyTo: request.GmailMessageId, References: BuildReferences(request.ReferencesHeader, request.GmailMessageId)); try { await _sender.SendAsync(new MailConnection(settings.GmailAddress!, password!, settings.Folder), mail, ct); } catch (AuthenticationException) { return SendOutcome.Fail("MailAuthFailed"); } request.Status = RequestStatus.Answered; request.AnsweredAt = DateTimeOffset.UtcNow; request.DraftReply = body; // record what was actually sent await _db.SaveChangesAsync(ct); return SendOutcome.Ok; } /// "Re: " prefix without double-prefixing an existing reply subject. internal static string ReplySubject(string? original) { var s = (original ?? "").Trim(); if (s.Length == 0) return "Re:"; return s.StartsWith("Re:", StringComparison.OrdinalIgnoreCase) ? s : $"Re: {s}"; } /// References = existing chain + the original Message-Id (deduped, in order). internal static string BuildReferences(string? existingChain, string originalMessageId) { var ids = (existingChain ?? "") .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToList(); if (!string.IsNullOrWhiteSpace(originalMessageId) && !ids.Contains(originalMessageId)) ids.Add(originalMessageId); return string.Join(' ', ids); } } }