feat: sent inbox (#64) (#69)

Drew Smirnoff created

Change summary

fetcher/fetcher.go     |  77 ++++++-
go.mod                 |   7 
main.go                | 446 ++++++++++++++++++++++++++++++-------------
tui/choice.go          |   4 
tui/email_view.go      |  11 
tui/email_view_test.go |  13 
tui/inbox.go           |  48 +++
tui/messages.go        |  29 ++
8 files changed, 465 insertions(+), 170 deletions(-)

Detailed changes

fetcher/fetcher.go 🔗

@@ -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)
 }

go.mod 🔗

@@ -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
 )

main.go 🔗

@@ -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}
 		}

tui/choice.go 🔗

@@ -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":

tui/email_view.go 🔗

@@ -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 {

tui/email_view_test.go 🔗

@@ -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 {

tui/inbox.go 🔗

@@ -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

tui/messages.go 🔗

@@ -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
 }