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}