From 11d4adbab713163ab71d2ee1c2c59ae582d7b713 Mon Sep 17 00:00:00 2001 From: "masukomi (a.k.a. Kay Rhodes)" Date: Fri, 5 Jun 2026 09:36:32 -0400 Subject: [PATCH] fix: JMAP integration (#1445) ## What? The code to support JMAP authentication via API tokens was in place, it just wasn't wired up correctly. This fixes that. - JMAP protocol selection ended up getting routed to IMAP code - no way to specify `token` for `account.AuthMethod` - `hasBackendProvider` didn't account for/support `jmap` - modified placeholder when `account.AuthMethod` is `token` to say `API Token` instead of `Password / App Password` - modified tip for `inputAuthMethod` when protocol is `jmap` ## Why? Because I use Fastmail, want to use JMAP, and want to be able to read my email. JMAP authentication with Fastmail via API Tokens wasn't working for multiple reasons. Fastmail is the largest (only?) JMAP supporting email host and they require API Tokens. I have manually confirmed that this authenticates just fine with my Fastmail account via JMAP and the API token. --- backend/jmap/jmap.go | 2 +- fetcher/dispatch.go | 7 ++++--- tui/login.go | 34 +++++++++++++++++++++++++++------- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/backend/jmap/jmap.go b/backend/jmap/jmap.go index 85181d84b7ee0cfce569b81157295145237383cf..6c2dd78eecc6bb2be32ccc7ab1487fb4edb9e33e 100644 --- a/backend/jmap/jmap.go +++ b/backend/jmap/jmap.go @@ -53,7 +53,7 @@ func New(account *config.Account) (*Provider, error) { SessionEndpoint: account.JMAPEndpoint, } - if account.AuthMethod == "oauth2" { + if account.AuthMethod == "oauth2" || account.AuthMethod == "token" || account.AuthMethod == "" { client.WithAccessToken(account.Password) } else { client.WithBasicAuth(account.Email, account.Password) diff --git a/fetcher/dispatch.go b/fetcher/dispatch.go index faafc7ef2e3407cc1c4a58a4de61421835f36aff..d1ab098eafd56f4f936b64f8b2a2815430e6cb32 100644 --- a/fetcher/dispatch.go +++ b/fetcher/dispatch.go @@ -2,15 +2,16 @@ package fetcher import ( "github.com/floatpane/matcha/backend" + _ "github.com/floatpane/matcha/backend/jmap" // register jmap backend _ "github.com/floatpane/matcha/backend/maildir" // register maildir backend "github.com/floatpane/matcha/config" ) // hasBackendProvider reports whether the account is served by a non-IMAP -// backend (currently only "maildir") and should be routed through the -// backend.Provider abstraction instead of the legacy IMAP code path. +// backend (currently only "maildir" and "jmap") and should be routed through +// the backend.Provider abstraction instead of the legacy IMAP code path. func hasBackendProvider(account *config.Account) bool { - return account != nil && account.Protocol == "maildir" + return account != nil && (account.Protocol == "maildir" || account.Protocol == "jmap") } // newBackendProvider builds the backend.Provider for the account. Callers diff --git a/tui/login.go b/tui/login.go index 2e3958e87c0b914cfbe017069ca118e57bc82fc1..e99122acc97d7eec2d6f3fc2832569591a31be1d 100644 --- a/tui/login.go +++ b/tui/login.go @@ -94,7 +94,7 @@ func NewLogin(hideTips bool) *Login { t.Placeholder = "Send As Email (optional From header override)" t.Prompt = "✉️ > " case inputAuthMethod: - t.Placeholder = "Auth Method (password or oauth2)" + t.Placeholder = "Auth Method (password, oauth2, or token)" t.Prompt = "🔐 > " case inputPassword: t.Placeholder = "Password / App Password" @@ -178,7 +178,7 @@ func (m *Login) visibleFields() []int { switch proto { case protocolJMAP: // JMAP: no provider selector, just endpoint + common fields - fields = append(fields, inputName, inputEmail, inputFetchEmail, inputSendAsEmail, inputCatchAll, inputPassword, inputJMAPEndpoint) + fields = append(fields, inputName, inputEmail, inputFetchEmail, inputSendAsEmail, inputCatchAll, inputAuthMethod, inputPassword, inputJMAPEndpoint) case protocolPOP3: // POP3: custom server fields + SMTP for sending fields = append(fields, inputName, inputEmail, inputFetchEmail, inputSendAsEmail, inputCatchAll, inputPassword, @@ -313,6 +313,13 @@ func (m *Login) updateFlags() { provider := m.inputs[inputProvider].Value() m.showCustom = provider == "custom" m.useOAuth2 = m.inputs[inputAuthMethod].Value() == "oauth2" + + authMethod := m.inputs[inputAuthMethod].Value() + if m.protocol() == protocolJMAP && (authMethod == "token" || authMethod == "") { + m.inputs[inputPassword].Placeholder = "API Token" + } else { + m.inputs[inputPassword].Placeholder = "Password / App Password" + } } // validPort parses a port string and returns the integer value if it is within @@ -335,13 +342,21 @@ func (m *Login) submitForm() func() tea.Msg { smtpPort := validPort(m.inputs[inputSMTPPort].Value(), 587) pop3Port := validPort(m.inputs[inputPOP3Port].Value(), 995) - authMethod := "password" - if m.useOAuth2 { + proto := m.protocol() + + var authMethod string + switch { + case proto == protocolJMAP: + authMethod = m.inputs[inputAuthMethod].Value() + if authMethod == "" { + authMethod = "token" + } + case m.useOAuth2: authMethod = "oauth2" + default: + authMethod = "password" } - proto := m.protocol() - insecure := m.inputs[inputInsecure].Value() == "true" catchAll := m.inputs[inputCatchAll].Value() == "true" @@ -414,6 +429,7 @@ func (m *Login) protocolFieldViews(proto string) []string { switch proto { case protocolJMAP: return append(common, + m.inputs[inputAuthMethod].View(), m.inputs[inputPassword].View(), "", listHeader.Render("JMAP Settings:"), @@ -500,7 +516,11 @@ func (m *Login) View() tea.View { case inputSendAsEmail: tip = "Optional From header override for outgoing email. Leave blank to send as the fetched address." case inputAuthMethod: - tip = "Type 'oauth2' for OAuth2 or 'password' for app password." + if m.protocol() == protocolJMAP { + tip = "Type 'token' for API token (Bearer auth, default) or 'password' for HTTP Basic auth." + } else { + tip = "Type 'oauth2' for OAuth2 or 'password' for app password." + } case inputPassword: tip = "Your password or an app-specific password if using 2FA." case inputIMAPServer: