package store import ( "errors" "fmt" "time" "github.com/google/uuid" ) type Mailbox struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` Label string `json:"label"` Email string `json:"email"` DisplayName string `json:"displayName"` IMAPHost string `json:"imapHost"` IMAPPort int `json:"imapPort"` IMAPUsername string `json:"imapUsername"` IMAPUseTLS bool `json:"imapUseTls"` SMTPHost string `json:"smtpHost"` SMTPPort int `json:"smtpPort"` SMTPUsername string `json:"smtpUsername"` SMTPUseTLS bool `json:"smtpUseTls"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` LastSyncedAt *time.Time `json:"lastSyncedAt,omitempty"` SyncStatus string `json:"syncStatus"` SyncError string `json:"syncError,omitempty"` } type MailAddress struct { Name string `json:"name"` Email string `json:"email"` } type MailMessage struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` MailboxID string `json:"mailboxId"` RemoteUID int64 `json:"remoteUid"` MessageID string `json:"messageId"` Folder string `json:"folder"` From MailAddress `json:"from"` To []MailAddress `json:"to"` Cc []MailAddress `json:"cc"` Subject string `json:"subject"` Snippet string `json:"snippet"` TextBody string `json:"textBody"` HTMLBody string `json:"htmlBody"` ReceivedAt time.Time `json:"receivedAt"` IsRead bool `json:"isRead"` LinkedTaskID *string `json:"linkedTaskId,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type OutgoingMail struct { ID string `json:"id"` WorkspaceSlug string `json:"workspaceSlug"` MailboxID string `json:"mailboxId"` To []MailAddress `json:"to"` Cc []MailAddress `json:"cc"` Bcc []MailAddress `json:"bcc"` Subject string `json:"subject"` TextBody string `json:"textBody"` HTMLBody string `json:"htmlBody"` Status string `json:"status"` ScheduledFor *time.Time `json:"scheduledFor,omitempty"` SentAt *time.Time `json:"sentAt,omitempty"` Error string `json:"error,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type MailboxConnection struct { Mailbox IMAPPasswordCiphertext string SMTPPasswordCiphertext string } type CreateMailboxRecordInput struct { WorkspaceSlug string Label string Email string DisplayName string IMAPHost string IMAPPort int IMAPUsername string IMAPPasswordCiphertext string IMAPUseTLS bool SMTPHost string SMTPPort int SMTPUsername string SMTPPasswordCiphertext string SMTPUseTLS bool } type UpdateMailboxSyncStatusInput struct { SyncStatus string SyncError *string LastSyncedAt *time.Time } type InboundMailMessage struct { WorkspaceSlug string MailboxID string RemoteUID int64 MessageID string Folder string From MailAddress To []MailAddress Cc []MailAddress Subject string Snippet string TextBody string HTMLBody string ReceivedAt time.Time IsRead bool } type CreateOutgoingMailInput struct { WorkspaceSlug string MailboxID string To []MailAddress Cc []MailAddress Bcc []MailAddress Subject string TextBody string HTMLBody string Status string ScheduledFor *time.Time } type UpdateOutgoingMailStatusInput struct { Status string SentAt *time.Time Error *string } func (s *State) ListAllMailboxes() []Mailbox { s.mu.RLock() defer s.mu.RUnlock() return append([]Mailbox(nil), s.Mailboxes...) } func (s *State) ListMailboxes(workspaceSlug string) []Mailbox { s.mu.RLock() defer s.mu.RUnlock() return filterByWorkspace(s.Mailboxes, workspaceSlug) } func (s *State) GetMailboxByID(mailboxID string) (Mailbox, error) { s.mu.RLock() defer s.mu.RUnlock() for _, mailbox := range s.Mailboxes { if mailbox.ID == mailboxID { return mailbox, nil } } return Mailbox{}, errors.New("mailbox not found") } func (s *State) GetMailboxConnection(mailboxID string) (MailboxConnection, error) { s.mu.RLock() defer s.mu.RUnlock() connection, ok := s.MailboxAuth[mailboxID] if !ok { return MailboxConnection{}, errors.New("mailbox connection not found") } return connection, nil } func (s *State) CreateMailbox(input CreateMailboxRecordInput) (Mailbox, error) { s.mu.Lock() defer s.mu.Unlock() now := time.Now().UTC() mailbox := Mailbox{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, Label: input.Label, Email: input.Email, DisplayName: input.DisplayName, IMAPHost: input.IMAPHost, IMAPPort: input.IMAPPort, IMAPUsername: input.IMAPUsername, IMAPUseTLS: input.IMAPUseTLS, SMTPHost: input.SMTPHost, SMTPPort: input.SMTPPort, SMTPUsername: input.SMTPUsername, SMTPUseTLS: input.SMTPUseTLS, CreatedAt: now, UpdatedAt: now, SyncStatus: "idle", } s.Mailboxes = append([]Mailbox{mailbox}, s.Mailboxes...) s.MailboxAuth[mailbox.ID] = MailboxConnection{ Mailbox: mailbox, IMAPPasswordCiphertext: input.IMAPPasswordCiphertext, SMTPPasswordCiphertext: input.SMTPPasswordCiphertext, } s.appendActivityLocked(input.WorkspaceSlug, "Mailbox connected", fmt.Sprintf("%s is ready for sync.", input.Email)) return mailbox, nil } func (s *State) UpdateMailboxSyncStatus(mailboxID string, input UpdateMailboxSyncStatusInput) (Mailbox, error) { s.mu.Lock() defer s.mu.Unlock() for index, mailbox := range s.Mailboxes { if mailbox.ID != mailboxID { continue } if input.SyncStatus != "" { mailbox.SyncStatus = input.SyncStatus } if input.SyncError != nil { mailbox.SyncError = *input.SyncError } if input.LastSyncedAt != nil { mailbox.LastSyncedAt = input.LastSyncedAt } mailbox.UpdatedAt = time.Now().UTC() s.Mailboxes[index] = mailbox if connection, ok := s.MailboxAuth[mailboxID]; ok { connection.Mailbox = mailbox s.MailboxAuth[mailboxID] = connection } return mailbox, nil } return Mailbox{}, errors.New("mailbox not found") } func (s *State) ListMailMessages(workspaceSlug string, mailboxID string) []MailMessage { s.mu.RLock() defer s.mu.RUnlock() items := filterByWorkspace(s.MailMessages, workspaceSlug) if mailboxID == "" { return items } filtered := make([]MailMessage, 0, len(items)) for _, item := range items { if item.MailboxID == mailboxID { filtered = append(filtered, item) } } return filtered } func (s *State) GetMailMessageByID(messageID string) (MailMessage, error) { s.mu.RLock() defer s.mu.RUnlock() for _, message := range s.MailMessages { if message.ID == messageID { return message, nil } } return MailMessage{}, errors.New("mail message not found") } func (s *State) UpsertMailMessages(mailboxID string, messages []InboundMailMessage) error { s.mu.Lock() defer s.mu.Unlock() now := time.Now().UTC() for _, input := range messages { found := false for index, message := range s.MailMessages { if message.MailboxID == mailboxID && message.Folder == input.Folder && message.RemoteUID == input.RemoteUID { linkedTaskID := message.LinkedTaskID s.MailMessages[index] = MailMessage{ ID: message.ID, WorkspaceSlug: input.WorkspaceSlug, MailboxID: mailboxID, RemoteUID: input.RemoteUID, MessageID: input.MessageID, Folder: input.Folder, From: input.From, To: input.To, Cc: input.Cc, Subject: input.Subject, Snippet: input.Snippet, TextBody: input.TextBody, HTMLBody: input.HTMLBody, ReceivedAt: input.ReceivedAt, IsRead: input.IsRead, LinkedTaskID: linkedTaskID, CreatedAt: message.CreatedAt, UpdatedAt: now, } found = true break } } if found { continue } s.MailMessages = append([]MailMessage{{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, MailboxID: mailboxID, RemoteUID: input.RemoteUID, MessageID: input.MessageID, Folder: input.Folder, From: input.From, To: input.To, Cc: input.Cc, Subject: input.Subject, Snippet: input.Snippet, TextBody: input.TextBody, HTMLBody: input.HTMLBody, ReceivedAt: input.ReceivedAt, IsRead: input.IsRead, CreatedAt: now, UpdatedAt: now, }}, s.MailMessages...) } return nil } func (s *State) LinkMailMessageTask(messageID string, taskID string) (MailMessage, error) { s.mu.Lock() defer s.mu.Unlock() for index, message := range s.MailMessages { if message.ID != messageID { continue } message.LinkedTaskID = &taskID message.UpdatedAt = time.Now().UTC() s.MailMessages[index] = message return message, nil } return MailMessage{}, errors.New("mail message not found") } func (s *State) ListOutgoingMails(workspaceSlug string, mailboxID string) []OutgoingMail { s.mu.RLock() defer s.mu.RUnlock() items := filterByWorkspace(s.OutgoingMails, workspaceSlug) if mailboxID == "" { return items } filtered := make([]OutgoingMail, 0, len(items)) for _, item := range items { if item.MailboxID == mailboxID { filtered = append(filtered, item) } } return filtered } func (s *State) ListDueOutgoingMails(now time.Time, limit int) []OutgoingMail { s.mu.RLock() defer s.mu.RUnlock() items := make([]OutgoingMail, 0, limit) for _, item := range s.OutgoingMails { if item.Status != "queued" && item.Status != "scheduled" { continue } if item.ScheduledFor != nil && item.ScheduledFor.After(now) { continue } items = append(items, item) if limit > 0 && len(items) >= limit { break } } return items } func (s *State) GetOutgoingMailByID(outgoingMailID string) (OutgoingMail, error) { s.mu.RLock() defer s.mu.RUnlock() for _, item := range s.OutgoingMails { if item.ID == outgoingMailID { return item, nil } } return OutgoingMail{}, errors.New("outgoing mail not found") } func (s *State) CreateOutgoingMail(input CreateOutgoingMailInput) (OutgoingMail, error) { s.mu.Lock() defer s.mu.Unlock() now := time.Now().UTC() item := OutgoingMail{ ID: uuid.NewString(), WorkspaceSlug: input.WorkspaceSlug, MailboxID: input.MailboxID, To: input.To, Cc: input.Cc, Bcc: input.Bcc, Subject: input.Subject, TextBody: input.TextBody, HTMLBody: input.HTMLBody, Status: input.Status, ScheduledFor: input.ScheduledFor, CreatedAt: now, UpdatedAt: now, } s.OutgoingMails = append([]OutgoingMail{item}, s.OutgoingMails...) s.appendActivityLocked(input.WorkspaceSlug, "Outgoing mail queued", input.Subject) return item, nil } func (s *State) UpdateOutgoingMailStatus(outgoingMailID string, input UpdateOutgoingMailStatusInput) (OutgoingMail, error) { s.mu.Lock() defer s.mu.Unlock() for index, item := range s.OutgoingMails { if item.ID != outgoingMailID { continue } if input.Status != "" { item.Status = input.Status } if input.SentAt != nil { item.SentAt = input.SentAt } if input.Error != nil { item.Error = *input.Error } item.UpdatedAt = time.Now().UTC() s.OutgoingMails[index] = item return item, nil } return OutgoingMail{}, errors.New("outgoing mail not found") } func (item Mailbox) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item MailMessage) GetWorkspaceSlug() string { return item.WorkspaceSlug } func (item OutgoingMail) GetWorkspaceSlug() string { return item.WorkspaceSlug }