From 34857a3869a58ad3dd6401d462520f581778e2ea Mon Sep 17 00:00:00 2001 From: Mohamed Mahmoud Date: Thu, 21 May 2026 19:38:52 +0300 Subject: [PATCH] fix: add TLS session resumption (#1304) ## What? Configured `TLS` session resumption for `IMAP` and `SMTP`. ## Why? Reduce reconnect latency by avoiding full `TLS` handshakes. Closes #1174 --- config/config.go | 19 +++++++++++++++++++ config/config_test.go | 2 ++ fetcher/fetcher.go | 6 ++++++ main.go | 2 ++ sender/sender.go | 12 ++++++++++++ 5 files changed, 41 insertions(+) diff --git a/config/config.go b/config/config.go index 9ca0a041af314bd355d4fe4fe4cc05c87f9a2dc0..a527e7616a9b0229810c59e36bc3c37267907439 100644 --- a/config/config.go +++ b/config/config.go @@ -1,12 +1,14 @@ package config import ( + "crypto/tls" "encoding/json" "fmt" "log" "os" "path/filepath" "strings" + "sync" "github.com/google/uuid" "github.com/zalando/go-keyring" @@ -29,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"` @@ -46,6 +53,8 @@ type Account struct { // regardless of which address they were delivered to. CatchAll bool `json:"catch_all,omitempty"` + 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"` IMAPPort int `json:"imap_port,omitempty"` @@ -229,6 +238,14 @@ func (a *Account) GetSMTPServer() string { } } +func (a *Account) GetClientSessionCache() tls.ClientSessionCache { + a.SC.once.Do(func() { + a.SC.cache = tls.NewLRUClientSessionCache(64) + }) + + return a.SC.cache +} + // GetSMTPPort returns the SMTP port for the account. func (a *Account) GetSMTPPort() int { switch a.ServiceProvider { @@ -580,6 +597,7 @@ func LoadConfig() (*Config, error) { Password: legacyConfig.Password, ServiceProvider: legacyConfig.ServiceProvider, FetchEmail: legacyConfig.Email, + SC: &SessionCache{}, }, }, } @@ -631,6 +649,7 @@ func LoadConfig() (*Config, error) { POP3Server: rawAcc.POP3Server, POP3Port: rawAcc.POP3Port, CatchAll: rawAcc.CatchAll, + SC: &SessionCache{}, } // Validate PGPKeySource diff --git a/config/config_test.go b/config/config_test.go index e7974d1843759016df371bd2332975c8aef50337..d60b2c84e9b853c69ab63c59fee948423c214731 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{}, }, }, } diff --git a/fetcher/fetcher.go b/fetcher/fetcher.go index c5435a26a04fecbfc9aa33b4680539259eb34de5..c0aa5132306130b9caff1d211c08ac09f09609b1 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" @@ -376,6 +377,11 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options) ServerName: imapServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, + ClientSessionCache: account.GetClientSessionCache(), + VerifyConnection: func(cs tls.ConnectionState) error { + loglevel.Debugf("IMAP TLS connection resumed: %t", cs.DidResume) + return nil + }, }, } if extraOpts != nil { diff --git a/main.go b/main.go index 406cab549f09d708a6a9be12f2482fcaee79a321..5d54f53bb6f1f20bd1799db0bbc7449bc6231b62 100644 --- a/main.go +++ b/main.go @@ -385,6 +385,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" { @@ -430,6 +431,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" { diff --git a/sender/sender.go b/sender/sender.go index 816b3bdecc156e4c7c1b2e0c0d69d252aea44eaf..e6cb3bbf6e6ce1d40d3a9c925439cfe6d823c628 100644 --- a/sender/sender.go +++ b/sender/sender.go @@ -25,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" @@ -671,6 +672,11 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody ServerName: smtpServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, + ClientSessionCache: account.GetClientSessionCache(), + VerifyConnection: func(cs tls.ConnectionState) error { + loglevel.Debugf("SMTP TLS connection resumed: %t", cs.DidResume) + return nil + }, } var c *smtp.Client @@ -881,10 +887,16 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody // Send via SMTP addr := fmt.Sprintf("%s:%d", smtpServer, smtpPort) + tlsConfig := &tls.Config{ ServerName: smtpServer, InsecureSkipVerify: account.Insecure, MinVersion: tls.VersionTLS12, + ClientSessionCache: account.GetClientSessionCache(), + VerifyConnection: func(cs tls.ConnectionState) error { + loglevel.Debugf("SMTP TLS connection resumed: %t", cs.DidResume) + return nil + }, } var c *smtp.Client