chore: init project
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-computed daily rollup so dashboard widgets render instantly without
|
||||
/// scanning the full email table. Refreshed by the analytics worker.
|
||||
/// </summary>
|
||||
public class AnalyticsAggregate : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>Calendar day (UTC, date-only) this row aggregates.</summary>
|
||||
public DateOnly Day { get; set; }
|
||||
|
||||
public int TotalReceived { get; set; }
|
||||
public int TotalUnread { get; set; }
|
||||
public int NewsletterCount { get; set; }
|
||||
public int WithAttachments { get; set; }
|
||||
public long TotalSizeBytes { get; set; }
|
||||
|
||||
/// <summary>JSON: hour-of-day -> count, backing the heatmap widget.</summary>
|
||||
public string? HourHistogramJson { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Attachment metadata only - the binary content is never downloaded or stored.
|
||||
/// Powers the "attachment size breakdown" widget and storage estimates.
|
||||
/// </summary>
|
||||
public class Attachment : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
public Guid EmailId { get; set; }
|
||||
public Email? Email { get; set; }
|
||||
|
||||
/// <summary>Gmail attachment id (for on-demand download if ever needed).</summary>
|
||||
public string? GmailAttachmentId { get; set; }
|
||||
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
public string? MimeType { get; set; }
|
||||
public long SizeBytes { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
using InboxIntel.Domain.Enums;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A single Gmail message stored locally. Designed for 100k+ rows per user:
|
||||
/// search fields are indexed (see EmailConfiguration) and a generated
|
||||
/// tsvector column backs PostgreSQL full-text search.
|
||||
/// </summary>
|
||||
public class Email : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>Gmail message id (stable, unique per account).</summary>
|
||||
public string GmailMessageId { get; set; } = string.Empty;
|
||||
|
||||
public Guid ThreadId { get; set; }
|
||||
public MailThread? Thread { get; set; }
|
||||
|
||||
public Guid SenderId { get; set; }
|
||||
public Sender? Sender { get; set; }
|
||||
|
||||
public string? Subject { get; set; }
|
||||
public string? Snippet { get; set; }
|
||||
|
||||
/// <summary>Plain-text body. Indexed for FTS via SearchVector.</summary>
|
||||
public string? BodyText { get; set; }
|
||||
|
||||
public DateTimeOffset SentAtUtc { get; set; }
|
||||
public DateTimeOffset? ReceivedAtUtc { get; set; }
|
||||
|
||||
public long SizeEstimateBytes { get; set; }
|
||||
|
||||
public bool IsUnread { get; set; }
|
||||
public bool IsStarred { get; set; }
|
||||
public bool IsImportant { get; set; }
|
||||
public bool IsInInbox { get; set; }
|
||||
public bool IsTrashed { get; set; }
|
||||
public bool HasAttachments { get; set; }
|
||||
|
||||
// Unsubscribe signals captured at parse time.
|
||||
public bool HasListUnsubscribe { get; set; }
|
||||
public string? ListUnsubscribeRaw { get; set; }
|
||||
public bool SupportsOneClickUnsubscribe { get; set; }
|
||||
|
||||
/// <summary>Heuristic/AI classification. Defaults to Unknown until classified.</summary>
|
||||
public EmailCategory Category { get; set; } = EmailCategory.Unknown;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL tsvector, maintained as a generated column. Never set in code;
|
||||
/// mapped read-only for querying. Nullable so non-Postgres test providers work.
|
||||
/// </summary>
|
||||
public NpgsqlTypes.NpgsqlTsVector? SearchVector { get; set; }
|
||||
|
||||
public ICollection<EmailLabel> EmailLabels { get; set; } = new List<EmailLabel>();
|
||||
public ICollection<Attachment> Attachments { get; set; } = new List<Attachment>();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>A Gmail label (system or user-defined), mapped locally.</summary>
|
||||
public class Label : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>Gmail label id, e.g. "INBOX", "Label_42".</summary>
|
||||
public string GmailLabelId { get; set; } = string.Empty;
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>"system" or "user".</summary>
|
||||
public string Type { get; set; } = "user";
|
||||
|
||||
public string? ColorHex { get; set; }
|
||||
|
||||
public ICollection<EmailLabel> EmailLabels { get; set; } = new List<EmailLabel>();
|
||||
}
|
||||
|
||||
/// <summary>Join entity for the many-to-many Email <-> Label relationship.</summary>
|
||||
public class EmailLabel
|
||||
{
|
||||
public Guid EmailId { get; set; }
|
||||
public Email? Email { get; set; }
|
||||
|
||||
public Guid LabelId { get; set; }
|
||||
public Label? Label { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A sending domain (e.g. "github.com"), extracted from sender addresses.
|
||||
/// Aggregating at the domain level powers fast grouping and the
|
||||
/// safe-to-unsubscribe list.
|
||||
/// </summary>
|
||||
public class MailDomain : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>Lower-cased registrable domain, e.g. "news.github.com".</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public int EmailCount { get; set; }
|
||||
public bool IsBulkSender { get; set; }
|
||||
|
||||
public ICollection<Sender> Senders { get; set; } = new List<Sender>();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A Gmail conversation thread, reconstructed from the Gmail threadId.
|
||||
/// </summary>
|
||||
public class MailThread : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>Gmail-assigned thread id.</summary>
|
||||
public string GmailThreadId { get; set; } = string.Empty;
|
||||
|
||||
public string? Subject { get; set; }
|
||||
public string? Snippet { get; set; }
|
||||
public int MessageCount { get; set; }
|
||||
public DateTimeOffset? FirstMessageUtc { get; set; }
|
||||
public DateTimeOffset? LastMessageUtc { get; set; }
|
||||
|
||||
public ICollection<Email> Emails { get; set; } = new List<Email>();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A distinct sender address (e.g. "noreply@github.com"). Cleanup and
|
||||
/// unsubscribe operations are most often grouped by sender.
|
||||
/// </summary>
|
||||
public class Sender : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
public Guid DomainId { get; set; }
|
||||
public MailDomain? Domain { get; set; }
|
||||
|
||||
/// <summary>Lower-cased full address.</summary>
|
||||
public string Address { get; set; } = string.Empty;
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
public int EmailCount { get; set; }
|
||||
public int UnreadCount { get; set; }
|
||||
public long TotalSizeBytes { get; set; }
|
||||
public DateTimeOffset? LastReceivedUtc { get; set; }
|
||||
|
||||
/// <summary>True if any message from this sender carried a List-Unsubscribe header.</summary>
|
||||
public bool HasUnsubscribe { get; set; }
|
||||
|
||||
public ICollection<Email> Emails { get; set; } = new List<Email>();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
using InboxIntel.Domain.Enums;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks Gmail sync progress so a run can resume after failure and so
|
||||
/// incremental (delta) syncs know where to continue from.
|
||||
/// </summary>
|
||||
public class SyncState : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
public SyncStatus Status { get; set; } = SyncStatus.Idle;
|
||||
public SyncType LastSyncType { get; set; } = SyncType.Full;
|
||||
|
||||
/// <summary>Gmail historyId watermark for incremental delta sync.</summary>
|
||||
public string? LastHistoryId { get; set; }
|
||||
|
||||
/// <summary>Opaque pageToken to resume an interrupted full sync.</summary>
|
||||
public string? ResumePageToken { get; set; }
|
||||
|
||||
public int TotalMessagesEstimate { get; set; }
|
||||
public int MessagesProcessed { get; set; }
|
||||
|
||||
public DateTimeOffset? StartedUtc { get; set; }
|
||||
public DateTimeOffset? CompletedUtc { get; set; }
|
||||
public DateTimeOffset? LastSuccessfulSyncUtc { get; set; }
|
||||
|
||||
public int ConsecutiveFailures { get; set; }
|
||||
public string? LastError { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
using InboxIntel.Domain.Enums;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A detected unsubscribe opportunity, grouped per sender/domain. Items move
|
||||
/// through a queue; actions only run after explicit user confirmation.
|
||||
/// </summary>
|
||||
public class UnsubscribeItem : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
public Guid SenderId { get; set; }
|
||||
public Sender? Sender { get; set; }
|
||||
|
||||
public UnsubscribeMethod Method { get; set; } = UnsubscribeMethod.None;
|
||||
public UnsubscribeStatus Status { get; set; } = UnsubscribeStatus.Detected;
|
||||
|
||||
/// <summary>http(s) unsubscribe URL or mailto target extracted from header/body.</summary>
|
||||
public string? UnsubscribeTarget { get; set; }
|
||||
|
||||
/// <summary>Number of emails from this sender (drives "safe to unsubscribe" ranking).</summary>
|
||||
public int EmailCount { get; set; }
|
||||
|
||||
public DateTimeOffset? LastAttemptUtc { get; set; }
|
||||
public string? ResultMessage { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A user authenticated via Google OAuth2. The system is single-user in the
|
||||
/// initial version, but the schema is multi-user ready (every owned entity
|
||||
/// carries a UserId foreign key).
|
||||
/// </summary>
|
||||
public class User : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>Google "sub" claim - stable unique identifier for the Google account.</summary>
|
||||
public string GoogleSubjectId { get; set; } = string.Empty;
|
||||
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string? DisplayName { get; set; }
|
||||
public string? PictureUrl { get; set; }
|
||||
|
||||
/// <summary>OAuth2 refresh token, AES-encrypted at rest. Never logged.</summary>
|
||||
public byte[]? EncryptedRefreshToken { get; set; }
|
||||
|
||||
/// <summary>Most recent access token expiry, used to decide when to refresh.</summary>
|
||||
public DateTimeOffset? AccessTokenExpiresAtUtc { get; set; }
|
||||
|
||||
public DateTimeOffset? LastLoginUtc { get; set; }
|
||||
|
||||
public ICollection<Email> Emails { get; set; } = new List<Email>();
|
||||
public ICollection<WidgetLayout> WidgetLayouts { get; set; } = new List<WidgetLayout>();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using InboxIntel.Domain.Common;
|
||||
|
||||
namespace InboxIntel.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Persisted dashboard layout for a user. One row per widget instance,
|
||||
/// storing grid geometry and visibility so the dashboard restores exactly.
|
||||
/// </summary>
|
||||
public class WidgetLayout : AuditableEntity
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>Stable widget key, e.g. "inbox-health", "top-senders".</summary>
|
||||
public string WidgetKey { get; set; } = string.Empty;
|
||||
|
||||
// react-grid-layout geometry.
|
||||
public int X { get; set; }
|
||||
public int Y { get; set; }
|
||||
public int W { get; set; } = 4;
|
||||
public int H { get; set; } = 4;
|
||||
|
||||
public bool Visible { get; set; } = true;
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
/// <summary>Optional per-widget settings as JSON.</summary>
|
||||
public string? SettingsJson { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user