Files
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

67 lines
2.4 KiB
C#

using System.ComponentModel.DataAnnotations;
namespace GerbilManagerWebAPI.Models
{
/// <summary>Triage state of an incoming Gmail request (serialised as string name).</summary>
public enum RequestStatus
{
New = 0,
InProgress = 1,
Assigned = 2,
Answered = 3,
Abandoned = 4,
}
/// <summary>
/// An incoming inquiry imported from Gmail (INBOX epic). One row per email,
/// deduped on the RFC Message-Id.
/// </summary>
public class Request
{
[Key]
public Guid Id { get; set; }
/// <summary>RFC 5322 Message-Id — unique; the dedup key.</summary>
public required string GmailMessageId { get; set; }
/// <summary>Gmail conversation id (X-GM-THRID).</summary>
public string? ThreadId { get; set; }
public string? InReplyToMessageId { get; set; }
public string? ReferencesHeader { get; set; }
public required string FromAddress { get; set; }
public string? FromName { get; set; }
public string? Subject { get; set; }
public string? BodyText { get; set; }
public DateTimeOffset ReceivedAt { get; set; }
public RequestStatus Status { get; set; } = RequestStatus.New;
public Guid? AssignedContactId { get; set; }
public Contact? AssignedContact { get; set; }
/// <summary>AI/edited draft reply (INBOX-2); send is INBOX-3. Null until drafted.</summary>
public string? DraftReply { get; set; }
public DateTimeOffset? AnsweredAt { get; set; }
}
/// <summary>
/// Singleton mail configuration (INBOX epic). The Gmail App Password is stored
/// ENCRYPTED at rest (ASP.NET Data Protection) and never returned over the wire.
/// </summary>
public class MailSettings
{
public static readonly Guid SingletonId = new("ab0c0000-0000-0000-0000-000000000001");
[Key]
public Guid Id { get; set; }
public string? GmailAddress { get; set; }
/// <summary>Data-Protection-encrypted Gmail App Password (never plaintext, never on the wire).</summary>
public string? AppPasswordProtected { get; set; }
public int PollIntervalMinutes { get; set; } = 15;
public string Folder { get; set; } = "INBOX";
public bool BackgroundPollEnabled { get; set; }
/// <summary>Highest IMAP UID seen, to avoid rescanning the whole folder.</summary>
public uint LastUid { get; set; }
}
}