1package main
2
3import (
4 "bytes"
5 "encoding/base64"
6 "fmt"
7 "log"
8 "os"
9 "path/filepath"
10 "regexp"
11 "strings"
12 "time"
13
14 "github.com/andrinoff/email-cli/config"
15 "github.com/andrinoff/email-cli/fetcher"
16 "github.com/andrinoff/email-cli/sender"
17 "github.com/andrinoff/email-cli/tui"
18 tea "github.com/charmbracelet/bubbletea"
19 "github.com/google/uuid"
20 "github.com/yuin/goldmark"
21 "github.com/yuin/goldmark/renderer/html"
22)
23
24// mainModel holds the state for the entire application.
25type mainModel struct {
26 current tea.Model
27 config *config.Config
28 emails []fetcher.Email
29 inbox *tui.Inbox
30 width int
31 height int
32 err error
33}
34
35// newInitialModel returns a pointer to the initial model.
36func newInitialModel(cfg *config.Config) *mainModel {
37 // If config is nil, start with the login screen.
38 if cfg == nil {
39 return &mainModel{
40 current: tui.NewLogin(),
41 }
42 }
43 return &mainModel{
44 current: tui.NewChoice(),
45 config: cfg,
46 }
47}
48
49func (m *mainModel) Init() tea.Cmd {
50 return m.current.Init()
51}
52
53func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
54 var cmd tea.Cmd
55 var cmds []tea.Cmd
56
57 switch msg := msg.(type) {
58 case tea.WindowSizeMsg:
59 m.width = msg.Width
60 m.height = msg.Height
61 // Pass the window size message to the current view.
62 m.current, cmd = m.current.Update(msg)
63 cmds = append(cmds, cmd)
64 return m, tea.Batch(cmds...)
65
66 case tea.KeyMsg:
67 if msg.String() == "ctrl+c" {
68 return m, tea.Quit
69 }
70 // Allow ESC to navigate back.
71 if msg.String() == "esc" {
72 switch m.current.(type) {
73 case *tui.EmailView:
74 m.current = m.inbox // Go back to the cached inbox.
75 return m, nil
76 case *tui.Inbox, *tui.Composer, *tui.Login:
77 m.current = tui.NewChoice() // Go back to the main menu.
78 return m, m.current.Init()
79 }
80 }
81
82 // --- Custom Messages for Switching Views ---
83 case tui.Credentials:
84 cfg := &config.Config{
85 ServiceProvider: msg.Provider,
86 Name: msg.Name,
87 Email: msg.Email,
88 Password: msg.Password,
89 }
90 if err := config.SaveConfig(cfg); err != nil {
91 log.Printf("could not save config: %v", err)
92 return m, tea.Quit
93 }
94 m.config = cfg
95 m.current = tui.NewChoice()
96 cmds = append(cmds, m.current.Init())
97
98 case tui.GoToInboxMsg:
99 m.current = tui.NewStatus("Fetching emails...")
100 return m, tea.Batch(m.current.Init(), fetchEmails(m.config))
101
102 case tui.EmailsFetchedMsg:
103 m.emails = msg.Emails
104 m.inbox = tui.NewInbox(m.emails)
105 m.current = m.inbox
106 // Manually set the size of the new view.
107 m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
108 cmds = append(cmds, m.current.Init())
109
110 case tui.GoToSendMsg:
111 m.current = tui.NewComposer(m.config.Email)
112 m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
113 cmds = append(cmds, m.current.Init())
114
115 case tui.GoToSettingsMsg:
116 m.current = tui.NewLogin()
117 m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
118 cmds = append(cmds, m.current.Init())
119
120 case tui.ViewEmailMsg:
121 emailView := tui.NewEmailView(m.emails[msg.Index], m.width, m.height)
122 m.current = emailView
123 cmds = append(cmds, m.current.Init())
124
125 case tui.SendEmailMsg:
126 m.current = tui.NewStatus("Sending email...")
127 cmds = append(cmds, m.current.Init(), sendEmail(m.config, msg))
128
129 case tui.EmailResultMsg:
130 m.current = tui.NewChoice()
131 cmds = append(cmds, m.current.Init())
132 }
133
134 // Pass all other messages to the current view.
135 m.current, cmd = m.current.Update(msg)
136 cmds = append(cmds, cmd)
137
138 return m, tea.Batch(cmds...)
139}
140
141func (m *mainModel) View() string {
142 return m.current.View()
143}
144
145// markdownToHTML converts a Markdown string to an HTML string.
146func markdownToHTML(md []byte) []byte {
147 var buf bytes.Buffer
148 p := goldmark.New(
149 goldmark.WithRendererOptions(
150 html.WithUnsafe(), // Allow raw HTML in email.
151 ),
152 )
153 if err := p.Convert(md, &buf); err != nil {
154 return md // Fallback to original markdown.
155 }
156 return buf.Bytes()
157}
158
159// sendEmail finds local image paths, embeds them, and sends the email.
160func sendEmail(cfg *config.Config, msg tui.SendEmailMsg) tea.Cmd {
161 return func() tea.Msg {
162 recipients := []string{msg.To}
163 body := msg.Body
164 images := make(map[string][]byte)
165
166 // Find all markdown image tags.
167 re := regexp.MustCompile(`!\[.*?\]\((.*?)\)`)
168 matches := re.FindAllStringSubmatch(body, -1)
169
170 for _, match := range matches {
171 imgPath := match[1]
172 imgData, err := os.ReadFile(imgPath) // Use os.ReadFile.
173 if err != nil {
174 log.Printf("Could not read image file %s: %v", imgPath, err)
175 continue
176 }
177
178 // Create a unique CID that includes the file extension.
179 cid := fmt.Sprintf("%s%s@%s", uuid.NewString(), filepath.Ext(imgPath), "email-cli")
180 images[cid] = []byte(base64.StdEncoding.EncodeToString(imgData))
181 body = strings.Replace(body, imgPath, "cid:"+cid, 1)
182 }
183
184 htmlBody := markdownToHTML([]byte(body))
185
186 err := sender.SendEmail(cfg, recipients, msg.Subject, msg.Body, string(htmlBody), images)
187 if err != nil {
188 log.Printf("Failed to send email: %v", err)
189 return tui.EmailResultMsg{Err: err}
190 }
191 time.Sleep(1 * time.Second)
192 return tui.EmailResultMsg{}
193 }
194}
195
196// fetchEmails retrieves emails in the background.
197func fetchEmails(cfg *config.Config) tea.Cmd {
198 return func() tea.Msg {
199 emails, err := fetcher.FetchEmails(cfg)
200 if err != nil {
201 return tui.FetchErr(err)
202 }
203 return tui.EmailsFetchedMsg{Emails: emails}
204 }
205}
206
207func main() {
208 cfg, err := config.LoadConfig()
209 var initialModel *mainModel
210 if err != nil {
211 initialModel = newInitialModel(nil)
212 } else {
213 initialModel = newInitialModel(cfg)
214 }
215
216 p := tea.NewProgram(initialModel, tea.WithAltScreen())
217
218 if _, err := p.Run(); err != nil {
219 fmt.Printf("Alas, there's been an error: %v", err)
220 os.Exit(1)
221 }
222}