Files
GerbilManager/GerbilManagerWebAPI/Inbox/GmailMailReader.cs
Gulum 5d7cbc66bf INBOX-0: Gmail request inbox backend (MailKit) + Request entity + sync/triage endpoints
- Request entity (GmailMessageId unique, ThreadId, threading headers, From*, Subject, BodyText,
  ReceivedAt, Status enum-as-string New|InProgress|Assigned|Answered|Abandoned, AssignedContactId? FK,
  DraftReply?, AnsweredAt?). MailSettings singleton (GmailAddress, AppPassword ENCRYPTED via Data
  Protection, PollInterval, Folder, LastUid). Migration AddRequestsAndMailSettings.
- IGmailMailReader (mockable) + MailKit GmailMailReader (imap.gmail.com:993 SSL, envelope+X-GM-THRID
  +body). RequestSyncService: dedup on Message-Id, track LastUid. MailKit 4.17.0.
- Endpoints: POST /api/requests/sync, GET /api/requests (Gridify, filter status), GET /api/requests/{id},
  PUT triage (status+assignedContactId), GET/PUT /api/mail-settings (App Password never returned).
- Tests (in-memory DB + canned reader): sync dedup/idempotency/config-guard, HTTP sync->triage->assign,
  mail-settings secrecy. + gitignore the runtime photo-storage/ dir (god housekeeping).
2026-06-06 09:39:36 +02:00

59 lines
2.7 KiB
C#

using MailKit;
using MailKit.Net.Imap;
using MailKit.Search;
using MailKit.Security;
namespace GerbilManagerWebAPI.Inbox
{
/// <summary>
/// Live Gmail reader over IMAP (App Password auth). Reads UIDs newer than the last seen,
/// fetches envelope + Gmail thread id + the plain-text body. Gated on Julian's App Password
/// (tests use a fake IGmailMailReader).
/// </summary>
public sealed class GmailMailReader : IGmailMailReader
{
public async Task<IReadOnlyList<MailSummary>> FetchAsync(MailConnection connection, uint sinceUid, CancellationToken ct = default)
{
using var client = new ImapClient();
await client.ConnectAsync("imap.gmail.com", 993, SecureSocketOptions.SslOnConnect, ct);
await client.AuthenticateAsync(connection.GmailAddress, connection.AppPassword, ct);
var folder = string.Equals(connection.Folder, "INBOX", StringComparison.OrdinalIgnoreCase)
? client.Inbox
: await client.GetFolderAsync(connection.Folder, ct);
await folder.OpenAsync(FolderAccess.ReadOnly, ct);
var range = new UniqueIdRange(new UniqueId(sinceUid + 1), UniqueId.MaxValue);
var uids = await folder.SearchAsync(SearchQuery.Uids(range), ct);
const MessageSummaryItems items = MessageSummaryItems.Envelope
| MessageSummaryItems.UniqueId
| MessageSummaryItems.GMailThreadId
| MessageSummaryItems.InternalDate;
var summaries = await folder.FetchAsync(uids, items, ct);
var result = new List<MailSummary>();
foreach (var s in summaries)
{
var env = s.Envelope;
var msg = await folder.GetMessageAsync(s.UniqueId, ct); // body + threading headers
var from = env?.From?.Mailboxes?.FirstOrDefault();
result.Add(new MailSummary(
MessageId: env?.MessageId ?? msg.MessageId ?? "",
ThreadId: s.GMailThreadId?.ToString(),
InReplyTo: msg.InReplyTo,
References: msg.References is { Count: > 0 } ? string.Join(' ', msg.References) : null,
FromAddress: from?.Address ?? "",
FromName: string.IsNullOrWhiteSpace(from?.Name) ? null : from!.Name,
Subject: env?.Subject ?? msg.Subject,
BodyText: msg.TextBody,
ReceivedAt: s.InternalDate ?? env?.Date ?? DateTimeOffset.UtcNow,
Uid: s.UniqueId.Id));
}
await client.DisconnectAsync(true, ct);
return result;
}
}
}