main.go

  1package main
  2
  3import (
  4	"bytes"
  5	"fmt"
  6	"log"
  7	"os"
  8	"time"
  9
 10	"github.com/andrinoff/email-cli/config"
 11	"github.com/andrinoff/email-cli/fetcher"
 12	"github.com/andrinoff/email-cli/sender"
 13	"github.com/andrinoff/email-cli/tui"
 14	tea "github.com/charmbracelet/bubbletea"
 15	"github.com/yuin/goldmark"
 16	"github.com/yuin/goldmark/renderer/html"
 17)
 18
 19// mainModel now holds the state for the entire application.
 20type mainModel struct {
 21	current tea.Model
 22	config  *config.Config
 23	emails  []fetcher.Email
 24	inbox   *tui.Inbox
 25	width   int
 26	height  int
 27	err     error
 28}
 29
 30// newInitialModel now returns a pointer, which is crucial for state management.
 31func newInitialModel(cfg *config.Config) *mainModel {
 32	// If config is nil, it means we're starting from the login screen.
 33	if cfg == nil {
 34		return &mainModel{
 35			current: tui.NewLogin(),
 36		}
 37	}
 38	return &mainModel{
 39		current: tui.NewChoice(),
 40		config:  cfg,
 41	}
 42}
 43
 44func (m *mainModel) Init() tea.Cmd {
 45	return m.current.Init()
 46}
 47
 48func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 49	var cmd tea.Cmd
 50	var cmds []tea.Cmd
 51
 52	switch msg := msg.(type) {
 53	case tea.WindowSizeMsg:
 54		m.width = msg.Width
 55		m.height = msg.Height
 56		// Pass the window size message to the current view
 57		m.current, cmd = m.current.Update(msg)
 58		cmds = append(cmds, cmd)
 59		return m, tea.Batch(cmds...)
 60
 61	case tea.KeyMsg:
 62		if msg.String() == "ctrl+c" {
 63			return m, tea.Quit
 64		}
 65		// Allow ESC to go back
 66		if msg.String() == "esc" {
 67			// Check the type of the current view to decide where to go.
 68			switch m.current.(type) {
 69			case *tui.EmailView:
 70				m.current = m.inbox // Go back to the cached inbox
 71				return m, nil
 72			case *tui.Inbox, *tui.Composer, *tui.Login:
 73				m.current = tui.NewChoice() // Go back to the main menu
 74				return m, m.current.Init()
 75			}
 76		}
 77
 78	// --- Custom Messages for switching views ---
 79	case tui.Credentials: // This is a struct, not a pointer
 80		cfg := &config.Config{
 81			ServiceProvider: msg.Provider,
 82			Name:            msg.Name,
 83			Email:           msg.Email,
 84			Password:        msg.Password,
 85		}
 86		if err := config.SaveConfig(cfg); err != nil {
 87			// TODO: Show an error message to the user
 88			log.Printf("could not save config: %v", err)
 89			return m, tea.Quit
 90		}
 91		m.config = cfg
 92		m.current = tui.NewChoice()
 93		cmds = append(cmds, m.current.Init())
 94
 95	case tui.GoToInboxMsg:
 96		m.current = tui.NewStatus("Fetching emails...")
 97		return m, tea.Batch(m.current.Init(), fetchEmails(m.config))
 98
 99	case tui.EmailsFetchedMsg:
100		m.emails = msg.Emails
101		m.inbox = tui.NewInbox(m.emails)
102		m.current = m.inbox
103		// Manually set the size of the new view
104		m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
105		cmds = append(cmds, m.current.Init())
106
107	case tui.GoToSendMsg:
108		// NewComposer now returns *Composer, so we assign it directly.
109		m.current = tui.NewComposer(m.config.Email)
110		m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
111		cmds = append(cmds, m.current.Init())
112
113	case tui.GoToSettingsMsg:
114		m.current = tui.NewLogin()
115		m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
116		cmds = append(cmds, m.current.Init())
117
118	case tui.ViewEmailMsg:
119		emailView := tui.NewEmailView(m.emails[msg.Index], m.width, m.height)
120		m.current = emailView
121		cmds = append(cmds, m.current.Init())
122
123	case tui.SendEmailMsg:
124		m.current = tui.NewStatus("Sending email...")
125		cmds = append(cmds, m.current.Init(), sendEmail(m.config, msg))
126
127	case tui.EmailResultMsg:
128		m.current = tui.NewChoice()
129		cmds = append(cmds, m.current.Init())
130	}
131
132	// Pass all other messages to the current view
133	m.current, cmd = m.current.Update(msg)
134	cmds = append(cmds, cmd)
135
136	return m, tea.Batch(cmds...)
137}
138
139func (m *mainModel) View() string {
140	return m.current.View()
141}
142
143// markdownToHTML converts a Markdown string to an HTML string.
144func markdownToHTML(md []byte) []byte {
145	var buf bytes.Buffer
146	p := goldmark.New(
147		goldmark.WithRendererOptions(
148			html.WithUnsafe(), // Allow raw HTML, which is useful in emails
149		),
150	)
151	if err := p.Convert(md, &buf); err != nil {
152		// As a fallback, just return the original markdown.
153		return md
154	}
155	return buf.Bytes()
156}
157
158// sendEmail is a command that sends an email in the background.
159func sendEmail(cfg *config.Config, msg tui.SendEmailMsg) tea.Cmd {
160	return func() tea.Msg {
161		recipients := []string{msg.To}
162
163		// Convert markdown body to HTML
164		htmlBody := markdownToHTML([]byte(msg.Body))
165
166		// The original markdown body will serve as our plain text fallback.
167		err := sender.SendEmail(cfg, recipients, msg.Subject, msg.Body, string(htmlBody))
168
169		if err != nil {
170			log.Printf("Failed to send email: %v", err) // Log error
171			return tui.EmailResultMsg{Err: err}
172		}
173		time.Sleep(1 * time.Second) // Give user time to see the "Sending" message
174		return tui.EmailResultMsg{}
175	}
176}
177
178func fetchEmails(cfg *config.Config) tea.Cmd {
179	return func() tea.Msg {
180		emails, err := fetcher.FetchEmails(cfg)
181		if err != nil {
182			return tui.FetchErr(err)
183		}
184		return tui.EmailsFetchedMsg{Emails: emails}
185	}
186}
187
188func main() {
189	cfg, err := config.LoadConfig()
190	var initialModel *mainModel
191	if err != nil {
192		// If the config doesn't exist, we can guide the user to create one.
193		initialModel = newInitialModel(nil)
194	} else {
195		initialModel = newInitialModel(cfg)
196	}
197
198	p := tea.NewProgram(initialModel, tea.WithAltScreen())
199
200	if _, err := p.Run(); err != nil {
201		fmt.Printf("Alas, there's been an error: %v", err)
202		os.Exit(1)
203	}
204}