main.go

  1package main
  2
  3import (
  4	"log"
  5	"os"
  6	"time"
  7
  8	// These imports were missing, causing all the "undefined" errors.
  9	"github.com/andrinoff/email-cli/config"
 10	"github.com/andrinoff/email-cli/fetcher"
 11	"github.com/andrinoff/email-cli/sender"
 12	"github.com/andrinoff/email-cli/tui"
 13
 14	tea "github.com/charmbracelet/bubbletea"
 15)
 16
 17type modelState int
 18
 19const (
 20	loginView modelState = iota
 21	mainMenu
 22	inboxView
 23	emailView
 24	composeView
 25)
 26
 27// Model holds the application's state.
 28type Model struct {
 29	state         modelState
 30	login         tea.Model
 31	mainMenu      tea.Model
 32	inbox         tea.Model
 33	emailView     tea.Model
 34	composer      tea.Model
 35	fetcher       *fetcher.Fetcher
 36	sender        *sender.Sender
 37	emails        []fetcher.Email
 38	statusMessage string
 39	err           error
 40}
 41
 42// New creates the initial model.
 43func New() Model {
 44	// We start with the login view.
 45	return Model{
 46		state:    loginView,
 47		login:    tui.NewLogin(),
 48		mainMenu: tui.NewChoice(),
 49	}
 50}
 51
 52// Init runs any initial commands.
 53func (m Model) Init() tea.Cmd {
 54	return tea.EnterAltScreen
 55}
 56
 57// Update handles all messages and updates the model.
 58func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 59	var cmd tea.Cmd
 60	var cmds []tea.Cmd
 61
 62	// Global messages that can be handled regardless of state.
 63	switch msg := msg.(type) {
 64	case tea.KeyMsg:
 65		if msg.String() == "ctrl+c" {
 66			return m, tea.Quit
 67		}
 68	case tui.EmailResultMsg:
 69		if msg.Err != nil {
 70			m.statusMessage = "❌ Error: " + msg.Err.Error()
 71		} else {
 72			m.statusMessage = "✅ Email sent successfully!"
 73		}
 74		m.state = mainMenu
 75		return m, tea.Tick(3*time.Second, func(t time.Time) tea.Msg {
 76			return tui.ClearStatusMsg{}
 77		})
 78	case tui.ClearStatusMsg:
 79		m.statusMessage = ""
 80		return m, nil
 81	}
 82
 83	// State-specific message handling.
 84	switch m.state {
 85	case loginView:
 86		// When the user submits their credentials...
 87		if msg, ok := msg.(tui.Credentials); ok {
 88			// For now, we'll default to Gmail.
 89			service := "gmail"
 90			cfg := &config.Config{Email: msg.Email, Password: msg.Password, Service: service}
 91			config.Save(cfg)
 92			m.fetcher = fetcher.New(cfg)
 93			m.sender = sender.New(msg.Email, msg.Password, service)
 94			m.state = mainMenu
 95			return m, nil
 96		}
 97
 98	case mainMenu:
 99		// Handle user's choice from the main menu.
100		if msg, ok := msg.(tea.KeyMsg); ok {
101			switch msg.String() {
102			case "1", "j", "down": // View Inbox
103				m.state = inboxView
104				m.inbox = tui.NewInbox(m.emails)
105				return m, func() tea.Msg {
106					emails, err := m.fetcher.FetchEmails("INBOX")
107					if err != nil {
108						return tui.FetchErr(err)
109					}
110					return tui.EmailsFetchedMsg{Emails: emails}
111				}
112			case "2", "k", "up": // Compose Email
113				m.state = composeView
114				m.composer = tui.NewComposer(m.fetcher.Config.Email)
115				return m, nil
116			}
117		}
118
119	case inboxView:
120		switch msg := msg.(type) {
121		case tui.EmailsFetchedMsg:
122			m.emails = msg.Emails
123			m.inbox = tui.NewInbox(m.emails)
124			return m, nil
125		case tui.ViewEmailMsg:
126			m.state = emailView
127			m.emailView = tui.NewEmailView(m.emails[msg.Index])
128			return m, nil
129		case tea.KeyMsg:
130			if msg.String() == "esc" {
131				m.state = mainMenu
132			}
133		}
134
135	case emailView:
136		if msg, ok := msg.(tea.KeyMsg); ok && msg.String() == "esc" {
137			m.state = inboxView // Go back to the inbox list.
138		}
139
140	case composeView:
141		switch msg := msg.(type) {
142		case tui.SendEmailMsg:
143			m.statusMessage = "⏳ Sending email..."
144			return m, func() tea.Msg {
145				err := m.sender.Send(msg.To, msg.Subject, msg.Body)
146				return tui.EmailResultMsg{Err: err}
147			}
148		case tea.KeyMsg:
149			if msg.String() == "esc" {
150				m.state = mainMenu
151			}
152		}
153	}
154
155	// Pass the message to the current view's Update function.
156	switch m.state {
157	case loginView:
158		m.login, cmd = m.login.Update(msg)
159	case mainMenu:
160		m.mainMenu, cmd = m.mainMenu.Update(msg)
161	case inboxView:
162		m.inbox, cmd = m.inbox.Update(msg)
163	case emailView:
164		m.emailView, cmd = m.emailView.Update(msg)
165	case composeView:
166		m.composer, cmd = m.composer.Update(msg)
167	}
168	cmds = append(cmds, cmd)
169	return m, tea.Batch(cmds...)
170}
171
172// View renders the current UI.
173func (m Model) View() string {
174	var s string
175	switch m.state {
176	case loginView:
177		s = m.login.View()
178	case mainMenu:
179		s = m.mainMenu.View()
180	case inboxView:
181		s = m.inbox.View()
182	case emailView:
183		s = m.emailView.View()
184	case composeView:
185		s = m.composer.View()
186	default:
187		s = "Unknown state."
188	}
189
190	if m.statusMessage != "" {
191		return s + "\n\n" + m.statusMessage
192	}
193	return s
194}
195
196func main() {
197	// Load config first to see if we can skip login.
198	cfg := config.Load()
199	m := New()
200	if cfg.Email != "" && cfg.Password != "" && cfg.Service != "" {
201		m.fetcher = fetcher.New(cfg)
202		m.sender = sender.New(cfg.Email, cfg.Password, cfg.Service)
203		m.state = mainMenu
204	}
205
206	p := tea.NewProgram(m)
207	if _, err := p.Run(); err != nil {
208		log.Fatal(err)
209		os.Exit(1)
210	}
211}