Detailed changes
@@ -18,12 +18,13 @@ import (
)
type Email struct {
- From string
- To []string
- Subject string
- Body string
- Date time.Time
- MessageID string
+ UID uint32 // Added UID to uniquely identify emails
+ From string
+ To []string
+ Subject string
+ Body string
+ Date time.Time
+ MessageID string
References []string
}
@@ -34,7 +35,7 @@ func decodePart(reader io.Reader, header mail.PartHeader) (string, error) {
return string(body), nil
}
- charset := "utf-8" // Default charset
+ charset := "utf-8"
if params["charset"] != "" {
charset = strings.ToLower(params["charset"])
}
@@ -73,8 +74,8 @@ func decodeHeader(header string) string {
return decoded
}
-// FetchEmails now supports pagination with a limit and offset.
-func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
+// connect initializes a connection to the IMAP server.
+func connect(cfg *config.Config) (*client.Client, error) {
var imapServer string
var imapPort int
@@ -86,7 +87,7 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
imapServer = "imap.mail.me.com"
imapPort = 993
default:
- return nil, fmt.Errorf("unsupported or missing service_provider in config.json: %s", cfg.ServiceProvider)
+ return nil, fmt.Errorf("unsupported service_provider: %s", cfg.ServiceProvider)
}
addr := fmt.Sprintf("%s:%d", imapServer, imapPort)
@@ -94,20 +95,26 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
if err != nil {
return nil, err
}
- log.Println("Connected")
- defer c.Logout()
if err := c.Login(cfg.Email, cfg.Password); err != nil {
return nil, err
}
- log.Println("Logged in")
+
+ return c, nil
+}
+
+func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
+ c, err := connect(cfg)
+ if err != nil {
+ return nil, err
+ }
+ defer c.Logout()
mbox, err := c.Select("INBOX", false)
if err != nil {
return nil, err
}
- // Handle pagination logic
if mbox.Messages == 0 {
return []Email{}, nil
}
@@ -119,7 +126,7 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
}
if to < 1 {
- return []Email{}, nil // No more messages to fetch
+ return []Email{}, nil
}
seqset := new(imap.SeqSet)
@@ -127,8 +134,9 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
messages := make(chan *imap.Message, limit)
done := make(chan error, 1)
+ fetchItems := []imap.FetchItem{imap.FetchEnvelope, imap.FetchUid, imap.FetchItem("BODY[]")}
go func() {
- done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchItem("BODY[]")}, messages)
+ done <- c.Fetch(seqset, fetchItems, messages)
}()
var emails []Email
@@ -188,6 +196,7 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
}
emails = append(emails, Email{
+ UID: msg.Uid,
From: fromAddr,
To: toAddrList,
Subject: subject,
@@ -207,4 +216,46 @@ func FetchEmails(cfg *config.Config, limit, offset uint32) ([]Email, error) {
}
return emails, nil
+}
+
+// moveEmail moves an email to a specified destination mailbox.
+func moveEmail(cfg *config.Config, uid uint32, destMailbox string) error {
+ c, err := connect(cfg)
+ if err != nil {
+ return err
+ }
+ defer c.Logout()
+
+ if _, err := c.Select("INBOX", false); err != nil {
+ return err
+ }
+
+ seqSet := new(imap.SeqSet)
+ seqSet.AddNum(uid)
+
+ return c.UidMove(seqSet, destMailbox)
+}
+
+// DeleteEmail moves an email to the trash folder.
+func DeleteEmail(cfg *config.Config, uid uint32) error {
+ var trashMailbox string
+ switch cfg.ServiceProvider {
+ case "gmail":
+ trashMailbox = "[Gmail]/Trash"
+ default:
+ trashMailbox = "Trash"
+ }
+ return moveEmail(cfg, uid, trashMailbox)
+}
+
+// ArchiveEmail moves an email to the archive folder.
+func ArchiveEmail(cfg *config.Config, uid uint32) error {
+ var archiveMailbox string
+ switch cfg.ServiceProvider {
+ case "gmail":
+ archiveMailbox = "[Gmail]/All Mail"
+ default:
+ archiveMailbox = "Archive"
+ }
+ return moveEmail(cfg, uid, archiveMailbox)
}
@@ -26,10 +26,9 @@ const (
paginationLimit = 20
)
-// mainModel holds the state for the entire application.
type mainModel struct {
current tea.Model
- previousModel tea.Model // To store the view before opening the file picker
+ previousModel tea.Model
config *config.Config
emails []fetcher.Email
inbox *tui.Inbox
@@ -38,7 +37,6 @@ type mainModel struct {
err error
}
-// newInitialModel returns a pointer to the initial model.
func newInitialModel(cfg *config.Config) *mainModel {
if cfg == nil {
return &mainModel{
@@ -72,7 +70,6 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit
}
if msg.String() == "esc" {
- // If we are in a nested view like email or file picker, handle it here
switch m.current.(type) {
case *tui.EmailView:
m.current = m.inbox
@@ -154,10 +151,8 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tui.FileSelectedMsg:
if m.previousModel != nil {
- // Send the selection message to the previous model (the composer)
m.previousModel, cmd = m.previousModel.Update(msg)
cmds = append(cmds, cmd)
- // Return to the composer view
m.current = m.previousModel
m.previousModel = nil
}
@@ -177,6 +172,36 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tui.EmailResultMsg:
m.current = tui.NewChoice()
cmds = append(cmds, m.current.Init())
+
+ case tui.DeleteEmailMsg:
+ m.current = tui.NewStatus("Deleting email...")
+ cmds = append(cmds, m.current.Init(), deleteEmailCmd(m.config, msg.UID))
+
+ case tui.ArchiveEmailMsg:
+ m.current = tui.NewStatus("Archiving email...")
+ cmds = append(cmds, m.current.Init(), archiveEmailCmd(m.config, msg.UID))
+
+ case tui.EmailActionDoneMsg:
+ if msg.Err != nil {
+ // In a real app, you might show an error message.
+ // For now, we'll just go back to the inbox.
+ log.Printf("Action failed: %v", msg.Err)
+ m.current = m.inbox
+ return m, nil
+ }
+ // Remove the email from the local cache
+ var updatedEmails []fetcher.Email
+ for _, email := range m.emails {
+ if email.UID != msg.UID {
+ updatedEmails = append(updatedEmails, email)
+ }
+ }
+ m.emails = updatedEmails
+ // Refresh the inbox view
+ m.inbox = tui.NewInbox(m.emails)
+ m.current = m.inbox
+ m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
+ return m, m.current.Init()
}
m.current, cmd = m.current.Update(msg)
@@ -259,6 +284,20 @@ func fetchEmails(cfg *config.Config, limit, offset uint32) tea.Cmd {
}
}
+func deleteEmailCmd(cfg *config.Config, uid uint32) tea.Cmd {
+ return func() tea.Msg {
+ err := fetcher.DeleteEmail(cfg, uid)
+ return tui.EmailActionDoneMsg{UID: uid, Err: err}
+ }
+}
+
+func archiveEmailCmd(cfg *config.Config, uid uint32) tea.Cmd {
+ return func() tea.Msg {
+ err := fetcher.ArchiveEmail(cfg, uid)
+ return tui.EmailActionDoneMsg{UID: uid, Err: err}
+ }
+}
+
func main() {
cfg, err := config.LoadConfig()
var initialModel *mainModel
@@ -5,6 +5,7 @@ import (
"io"
"github.com/andrinoff/email-cli/fetcher"
+ "github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
@@ -15,10 +16,10 @@ var (
inboxHelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
)
-// item now holds its original index from the main email slice.
type item struct {
title, desc string
originalIndex int
+ uid uint32 // Added UID to item
}
func (i item) Title() string { return i.title }
@@ -48,7 +49,6 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
fmt.Fprint(w, fn(str))
}
-// Inbox is now stateful to handle pagination.
type Inbox struct {
list list.Model
isFetching bool
@@ -61,7 +61,8 @@ func NewInbox(emails []fetcher.Email) *Inbox {
items[i] = item{
title: email.Subject,
desc: email.From,
- originalIndex: i, // Store the original index here.
+ originalIndex: i,
+ uid: email.UID, // Store UID
}
}
@@ -73,6 +74,12 @@ func NewInbox(emails []fetcher.Email) *Inbox {
l.Styles.PaginationStyle = paginationStyle
l.Styles.HelpStyle = inboxHelpStyle
l.SetStatusBarItemName("email", "emails")
+ l.AdditionalShortHelpKeys = func() []key.Binding {
+ return []key.Binding{
+ key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "delete")),
+ key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "archive")),
+ }
+ }
return &Inbox{
list: l,
@@ -90,13 +97,28 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
- // When the user presses enter, we look at the selected item and send
- // a message with its *original* index.
- if msg.String() == "enter" {
+ if m.list.FilterState() == list.Filtering {
+ break
+ }
+ switch keypress := msg.String(); keypress {
+ case "d":
+ selectedItem, ok := m.list.SelectedItem().(item)
+ if ok {
+ return m, func() tea.Msg {
+ return DeleteEmailMsg{UID: selectedItem.uid}
+ }
+ }
+ case "a":
+ selectedItem, ok := m.list.SelectedItem().(item)
+ if ok {
+ return m, func() tea.Msg {
+ return ArchiveEmailMsg{UID: selectedItem.uid}
+ }
+ }
+ case "enter":
selectedItem, ok := m.list.SelectedItem().(item)
if ok {
return m, func() tea.Msg {
- // Use the stored original index, which is correct even when filtered.
return ViewEmailMsg{Index: selectedItem.originalIndex}
}
}
@@ -118,7 +140,8 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
newItems[i] = item{
title: email.Subject,
desc: email.From,
- originalIndex: m.emailsCount + i, // The original index continues to grow.
+ originalIndex: m.emailsCount + i,
+ uid: email.UID,
}
}
currentItems := m.list.Items()
@@ -129,26 +152,14 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
- // Infinite scroll logic
if !m.isFetching && len(m.list.Items()) > 0 && m.list.Index() >= len(m.list.Items())-5 {
cmds = append(cmds, func() tea.Msg {
return FetchMoreEmailsMsg{Offset: uint32(m.emailsCount)}
})
}
- // New logic to fetch more emails when filtering is active
- if m.list.FilterState() == list.Filtering && !m.isFetching {
- m.isFetching = true
- m.list.Title = "Fetching more emails..."
- cmds = append(cmds, func() tea.Msg {
- return FetchMoreEmailsMsg{Offset: uint32(m.emailsCount)}
- })
- }
-
var cmd tea.Cmd
- var newModel list.Model
- newModel, cmd = m.list.Update(msg)
- m.list = newModel
+ m.list, cmd = m.list.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
@@ -2,12 +2,10 @@ package tui
import "github.com/andrinoff/email-cli/fetcher"
-// A message to view an email.
type ViewEmailMsg struct {
Index int
}
-// A message to indicate that an email has been sent.
type SendEmailMsg struct {
To string
Subject string
@@ -17,7 +15,6 @@ type SendEmailMsg struct {
References []string
}
-// A message to indicate that the user has entered their credentials.
type Credentials struct {
Provider string
Name string
@@ -25,71 +22,68 @@ type Credentials struct {
Password string
}
-// A message to indicate that the user has chosen a service.
type ChooseServiceMsg struct {
Service string
}
-// EmailResultMsg is sent after an email sending attempt.
-// If Err is not nil, the email failed to send.
type EmailResultMsg struct {
Err error
}
-// ClearStatusMsg is sent to clear the status message from the view.
type ClearStatusMsg struct{}
-// A message containing the fetched emails.
type EmailsFetchedMsg struct {
Emails []fetcher.Email
}
-// A message to indicate that an error occurred while fetching emails.
type FetchErr error
-// A message to navigate to the inbox view.
type GoToInboxMsg struct{}
-// A message to navigate to the composer view.
type GoToSendMsg struct {
To string
Subject string
Body string
}
-// A message to navigate to the settings view.
type GoToSettingsMsg struct{}
-// A message to fetch more emails with a given offset.
type FetchMoreEmailsMsg struct {
Offset uint32
}
-// A message to indicate that the app is fetching more emails.
type FetchingMoreEmailsMsg struct{}
-// A message to indicate that new emails have been fetched and should be appended.
type EmailsAppendedMsg struct {
Emails []fetcher.Email
}
-// A message to reply to an email.
type ReplyToEmailMsg struct {
Email fetcher.Email
}
-// A message to set the composer cursor to the start.
type SetComposerCursorToStartMsg struct{}
-// --- File Picker Messages ---
-
-// A message to open the file picker.
type GoToFilePickerMsg struct{}
-// A message sent when a file has been selected.
type FileSelectedMsg struct {
Path string
}
-// A message to cancel the file picker and return to the previous view.
-type CancelFilePickerMsg struct{}
+type CancelFilePickerMsg struct{}
+
+// --- Email Action Messages ---
+
+type DeleteEmailMsg struct {
+ UID uint32
+}
+
+type ArchiveEmailMsg struct {
+ UID uint32
+}
+
+// EmailActionDoneMsg reports the result of an action like delete or archive.
+type EmailActionDoneMsg struct {
+ UID uint32
+ Err error
+}