From cdacab1abb0d757f5eb12f5d2c236cef5f18c81d Mon Sep 17 00:00:00 2001 From: mavonx Date: Mon, 18 May 2026 01:05:46 +0300 Subject: [PATCH 1/7] fix: add TLS session resumption --- config/config.go | 9 +++++++++ fetcher/fetcher.go | 7 +++++++ sender/sender.go | 16 ++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/config/config.go b/config/config.go index 9ca0a04..e18c46a 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "crypto/tls" "encoding/json" "fmt" "log" @@ -46,6 +47,8 @@ type Account struct { // regardless of which address they were delivered to. CatchAll bool `json:"catch_all,omitempty"` + ClientSessionCache tls.ClientSessionCache `json:"-"` // "-" prevents the ClientSessionCache from being saved to config.json + // Custom server settings (used when ServiceProvider is "custom") IMAPServer string `json:"imap_server,omitempty"` IMAPPort int `json:"imap_port,omitempty"` @@ -244,6 +247,12 @@ func (a *Account) GetSMTPPort() int { } } +func (a *Account) EnsureSessionCache() { + if a.ClientSessionCache == nil { + a.ClientSessionCache = tls.NewLRUClientSessionCache(64) + } +} + // GetFetchEmail returns the configured fetch identity, falling back to Email. func (a *Account) GetFetchEmail() string { if a.FetchEmail != "" { diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index 0b83c48..f6cb794 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -371,11 +371,18 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options) addr := fmt.Sprintf("%s:%d", imapServer, imapPort) + account.EnsureSessionCache() + options := &imapclient.Options{ TLSConfig: &tls.Config{ ServerName: imapServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, + ClientSessionCache: account.ClientSessionCache, + VerifyConnection: func(cs tls.ConnectionState) error { + log.Printf("SMTP connection resumed: %t", cs.DidResume) + return nil + }, }, } if extraOpts != nil { diff --git a/sender/sender.go b/sender/sender.go index 4df43a9..21c140c 100644 --- a/sender/sender.go +++ b/sender/sender.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io" + "log" "mime" "mime/multipart" "mime/quotedprintable" @@ -656,10 +657,17 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort) + account.EnsureSessionCache() + tlsConfig := &tls.Config{ ServerName: smtpServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, + ClientSessionCache: account.ClientSessionCache, + VerifyConnection: func(cs tls.ConnectionState) error { + log.Printf("SMTP connection resumed: %t", cs.DidResume) + return nil + }, } var c *smtp.Client @@ -870,10 +878,18 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody // Send via SMTP addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort) + + account.EnsureSessionCache() + tlsConfig := &tls.Config{ ServerName: smtpServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, + ClientSessionCache: account.ClientSessionCache, + VerifyConnection: func(cs tls.ConnectionState) error { + log.Printf("SMTP connection resumed: %t", cs.DidResume) + return nil + }, } var c *smtp.Client From 2400795827b3535c9cc2faa4fd0d1adce0caaa44 Mon Sep 17 00:00:00 2001 From: mavonx Date: Mon, 18 May 2026 21:45:44 +0300 Subject: [PATCH 2/7] fix: move TLS session cache initialization to account creation/loading time --- config/config.go | 7 +------ fetcher/fetcher.go | 4 +--- main.go | 30 ++++++++++++++++-------------- sender/sender.go | 8 ++------ 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/config/config.go b/config/config.go index e18c46a..81d3098 100644 --- a/config/config.go +++ b/config/config.go @@ -247,12 +247,6 @@ func (a *Account) GetSMTPPort() int { } } -func (a *Account) EnsureSessionCache() { - if a.ClientSessionCache == nil { - a.ClientSessionCache = tls.NewLRUClientSessionCache(64) - } -} - // GetFetchEmail returns the configured fetch identity, falling back to Email. func (a *Account) GetFetchEmail() string { if a.FetchEmail != "" { @@ -640,6 +634,7 @@ func LoadConfig() (*Config, error) { POP3Server: rawAcc.POP3Server, POP3Port: rawAcc.POP3Port, CatchAll: rawAcc.CatchAll, + ClientSessionCache: tls.NewLRUClientSessionCache(64), } // Validate PGPKeySource diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index f6cb794..b599abc 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -371,8 +371,6 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options) addr := fmt.Sprintf("%s:%d", imapServer, imapPort) - account.EnsureSessionCache() - options := &imapclient.Options{ TLSConfig: &tls.Config{ ServerName: imapServer, @@ -380,7 +378,7 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options) MinVersion: tls.VersionTLS12, ClientSessionCache: account.ClientSessionCache, VerifyConnection: func(cs tls.ConnectionState) error { - log.Printf("SMTP connection resumed: %t", cs.DidResume) + log.Printf("IMAP TLS connection resumed: %t", cs.DidResume) return nil }, }, diff --git a/main.go b/main.go index 9462de2..20d94b4 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "archive/zip" "compress/gzip" "context" + "crypto/tls" "encoding/base64" "encoding/json" "errors" @@ -414,20 +415,21 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // New account: create one account per fetch email address for _, fe := range fetchEmails { account := config.Account{ - ID: uuid.New().String(), - Name: msg.Name, - Email: msg.Host, - Password: msg.Password, - ServiceProvider: msg.Provider, - FetchEmail: fe, - SendAsEmail: msg.SendAsEmail, - CatchAll: msg.CatchAll, - AuthMethod: msg.AuthMethod, - Protocol: msg.Protocol, - JMAPEndpoint: msg.JMAPEndpoint, - POP3Server: msg.POP3Server, - POP3Port: msg.POP3Port, - MaildirPath: msg.MaildirPath, + ID: uuid.New().String(), + Name: msg.Name, + Email: msg.Host, + Password: msg.Password, + ServiceProvider: msg.Provider, + FetchEmail: fe, + SendAsEmail: msg.SendAsEmail, + CatchAll: msg.CatchAll, + AuthMethod: msg.AuthMethod, + Protocol: msg.Protocol, + JMAPEndpoint: msg.JMAPEndpoint, + POP3Server: msg.POP3Server, + POP3Port: msg.POP3Port, + MaildirPath: msg.MaildirPath, + ClientSessionCache: tls.NewLRUClientSessionCache(64), } if msg.Provider == "custom" || msg.Protocol == "pop3" { diff --git a/sender/sender.go b/sender/sender.go index 21c140c..a151b65 100644 --- a/sender/sender.go +++ b/sender/sender.go @@ -657,15 +657,13 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort) - account.EnsureSessionCache() - tlsConfig := &tls.Config{ ServerName: smtpServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, ClientSessionCache: account.ClientSessionCache, VerifyConnection: func(cs tls.ConnectionState) error { - log.Printf("SMTP connection resumed: %t", cs.DidResume) + log.Printf("SMTP TLS connection resumed: %t", cs.DidResume) return nil }, } @@ -879,15 +877,13 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody // Send via SMTP addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort) - account.EnsureSessionCache() - tlsConfig := &tls.Config{ ServerName: smtpServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, ClientSessionCache: account.ClientSessionCache, VerifyConnection: func(cs tls.ConnectionState) error { - log.Printf("SMTP connection resumed: %t", cs.DidResume) + log.Printf("SMTP TLS connection resumed: %t", cs.DidResume) return nil }, } From d0901ff70704899049dc14c097f02e1898da15a9 Mon Sep 17 00:00:00 2001 From: mavonx Date: Mon, 18 May 2026 22:17:03 +0300 Subject: [PATCH 3/7] fix: implement ClientSessionCache getter --- config/config.go | 8 +++++++- fetcher/fetcher.go | 2 +- sender/sender.go | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/config/config.go b/config/config.go index 81d3098..0fd5b81 100644 --- a/config/config.go +++ b/config/config.go @@ -232,6 +232,13 @@ func (a *Account) GetSMTPServer() string { } } +func (a *Account) GetClientSessionCache() tls.ClientSessionCache { + if a.ClientSessionCache == nil { + a.ClientSessionCache = tls.NewLRUClientSessionCache(64) + } + return a.ClientSessionCache +} + // GetSMTPPort returns the SMTP port for the account. func (a *Account) GetSMTPPort() int { switch a.ServiceProvider { @@ -634,7 +641,6 @@ func LoadConfig() (*Config, error) { POP3Server: rawAcc.POP3Server, POP3Port: rawAcc.POP3Port, CatchAll: rawAcc.CatchAll, - ClientSessionCache: tls.NewLRUClientSessionCache(64), } // Validate PGPKeySource diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index b599abc..240941f 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -376,7 +376,7 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options) ServerName: imapServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, - ClientSessionCache: account.ClientSessionCache, + ClientSessionCache: account.GetClientSessionCache(), VerifyConnection: func(cs tls.ConnectionState) error { log.Printf("IMAP TLS connection resumed: %t", cs.DidResume) return nil diff --git a/sender/sender.go b/sender/sender.go index a151b65..89d3beb 100644 --- a/sender/sender.go +++ b/sender/sender.go @@ -661,7 +661,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody ServerName: smtpServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, - ClientSessionCache: account.ClientSessionCache, + ClientSessionCache: account.GetClientSessionCache(), VerifyConnection: func(cs tls.ConnectionState) error { log.Printf("SMTP TLS connection resumed: %t", cs.DidResume) return nil @@ -881,7 +881,7 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody ServerName: smtpServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, - ClientSessionCache: account.ClientSessionCache, + ClientSessionCache: account.GetClientSessionCache(), VerifyConnection: func(cs tls.ConnectionState) error { log.Printf("SMTP TLS connection resumed: %t", cs.DidResume) return nil From d0391e33ad6981639162ba27cad2a23c36b5c859 Mon Sep 17 00:00:00 2001 From: mavonx Date: Tue, 19 May 2026 09:27:05 +0300 Subject: [PATCH 4/7] fix: prevent race condition in TLS session cache initialization --- config/config.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index 0fd5b81..33502b4 100644 --- a/config/config.go +++ b/config/config.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "strings" + "sync" "github.com/google/uuid" "github.com/zalando/go-keyring" @@ -48,6 +49,7 @@ type Account struct { CatchAll bool `json:"catch_all,omitempty"` ClientSessionCache tls.ClientSessionCache `json:"-"` // "-" prevents the ClientSessionCache from being saved to config.json + sessionCacheOnce sync.Once `json:"-"` // "-" prevents the sessionCacheOnce from being saved to config.json // Custom server settings (used when ServiceProvider is "custom") IMAPServer string `json:"imap_server,omitempty"` @@ -233,9 +235,10 @@ func (a *Account) GetSMTPServer() string { } func (a *Account) GetClientSessionCache() tls.ClientSessionCache { - if a.ClientSessionCache == nil { + a.sessionCacheOnce.Do(func() { a.ClientSessionCache = tls.NewLRUClientSessionCache(64) - } + }) + return a.ClientSessionCache } From 2494f3d99ab7100a6e89e650e1cfcd6c611750f5 Mon Sep 17 00:00:00 2001 From: mavonx Date: Tue, 19 May 2026 10:23:50 +0300 Subject: [PATCH 5/7] fix: replace sync.Once in Account with pointer based SessionCache --- config/config.go | 16 +++++++++++----- main.go | 32 ++++++++++++++++---------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/config/config.go b/config/config.go index 33502b4..a527e76 100644 --- a/config/config.go +++ b/config/config.go @@ -31,6 +31,11 @@ const ( DateFormatEU = "DD/MM/YYYY HH:MM" ) +type SessionCache struct { + once sync.Once + cache tls.ClientSessionCache +} + // Account stores the configuration for a single email account. type Account struct { ID string `json:"id"` @@ -48,8 +53,7 @@ type Account struct { // regardless of which address they were delivered to. CatchAll bool `json:"catch_all,omitempty"` - ClientSessionCache tls.ClientSessionCache `json:"-"` // "-" prevents the ClientSessionCache from being saved to config.json - sessionCacheOnce sync.Once `json:"-"` // "-" prevents the sessionCacheOnce from being saved to config.json + SC *SessionCache `json:"-"` // "-" prevents the SessionCache from being saved to config.json // Custom server settings (used when ServiceProvider is "custom") IMAPServer string `json:"imap_server,omitempty"` @@ -235,11 +239,11 @@ func (a *Account) GetSMTPServer() string { } func (a *Account) GetClientSessionCache() tls.ClientSessionCache { - a.sessionCacheOnce.Do(func() { - a.ClientSessionCache = tls.NewLRUClientSessionCache(64) + a.SC.once.Do(func() { + a.SC.cache = tls.NewLRUClientSessionCache(64) }) - return a.ClientSessionCache + return a.SC.cache } // GetSMTPPort returns the SMTP port for the account. @@ -593,6 +597,7 @@ func LoadConfig() (*Config, error) { Password: legacyConfig.Password, ServiceProvider: legacyConfig.ServiceProvider, FetchEmail: legacyConfig.Email, + SC: &SessionCache{}, }, }, } @@ -644,6 +649,7 @@ func LoadConfig() (*Config, error) { POP3Server: rawAcc.POP3Server, POP3Port: rawAcc.POP3Port, CatchAll: rawAcc.CatchAll, + SC: &SessionCache{}, } // Validate PGPKeySource diff --git a/main.go b/main.go index 20d94b4..844460b 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,6 @@ import ( "archive/zip" "compress/gzip" "context" - "crypto/tls" "encoding/base64" "encoding/json" "errors" @@ -384,6 +383,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { POP3Server: msg.POP3Server, POP3Port: msg.POP3Port, MaildirPath: msg.MaildirPath, + SC: &config.SessionCache{}, } if msg.Provider == "custom" || msg.Protocol == "pop3" { @@ -415,21 +415,21 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // New account: create one account per fetch email address for _, fe := range fetchEmails { account := config.Account{ - ID: uuid.New().String(), - Name: msg.Name, - Email: msg.Host, - Password: msg.Password, - ServiceProvider: msg.Provider, - FetchEmail: fe, - SendAsEmail: msg.SendAsEmail, - CatchAll: msg.CatchAll, - AuthMethod: msg.AuthMethod, - Protocol: msg.Protocol, - JMAPEndpoint: msg.JMAPEndpoint, - POP3Server: msg.POP3Server, - POP3Port: msg.POP3Port, - MaildirPath: msg.MaildirPath, - ClientSessionCache: tls.NewLRUClientSessionCache(64), + ID: uuid.New().String(), + Name: msg.Name, + Email: msg.Host, + Password: msg.Password, + ServiceProvider: msg.Provider, + FetchEmail: fe, + SendAsEmail: msg.SendAsEmail, + CatchAll: msg.CatchAll, + AuthMethod: msg.AuthMethod, + Protocol: msg.Protocol, + JMAPEndpoint: msg.JMAPEndpoint, + POP3Server: msg.POP3Server, + POP3Port: msg.POP3Port, + MaildirPath: msg.MaildirPath, + SC: &config.SessionCache{}, } if msg.Provider == "custom" || msg.Protocol == "pop3" { From 663ff6728b8c3393a1a1d4b6bfaebeed5c08a835 Mon Sep 17 00:00:00 2001 From: mavonx Date: Tue, 19 May 2026 10:37:19 +0300 Subject: [PATCH 6/7] fix: add SC initialization to accounts in TestSaveAndLoadConfig --- config/config_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config_test.go b/config/config_test.go index e7974d1..d60b2c8 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -32,6 +32,7 @@ func TestSaveAndLoadConfig(t *testing.T) { Password: "supersecret", ServiceProvider: "gmail", SendAsEmail: "alias@example.com", + SC: &SessionCache{}, }, { ID: "test-id-2", @@ -44,6 +45,7 @@ func TestSaveAndLoadConfig(t *testing.T) { SMTPServer: "smtp.custom.com", SMTPPort: 587, CatchAll: true, + SC: &SessionCache{}, }, }, } From d7918b654c95f9e09149afff2afa2afc7ba99a9a Mon Sep 17 00:00:00 2001 From: mavonx Date: Wed, 20 May 2026 19:51:49 +0300 Subject: [PATCH 7/7] fix: replace log.Printf with loglevel.Debugf --- fetcher/fetcher.go | 3 ++- sender/sender.go | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index 240941f..ad61b12 100644 --- a/fetcher/fetcher.go +++ b/fetcher/fetcher.go @@ -29,6 +29,7 @@ import ( "github.com/emersion/go-message/mail" "github.com/emersion/go-pgpmail" "github.com/floatpane/matcha/config" + "github.com/floatpane/matcha/internal/loglevel" "go.mozilla.org/pkcs7" "golang.org/x/text/encoding" "golang.org/x/text/encoding/ianaindex" @@ -378,7 +379,7 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options) MinVersion: tls.VersionTLS12, ClientSessionCache: account.GetClientSessionCache(), VerifyConnection: func(cs tls.ConnectionState) error { - log.Printf("IMAP TLS connection resumed: %t", cs.DidResume) + loglevel.Debugf("IMAP TLS connection resumed: %t", cs.DidResume) return nil }, }, diff --git a/sender/sender.go b/sender/sender.go index 89d3beb..c40bec2 100644 --- a/sender/sender.go +++ b/sender/sender.go @@ -10,7 +10,6 @@ import ( "errors" "fmt" "io" - "log" "mime" "mime/multipart" "mime/quotedprintable" @@ -26,6 +25,7 @@ import ( "github.com/emersion/go-pgpmail" "github.com/floatpane/matcha/clib" "github.com/floatpane/matcha/config" + "github.com/floatpane/matcha/internal/loglevel" "github.com/floatpane/matcha/pgp" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" @@ -663,7 +663,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody MinVersion: tls.VersionTLS12, ClientSessionCache: account.GetClientSessionCache(), VerifyConnection: func(cs tls.ConnectionState) error { - log.Printf("SMTP TLS connection resumed: %t", cs.DidResume) + loglevel.Debugf("SMTP TLS connection resumed: %t", cs.DidResume) return nil }, } @@ -883,7 +883,7 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody MinVersion: tls.VersionTLS12, ClientSessionCache: account.GetClientSessionCache(), VerifyConnection: func(cs tls.ConnectionState) error { - log.Printf("SMTP TLS connection resumed: %t", cs.DidResume) + loglevel.Debugf("SMTP TLS connection resumed: %t", cs.DidResume) return nil }, }