Detailed changes
@@ -1,122 +1,211 @@
package main
import (
- "fmt"
"log"
"os"
"time"
+ // These imports were missing, causing all the "undefined" errors.
"github.com/andrinoff/email-cli/config"
+ "github.com/andrinoff/email-cli/fetcher"
"github.com/andrinoff/email-cli/sender"
"github.com/andrinoff/email-cli/tui"
+
tea "github.com/charmbracelet/bubbletea"
)
-// mainModel now holds the state for the entire application.
-type mainModel struct {
- current tea.Model
- config *config.Config
- width int
- height int
- err error
+type modelState int
+
+const (
+ loginView modelState = iota
+ mainMenu
+ inboxView
+ emailView
+ composeView
+)
+
+// Model holds the application's state.
+type Model struct {
+ state modelState
+ login tea.Model
+ mainMenu tea.Model
+ inbox tea.Model
+ emailView tea.Model
+ composer tea.Model
+ fetcher *fetcher.Fetcher
+ sender *sender.Sender
+ emails []fetcher.Email
+ statusMessage string
+ err error
}
-// newInitialModel now returns a pointer, which is crucial for state management.
-func newInitialModel(cfg *config.Config) *mainModel {
- return &mainModel{
- current: tui.NewChoice(),
- config: cfg,
+// New creates the initial model.
+func New() Model {
+ // We start with the login view.
+ return Model{
+ state: loginView,
+ login: tui.NewLogin(),
+ mainMenu: tui.NewChoice(),
}
}
-func (m *mainModel) Init() tea.Cmd {
- return m.current.Init()
+// Init runs any initial commands.
+func (m Model) Init() tea.Cmd {
+ return tea.EnterAltScreen
}
-func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+// Update handles all messages and updates the model.
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
var cmds []tea.Cmd
+ // Global messages that can be handled regardless of state.
switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- m.width = msg.Width
- m.height = msg.Height
- // Pass the window size message to the current view
- m.current, cmd = m.current.Update(msg)
- cmds = append(cmds, cmd)
- return m, tea.Batch(cmds...)
-
case tea.KeyMsg:
if msg.String() == "ctrl+c" {
return m, tea.Quit
}
- // Allow ESC to go back to the main menu
- if msg.String() == "esc" {
- if _, ok := m.current.(*tui.Choice); !ok {
- m.current = tui.NewChoice()
- return m, m.current.Init()
+ case tui.EmailResultMsg:
+ if msg.Err != nil {
+ m.statusMessage = "❌ Error: " + msg.Err.Error()
+ } else {
+ m.statusMessage = "✅ Email sent successfully!"
+ }
+ m.state = mainMenu
+ return m, tea.Tick(3*time.Second, func(t time.Time) tea.Msg {
+ return tui.ClearStatusMsg{}
+ })
+ case tui.ClearStatusMsg:
+ m.statusMessage = ""
+ return m, nil
+ }
+
+ // State-specific message handling.
+ switch m.state {
+ case loginView:
+ // When the user submits their credentials...
+ if msg, ok := msg.(tui.Credentials); ok {
+ // For now, we'll default to Gmail.
+ service := "gmail"
+ cfg := &config.Config{Email: msg.Email, Password: msg.Password, Service: service}
+ config.Save(cfg)
+ m.fetcher = fetcher.New(cfg)
+ m.sender = sender.New(msg.Email, msg.Password, service)
+ m.state = mainMenu
+ return m, nil
+ }
+
+ case mainMenu:
+ // Handle user's choice from the main menu.
+ if msg, ok := msg.(tea.KeyMsg); ok {
+ switch msg.String() {
+ case "1", "j", "down": // View Inbox
+ m.state = inboxView
+ m.inbox = tui.NewInbox(m.emails)
+ return m, func() tea.Msg {
+ emails, err := m.fetcher.FetchEmails("INBOX")
+ if err != nil {
+ return tui.FetchErr(err)
+ }
+ return tui.EmailsFetchedMsg{Emails: emails}
+ }
+ case "2", "k", "up": // Compose Email
+ m.state = composeView
+ m.composer = tui.NewComposer(m.fetcher.Config.Email)
+ return m, nil
+ }
+ }
+
+ case inboxView:
+ switch msg := msg.(type) {
+ case tui.EmailsFetchedMsg:
+ m.emails = msg.Emails
+ m.inbox = tui.NewInbox(m.emails)
+ return m, nil
+ case tui.ViewEmailMsg:
+ m.state = emailView
+ m.emailView = tui.NewEmailView(m.emails[msg.Index])
+ return m, nil
+ case tea.KeyMsg:
+ if msg.String() == "esc" {
+ m.state = mainMenu
}
}
- // --- Custom Messages for switching views ---
- case tui.GoToInboxMsg:
- m.current = tui.NewInbox()
- // Manually set the size of the new view
- m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
- cmds = append(cmds, m.current.Init())
-
- case tui.GoToSendMsg:
- m.current = tui.NewComposer(m.config.Email)
- m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
- cmds = append(cmds, m.current.Init())
-
- case tui.ViewEmailMsg:
- m.current = tui.NewEmailView(msg.Email, m.width, m.height)
- cmds = append(cmds, m.current.Init())
-
- case tui.SendEmailMsg:
- m.current = tui.NewStatus("Sending email...")
- cmds = append(cmds, m.current.Init(), sendEmail(m.config, msg))
-
- case tui.EmailSentMsg:
- m.current = tui.NewChoice()
- cmds = append(cmds, m.current.Init())
+ case emailView:
+ if msg, ok := msg.(tea.KeyMsg); ok && msg.String() == "esc" {
+ m.state = inboxView // Go back to the inbox list.
+ }
+
+ case composeView:
+ switch msg := msg.(type) {
+ case tui.SendEmailMsg:
+ m.statusMessage = "⏳ Sending email..."
+ return m, func() tea.Msg {
+ err := m.sender.Send(msg.To, msg.Subject, msg.Body)
+ return tui.EmailResultMsg{Err: err}
+ }
+ case tea.KeyMsg:
+ if msg.String() == "esc" {
+ m.state = mainMenu
+ }
+ }
}
- // Pass all other messages to the current view
- m.current, cmd = m.current.Update(msg)
+ // Pass the message to the current view's Update function.
+ switch m.state {
+ case loginView:
+ m.login, cmd = m.login.Update(msg)
+ case mainMenu:
+ m.mainMenu, cmd = m.mainMenu.Update(msg)
+ case inboxView:
+ m.inbox, cmd = m.inbox.Update(msg)
+ case emailView:
+ m.emailView, cmd = m.emailView.Update(msg)
+ case composeView:
+ m.composer, cmd = m.composer.Update(msg)
+ }
cmds = append(cmds, cmd)
-
return m, tea.Batch(cmds...)
}
-func (m *mainModel) View() string {
- return m.current.View()
-}
+// View renders the current UI.
+func (m Model) View() string {
+ var s string
+ switch m.state {
+ case loginView:
+ s = m.login.View()
+ case mainMenu:
+ s = m.mainMenu.View()
+ case inboxView:
+ s = m.inbox.View()
+ case emailView:
+ s = m.emailView.View()
+ case composeView:
+ s = m.composer.View()
+ default:
+ s = "Unknown state."
+ }
-// sendEmail is a command that sends an email in the background.
-func sendEmail(cfg *config.Config, msg tui.SendEmailMsg) tea.Cmd {
- return func() tea.Msg {
- recipients := []string{msg.To}
- err := sender.SendEmail(cfg, recipients, msg.Subject, msg.Body)
- if err != nil {
- log.Printf("Failed to send email: %v", err) // Log error
- }
- time.Sleep(1 * time.Second) // Give user time to see the "Sending" message
- return tui.EmailSentMsg{}
+ if m.statusMessage != "" {
+ return s + "\n\n" + m.statusMessage
}
+ return s
}
func main() {
- cfg, err := config.LoadConfig()
- if err != nil {
- log.Fatalf("could not load config: %v", err)
+ // Load config first to see if we can skip login.
+ cfg := config.Load()
+ m := New()
+ if cfg.Email != "" && cfg.Password != "" && cfg.Service != "" {
+ m.fetcher = fetcher.New(cfg)
+ m.sender = sender.New(cfg.Email, cfg.Password, cfg.Service)
+ m.state = mainMenu
}
- p := tea.NewProgram(newInitialModel(cfg), tea.WithAltScreen())
-
+ p := tea.NewProgram(m)
if _, err := p.Run(); err != nil {
- fmt.Printf("Alas, there's been an error: %v", err)
+ log.Fatal(err)
os.Exit(1)
}
}
@@ -1,57 +1,70 @@
package tui
import (
- "github.com/charmbracelet/bubbles/list"
+ "fmt"
+
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
)
-type Choice struct {
- list list.Model
-}
-
-func NewChoice() *Choice {
- items := []list.Item{
- choiceItem("View Inbox"),
- choiceItem("Send Email"),
- }
+var (
+ titleStyle = lipgloss.NewStyle().
+ Bold(true).
+ Foreground(lipgloss.Color("#FAFAFA")).
+ Background(lipgloss.Color("#7D56F4")).
+ PaddingLeft(2).
+ PaddingRight(2)
- l := list.New(items, list.NewDefaultDelegate(), 0, 0)
- l.Title = "What would you like to do?"
- l.SetShowStatusBar(false)
- l.SetFilteringEnabled(false)
+ choiceStyle = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("205")).
+ PaddingLeft(2)
+)
- return &Choice{list: l}
+type Choice struct {
+ cursor int
}
-type choiceItem string
-
-func (i choiceItem) FilterValue() string { return "" }
-func (i choiceItem) Title() string { return string(i) }
-func (i choiceItem) Description() string { return "" }
+func NewChoice() Choice {
+ return Choice{cursor: 0}
+}
-func (m *Choice) Init() tea.Cmd {
+func (m Choice) Init() tea.Cmd {
return nil
}
-func (m *Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmd tea.Cmd
+func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
- case tea.WindowSizeMsg:
- m.list.SetSize(msg.Width, msg.Height)
case tea.KeyMsg:
- if msg.String() == "enter" {
- switch m.list.SelectedItem().(choiceItem) {
- case "View Inbox":
- return m, func() tea.Msg { return GoToInboxMsg{} }
- case "Send Email":
- return m, func() tea.Msg { return GoToSendMsg{} }
+ switch msg.String() {
+ case "up", "k":
+ if m.cursor > 0 {
+ m.cursor--
+ }
+ case "down", "j":
+ if m.cursor < 1 {
+ m.cursor++
}
}
}
- m.list, cmd = m.list.Update(msg)
- return m, cmd
+ return m, nil
}
-func (m *Choice) View() string {
- return DocStyle.Render(m.list.View())
+func (m Choice) View() string {
+ s := "What would you like to do?\n\n"
+
+ choices := []string{"View Inbox", "Compose Email"}
+
+ for i, choice := range choices {
+ cursor := " "
+ if m.cursor == i {
+ cursor = ">"
+ s += choiceStyle.Render(fmt.Sprintf("%s %s\n", cursor, choice))
+ } else {
+ s += fmt.Sprintf("%s %s\n", cursor, choice)
+ }
+ }
+
+ s += "\nPress q to quit.\n"
+
+ return s
}
@@ -2,115 +2,103 @@ package tui
import (
"fmt"
+ "io"
- "github.com/andrinoff/email-cli/config"
"github.com/andrinoff/email-cli/fetcher"
"github.com/charmbracelet/bubbles/list"
- "github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
-// inboxItem wraps fetcher.Email to satisfy the list.Item interface.
-type inboxItem struct {
- fetcher.Email
+var (
+ itemStyle = lipgloss.NewStyle().PaddingLeft(4)
+ selectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("205"))
+ paginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
+ inboxHelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1)
+)
+
+type item struct {
+ title, desc string
}
-func (i inboxItem) Title() string { return i.Subject }
-func (i inboxItem) Description() string { return fmt.Sprintf("From: %s", i.From) }
-func (i inboxItem) FilterValue() string { return i.Subject }
+func (i item) Title() string { return i.title }
+func (i item) Description() string { return i.desc }
+func (i item) FilterValue() string { return i.title + " " + i.desc }
-type Inbox struct {
- list list.Model
- spinner spinner.Model
- loading bool
- err error
-}
+type itemDelegate struct{}
-func NewInbox() *Inbox {
- s := spinner.New()
- s.Spinner = spinner.Dot
- s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
+func (d itemDelegate) Height() int { return 1 }
+func (d itemDelegate) Spacing() int { return 0 }
+func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
+func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
+ i, ok := listItem.(item)
+ if !ok {
+ return
+ }
- // Create an empty list for now. It will be populated later.
- l := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0)
- l.Title = "Inbox"
+ str := fmt.Sprintf("%d. %s", index+1, i.title)
- return &Inbox{
- spinner: s,
- list: l,
- loading: true,
+ // **FIX**: Corrected the function literal to accept a variadic string
+ fn := itemStyle.Render
+ if index == m.Index() {
+ fn = func(s ...string) string {
+ return selectedItemStyle.Render("> " + s[0])
+ }
}
+
+ fmt.Fprint(w, fn(str))
}
-func (m *Inbox) Init() tea.Cmd {
- return tea.Batch(m.spinner.Tick, fetchEmails)
+type Inbox struct {
+ list list.Model
}
-func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- var cmd tea.Cmd
- switch msg := msg.(type) {
- case spinner.TickMsg:
- if m.loading {
- m.spinner, cmd = m.spinner.Update(msg)
+func NewInbox(emails []fetcher.Email) Inbox {
+ items := make([]list.Item, len(emails))
+ for i, email := range emails {
+ items[i] = item{
+ title: email.Subject,
+ desc: email.From,
}
- return m, cmd
+ }
- case emailsFetchedMsg:
- m.loading = false
- items := make([]list.Item, len(msg))
- for i, email := range msg {
- items[i] = inboxItem{email}
- }
- m.list.SetItems(items)
- return m, nil
+ l := list.New(items, itemDelegate{}, 20, 14)
+ l.Title = "Inbox"
+ l.SetShowStatusBar(false)
+ l.SetFilteringEnabled(false)
+ l.Styles.Title = lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Bold(true)
+ l.Styles.PaginationStyle = paginationStyle
+ l.Styles.HelpStyle = inboxHelpStyle
- case errMsg:
- m.err = msg.err
- return m, tea.Quit
+ return Inbox{list: l}
+}
+func (m Inbox) Init() tea.Cmd {
+ return nil
+}
+
+func (m Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
case tea.KeyMsg:
- if msg.String() == "enter" && !m.loading {
- selected := m.list.SelectedItem().(inboxItem)
- return m, func() tea.Msg {
- return ViewEmailMsg{Email: selected.Email}
+ if msg.String() == "enter" {
+ i, ok := m.list.SelectedItem().(item)
+ if ok {
+ _ = i
+ return m, func() tea.Msg {
+ return ViewEmailMsg{Index: m.list.Index()}
+ }
}
}
-
case tea.WindowSizeMsg:
- // When the window size changes, update the list's dimensions.
- m.list.SetSize(msg.Width, msg.Height)
+ m.list.SetWidth(msg.Width)
+ return m, nil
}
- if !m.loading {
- m.list, cmd = m.list.Update(msg)
- }
+ var cmd tea.Cmd
+ m.list, cmd = m.list.Update(msg)
return m, cmd
}
-func (m *Inbox) View() string {
- if m.err != nil {
- return fmt.Sprintf("Error: %v", m.err)
- }
- if m.loading {
- return fmt.Sprintf("\n\n %s Fetching emails...\n\n", m.spinner.View())
- }
- return DocStyle.Render(m.list.View())
-}
-
-// --- Bubble Tea Commands and Messages ---
-
-type emailsFetchedMsg []fetcher.Email
-type errMsg struct{ err error }
-
-func fetchEmails() tea.Msg {
- cfg, err := config.LoadConfig()
- if err != nil {
- return errMsg{err}
- }
- emails, err := fetcher.FetchEmails(cfg)
- if err != nil {
- return errMsg{err}
- }
- return emailsFetchedMsg(emails)
+func (m Inbox) View() string {
+ return "\n" + m.list.View()
}
@@ -1,54 +1,38 @@
package tui
import (
- "strings"
-
- "github.com/andrinoff/email-cli/config"
- "github.com/charmbracelet/bubbles/cursor"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
-type loginModel struct {
+// Login holds the state for the login form.
+type Login struct {
focusIndex int
inputs []textinput.Model
- cursorMode cursor.Mode
- config *config.Config
- err error
}
-func initialLoginModel() loginModel {
- m := loginModel{
- inputs: make([]textinput.Model, 4),
+// NewLogin creates a new login model. This function was missing.
+func NewLogin() tea.Model {
+ m := Login{
+ inputs: make([]textinput.Model, 2),
}
var t textinput.Model
for i := range m.inputs {
t = textinput.New()
- t.Cursor.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
- t.CharLimit = 32
+ t.Cursor.Style = focusedStyle
+ t.CharLimit = 64
switch i {
case 0:
- t.Placeholder = "gmail or icloud"
+ t.Placeholder = "Email"
t.Focus()
- t.Prompt = "> "
- t.CharLimit = 10
+ t.Prompt = "✉️ > "
case 1:
- t.Placeholder = "your@email.com"
- t.Prompt = " "
- t.CharLimit = 64
- case 2:
- t.Placeholder = "App-specific password"
- t.Prompt = " "
+ t.Placeholder = "Password"
t.EchoMode = textinput.EchoPassword
- t.EchoCharacter = '•'
- t.CharLimit = 64
- case 3:
- t.Placeholder = "Name"
- t.Prompt = " "
- t.CharLimit = 64
+ t.Prompt = "🔑 > "
}
m.inputs[i] = t
}
@@ -56,92 +40,69 @@ func initialLoginModel() loginModel {
return m
}
-func (m loginModel) Init() tea.Cmd { return textinput.Blink }
+// Init initializes the login model.
+func (m Login) Init() tea.Cmd {
+ return textinput.Blink
+}
-func (m loginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+// Update handles messages for the login model.
+func (m Login) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
- switch msg.String() {
- case "ctrl+c", "esc":
- return m, tea.Quit
- case "tab", "shift+tab", "enter", "up", "down":
- s := msg.String()
- if s == "enter" && m.focusIndex == len(m.inputs) {
- m.config = &config.Config{
- ServiceProvider: m.inputs[0].Value(),
- Email: m.inputs[1].Value(),
- Password: m.inputs[2].Value(),
- Name: m.inputs[3].Value(),
- }
- if err := config.SaveConfig(m.config); err != nil {
- m.err = err
- return m, nil
+ switch msg.Type {
+ // On Enter, if we are on the last field, submit the credentials.
+ case tea.KeyEnter:
+ if m.focusIndex == len(m.inputs)-1 {
+ return m, func() tea.Msg {
+ return Credentials{
+ Email: m.inputs[0].Value(),
+ Password: m.inputs[1].Value(),
+ }
}
- return m, tea.Quit
}
+ fallthrough
+ // Cycle focus between inputs.
+ case tea.KeyTab, tea.KeyShiftTab, tea.KeyUp, tea.KeyDown:
+ s := msg.String()
if s == "up" || s == "shift+tab" {
m.focusIndex--
} else {
m.focusIndex++
}
- if m.focusIndex > len(m.inputs) {
+
+ if m.focusIndex >= len(m.inputs) {
m.focusIndex = 0
} else if m.focusIndex < 0 {
- m.focusIndex = len(m.inputs)
+ m.focusIndex = len(m.inputs) - 1
}
+
cmds := make([]tea.Cmd, len(m.inputs))
for i := 0; i < len(m.inputs); i++ {
if i == m.focusIndex {
cmds[i] = m.inputs[i].Focus()
- m.inputs[i].Prompt = "> "
- continue
+ } else {
+ m.inputs[i].Blur()
}
- m.inputs[i].Blur()
- m.inputs[i].Prompt = " "
}
return m, tea.Batch(cmds...)
}
}
- cmd := m.updateInputs(msg)
- return m, cmd
-}
-func (m *loginModel) updateInputs(msg tea.Msg) tea.Cmd {
- cmds := make([]tea.Cmd, len(m.inputs))
+ // Update the focused input field.
+ var cmds = make([]tea.Cmd, len(m.inputs))
for i := range m.inputs {
m.inputs[i], cmds[i] = m.inputs[i].Update(msg)
}
- return tea.Batch(cmds...)
+ return m, tea.Batch(cmds...)
}
-func (m loginModel) View() string {
- var b strings.Builder
- b.WriteString(InfoStyle.Render(" Welcome to email-cli! Please configure your email account. ") + "\n\n")
- for i := range m.inputs {
- b.WriteString(m.inputs[i].View())
- if i < len(m.inputs)-1 {
- b.WriteRune('\n')
- }
- }
- button := "\n\n" + "[ Submit ]"
- if m.focusIndex == len(m.inputs) {
- button = "\n\n" + lipgloss.NewStyle().Foreground(lipgloss.Color("205")).Render("[ Submit ]")
- }
- b.WriteString(button)
- if m.err != nil {
- b.WriteString("\n\nError: " + m.err.Error())
- }
- b.WriteString(HelpStyle.Render("\n\n(tab to navigate, enter to submit, esc to quit)"))
- return DialogBoxStyle.Render(b.String())
-}
-
-// RunLogin starts the login UI and returns the created config.
-func RunLogin() (*config.Config, error) {
- p := tea.NewProgram(initialLoginModel())
- m, err := p.Run()
- if err != nil {
- return nil, err
- }
- finalModel := m.(loginModel)
- return finalModel.config, nil
+// View renders the login form.
+func (m Login) View() string {
+ return lipgloss.JoinVertical(lipgloss.Left,
+ titleStyle.Render("Welcome to Email CLI"),
+ "Please enter your credentials.",
+ m.inputs[0].View(),
+ m.inputs[1].View(),
+ helpStyle.Render("\nenter: submit • tab: next field • esc: quit"),
+ )
}
@@ -2,23 +2,42 @@ package tui
import "github.com/andrinoff/email-cli/fetcher"
-// A message to tell the main model to switch to the inbox view.
-type GoToInboxMsg struct{}
-
-// A message to tell the main model to switch to the composer view.
-type GoToSendMsg struct{}
-
-// A message containing a fetched email to be viewed.
+// A message to view an email.
type ViewEmailMsg struct {
- Email fetcher.Email
+ Index int
}
-// A message with the content of an email to be sent.
+// A message to indicate that an email has been sent.
type SendEmailMsg struct {
To string
Subject string
Body string
}
-// A message to indicate that an email has been successfully sent.
-type EmailSentMsg struct{}
+// A message to indicate that the user has entered their credentials.
+type Credentials struct {
+ Email string
+ 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