Files
Tomas Dvorak 3cb40adb23 first commit
2026-04-10 12:04:09 +02:00

454 lines
12 KiB
Go

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 }