feat: version check and "matcha update" (#61)

Drew Smirnoff created

Change summary

config/config.go   |  9 +++++++++
fetcher/fetcher.go | 24 ++++++++++++++++++++++++
main.go            |  8 +++++++-
tui/login.go       | 11 +++++++++--
tui/messages.go    |  3 ++-
5 files changed, 51 insertions(+), 4 deletions(-)

Detailed changes

config/config.go 🔗

@@ -15,6 +15,9 @@ type Account struct {
 	Email           string `json:"email"`
 	Password        string `json:"password"`
 	ServiceProvider string `json:"service_provider"` // "gmail", "icloud", or "custom"
+	// FetchEmail is the single email address for which messages should be fetched.
+	// If empty, it will default to `Email` when accounts are added.
+	FetchEmail string `json:"fetch_email,omitempty"`
 
 	// Custom server settings (used when ServiceProvider is "custom")
 	IMAPServer string `json:"imap_server,omitempty"`
@@ -144,6 +147,8 @@ func LoadConfig() (*Config, error) {
 						Email:           legacyConfig.Email,
 						Password:        legacyConfig.Password,
 						ServiceProvider: legacyConfig.ServiceProvider,
+						// Default FetchEmail to the legacy Email value
+						FetchEmail: legacyConfig.Email,
 					},
 				},
 			}
@@ -171,6 +176,10 @@ func (c *Config) AddAccount(account Account) {
 	if account.ID == "" {
 		account.ID = uuid.New().String()
 	}
+	// Ensure FetchEmail defaults to the login Email if not explicitly set.
+	if account.FetchEmail == "" && account.Email != "" {
+		account.FetchEmail = account.Email
+	}
 	c.Accounts = append(c.Accounts, account)
 }
 

fetcher/fetcher.go 🔗

@@ -164,9 +164,33 @@ func FetchEmails(account *config.Account, limit, offset uint32) ([]Email, error)
 		}
 
 		var toAddrList []string
+		// Build recipient list from To and Cc for matching and display
 		for _, addr := range msg.Envelope.To {
 			toAddrList = append(toAddrList, addr.Address())
 		}
+		for _, addr := range msg.Envelope.Cc {
+			toAddrList = append(toAddrList, addr.Address())
+		}
+
+		// Determine which email to filter on: prefer Account.FetchEmail, fallback to Account.Email
+		fetchEmail := strings.ToLower(strings.TrimSpace(account.FetchEmail))
+		if fetchEmail == "" {
+			fetchEmail = strings.ToLower(strings.TrimSpace(account.Email))
+		}
+
+		// Check if any recipient matches the fetchEmail
+		matched := false
+		for _, r := range toAddrList {
+			if strings.EqualFold(strings.TrimSpace(r), fetchEmail) {
+				matched = true
+				break
+			}
+		}
+
+		if !matched {
+			// Skip messages not addressed to the configured fetch email
+			continue
+		}
 
 		emails = append(emails, Email{
 			UID:       msg.Uid,

main.go 🔗

@@ -140,9 +140,10 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		account := config.Account{
 			ID:              uuid.New().String(),
 			Name:            msg.Name,
-			Email:           msg.Email,
+			Email:           msg.Host, // login/email used for authentication comes from Host field in the form
 			Password:        msg.Password,
 			ServiceProvider: msg.Provider,
+			FetchEmail:      msg.FetchEmail,
 		}
 
 		if msg.Provider == "custom" {
@@ -152,6 +153,11 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			account.SMTPPort = msg.SMTPPort
 		}
 
+		// Ensure FetchEmail defaults to the login Email (Host) if not explicitly set
+		if account.FetchEmail == "" && account.Email != "" {
+			account.FetchEmail = account.Email
+		}
+
 		if m.config == nil {
 			m.config = &config.Config{}
 		}

tui/login.go 🔗

@@ -23,6 +23,7 @@ const (
 	inputProvider = iota
 	inputName
 	inputEmail
+	inputFetchEmail
 	inputPassword
 	inputIMAPServer
 	inputIMAPPort
@@ -52,6 +53,9 @@ func NewLogin() *Login {
 			t.Placeholder = "Display Name"
 			t.Prompt = "👤 > "
 		case inputEmail:
+			t.Placeholder = "Host"
+			t.Prompt = "🏠 > "
+		case inputFetchEmail:
 			t.Placeholder = "Email Address"
 			t.Prompt = "✉️ > "
 		case inputPassword:
@@ -130,7 +134,8 @@ func (m *Login) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					return Credentials{
 						Provider:   m.inputs[inputProvider].Value(),
 						Name:       m.inputs[inputName].Value(),
-						Email:      m.inputs[inputEmail].Value(),
+						Host:       m.inputs[inputEmail].Value(),
+						FetchEmail: m.inputs[inputFetchEmail].Value(),
 						Password:   m.inputs[inputPassword].Value(),
 						IMAPServer: m.inputs[inputIMAPServer].Value(),
 						IMAPPort:   imapPort,
@@ -218,6 +223,7 @@ func (m *Login) View() string {
 		m.inputs[inputProvider].View(),
 		m.inputs[inputName].View(),
 		m.inputs[inputEmail].View(),
+		m.inputs[inputFetchEmail].View(),
 		m.inputs[inputPassword].View(),
 	}
 
@@ -238,12 +244,13 @@ func (m *Login) View() string {
 }
 
 // SetEditMode sets the login form to edit an existing account.
-func (m *Login) SetEditMode(accountID, provider, name, email, imapServer string, imapPort int, smtpServer string, smtpPort int) {
+func (m *Login) SetEditMode(accountID, provider, name, email, fetchEmail, imapServer string, imapPort int, smtpServer string, smtpPort int) {
 	m.isEditMode = true
 	m.accountID = accountID
 	m.inputs[inputProvider].SetValue(provider)
 	m.inputs[inputName].SetValue(name)
 	m.inputs[inputEmail].SetValue(email)
+	m.inputs[inputFetchEmail].SetValue(fetchEmail)
 	m.showCustom = provider == "custom"
 
 	if m.showCustom {

tui/messages.go 🔗

@@ -24,7 +24,8 @@ type SendEmailMsg struct {
 type Credentials struct {
 	Provider   string
 	Name       string
-	Email      string
+	Host       string // Host (this was the previous \"Email Address\" field in the UI)
+	FetchEmail string // Single email address to fetch messages for. If empty, code should default this to Host when creating the account.
 	Password   string
 	IMAPServer string
 	IMAPPort   int