package mail import ( "bufio" "encoding/json" "errors" "os" "path/filepath" "sort" ) // Common errors var ( ErrMessageNotFound = errors.New("message not found") ErrEmptyInbox = errors.New("inbox is empty") ) // Mailbox manages a JSONL-based inbox. type Mailbox struct { path string } // NewMailbox creates a mailbox at the given path. func NewMailbox(path string) *Mailbox { return &Mailbox{path: path} } // Path returns the mailbox file path. func (m *Mailbox) Path() string { return m.path } // List returns all messages in the mailbox. func (m *Mailbox) List() ([]*Message, error) { file, err := os.Open(m.path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } defer file.Close() var messages []*Message scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if line == "" { continue } var msg Message if err := json.Unmarshal([]byte(line), &msg); err != nil { continue // Skip malformed lines } messages = append(messages, &msg) } if err := scanner.Err(); err != nil { return nil, err } // Sort by timestamp (newest first) sort.Slice(messages, func(i, j int) bool { return messages[i].Timestamp.After(messages[j].Timestamp) }) return messages, nil } // ListUnread returns unread messages. func (m *Mailbox) ListUnread() ([]*Message, error) { all, err := m.List() if err != nil { return nil, err } var unread []*Message for _, msg := range all { if !msg.Read { unread = append(unread, msg) } } return unread, nil } // Get returns a message by ID. func (m *Mailbox) Get(id string) (*Message, error) { messages, err := m.List() if err != nil { return nil, err } for _, msg := range messages { if msg.ID == id { return msg, nil } } return nil, ErrMessageNotFound } // Append adds a message to the mailbox. func (m *Mailbox) Append(msg *Message) error { // Ensure directory exists dir := filepath.Dir(m.path) if err := os.MkdirAll(dir, 0755); err != nil { return err } // Open for append file, err := os.OpenFile(m.path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } defer file.Close() data, err := json.Marshal(msg) if err != nil { return err } _, err = file.WriteString(string(data) + "\n") return err } // MarkRead marks a message as read. func (m *Mailbox) MarkRead(id string) error { messages, err := m.List() if err != nil { return err } found := false for _, msg := range messages { if msg.ID == id { msg.Read = true found = true } } if !found { return ErrMessageNotFound } return m.rewrite(messages) } // Delete removes a message from the mailbox. func (m *Mailbox) Delete(id string) error { messages, err := m.List() if err != nil { return err } var filtered []*Message found := false for _, msg := range messages { if msg.ID == id { found = true } else { filtered = append(filtered, msg) } } if !found { return ErrMessageNotFound } return m.rewrite(filtered) } // Count returns the total and unread message counts. func (m *Mailbox) Count() (total, unread int, err error) { messages, err := m.List() if err != nil { return 0, 0, err } total = len(messages) for _, msg := range messages { if !msg.Read { unread++ } } return total, unread, nil } // rewrite rewrites the mailbox with the given messages. func (m *Mailbox) rewrite(messages []*Message) error { // Sort by timestamp (oldest first for JSONL) sort.Slice(messages, func(i, j int) bool { return messages[i].Timestamp.Before(messages[j].Timestamp) }) // Write to temp file tmpPath := m.path + ".tmp" file, err := os.Create(tmpPath) if err != nil { return err } for _, msg := range messages { data, err := json.Marshal(msg) if err != nil { file.Close() os.Remove(tmpPath) return err } file.WriteString(string(data) + "\n") } if err := file.Close(); err != nil { os.Remove(tmpPath) return err } // Atomic rename return os.Rename(tmpPath, m.path) }