fix: add catch-all support (#1208)

Drew Smirnoff , Steve Evans , and Andriy Chernov created

## What?


Ports catch-all support from GitLab

## Why?

Requested in #1201 and #1207

Co-authored-by: Steve Evans <steve@floatpane.com>
Co-authored-by: Andriy Chernov <andriy@floatpane.com>

Change summary

config/cache.go          |  1 
config/config.go         | 10 ++++++
config/config_test.go    | 11 ++++++
fetcher/fetcher.go       | 36 +++++++++++++---------
main.go                  | 11 ++++++
tui/composer.go          | 67 ++++++++++++++++++++++++++++++++++++++---
tui/inbox.go             | 31 +++++++++++++-----
tui/login.go             | 24 ++++++++++++--
tui/messages.go          |  3 +
tui/settings_accounts.go |  4 ++
10 files changed, 163 insertions(+), 35 deletions(-)

Detailed changes

config/cache.go 🔗

@@ -260,6 +260,7 @@ type Draft struct {
 	Body            string    `json:"body"`
 	AttachmentPaths []string  `json:"attachment_paths,omitempty"`
 	AccountID       string    `json:"account_id"`
+	FromOverride    string    `json:"from_override,omitempty"`
 	InReplyTo       string    `json:"in_reply_to,omitempty"`
 	References      []string  `json:"references,omitempty"`
 	QuotedText      string    `json:"quoted_text,omitempty"`

config/config.go 🔗

@@ -42,6 +42,9 @@ type Account struct {
 	// SendAsEmail controls the visible From header on outgoing mail.
 	// If empty, it defaults to FetchEmail, then Email.
 	SendAsEmail string `json:"send_as_email,omitempty"`
+	// CatchAll skips per-address filtering so all inbox messages are shown,
+	// regardless of which address they were delivered to.
+	CatchAll bool `json:"catch_all,omitempty"`
 
 	// Custom server settings (used when ServiceProvider is "custom")
 	IMAPServer string `json:"imap_server,omitempty"`
@@ -243,6 +246,9 @@ func (a *Account) GetSendAsEmail() string {
 // FormatFromHeader returns the display-ready From header value.
 func (a *Account) FormatFromHeader() string {
 	sendAs := a.GetSendAsEmail()
+	if strings.Contains(sendAs, "<") && strings.Contains(sendAs, ">") {
+		return sendAs
+	}
 	if a.Name != "" && sendAs != "" {
 		return fmt.Sprintf("%s <%s>", a.Name, sendAs)
 	}
@@ -373,6 +379,7 @@ type secureDiskAccount struct {
 	JMAPEndpoint       string `json:"jmap_endpoint,omitempty"`
 	POP3Server         string `json:"pop3_server,omitempty"`
 	POP3Port           int    `json:"pop3_port,omitempty"`
+	CatchAll           bool   `json:"catch_all,omitempty"`
 }
 
 type secureDiskConfig struct {
@@ -456,6 +463,7 @@ func SaveConfig(config *Config) error {
 				JMAPEndpoint:       acc.JMAPEndpoint,
 				POP3Server:         acc.POP3Server,
 				POP3Port:           acc.POP3Port,
+				CatchAll:           acc.CatchAll,
 			})
 		}
 		data, err = json.MarshalIndent(sdc, "", "  ")
@@ -517,6 +525,7 @@ func LoadConfig() (*Config, error) {
 		JMAPEndpoint       string `json:"jmap_endpoint,omitempty"`
 		POP3Server         string `json:"pop3_server,omitempty"`
 		POP3Port           int    `json:"pop3_port,omitempty"`
+		CatchAll           bool   `json:"catch_all,omitempty"`
 	}
 	type diskConfig struct {
 		Accounts             []rawAccount  `json:"accounts"`
@@ -588,6 +597,7 @@ func LoadConfig() (*Config, error) {
 			JMAPEndpoint:       rawAcc.JMAPEndpoint,
 			POP3Server:         rawAcc.POP3Server,
 			POP3Port:           rawAcc.POP3Port,
+			CatchAll:           rawAcc.CatchAll,
 		}
 
 		// Validate PGPKeySource

config/config_test.go 🔗

@@ -41,6 +41,7 @@ func TestSaveAndLoadConfig(t *testing.T) {
 				IMAPPort:        993,
 				SMTPServer:      "smtp.custom.com",
 				SMTPPort:        587,
+				CatchAll:        true,
 			},
 		},
 	}
@@ -312,6 +313,16 @@ func TestAccountSendIdentityHelpers(t *testing.T) {
 			t.Fatalf("GetSendAsEmail() = %q, want %q", got, "login@gmail.com")
 		}
 	})
+
+	t.Run("format from header avoids double wrapping", func(t *testing.T) {
+		account := Account{
+			Name:        "Account Name",
+			SendAsEmail: "Custom Name <custom@example.com>",
+		}
+		if got := account.FormatFromHeader(); got != "Custom Name <custom@example.com>" {
+			t.Fatalf("FormatFromHeader() = %q, want %q", got, "Custom Name <custom@example.com>")
+		}
+	})
 }
 
 func TestTranslateDateFormat(t *testing.T) {

fetcher/fetcher.go 🔗

@@ -498,7 +498,9 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u
 			}
 
 			matched := false
-			if isSentMailbox {
+			if account.CatchAll {
+				matched = true
+			} else if isSentMailbox {
 				var senderEmail string
 				if len(msg.Envelope.From) > 0 {
 					senderEmail = msg.Envelope.From[0].Addr()
@@ -1518,23 +1520,27 @@ func FetchArchiveEmails(account *config.Account, limit, offset uint32) ([]Email,
 
 		// For archive/All Mail, match emails where user is sender OR recipient
 		matched := false
-		// Check if user is the sender
-		if addressMatches(fromAddr, fetchEmail, account) {
+		if account.CatchAll {
 			matched = true
-		}
-		// Check if user is a recipient
-		if !matched {
-			for _, r := range toAddrList {
-				if addressMatches(r, fetchEmail, account) {
-					matched = true
-					break
+		} else {
+			// Check if user is the sender
+			if addressMatches(fromAddr, fetchEmail, account) {
+				matched = true
+			}
+			// Check if user is a recipient
+			if !matched {
+				for _, r := range toAddrList {
+					if addressMatches(r, fetchEmail, account) {
+						matched = true
+						break
+					}
 				}
 			}
-		}
-		// Check delivery headers for auto-forwarded emails
-		if !matched {
-			headerData := msg.FindBodySection(deliveryHeaderSection)
-			matched = deliveryHeadersMatch(headerData, fetchEmail, account)
+			// Check delivery headers for auto-forwarded emails
+			if !matched {
+				headerData := msg.FindBodySection(deliveryHeaderSection)
+				matched = deliveryHeadersMatch(headerData, fetchEmail, account)
+			}
 		}
 
 		if !matched {

main.go 🔗

@@ -345,6 +345,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				ServiceProvider: msg.Provider,
 				FetchEmail:      fetchEmails[0],
 				SendAsEmail:     msg.SendAsEmail,
+				CatchAll:        msg.CatchAll,
 				AuthMethod:      msg.AuthMethod,
 				Protocol:        msg.Protocol,
 				Insecure:        msg.Insecure,
@@ -389,6 +390,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					ServiceProvider: msg.Provider,
 					FetchEmail:      fe,
 					SendAsEmail:     msg.SendAsEmail,
+					CatchAll:        msg.CatchAll,
 					AuthMethod:      msg.AuthMethod,
 					Protocol:        msg.Protocol,
 					JMAPEndpoint:    msg.JMAPEndpoint,
@@ -1066,7 +1068,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			hideTips = m.config.HideTips
 		}
 		login := tui.NewLogin(hideTips)
-		login.SetEditMode(msg.AccountID, msg.Protocol, msg.Provider, msg.Name, msg.Email, msg.FetchEmail, msg.SendAsEmail, msg.IMAPServer, msg.IMAPPort, msg.SMTPServer, msg.SMTPPort, msg.Insecure, msg.JMAPEndpoint, msg.POP3Server, msg.POP3Port)
+		login.SetEditMode(msg.AccountID, msg.Protocol, msg.Provider, msg.Name, msg.Email, msg.FetchEmail, msg.SendAsEmail, msg.IMAPServer, msg.IMAPPort, msg.SMTPServer, msg.SMTPPort, msg.Insecure, msg.JMAPEndpoint, msg.POP3Server, msg.POP3Port, msg.CatchAll)
 		m.current = login
 		m.current, _ = m.current.Update(tea.WindowSizeMsg{Width: m.width, Height: m.height})
 		return m, m.current.Init()
@@ -2409,6 +2411,13 @@ func sendEmail(account *config.Account, msg tui.SendEmailMsg) tea.Cmd {
 			return tui.EmailResultMsg{Err: fmt.Errorf("no account configured")}
 		}
 
+		// Apply custom From address for catch-all accounts.
+		if msg.FromOverride != "" {
+			acc := *account
+			acc.SendAsEmail = msg.FromOverride
+			account = &acc
+		}
+
 		recipients := splitEmails(msg.To)
 		cc := splitEmails(msg.Cc)
 		bcc := splitEmails(msg.Bcc)

tui/composer.go 🔗

@@ -64,6 +64,7 @@ type Composer struct {
 	accounts           []config.Account
 	selectedAccountIdx int
 	showAccountPicker  bool
+	fromInput          textinput.Model // editable From when account is catch-all
 
 	// Contact suggestions
 	suggestions        []config.Contact
@@ -141,6 +142,12 @@ func NewComposer(from, to, subject, body string, hideTips bool) *Composer {
 	m.signatureInput.SetStyles(taStyles)
 	m.updateSignature()
 
+	m.fromInput = textinput.New()
+	m.fromInput.Placeholder = t("composer.from_placeholder")
+	m.fromInput.Prompt = "> "
+	m.fromInput.CharLimit = 256
+	m.fromInput.SetStyles(tiStyles)
+
 	// Start focus on To field (From is selectable but not a text input)
 	m.focusIndex = focusTo
 	m.toInput.Focus()
@@ -151,10 +158,17 @@ func NewComposer(from, to, subject, body string, hideTips bool) *Composer {
 // updateSignature updates the signature input based on the current selected account.
 func (m *Composer) updateSignature() {
 	if len(m.accounts) > 0 && m.selectedAccountIdx < len(m.accounts) {
-		if sig, err := config.LoadSignatureForAccount(&m.accounts[m.selectedAccountIdx]); err == nil && sig != "" {
+		acc := &m.accounts[m.selectedAccountIdx]
+		if sig, err := config.LoadSignatureForAccount(acc); err == nil && sig != "" {
 			m.signatureInput.SetValue(sig)
-			return
+		} else if sig, err := config.LoadSignature(); err == nil && sig != "" {
+			m.signatureInput.SetValue(sig)
+		} else {
+			m.signatureInput.SetValue("")
 		}
+		// Seed the editable From address for catch-all accounts.
+		m.fromInput.SetValue(acc.FormatFromHeader())
+		return
 	}
 
 	if sig, err := config.LoadSignature(); err == nil && sig != "" {
@@ -197,6 +211,13 @@ func (m *Composer) getFromAddress() string {
 	return ""
 }
 
+func (m *Composer) isCatchAllAccount() bool {
+	if len(m.accounts) > 0 && m.selectedAccountIdx < len(m.accounts) {
+		return m.accounts[m.selectedAccountIdx].CatchAll
+	}
+	return false
+}
+
 func (m *Composer) getSelectedAccount() *config.Account {
 	if len(m.accounts) > 0 && m.selectedAccountIdx < len(m.accounts) {
 		return &m.accounts[m.selectedAccountIdx]
@@ -385,8 +406,8 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 			maxFocus := focusSend
 			minFocus := focusFrom
-			// Skip From field if only one account (nothing to switch)
-			if len(m.accounts) <= 1 {
+			// Skip From field if only one non-catch-all account (nothing to switch or edit)
+			if len(m.accounts) <= 1 && !m.isCatchAllAccount() {
 				minFocus = focusTo
 			}
 
@@ -396,6 +417,7 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				m.focusIndex = maxFocus
 			}
 
+			m.fromInput.Blur()
 			m.toInput.Blur()
 			m.ccInput.Blur()
 			m.bccInput.Blur()
@@ -404,6 +426,10 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			m.signatureInput.Blur()
 
 			switch m.focusIndex {
+			case focusFrom:
+				if m.isCatchAllAccount() {
+					cmds = append(cmds, m.fromInput.Focus())
+				}
 			case focusTo:
 				cmds = append(cmds, m.toInput.Focus())
 			case focusCc:
@@ -428,8 +454,12 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		case "enter", " ":
 			switch m.focusIndex {
 			case focusFrom:
-				if len(m.accounts) > 1 && msg.String() == "enter" {
+				if msg.String() == "enter" && len(m.accounts) > 1 {
 					m.showAccountPicker = true
+					return m, nil
+				}
+				if m.isCatchAllAccount() && msg.String() == " " {
+					break
 				}
 				return m, nil
 			case focusAttachment:
@@ -449,6 +479,10 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					if acc != nil {
 						accountID = acc.ID
 					}
+					fromOverride := ""
+					if m.isCatchAllAccount() {
+						fromOverride = m.fromInput.Value()
+					}
 					return m, func() tea.Msg {
 						return SendEmailMsg{
 							To:              m.toInput.Value(),
@@ -458,6 +492,7 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 							Body:            m.bodyInput.Value(),
 							AttachmentPaths: m.attachmentPaths,
 							AccountID:       accountID,
+							FromOverride:    fromOverride,
 							QuotedText:      m.quotedText,
 							InReplyTo:       m.inReplyTo,
 							References:      m.references,
@@ -473,6 +508,11 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	}
 
 	switch m.focusIndex {
+	case focusFrom:
+		if m.isCatchAllAccount() {
+			m.fromInput, cmd = m.fromInput.Update(msg)
+			cmds = append(cmds, cmd)
+		}
 	case focusTo:
 		m.toInput, cmd = m.toInput.Update(msg)
 		cmds = append(cmds, cmd)
@@ -528,7 +568,18 @@ func (m *Composer) View() tea.View {
 	// From field with account selector
 	fromAddr := m.getFromAddress()
 	var fromField string
-	if len(m.accounts) > 1 {
+	if m.isCatchAllAccount() {
+		fromAddrView := m.fromInput.View()
+		if len(m.accounts) > 1 {
+			if m.focusIndex == focusFrom {
+				fromField = focusedStyle.Render(fmt.Sprintf("> %s ", t("composer.from"))) + fromAddrView + " " + blurredStyle.Render("["+t("composer.enter_to_switch")+"]")
+			} else {
+				fromField = blurredStyle.Render(fmt.Sprintf("  %s ", t("composer.from"))) + fromAddrView + " " + blurredStyle.Render("["+t("composer.switchable")+"]")
+			}
+		} else {
+			fromField = "  " + t("composer.from") + " " + fromAddrView
+		}
+	} else if len(m.accounts) > 1 {
 		if m.focusIndex == focusFrom {
 			fromField = focusedStyle.Render(fmt.Sprintf("> %s %s [%s]", t("composer.from"), fromAddr, t("composer.enter_to_switch")))
 		} else {
@@ -876,6 +927,7 @@ func (m *Composer) ToDraft() config.Draft {
 		Body:            m.bodyInput.Value(),
 		AttachmentPaths: m.attachmentPaths,
 		AccountID:       m.GetSelectedAccountID(),
+		FromOverride:    m.fromInput.Value(),
 		InReplyTo:       m.inReplyTo,
 		References:      m.references,
 		QuotedText:      m.quotedText,
@@ -889,6 +941,9 @@ func NewComposerFromDraft(draft config.Draft, accounts []config.Account, hideTip
 	m.bccInput.SetValue(draft.Bcc)
 	m.draftID = draft.ID
 	m.attachmentPaths = draft.AttachmentPaths
+	if m.isCatchAllAccount() && draft.FromOverride != "" {
+		m.fromInput.SetValue(draft.FromOverride)
+	}
 	m.inReplyTo = draft.InReplyTo
 	m.references = draft.References
 	m.quotedText = draft.QuotedText

tui/inbox.go 🔗

@@ -389,16 +389,18 @@ func (m *Inbox) updateList() {
 	currentIndex := m.list.Index()
 
 	displayEmails := m.displayEmails()
-	var showAccountLabel bool
+	m.emailsCount = len(displayEmails)
 
+	var showAccountLabel bool
 	if m.searchActive {
-		showAccountLabel = !(len(m.accounts) <= 1)
+		showAccountLabel = len(m.accounts) > 1
 	} else if m.currentAccountID == "" {
-		// "ALL" view - show all emails sorted by date
-		showAccountLabel = !(len(m.accounts) <= 1)
+		showAccountLabel = len(m.accounts) > 1
 	}
 
-	m.emailsCount = len(displayEmails)
+	if !showAccountLabel && len(m.accounts) == 1 && m.accounts[0].CatchAll {
+		showAccountLabel = true
+	}
 
 	items := make([]list.Item, len(displayEmails))
 	for i, email := range displayEmails {
@@ -500,6 +502,18 @@ func (m *Inbox) filteredSearchResults() []fetcher.Email {
 }
 
 func (m *Inbox) accountLabelForEmail(email fetcher.Email) string {
+	var owningAcc *config.Account
+	for i := range m.accounts {
+		if m.accounts[i].ID == email.AccountID {
+			owningAcc = &m.accounts[i]
+			break
+		}
+	}
+
+	if owningAcc != nil && owningAcc.CatchAll && len(email.To) > 0 {
+		return extractEmailAddress(email.To[0])
+	}
+
 	for _, acc := range m.accounts {
 		fetchEmail := accountDisplayEmail(acc)
 		for _, recipient := range email.To {
@@ -508,10 +522,9 @@ func (m *Inbox) accountLabelForEmail(email fetcher.Email) string {
 			}
 		}
 	}
-	for _, acc := range m.accounts {
-		if acc.ID == email.AccountID {
-			return accountDisplayEmail(acc)
-		}
+
+	if owningAcc != nil {
+		return accountDisplayEmail(*owningAcc)
 	}
 	return ""
 }

tui/login.go 🔗

@@ -35,6 +35,7 @@ const (
 	inputSMTPServer
 	inputSMTPPort
 	inputInsecure
+	inputCatchAll     // "true/false" — show all inbox messages regardless of To address
 	inputJMAPEndpoint // JMAP session URL
 	inputPOP3Server
 	inputPOP3Port
@@ -97,6 +98,9 @@ func NewLogin(hideTips bool) *Login {
 		case inputInsecure:
 			t.Placeholder = "Insecure (true/false) - Skip TLS verification"
 			t.Prompt = "🔓 > "
+		case inputCatchAll:
+			t.Placeholder = "Catch-All (true/false) - Show all inbox messages"
+			t.Prompt = "📬 > "
 		case inputJMAPEndpoint:
 			t.Placeholder = "JMAP Session URL (e.g., https://api.fastmail.com/jmap/session)"
 			t.Prompt = "🔗 > "
@@ -139,14 +143,14 @@ func (m *Login) visibleFields() []int {
 	switch proto {
 	case "jmap":
 		// JMAP: no provider selector, just endpoint + common fields
-		fields = append(fields, inputName, inputEmail, inputFetchEmail, inputSendAsEmail, inputPassword, inputJMAPEndpoint)
+		fields = append(fields, inputName, inputEmail, inputFetchEmail, inputSendAsEmail, inputCatchAll, inputPassword, inputJMAPEndpoint)
 	case "pop3":
 		// POP3: custom server fields + SMTP for sending
-		fields = append(fields, inputName, inputEmail, inputFetchEmail, inputSendAsEmail, inputPassword,
+		fields = append(fields, inputName, inputEmail, inputFetchEmail, inputSendAsEmail, inputCatchAll, inputPassword,
 			inputPOP3Server, inputPOP3Port, inputSMTPServer, inputSMTPPort, inputInsecure)
 	default:
 		// IMAP (default): existing flow
-		fields = append(fields, inputProvider, inputName, inputEmail, inputFetchEmail, inputSendAsEmail)
+		fields = append(fields, inputProvider, inputName, inputEmail, inputFetchEmail, inputSendAsEmail, inputCatchAll)
 		if hasOAuth {
 			fields = append(fields, inputAuthMethod)
 		}
@@ -284,6 +288,7 @@ func (m *Login) submitForm() func() tea.Msg {
 	proto := m.protocol()
 
 	insecure := m.inputs[inputInsecure].Value() == "true"
+	catchAll := m.inputs[inputCatchAll].Value() == "true"
 
 	return func() tea.Msg {
 		return Credentials{
@@ -293,6 +298,7 @@ func (m *Login) submitForm() func() tea.Msg {
 			Host:         m.inputs[inputEmail].Value(),
 			FetchEmail:   m.inputs[inputFetchEmail].Value(),
 			SendAsEmail:  m.inputs[inputSendAsEmail].Value(),
+			CatchAll:     catchAll,
 			Password:     m.inputs[inputPassword].Value(),
 			IMAPServer:   m.inputs[inputIMAPServer].Value(),
 			IMAPPort:     imapPort,
@@ -344,6 +350,8 @@ func (m *Login) View() tea.View {
 		tip = "The port for the SMTP server (usually 587 for TLS)."
 	case inputInsecure:
 		tip = "Type 'true' to disable TLS certificate verification (not recommended)."
+	case inputCatchAll:
+		tip = "Type 'true' to show all inbox messages regardless of To address (useful for catch-all domains)."
 	case inputJMAPEndpoint:
 		tip = "The JMAP session resource URL (e.g., https://api.fastmail.com/jmap/session)."
 	case inputPOP3Server:
@@ -366,6 +374,7 @@ func (m *Login) View() tea.View {
 			m.inputs[inputEmail].View(),
 			m.inputs[inputFetchEmail].View(),
 			m.inputs[inputSendAsEmail].View(),
+			m.inputs[inputCatchAll].View(),
 			m.inputs[inputPassword].View(),
 			"",
 			listHeader.Render("JMAP Settings:"),
@@ -377,6 +386,7 @@ func (m *Login) View() tea.View {
 			m.inputs[inputEmail].View(),
 			m.inputs[inputFetchEmail].View(),
 			m.inputs[inputSendAsEmail].View(),
+			m.inputs[inputCatchAll].View(),
 			m.inputs[inputPassword].View(),
 			"",
 			listHeader.Render("POP3 Server Settings:"),
@@ -398,6 +408,7 @@ func (m *Login) View() tea.View {
 			m.inputs[inputEmail].View(),
 			m.inputs[inputFetchEmail].View(),
 			m.inputs[inputSendAsEmail].View(),
+			m.inputs[inputCatchAll].View(),
 		)
 
 		if hasOAuth {
@@ -438,7 +449,7 @@ func (m *Login) View() tea.View {
 }
 
 // SetEditMode sets the login form to edit an existing account.
-func (m *Login) SetEditMode(accountID, protocol, provider, name, email, fetchEmail, sendAsEmail, imapServer string, imapPort int, smtpServer string, smtpPort int, insecure bool, jmapEndpoint, pop3Server string, pop3Port int) {
+func (m *Login) SetEditMode(accountID, protocol, provider, name, email, fetchEmail, sendAsEmail, imapServer string, imapPort int, smtpServer string, smtpPort int, insecure bool, jmapEndpoint, pop3Server string, pop3Port int, catchAll bool) {
 	m.isEditMode = true
 	m.accountID = accountID
 
@@ -451,6 +462,11 @@ func (m *Login) SetEditMode(accountID, protocol, provider, name, email, fetchEma
 	m.inputs[inputEmail].SetValue(email)
 	m.inputs[inputFetchEmail].SetValue(fetchEmail)
 	m.inputs[inputSendAsEmail].SetValue(sendAsEmail)
+	if catchAll {
+		m.inputs[inputCatchAll].SetValue("true")
+	} else {
+		m.inputs[inputCatchAll].SetValue("false")
+	}
 	m.showCustom = provider == "custom"
 
 	if m.showCustom {

tui/messages.go 🔗

@@ -35,6 +35,7 @@ type SendEmailMsg struct {
 	InReplyTo       string
 	References      []string
 	AccountID       string // ID of the account to send from
+	FromOverride    string // Custom From address (used when account is catch-all)
 	QuotedText      string // Hidden quoted text appended when sending
 	Signature       string // Signature to append to email body
 	SignSMIME       bool   // Whether to sign the email using S/MIME
@@ -48,6 +49,7 @@ type Credentials struct {
 	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.
 	SendAsEmail  string // Optional From header email. If empty, sending falls back to FetchEmail, then Host.
+	CatchAll     bool   // Show all inbox messages regardless of To address.
 	Password     string
 	IMAPServer   string
 	IMAPPort     int
@@ -277,6 +279,7 @@ type GoToEditAccountMsg struct {
 	Email        string
 	FetchEmail   string
 	SendAsEmail  string
+	CatchAll     bool
 	IMAPServer   string
 	IMAPPort     int
 	SMTPServer   string

tui/settings_accounts.go 🔗

@@ -59,6 +59,7 @@ func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 					Email:        acc.Email,
 					FetchEmail:   acc.FetchEmail,
 					SendAsEmail:  acc.SendAsEmail,
+					CatchAll:     acc.CatchAll,
 					IMAPServer:   acc.IMAPServer,
 					IMAPPort:     acc.IMAPPort,
 					SMTPServer:   acc.SMTPServer,
@@ -147,6 +148,9 @@ func (m *Settings) viewAccounts() string {
 		if config.HasAccountSignature(&account) {
 			providerInfo += " [Signature]"
 		}
+		if account.CatchAll {
+			providerInfo += " [Catch-All]"
+		}
 
 		line := fmt.Sprintf("%s - %s", displayName, accountEmailStyle.Render(providerInfo))