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).
This commit is contained in:
2026-06-06 09:38:02 +02:00
parent 84181ed74d
commit 5d7cbc66bf
15 changed files with 1818 additions and 0 deletions

View File

@@ -21,6 +21,8 @@ public class ApplicationContext : DbContext
public DbSet<Page> Pages => Set<Page>();
public DbSet<Block> Blocks => Set<Block>();
public DbSet<Media> Media => Set<Media>();
public DbSet<Request> Requests => Set<Request>();
public DbSet<MailSettings> MailSettings => Set<MailSettings>();
// Keep Gerbil.NameSearch in sync on every save (separator-insensitive search key),
// so it can never drift from Name regardless of which code path mutates the entity.
@@ -152,6 +154,19 @@ public class ApplicationContext : DbContext
modelBuilder.Entity<Block>(e => e.Property(b => b.Type).HasConversion<string>());
SeedCms(modelBuilder);
// INBOX epic: Gmail request inbox.
modelBuilder.Entity<Request>(e =>
{
e.Property(r => r.Status).HasConversion<string>();
e.HasIndex(r => r.GmailMessageId).IsUnique();
e.HasOne(r => r.AssignedContact).WithMany()
.HasForeignKey(r => r.AssignedContactId).OnDelete(DeleteBehavior.Restrict);
});
// exactly one MailSettings row, fixed id.
modelBuilder.Entity<MailSettings>()
.HasData(new MailSettings { Id = GerbilManagerWebAPI.Models.MailSettings.SingletonId });
SeedColorVarieties(modelBuilder);
}