Detailed changes
@@ -107,14 +107,25 @@ func connect(account *config.Account) (*client.Client, error) {
return c, nil
}
-func FetchEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
+func getSentMailbox(account *config.Account) string {
+ switch account.ServiceProvider {
+ case "gmail":
+ return "[Gmail]/Sent Mail"
+ case "icloud":
+ return "Sent Messages"
+ default:
+ return "Sent"
+ }
+}
+
+func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset uint32) ([]Email, error) {
c, err := connect(account)
if err != nil {
return nil, err
}
defer c.Logout()
- mbox, err := c.Select("INBOX", false)
+ mbox, err := c.Select(mailbox, false)
if err != nil {
return nil, err
}
@@ -209,14 +220,14 @@ func FetchEmails(account *config.Account, limit, offset uint32) ([]Email, error)
return emails, nil
}
-func FetchEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
+func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint32) (string, []Attachment, error) {
c, err := connect(account)
if err != nil {
return "", nil, err
}
defer c.Logout()
- if _, err := c.Select("INBOX", false); err != nil {
+ if _, err := c.Select(mailbox, false); err != nil {
return "", nil, err
}
@@ -374,14 +385,14 @@ func FetchEmailBody(account *config.Account, uid uint32) (string, []Attachment,
return body, attachments, nil
}
-func FetchAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
+func FetchAttachmentFromMailbox(account *config.Account, mailbox string, uid uint32, partID string, encoding string) ([]byte, error) {
c, err := connect(account)
if err != nil {
return nil, err
}
defer c.Logout()
- if _, err := c.Select("INBOX", false); err != nil {
+ if _, err := c.Select(mailbox, false); err != nil {
return nil, err
}
@@ -438,14 +449,14 @@ func FetchAttachment(account *config.Account, uid uint32, partID string, encodin
}
}
-func moveEmail(account *config.Account, uid uint32, destMailbox string) error {
+func moveEmail(account *config.Account, uid uint32, sourceMailbox, destMailbox string) error {
c, err := connect(account)
if err != nil {
return err
}
defer c.Logout()
- if _, err := c.Select("INBOX", false); err != nil {
+ if _, err := c.Select(sourceMailbox, false); err != nil {
return err
}
@@ -455,14 +466,14 @@ func moveEmail(account *config.Account, uid uint32, destMailbox string) error {
return c.UidMove(seqSet, destMailbox)
}
-func DeleteEmail(account *config.Account, uid uint32) error {
+func DeleteEmailFromMailbox(account *config.Account, mailbox string, uid uint32) error {
c, err := connect(account)
if err != nil {
return err
}
defer c.Logout()
- if _, err := c.Select("INBOX", false); err != nil {
+ if _, err := c.Select(mailbox, false); err != nil {
return err
}
@@ -479,7 +490,7 @@ func DeleteEmail(account *config.Account, uid uint32) error {
return c.Expunge(nil)
}
-func ArchiveEmail(account *config.Account, uid uint32) error {
+func ArchiveEmailFromMailbox(account *config.Account, mailbox string, uid uint32) error {
var archiveMailbox string
switch account.ServiceProvider {
case "gmail":
@@ -487,5 +498,47 @@ func ArchiveEmail(account *config.Account, uid uint32) error {
default:
archiveMailbox = "Archive"
}
- return moveEmail(account, uid, archiveMailbox)
+ return moveEmail(account, uid, mailbox, archiveMailbox)
+}
+
+// Convenience wrappers defaulting to INBOX for existing call sites.
+
+func FetchEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
+ return FetchMailboxEmails(account, "INBOX", limit, offset)
+}
+
+func FetchSentEmails(account *config.Account, limit, offset uint32) ([]Email, error) {
+ return FetchMailboxEmails(account, getSentMailbox(account), limit, offset)
+}
+
+func FetchEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
+ return FetchEmailBodyFromMailbox(account, "INBOX", uid)
+}
+
+func FetchSentEmailBody(account *config.Account, uid uint32) (string, []Attachment, error) {
+ return FetchEmailBodyFromMailbox(account, getSentMailbox(account), uid)
+}
+
+func FetchAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
+ return FetchAttachmentFromMailbox(account, "INBOX", uid, partID, encoding)
+}
+
+func FetchSentAttachment(account *config.Account, uid uint32, partID string, encoding string) ([]byte, error) {
+ return FetchAttachmentFromMailbox(account, getSentMailbox(account), uid, partID, encoding)
+}
+
+func DeleteEmail(account *config.Account, uid uint32) error {
+ return DeleteEmailFromMailbox(account, "INBOX", uid)
+}
+
+func DeleteSentEmail(account *config.Account, uid uint32) error {
+ return DeleteEmailFromMailbox(account, getSentMailbox(account), uid)
+}
+
+func ArchiveEmail(account *config.Account, uid uint32) error {
+ return ArchiveEmailFromMailbox(account, "INBOX", uid)
+}
+
+func ArchiveSentEmail(account *config.Account, uid uint32) error {
+ return ArchiveEmailFromMailbox(account, getSentMailbox(account), uid)
}
@@ -19,7 +19,7 @@ require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
- github.com/charmbracelet/x/ansi v0.9.3 // indirect
+ github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
@@ -34,7 +34,6 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
- golang.org/x/net v0.39.0 // indirect
- golang.org/x/sync v0.16.0 // indirect
- golang.org/x/sys v0.34.0 // indirect
+ golang.org/x/net v0.47.0 // indirect
+ golang.org/x/sys v0.38.0 // indirect
)
@@ -63,7 +63,10 @@ type mainModel struct {
config *config.Config
emails []fetcher.Email
emailsByAcct map[string][]fetcher.Email
+ sentEmails []fetcher.Email
+ sentByAcct map[string][]fetcher.Email
inbox *tui.Inbox
+ sentInbox *tui.Inbox
width int
height int
err error
@@ -72,6 +75,7 @@ type mainModel struct {
func newInitialModel(cfg *config.Config) *mainModel {
initialModel := &mainModel{
emailsByAcct: make(map[string][]fetcher.Email),
+ sentByAcct: make(map[string][]fetcher.Email),
}
if cfg == nil || !cfg.HasAccounts() {
@@ -122,6 +126,22 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, nil
+ case tui.BackToMailboxMsg:
+ switch msg.Mailbox {
+ case tui.MailboxSent:
+ if m.sentInbox != nil {
+ m.current = m.sentInbox
+ return m, nil
+ }
+ case tui.MailboxInbox:
+ if m.inbox != nil {
+ m.current = m.inbox
+ return m, nil
+ }
+ }
+ m.current = tui.NewChoice()
+ return m, nil
+
case tui.DiscardDraftMsg:
// Save draft to disk
if msg.ComposerState != nil {
@@ -196,13 +216,21 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
// No cache, fetch normally
m.current = tui.NewStatus("Fetching emails from all accounts...")
- return m, tea.Batch(m.current.Init(), fetchAllAccountsEmails(m.config))
+ return m, tea.Batch(m.current.Init(), fetchAllAccountsEmails(m.config, tui.MailboxInbox))
+
+ case tui.GoToSentInboxMsg:
+ if m.config == nil || !m.config.HasAccounts() {
+ m.current = tui.NewLogin()
+ return m, m.current.Init()
+ }
+ m.current = tui.NewStatus("Fetching sent emails from all accounts...")
+ return m, tea.Batch(m.current.Init(), fetchAllAccountsEmails(m.config, tui.MailboxSent))
case tui.CachedEmailsLoadedMsg:
if msg.Cache == nil {
// Cache load failed, fetch normally
m.current = tui.NewStatus("Fetching emails from all accounts...")
- return m, tea.Batch(m.current.Init(), fetchAllAccountsEmails(m.config))
+ return m, tea.Batch(m.current.Init(), fetchAllAccountsEmails(m.config, tui.MailboxInbox))
}
// Convert cached emails to fetcher.Email
@@ -231,31 +259,25 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Start background refresh
return m, tea.Batch(
m.current.Init(),
- func() tea.Msg { return tui.RefreshingEmailsMsg{} },
- refreshEmails(m.config),
+ func() tea.Msg { return tui.RefreshingEmailsMsg{Mailbox: tui.MailboxInbox} },
+ refreshEmails(m.config, tui.MailboxInbox),
)
case tui.EmailsRefreshedMsg:
- m.emailsByAcct = msg.EmailsByAccount
-
- // Flatten all emails
- var allEmails []fetcher.Email
- for _, emails := range msg.EmailsByAccount {
- allEmails = append(allEmails, emails...)
- }
-
- // Sort by date (newest first)
- for i := 0; i < len(allEmails); i++ {
- for j := i + 1; j < len(allEmails); j++ {
- if allEmails[j].Date.After(allEmails[i].Date) {
- allEmails[i], allEmails[j] = allEmails[j], allEmails[i]
- }
+ if msg.Mailbox == tui.MailboxSent {
+ m.sentByAcct = msg.EmailsByAccount
+ m.sentEmails = flattenAndSort(msg.EmailsByAccount)
+ if m.sentInbox != nil {
+ m.sentInbox.SetEmails(m.sentEmails, m.config.Accounts)
+ m.current, _ = m.current.Update(msg)
}
+ return m, nil
}
- m.emails = allEmails
+ m.emailsByAcct = msg.EmailsByAccount
+ m.emails = flattenAndSort(msg.EmailsByAccount)
- // Save to cache
+ // Save to cache (inbox only)
go saveEmailsToCache(m.emails)
// Update inbox if it exists
@@ -267,24 +289,18 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case tui.AllEmailsFetchedMsg:
- m.emailsByAcct = msg.EmailsByAccount
-
- // Flatten all emails
- var allEmails []fetcher.Email
- for _, emails := range msg.EmailsByAccount {
- allEmails = append(allEmails, emails...)
- }
+ if msg.Mailbox == tui.MailboxSent {
+ m.sentByAcct = msg.EmailsByAccount
+ m.sentEmails = flattenAndSort(msg.EmailsByAccount)
- // Sort by date (newest first)
- for i := 0; i < len(allEmails); i++ {
- for j := i + 1; j < len(allEmails); j++ {
- if allEmails[j].Date.After(allEmails[i].Date) {
- allEmails[i], allEmails[j] = allEmails[j], allEmails[i]
- }
- }
+ m.sentInbox = tui.NewSentInbox(m.sentEmails, m.config.Accounts)
+ m.current = m.sentInbox
+ m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ return m, m.current.Init()
}
- m.emails = allEmails
+ m.emailsByAcct = msg.EmailsByAccount
+ m.emails = flattenAndSort(msg.EmailsByAccount)
// Save to cache
go saveEmailsToCache(m.emails)
@@ -295,28 +311,28 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.current.Init()
case tui.EmailsFetchedMsg:
- // Single account fetch result
+ if msg.Mailbox == tui.MailboxSent {
+ if m.sentByAcct == nil {
+ m.sentByAcct = make(map[string][]fetcher.Email)
+ }
+ m.sentByAcct[msg.AccountID] = msg.Emails
+ m.sentEmails = flattenAndSort(m.sentByAcct)
+ if m.sentInbox == nil {
+ m.sentInbox = tui.NewSentInbox(m.sentEmails, m.config.Accounts)
+ } else {
+ m.sentInbox.SetEmails(m.sentEmails, m.config.Accounts)
+ }
+ m.current = m.sentInbox
+ m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ return m, m.current.Init()
+ }
+
+ // Inbox path
if m.emailsByAcct == nil {
m.emailsByAcct = make(map[string][]fetcher.Email)
}
m.emailsByAcct[msg.AccountID] = msg.Emails
-
- // Rebuild all emails
- var allEmails []fetcher.Email
- for _, emails := range m.emailsByAcct {
- allEmails = append(allEmails, emails...)
- }
-
- // Sort by date
- for i := 0; i < len(allEmails); i++ {
- for j := i + 1; j < len(allEmails); j++ {
- if allEmails[j].Date.After(allEmails[i].Date) {
- allEmails[i], allEmails[j] = allEmails[j], allEmails[i]
- }
- }
- }
-
- m.emails = allEmails
+ m.emails = flattenAndSort(m.emailsByAcct)
if m.inbox == nil {
m.inbox = tui.NewInbox(m.emails, m.config.Accounts)
} else {
@@ -336,11 +352,19 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, tea.Batch(
func() tea.Msg { return tui.FetchingMoreEmailsMsg{} },
- fetchEmails(account, paginationLimit, msg.Offset),
+ fetchEmails(account, paginationLimit, msg.Offset, msg.Mailbox),
)
case tui.EmailsAppendedMsg:
- // Add new emails to the appropriate account
+ if msg.Mailbox == tui.MailboxSent {
+ if m.sentByAcct == nil {
+ m.sentByAcct = make(map[string][]fetcher.Email)
+ }
+ m.sentByAcct[msg.AccountID] = append(m.sentByAcct[msg.AccountID], msg.Emails...)
+ m.sentEmails = append(m.sentEmails, msg.Emails...)
+ return m, nil
+ }
+ // Inbox
if m.emailsByAcct == nil {
m.emailsByAcct = make(map[string][]fetcher.Email)
}
@@ -426,32 +450,40 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, m.current.Init()
case tui.ViewEmailMsg:
- email := m.getEmailByUIDAndAccount(msg.UID, msg.AccountID)
+ email := m.getEmailByUIDAndAccount(msg.UID, msg.AccountID, msg.Mailbox)
if email == nil {
return m, nil
}
m.current = tui.NewStatus("Fetching email content...")
- return m, tea.Batch(m.current.Init(), fetchEmailBodyCmd(m.config, *email, msg.UID, msg.AccountID))
+ return m, tea.Batch(m.current.Init(), fetchEmailBodyCmd(m.config, *email, msg.UID, msg.AccountID, msg.Mailbox))
case tui.EmailBodyFetchedMsg:
if msg.Err != nil {
log.Printf("could not fetch email body: %v", msg.Err)
- m.current = m.inbox
+ if msg.Mailbox == tui.MailboxSent && m.sentInbox != nil {
+ m.current = m.sentInbox
+ } else {
+ m.current = m.inbox
+ }
return m, nil
}
// Update the email in our stores
- m.updateEmailBodyByUID(msg.UID, msg.AccountID, msg.Body, msg.Attachments)
+ m.updateEmailBodyByUID(msg.UID, msg.AccountID, msg.Mailbox, msg.Body, msg.Attachments)
- email := m.getEmailByUIDAndAccount(msg.UID, msg.AccountID)
+ email := m.getEmailByUIDAndAccount(msg.UID, msg.AccountID, msg.Mailbox)
if email == nil {
- m.current = m.inbox
+ if msg.Mailbox == tui.MailboxSent && m.sentInbox != nil {
+ m.current = m.sentInbox
+ } else {
+ m.current = m.inbox
+ }
return m, nil
}
// Find the index for the email view (used for display purposes)
- emailIndex := m.getEmailIndex(msg.UID, msg.AccountID)
- emailView := tui.NewEmailView(*email, emailIndex, m.width, m.height)
+ emailIndex := m.getEmailIndex(msg.UID, msg.AccountID, msg.Mailbox)
+ emailView := tui.NewEmailView(*email, emailIndex, m.width, m.height, msg.Mailbox)
m.current = emailView
return m, m.current.Init()
@@ -536,11 +568,15 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
account := m.config.GetAccountByID(msg.AccountID)
if account == nil {
- m.current = m.inbox
+ if msg.Mailbox == tui.MailboxSent && m.sentInbox != nil {
+ m.current = m.sentInbox
+ } else {
+ m.current = m.inbox
+ }
return m, nil
}
- return m, tea.Batch(m.current.Init(), deleteEmailCmd(account, msg.UID, msg.AccountID))
+ return m, tea.Batch(m.current.Init(), deleteEmailCmd(account, msg.UID, msg.AccountID, msg.Mailbox))
case tui.ArchiveEmailMsg:
m.previousModel = m.current
@@ -548,27 +584,48 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
account := m.config.GetAccountByID(msg.AccountID)
if account == nil {
- m.current = m.inbox
+ if msg.Mailbox == tui.MailboxSent && m.sentInbox != nil {
+ m.current = m.sentInbox
+ } else {
+ m.current = m.inbox
+ }
return m, nil
}
- return m, tea.Batch(m.current.Init(), archiveEmailCmd(account, msg.UID, msg.AccountID))
+ return m, tea.Batch(m.current.Init(), archiveEmailCmd(account, msg.UID, msg.AccountID, msg.Mailbox))
case tui.EmailActionDoneMsg:
if msg.Err != nil {
log.Printf("Action failed: %v", msg.Err)
- m.current = m.inbox
+ if msg.Mailbox == tui.MailboxSent && m.sentInbox != nil {
+ m.current = m.sentInbox
+ } else {
+ m.current = m.inbox
+ }
return m, nil
}
// Remove email from stores
- m.removeEmail(msg.UID, msg.AccountID)
+ m.removeEmailByMailbox(msg.UID, msg.AccountID, msg.Mailbox)
+
+ if msg.Mailbox == tui.MailboxSent {
+ if m.sentInbox != nil {
+ m.sentInbox.RemoveEmail(msg.UID, msg.AccountID)
+ m.current = m.sentInbox
+ m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ return m, m.current.Init()
+ }
+ m.current = tui.NewChoice()
+ return m, m.current.Init()
+ }
if m.inbox != nil {
m.inbox.RemoveEmail(msg.UID, msg.AccountID)
+ m.current = m.inbox
+ m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ return m, m.current.Init()
}
- m.current = m.inbox
- m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ m.current = tui.NewChoice()
return m, m.current.Init()
case tui.DownloadAttachmentMsg:
@@ -581,7 +638,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
- email := m.getEmailByIndex(msg.Index)
+ email := m.getEmailByIndex(msg.Index, msg.Mailbox)
if email == nil {
m.current = m.previousModel
return m, nil
@@ -602,6 +659,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Data: msg.Data,
AccountID: msg.AccountID,
Encoding: encoding,
+ Mailbox: msg.Mailbox,
}
return m, tea.Batch(m.current.Init(), downloadAttachmentCmd(account, email.UID, newMsg))
@@ -628,72 +686,131 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
-func (m *mainModel) getEmailByIndex(index int) *fetcher.Email {
- if index >= 0 && index < len(m.emails) {
- return &m.emails[index]
+func (m *mainModel) getEmailByIndex(index int, mailbox tui.MailboxKind) *fetcher.Email {
+ switch mailbox {
+ case tui.MailboxSent:
+ if index >= 0 && index < len(m.sentEmails) {
+ return &m.sentEmails[index]
+ }
+ default:
+ if index >= 0 && index < len(m.emails) {
+ return &m.emails[index]
+ }
}
return nil
}
-func (m *mainModel) getEmailByUIDAndAccount(uid uint32, accountID string) *fetcher.Email {
- for i := range m.emails {
- if m.emails[i].UID == uid && m.emails[i].AccountID == accountID {
- return &m.emails[i]
+func (m *mainModel) getEmailByUIDAndAccount(uid uint32, accountID string, mailbox tui.MailboxKind) *fetcher.Email {
+ switch mailbox {
+ case tui.MailboxSent:
+ for i := range m.sentEmails {
+ if m.sentEmails[i].UID == uid && m.sentEmails[i].AccountID == accountID {
+ return &m.sentEmails[i]
+ }
+ }
+ default:
+ for i := range m.emails {
+ if m.emails[i].UID == uid && m.emails[i].AccountID == accountID {
+ return &m.emails[i]
+ }
}
}
return nil
}
-func (m *mainModel) getEmailIndex(uid uint32, accountID string) int {
- for i := range m.emails {
- if m.emails[i].UID == uid && m.emails[i].AccountID == accountID {
- return i
+func (m *mainModel) getEmailIndex(uid uint32, accountID string, mailbox tui.MailboxKind) int {
+ switch mailbox {
+ case tui.MailboxSent:
+ for i := range m.sentEmails {
+ if m.sentEmails[i].UID == uid && m.sentEmails[i].AccountID == accountID {
+ return i
+ }
+ }
+ default:
+ for i := range m.emails {
+ if m.emails[i].UID == uid && m.emails[i].AccountID == accountID {
+ return i
+ }
}
}
return -1
}
-func (m *mainModel) updateEmailBodyByUID(uid uint32, accountID string, body string, attachments []fetcher.Attachment) {
- // Update in all emails list
- for i := range m.emails {
- if m.emails[i].UID == uid && m.emails[i].AccountID == accountID {
- m.emails[i].Body = body
- m.emails[i].Attachments = attachments
- break
+func (m *mainModel) updateEmailBodyByUID(uid uint32, accountID string, mailbox tui.MailboxKind, body string, attachments []fetcher.Attachment) {
+ switch mailbox {
+ case tui.MailboxSent:
+ for i := range m.sentEmails {
+ if m.sentEmails[i].UID == uid && m.sentEmails[i].AccountID == accountID {
+ m.sentEmails[i].Body = body
+ m.sentEmails[i].Attachments = attachments
+ break
+ }
}
- }
-
- // Also update in account-specific store
- if emails, ok := m.emailsByAcct[accountID]; ok {
- for i := range emails {
- if emails[i].UID == uid {
- emails[i].Body = body
- emails[i].Attachments = attachments
+ if emails, ok := m.sentByAcct[accountID]; ok {
+ for i := range emails {
+ if emails[i].UID == uid {
+ emails[i].Body = body
+ emails[i].Attachments = attachments
+ break
+ }
+ }
+ }
+ default:
+ for i := range m.emails {
+ if m.emails[i].UID == uid && m.emails[i].AccountID == accountID {
+ m.emails[i].Body = body
+ m.emails[i].Attachments = attachments
break
}
}
+ if emails, ok := m.emailsByAcct[accountID]; ok {
+ for i := range emails {
+ if emails[i].UID == uid {
+ emails[i].Body = body
+ emails[i].Attachments = attachments
+ break
+ }
+ }
+ }
}
}
-func (m *mainModel) removeEmail(uid uint32, accountID string) {
- // Remove from all emails
- var filtered []fetcher.Email
- for _, e := range m.emails {
- if !(e.UID == uid && e.AccountID == accountID) {
- filtered = append(filtered, e)
+func (m *mainModel) removeEmailByMailbox(uid uint32, accountID string, mailbox tui.MailboxKind) {
+ switch mailbox {
+ case tui.MailboxSent:
+ var filtered []fetcher.Email
+ for _, e := range m.sentEmails {
+ if !(e.UID == uid && e.AccountID == accountID) {
+ filtered = append(filtered, e)
+ }
}
- }
- m.emails = filtered
-
- // Remove from account-specific store
- if emails, ok := m.emailsByAcct[accountID]; ok {
- var filteredAcct []fetcher.Email
- for _, e := range emails {
- if e.UID != uid {
- filteredAcct = append(filteredAcct, e)
+ m.sentEmails = filtered
+ if emails, ok := m.sentByAcct[accountID]; ok {
+ var filteredAcct []fetcher.Email
+ for _, e := range emails {
+ if e.UID != uid {
+ filteredAcct = append(filteredAcct, e)
+ }
+ }
+ m.sentByAcct[accountID] = filteredAcct
+ }
+ default:
+ var filtered []fetcher.Email
+ for _, e := range m.emails {
+ if !(e.UID == uid && e.AccountID == accountID) {
+ filtered = append(filtered, e)
}
}
- m.emailsByAcct[accountID] = filteredAcct
+ m.emails = filtered
+ if emails, ok := m.emailsByAcct[accountID]; ok {
+ var filteredAcct []fetcher.Email
+ for _, e := range emails {
+ if e.UID != uid {
+ filteredAcct = append(filteredAcct, e)
+ }
+ }
+ m.emailsByAcct[accountID] = filteredAcct
+ }
}
}
@@ -701,7 +818,22 @@ func (m *mainModel) View() string {
return m.current.View()
}
-func fetchAllAccountsEmails(cfg *config.Config) tea.Cmd {
+func flattenAndSort(emailsByAccount map[string][]fetcher.Email) []fetcher.Email {
+ var allEmails []fetcher.Email
+ for _, emails := range emailsByAccount {
+ allEmails = append(allEmails, emails...)
+ }
+ for i := 0; i < len(allEmails); i++ {
+ for j := i + 1; j < len(allEmails); j++ {
+ if allEmails[j].Date.After(allEmails[i].Date) {
+ allEmails[i], allEmails[j] = allEmails[j], allEmails[i]
+ }
+ }
+ }
+ return allEmails
+}
+
+func fetchAllAccountsEmails(cfg *config.Config, mailbox tui.MailboxKind) tea.Cmd {
return func() tea.Msg {
emailsByAccount := make(map[string][]fetcher.Email)
var mu sync.Mutex
@@ -711,7 +843,13 @@ func fetchAllAccountsEmails(cfg *config.Config) tea.Cmd {
wg.Add(1)
go func(acc config.Account) {
defer wg.Done()
- emails, err := fetcher.FetchEmails(&acc, initialEmailLimit, 0)
+ var emails []fetcher.Email
+ var err error
+ if mailbox == tui.MailboxSent {
+ emails, err = fetcher.FetchSentEmails(&acc, initialEmailLimit, 0)
+ } else {
+ emails, err = fetcher.FetchEmails(&acc, initialEmailLimit, 0)
+ }
if err != nil {
log.Printf("Error fetching from %s: %v", acc.Email, err)
return
@@ -723,20 +861,26 @@ func fetchAllAccountsEmails(cfg *config.Config) tea.Cmd {
}
wg.Wait()
- return tui.AllEmailsFetchedMsg{EmailsByAccount: emailsByAccount}
+ return tui.AllEmailsFetchedMsg{EmailsByAccount: emailsByAccount, Mailbox: mailbox}
}
}
-func fetchEmails(account *config.Account, limit, offset uint32) tea.Cmd {
+func fetchEmails(account *config.Account, limit, offset uint32, mailbox tui.MailboxKind) tea.Cmd {
return func() tea.Msg {
- emails, err := fetcher.FetchEmails(account, limit, offset)
+ var emails []fetcher.Email
+ var err error
+ if mailbox == tui.MailboxSent {
+ emails, err = fetcher.FetchSentEmails(account, limit, offset)
+ } else {
+ emails, err = fetcher.FetchEmails(account, limit, offset)
+ }
if err != nil {
return tui.FetchErr(err)
}
if offset == 0 {
- return tui.EmailsFetchedMsg{Emails: emails, AccountID: account.ID}
+ return tui.EmailsFetchedMsg{Emails: emails, AccountID: account.ID, Mailbox: mailbox}
}
- return tui.EmailsAppendedMsg{Emails: emails, AccountID: account.ID}
+ return tui.EmailsAppendedMsg{Emails: emails, AccountID: account.ID, Mailbox: mailbox}
}
}
@@ -750,7 +894,7 @@ func loadCachedEmails() tea.Cmd {
}
}
-func refreshEmails(cfg *config.Config) tea.Cmd {
+func refreshEmails(cfg *config.Config, mailbox tui.MailboxKind) tea.Cmd {
return func() tea.Msg {
emailsByAccount := make(map[string][]fetcher.Email)
var mu sync.Mutex
@@ -760,7 +904,13 @@ func refreshEmails(cfg *config.Config) tea.Cmd {
wg.Add(1)
go func(acc config.Account) {
defer wg.Done()
- emails, err := fetcher.FetchEmails(&acc, initialEmailLimit, 0)
+ var emails []fetcher.Email
+ var err error
+ if mailbox == tui.MailboxSent {
+ emails, err = fetcher.FetchSentEmails(&acc, initialEmailLimit, 0)
+ } else {
+ emails, err = fetcher.FetchEmails(&acc, initialEmailLimit, 0)
+ }
if err != nil {
log.Printf("Error fetching from %s: %v", acc.Email, err)
return
@@ -772,7 +922,7 @@ func refreshEmails(cfg *config.Config) tea.Cmd {
}
wg.Wait()
- return tui.EmailsRefreshedMsg{EmailsByAccount: emailsByAccount}
+ return tui.EmailsRefreshedMsg{EmailsByAccount: emailsByAccount, Mailbox: mailbox}
}
}
@@ -820,16 +970,25 @@ func parseEmailAddress(addr string) (name, email string) {
return name, email
}
-func fetchEmailBodyCmd(cfg *config.Config, email fetcher.Email, uid uint32, accountID string) tea.Cmd {
+func fetchEmailBodyCmd(cfg *config.Config, email fetcher.Email, uid uint32, accountID string, mailbox tui.MailboxKind) tea.Cmd {
return func() tea.Msg {
account := cfg.GetAccountByID(accountID)
if account == nil {
- return tui.EmailBodyFetchedMsg{UID: uid, AccountID: accountID, Err: fmt.Errorf("account not found")}
+ return tui.EmailBodyFetchedMsg{UID: uid, AccountID: accountID, Mailbox: mailbox, Err: fmt.Errorf("account not found")}
}
- body, attachments, err := fetcher.FetchEmailBody(account, uid)
+ var (
+ body string
+ attachments []fetcher.Attachment
+ err error
+ )
+ if mailbox == tui.MailboxSent {
+ body, attachments, err = fetcher.FetchSentEmailBody(account, uid)
+ } else {
+ body, attachments, err = fetcher.FetchEmailBody(account, uid)
+ }
if err != nil {
- return tui.EmailBodyFetchedMsg{UID: uid, AccountID: accountID, Err: err}
+ return tui.EmailBodyFetchedMsg{UID: uid, AccountID: accountID, Mailbox: mailbox, Err: err}
}
return tui.EmailBodyFetchedMsg{
@@ -837,6 +996,7 @@ func fetchEmailBodyCmd(cfg *config.Config, email fetcher.Email, uid uint32, acco
Body: body,
Attachments: attachments,
AccountID: accountID,
+ Mailbox: mailbox,
}
}
}
@@ -897,24 +1057,40 @@ func sendEmail(account *config.Account, msg tui.SendEmailMsg) tea.Cmd {
}
}
-func deleteEmailCmd(account *config.Account, uid uint32, accountID string) tea.Cmd {
+func deleteEmailCmd(account *config.Account, uid uint32, accountID string, mailbox tui.MailboxKind) tea.Cmd {
return func() tea.Msg {
- err := fetcher.DeleteEmail(account, uid)
- return tui.EmailActionDoneMsg{UID: uid, AccountID: accountID, Err: err}
+ var err error
+ if mailbox == tui.MailboxSent {
+ err = fetcher.DeleteSentEmail(account, uid)
+ } else {
+ err = fetcher.DeleteEmail(account, uid)
+ }
+ return tui.EmailActionDoneMsg{UID: uid, AccountID: accountID, Mailbox: mailbox, Err: err}
}
}
-func archiveEmailCmd(account *config.Account, uid uint32, accountID string) tea.Cmd {
+func archiveEmailCmd(account *config.Account, uid uint32, accountID string, mailbox tui.MailboxKind) tea.Cmd {
return func() tea.Msg {
- err := fetcher.ArchiveEmail(account, uid)
- return tui.EmailActionDoneMsg{UID: uid, AccountID: accountID, Err: err}
+ var err error
+ if mailbox == tui.MailboxSent {
+ err = fetcher.ArchiveSentEmail(account, uid)
+ } else {
+ err = fetcher.ArchiveEmail(account, uid)
+ }
+ return tui.EmailActionDoneMsg{UID: uid, AccountID: accountID, Mailbox: mailbox, Err: err}
}
}
func downloadAttachmentCmd(account *config.Account, uid uint32, msg tui.DownloadAttachmentMsg) tea.Cmd {
return func() tea.Msg {
// Download and decode the attachment using encoding provided in msg.Encoding.
- data, err := fetcher.FetchAttachment(account, uid, msg.PartID, msg.Encoding)
+ var data []byte
+ var err error
+ if msg.Mailbox == tui.MailboxSent {
+ data, err = fetcher.FetchSentAttachment(account, uid, msg.PartID, msg.Encoding)
+ } else {
+ data, err = fetcher.FetchAttachment(account, uid, msg.PartID, msg.Encoding)
+ }
if err != nil {
return tui.AttachmentDownloadedMsg{Err: err}
}
@@ -40,7 +40,7 @@ type Choice struct {
func NewChoice() Choice {
hasSavedDrafts := config.HasDrafts()
- choices := []string{"View Inbox", "Compose Email"}
+ choices := []string{"View Inbox", "View Sent", "Compose Email"}
if hasSavedDrafts {
choices = append(choices, "Drafts")
}
@@ -75,6 +75,8 @@ func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch selectedChoice {
case "View Inbox":
return m, func() tea.Msg { return GoToInboxMsg{} }
+ case "View Sent":
+ return m, func() tea.Msg { return GoToSentInboxMsg{} }
case "Compose Email":
return m, func() tea.Msg { return GoToSendMsg{} }
case "Drafts":
@@ -23,9 +23,10 @@ type EmailView struct {
attachmentCursor int
focusOnAttachments bool
accountID string
+ mailbox MailboxKind
}
-func NewEmailView(email fetcher.Email, emailIndex, width, height int) *EmailView {
+func NewEmailView(email fetcher.Email, emailIndex, width, height int, mailbox MailboxKind) *EmailView {
// Pass the styles from the tui package to the view package
body, err := view.ProcessBody(email.Body, H1Style, H2Style, BodyStyle)
if err != nil {
@@ -53,6 +54,7 @@ func NewEmailView(email fetcher.Email, emailIndex, width, height int) *EmailView
email: email,
emailIndex: emailIndex,
accountID: email.AccountID,
+ mailbox: mailbox,
}
}
@@ -72,7 +74,7 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.focusOnAttachments = false
return m, nil
}
- return m, func() tea.Msg { return BackToInboxMsg{} }
+ return m, func() tea.Msg { return BackToMailboxMsg{Mailbox: m.mailbox} }
}
if m.focusOnAttachments {
@@ -97,6 +99,7 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
PartID: selected.PartID,
Data: selected.Data,
AccountID: accountID,
+ Mailbox: m.mailbox,
}
}
}
@@ -111,13 +114,13 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
accountID := m.accountID
uid := m.email.UID
return m, func() tea.Msg {
- return DeleteEmailMsg{UID: uid, AccountID: accountID}
+ return DeleteEmailMsg{UID: uid, AccountID: accountID, Mailbox: m.mailbox}
}
case "a":
accountID := m.accountID
uid := m.email.UID
return m, func() tea.Msg {
- return ArchiveEmailMsg{UID: uid, AccountID: accountID}
+ return ArchiveEmailMsg{UID: uid, AccountID: accountID, Mailbox: m.mailbox}
}
case "tab":
if len(m.email.Attachments) > 0 {
@@ -28,7 +28,7 @@ func TestEmailViewUpdate(t *testing.T) {
}
t.Run("Focus on attachments", func(t *testing.T) {
- emailView := NewEmailView(emailWithAttachments, 0, 80, 24)
+ emailView := NewEmailView(emailWithAttachments, 0, 80, 24, MailboxInbox)
if emailView.focusOnAttachments {
t.Error("focusOnAttachments should be initially false")
}
@@ -50,7 +50,7 @@ func TestEmailViewUpdate(t *testing.T) {
})
t.Run("No focus on attachments when there are none", func(t *testing.T) {
- emailView := NewEmailView(emailWithoutAttachments, 0, 80, 24)
+ emailView := NewEmailView(emailWithoutAttachments, 0, 80, 24, MailboxInbox)
if emailView.focusOnAttachments {
t.Error("focusOnAttachments should be initially false")
}
@@ -63,7 +63,7 @@ func TestEmailViewUpdate(t *testing.T) {
})
t.Run("Navigate attachments", func(t *testing.T) {
- emailView := NewEmailView(emailWithAttachments, 0, 80, 24)
+ emailView := NewEmailView(emailWithAttachments, 0, 80, 24, MailboxInbox)
// Focus on attachments
model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab})
emailView = model.(*EmailView)
@@ -95,7 +95,7 @@ func TestEmailViewUpdate(t *testing.T) {
})
t.Run("Download attachment", func(t *testing.T) {
- emailView := NewEmailView(emailWithAttachments, 0, 80, 24)
+ emailView := NewEmailView(emailWithAttachments, 0, 80, 24, MailboxInbox)
// Focus on attachments
model, _ := emailView.Update(tea.KeyMsg{Type: tea.KeyTab})
emailView = model.(*EmailView)
@@ -118,10 +118,13 @@ func TestEmailViewUpdate(t *testing.T) {
if downloadMsg.Filename != "attachment2.txt" {
t.Errorf("Expected to download 'attachment2.txt', but got '%s'", downloadMsg.Filename)
}
+ if downloadMsg.Mailbox != MailboxInbox {
+ t.Errorf("Expected mailbox to be MailboxInbox, got %s", downloadMsg.Mailbox)
+ }
})
t.Run("Reply to email", func(t *testing.T) {
- emailView := NewEmailView(emailWithAttachments, 0, 80, 24)
+ emailView := NewEmailView(emailWithAttachments, 0, 80, 24, MailboxInbox)
_, cmd := emailView.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("r")})
if cmd == nil {
@@ -94,9 +94,18 @@ type Inbox struct {
height int
currentAccountID string // Empty means "ALL"
emailCountByAcct map[string]int
+ mailbox MailboxKind
}
func NewInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
+ return NewInboxWithMailbox(emails, accounts, MailboxInbox)
+}
+
+func NewSentInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
+ return NewInboxWithMailbox(emails, accounts, MailboxSent)
+}
+
+func NewInboxWithMailbox(emails []fetcher.Email, accounts []config.Account, mailbox MailboxKind) *Inbox {
// Build tabs: "ALL" + one per account
tabs := []AccountTab{{ID: "", Label: "ALL", Email: ""}}
for _, acc := range accounts {
@@ -124,6 +133,7 @@ func NewInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
activeTabIndex: 0,
currentAccountID: "",
emailCountByAcct: emailCountByAcct,
+ mailbox: mailbox,
}
inbox.updateList()
@@ -207,15 +217,15 @@ func (m *Inbox) updateList() {
func (m *Inbox) getTitle() string {
var title string
if m.currentAccountID == "" {
- title = "Inbox - All Accounts"
+ title = m.getBaseTitle() + " - All Accounts"
} else {
- title = "Inbox"
+ title = m.getBaseTitle()
for _, acc := range m.accounts {
if acc.ID == m.currentAccountID {
if acc.Name != "" {
- title = fmt.Sprintf("Inbox - %s", acc.Name)
+ title = fmt.Sprintf("%s - %s", m.getBaseTitle(), acc.Name)
} else {
- title = fmt.Sprintf("Inbox - %s", acc.Email)
+ title = fmt.Sprintf("%s - %s", m.getBaseTitle(), acc.Email)
}
break
}
@@ -227,6 +237,15 @@ func (m *Inbox) getTitle() string {
return title
}
+func (m *Inbox) getBaseTitle() string {
+ switch m.mailbox {
+ case MailboxSent:
+ return "Sent"
+ default:
+ return "Inbox"
+ }
+}
+
func (m *Inbox) Init() tea.Cmd {
return nil
}
@@ -264,14 +283,14 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
selectedItem, ok := m.list.SelectedItem().(item)
if ok {
return m, func() tea.Msg {
- return DeleteEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID}
+ return DeleteEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
}
}
case "a":
selectedItem, ok := m.list.SelectedItem().(item)
if ok {
return m, func() tea.Msg {
- return ArchiveEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID}
+ return ArchiveEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
}
}
case "enter":
@@ -281,7 +300,7 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
uid := selectedItem.uid
accountID := selectedItem.accountID
return m, func() tea.Msg {
- return ViewEmailMsg{Index: idx, UID: uid, AccountID: accountID}
+ return ViewEmailMsg{Index: idx, UID: uid, AccountID: accountID, Mailbox: m.mailbox}
}
}
}
@@ -297,6 +316,9 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case EmailsAppendedMsg:
+ if msg.Mailbox != m.mailbox {
+ return m, nil
+ }
m.isFetching = false
m.list.Title = m.getTitle()
@@ -311,11 +333,17 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
case RefreshingEmailsMsg:
+ if msg.Mailbox != m.mailbox {
+ return m, nil
+ }
m.isRefreshing = true
m.list.Title = m.getTitle()
return m, nil
case EmailsRefreshedMsg:
+ if msg.Mailbox != m.mailbox {
+ return m, nil
+ }
m.isRefreshing = false
// Replace emails with fresh data
@@ -356,7 +384,7 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// For simplicity, don't auto-fetch more in ALL view
} else if accountID != "" {
cmds = append(cmds, func() tea.Msg {
- return FetchMoreEmailsMsg{Offset: offset, AccountID: accountID}
+ return FetchMoreEmailsMsg{Offset: offset, AccountID: accountID, Mailbox: m.mailbox}
})
}
}
@@ -414,6 +442,10 @@ func (m *Inbox) GetEmailAtIndex(index int) *fetcher.Email {
return nil
}
+func (m *Inbox) GetMailbox() MailboxKind {
+ return m.mailbox
+}
+
// RemoveEmail removes an email by UID and account ID
func (m *Inbox) RemoveEmail(uid uint32, accountID string) {
// Remove from account-specific list
@@ -5,10 +5,18 @@ import (
"github.com/floatpane/matcha/fetcher"
)
+type MailboxKind string
+
+const (
+ MailboxInbox MailboxKind = "inbox"
+ MailboxSent MailboxKind = "sent"
+)
+
type ViewEmailMsg struct {
Index int
UID uint32
AccountID string
+ Mailbox MailboxKind
}
type SendEmailMsg struct {
@@ -46,12 +54,16 @@ type ClearStatusMsg struct{}
type EmailsFetchedMsg struct {
Emails []fetcher.Email
AccountID string
+ Mailbox MailboxKind
}
type FetchErr error
type GoToInboxMsg struct{}
+type GoToSentInboxMsg struct{}
+
+
type GoToSendMsg struct {
To string
Subject string
@@ -63,6 +75,7 @@ type GoToSettingsMsg struct{}
type FetchMoreEmailsMsg struct {
Offset uint32
AccountID string
+ Mailbox MailboxKind
}
type FetchingMoreEmailsMsg struct{}
@@ -70,6 +83,7 @@ type FetchingMoreEmailsMsg struct{}
type EmailsAppendedMsg struct {
Emails []fetcher.Email
AccountID string
+ Mailbox MailboxKind
}
type ReplyToEmailMsg struct {
@@ -89,16 +103,19 @@ type CancelFilePickerMsg struct{}
type DeleteEmailMsg struct {
UID uint32
AccountID string
+ Mailbox MailboxKind
}
type ArchiveEmailMsg struct {
UID uint32
AccountID string
+ Mailbox MailboxKind
}
type EmailActionDoneMsg struct {
UID uint32
AccountID string
+ Mailbox MailboxKind
Err error
}
@@ -111,6 +128,7 @@ type DownloadAttachmentMsg struct {
Data []byte
AccountID string
Encoding string
+ Mailbox MailboxKind
}
type AttachmentDownloadedMsg struct {
@@ -122,6 +140,10 @@ type RestoreViewMsg struct{}
type BackToInboxMsg struct{}
+type BackToMailboxMsg struct {
+ Mailbox MailboxKind
+}
+
// --- Draft Messages ---
// DiscardDraftMsg signals that a draft should be cached.
@@ -135,6 +157,7 @@ type EmailBodyFetchedMsg struct {
Attachments []fetcher.Attachment
Err error
AccountID string
+ Mailbox MailboxKind
}
// --- Multi-Account Messages ---
@@ -172,6 +195,7 @@ type SwitchAccountMsg struct {
// AllEmailsFetchedMsg signals that emails from all accounts have been fetched.
type AllEmailsFetchedMsg struct {
EmailsByAccount map[string][]fetcher.Email
+ Mailbox MailboxKind
}
// SwitchFromAccountMsg signals changing the "From" account in composer.
@@ -230,9 +254,12 @@ type CachedEmailsLoadedMsg struct {
}
// RefreshingEmailsMsg signals that a background refresh is in progress.
-type RefreshingEmailsMsg struct{}
+type RefreshingEmailsMsg struct{
+ Mailbox MailboxKind
+}
// EmailsRefreshedMsg signals that fresh emails have been fetched in the background.
type EmailsRefreshedMsg struct {
EmailsByAccount map[string][]fetcher.Email
+ Mailbox MailboxKind
}