fix: lint issues (#1358)

Drew Smirnoff created

Signed-off-by: drew <me@andrinoff.com>

Change summary

backend/jmap/jmap.go               |  18 
backend/maildir/maildir.go         |  20 
backend/maildir/maildir_test.go    |   5 
backend/pop3/pop3.go               |  22 
calendar/calendar.go               |   2 
cli/config.go                      |   4 
cli/contacts_export.go             |   2 
cli/install.go                     |   8 
cli/integration.go                 |  30 +-
clib/htmlconv.go                   |   2 
clib/imgconv.go                    |   2 
clib/macos/appearance.go           |   6 
clib/macos/badge.go                |   6 
clib/macos/contacts.go             |   6 
clib/macos/file_picker.go          |   6 
config/cache.go                    |  14 
config/config.go                   |  38 +-
config/encryption.go               |   6 
config/keybinds.go                 |  12 
config/lru.go                      |   5 
config/oauth.go                    |   4 
config/signature.go                |   2 
daemon/daemon.go                   |  42 +-
daemon/handler.go                  |  87 +++---
daemonclient/client.go             |   2 
daemonclient/service.go            |   6 
daemonrpc/protocol.go              |   7 
fetcher/fetcher.go                 | 169 ++++++------
fetcher/idle.go                    |  12 
fetcher/search.go                  |   2 
i18n/detector.go                   |   5 
i18n/interpolator.go               |   2 
i18n/loader.go                     |   4 
i18n/message.go                    |   4 
i18n/template.go                   |   2 
i18n/validator.go                  |  12 
internal/threading/jwz_test.go     |   4 
main.go                            | 411 ++++++++-----------------------
notify/notify.go                   |   4 
pgp/yubikey.go                     |  54 ++-
pgp/yubikey_test.go                |   1 
plugin/api.go                      |  24 
plugin/http.go                     |   6 
plugin/http_test.go                |   6 
plugin/prompt.go                   |   2 
plugin/settings.go                 |   6 
plugin/storage.go                  |  54 ++--
plugins/embed.go                   |   8 
screenshots/cmd/inbox_view/main.go |  57 ++--
sender/sender.go                   |  64 ++--
tests/integration/imap_test.go     |   2 
theme/theme.go                     |   2 
tui/choice.go                      |   7 
tui/composer.go                    |  47 +-
tui/constants.go                   |  11 
tui/email_view.go                  |  26 +-
tui/filepicker.go                  |   8 
tui/folder_inbox.go                |  70 ++---
tui/folder_inbox_test.go           |   6 
tui/inbox.go                       |  67 ++--
tui/login.go                       |   6 
tui/mailing_list.go                |  27 +-
tui/marketplace.go                 |  12 
tui/password_prompt.go             |   3 
tui/search.go                      |   3 
tui/settings.go                    |  46 +-
tui/settings_accounts.go           |   4 
tui/settings_crypto.go             |  16 
tui/settings_encryption.go         |  16 
tui/settings_general.go            |  15 
tui/settings_lists.go              |   6 
tui/settings_plugins.go            |  22 
tui/settings_theme.go              |   4 
tui/signature.go                   |   4 
tui/theme.go                       |   3 
view/html.go                       | 214 +++-------------
76 files changed, 807 insertions(+), 1,117 deletions(-)

Detailed changes

backend/jmap/jmap.go 🔗

@@ -23,6 +23,8 @@ import (
 	"github.com/floatpane/matcha/config"
 )
 
+const jmapMailboxIds = "mailboxIds"
+
 func init() {
 	backend.RegisterBackend("jmap", func(account *config.Account) (backend.Provider, error) {
 		return New(account)
@@ -167,7 +169,7 @@ func (p *Provider) FetchEmails(_ context.Context, folder string, limit, offset u
 		},
 		Properties: []string{
 			"id", "subject", "from", "to", "replyTo", "receivedAt",
-			"preview", "keywords", "mailboxIds", "hasAttachment",
+			"preview", "keywords", jmapMailboxIds, "hasAttachment",
 			"messageId", "inReplyTo", "references",
 		},
 	})
@@ -220,7 +222,7 @@ func (p *Provider) Search(_ context.Context, folder string, query backend.Search
 		},
 		Properties: []string{
 			"id", "subject", "from", "to", "replyTo", "receivedAt",
-			"preview", "keywords", "mailboxIds", "hasAttachment",
+			"preview", "keywords", jmapMailboxIds, "hasAttachment",
 			"messageId",
 		},
 	})
@@ -356,7 +358,7 @@ func (p *Provider) FetchAttachment(_ context.Context, _ string, _ uint32, partID
 	if err != nil {
 		return nil, fmt.Errorf("jmap download: %w", err)
 	}
-	defer reader.Close()
+	defer reader.Close() //nolint:errcheck
 	return io.ReadAll(reader)
 }
 
@@ -419,7 +421,7 @@ func (p *Provider) DeleteEmail(_ context.Context, _ string, uid uint32) error {
 	req.Invoke(&email.Set{
 		Account: p.accountID,
 		Update: map[jmapclient.ID]jmapclient.Patch{
-			jmapID: {"mailboxIds": map[jmapclient.ID]bool{trashID: true}},
+			jmapID: {jmapMailboxIds: map[jmapclient.ID]bool{trashID: true}},
 		},
 	})
 	_, err = p.client.Do(req)
@@ -441,7 +443,7 @@ func (p *Provider) ArchiveEmail(_ context.Context, _ string, uid uint32) error {
 	req.Invoke(&email.Set{
 		Account: p.accountID,
 		Update: map[jmapclient.ID]jmapclient.Patch{
-			jmapID: {"mailboxIds": map[jmapclient.ID]bool{archiveID: true}},
+			jmapID: {jmapMailboxIds: map[jmapclient.ID]bool{archiveID: true}},
 		},
 	})
 	_, err = p.client.Do(req)
@@ -463,7 +465,7 @@ func (p *Provider) MoveEmail(_ context.Context, uid uint32, _, dstFolder string)
 	req.Invoke(&email.Set{
 		Account: p.accountID,
 		Update: map[jmapclient.ID]jmapclient.Patch{
-			jmapID: {"mailboxIds": map[jmapclient.ID]bool{dstID: true}},
+			jmapID: {jmapMailboxIds: map[jmapclient.ID]bool{dstID: true}},
 		},
 	})
 	_, err = p.client.Do(req)
@@ -596,7 +598,7 @@ func (p *Provider) SendEmail(_ context.Context, msg *backend.OutgoingEmail) erro
 	if sentID != "" {
 		subReq.OnSuccessUpdateEmail = map[jmapclient.ID]jmapclient.Patch{
 			"#sub": {
-				"mailboxIds":      map[jmapclient.ID]bool{sentID: true},
+				jmapMailboxIds:    map[jmapclient.ID]bool{sentID: true},
 				"keywords/$draft": nil,
 			},
 		}
@@ -690,7 +692,7 @@ func (p *Provider) lookupJMAPID(uid uint32) (jmapclient.ID, error) {
 // jmapIDToUID converts a JMAP string ID to a uint32 hash for use as a UID.
 func jmapIDToUID(id jmapclient.ID) uint32 {
 	h := fnv.New32a()
-	h.Write([]byte(id))
+	h.Write([]byte(id)) //nolint:gosec
 	v := h.Sum32()
 	if v == 0 {
 		v = 1

backend/maildir/maildir.go 🔗

@@ -10,6 +10,7 @@ package maildir
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"mime"
@@ -165,7 +166,7 @@ func (p *Provider) readHeader(msg *emaildir.Message) (backend.Email, error) {
 	if err != nil {
 		return backend.Email{}, err
 	}
-	defer rc.Close()
+	defer rc.Close() //nolint:errcheck
 
 	entity, err := message.Read(rc)
 	if err != nil && entity == nil {
@@ -194,7 +195,7 @@ func (p *Provider) FetchEmailBody(_ context.Context, folder string, uid uint32)
 	if err != nil {
 		return "", "", nil, fmt.Errorf("maildir open: %w", err)
 	}
-	defer rc.Close()
+	defer rc.Close() //nolint:errcheck
 
 	return parseMessageBody(rc)
 }
@@ -209,7 +210,7 @@ func (p *Provider) FetchAttachment(_ context.Context, folder string, uid uint32,
 	if err != nil {
 		return nil, fmt.Errorf("maildir open: %w", err)
 	}
-	defer rc.Close()
+	defer rc.Close() //nolint:errcheck
 
 	return findAttachmentData(rc, partID)
 }
@@ -348,7 +349,7 @@ func (p *Provider) matchOpen(msg *emaildir.Message) (backend.Email, string, erro
 	if err != nil {
 		return backend.Email{}, "", err
 	}
-	defer rc.Close()
+	defer rc.Close() //nolint:errcheck
 
 	entity, err := message.Read(rc)
 	if err != nil && entity == nil {
@@ -552,7 +553,7 @@ func parseMessageBody(r io.Reader) (string, string, []backend.Attachment, error)
 
 	for {
 		part, err := mr.NextPart()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			break
 		}
 		if err != nil {
@@ -568,7 +569,8 @@ func parseMessageBody(r io.Reader) (string, string, []backend.Attachment, error)
 			continue
 		}
 
-		if disposition == "attachment" || (disposition == "inline" && !strings.HasPrefix(contentType, "text/")) {
+		switch {
+		case disposition == "attachment" || (disposition == "inline" && !strings.HasPrefix(contentType, "text/")):
 			filename := dParams["filename"]
 			if filename == "" {
 				_, cp, _ := mime.ParseMediaType(part.Header.Get("Content-Type"))
@@ -585,9 +587,9 @@ func parseMessageBody(r io.Reader) (string, string, []backend.Attachment, error)
 				att.ContentID = strings.Trim(cid, "<>")
 			}
 			attachments = append(attachments, att)
-		} else if contentType == "text/html" {
+		case contentType == "text/html":
 			htmlBody = string(data)
-		} else if contentType == "text/plain" && bodyText == "" {
+		case contentType == "text/plain" && bodyText == "":
 			bodyText = string(data)
 		}
 	}
@@ -608,7 +610,7 @@ func findAttachmentData(r io.Reader, targetPartID string) ([]byte, error) {
 	partIdx := 0
 	for {
 		part, err := mr.NextPart()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			break
 		}
 		if err != nil {

backend/maildir/maildir_test.go 🔗

@@ -2,6 +2,7 @@ package maildir
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 	"path/filepath"
@@ -238,7 +239,7 @@ func TestArchiveEmailRequiresArchiveFolder(t *testing.T) {
 	p := newProvider(t, root)
 	emails, _ := p.FetchEmails(context.Background(), "INBOX", 10, 0)
 	err := p.ArchiveEmail(context.Background(), "INBOX", emails[0].UID)
-	if err != backend.ErrNotSupported {
+	if !errors.Is(err, backend.ErrNotSupported) {
 		t.Errorf("want ErrNotSupported, got %v", err)
 	}
 }
@@ -246,7 +247,7 @@ func TestArchiveEmailRequiresArchiveFolder(t *testing.T) {
 func TestSendEmailNotSupported(t *testing.T) {
 	root := makeMaildir(t)
 	p := newProvider(t, root)
-	if err := p.SendEmail(context.Background(), &backend.OutgoingEmail{}); err != backend.ErrNotSupported {
+	if err := p.SendEmail(context.Background(), &backend.OutgoingEmail{}); !errors.Is(err, backend.ErrNotSupported) {
 		t.Errorf("want ErrNotSupported, got %v", err)
 	}
 }

backend/pop3/pop3.go 🔗

@@ -11,6 +11,7 @@ package pop3
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"mime"
@@ -78,7 +79,7 @@ func (p *Provider) connect() (*pop3client.Conn, error) {
 	}
 
 	if err := conn.Auth(p.account.Email, p.account.Password); err != nil {
-		conn.Quit()
+		_ = conn.Quit()
 		return nil, fmt.Errorf("pop3 auth: %w", err)
 	}
 
@@ -90,7 +91,7 @@ func (p *Provider) FetchEmails(_ context.Context, _ string, limit, offset uint32
 	if err != nil {
 		return nil, err
 	}
-	defer conn.Quit()
+	defer conn.Quit() //nolint:errcheck
 
 	// Get message list with UIDs
 	msgs, err := conn.Uidl(0)
@@ -139,7 +140,7 @@ func (p *Provider) FetchEmailBody(_ context.Context, _ string, uid uint32) (stri
 	if err != nil {
 		return "", "", nil, err
 	}
-	defer conn.Quit()
+	defer conn.Quit() //nolint:errcheck
 
 	msgID, err := p.findMessageByUID(conn, uid)
 	if err != nil {
@@ -159,7 +160,7 @@ func (p *Provider) FetchAttachment(_ context.Context, _ string, uid uint32, part
 	if err != nil {
 		return nil, err
 	}
-	defer conn.Quit()
+	defer conn.Quit() //nolint:errcheck
 
 	msgID, err := p.findMessageByUID(conn, uid)
 	if err != nil {
@@ -212,7 +213,7 @@ func (p *Provider) DeleteEmails(_ context.Context, _ string, uids []uint32) erro
 
 	messageIDsByUID, err := p.buildMessageIDsByUID(conn)
 	if err != nil {
-		conn.Quit()
+		_ = conn.Quit()
 		return err
 	}
 
@@ -415,7 +416,7 @@ func parseMessageBody(r io.Reader) (string, string, []backend.Attachment, error)
 
 	for {
 		part, err := mr.NextPart()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			break
 		}
 		if err != nil {
@@ -431,7 +432,8 @@ func parseMessageBody(r io.Reader) (string, string, []backend.Attachment, error)
 			continue
 		}
 
-		if disposition == "attachment" || (disposition == "inline" && !strings.HasPrefix(contentType, "text/")) {
+		switch {
+		case disposition == "attachment" || (disposition == "inline" && !strings.HasPrefix(contentType, "text/")):
 			filename := dParams["filename"]
 			if filename == "" {
 				_, cp, _ := mime.ParseMediaType(part.Header.Get("Content-Type"))
@@ -448,9 +450,9 @@ func parseMessageBody(r io.Reader) (string, string, []backend.Attachment, error)
 				att.ContentID = strings.Trim(cid, "<>")
 			}
 			attachments = append(attachments, att)
-		} else if contentType == "text/html" {
+		case contentType == "text/html":
 			htmlBody = string(data)
-		} else if contentType == "text/plain" && bodyText == "" {
+		case contentType == "text/plain" && bodyText == "":
 			bodyText = string(data)
 		}
 	}
@@ -472,7 +474,7 @@ func findAttachmentData(r io.Reader, targetPartID string) ([]byte, error) {
 	var scanErr error
 	for {
 		part, err := mr.NextPart()
-		if err == io.EOF {
+		if errors.Is(err, io.EOF) {
 			break
 		}
 		if err != nil {

calendar/calendar.go 🔗

@@ -116,7 +116,7 @@ func GenerateRSVP(originalData []byte, userEmail, response string) ([]byte, erro
 			// Attendee not found in original - add ourselves with full parameters
 			vevent.AddAttendee("mailto:"+userEmail,
 				ics.WithRSVP(true),
-				ics.ParticipationStatus(ics.ParticipationStatusNeedsAction),
+				ics.ParticipationStatusNeedsAction,
 				ics.CalendarUserTypeIndividual,
 				ics.ParticipationRoleReqParticipant,
 			)

cli/config.go 🔗

@@ -26,7 +26,7 @@ func RunConfig(args []string) error {
 		name := args[0]
 		// Add .lua extension if not present
 		if filepath.Ext(name) != ".lua" {
-			name = name + ".lua"
+			name += ".lua"
 		}
 		target = filepath.Join(home, ".config", "matcha", "plugins", name)
 	}
@@ -35,7 +35,7 @@ func RunConfig(args []string) error {
 		return fmt.Errorf("file not found: %s", target)
 	}
 
-	cmd := exec.Command(editor, target)
+	cmd := exec.Command(editor, target) //nolint:gosec,noctx
 	cmd.Stdin = os.Stdin
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr

cli/contacts_export.go 🔗

@@ -101,7 +101,7 @@ func runExportContacts(format, outputPath string, noHeader bool) error {
 	if outputPath != "" {
 		dir := filepath.Dir(outputPath)
 		if dir != "." {
-			if err := os.MkdirAll(dir, 0755); err != nil {
+			if err := os.MkdirAll(dir, 0750); err != nil {
 				return fmt.Errorf("failed to create output directory: %w", err)
 			}
 		}

cli/install.go 🔗

@@ -24,11 +24,11 @@ func RunInstall(args []string) error {
 	if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
 		// Download from URL
 		client := httpclient.New(httpclient.InstallTimeout)
-		resp, err := client.Get(source)
+		resp, err := client.Get(source) //nolint:noctx
 		if err != nil {
 			return fmt.Errorf("failed to download: %w", err)
 		}
-		defer resp.Body.Close()
+		defer resp.Body.Close() //nolint:errcheck
 
 		if resp.StatusCode != http.StatusOK {
 			return fmt.Errorf("download returned status %d", resp.StatusCode)
@@ -62,7 +62,7 @@ func RunInstall(args []string) error {
 	}
 
 	dest := filepath.Join(pluginsDir, filename)
-	if err := os.WriteFile(dest, data, 0644); err != nil {
+	if err := os.WriteFile(dest, data, 0644); err != nil { //nolint:gosec
 		return fmt.Errorf("failed to write plugin: %w", err)
 	}
 
@@ -76,7 +76,7 @@ func pluginsDir() (string, error) {
 		return "", fmt.Errorf("cannot find home directory: %w", err)
 	}
 	dir := filepath.Join(home, ".config", "matcha", "plugins")
-	if err := os.MkdirAll(dir, 0755); err != nil {
+	if err := os.MkdirAll(dir, 0750); err != nil {
 		return "", fmt.Errorf("cannot create plugins directory: %w", err)
 	}
 	return dir, nil

cli/integration.go 🔗

@@ -54,14 +54,14 @@ MimeType=x-scheme-handler/mailto;
 	}
 
 	iconsDir := filepath.Join(home, ".local", "share", "icons", "hicolor", "512x512", "apps")
-	if err := os.MkdirAll(iconsDir, 0755); err == nil {
+	if err := os.MkdirAll(iconsDir, 0750); err == nil {
 		iconFile := filepath.Join(iconsDir, "matcha.png")
 		_ = os.WriteFile(iconFile, assets.Logo, 0644)
-		_ = exec.Command("gtk-update-icon-cache", filepath.Join(home, ".local", "share", "icons", "hicolor")).Run()
+		_ = exec.Command("gtk-update-icon-cache", filepath.Join(home, ".local", "share", "icons", "hicolor")).Run() //nolint:noctx
 	}
 
 	appsDir := filepath.Join(home, ".local", "share", "applications")
-	if err := os.MkdirAll(appsDir, 0755); err != nil {
+	if err := os.MkdirAll(appsDir, 0750); err != nil {
 		return err
 	}
 
@@ -70,13 +70,11 @@ MimeType=x-scheme-handler/mailto;
 		return err
 	}
 
-	// Update desktop database
-	if err := exec.Command("update-desktop-database", appsDir).Run(); err != nil {
-		// Ignore error if command doesn't exist
-	}
+	// Update desktop database (ignore error if command doesn't exist)
+	_ = exec.Command("update-desktop-database", appsDir).Run() //nolint:noctx
 
 	// Try to set xdg-mime default
-	cmd := exec.Command("xdg-mime", "default", "matcha.desktop", "x-scheme-handler/mailto")
+	cmd := exec.Command("xdg-mime", "default", "matcha.desktop", "x-scheme-handler/mailto") //nolint:noctx
 	if err := cmd.Run(); err != nil {
 		return fmt.Errorf("failed to run xdg-mime: %w", err)
 	}
@@ -96,16 +94,16 @@ func setupMailtoDarwin(exe string) error {
 
 	appDir := filepath.Join(home, "Applications", "MatchaMail.app")
 	// Cleanup old version to avoid conflicts
-	os.RemoveAll(appDir)
+	os.RemoveAll(appDir) //nolint:errcheck,gosec
 
 	contentsDir := filepath.Join(appDir, "Contents")
 	macosDir := filepath.Join(contentsDir, "MacOS")
 	resourcesDir := filepath.Join(contentsDir, "Resources")
 
-	if err := os.MkdirAll(macosDir, 0755); err != nil {
+	if err := os.MkdirAll(macosDir, 0750); err != nil {
 		return err
 	}
-	if err := os.MkdirAll(resourcesDir, 0755); err != nil {
+	if err := os.MkdirAll(resourcesDir, 0750); err != nil {
 		return err
 	}
 
@@ -113,8 +111,8 @@ func setupMailtoDarwin(exe string) error {
 	tmpLogo := filepath.Join(os.TempDir(), "matcha_logo.png")
 	if err := os.WriteFile(tmpLogo, assets.Logo, 0644); err == nil {
 		icnsPath := filepath.Join(resourcesDir, "MatchaMail.icns")
-		_ = exec.Command("sips", "-s", "format", "icns", tmpLogo, "--out", icnsPath).Run()
-		os.Remove(tmpLogo)
+		_ = exec.Command("sips", "-s", "format", "icns", tmpLogo, "--out", icnsPath).Run() //nolint:noctx
+		os.Remove(tmpLogo)                                                                 //nolint:errcheck,gosec
 	}
 
 	infoPlist := `<?xml version="1.0" encoding="UTF-8"?>
@@ -164,19 +162,19 @@ func setupMailtoDarwin(exe string) error {
 	if err := os.WriteFile(tmpSwiftFile, []byte(swiftCode), 0644); err != nil {
 		return err
 	}
-	defer os.Remove(tmpSwiftFile)
+	defer os.Remove(tmpSwiftFile) //nolint:errcheck
 
 	exeDest := filepath.Join(macosDir, "MatchaMail")
 
 	// Compile the Swift file
-	cmd := exec.Command("swiftc", "-O", tmpSwiftFile, "-o", exeDest)
+	cmd := exec.Command("swiftc", "-O", tmpSwiftFile, "-o", exeDest) //nolint:noctx
 	if err := cmd.Run(); err != nil {
 		return fmt.Errorf("failed to compile Swift handler app: %w", err)
 	}
 
 	// Register the application
 	lsregister := "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"
-	_ = exec.Command(lsregister, "-f", appDir).Run()
+	_ = exec.Command(lsregister, "-f", appDir).Run() //nolint:noctx
 
 	fmt.Printf("Successfully created %s.\n", appDir)
 

clib/htmlconv.go 🔗

@@ -23,7 +23,7 @@ func HTMLToElements(html string) ([]HTMLElement, bool) {
 	if result.ok == 0 {
 		return nil, false
 	}
-	defer C.free_html_result(&result)
+	defer C.free_html_result(&result) //nolint:gocritic
 
 	count := int(result.count)
 	if count == 0 {

clib/imgconv.go 🔗

@@ -26,7 +26,7 @@ func DecodeToPNG(data []byte) (ImageConvertResult, bool) {
 	if result.ok == 0 {
 		return ImageConvertResult{}, false
 	}
-	defer C.free_image_result(&result)
+	defer C.free_image_result(&result) //nolint:gocritic
 
 	pngData := C.GoBytes(unsafe.Pointer(result.png_data), C.int(result.png_len))
 

clib/macos/appearance.go 🔗

@@ -28,7 +28,7 @@ func GetAppearance() (*MacOSAppearance, error) {
 	if err != nil {
 		return nil, err
 	}
-	defer os.RemoveAll(tmpDir)
+	defer os.RemoveAll(tmpDir) //nolint:errcheck
 
 	swiftFile := filepath.Join(tmpDir, "appearance.swift")
 	if err := os.WriteFile(swiftFile, []byte(appearanceSwift), 0644); err != nil {
@@ -38,13 +38,13 @@ func GetAppearance() (*MacOSAppearance, error) {
 	binFile := filepath.Join(tmpDir, "appearance")
 
 	// Compile
-	cmd := exec.Command("swiftc", swiftFile, "-o", binFile)
+	cmd := exec.Command("swiftc", swiftFile, "-o", binFile) //nolint:noctx
 	if out, err := cmd.CombinedOutput(); err != nil {
 		return nil, fmt.Errorf("failed to compile appearance helper: %w\n%s", err, string(out))
 	}
 
 	// Run
-	out, err := exec.Command(binFile).Output()
+	out, err := exec.Command(binFile).Output() //nolint:noctx
 	if err != nil {
 		return nil, fmt.Errorf("failed to run appearance helper: %w", err)
 	}

clib/macos/badge.go 🔗

@@ -23,7 +23,7 @@ func SetBadge(count int) error {
 	if err != nil {
 		return err
 	}
-	defer os.RemoveAll(tmpDir)
+	defer os.RemoveAll(tmpDir) //nolint:errcheck
 
 	swiftFile := filepath.Join(tmpDir, "badge.swift")
 	if err := os.WriteFile(swiftFile, []byte(badgeSwift), 0644); err != nil {
@@ -33,7 +33,7 @@ func SetBadge(count int) error {
 	binFile := filepath.Join(tmpDir, "badge")
 
 	// Compile
-	cmd := exec.Command("swiftc", swiftFile, "-o", binFile)
+	cmd := exec.Command("swiftc", swiftFile, "-o", binFile) //nolint:noctx
 	if out, err := cmd.CombinedOutput(); err != nil {
 		return fmt.Errorf("failed to compile badge helper: %w\n%s", err, string(out))
 	}
@@ -44,7 +44,7 @@ func SetBadge(count int) error {
 	// To set it for the 'MatchaMail.app', we'd need that app to be running and
 	// listen for a notification, OR we run this compiled tool *inside* the app bundle context.
 
-	err = exec.Command(binFile, strconv.Itoa(count)).Run()
+	err = exec.Command(binFile, strconv.Itoa(count)).Run() //nolint:noctx
 	if err != nil {
 		return fmt.Errorf("failed to set badge: %w", err)
 	}

clib/macos/contacts.go 🔗

@@ -28,7 +28,7 @@ func FetchContacts() ([]MacOSContact, error) {
 	if err != nil {
 		return nil, err
 	}
-	defer os.RemoveAll(tmpDir)
+	defer os.RemoveAll(tmpDir) //nolint:errcheck
 
 	swiftFile := filepath.Join(tmpDir, "contacts.swift")
 	if err := os.WriteFile(swiftFile, []byte(contactsSwift), 0644); err != nil {
@@ -38,13 +38,13 @@ func FetchContacts() ([]MacOSContact, error) {
 	binFile := filepath.Join(tmpDir, "contacts")
 
 	// Compile the Swift helper
-	cmd := exec.Command("swiftc", swiftFile, "-o", binFile)
+	cmd := exec.Command("swiftc", swiftFile, "-o", binFile) //nolint:noctx
 	if out, err := cmd.CombinedOutput(); err != nil {
 		return nil, fmt.Errorf("failed to compile contacts helper: %w\n%s", err, string(out))
 	}
 
 	// Run the helper
-	out, err := exec.Command(binFile).Output()
+	out, err := exec.Command(binFile).Output() //nolint:noctx
 	if err != nil {
 		return nil, fmt.Errorf("failed to run contacts helper: %w", err)
 	}

clib/macos/file_picker.go 🔗

@@ -24,7 +24,7 @@ func OpenFilePicker(initialPath string) ([]string, error) {
 	if err != nil {
 		return nil, err
 	}
-	defer os.RemoveAll(tmpDir)
+	defer os.RemoveAll(tmpDir) //nolint:errcheck
 
 	swiftFile := filepath.Join(tmpDir, "file_picker.swift")
 	if err := os.WriteFile(swiftFile, []byte(filePickerSwift), 0644); err != nil {
@@ -34,7 +34,7 @@ func OpenFilePicker(initialPath string) ([]string, error) {
 	binFile := filepath.Join(tmpDir, "file_picker")
 
 	// Compile
-	cmd := exec.Command("swiftc", swiftFile, "-o", binFile)
+	cmd := exec.Command("swiftc", swiftFile, "-o", binFile) //nolint:noctx
 	if out, err := cmd.CombinedOutput(); err != nil {
 		return nil, fmt.Errorf("failed to compile file picker helper: %w\n%s", err, string(out))
 	}
@@ -44,7 +44,7 @@ func OpenFilePicker(initialPath string) ([]string, error) {
 	if initialPath != "" {
 		args = append(args, initialPath)
 	}
-	out, err := exec.Command(binFile, args...).Output()
+	out, err := exec.Command(binFile, args...).Output() //nolint:noctx
 	if err != nil {
 		// Exit code 1 usually means user cancelled
 		return nil, nil

config/cache.go 🔗

@@ -378,7 +378,7 @@ func SearchContactsForAccount(query, accountID string) []Contact {
 func MigrateContactsCacheUsage(accountIDs []string) error {
 	cache, err := LoadContactsCache()
 	if err != nil {
-		return nil
+		return err
 	}
 
 	changed := false
@@ -539,7 +539,7 @@ func SaveDraft(draft Draft) error {
 func DeleteDraft(id string) error {
 	cache, err := LoadDraftsCache()
 	if err != nil {
-		return nil // No cache, nothing to delete
+		return err
 	}
 
 	var filtered []Draft
@@ -728,14 +728,6 @@ func calculateEmailBodySize(body *CachedEmailBody) int {
 	return size
 }
 
-func calculateTotalCacheSize(cache *EmailBodyCache) int {
-	total := 0
-	for _, b := range cache.Bodies {
-		total += b.SizeBytes
-	}
-	return total
-}
-
 // SaveEmailBody saves or updates a cached email body for a folder.
 func SaveEmailBody(folderName string, body CachedEmailBody, threshold int) error {
 	body.CachedAt = time.Now()
@@ -753,7 +745,7 @@ func PruneEmailBodyCache(folderName string, validUIDs map[uint32]string, thresho
 	cache, err := LoadEmailBodyCache(folderName)
 
 	if err != nil {
-		return nil
+		return err
 	}
 
 	lru := GetLRUInstance(threshold)

config/config.go 🔗

@@ -3,6 +3,7 @@ package config
 import (
 	"crypto/tls"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"log"
 	"os"
@@ -16,6 +17,12 @@ import (
 
 const keyringServiceName = "matcha-email-client"
 
+const (
+	ProviderGmail  = "gmail"
+	ProviderICloud = "icloud"
+	ProviderCustom = "custom"
+)
+
 // Date format presets use human-readable tokens. Supported tokens:
 //
 //	YYYY (4-digit year), YY (2-digit year)
@@ -194,13 +201,13 @@ func translateDateFormat(f string) string {
 // GetIMAPServer returns the IMAP server address for the account.
 func (a *Account) GetIMAPServer() string {
 	switch a.ServiceProvider {
-	case "gmail":
+	case ProviderGmail:
 		return "imap.gmail.com"
 	case "outlook":
 		return "outlook.office365.com"
-	case "icloud":
+	case ProviderICloud:
 		return "imap.mail.me.com"
-	case "custom":
+	case ProviderCustom:
 		return a.IMAPServer
 	default:
 		return ""
@@ -210,9 +217,9 @@ func (a *Account) GetIMAPServer() string {
 // GetIMAPPort returns the IMAP port for the account.
 func (a *Account) GetIMAPPort() int {
 	switch a.ServiceProvider {
-	case "gmail", "outlook", "icloud":
+	case ProviderGmail, "outlook", "icloud":
 		return 993
-	case "custom":
+	case ProviderCustom:
 		if a.IMAPPort != 0 {
 			return a.IMAPPort
 		}
@@ -225,13 +232,13 @@ func (a *Account) GetIMAPPort() int {
 // GetSMTPServer returns the SMTP server address for the account.
 func (a *Account) GetSMTPServer() string {
 	switch a.ServiceProvider {
-	case "gmail":
+	case ProviderGmail:
 		return "smtp.gmail.com"
 	case "outlook":
 		return "smtp.office365.com"
-	case "icloud":
+	case ProviderICloud:
 		return "smtp.mail.me.com"
-	case "custom":
+	case ProviderCustom:
 		return a.SMTPServer
 	default:
 		return ""
@@ -249,9 +256,9 @@ func (a *Account) GetClientSessionCache() tls.ClientSessionCache {
 // GetSMTPPort returns the SMTP port for the account.
 func (a *Account) GetSMTPPort() int {
 	switch a.ServiceProvider {
-	case "gmail", "outlook", "icloud":
+	case ProviderGmail, "outlook", "icloud":
 		return 587
-	case "custom":
+	case ProviderCustom:
 		if a.SMTPPort != 0 {
 			return a.SMTPPort
 		}
@@ -657,18 +664,19 @@ func LoadConfig() (*Config, error) {
 			return nil, fmt.Errorf("account %q: invalid pgp_key_source %q (must be \"file\" or \"yubikey\")", acc.Name, acc.PGPKeySource)
 		}
 
-		if secureMode {
+		switch {
+		case secureMode:
 			// In secure mode, passwords and PINs are stored in the encrypted config JSON
 			acc.Password = rawAcc.Password
 			acc.PGPPIN = rawAcc.PGPPIN
-		} else if rawAcc.Password != "" {
+		case rawAcc.Password != "":
 			// Found a plain-text password! Move it to the OS Keyring.
 			if err := keyring.Set(keyringServiceName, rawAcc.Email, rawAcc.Password); err != nil {
 				log.Printf("matcha: failed to migrate password for %s into keyring: %v", rawAcc.Email, err)
 			}
 			acc.Password = rawAcc.Password
 			needsMigration = true
-		} else {
+		default:
 			// No plaintext password in JSON, fetch from Keyring as normal.
 			if pwd, err := keyring.Get(keyringServiceName, acc.Email); err == nil {
 				acc.Password = pwd
@@ -724,11 +732,11 @@ func (c *Config) RemoveAccount(id string) bool {
 			// missing entry is expected and not worth logging (keyring.Get is
 			// what we rely on elsewhere to detect that), but any other error
 			// means we failed to clean up a still-reachable secret.
-			if err := keyring.Delete(keyringServiceName, acc.Email); err != nil && err != keyring.ErrNotFound {
+			if err := keyring.Delete(keyringServiceName, acc.Email); err != nil && !errors.Is(err, keyring.ErrNotFound) {
 				log.Printf("matcha: failed to delete password for %s from keyring: %v", acc.Email, err)
 			}
 			// Delete PGP PIN from OS Keyring if present
-			if err := keyring.Delete(keyringServiceName, acc.Email+":pgp-pin"); err != nil && err != keyring.ErrNotFound {
+			if err := keyring.Delete(keyringServiceName, acc.Email+":pgp-pin"); err != nil && !errors.Is(err, keyring.ErrNotFound) {
 				log.Printf("matcha: failed to delete PGP PIN for %s from keyring: %v", acc.Email, err)
 			}
 

config/encryption.go 🔗

@@ -282,7 +282,7 @@ func DisableSecureMode(cfg *Config) error {
 		if err != nil {
 			continue // File may not be encrypted
 		}
-		if err := os.WriteFile(f, plain, 0600); err != nil {
+		if err := os.WriteFile(f, plain, 0600); err != nil { //nolint:gosec
 			return err
 		}
 	}
@@ -324,13 +324,13 @@ func SecureReadFile(path string) ([]byte, error) {
 func SecureWriteFile(path string, data []byte, perm os.FileMode) error {
 	key := GetSessionKey()
 	if key == nil {
-		return os.WriteFile(path, data, perm)
+		return os.WriteFile(path, data, perm) //nolint:gosec
 	}
 	encrypted, err := Encrypt(data, key)
 	if err != nil {
 		return err
 	}
-	return os.WriteFile(path, encrypted, perm)
+	return os.WriteFile(path, encrypted, perm) //nolint:gosec
 }
 
 // reEncryptCacheFiles reads all plain cache/data files (excluding config.json) and writes them encrypted.

config/keybinds.go 🔗

@@ -8,6 +8,8 @@ import (
 	"path/filepath"
 )
 
+const keyDelete = "delete"
+
 //go:embed default_keybinds.json
 var defaultKeybindsJSON []byte
 
@@ -144,7 +146,7 @@ func ValidateKeybinds(kb KeybindsConfig) []string {
 	check("inbox", map[string]string{
 		"visual_mode":     kb.Inbox.VisualMode,
 		"toggle_threaded": kb.Inbox.ToggleThreaded,
-		"delete":          kb.Inbox.Delete,
+		keyDelete:         kb.Inbox.Delete,
 		"archive":         kb.Inbox.Archive,
 		"refresh":         kb.Inbox.Refresh,
 		"search":          kb.Inbox.Search,
@@ -156,7 +158,7 @@ func ValidateKeybinds(kb KeybindsConfig) []string {
 	check("email", map[string]string{
 		"reply":             kb.Email.Reply,
 		"forward":           kb.Email.Forward,
-		"delete":            kb.Email.Delete,
+		keyDelete:           kb.Email.Delete,
 		"archive":           kb.Email.Archive,
 		"toggle_images":     kb.Email.ToggleImages,
 		"rsvp_accept":       kb.Email.RsvpAccept,
@@ -168,7 +170,7 @@ func ValidateKeybinds(kb KeybindsConfig) []string {
 		"external_editor": kb.Composer.ExternalEditor,
 		"next_field":      kb.Composer.NextField,
 		"prev_field":      kb.Composer.PrevField,
-		"delete":          kb.Composer.Delete,
+		keyDelete:         kb.Composer.Delete,
 	})
 	check("folder", map[string]string{
 		"next_folder":   kb.Folder.NextFolder,
@@ -178,8 +180,8 @@ func ValidateKeybinds(kb KeybindsConfig) []string {
 		"focus_inbox":   kb.Folder.FocusInbox,
 	})
 	check("drafts", map[string]string{
-		"open":   kb.Drafts.Open,
-		"delete": kb.Drafts.Delete,
+		"open":    kb.Drafts.Open,
+		keyDelete: kb.Drafts.Delete,
 	})
 
 	return conflicts

config/lru.go 🔗

@@ -41,7 +41,6 @@ func GetLRUInstance(threshold int) *LRU {
 			if err := lru.LoadFromDisk(); err != nil {
 				log.Printf("Failed to load LRU from disk: %v\n", err)
 			}
-
 		})
 
 	lru.mu.Lock()
@@ -65,12 +64,12 @@ func removeBodyFromDisk(folder string, uid uint32, accountID string) error {
 	cache, err := LoadEmailBodyCache(folder)
 
 	if err != nil {
-		return nil
+		return err
 	}
 
 	kept := cache.Bodies[:0]
 	for _, b := range cache.Bodies {
-		if !(b.UID == uid && b.AccountID == accountID) {
+		if b.UID != uid || b.AccountID != accountID {
 			kept = append(kept, b)
 		}
 	}

config/oauth.go 🔗

@@ -49,7 +49,7 @@ func GetOAuth2Token(email string) (string, error) {
 		return "", err
 	}
 
-	cmd := exec.Command("python3", script, "token", email)
+	cmd := exec.Command("python3", script, "token", email) //nolint:noctx
 	cmd.Stderr = os.Stderr
 	out, err := cmd.Output()
 	if err != nil {
@@ -82,7 +82,7 @@ func RunOAuth2Flow(email, provider, clientID, clientSecret string) error {
 		args = append(args, "--client-id", clientID, "--client-secret", clientSecret)
 	}
 
-	cmd := exec.Command("python3", args...)
+	cmd := exec.Command("python3", args...) //nolint:noctx
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	return cmd.Run()

config/signature.go 🔗

@@ -98,7 +98,7 @@ func SaveSignatureForAccount(accountID, signature string) error {
 	}
 	if signature == "" {
 		// Remove the file to fall back to global
-		os.Remove(path)
+		os.Remove(path) //nolint:errcheck,gosec
 		return nil
 	}
 	return SecureWriteFile(path, []byte(signature), 0600)

daemon/daemon.go 🔗

@@ -18,6 +18,8 @@ import (
 	"github.com/floatpane/matcha/notify"
 )
 
+const inboxFolder = "INBOX"
+
 // Daemon is the long-running background process that manages email
 // connections, caching, sync, and notifications.
 type Daemon struct {
@@ -83,22 +85,22 @@ func (d *Daemon) Run() error {
 	if err := WritePID(pidPath); err != nil {
 		return fmt.Errorf("write PID file: %w", err)
 	}
-	defer RemovePID(pidPath)
+	defer RemovePID(pidPath) //nolint:errcheck
 
 	// Remove stale socket file.
 	sockPath := daemonrpc.SocketPath()
-	os.Remove(sockPath)
+	os.Remove(sockPath) //nolint:errcheck,gosec
 
 	// Listen on Unix domain socket.
 	var err error
-	d.listener, err = net.Listen("unix", sockPath)
+	d.listener, err = net.Listen("unix", sockPath) //nolint:noctx
 	if err != nil {
 		return fmt.Errorf("listen: %w", err)
 	}
-	defer d.listener.Close()
+	defer d.listener.Close() //nolint:errcheck
 
 	// Set socket permissions (owner only).
-	os.Chmod(sockPath, 0700)
+	os.Chmod(sockPath, 0700) //nolint:errcheck,gosec
 
 	log.Printf("daemon: listening on %s (PID %d)", sockPath, os.Getpid())
 
@@ -125,7 +127,7 @@ func (d *Daemon) Run() error {
 
 	// Cleanup.
 	log.Println("daemon: shutting down")
-	d.listener.Close()
+	d.listener.Close() //nolint:errcheck,gosec
 	d.idleWatcher.StopAll()
 	cancel()
 	d.closeAllClients()
@@ -215,7 +217,7 @@ func (d *Daemon) acceptLoop() {
 
 func (d *Daemon) handleClient(conn *daemonrpc.Conn) {
 	defer d.removeClient(conn)
-	defer conn.Close()
+	defer conn.Close() //nolint:errcheck
 
 	for {
 		msg, err := conn.ReceiveMessage()
@@ -252,7 +254,7 @@ func (d *Daemon) closeAllClients() {
 	d.mu.Lock()
 	defer d.mu.Unlock()
 	for conn := range d.clients {
-		conn.Close()
+		conn.Close() //nolint:errcheck,gosec
 	}
 	d.clients = make(map[*daemonrpc.Conn]struct{})
 }
@@ -339,9 +341,9 @@ func (d *Daemon) syncAllAccounts(ctx context.Context) {
 		default:
 		}
 
-		d.broadcastToSubscribers(acct.ID, "INBOX", daemonrpc.EventSyncStarted, daemonrpc.SyncStartedEvent{
+		d.broadcastToSubscribers(acct.ID, inboxFolder, daemonrpc.EventSyncStarted, daemonrpc.SyncStartedEvent{
 			AccountID: acct.ID,
-			Folder:    "INBOX",
+			Folder:    inboxFolder,
 		})
 
 		p, err := d.getProvider(acct.ID)
@@ -349,18 +351,18 @@ func (d *Daemon) syncAllAccounts(ctx context.Context) {
 			continue
 		}
 
-		emails, err := p.FetchEmails(ctx, "INBOX", 50, 0)
+		emails, err := p.FetchEmails(ctx, inboxFolder, 50, 0)
 		if err != nil {
 			log.Printf("daemon: sync %s failed: %v", acct.Email, err)
-			d.broadcastToSubscribers(acct.ID, "INBOX", daemonrpc.EventSyncError, daemonrpc.SyncErrorEvent{
+			d.broadcastToSubscribers(acct.ID, inboxFolder, daemonrpc.EventSyncError, daemonrpc.SyncErrorEvent{
 				AccountID: acct.ID,
-				Folder:    "INBOX",
+				Folder:    inboxFolder,
 				Error:     err.Error(),
 			})
 			continue
 		}
 
-		oldCached, _ := config.LoadFolderEmailCache("INBOX")
+		oldCached, _ := config.LoadFolderEmailCache(inboxFolder)
 		oldUIDs := make(map[uint32]struct{}, len(oldCached))
 		for _, e := range oldCached {
 			if e.AccountID == acct.ID {
@@ -384,13 +386,13 @@ func (d *Daemon) syncAllAccounts(ctx context.Context) {
 				IsRead:     e.IsRead,
 			})
 		}
-		if err := d.updateFolderCache("INBOX", acct.ID, cached); err != nil {
+		if err := d.updateFolderCache(inboxFolder, acct.ID, cached); err != nil {
 			log.Printf("daemon: cache update for INBOX failed: %v", err)
 		}
 
-		d.broadcastToSubscribers(acct.ID, "INBOX", daemonrpc.EventSyncComplete, daemonrpc.SyncCompleteEvent{
+		d.broadcastToSubscribers(acct.ID, inboxFolder, daemonrpc.EventSyncComplete, daemonrpc.SyncCompleteEvent{
 			AccountID:  acct.ID,
-			Folder:     "INBOX",
+			Folder:     inboxFolder,
 			EmailCount: len(emails),
 		})
 
@@ -408,7 +410,7 @@ func (d *Daemon) syncAllAccounts(ctx context.Context) {
 
 		if noClients && newCount > 0 {
 			if !d.config.DisableNotifications {
-				go notify.Send("Matcha", fmt.Sprintf("New mail for %s", acct.FetchEmail))
+				go notify.Send("Matcha", fmt.Sprintf("New mail for %s", acct.FetchEmail)) //nolint:errcheck
 			}
 		}
 	}
@@ -429,7 +431,7 @@ func (d *Daemon) startIdleWatchers() {
 		if protocol != "imap" {
 			continue
 		}
-		d.idleWatcher.Watch(acct, "INBOX")
+		d.idleWatcher.Watch(acct, inboxFolder)
 		log.Printf("daemon: IDLE watcher started for %s", acct.Email)
 	}
 }
@@ -456,7 +458,7 @@ func (d *Daemon) idleEventLoop() {
 				if acct := d.config.GetAccountByID(update.AccountID); acct != nil {
 					accountName = acct.Email
 				}
-				go notify.Send("Matcha", fmt.Sprintf("New mail in %s (%s)", update.FolderName, accountName))
+				go notify.Send("Matcha", fmt.Sprintf("New mail in %s (%s)", update.FolderName, accountName)) //nolint:errcheck
 			}
 
 			// Broadcast to subscribed clients.

daemon/handler.go 🔗

@@ -51,7 +51,7 @@ func (d *Daemon) handleRequest(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	case daemonrpc.MethodUnsubscribe:
 		d.handleUnsubscribe(conn, req)
 	default:
-		conn.SendError(req.ID, daemonrpc.ErrCodeNotFound, fmt.Sprintf("unknown method: %s", req.Method))
+		conn.SendError(req.ID, daemonrpc.ErrCodeNotFound, fmt.Sprintf("unknown method: %s", req.Method)) //nolint:errcheck,gosec
 	}
 }
 
@@ -66,18 +66,18 @@ func decodeParams[T any](req *daemonrpc.Request) (T, error) {
 }
 
 func (d *Daemon) handlePing(conn *daemonrpc.Conn, req *daemonrpc.Request) {
-	conn.SendResponse(req.ID, daemonrpc.PingResult{Pong: true})
+	conn.SendResponse(req.ID, daemonrpc.PingResult{Pong: true}) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleGetStatus(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	d.mu.RLock()
-	var accounts []string
+	accounts := make([]string, 0, len(d.config.Accounts))
 	for _, acct := range d.config.Accounts {
 		accounts = append(accounts, acct.Email)
 	}
 	d.mu.RUnlock()
 
-	conn.SendResponse(req.ID, daemonrpc.StatusResult{
+	conn.SendResponse(req.ID, daemonrpc.StatusResult{ //nolint:errcheck,gosec
 		Running:  true,
 		Uptime:   int64(time.Since(d.startTime).Seconds()),
 		Accounts: accounts,
@@ -89,7 +89,7 @@ func (d *Daemon) handleGetAccounts(conn *daemonrpc.Conn, req *daemonrpc.Request)
 	d.mu.RLock()
 	defer d.mu.RUnlock()
 
-	var infos []daemonrpc.AccountInfo
+	infos := make([]daemonrpc.AccountInfo, 0, len(d.config.Accounts))
 	for _, acct := range d.config.Accounts {
 		protocol := acct.Protocol
 		if protocol == "" {
@@ -102,27 +102,27 @@ func (d *Daemon) handleGetAccounts(conn *daemonrpc.Conn, req *daemonrpc.Request)
 			Protocol: protocol,
 		})
 	}
-	conn.SendResponse(req.ID, infos)
+	conn.SendResponse(req.ID, infos) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleReloadConfig(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	if err := d.ReloadConfig(); err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
-	conn.SendResponse(req.ID, true)
+	conn.SendResponse(req.ID, true) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleFetchEmails(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.FetchEmailsParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
 	p, err := d.getProvider(params.AccountID)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -131,23 +131,23 @@ func (d *Daemon) handleFetchEmails(conn *daemonrpc.Conn, req *daemonrpc.Request)
 
 	emails, err := p.FetchEmails(ctx, params.Folder, params.Limit, params.Offset)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
-	conn.SendResponse(req.ID, emails)
+	conn.SendResponse(req.ID, emails) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleFetchEmailBody(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.FetchEmailBodyParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
 	p, err := d.getProvider(params.AccountID)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -156,7 +156,7 @@ func (d *Daemon) handleFetchEmailBody(conn *daemonrpc.Conn, req *daemonrpc.Reque
 
 	body, mimeType, attachments, err := p.FetchEmailBody(ctx, params.Folder, params.UID)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -171,7 +171,7 @@ func (d *Daemon) handleFetchEmailBody(conn *daemonrpc.Conn, req *daemonrpc.Reque
 		})
 	}
 
-	conn.SendResponse(req.ID, daemonrpc.FetchEmailBodyResult{
+	conn.SendResponse(req.ID, daemonrpc.FetchEmailBodyResult{ //nolint:errcheck,gosec
 		Body:         body,
 		BodyMIMEType: mimeType,
 		Attachments:  attInfos,
@@ -181,13 +181,13 @@ func (d *Daemon) handleFetchEmailBody(conn *daemonrpc.Conn, req *daemonrpc.Reque
 func (d *Daemon) handleDeleteEmails(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.DeleteEmailsParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
 	p, err := d.getProvider(params.AccountID)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -195,22 +195,22 @@ func (d *Daemon) handleDeleteEmails(conn *daemonrpc.Conn, req *daemonrpc.Request
 	defer cancel()
 
 	if err := p.DeleteEmails(ctx, params.Folder, params.UIDs); err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
-	conn.SendResponse(req.ID, true)
+	conn.SendResponse(req.ID, true) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleArchiveEmails(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.ArchiveEmailsParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
 	p, err := d.getProvider(params.AccountID)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -218,22 +218,22 @@ func (d *Daemon) handleArchiveEmails(conn *daemonrpc.Conn, req *daemonrpc.Reques
 	defer cancel()
 
 	if err := p.ArchiveEmails(ctx, params.Folder, params.UIDs); err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
-	conn.SendResponse(req.ID, true)
+	conn.SendResponse(req.ID, true) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleMoveEmails(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.MoveEmailsParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
 	p, err := d.getProvider(params.AccountID)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -241,22 +241,22 @@ func (d *Daemon) handleMoveEmails(conn *daemonrpc.Conn, req *daemonrpc.Request)
 	defer cancel()
 
 	if err := p.MoveEmails(ctx, params.UIDs, params.SourceFolder, params.DestFolder); err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
-	conn.SendResponse(req.ID, true)
+	conn.SendResponse(req.ID, true) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleMarkRead(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.MarkReadParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
 	p, err := d.getProvider(params.AccountID)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -274,19 +274,19 @@ func (d *Daemon) handleMarkRead(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 			log.Printf("daemon: mark read=%v %d failed: %v", params.Read, uid, err)
 		}
 	}
-	conn.SendResponse(req.ID, true)
+	conn.SendResponse(req.ID, true) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleFetchFolders(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.FetchFoldersParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
 	p, err := d.getProvider(params.AccountID)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -295,16 +295,16 @@ func (d *Daemon) handleFetchFolders(conn *daemonrpc.Conn, req *daemonrpc.Request
 
 	folders, err := p.FetchFolders(ctx)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeInternal, err.Error()) //nolint:errcheck,gosec
 		return
 	}
-	conn.SendResponse(req.ID, folders)
+	conn.SendResponse(req.ID, folders) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleRefreshFolder(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.RefreshFolderParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -327,10 +327,7 @@ func (d *Daemon) handleRefreshFolder(conn *daemonrpc.Conn, req *daemonrpc.Reques
 			return
 		}
 
-		d.broadcastToSubscribers(params.AccountID, params.Folder, daemonrpc.EventSyncStarted, daemonrpc.SyncStartedEvent{
-			AccountID: params.AccountID,
-			Folder:    params.Folder,
-		})
+		d.broadcastToSubscribers(params.AccountID, params.Folder, daemonrpc.EventSyncStarted, daemonrpc.SyncStartedEvent(params))
 
 		ctx, cancel := context.WithTimeout(context.Background(), fetchTimeout)
 		defer cancel()
@@ -352,13 +349,13 @@ func (d *Daemon) handleRefreshFolder(conn *daemonrpc.Conn, req *daemonrpc.Reques
 		})
 	}()
 
-	conn.SendResponse(req.ID, true)
+	conn.SendResponse(req.ID, true) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleSubscribe(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.SubscribeParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -372,13 +369,13 @@ func (d *Daemon) handleSubscribe(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	d.subMu.Unlock()
 
 	log.Printf("daemon: client subscribed to %s", key)
-	conn.SendResponse(req.ID, true)
+	conn.SendResponse(req.ID, true) //nolint:errcheck,gosec
 }
 
 func (d *Daemon) handleUnsubscribe(conn *daemonrpc.Conn, req *daemonrpc.Request) {
 	params, err := decodeParams[daemonrpc.UnsubscribeParams](req)
 	if err != nil {
-		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error())
+		conn.SendError(req.ID, daemonrpc.ErrCodeParse, err.Error()) //nolint:errcheck,gosec
 		return
 	}
 
@@ -390,5 +387,5 @@ func (d *Daemon) handleUnsubscribe(conn *daemonrpc.Conn, req *daemonrpc.Request)
 	}
 	d.subMu.Unlock()
 
-	conn.SendResponse(req.ID, true)
+	conn.SendResponse(req.ID, true) //nolint:errcheck,gosec
 }

daemonclient/client.go 🔗

@@ -23,7 +23,7 @@ type Client struct {
 // Dial connects to the daemon socket.
 func Dial() (*Client, error) {
 	sockPath := daemonrpc.SocketPath()
-	conn, err := net.Dial("unix", sockPath)
+	conn, err := net.Dial("unix", sockPath) //nolint:noctx
 	if err != nil {
 		return nil, fmt.Errorf("connect to daemon: %w", err)
 	}

daemonclient/service.go 🔗

@@ -68,7 +68,7 @@ func tryConnect() *daemonService {
 		return nil
 	}
 	if err := client.Ping(); err != nil {
-		client.Close()
+		client.Close() //nolint:errcheck,gosec
 		return nil
 	}
 	return &daemonService{client: client}
@@ -80,7 +80,7 @@ func autoStartDaemon() error {
 		return err
 	}
 
-	cmd := exec.Command(exe, "daemon", "run")
+	cmd := exec.Command(exe, "daemon", "run") //nolint:noctx
 	cmd.Stdout = nil
 	cmd.Stderr = nil
 	cmd.Stdin = nil
@@ -361,7 +361,7 @@ func (s *directService) IsDaemon() bool { return false }
 
 func (s *directService) Close() error {
 	for _, p := range s.providers {
-		p.Close()
+		p.Close() //nolint:errcheck,gosec
 	}
 	close(s.events)
 	return nil

daemonrpc/protocol.go 🔗

@@ -50,19 +50,20 @@ func DecodeMessage(raw json.RawMessage) (Message, error) {
 	}
 
 	var m Message
-	if probe.Type != "" {
+	switch {
+	case probe.Type != "":
 		var ev Event
 		if err := json.Unmarshal(raw, &ev); err != nil {
 			return m, err
 		}
 		m.Event = &ev
-	} else if probe.Method != "" {
+	case probe.Method != "":
 		var req Request
 		if err := json.Unmarshal(raw, &req); err != nil {
 			return m, err
 		}
 		m.Request = &req
-	} else {
+	default:
 		var resp Response
 		if err := json.Unmarshal(raw, &resp); err != nil {
 			return m, err

fetcher/fetcher.go 🔗

@@ -43,10 +43,16 @@ var (
 	debugIMAPOnce sync.Once
 )
 
+const (
+	mimeTextPlain = "text/plain"
+	mimeTextHTML  = "text/html"
+	partExtracted = "extracted"
+)
+
 func getDebugIMAPWriter() io.Writer {
 	debugIMAPOnce.Do(func() {
 		if path := os.Getenv("DEBUG_IMAP"); path != "" {
-			f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
+			f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) //nolint:gosec
 			if err == nil {
 				debugIMAPFile = f
 			}
@@ -84,7 +90,7 @@ type Email struct {
 	ReplyTo      []string
 	Subject      string
 	Body         string
-	BodyMIMEType string // "text/html" or "text/plain"; empty when unknown (legacy cache rows). Lets the renderer skip markdown→HTML for already-HTML bodies.
+	BodyMIMEType string // mimeTextHTML or mimeTextPlain; empty when unknown (legacy cache rows). Lets the renderer skip markdown→HTML for already-HTML bodies.
 	Date         time.Time
 	IsRead       bool
 	MessageID    string
@@ -374,7 +380,7 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options)
 	options := &imapclient.Options{
 		TLSConfig: &tls.Config{
 			ServerName:         imapServer,
-			InsecureSkipVerify: account.Insecure,
+			InsecureSkipVerify: account.Insecure, //nolint:gosec
 			MinVersion:         tls.VersionTLS12,
 			ClientSessionCache: account.GetClientSessionCache(),
 			VerifyConnection: func(cs tls.ConnectionState) error {
@@ -409,7 +415,7 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options)
 	}
 
 	if err := c.WaitGreeting(); err != nil {
-		c.Close()
+		c.Close() //nolint:errcheck,gosec
 		return nil, err
 	}
 
@@ -433,7 +439,7 @@ func connectWithOptions(account *config.Account, extraOpts *imapclient.Options)
 
 func getSentMailbox(account *config.Account) string {
 	switch account.ServiceProvider {
-	case "gmail":
+	case config.ProviderGmail:
 		return "[Gmail]/Sent Mail"
 	case "outlook":
 		return "Sent Items"
@@ -447,7 +453,7 @@ func getSentMailbox(account *config.Account) string {
 // getMailboxByAttr finds a mailbox with the given IMAP attribute (e.g., \All, \Sent, \Trash).
 func getMailboxByAttr(c *imapclient.Client, attr imap.MailboxAttr) (string, error) {
 	listCmd := c.List("", "*", nil)
-	defer listCmd.Close()
+	defer listCmd.Close() //nolint:errcheck
 
 	var foundMailbox string
 	for {
@@ -479,7 +485,7 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u
 	if err != nil {
 		return nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	selectData, err := c.Select(mailbox, nil).Wait()
 	if err != nil {
@@ -493,12 +499,10 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u
 	var allEmails []Email
 
 	// Start from the top minus offset
-	cursor := uint32(0)
-	if selectData.NumMessages > offset {
-		cursor = selectData.NumMessages - offset
-	} else {
+	if selectData.NumMessages <= offset {
 		return []Email{}, nil
 	}
+	cursor := selectData.NumMessages - offset
 
 	// Determine if we should filter
 	fetchEmail := strings.ToLower(strings.TrimSpace(account.FetchEmail))
@@ -523,8 +527,8 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u
 		}
 
 		from := uint32(1)
-		if cursor > uint32(chunkSize) {
-			from = cursor - uint32(chunkSize) + 1
+		if cursor > chunkSize {
+			from = cursor - chunkSize + 1
 		}
 
 		var seqset imap.SeqSet
@@ -568,9 +572,10 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u
 			}
 
 			matched := false
-			if account.CatchAll {
+			switch {
+			case account.CatchAll:
 				matched = true
-			} else if isSentMailbox {
+			case isSentMailbox:
 				var senderEmail string
 				if len(msg.Envelope.From) > 0 {
 					senderEmail = msg.Envelope.From[0].Addr()
@@ -578,7 +583,7 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u
 				if addressMatches(senderEmail, fetchEmail, account) {
 					matched = true
 				}
-			} else {
+			default:
 				for _, r := range toAddrList {
 					if addressMatches(r, fetchEmail, account) {
 						matched = true
@@ -630,15 +635,15 @@ func FetchMailboxEmails(account *config.Account, mailbox string, limit, offset u
 }
 
 // FetchEmailBodyFromMailbox returns the chosen body, its MIME type
-// ("text/html" or "text/plain"; empty if it could not be resolved), the
+// (mimeTextHTML or mimeTextPlain; empty if it could not be resolved), the
 // parsed attachments, and any error. The MIME type lets the renderer
 // skip the markdown→HTML pre-pass for already-HTML bodies.
-func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint32) (string, string, []Attachment, error) {
+func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint32) (string, string, []Attachment, error) { //nolint:gocyclo
 	c, err := connect(account)
 	if err != nil {
 		return "", "", nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if _, err := c.Select(mailbox, nil).Wait(); err != nil {
 		return "", "", nil, err
@@ -713,7 +718,7 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 	// still letting markdown error messages render formatted.
 	var extractedBodyMIMEType string
 
-	var checkPart func(part *imap.BodyStructureSinglePart, partID string)
+	var checkPart func(part *imap.BodyStructureSinglePart, partID string) //nolint:staticcheck
 	checkPart = func(part *imap.BodyStructureSinglePart, partID string) {
 		// Check for text content (prefer html over plain)
 		if strings.EqualFold(part.Type, "text") {
@@ -775,8 +780,8 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 
 			if err != nil {
 				extractedBody = fmt.Sprintf("**S/MIME Error:** Failed to fetch encrypted part from IMAP server: %v\n", err)
-				extractedBodyMIMEType = "text/plain"
-				htmlPartID = "extracted"
+				extractedBodyMIMEType = mimeTextPlain
+				htmlPartID = partExtracted
 			} else {
 				p7, parseErr := pkcs7.Parse(data)
 				if parseErr != nil {
@@ -791,8 +796,8 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 
 				if parseErr != nil {
 					extractedBody = fmt.Sprintf("**S/MIME Error:** Failed to parse PKCS7 payload: %v\n", parseErr)
-					extractedBodyMIMEType = "text/plain"
-					htmlPartID = "extracted"
+					extractedBodyMIMEType = mimeTextPlain
+					htmlPartID = partExtracted
 				} else {
 					var innerBytes []byte
 					isEncrypted, isOpaqueSigned, smimeTrusted := false, false, false
@@ -873,7 +878,7 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 									continue
 								}
 
-								if disp == "attachment" || disp == "inline" || (!strings.HasPrefix(cType, "multipart/") && cType != "text/plain" && cType != "text/html") {
+								if disp == "attachment" || disp == "inline" || (!strings.HasPrefix(cType, "multipart/") && cType != mimeTextPlain && cType != mimeTextHTML) {
 									fn := dParams["filename"]
 									if fn == "" {
 										_, cp, _ := mime.ParseMediaType(p.Header.Get("Content-Type"))
@@ -883,21 +888,21 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 										Filename: fn, Data: b, MIMEType: cType, Inline: disp == "inline",
 									})
 								} else {
-									if cType == "text/html" {
+									if cType == mimeTextHTML {
 										extractedBody = string(b)
-										extractedBodyMIMEType = "text/html"
-										htmlPartID = "extracted" // Skip IMAP fetch
-									} else if cType == "text/plain" && extractedBody == "" {
+										extractedBodyMIMEType = mimeTextHTML
+										htmlPartID = partExtracted // Skip IMAP fetch
+									} else if cType == mimeTextPlain && extractedBody == "" {
 										extractedBody = string(b)
-										extractedBodyMIMEType = "text/plain"
-										plainPartID = "extracted"
+										extractedBodyMIMEType = mimeTextPlain
+										plainPartID = partExtracted
 									}
 								}
 							}
 						} else {
 							extractedBody = fmt.Sprintf("**S/MIME Error:** Failed to read inner decrypted MIME: %v\n\n```\n%s\n```", err, string(innerBytes))
-							extractedBodyMIMEType = "text/plain"
-							htmlPartID = "extracted"
+							extractedBodyMIMEType = mimeTextPlain
+							htmlPartID = partExtracted
 						}
 
 						attachments = append(attachments, Attachment{
@@ -907,11 +912,10 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 							IsSMIMEEncrypted: isEncrypted,
 						})
 						return // Stop checking IMAP structure, we hijacked it
-					} else {
-						extractedBody = fmt.Sprintf("**S/MIME Decryption Failed:** %s\n", decryptionErr)
-						extractedBodyMIMEType = "text/plain"
-						htmlPartID = "extracted"
 					}
+					extractedBody = fmt.Sprintf("**S/MIME Decryption Failed:** %s\n", decryptionErr)
+					extractedBodyMIMEType = mimeTextPlain
+					htmlPartID = partExtracted
 				}
 			}
 		}
@@ -967,7 +971,7 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 									if err := p7.VerifyWithChain(roots); err == nil {
 										att.SMIMEVerified = true
 									} else {
-										p7.Content = append(canonical, '\r', '\n')
+										p7.Content = append(canonical, '\r', '\n') //nolint:gocritic
 										if err := p7.VerifyWithChain(roots); err == nil {
 											att.SMIMEVerified = true
 										} else {
@@ -987,13 +991,9 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 		}
 
 		// === PGP ENCRYPTED MESSAGE DETECTION ===
-		if mimeType == "application/pgp-encrypted" || (mimeType == "multipart/encrypted" && strings.Contains(part.Subtype, "pgp")) {
-			// PGP encrypted messages typically have two parts:
-			// 1. Version info (application/pgp-encrypted)
-			// 2. Encrypted data (application/octet-stream)
-			// We'll handle decryption when we find the encrypted data part
-			// Skip this part and continue processing
-		}
+		// PGP encrypted messages have two parts: version info and encrypted data.
+		// We handle decryption when we find the encrypted data part (application/octet-stream).
+		// Skip the version info part (application/pgp-encrypted) and continue processing.
 
 		// Detect encrypted data part of PGP message
 		if strings.Contains(filename, ".asc") || (mimeType == "application/octet-stream" && part.Encoding == "7bit") {
@@ -1009,25 +1009,24 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 						if err == nil {
 							for {
 								p, err := mr.NextPart()
-								if err == io.EOF {
+								if errors.Is(err, io.EOF) {
 									break
 								}
 								if err != nil {
 									break
 								}
 
-								switch h := p.Header.(type) {
-								case *mail.InlineHeader:
+								if h, ok := p.Header.(*mail.InlineHeader); ok {
 									ct, _, _ := h.ContentType()
-									if strings.HasPrefix(ct, "text/html") {
+									if strings.HasPrefix(ct, mimeTextHTML) {
 										body, _ := io.ReadAll(p.Body)
 										extractedBody = string(body)
-										extractedBodyMIMEType = "text/html"
+										extractedBodyMIMEType = mimeTextHTML
 										htmlPartID = "decrypted"
-									} else if strings.HasPrefix(ct, "text/plain") && extractedBody == "" {
+									} else if strings.HasPrefix(ct, mimeTextPlain) && extractedBody == "" {
 										body, _ := io.ReadAll(p.Body)
 										extractedBody = string(body)
-										extractedBodyMIMEType = "text/plain"
+										extractedBodyMIMEType = mimeTextPlain
 										plainPartID = "decrypted"
 									}
 								}
@@ -1042,19 +1041,19 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 						}
 					} else {
 						extractedBody = fmt.Sprintf("**PGP Decryption Failed:** %s\n", err)
-						extractedBodyMIMEType = "text/plain"
-						htmlPartID = "extracted"
+						extractedBodyMIMEType = mimeTextPlain
+						htmlPartID = partExtracted
 					}
 				} else {
 					extractedBody = "**PGP Encrypted:** Private key not configured\n"
-					extractedBodyMIMEType = "text/plain"
-					htmlPartID = "extracted"
+					extractedBodyMIMEType = mimeTextPlain
+					htmlPartID = partExtracted
 				}
 			}
 		}
 
 		// === PGP DETACHED SIGNATURE VERIFICATION ===
-		if filename == "signature.asc" || mimeType == "application/pgp-signature" {
+		if filename == "signature.asc" || mimeType == "application/pgp-signature" { //nolint:gocritic
 			att := Attachment{
 				Filename:       filename,
 				PartID:         partID,
@@ -1159,11 +1158,11 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 	if htmlPartID != "" {
 		textPartID = htmlPartID
 		textPartEncoding = htmlPartEncoding
-		bodyMIMEType = "text/html"
+		bodyMIMEType = mimeTextHTML
 	} else if plainPartID != "" {
 		textPartID = plainPartID
 		textPartEncoding = plainPartEncoding
-		bodyMIMEType = "text/plain"
+		bodyMIMEType = mimeTextPlain
 	}
 	if os.Getenv("DEBUG_KITTY_IMAGES") != "" {
 		msg := fmt.Sprintf("[kitty-img] body selection html=%s plain=%s chosen=%s\n", htmlPartID, plainPartID, textPartID)
@@ -1172,11 +1171,11 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
 			// Use a closure with defer so a panic between open and
 			// WriteString doesn't leak the file descriptor (#894).
 			func() {
-				f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+				f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) //nolint:gosec
 				if err != nil {
 					return
 				}
-				defer f.Close()
+				defer f.Close() //nolint:errcheck
 				_, _ = f.WriteString(msg)
 			}()
 		}
@@ -1216,7 +1215,7 @@ func FetchAttachmentFromMailbox(account *config.Account, mailbox string, uid uin
 	if err != nil {
 		return nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if _, err := c.Select(mailbox, nil).Wait(); err != nil {
 		return nil, err
@@ -1258,7 +1257,7 @@ func moveEmail(account *config.Account, uid uint32, sourceMailbox, destMailbox s
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if _, err := c.Select(sourceMailbox, nil).Wait(); err != nil {
 		return err
@@ -1274,7 +1273,7 @@ func MarkEmailAsReadInMailbox(account *config.Account, mailbox string, uid uint3
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if _, err := c.Select(mailbox, nil).Wait(); err != nil {
 		return err
@@ -1293,7 +1292,7 @@ func MarkEmailAsUnreadInMailbox(account *config.Account, mailbox string, uid uin
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if _, err := c.Select(mailbox, nil).Wait(); err != nil {
 		return err
@@ -1312,7 +1311,7 @@ func DeleteEmailFromMailbox(account *config.Account, mailbox string, uid uint32)
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if _, err := c.Select(mailbox, nil).Wait(); err != nil {
 		return err
@@ -1335,11 +1334,11 @@ func ArchiveEmailFromMailbox(account *config.Account, mailbox string, uid uint32
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	var archiveMailbox string
 	switch account.ServiceProvider {
-	case "gmail":
+	case config.ProviderGmail:
 		// For Gmail, find the mailbox with the \All attribute
 		archiveMailbox, err = getMailboxByAttr(c, imap.MailboxAttrAll)
 		if err != nil {
@@ -1371,7 +1370,7 @@ func DeleteEmailsFromMailbox(account *config.Account, mailbox string, uids []uin
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if _, err := c.Select(mailbox, nil).Wait(); err != nil {
 		return err
@@ -1399,11 +1398,11 @@ func ArchiveEmailsFromMailbox(account *config.Account, mailbox string, uids []ui
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	var archiveMailbox string
 	switch account.ServiceProvider {
-	case "gmail":
+	case config.ProviderGmail:
 		archiveMailbox, err = getMailboxByAttr(c, imap.MailboxAttrAll)
 		if err != nil {
 			archiveMailbox = "[Gmail]/All Mail"
@@ -1431,7 +1430,7 @@ func MoveEmailsToFolder(account *config.Account, uids []uint32, sourceFolder, de
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if _, err := c.Select(sourceFolder, nil).Wait(); err != nil {
 		return err
@@ -1490,7 +1489,7 @@ func AppendToSentMailbox(account *config.Account, rawMsg []byte) error {
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	sentMailbox := getSentMailbox(account)
 	appendCmd := c.Append(sentMailbox, int64(len(rawMsg)), &imap.AppendOptions{
@@ -1510,7 +1509,7 @@ func AppendToSentMailbox(account *config.Account, rawMsg []byte) error {
 // getTrashMailbox returns the trash mailbox name for the account
 func getTrashMailbox(account *config.Account) string {
 	switch account.ServiceProvider {
-	case "gmail":
+	case config.ProviderGmail:
 		return "[Gmail]/Trash"
 	case "outlook":
 		return "Deleted Items"
@@ -1524,7 +1523,7 @@ func getTrashMailbox(account *config.Account) string {
 // getArchiveMailbox returns the archive/all mail mailbox name for the account
 func getArchiveMailbox(account *config.Account) string {
 	switch account.ServiceProvider {
-	case "gmail":
+	case config.ProviderGmail:
 		return "[Gmail]/All Mail"
 	case "outlook", "icloud":
 		return "Archive"
@@ -1539,7 +1538,7 @@ func FetchTrashEmails(account *config.Account, limit, offset uint32) ([]Email, e
 	if err != nil {
 		return nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	// Try to find trash by attribute first
 	trashMailbox, err := getMailboxByAttr(c, imap.MailboxAttrTrash)
@@ -1558,7 +1557,7 @@ func FetchArchiveEmails(account *config.Account, limit, offset uint32) ([]Email,
 	if err != nil {
 		return nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	// Try to find archive by attribute first (Gmail uses \All)
 	archiveMailbox, err := getMailboxByAttr(c, imap.MailboxAttrAll)
@@ -1690,7 +1689,7 @@ func FetchTrashEmailBody(account *config.Account, uid uint32) (string, string, [
 	if err != nil {
 		return "", "", nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	trashMailbox, err := getMailboxByAttr(c, imap.MailboxAttrTrash)
 	if err != nil {
@@ -1706,7 +1705,7 @@ func FetchArchiveEmailBody(account *config.Account, uid uint32) (string, string,
 	if err != nil {
 		return "", "", nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	archiveMailbox, err := getMailboxByAttr(c, imap.MailboxAttrAll)
 	if err != nil {
@@ -1722,7 +1721,7 @@ func FetchTrashAttachment(account *config.Account, uid uint32, partID string, en
 	if err != nil {
 		return nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	trashMailbox, err := getMailboxByAttr(c, imap.MailboxAttrTrash)
 	if err != nil {
@@ -1738,7 +1737,7 @@ func FetchArchiveAttachment(account *config.Account, uid uint32, partID string,
 	if err != nil {
 		return nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	archiveMailbox, err := getMailboxByAttr(c, imap.MailboxAttrAll)
 	if err != nil {
@@ -1754,7 +1753,7 @@ func DeleteTrashEmail(account *config.Account, uid uint32) error {
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	trashMailbox, err := getMailboxByAttr(c, imap.MailboxAttrTrash)
 	if err != nil {
@@ -1770,7 +1769,7 @@ func DeleteArchiveEmail(account *config.Account, uid uint32) error {
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	archiveMailbox, err := getMailboxByAttr(c, imap.MailboxAttrAll)
 	if err != nil {
@@ -1786,10 +1785,10 @@ func FetchFolders(account *config.Account) ([]Folder, error) {
 	if err != nil {
 		return nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	listCmd := c.List("", "*", nil)
-	defer listCmd.Close()
+	defer listCmd.Close() //nolint:errcheck
 
 	var folders []Folder
 	for {

fetcher/idle.go 🔗

@@ -89,7 +89,7 @@ func (w *IdleWatcher) StopAll() {
 // StopAllAndWait stops all IDLE watchers and waits for them to finish.
 func (w *IdleWatcher) StopAllAndWait() {
 	w.mu.Lock()
-	var pending []chan struct{}
+	pending := make([]chan struct{}, 0, len(w.watchers))
 	for id, a := range w.watchers {
 		close(a.stop)
 		pending = append(pending, a.done)
@@ -175,7 +175,7 @@ func (a *accountIdle) idleOnce() error {
 	if err != nil {
 		return err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	// Select the mailbox in read-only mode
 	selectData, err := c.Select(a.folder, nil).Wait()
@@ -193,8 +193,8 @@ func (a *accountIdle) idleOnce() error {
 	for {
 		select {
 		case <-a.stop:
-			idleCmd.Close()
-			idleCmd.Wait()
+			idleCmd.Close() //nolint:errcheck,gosec
+			idleCmd.Wait()  //nolint:errcheck,gosec
 			return nil
 
 		case newExists := <-mailboxUpdates:
@@ -205,8 +205,8 @@ func (a *accountIdle) idleOnce() error {
 					FolderName: a.folder,
 				}:
 				case <-a.stop:
-					idleCmd.Close()
-					idleCmd.Wait()
+					idleCmd.Close() //nolint:errcheck,gosec
+					idleCmd.Wait()  //nolint:errcheck,gosec
 					return nil
 				}
 			}

fetcher/search.go 🔗

@@ -15,7 +15,7 @@ func SearchMailbox(account *config.Account, folder string, query backend.SearchQ
 	if err != nil {
 		return nil, err
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if _, err := c.Select(folder, nil).Wait(); err != nil {
 		return nil, err

i18n/detector.go 🔗

@@ -73,8 +73,3 @@ func normalizeLanguageCode(code string) string {
 
 	return code
 }
-
-// isValidLanguage checks if a language code is registered.
-func isValidLanguage(code string) bool {
-	return HasLanguage(code)
-}

i18n/interpolator.go 🔗

@@ -8,7 +8,7 @@ import (
 // Interpolate replaces placeholders in a template string with values from data.
 // Supports {key} syntax for variable interpolation.
 func Interpolate(template string, data map[string]interface{}) string {
-	if data == nil || len(data) == 0 {
+	if len(data) == 0 {
 		return template
 	}
 

i18n/loader.go 🔗

@@ -59,7 +59,7 @@ func loadFromEmbedded(bundle *Bundle) error {
 func LoadFromDirectory(bundle *Bundle, dir string) error {
 	entries, err := os.ReadDir(dir)
 	if err != nil {
-		return fmt.Errorf("%w: %v", ErrLoadFailed, err)
+		return fmt.Errorf("%w: %w", ErrLoadFailed, err)
 	}
 
 	for _, entry := range entries {
@@ -95,7 +95,7 @@ func LoadFromDirectory(bundle *Bundle, dir string) error {
 func loadLanguageFile(bundle *Bundle, lang string, data []byte) error {
 	messages, err := ParseJSON(data)
 	if err != nil {
-		return fmt.Errorf("%w: language %s: %v", ErrParseFailed, lang, err)
+		return fmt.Errorf("%w: language %s: %w", ErrParseFailed, lang, err)
 	}
 
 	return bundle.AddMessages(lang, messages)

i18n/message.go 🔗

@@ -56,6 +56,10 @@ func (m *Message) GetText(form PluralForm) string {
 		if m.Many != "" {
 			return m.Many
 		}
+	case Other:
+		if m.Other != "" {
+			return m.Other
+		}
 	}
 	// Fallback to Other or One
 	if m.Other != "" {

i18n/template.go 🔗

@@ -56,7 +56,7 @@ func parseTemplate(s string) []templatePart {
 	for i := 0; i < len(s); i++ {
 		ch := s[i]
 
-		if ch == '{' && !inVar {
+		if ch == '{' && !inVar { //nolint:gocritic
 			// Start of variable
 			if current.Len() > 0 {
 				parts = append(parts, templatePart{isVar: false, value: current.String()})

i18n/validator.go 🔗

@@ -125,9 +125,9 @@ func (v *ValidationResult) String() string {
 		report.WriteString("Errors:\n")
 		for _, err := range v.Errors {
 			if err.Key != "" {
-				report.WriteString(fmt.Sprintf("  [%s] %s: %s\n", err.Language, err.Key, err.Message))
+				fmt.Fprintf(&report, "  [%s] %s: %s\n", err.Language, err.Key, err.Message)
 			} else {
-				report.WriteString(fmt.Sprintf("  [%s] %s\n", err.Language, err.Message))
+				fmt.Fprintf(&report, "  [%s] %s\n", err.Language, err.Message)
 			}
 		}
 		report.WriteString("\n")
@@ -137,9 +137,9 @@ func (v *ValidationResult) String() string {
 	if len(v.Missing) > 0 {
 		report.WriteString("Missing translations:\n")
 		for lang, keys := range v.Missing {
-			report.WriteString(fmt.Sprintf("  [%s] %d missing keys:\n", lang, len(keys)))
+			fmt.Fprintf(&report, "  [%s] %d missing keys:\n", lang, len(keys))
 			for _, key := range keys {
-				report.WriteString(fmt.Sprintf("    - %s\n", key))
+				fmt.Fprintf(&report, "    - %s\n", key)
 			}
 		}
 		report.WriteString("\n")
@@ -149,9 +149,9 @@ func (v *ValidationResult) String() string {
 	if len(v.Extra) > 0 {
 		report.WriteString("Extra translations (not in base):\n")
 		for lang, keys := range v.Extra {
-			report.WriteString(fmt.Sprintf("  [%s] %d extra keys:\n", lang, len(keys)))
+			fmt.Fprintf(&report, "  [%s] %d extra keys:\n", lang, len(keys))
 			for _, key := range keys {
-				report.WriteString(fmt.Sprintf("    - %s\n", key))
+				fmt.Fprintf(&report, "    - %s\n", key)
 			}
 		}
 	}

internal/threading/jwz_test.go 🔗

@@ -11,7 +11,7 @@ func TestBuildThreeMessageChain(t *testing.T) {
 	threads := Build([]EmailHeader{
 		{ID: "<a@example>", Subject: "Foo", Date: base, EmailID: "1", Sender: "a"},
 		{ID: "<b@example>", References: []string{"<a@example>"}, Subject: "Re: Foo", Date: base.Add(time.Minute), EmailID: "2", Sender: "b"},
-		{ID: "<c@example>", References: []string{"<a@example>", "<b@example>"}, Subject: "Re: Re: Foo", Date: base.Add(2 * time.Minute), EmailID: "3", Sender: "c"},
+		{ID: "<c@example>", References: []string{"<a@example>", "<b@example>"}, Subject: "Re: Re: Foo", Date: base.Add(2 * time.Minute), EmailID: "3", Sender: "c"}, //nolint:dupword
 	})
 
 	if len(threads) != 1 {
@@ -136,7 +136,7 @@ func TestBuildStableOrderingAcrossCalls(t *testing.T) {
 
 func TestCanonicalSubjectNormalizesReplyAndForwardPrefixes(t *testing.T) {
 	tests := map[string]string{
-		"Re: Re: Foo":     "foo",
+		"Re: Re: Foo":     "foo", //nolint:dupword
 		"Fwd: FW: Foo":    "foo",
 		"AW: WG: Tr: Foo": "foo",
 		"Reé: Resp: Foo":  "foo",

main.go 🔗

@@ -73,6 +73,11 @@ var (
 	httpClient = httpclient.NewWithRedirectCap(httpclient.UpdateCheckTimeout, 5)
 )
 
+const (
+	goosDarwin  = "darwin"
+	folderInbox = "INBOX"
+)
+
 // UpdateAvailableMsg is sent into the TUI when a newer release is detected.
 type UpdateAvailableMsg struct {
 	Latest  string
@@ -101,7 +106,6 @@ type mainModel struct {
 	emailsByAcct map[string][]fetcher.Email
 	width        int
 	height       int
-	err          error
 	// IMAP IDLE
 	idleWatcher *fetcher.IdleWatcher
 	idleUpdates chan fetcher.IdleUpdate
@@ -155,7 +159,6 @@ func newInitialModel(cfg *config.Config, mailtoURL *url.URL) *mainModel {
 			body := mailtoURL.Query().Get("body")
 			initialModel.current = tui.NewComposerWithAccounts(cfg.Accounts, cfg.Accounts[0].ID, to, subject, body, cfg.HideTips)
 		} else {
-
 			initialModel.current = tui.NewChoice()
 		}
 		initialModel.config = cfg
@@ -228,7 +231,7 @@ func waitForLogEntry(ch <-chan logging.Entry) tea.Cmd {
 }
 
 func (m *mainModel) syncUnreadBadge() {
-	if runtime.GOOS != "darwin" {
+	if runtime.GOOS != goosDarwin {
 		return
 	}
 	count := 0
@@ -251,7 +254,7 @@ func (m *mainModel) syncUnreadBadge() {
 	_ = macos.SetBadge(count)
 }
 
-func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo
 	var cmd tea.Cmd
 	var cmds []tea.Cmd
 	searchWasActive := false
@@ -308,7 +311,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if msg.String() == "ctrl+c" {
 			m.idleWatcher.StopAll()
 			if m.service != nil {
-				m.service.Close()
+				m.service.Close() //nolint:errcheck,gosec
 			}
 			return m, tea.Quit
 		}
@@ -355,7 +358,6 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			if err := config.SaveDraft(draft); err != nil {
 				log.Printf("Error saving draft: %v", err)
 			}
-
 		}
 		m.current = tui.NewChoice()
 		m.current, _ = m.current.Update(m.currentWindowSize())
@@ -523,8 +525,8 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 		}
 		// Always ensure INBOX is present, even if cache is empty or stale
-		if !seen["INBOX"] {
-			cachedFolders = append([]string{"INBOX"}, cachedFolders...)
+		if !seen[folderInbox] {
+			cachedFolders = append([]string{folderInbox}, cachedFolders...)
 		}
 		m.folderInbox = tui.NewFolderInbox(cachedFolders, m.config.Accounts)
 		m.folderInbox.SetDateFormat(m.config.GetDateFormat())
@@ -532,10 +534,10 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		m.folderInbox.SetDefaultThreaded(m.config.EnableThreaded)
 		m.folderInbox.SetDisableImages(m.config.DisableImages)
 		// Use cached INBOX emails for instant display (memory first, then disk)
-		if cached, ok := m.folderEmails["INBOX"]; ok && len(cached) > 0 {
+		if cached, ok := m.folderEmails[folderInbox]; ok && len(cached) > 0 {
 			m.folderInbox.SetEmails(cached, m.config.Accounts)
-		} else if diskCached := loadFolderEmailsFromCache("INBOX"); len(diskCached) > 0 {
-			m.folderEmails["INBOX"] = diskCached
+		} else if diskCached := loadFolderEmailsFromCache(folderInbox); len(diskCached) > 0 {
+			m.folderEmails[folderInbox] = diskCached
 			m.emails = diskCached
 			m.emailsByAcct = make(map[string][]fetcher.Email)
 			for _, email := range diskCached {
@@ -552,19 +554,19 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if m.service.IsDaemon() {
 			// Subscribe to INBOX updates if using daemon.
 			for _, acct := range m.config.Accounts {
-				m.service.Subscribe(acct.ID, "INBOX")
+				m.service.Subscribe(acct.ID, folderInbox) //nolint:errcheck,gosec
 			}
 		} else {
 			// Start IDLE watchers for all accounts on INBOX
 			for i := range m.config.Accounts {
-				m.idleWatcher.Watch(&m.config.Accounts[i], "INBOX")
+				m.idleWatcher.Watch(&m.config.Accounts[i], folderInbox)
 			}
 		}
 		// Fetch folders and INBOX emails in parallel (background refresh)
 		batchCmds := []tea.Cmd{
 			m.current.Init(),
 			fetchFoldersCmd(m.config),
-			fetchFolderEmailsCmd(m.config, "INBOX"),
+			fetchFolderEmailsCmd(m.config, folderInbox),
 			listenForIdleUpdates(m.idleUpdates),
 		}
 		if m.service.IsDaemon() {
@@ -587,7 +589,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			for _, f := range folders {
 				names = append(names, f.Name)
 			}
-			go config.SaveAccountFolders(accID, names)
+			go config.SaveAccountFolders(accID, names) //nolint:errcheck
 		}
 		// Per-account fetch errors (e.g. broken IMAP login, unreachable
 		// server) are non-fatal: other accounts' folders are still shown.
@@ -639,7 +641,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			folders := config.GetCachedFolders(m.config.Accounts[i].ID)
 			if !slices.Contains(folders, msg.FolderName) {
 				if m.service != nil && m.service.IsDaemon() {
-					m.service.Unsubscribe(m.config.Accounts[i].ID, msg.PreviousFolder)
+					m.service.Unsubscribe(m.config.Accounts[i].ID, msg.PreviousFolder) //nolint:errcheck,gosec
 				} else {
 					m.idleWatcher.Stop(m.config.Accounts[i].ID)
 				}
@@ -648,9 +650,9 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			if m.service != nil && m.service.IsDaemon() {
 				// Unsubscribe from old, subscribe to new.
 				if msg.PreviousFolder != "" {
-					m.service.Unsubscribe(m.config.Accounts[i].ID, msg.PreviousFolder)
+					m.service.Unsubscribe(m.config.Accounts[i].ID, msg.PreviousFolder) //nolint:errcheck,gosec
 				}
-				m.service.Subscribe(m.config.Accounts[i].ID, msg.FolderName)
+				m.service.Subscribe(m.config.Accounts[i].ID, msg.FolderName) //nolint:errcheck,gosec
 			} else {
 				m.idleWatcher.Watch(&m.config.Accounts[i], msg.FolderName)
 			}
@@ -916,7 +918,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					accountName = acc.Email
 				}
 			}
-			go notify.Send("Matcha", fmt.Sprintf("New mail in %s (%s)", msg.FolderName, accountName))
+			go notify.Send("Matcha", fmt.Sprintf("New mail in %s (%s)", msg.FolderName, accountName)) //nolint:errcheck
 		}
 
 		// IDLE detected new mail — refetch the folder if we're viewing it
@@ -949,7 +951,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 							accountName = acc.Email
 						}
 					}
-					go notify.Send("Matcha", fmt.Sprintf("New mail in %s (%s)", ev.Folder, accountName))
+					go notify.Send("Matcha", fmt.Sprintf("New mail in %s (%s)", ev.Folder, accountName)) //nolint:errcheck
 				}
 
 				if m.folderInbox != nil && m.folderInbox.GetCurrentFolder() == ev.Folder {
@@ -1042,7 +1044,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if msg.Limit > 0 {
 			limit = msg.Limit
 		}
-		folderName := "INBOX"
+		folderName := folderInbox
 		if m.folderInbox != nil {
 			folderName = m.folderInbox.GetCurrentFolder()
 		}
@@ -1054,7 +1056,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tui.SearchRequestedMsg:
 		folderName := msg.FolderName
 		if folderName == "" {
-			folderName = "INBOX"
+			folderName = folderInbox
 		}
 		return m, m.searchEmailsCmd(msg.Query, folderName, msg.AccountID)
 
@@ -1324,14 +1326,14 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	case tui.ViewEmailMsg:
 		email := msg.Email
 		if email == nil {
-			email = m.getEmailByUIDAndAccount(msg.UID, msg.AccountID, msg.Mailbox)
+			email = m.getEmailByUIDAndAccount(msg.UID, msg.AccountID)
 		} else {
 			m.addEmailToStoresIfMissing(*email, msg.Mailbox)
 		}
 		if email == nil {
 			return m, nil
 		}
-		folderName := "INBOX"
+		folderName := folderInbox
 		if m.folderInbox != nil {
 			folderName = m.folderInbox.GetCurrentFolder()
 		}
@@ -1404,10 +1406,10 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		}
 
 		// Update the email in our stores
-		m.updateEmailBodyByUID(msg.UID, msg.AccountID, msg.Mailbox, msg.Body, msg.BodyMIMEType, msg.Attachments)
+		m.updateEmailBodyByUID(msg.UID, msg.AccountID, msg.Body, msg.BodyMIMEType, msg.Attachments)
 
 		// Cache the body to disk
-		folderForCache := "INBOX"
+		folderForCache := folderInbox
 		if m.folderInbox != nil {
 			folderForCache = m.folderInbox.GetCurrentFolder()
 		}
@@ -1442,7 +1444,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			loglevel.Debugf("error caching email body fails (disk full, permission denied) for UID: %d: %v", msg.UID, err)
 		}
 
-		email := m.getEmailByUIDAndAccount(msg.UID, msg.AccountID, msg.Mailbox)
+		email := m.getEmailByUIDAndAccount(msg.UID, msg.AccountID)
 		if email == nil {
 			if m.folderInbox != nil {
 				m.current = m.folderInbox
@@ -1456,7 +1458,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		if !email.IsRead && !pluginSuppressed {
 			m.markEmailAsReadInStores(msg.UID, msg.AccountID)
 
-			folderName := "INBOX"
+			folderName := folderInbox
 			if m.folderInbox != nil {
 				folderName = m.folderInbox.GetCurrentFolder()
 			}
@@ -1467,7 +1469,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		}
 
 		// Find the index for the email view (used for display purposes)
-		emailIndex := m.getEmailIndex(msg.UID, msg.AccountID, msg.Mailbox)
+		emailIndex := m.getEmailIndex(msg.UID, msg.AccountID)
 		emailView := tui.NewEmailView(*email, emailIndex, m.width, m.height, msg.Mailbox, m.config.DisableImages)
 		m.current = emailView
 		m.syncPluginStatus()
@@ -1530,7 +1532,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 		// Set reply headers
 		inReplyTo := msg.Email.MessageID
-		references := append(msg.Email.References, msg.Email.MessageID)
+		references := append(msg.Email.References, msg.Email.MessageID) //nolint:gocritic
 		composer.SetReplyContext(inReplyTo, references)
 
 		m.current = composer
@@ -1592,7 +1594,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		return m, nil
 
 	case tui.GoToFilePickerMsg:
-		if runtime.GOOS == "darwin" {
+		if runtime.GOOS == goosDarwin {
 			return m, func() tea.Msg {
 				wd, _ := os.Getwd()
 				paths, err := macos.OpenFilePicker(wd)
@@ -1727,7 +1729,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, nil
 		}
 
-		folderName := "INBOX"
+		folderName := folderInbox
 		if m.folderInbox != nil {
 			folderName = m.folderInbox.GetCurrentFolder()
 		}
@@ -1746,7 +1748,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, nil
 		}
 
-		folderName := "INBOX"
+		folderName := folderInbox
 		if m.folderInbox != nil {
 			folderName = m.folderInbox.GetCurrentFolder()
 		}
@@ -1805,7 +1807,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, nil
 		}
 
-		folderName := "INBOX"
+		folderName := folderInbox
 		if m.folderInbox != nil {
 			folderName = m.folderInbox.GetCurrentFolder()
 		}
@@ -1829,7 +1831,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, nil
 		}
 
-		folderName := "INBOX"
+		folderName := folderInbox
 		if m.folderInbox != nil {
 			folderName = m.folderInbox.GetCurrentFolder()
 		}
@@ -1889,7 +1891,7 @@ func (m *mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, nil
 		}
 
-		email := m.getEmailByIndex(msg.Index, msg.Mailbox)
+		email := m.getEmailByIndex(msg.Index)
 		if email == nil {
 			m.current = m.previousModel
 			return m, nil
@@ -1995,14 +1997,14 @@ func (m *mainModel) logPanelHeight() int {
 	return 7
 }
 
-func (m *mainModel) getEmailByIndex(index int, mailbox tui.MailboxKind) *fetcher.Email {
+func (m *mainModel) getEmailByIndex(index int) *fetcher.Email {
 	if index >= 0 && index < len(m.emails) {
 		return &m.emails[index]
 	}
 	return nil
 }
 
-func (m *mainModel) getEmailByUIDAndAccount(uid uint32, accountID string, mailbox tui.MailboxKind) *fetcher.Email {
+func (m *mainModel) getEmailByUIDAndAccount(uid uint32, accountID string) *fetcher.Email {
 	for i := range m.emails {
 		if m.emails[i].UID == uid && m.emails[i].AccountID == accountID {
 			return &m.emails[i]
@@ -2011,7 +2013,7 @@ func (m *mainModel) getEmailByUIDAndAccount(uid uint32, accountID string, mailbo
 	return nil
 }
 
-func (m *mainModel) getEmailIndex(uid uint32, accountID string, mailbox tui.MailboxKind) int {
+func (m *mainModel) getEmailIndex(uid uint32, accountID string) int {
 	for i := range m.emails {
 		if m.emails[i].UID == uid && m.emails[i].AccountID == accountID {
 			return i
@@ -2020,7 +2022,7 @@ func (m *mainModel) getEmailIndex(uid uint32, accountID string, mailbox tui.Mail
 	return -1
 }
 
-func (m *mainModel) updateEmailBodyByUID(uid uint32, accountID string, mailbox tui.MailboxKind, body, bodyMIMEType string, attachments []fetcher.Attachment) {
+func (m *mainModel) updateEmailBodyByUID(uid uint32, accountID string, body, bodyMIMEType string, attachments []fetcher.Attachment) {
 	for i := range m.emails {
 		if m.emails[i].UID == uid && m.emails[i].AccountID == accountID {
 			m.emails[i].Body = body
@@ -2041,8 +2043,8 @@ func (m *mainModel) updateEmailBodyByUID(uid uint32, accountID string, mailbox t
 	}
 }
 
-func (m *mainModel) addEmailToStoresIfMissing(email fetcher.Email, mailbox tui.MailboxKind) {
-	if m.getEmailByUIDAndAccount(email.UID, email.AccountID, mailbox) != nil {
+func (m *mainModel) addEmailToStoresIfMissing(email fetcher.Email, _ tui.MailboxKind) {
+	if m.getEmailByUIDAndAccount(email.UID, email.AccountID) != nil {
 		return
 	}
 	if m.emailsByAcct == nil {
@@ -2117,7 +2119,7 @@ func (m *mainModel) markEmailAsUnreadInStores(uid uint32, accountID string) {
 func (m *mainModel) removeEmailFromStores(uid uint32, accountID string) {
 	var filtered []fetcher.Email
 	for _, e := range m.emails {
-		if !(e.UID == uid && e.AccountID == accountID) {
+		if e.UID != uid || e.AccountID != accountID {
 			filtered = append(filtered, e)
 		}
 	}
@@ -2144,7 +2146,6 @@ func (m *mainModel) pluginFlagCmds() []tea.Cmd {
 	}
 	var cmds []tea.Cmd
 	for _, op := range ops {
-		op := op
 		account := m.config.GetAccountByID(op.AccountID)
 		if account == nil {
 			continue
@@ -2316,86 +2317,6 @@ func flattenAndSort(emailsByAccount map[string][]fetcher.Email) []fetcher.Email
 	return allEmails
 }
 
-func fetchAllAccountsEmails(cfg *config.Config, mailbox tui.MailboxKind) tea.Cmd {
-	return func() tea.Msg {
-		emailsByAccount := make(map[string][]fetcher.Email)
-		var mu sync.Mutex
-		var wg sync.WaitGroup
-
-		for _, account := range cfg.Accounts {
-			wg.Add(1)
-			go func(acc config.Account) {
-				defer wg.Done()
-				var emails []fetcher.Email
-				var err error
-				switch mailbox {
-				case tui.MailboxSent:
-					emails, err = fetcher.FetchSentEmails(&acc, initialEmailLimit, 0)
-				case tui.MailboxTrash:
-					emails, err = fetcher.FetchTrashEmails(&acc, initialEmailLimit, 0)
-				case tui.MailboxArchive:
-					emails, err = fetcher.FetchArchiveEmails(&acc, initialEmailLimit, 0)
-				default:
-					emails, err = fetcher.FetchEmails(&acc, initialEmailLimit, 0)
-				}
-				if err != nil {
-					log.Printf("Error fetching from %s: %v", acc.Email, err)
-					return
-				}
-				mu.Lock()
-				emailsByAccount[acc.ID] = emails
-				mu.Unlock()
-			}(account)
-		}
-
-		wg.Wait()
-		return tui.AllEmailsFetchedMsg{EmailsByAccount: emailsByAccount, Mailbox: mailbox}
-	}
-}
-
-func fetchEmails(account *config.Account, limit, offset uint32, mailbox tui.MailboxKind) tea.Cmd {
-	return func() tea.Msg {
-		var emails []fetcher.Email
-		var err error
-		if mailbox == tui.MailboxSent {
-			emails, err = fetcher.FetchSentEmails(account, limit, offset)
-		} else {
-			emails, err = fetcher.FetchEmails(account, limit, offset)
-		}
-		if err != nil {
-			return tui.FetchErr(err)
-		}
-		if offset == 0 {
-			return tui.EmailsFetchedMsg{Emails: emails, AccountID: account.ID, Mailbox: mailbox}
-		}
-		return tui.EmailsAppendedMsg{Emails: emails, AccountID: account.ID, Mailbox: mailbox}
-	}
-}
-
-func fetchEmailsForMailbox(account *config.Account, limit, offset uint32, mailbox tui.MailboxKind) tea.Cmd {
-	return func() tea.Msg {
-		var emails []fetcher.Email
-		var err error
-		switch mailbox {
-		case tui.MailboxSent:
-			emails, err = fetcher.FetchSentEmails(account, limit, offset)
-		case tui.MailboxTrash:
-			emails, err = fetcher.FetchTrashEmails(account, limit, offset)
-		case tui.MailboxArchive:
-			emails, err = fetcher.FetchArchiveEmails(account, limit, offset)
-		default:
-			emails, err = fetcher.FetchEmails(account, limit, offset)
-		}
-		if err != nil {
-			return tui.FetchErr(err)
-		}
-		if offset == 0 {
-			return tui.EmailsFetchedMsg{Emails: emails, AccountID: account.ID, Mailbox: mailbox}
-		}
-		return tui.EmailsAppendedMsg{Emails: emails, AccountID: account.ID, Mailbox: mailbox}
-	}
-}
-
 func (m *mainModel) searchEmailsCmd(query backend.SearchQuery, folderName, accountID string) tea.Cmd {
 	return func() tea.Msg {
 		ctx, cancel := context.WithTimeout(context.Background(), httpclient.IMAPSearchTimeout)
@@ -2463,16 +2384,6 @@ func sortFetcherEmails(emails []fetcher.Email) {
 	})
 }
 
-func loadCachedEmails() tea.Cmd {
-	return func() tea.Msg {
-		cache, err := config.LoadEmailCache()
-		if err != nil {
-			return tui.CachedEmailsLoadedMsg{Cache: nil}
-		}
-		return tui.CachedEmailsLoadedMsg{Cache: cache}
-	}
-}
-
 func refreshEmails(cfg *config.Config, mailbox tui.MailboxKind, counts map[string]int) tea.Cmd {
 	return func() tea.Msg {
 		emailsByAccount := make(map[string][]fetcher.Email)
@@ -2514,7 +2425,7 @@ func refreshEmails(cfg *config.Config, mailbox tui.MailboxKind, counts map[strin
 }
 
 func emailsToCache(emails []fetcher.Email) []config.CachedEmail {
-	var cached []config.CachedEmail
+	cached := make([]config.CachedEmail, 0, len(emails))
 	for _, email := range emails {
 		cached = append(cached, config.CachedEmail{
 			UID:        email.UID,
@@ -2533,7 +2444,7 @@ func emailsToCache(emails []fetcher.Email) []config.CachedEmail {
 }
 
 func cacheToEmails(cached []config.CachedEmail) []fetcher.Email {
-	var emails []fetcher.Email
+	emails := make([]fetcher.Email, 0, len(cached))
 	for _, c := range cached {
 		emails = append(emails, fetcher.Email{
 			UID:        c.UID,
@@ -2566,39 +2477,6 @@ func loadFolderEmailsFromCache(folderName string) []fetcher.Email {
 	return cacheToEmails(cached)
 }
 
-func saveEmailsToCache(emails []fetcher.Email) {
-	if len(emails) > maxCacheEmails {
-		emails = emails[:maxCacheEmails]
-	}
-	var cachedEmails []config.CachedEmail
-	for _, email := range emails {
-		cachedEmails = append(cachedEmails, config.CachedEmail{
-			UID:        email.UID,
-			From:       email.From,
-			To:         email.To,
-			Subject:    email.Subject,
-			Date:       email.Date,
-			MessageID:  email.MessageID,
-			InReplyTo:  email.InReplyTo,
-			References: email.References,
-			AccountID:  email.AccountID,
-			IsRead:     email.IsRead,
-		})
-
-		// Save sender as a contact
-		if email.From != "" {
-			name, emailAddr := parseEmailAddress(email.From)
-			if err := config.AddContactForAccount(name, emailAddr, email.AccountID); err != nil {
-				log.Printf("Error saving contact from email: %v", err)
-			}
-		}
-	}
-	cache := &config.EmailCache{Emails: cachedEmails}
-	if err := config.SaveEmailCache(cache); err != nil {
-		log.Printf("Error saving email cache: %v", err)
-	}
-}
-
 // parseEmailAddress parses "Name <email>" or just "email" format
 func parseEmailAddress(addr string) (name, email string) {
 	addr = strings.TrimSpace(addr)
@@ -2616,44 +2494,6 @@ func parseEmailAddress(addr string) (name, email string) {
 	return name, email
 }
 
-func fetchEmailBodyCmd(cfg *config.Config, uid uint32, accountID string, mailbox tui.MailboxKind) tea.Cmd {
-	return func() tea.Msg {
-		account := cfg.GetAccountByID(accountID)
-		if account == nil {
-			return tui.EmailBodyFetchedMsg{UID: uid, AccountID: accountID, Mailbox: mailbox, Err: fmt.Errorf("account not found")}
-		}
-
-		var (
-			body         string
-			bodyMIMEType string
-			attachments  []fetcher.Attachment
-			err          error
-		)
-		switch mailbox {
-		case tui.MailboxSent:
-			body, bodyMIMEType, attachments, err = fetcher.FetchSentEmailBody(account, uid)
-		case tui.MailboxTrash:
-			body, bodyMIMEType, attachments, err = fetcher.FetchTrashEmailBody(account, uid)
-		case tui.MailboxArchive:
-			body, bodyMIMEType, attachments, err = fetcher.FetchArchiveEmailBody(account, uid)
-		default:
-			body, bodyMIMEType, attachments, err = fetcher.FetchEmailBody(account, uid)
-		}
-		if err != nil {
-			return tui.EmailBodyFetchedMsg{UID: uid, AccountID: accountID, Mailbox: mailbox, Err: err}
-		}
-
-		return tui.EmailBodyFetchedMsg{
-			UID:          uid,
-			Body:         body,
-			BodyMIMEType: bodyMIMEType,
-			Attachments:  attachments,
-			AccountID:    accountID,
-			Mailbox:      mailbox,
-		}
-	}
-}
-
 func markdownToHTML(md []byte) []byte {
 	return clib.MarkdownToHTML(md)
 }
@@ -2695,7 +2535,7 @@ func sendEmail(account *config.Account, msg tui.SendEmailMsg) tea.Cmd {
 		}
 		// Append quoted text if present (for replies)
 		if msg.QuotedText != "" {
-			body = body + msg.QuotedText
+			body += msg.QuotedText
 		}
 		images := make(map[string][]byte)
 		attachments := make(map[string][]byte)
@@ -2768,7 +2608,7 @@ func sendRSVP(account *config.Account, msg tui.SendRSVPMsg) tea.Cmd {
 
 		// Send as multipart/alternative with text/calendar; method=REPLY
 		// This iMIP format is required for Google Calendar to recognize the RSVP
-		references := append(msg.References, msg.InReplyTo)
+		references := append(msg.References, msg.InReplyTo) //nolint:gocritic
 		rawMsg, err := sender.SendCalendarReply(
 			account,
 			[]string{msg.Event.Organizer},
@@ -2794,35 +2634,6 @@ func sendRSVP(account *config.Account, msg tui.SendRSVPMsg) tea.Cmd {
 	}
 }
 
-func deleteEmailCmd(account *config.Account, uid uint32, accountID string, mailbox tui.MailboxKind) tea.Cmd {
-	return func() tea.Msg {
-		var err error
-		switch mailbox {
-		case tui.MailboxSent:
-			err = fetcher.DeleteSentEmail(account, uid)
-		case tui.MailboxTrash:
-			err = fetcher.DeleteTrashEmail(account, uid)
-		case tui.MailboxArchive:
-			err = fetcher.DeleteArchiveEmail(account, uid)
-		default:
-			err = fetcher.DeleteEmail(account, uid)
-		}
-		return tui.EmailActionDoneMsg{UID: uid, AccountID: accountID, Mailbox: mailbox, Err: err}
-	}
-}
-
-func archiveEmailCmd(account *config.Account, uid uint32, accountID string, mailbox tui.MailboxKind) tea.Cmd {
-	return func() tea.Msg {
-		var err error
-		if mailbox == tui.MailboxSent {
-			err = fetcher.ArchiveSentEmail(account, uid)
-		} else {
-			err = fetcher.ArchiveEmail(account, uid)
-		}
-		return tui.EmailActionDoneMsg{UID: uid, AccountID: accountID, Mailbox: mailbox, Err: err}
-	}
-}
-
 // --- External editor command ---
 
 // openExternalEditor writes the body to a temp file, opens $EDITOR, and reads back the result.
@@ -2844,19 +2655,19 @@ func openExternalEditor(body string) tea.Cmd {
 	tmpPath := tmpFile.Name()
 
 	if _, err := tmpFile.WriteString(body); err != nil {
-		tmpFile.Close()
-		os.Remove(tmpPath)
+		tmpFile.Close()    //nolint:errcheck,gosec
+		os.Remove(tmpPath) //nolint:errcheck,gosec
 		return func() tea.Msg {
 			return tui.EditorFinishedMsg{Err: fmt.Errorf("writing temp file: %w", err)}
 		}
 	}
-	tmpFile.Close()
+	tmpFile.Close() //nolint:errcheck,gosec
 
 	parts := strings.Fields(editor)
-	args := append(parts[1:], tmpPath)
-	c := exec.Command(parts[0], args...)
+	args := append(parts[1:], tmpPath)   //nolint:gocritic
+	c := exec.Command(parts[0], args...) //nolint:gosec,noctx
 	return tea.ExecProcess(c, func(err error) tea.Msg {
-		defer os.Remove(tmpPath)
+		defer os.Remove(tmpPath) //nolint:errcheck
 		if err != nil {
 			return tui.EditorFinishedMsg{Err: err}
 		}
@@ -3261,7 +3072,7 @@ func downloadAttachmentCmd(account *config.Account, uid uint32, msg tui.Download
 			data, err = fetcher.FetchTrashAttachment(account, uid, msg.PartID, msg.Encoding)
 		case tui.MailboxArchive:
 			data, err = fetcher.FetchArchiveAttachment(account, uid, msg.PartID, msg.Encoding)
-		default:
+		case tui.MailboxInbox:
 			data, err = fetcher.FetchAttachment(account, uid, msg.PartID, msg.Encoding)
 		}
 
@@ -3275,7 +3086,7 @@ func downloadAttachmentCmd(account *config.Account, uid uint32, msg tui.Download
 		}
 		downloadsPath := filepath.Join(homeDir, "Downloads")
 		if _, err := os.Stat(downloadsPath); os.IsNotExist(err) {
-			if mkErr := os.MkdirAll(downloadsPath, 0755); mkErr != nil {
+			if mkErr := os.MkdirAll(downloadsPath, 0750); mkErr != nil {
 				return tui.AttachmentDownloadedMsg{Err: mkErr}
 			}
 		}
@@ -3294,7 +3105,7 @@ func downloadAttachmentCmd(account *config.Account, uid uint32, msg tui.Download
 
 			// Try to create file exclusively. If it already exists, os.OpenFile will return an error
 			// that satisfies os.IsExist(err), so we can increment the candidate.
-			f, err := os.OpenFile(filePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+			f, err := os.OpenFile(filePath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) //nolint:gosec
 			if err != nil {
 				if os.IsExist(err) {
 					// file exists, try next candidate
@@ -3327,13 +3138,13 @@ func downloadAttachmentCmd(account *config.Account, uid uint32, msg tui.Download
 		go func(p string) {
 			var cmd *exec.Cmd
 			switch runtime.GOOS {
-			case "darwin":
-				cmd = exec.Command("open", p)
+			case goosDarwin:
+				cmd = exec.Command("open", p) //nolint:noctx
 			case "linux":
-				cmd = exec.Command("xdg-open", p)
+				cmd = exec.Command("xdg-open", p) //nolint:noctx
 			case "windows":
 				// 'start' is a cmd builtin; provide an empty title argument to avoid interpreting the path as the title.
-				cmd = exec.Command("cmd", "/c", "start", "", p)
+				cmd = exec.Command("cmd", "/c", "start", "", p) //nolint:noctx
 			default:
 				// Unsupported OS: nothing to do.
 				return
@@ -3362,10 +3173,10 @@ func detectInstalledVersion() string {
 	}
 
 	// Try Homebrew (macOS)
-	if runtime.GOOS == "darwin" {
+	if runtime.GOOS == goosDarwin {
 		if _, err := exec.LookPath("brew"); err == nil {
 			// `brew list --versions matcha` prints: matcha 1.2.3
-			if out, err := exec.Command("brew", "list", "--versions", "matcha").Output(); err == nil {
+			if out, err := exec.Command("brew", "list", "--versions", "matcha").Output(); err == nil { //nolint:noctx
 				parts := strings.Fields(string(out))
 				if len(parts) >= 2 {
 					return parts[1]
@@ -3377,7 +3188,7 @@ func detectInstalledVersion() string {
 	// Try WinGet (Windows)
 	if runtime.GOOS == "windows" {
 		if _, err := exec.LookPath("winget"); err == nil {
-			if out, err := exec.Command("winget", "list", "--id", "floatpane.matcha", "--disable-interactivity").Output(); err == nil {
+			if out, err := exec.Command("winget", "list", "--id", "floatpane.matcha", "--disable-interactivity").Output(); err == nil { //nolint:noctx
 				lines := strings.Split(strings.TrimSpace(string(out)), "\n")
 				for _, line := range lines {
 					if strings.Contains(strings.ToLower(line), "floatpane.matcha") {
@@ -3396,7 +3207,7 @@ func detectInstalledVersion() string {
 	// Try snap (Linux)
 	if runtime.GOOS == "linux" {
 		if _, err := exec.LookPath("snap"); err == nil {
-			if out, err := exec.Command("snap", "list", "matcha").Output(); err == nil {
+			if out, err := exec.Command("snap", "list", "matcha").Output(); err == nil { //nolint:noctx
 				lines := strings.Split(strings.TrimSpace(string(out)), "\n")
 				if len(lines) >= 2 {
 					fields := strings.Fields(lines[1])
@@ -3408,7 +3219,7 @@ func detectInstalledVersion() string {
 		}
 
 		if _, err := exec.LookPath("flatpak"); err == nil {
-			if out, err := exec.Command("flatpak", "info", "com.floatpane.matcha").Output(); err == nil {
+			if out, err := exec.Command("flatpak", "info", "com.floatpane.matcha").Output(); err == nil { //nolint:noctx
 				lines := strings.Split(strings.TrimSpace(string(out)), "\n")
 				for _, line := range lines {
 					line = strings.TrimSpace(line)
@@ -3439,7 +3250,7 @@ func checkForUpdatesCmd() tea.Cmd {
 		if err != nil {
 			return nil
 		}
-		defer resp.Body.Close()
+		defer resp.Body.Close() //nolint:errcheck
 
 		var rel githubRelease
 		if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
@@ -3492,13 +3303,14 @@ func runOAuthCLI(args []string) {
 	}
 
 	cmdArgs := append([]string{script}, args...)
-	cmd := exec.Command("python3", cmdArgs...)
+	cmd := exec.Command("python3", cmdArgs...) //nolint:gosec,noctx
 	cmd.Stdin = os.Stdin
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 
 	if err := cmd.Run(); err != nil {
-		if exitErr, ok := err.(*exec.ExitError); ok {
+		var exitErr *exec.ExitError
+		if errors.As(err, &exitErr) {
 			os.Exit(exitErr.ExitCode())
 		}
 		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@@ -3677,32 +3489,26 @@ func isFlagSet(fs *flag.FlagSet, name string) bool {
 	return found
 }
 
-func runUpdateCLI() (err error) {
+func runUpdateCLI() (err error) { //nolint:gocyclo
 	const api = "https://api.github.com/repos/floatpane/matcha/releases/latest"
 	resp, err := httpClient.Get(api)
 	if err != nil {
 		return fmt.Errorf("could not query releases: %w", err)
 	}
-	defer resp.Body.Close()
+	defer resp.Body.Close() //nolint:errcheck
 
 	var rel githubRelease
 	if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
 		return fmt.Errorf("could not parse release info: %w", err)
 	}
 
-	latestTag := rel.TagName
-	if strings.HasPrefix(latestTag, "v") {
-		latestTag = latestTag[1:]
-	}
+	latestTag := strings.TrimPrefix(rel.TagName, "v")
 
 	fmt.Printf("Current version: %s\n", version)
 	fmt.Printf("Latest version: %s\n", latestTag)
 
 	// Quick check: if already up-to-date, exit
-	cur := version
-	if strings.HasPrefix(cur, "v") {
-		cur = cur[1:]
-	}
+	cur := strings.TrimPrefix(version, "v")
 	if latestTag == "" || cur == latestTag {
 		fmt.Println("Already up to date.")
 		return nil
@@ -3712,7 +3518,7 @@ func runUpdateCLI() (err error) {
 	if _, err := exec.LookPath("brew"); err == nil {
 		fmt.Println("Detected Homebrew — updating taps and attempting to upgrade via brew.")
 
-		updateCmd := exec.Command("brew", "update")
+		updateCmd := exec.Command("brew", "update") //nolint:noctx
 		updateCmd.Stdout = os.Stdout
 		updateCmd.Stderr = os.Stderr
 		if err := updateCmd.Run(); err != nil {
@@ -3720,7 +3526,7 @@ func runUpdateCLI() (err error) {
 			// continue to attempt upgrade even if update failed
 		}
 
-		upgradeCmd := exec.Command("brew", "upgrade", "floatpane/matcha/matcha")
+		upgradeCmd := exec.Command("brew", "upgrade", "floatpane/matcha/matcha") //nolint:noctx
 		upgradeCmd.Stdout = os.Stdout
 		upgradeCmd.Stderr = os.Stderr
 		if err := upgradeCmd.Run(); err == nil {
@@ -3734,10 +3540,10 @@ func runUpdateCLI() (err error) {
 	// Detect snap
 	if _, err := exec.LookPath("snap"); err == nil {
 		// Check if matcha is installed as a snap
-		cmdCheck := exec.Command("snap", "list", "matcha")
+		cmdCheck := exec.Command("snap", "list", "matcha") //nolint:noctx
 		if err := cmdCheck.Run(); err == nil {
 			fmt.Println("Detected Snap package — attempting to refresh.")
-			cmd := exec.Command("snap", "refresh", "matcha")
+			cmd := exec.Command("snap", "refresh", "matcha") //nolint:noctx
 			cmd.Stdout = os.Stdout
 			cmd.Stderr = os.Stderr
 			if err := cmd.Run(); err == nil {
@@ -3751,10 +3557,10 @@ func runUpdateCLI() (err error) {
 	// Detect flatpak
 	if _, err := exec.LookPath("flatpak"); err == nil {
 		// Check if matcha is installed as a flatpak
-		cmdCheck := exec.Command("flatpak", "info", "com.floatpane.matcha")
+		cmdCheck := exec.Command("flatpak", "info", "com.floatpane.matcha") //nolint:noctx
 		if err := cmdCheck.Run(); err == nil {
 			fmt.Println("Detected Flatpak package — attempting to update.")
-			cmd := exec.Command("flatpak", "update", "-y", "com.floatpane.matcha")
+			cmd := exec.Command("flatpak", "update", "-y", "com.floatpane.matcha") //nolint:noctx
 			cmd.Stdout = os.Stdout
 			cmd.Stderr = os.Stderr
 			if err := cmd.Run(); err == nil {
@@ -3768,10 +3574,10 @@ func runUpdateCLI() (err error) {
 
 	// Detect WinGet
 	if _, err := exec.LookPath("winget"); err == nil {
-		cmdCheck := exec.Command("winget", "list", "--id", "floatpane.matcha", "--disable-interactivity")
+		cmdCheck := exec.Command("winget", "list", "--id", "floatpane.matcha", "--disable-interactivity") //nolint:noctx
 		if err := cmdCheck.Run(); err == nil {
 			fmt.Println("Detected WinGet package — attempting to upgrade.")
-			cmd := exec.Command("winget", "upgrade", "--id", "floatpane.matcha", "--disable-interactivity")
+			cmd := exec.Command("winget", "upgrade", "--id", "floatpane.matcha", "--disable-interactivity") //nolint:noctx
 			cmd.Stdout = os.Stdout
 			cmd.Stderr = os.Stderr
 			if err := cmd.Run(); err == nil {
@@ -3821,14 +3627,14 @@ func runUpdateCLI() (err error) {
 	if err != nil {
 		return fmt.Errorf("download failed: %w", err)
 	}
-	defer respAsset.Body.Close()
+	defer respAsset.Body.Close() //nolint:errcheck
 
 	// Create a temp file for the download
 	tmpDir, err := os.MkdirTemp("", "matcha-update-*")
 	if err != nil {
 		return fmt.Errorf("could not create temp dir: %w", err)
 	}
-	defer os.RemoveAll(tmpDir)
+	defer os.RemoveAll(tmpDir) //nolint:errcheck
 
 	assetPath := filepath.Join(tmpDir, assetName)
 	outFile, err := os.Create(assetPath)
@@ -3836,7 +3642,7 @@ func runUpdateCLI() (err error) {
 		return fmt.Errorf("could not create temp file: %w", err)
 	}
 	_, err = io.Copy(outFile, respAsset.Body)
-	outFile.Close()
+	outFile.Close() //nolint:errcheck,gosec
 	if err != nil {
 		return fmt.Errorf("could not write asset to disk: %w", err)
 	}
@@ -3849,12 +3655,12 @@ func runUpdateCLI() (err error) {
 
 	// Extract the binary from the archive.
 	var binPath string
-	if strings.HasSuffix(assetName, ".tar.gz") || strings.HasSuffix(assetName, ".tgz") {
+	if strings.HasSuffix(assetName, ".tar.gz") || strings.HasSuffix(assetName, ".tgz") { //nolint:gocritic
 		f, err := os.Open(assetPath)
 		if err != nil {
 			return fmt.Errorf("could not open archive: %w", err)
 		}
-		defer f.Close()
+		defer f.Close() //nolint:errcheck
 		gzr, err := gzip.NewReader(f)
 		if err != nil {
 			return fmt.Errorf("could not create gzip reader: %w", err)
@@ -3875,12 +3681,12 @@ func runUpdateCLI() (err error) {
 				if err != nil {
 					return fmt.Errorf("could not create binary file: %w", err)
 				}
-				if _, err := io.Copy(out, tr); err != nil {
-					out.Close()
+				if _, err := io.Copy(out, tr); err != nil { //nolint:gosec
+					out.Close() //nolint:errcheck,gosec
 					return fmt.Errorf("could not extract binary: %w", err)
 				}
-				out.Close()
-				if err := os.Chmod(binPath, 0755); err != nil {
+				out.Close()                                     //nolint:errcheck,gosec
+				if err := os.Chmod(binPath, 0755); err != nil { //nolint:gosec
 					return fmt.Errorf("could not make binary executable: %w", err)
 				}
 				break
@@ -3891,7 +3697,7 @@ func runUpdateCLI() (err error) {
 		if err != nil {
 			return fmt.Errorf("could not open zip archive: %w", err)
 		}
-		defer zr.Close()
+		defer zr.Close() //nolint:errcheck
 		for _, zf := range zr.File {
 			name := filepath.Base(zf.Name)
 			if name == binaryName || strings.Contains(strings.ToLower(name), "matcha") && !zf.FileInfo().IsDir() {
@@ -3902,17 +3708,17 @@ func runUpdateCLI() (err error) {
 				binPath = filepath.Join(tmpDir, binaryName)
 				out, err := os.Create(binPath)
 				if err != nil {
-					rc.Close()
+					rc.Close() //nolint:errcheck,gosec
 					return fmt.Errorf("could not create binary file: %w", err)
 				}
-				if _, err := io.Copy(out, rc); err != nil {
-					out.Close()
-					rc.Close()
+				if _, err := io.Copy(out, rc); err != nil { //nolint:gosec
+					out.Close() //nolint:errcheck,gosec
+					rc.Close()  //nolint:errcheck,gosec
 					return fmt.Errorf("could not extract binary: %w", err)
 				}
-				out.Close()
-				rc.Close()
-				if err := os.Chmod(binPath, 0755); err != nil {
+				out.Close()                                     //nolint:errcheck,gosec
+				rc.Close()                                      //nolint:errcheck,gosec
+				if err := os.Chmod(binPath, 0755); err != nil { //nolint:gosec
 					return fmt.Errorf("could not make binary executable: %w", err)
 				}
 				break
@@ -3921,7 +3727,7 @@ func runUpdateCLI() (err error) {
 	} else {
 		// For non-archive assets, assume the asset is the binary itself.
 		binPath = assetPath
-		if err := os.Chmod(binPath, 0755); err != nil {
+		if err := os.Chmod(binPath, 0755); err != nil { //nolint:gosec
 			// ignore chmod errors but warn
 			fmt.Printf("warning: could not chmod downloaded binary: %v\n", err)
 		}
@@ -3944,8 +3750,8 @@ func runUpdateCLI() (err error) {
 	if err != nil {
 		return fmt.Errorf("could not open new binary: %w", err)
 	}
-	defer in.Close()
-	out, err := os.OpenFile(tmpNew, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755)
+	defer in.Close()                                                          //nolint:errcheck
+	out, err := os.OpenFile(tmpNew, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) //nolint:gosec
 	if err != nil {
 		return fmt.Errorf("could not create temp binary in target dir: %w", err)
 	}
@@ -4022,7 +3828,7 @@ func parseGlobalFlags(args []string) ([]string, loglevel.Level, bool) {
 	return filtered, level, showLogPanel
 }
 
-func main() {
+func main() { //nolint:gocyclo
 	args, level, showLogPanel := parseGlobalFlags(os.Args)
 	os.Args = args
 	loglevel.Set(level)
@@ -4192,7 +3998,7 @@ func main() {
 	}
 	initialModel.plugins = plugins
 	tui.BodyTransformer = func(body string, email fetcher.Email) string {
-		folder := "INBOX"
+		folder := folderInbox
 		if initialModel.folderInbox != nil {
 			folder = initialModel.folderInbox.GetCurrentFolder()
 		}
@@ -4202,7 +4008,7 @@ func main() {
 	plugins.CallHook(plugin.HookStartup)
 
 	// Background sync macOS features
-	if runtime.GOOS == "darwin" {
+	if runtime.GOOS == goosDarwin {
 		disableNotifications := false
 		if initialModel.config != nil {
 			disableNotifications = initialModel.config.DisableNotifications
@@ -4273,7 +4079,7 @@ func runDaemonStart() {
 		os.Exit(1)
 	}
 
-	cmd := exec.Command(exe, "daemon", "run")
+	cmd := exec.Command(exe, "daemon", "run") //nolint:noctx
 	cmd.Stdout = nil
 	cmd.Stderr = nil
 	cmd.Stdin = nil
@@ -4323,9 +4129,8 @@ func runDaemonStatus() {
 		}
 		return
 	}
-	defer client.Close()
-
 	status, err := client.Status()
+	client.Close() //nolint:errcheck,gosec
 	if err != nil {
 		fmt.Fprintf(os.Stderr, "failed to get status: %v\n", err)
 		os.Exit(1)

notify/notify.go 🔗

@@ -12,9 +12,9 @@ func Send(title, body string) error {
 	switch runtime.GOOS {
 	case "darwin":
 		script := fmt.Sprintf(`display notification %q with title %q sound name "default"`, body, title)
-		return exec.Command("osascript", "-e", script).Run()
+		return exec.Command("osascript", "-e", script).Run() //nolint:noctx
 	case "linux":
-		return exec.Command("notify-send", title, body).Run()
+		return exec.Command("notify-send", title, body).Run() //nolint:noctx
 	default:
 		return nil
 	}

pgp/yubikey.go 🔗

@@ -34,17 +34,17 @@ func openCard() (*openpgp.Card, error) {
 			"failed to connect to PC/SC daemon: %w\n"+
 				"Make sure pcscd is running:\n"+
 				"  sudo systemctl enable --now pcscd.socket\n"+
-				"You may also need the ccid package for USB smartcard support.",
+				"You may also need the ccid package for USB smartcard support",
 			err,
 		)
 	}
 
 	pcscCard, err := pcsc.OpenFirstCard(ctx, filter.HasApplet(iso.AidOpenPGP), true)
 	if err != nil {
-		ctx.Release()
+		ctx.Release() //nolint:errcheck,gosec
 		return nil, fmt.Errorf(
 			"no OpenPGP smartcard found: %w\n"+
-				"Make sure your YubiKey is plugged in and has an OpenPGP key configured.",
+				"Make sure your YubiKey is plugged in and has an OpenPGP key configured",
 			err,
 		)
 	}
@@ -52,8 +52,8 @@ func openCard() (*openpgp.Card, error) {
 	isoCard := iso.NewCard(pcscCard)
 	card, err := openpgp.NewCard(isoCard)
 	if err != nil {
-		pcscCard.Close()
-		ctx.Release()
+		pcscCard.Close() //nolint:errcheck,gosec
+		ctx.Release()    //nolint:errcheck,gosec
 		return nil, fmt.Errorf("failed to initialize OpenPGP card: %w", err)
 	}
 
@@ -69,7 +69,7 @@ func BuildPGPSignedMessage(payload []byte, pin string, publicKeyPath string) ([]
 	if err != nil {
 		return nil, err
 	}
-	defer card.Close()
+	defer card.Close() //nolint:errcheck
 
 	// Verify PIN (PW1 for signing operations)
 	if err := card.VerifyPassword(openpgp.PW1, pin); err != nil {
@@ -231,7 +231,7 @@ func buildSignaturePacket(signedContent []byte, signer crypto.Signer, pubKey *pa
 	body.WriteByte(digest[1])
 
 	// Encode the signature MPIs based on algorithm
-	switch pubKey.PubKeyAlgo {
+	switch pubKey.PubKeyAlgo { //nolint:exhaustive
 	case packet.PubKeyAlgoEdDSA:
 		// EdDSA: raw signature is r || s, 32 bytes each
 		if len(rawSig) != 64 {
@@ -293,7 +293,7 @@ func splitPayload(payload []byte) (headers, body []byte) {
 
 // buildSignedPart constructs the first MIME part content that gets hashed.
 // This must exactly match what appears between the boundary markers.
-func buildSignedPart(headers, body []byte, boundary string) []byte {
+func buildSignedPart(headers, body []byte, _ string) []byte {
 	var originalContentType []byte
 	if len(headers) > 0 {
 		for _, line := range bytes.Split(headers, []byte("\r\n")) {
@@ -395,8 +395,8 @@ func writeMPI(w io.Writer, data []byte) {
 	bitLen := uint16((len(data)-1)*8 + bitLength(data[0]))
 	buf := make([]byte, 2)
 	binary.BigEndian.PutUint16(buf, bitLen)
-	w.Write(buf)  //nolint:errcheck
-	w.Write(data) //nolint:errcheck
+	w.Write(buf)  //nolint:errcheck,gosec
+	w.Write(data) //nolint:errcheck,gosec
 }
 
 // bitLength returns the number of significant bits in a byte.
@@ -411,17 +411,18 @@ func bitLength(b byte) int {
 
 // writeNewFormatLength writes an OpenPGP new-format packet body length.
 func writeNewFormatLength(w *bytes.Buffer, length int) {
-	if length < 192 {
+	switch {
+	case length < 192:
 		w.WriteByte(byte(length))
-	} else if length < 8384 {
+	case length < 8384:
 		length -= 192
 		w.WriteByte(byte(length>>8) + 192)
 		w.WriteByte(byte(length))
-	} else {
+	default:
 		w.WriteByte(255)
 		buf := make([]byte, 4)
 		binary.BigEndian.PutUint32(buf, uint32(length))
-		w.Write(buf)
+		_, _ = w.Write(buf)
 	}
 }
 
@@ -478,7 +479,7 @@ func VerifyYubiKeyAvailable() error {
 	if err != nil {
 		return err
 	}
-	card.Close()
+	card.Close() //nolint:errcheck,gosec
 	return nil
 }
 
@@ -488,27 +489,30 @@ func GetYubiKeyInfo() (string, error) {
 	if err != nil {
 		return "", err
 	}
-	defer card.Close()
+	defer card.Close() //nolint:errcheck
 
 	var info strings.Builder
 
-	aid := card.ApplicationRelated.AID
-	info.WriteString(fmt.Sprintf("Manufacturer: %s\n", aid.Manufacturer))
-	info.WriteString(fmt.Sprintf("Serial:       %X\n", aid.Serial))
-	info.WriteString(fmt.Sprintf("Version:      %s\n", aid.Version))
+	aid := card.AID
+	fmt.Fprintf(&info, "Manufacturer: %s\n", aid.Manufacturer)
+	fmt.Fprintf(&info, "Serial:       %X\n", aid.Serial)
+	fmt.Fprintf(&info, "Version:      %s\n", aid.Version)
 
 	ch, err := card.GetCardholder()
 	if err == nil && ch.Name != "" {
-		info.WriteString(fmt.Sprintf("Cardholder:   %s\n", ch.Name))
+		fmt.Fprintf(&info, "Cardholder:   %s\n", ch.Name)
 	}
 
-	if keys := card.ApplicationRelated.Keys; keys != nil {
+	if keys := card.Keys; keys != nil {
 		if ki, ok := keys[openpgp.KeySign]; ok {
-			info.WriteString(fmt.Sprintf("Sign Key:     %s", ki.AlgAttrs))
-			if ki.Status == openpgp.KeyGenerated {
+			fmt.Fprintf(&info, "Sign Key:     %s", ki.AlgAttrs)
+			switch ki.Status {
+			case openpgp.KeyGenerated:
 				info.WriteString(" (generated)")
-			} else if ki.Status == openpgp.KeyImported {
+			case openpgp.KeyImported:
 				info.WriteString(" (imported)")
+			case openpgp.KeyNotPresent:
+				// no key on card
 			}
 			info.WriteString("\n")
 		}

pgp/yubikey_test.go 🔗

@@ -37,7 +37,6 @@ func TestParseASN1Signature_TruncatedDoesNotPanic(t *testing.T) {
 	}
 
 	for _, tc := range cases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			// The test must not panic: the fix replaces panics with errors.
 			defer func() {

plugin/api.go 🔗

@@ -36,7 +36,7 @@ func (m *Manager) registerAPI() {
 }
 
 // matcha.on(event, callback) — register a hook callback.
-func (m *Manager) luaOn(L *lua.LState) int {
+func (m *Manager) luaOn(L *lua.LState) int { //nolint:gocritic
 	event := L.CheckString(1)
 	fn := L.CheckFunction(2)
 	m.registerHook(event, fn)
@@ -44,7 +44,7 @@ func (m *Manager) luaOn(L *lua.LState) int {
 }
 
 // matcha.log(msg) — log a message to stderr.
-func (m *Manager) luaLog(L *lua.LState) int {
+func (m *Manager) luaLog(L *lua.LState) int { //nolint:gocritic
 	msg := L.CheckString(1)
 	log.Printf("[plugin] %s", msg)
 	return 0
@@ -52,7 +52,7 @@ func (m *Manager) luaLog(L *lua.LState) int {
 
 // matcha.set_status(area, text) — set a persistent status string for a view area.
 // Valid areas: "inbox", "composer", "email_view".
-func (m *Manager) luaSetStatus(L *lua.LState) int {
+func (m *Manager) luaSetStatus(L *lua.LState) int { //nolint:gocritic
 	area := L.CheckString(1)
 	text := L.CheckString(2)
 	m.statuses[area] = text
@@ -61,7 +61,7 @@ func (m *Manager) luaSetStatus(L *lua.LState) int {
 
 // matcha.notify(msg [, seconds]) — show a temporary notification in the TUI.
 // The optional second argument sets the display duration in seconds (default 2).
-func (m *Manager) luaNotify(L *lua.LState) int {
+func (m *Manager) luaNotify(L *lua.LState) int { //nolint:gocritic
 	m.pendingNotification = L.CheckString(1)
 	m.pendingDuration = float64(L.OptNumber(2, 2))
 	return 0
@@ -69,7 +69,7 @@ func (m *Manager) luaNotify(L *lua.LState) int {
 
 // matcha.bind_key(key, area, description, callback) — register a custom keyboard shortcut.
 // Valid areas: "inbox", "email_view", "composer".
-func (m *Manager) luaBindKey(L *lua.LState) int {
+func (m *Manager) luaBindKey(L *lua.LState) int { //nolint:gocritic
 	key := L.CheckString(1)
 	area := L.CheckString(2)
 	description := L.CheckString(3)
@@ -102,7 +102,7 @@ func (m *Manager) luaBindKey(L *lua.LState) int {
 //	        return matcha.style(m, {color = "#ff0000", bold = true})
 //	    end))
 //	end)
-func (m *Manager) luaStyle(L *lua.LState) int {
+func (m *Manager) luaStyle(L *lua.LState) int { //nolint:gocritic
 	text := L.CheckString(1)
 	opts := L.OptTable(2, nil)
 
@@ -145,7 +145,7 @@ func (m *Manager) luaStyle(L *lua.LState) int {
 // plugin. spec is a table mapping setting key -> { type, default, label,
 // description }. Valid types: "boolean", "number", "string". Must be called
 // while the plugin file is being loaded (typically at the top level).
-func (m *Manager) luaSettings(L *lua.LState) int {
+func (m *Manager) luaSettings(L *lua.LState) int { //nolint:gocritic
 	spec := L.CheckTable(1)
 	return m.declareSettings(L, spec)
 }
@@ -153,13 +153,13 @@ func (m *Manager) luaSettings(L *lua.LState) int {
 // matcha.get_setting(key [, plugin_name]) — return the current value of a
 // setting. The optional second argument allows reading another plugin's
 // setting; defaults to the current plugin when called during load.
-func (m *Manager) luaGetSetting(L *lua.LState) int {
+func (m *Manager) luaGetSetting(L *lua.LState) int { //nolint:gocritic
 	return m.getSetting(L)
 }
 
 // matcha.mark_read(uid, account_id, folder) — queue a mark-as-read op for the given email.
 // The orchestrator dispatches the IMAP/backend call after the hook or keybinding returns.
-func (m *Manager) luaMarkRead(L *lua.LState) int {
+func (m *Manager) luaMarkRead(L *lua.LState) int { //nolint:gocritic
 	uid := uint32(L.CheckInt(1))
 	accountID := L.CheckString(2)
 	folder := L.CheckString(3)
@@ -168,7 +168,7 @@ func (m *Manager) luaMarkRead(L *lua.LState) int {
 }
 
 // matcha.mark_unread(uid, account_id, folder) — queue a mark-as-unread op for the given email.
-func (m *Manager) luaMarkUnread(L *lua.LState) int {
+func (m *Manager) luaMarkUnread(L *lua.LState) int { //nolint:gocritic
 	uid := uint32(L.CheckInt(1))
 	accountID := L.CheckString(2)
 	folder := L.CheckString(3)
@@ -178,14 +178,14 @@ func (m *Manager) luaMarkUnread(L *lua.LState) int {
 
 // matcha.suppress_auto_read() — prevent the currently viewed email from being
 // automatically marked as read. Must be called inside an email_viewed callback.
-func (m *Manager) luaSuppressAutoRead(L *lua.LState) int {
+func (m *Manager) luaSuppressAutoRead(L *lua.LState) int { //nolint:gocritic
 	m.suppressAutoRead = true
 	return 0
 }
 
 // matcha.set_compose_field(field, value) — set a compose field value.
 // Valid fields: "to", "cc", "bcc", "subject", "body".
-func (m *Manager) luaSetComposeField(L *lua.LState) int {
+func (m *Manager) luaSetComposeField(L *lua.LState) int { //nolint:gocritic
 	field := L.CheckString(1)
 	value := L.CheckString(2)
 

plugin/http.go 🔗

@@ -25,7 +25,7 @@ var httpClient = httpclient.New(httpclient.PluginCallTimeout)
 //
 // Returns (response_table, nil) on success or (nil, error_string) on failure.
 // response_table has fields: status (number), body (string), headers (table).
-func (m *Manager) luaHTTP(L *lua.LState) int {
+func (m *Manager) luaHTTP(L *lua.LState) int { //nolint:gocritic
 	opts := L.CheckTable(1)
 
 	// URL (required).
@@ -64,7 +64,7 @@ func (m *Manager) luaHTTP(L *lua.LState) int {
 		bodyReader = strings.NewReader(v.String())
 	}
 
-	req, err := http.NewRequest(method, rawURL, bodyReader)
+	req, err := http.NewRequest(method, rawURL, bodyReader) //nolint:noctx
 	if err != nil {
 		L.Push(lua.LNil)
 		L.Push(lua.LString(err.Error()))
@@ -86,7 +86,7 @@ func (m *Manager) luaHTTP(L *lua.LState) int {
 		L.Push(lua.LString(err.Error()))
 		return 2
 	}
-	defer resp.Body.Close()
+	defer resp.Body.Close() //nolint:errcheck
 
 	body, err := io.ReadAll(io.LimitReader(resp.Body, httpMaxBodySize))
 	if err != nil {

plugin/http_test.go 🔗

@@ -17,11 +17,11 @@ func newTestManager() *Manager {
 
 func TestHTTPGet(t *testing.T) {
 	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		if r.Method != "GET" {
+		if r.Method != http.MethodGet {
 			t.Errorf("expected GET, got %s", r.Method)
 		}
 		w.Header().Set("X-Test", "hello")
-		w.WriteHeader(200)
+		w.WriteHeader(http.StatusOK)
 		w.Write([]byte("ok"))
 	}))
 	defer srv.Close()
@@ -63,7 +63,7 @@ func TestHTTPGet(t *testing.T) {
 
 func TestHTTPPostWithBodyAndHeaders(t *testing.T) {
 	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		if r.Method != "POST" {
+		if r.Method != http.MethodPost {
 			t.Errorf("expected POST, got %s", r.Method)
 		}
 		if ct := r.Header.Get("Content-Type"); ct != "application/json" {

plugin/prompt.go 🔗

@@ -15,7 +15,7 @@ type PendingPrompt struct {
 // luaPrompt implements matcha.prompt(placeholder, callback).
 // It requests a text input overlay in the TUI. When the user submits,
 // the callback is called with their input string.
-func (m *Manager) luaPrompt(L *lua.LState) int {
+func (m *Manager) luaPrompt(L *lua.LState) int { //nolint:gocritic
 	placeholder := L.CheckString(1)
 	fn := L.CheckFunction(2)
 

plugin/settings.go 🔗

@@ -43,7 +43,7 @@ type PluginSettings struct {
 //	matcha.on("email_received", function(email)
 //	    if cfg.enabled and #email.subject > cfg.threshold then ... end
 //	end)
-func (m *Manager) declareSettings(L *lua.LState, spec *lua.LTable) int {
+func (m *Manager) declareSettings(L *lua.LState, spec *lua.LTable) int { //nolint:gocritic
 	if m.currentPlugin == "" {
 		L.RaiseError("matcha.settings() must be called from a plugin file")
 		return 0
@@ -137,7 +137,7 @@ func (m *Manager) declareSettings(L *lua.LState, spec *lua.LTable) int {
 // load (e.g. inside a hook callback), it falls back to the plugin that owns
 // the running closure — for now we use currentPlugin and only allow lookups
 // to the plugin that declared the schema by name.
-func (m *Manager) getSetting(L *lua.LState) int {
+func (m *Manager) getSetting(L *lua.LState) int { //nolint:gocritic
 	plugin := m.currentPlugin
 	key := L.CheckString(1)
 
@@ -177,7 +177,7 @@ func (m *Manager) lookupValue(plugin, key string, def SettingDef) interface{} {
 	return def.Default
 }
 
-func toLuaValue(L *lua.LState, v interface{}) lua.LValue {
+func toLuaValue(_ *lua.LState, v interface{}) lua.LValue {
 	switch x := v.(type) {
 	case bool:
 		return lua.LBool(x)

plugin/storage.go 🔗

@@ -16,6 +16,10 @@ import (
 
 var validPluginStoreName = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
 
+// ErrNoActivePlugin is returned when a storage operation is attempted without
+// an active plugin context.
+var ErrNoActivePlugin = errors.New("plugin: no active plugin")
+
 type pluginStore struct {
 	path string
 	mu   sync.Mutex
@@ -75,14 +79,14 @@ func (s *pluginStore) flush() error {
 		return err
 	}
 	tmpPath := tmp.Name()
-	defer os.Remove(tmpPath)
+	defer os.Remove(tmpPath) //nolint:errcheck
 
 	if _, err := tmp.Write(raw); err != nil {
-		tmp.Close()
+		tmp.Close() //nolint:errcheck,gosec
 		return err
 	}
 	if err := os.Chmod(tmpPath, 0o600); err != nil {
-		tmp.Close()
+		tmp.Close() //nolint:errcheck,gosec
 		return err
 	}
 	if err := tmp.Close(); err != nil {
@@ -131,7 +135,7 @@ func (s *pluginStore) Keys() []string {
 
 func (m *Manager) currentStore() (*pluginStore, error) {
 	if m.currentPlugin == "" {
-		return nil, nil
+		return nil, ErrNoActivePlugin
 	}
 	if m.stores == nil {
 		m.stores = make(map[string]*pluginStore)
@@ -148,17 +152,17 @@ func (m *Manager) currentStore() (*pluginStore, error) {
 	return s, nil
 }
 
-func (m *Manager) luaStoreSet(L *lua.LState) int {
+func (m *Manager) luaStoreSet(L *lua.LState) int { //nolint:gocritic
 	key := L.CheckString(1)
 	val := L.CheckString(2)
 
 	s, err := m.currentStore()
-	if err != nil {
-		L.RaiseError("store_set: %v", err)
+	if errors.Is(err, ErrNoActivePlugin) {
+		L.RaiseError("store_set: no plugin context")
 		return 0
 	}
-	if s == nil {
-		L.RaiseError("store_set: no plugin context")
+	if err != nil {
+		L.RaiseError("store_set: %v", err)
 		return 0
 	}
 	if err := s.Set(key, val); err != nil {
@@ -167,18 +171,18 @@ func (m *Manager) luaStoreSet(L *lua.LState) int {
 	return 0
 }
 
-func (m *Manager) luaStoreGet(L *lua.LState) int {
+func (m *Manager) luaStoreGet(L *lua.LState) int { //nolint:gocritic
 	key := L.CheckString(1)
 
 	s, err := m.currentStore()
+	if errors.Is(err, ErrNoActivePlugin) {
+		L.Push(lua.LNil)
+		return 1
+	}
 	if err != nil {
 		L.RaiseError("store_get: %v", err)
 		return 0
 	}
-	if s == nil {
-		L.Push(lua.LNil)
-		return 1
-	}
 	if v, ok := s.Get(key); ok {
 		L.Push(lua.LString(v))
 	} else {
@@ -187,37 +191,33 @@ func (m *Manager) luaStoreGet(L *lua.LState) int {
 	return 1
 }
 
-func (m *Manager) luaStoreDelete(L *lua.LState) int {
+func (m *Manager) luaStoreDelete(L *lua.LState) int { //nolint:gocritic
 	key := L.CheckString(1)
 
 	s, err := m.currentStore()
+	if errors.Is(err, ErrNoActivePlugin) {
+		return 0 // silent no-op outside plugin context, matching store_get behavior
+	}
 	if err != nil {
 		L.RaiseError("store_delete: %v", err)
 		return 0
 	}
-	// No plugin context: silently no-op, matching store_get's behavior so
-	// read+remove operations behave the same when called outside a plugin
-	// (e.g. from a non-plugin Lua chunk). store_set still raises so a
-	// missing-context write is surfaced loudly.
-	if s == nil {
-		return 0
-	}
 	if err := s.Delete(key); err != nil {
 		L.RaiseError("store_delete: %v", err)
 	}
 	return 0
 }
 
-func (m *Manager) luaStoreKeys(L *lua.LState) int {
+func (m *Manager) luaStoreKeys(L *lua.LState) int { //nolint:gocritic
 	s, err := m.currentStore()
+	if errors.Is(err, ErrNoActivePlugin) {
+		L.Push(L.NewTable())
+		return 1
+	}
 	if err != nil {
 		L.RaiseError("store_keys: %v", err)
 		return 0
 	}
-	if s == nil {
-		L.Push(L.NewTable())
-		return 1
-	}
 
 	t := L.NewTable()
 	for i, key := range s.Keys() {

plugins/embed.go 🔗

@@ -24,11 +24,11 @@ type PluginEntry struct {
 // FetchRegistry fetches the plugin registry from GitHub.
 func FetchRegistry() ([]PluginEntry, error) {
 	client := httpclient.New(httpclient.RegistryFetchTimeout)
-	resp, err := client.Get(RegistryURL)
+	resp, err := client.Get(RegistryURL) //nolint:noctx
 	if err != nil {
 		return nil, fmt.Errorf("failed to fetch registry: %w", err)
 	}
-	defer resp.Body.Close()
+	defer resp.Body.Close() //nolint:errcheck
 
 	if resp.StatusCode != http.StatusOK {
 		return nil, fmt.Errorf("registry returned status %d", resp.StatusCode)
@@ -55,11 +55,11 @@ func FetchPlugin(entry PluginEntry) ([]byte, error) {
 	}
 
 	client := httpclient.New(httpclient.RegistryFetchTimeout)
-	resp, err := client.Get(url)
+	resp, err := client.Get(url) //nolint:noctx
 	if err != nil {
 		return nil, fmt.Errorf("failed to fetch plugin: %w", err)
 	}
-	defer resp.Body.Close()
+	defer resp.Body.Close() //nolint:errcheck
 
 	if resp.StatusCode != http.StatusOK {
 		return nil, fmt.Errorf("plugin download returned status %d", resp.StatusCode)

screenshots/cmd/inbox_view/main.go 🔗

@@ -14,6 +14,11 @@ import (
 	"github.com/floatpane/matcha/tui"
 )
 
+const (
+	demoUserID    = "demo-user"
+	demoUserEmail = "matcha@floatpane.com"
+)
+
 // wrapper forwards all messages to the FolderInbox and ensures it renders correctly.
 type wrapper struct {
 	folderInbox *tui.FolderInbox
@@ -42,10 +47,10 @@ func main() {
 
 	accounts := []config.Account{
 		{
-			ID:         "demo-user",
+			ID:         demoUserID,
 			Name:       "Matcha Client",
-			Email:      "matcha@floatpane.com",
-			FetchEmail: "matcha@floatpane.com",
+			Email:      demoUserEmail,
+			FetchEmail: demoUserEmail,
 		},
 	}
 
@@ -53,20 +58,20 @@ func main() {
 		{
 			UID:       1012,
 			From:      "Alice Park <alice.park@example.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "Quick sync on the API migration?",
 			Date:      now.Add(-12 * time.Minute),
 			MessageID: "<api-migration-012@example.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 		{
 			UID:       1011,
 			From:      "GitHub <notifications@github.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "[floatpane/matcha] Fix: resolve inbox pagination issue (#281)",
 			Date:      now.Add(-47 * time.Minute),
 			MessageID: "<gh-notif-281@github.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 		{
 			UID:       1010,
@@ -75,7 +80,7 @@ func main() {
 			Subject:   "New Dashboard Redesign - Preview & Feedback",
 			Date:      now.Add(-2 * time.Hour),
 			MessageID: "<dashboard-redesign-001@example.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 			Attachments: []fetcher.Attachment{
 				{Filename: "dashboard-mockup.png", MIMEType: "image/png"},
 			},
@@ -83,29 +88,29 @@ func main() {
 		{
 			UID:       1009,
 			From:      "David Kim <david.kim@example.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "Re: Quarterly budget review notes",
 			Date:      now.Add(-5 * time.Hour),
 			MessageID: "<budget-review-009@example.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 		{
 			UID:       1008,
 			From:      "Stripe <receipts@stripe.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "Your receipt from Acme Corp - Invoice #4821",
 			Date:      now.Add(-23 * time.Hour),
 			MessageID: "<stripe-receipt-4821@stripe.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 		{
 			UID:       1007,
 			From:      "Maria Gonzalez <maria.g@example.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "Design system tokens - final version attached",
 			Date:      now.Add(-1*24*time.Hour - 6*time.Hour),
 			MessageID: "<design-tokens-007@example.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 			Attachments: []fetcher.Attachment{
 				{Filename: "design-tokens-v3.json", MIMEType: "application/json"},
 			},
@@ -113,56 +118,56 @@ func main() {
 		{
 			UID:       1006,
 			From:      "Linear <notifications@linear.app>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "MAT-342: Implement keyboard shortcuts for compose view",
 			Date:      now.Add(-2*24*time.Hour - 3*time.Hour),
 			MessageID: "<linear-342@linear.app>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 		{
 			UID:       1005,
 			From:      "James Wright <j.wright@example.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "Onboarding docs are ready for review",
 			Date:      now.Add(-3*24*time.Hour - 1*time.Hour),
 			MessageID: "<onboarding-005@example.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 		{
 			UID:       1004,
 			From:      "Vercel <notifications@vercel.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "Deployment successful: matcha-docs-8f3a2b1",
 			Date:      now.Add(-4*24*time.Hour - 8*time.Hour),
 			MessageID: "<vercel-deploy-004@vercel.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 		{
 			UID:       1003,
 			From:      "Lena Muller <lena.m@example.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "Conference talk proposal - Rethinking TUI Design",
 			Date:      now.Add(-5*24*time.Hour - 2*time.Hour),
 			MessageID: "<conference-003@example.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 		{
 			UID:       1002,
 			From:      "GitHub <notifications@github.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "[floatpane/matcha] Release v1.4.0 published",
 			Date:      now.Add(-5*24*time.Hour - 14*time.Hour),
 			MessageID: "<gh-release-140@github.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 		{
 			UID:       1001,
 			From:      "Omar Hassan <omar.h@example.com>",
-			To:        []string{"matcha@floatpane.com"},
+			To:        []string{demoUserEmail},
 			Subject:   "Re: Open source contribution guidelines",
 			Date:      now.Add(-6*24*time.Hour - 5*time.Hour),
 			MessageID: "<oss-contrib-001@example.com>",
-			AccountID: "demo-user",
+			AccountID: demoUserID,
 		},
 	}
 

sender/sender.go 🔗

@@ -128,7 +128,7 @@ func containsMarkup(body string) bool {
 	doc := md.Parser().Parse(reader)
 
 	var hasMarkup bool
-	ast.Walk(doc, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
+	ast.Walk(doc, func(node ast.Node, entering bool) (ast.WalkStatus, error) { //nolint:errcheck,gosec
 		if !entering {
 			return ast.WalkContinue, nil
 		}
@@ -197,7 +197,7 @@ func writeQuotedPrintable(w io.Writer, body string) error {
 }
 
 // SendEmail constructs a multipart message with plain text, HTML, embedded images, and attachments.
-func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody, htmlBody string, images map[string][]byte, attachments map[string][]byte, inReplyTo string, references []string, signSMIME bool, encryptSMIME bool, signPGP bool, encryptPGP bool) ([]byte, error) {
+func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody, htmlBody string, images map[string][]byte, attachments map[string][]byte, inReplyTo string, references []string, signSMIME bool, encryptSMIME bool, signPGP bool, encryptPGP bool) ([]byte, error) { //nolint:gocyclo
 	smtpServer := account.GetSMTPServer()
 	smtpPort := account.GetSMTPPort()
 
@@ -355,7 +355,6 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 				msg.Write(encodedBody)
 			}
 		}
-
 	} else {
 		// --- Non-plaintext path: build multipart/mixed with related/alternative, images and attachments ---
 		var innerMsg bytes.Buffer
@@ -371,7 +370,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 			return nil, err
 		}
 		relatedWriter := multipart.NewWriter(relatedPartWriter)
-		relatedWriter.SetBoundary(relatedBoundary)
+		relatedWriter.SetBoundary(relatedBoundary) //nolint:errcheck,gosec
 
 		// --- Alternative Part (text and html) ---
 		altHeader := textproto.MIMEHeader{}
@@ -382,7 +381,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 			return nil, err
 		}
 		altWriter := multipart.NewWriter(altPartWriter)
-		altWriter.SetBoundary(altBoundary)
+		altWriter.SetBoundary(altBoundary) //nolint:errcheck,gosec
 
 		// Plain text part
 		textHeader := textproto.MIMEHeader{
@@ -410,7 +409,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 			return nil, err
 		}
 
-		altWriter.Close() // Finish the alternative part
+		altWriter.Close() //nolint:errcheck,gosec
 
 		// --- Inline Images ---
 		for cid, data := range images {
@@ -432,10 +431,10 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 			}
 			// Encode raw image bytes to base64, then wrap at 76 chars per MIME rules
 			encodedImg := base64.StdEncoding.EncodeToString(data)
-			imgPart.Write([]byte(clib.WrapBase64(encodedImg)))
+			imgPart.Write([]byte(clib.WrapBase64(encodedImg))) //nolint:errcheck,gosec
 		}
 
-		relatedWriter.Close() // Finish the related part
+		relatedWriter.Close() //nolint:errcheck,gosec
 
 		// --- Attachments ---
 		for filename, data := range attachments {
@@ -455,10 +454,10 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 			}
 			encodedData := base64.StdEncoding.EncodeToString(data)
 			// MIME requires base64 to be line-wrapped at 76 characters
-			attachmentPart.Write([]byte(clib.WrapBase64(encodedData)))
+			attachmentPart.Write([]byte(clib.WrapBase64(encodedData))) //nolint:errcheck,gosec
 		}
 
-		innerWriter.Close() // Finish the inner message
+		innerWriter.Close() //nolint:errcheck,gosec
 
 		innerBodyBytes = append([]byte(innerHeaders), innerMsg.Bytes()...)
 
@@ -641,13 +640,14 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 		allRecipients = append(allRecipients, bcc...)
 
 		var toEncrypt []byte
-		if len(pgpPayload) > 0 {
+		switch {
+		case len(pgpPayload) > 0:
 			// Encrypt the signed message
 			toEncrypt = pgpPayload
-		} else if len(payloadToEncrypt) > 0 {
+		case len(payloadToEncrypt) > 0:
 			// Encrypt pre-prepared payload
 			toEncrypt = payloadToEncrypt
-		} else {
+		default:
 			// Encrypt what we've built so far
 			toEncrypt = msg.Bytes()
 		}
@@ -670,7 +670,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 
 	tlsConfig := &tls.Config{
 		ServerName:         smtpServer,
-		InsecureSkipVerify: account.Insecure,
+		InsecureSkipVerify: account.Insecure, //nolint:gosec
 		MinVersion:         tls.VersionTLS12,
 		ClientSessionCache: account.GetClientSessionCache(),
 		VerifyConnection: func(cs tls.ConnectionState) error {
@@ -684,13 +684,13 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 	// Port 465 uses implicit TLS (the connection starts with TLS).
 	// All other ports use plain TCP with optional STARTTLS upgrade.
 	if smtpPort == 465 {
-		conn, err := tls.Dial("tcp", addr, tlsConfig)
+		conn, err := tls.Dial("tcp", addr, tlsConfig) //nolint:noctx
 		if err != nil {
 			return nil, err
 		}
 		c, err = smtp.NewClient(conn, smtpServer)
 		if err != nil {
-			conn.Close()
+			conn.Close() //nolint:errcheck,gosec
 			return nil, err
 		}
 	} else {
@@ -700,7 +700,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 			return nil, err
 		}
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if err = c.Hello(smtpHelloHostname()); err != nil {
 		return nil, err
@@ -720,18 +720,19 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 	if ok, mechs := c.Extension("AUTH"); ok {
 		mechList := strings.ToUpper(mechs)
 
-		if account.IsOAuth2() {
+		switch {
+		case account.IsOAuth2():
 			// Use XOAUTH2 for OAuth2-enabled accounts
 			token, tokenErr := config.GetOAuth2Token(account.Email)
 			if tokenErr != nil {
 				return nil, fmt.Errorf("oauth2: %w", tokenErr)
 			}
 			err = c.Auth(&xoauth2Auth{username: account.Email, token: token})
-		} else if strings.Contains(mechList, "PLAIN") {
+		case strings.Contains(mechList, "PLAIN"):
 			err = c.Auth(plainAuth)
-		} else if strings.Contains(mechList, "LOGIN") {
+		case strings.Contains(mechList, "LOGIN"):
 			err = c.Auth(loginAuthFallback)
-		} else {
+		default:
 			// Fall back to PLAIN and let the server decide
 			err = c.Auth(plainAuth)
 		}
@@ -778,7 +779,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
 // Google Calendar requires:
 // - multipart/alternative with text/plain + text/calendar; method=REPLY
 // - text/calendar part must NOT be Content-Disposition: attachment
-func SendCalendarReply(account *config.Account, to []string, subject, plainBody string, icsData []byte, inReplyTo string, references []string) ([]byte, error) {
+func SendCalendarReply(account *config.Account, to []string, subject, plainBody string, icsData []byte, inReplyTo string, references []string) ([]byte, error) { //nolint:gocyclo
 	smtpServer := account.GetSMTPServer()
 	smtpPort := account.GetSMTPPort()
 
@@ -890,7 +891,7 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody
 
 	tlsConfig := &tls.Config{
 		ServerName:         smtpServer,
-		InsecureSkipVerify: account.Insecure,
+		InsecureSkipVerify: account.Insecure, //nolint:gosec
 		MinVersion:         tls.VersionTLS12,
 		ClientSessionCache: account.GetClientSessionCache(),
 		VerifyConnection: func(cs tls.ConnectionState) error {
@@ -902,13 +903,13 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody
 	var c *smtp.Client
 
 	if smtpPort == 465 {
-		conn, err := tls.Dial("tcp", addr, tlsConfig)
+		conn, err := tls.Dial("tcp", addr, tlsConfig) //nolint:noctx
 		if err != nil {
 			return nil, err
 		}
 		c, err = smtp.NewClient(conn, smtpServer)
 		if err != nil {
-			conn.Close()
+			conn.Close() //nolint:errcheck,gosec
 			return nil, err
 		}
 	} else {
@@ -918,7 +919,7 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody
 			return nil, err
 		}
 	}
-	defer c.Close()
+	defer c.Close() //nolint:errcheck
 
 	if err = c.Hello(smtpHelloHostname()); err != nil {
 		return nil, err
@@ -934,17 +935,18 @@ func SendCalendarReply(account *config.Account, to []string, subject, plainBody
 
 	if ok, mechs := c.Extension("AUTH"); ok {
 		mechList := strings.ToUpper(mechs)
-		if account.IsOAuth2() {
+		switch {
+		case account.IsOAuth2():
 			token, tokenErr := config.GetOAuth2Token(account.Email)
 			if tokenErr != nil {
 				return nil, fmt.Errorf("oauth2: %w", tokenErr)
 			}
 			err = c.Auth(&xoauth2Auth{username: account.Email, token: token})
-		} else if strings.Contains(mechList, "PLAIN") {
+		case strings.Contains(mechList, "PLAIN"):
 			err = c.Auth(plainAuth)
-		} else if strings.Contains(mechList, "LOGIN") {
+		case strings.Contains(mechList, "LOGIN"):
 			err = c.Auth(loginAuthFallback)
-		} else {
+		default:
 			err = c.Auth(plainAuth)
 		}
 		if err != nil {
@@ -1162,7 +1164,7 @@ func encryptEmailPGP(payload []byte, recipients []string, account *config.Accoun
 			if entities == nil {
 				entities, _ = openpgp.ReadKeyRing(bytes.NewReader(senderKey))
 			}
-			if entities != nil && len(entities) > 0 {
+			if len(entities) > 0 {
 				entityList = append(entityList, entities[0])
 			}
 		}

tests/integration/imap_test.go 🔗

@@ -72,7 +72,7 @@ func waitForGreenmail(t *testing.T, env testEnv) {
 	deadline := time.Now().Add(60 * time.Second)
 	url := fmt.Sprintf("http://%s:%d/api/configuration", env.host, env.apiPort)
 	for time.Now().Before(deadline) {
-		resp, err := http.Get(url) //nolint:gosec
+		resp, err := http.Get(url)
 		if err == nil && resp.StatusCode == http.StatusOK {
 			resp.Body.Close()
 			return

theme/theme.go 🔗

@@ -226,7 +226,7 @@ func SetTheme(name string) bool {
 
 // AllThemes returns all available themes (built-in + custom).
 func AllThemes() []Theme {
-	all := make([]Theme, len(BuiltinThemes))
+	all := make([]Theme, len(BuiltinThemes)) //nolint:prealloc
 	copy(all, BuiltinThemes)
 	all = append(all, LoadCustomThemes()...)
 	return all

tui/choice.go 🔗

@@ -78,12 +78,12 @@ func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		switch msg.String() {
 		case "up", kb.Global.NavUp:
 			m.cursor = (m.cursor - 1 + len(m.choices)) % len(m.choices)
-		case "down", kb.Global.NavDown:
+		case keyDown, kb.Global.NavDown:
 			m.cursor = (m.cursor + 1) % len(m.choices)
-		case "enter":
+		case keyEnter:
 			// Use cursor index instead of string comparison
 			idx := m.cursor
-			if idx == 0 {
+			if idx == 0 { //nolint:gocritic
 				// Inbox
 				return m, func() tea.Msg { return GoToInboxMsg{} }
 			} else if idx == 1 {
@@ -99,7 +99,6 @@ func (m Choice) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				// Settings
 				return m, func() tea.Msg { return GoToSettingsMsg{} }
 			}
-
 		}
 	}
 

tui/composer.go 🔗

@@ -26,11 +26,9 @@ var (
 var (
 	focusedStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
 	blurredStyle        = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
-	noStyle             = lipgloss.NewStyle()
 	helpStyle           = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
 	emailRecipientStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
 	attachmentStyle     = lipgloss.NewStyle().PaddingLeft(4).Foreground(lipgloss.Color("245"))
-	fromSelectorStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
 	smimeToggleStyle    = lipgloss.NewStyle().PaddingLeft(4).Foreground(lipgloss.Color("245"))
 	composerErrorStyle  = lipgloss.NewStyle().PaddingLeft(2).Foreground(lipgloss.Color("196"))
 )
@@ -214,7 +212,7 @@ func (m *Composer) hideComposerNotice() {
 	m.noticeText = ""
 }
 
-func (m *Composer) validateFromField() bool {
+func (m *Composer) validateFromField() bool { //nolint:unparam
 	if !m.isCatchAllAccount() {
 		m.fromError = ""
 		return true
@@ -229,7 +227,7 @@ func (m *Composer) validateFromField() bool {
 	return true
 }
 
-func (m *Composer) validateEmailField(focus int) bool {
+func (m *Composer) validateEmailField(focus int) bool { //nolint:unparam
 	var input *textinput.Model
 	var setError func(string)
 	switch focus {
@@ -412,7 +410,7 @@ func truncateSuggestionDisplay(s string, maxLen int) string {
 	return string(runes[:maxLen-3]) + "..."
 }
 
-func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo
 	var cmds []tea.Cmd
 	var cmd tea.Cmd
 
@@ -478,22 +476,23 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					m.selectedSuggestion--
 				}
 				return m, nil
-			case "down", "ctrl+n":
+			case keyDown, "ctrl+n":
 				if m.selectedSuggestion < len(m.suggestions)-1 {
 					m.selectedSuggestion++
 				}
 				return m, nil
-			case "tab", "enter":
+			case "tab", keyEnter:
 				// Select the suggestion
 				selected := m.suggestions[m.selectedSuggestion]
 
 				var newEmail string
-				if len(selected.Addresses) > 0 {
+				switch {
+				case len(selected.Addresses) > 0:
 					// Mailing list: emit just the addresses to maintain valid email formatting
 					newEmail = strings.Join(selected.Addresses, ", ")
-				} else if selected.Name != "" && selected.Name != selected.Email {
+				case selected.Name != "" && selected.Name != selected.Email:
 					newEmail = fmt.Sprintf("%s <%s>", selected.Name, selected.Email)
-				} else {
+				default:
 					newEmail = selected.Email
 				}
 
@@ -535,7 +534,7 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		// Handle plugin prompt overlay
 		if m.showPluginPrompt {
 			switch msg.String() {
-			case "enter":
+			case keyEnter:
 				value := m.pluginPromptInput.Value()
 				m.showPluginPrompt = false
 				return m, func() tea.Msg { return PluginPromptSubmitMsg{Value: value} }
@@ -556,12 +555,12 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					m.selectedAccountIdx--
 					m.updateSignature()
 				}
-			case "down", "j":
+			case keyDown, "j":
 				if m.selectedAccountIdx < len(m.accounts)-1 {
 					m.selectedAccountIdx++
 					m.updateSignature()
 				}
-			case "enter":
+			case keyEnter:
 				m.showAccountPicker = false
 			case "esc":
 				m.showAccountPicker = false
@@ -583,7 +582,7 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 		if m.showNotice {
 			switch msg.String() {
-			case "enter", "esc", " ":
+			case keyEnter, "esc", " ":
 				m.hideComposerNotice()
 			}
 			return m, nil
@@ -596,7 +595,7 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			case "up", kb.Global.NavUp:
 				m.attachmentCursor = (m.attachmentCursor - 1 + attachmentPathSize) % attachmentPathSize
 				return m, nil
-			case "down", kb.Global.NavDown:
+			case keyDown, kb.Global.NavDown:
 				m.attachmentCursor = (m.attachmentCursor + 1) % attachmentPathSize
 				return m, nil
 			}
@@ -672,10 +671,10 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				return m, nil
 			}
 
-		case "enter", " ":
+		case keyEnter, " ":
 			switch m.focusIndex {
 			case focusFrom:
-				if msg.String() == "enter" && len(m.accounts) > 1 {
+				if msg.String() == keyEnter && len(m.accounts) > 1 {
 					m.showAccountPicker = true
 					return m, nil
 				}
@@ -684,17 +683,17 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				}
 				return m, nil
 			case focusAttachment:
-				if msg.String() == "enter" {
+				if msg.String() == keyEnter {
 					return m, func() tea.Msg { return GoToFilePickerMsg{} }
 				}
 			case focusEncryptSMIME:
-				if msg.String() == "enter" || msg.String() == " " {
+				if msg.String() == keyEnter || msg.String() == " " {
 					m.encryptSMIME = !m.encryptSMIME
 				}
 				return m, nil
 
 			case focusSend:
-				if msg.String() == "enter" {
+				if msg.String() == keyEnter {
 					if !m.canSendEmail() {
 						return m, m.showComposerNotice(t("composer.invalid_email_fields"))
 					}
@@ -798,21 +797,21 @@ func (m *Composer) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	return m, tea.Batch(cmds...)
 }
 
-func (m *Composer) View() tea.View {
+func (m *Composer) View() tea.View { //nolint:gocyclo
 	var composerView strings.Builder
 	var button string
 	ck := config.Keybinds.Composer
 
 	if m.focusIndex == focusSend {
-		button = focusedStyle.Copy().Render("[ " + t("composer.send") + " ]")
+		button = focusedStyle.Render("[ " + t("composer.send") + " ]")
 	} else {
-		button = blurredStyle.Copy().Render("[ " + t("composer.send") + " ]")
+		button = blurredStyle.Render("[ " + t("composer.send") + " ]")
 	}
 
 	// From field with account selector
 	fromAddr := m.getFromAddress()
 	var fromField string
-	if m.isCatchAllAccount() {
+	if m.isCatchAllAccount() { //nolint:gocritic
 		fromAddrView := m.fromInput.View()
 		if len(m.accounts) > 1 {
 			if m.focusIndex == focusFrom {

tui/constants.go 🔗

@@ -0,0 +1,11 @@
+package tui
+
+const (
+	keyEnter    = "enter"
+	keyDown     = "down"
+	keyRight    = "right"
+	keyCount    = "count"
+	keyINBOX    = "INBOX"
+	keyYubikey  = "yubikey"
+	keyShiftTab = "shift+tab"
+)

tui/email_view.go 🔗

@@ -20,8 +20,8 @@ import (
 // ClearKittyGraphics sends the Kitty graphics protocol delete command directly to stdout.
 func ClearKittyGraphics() {
 	// Delete all images: a=d (action=delete), d=A (delete all)
-	os.Stdout.WriteString("\x1b_Ga=d,d=A\x1b\\")
-	os.Stdout.Sync()
+	os.Stdout.WriteString("\x1b_Ga=d,d=A\x1b\\") //nolint:errcheck,gosec
+	os.Stdout.Sync()                             //nolint:errcheck,gosec
 }
 
 var (
@@ -80,7 +80,7 @@ func NewEmailView(email fetcher.Email, emailIndex, width, height int, mailbox Ma
 	var originalICSData []byte
 
 	for _, att := range email.Attachments {
-		if att.Filename == "smime-status.internal" {
+		if att.Filename == "smime-status.internal" { //nolint:gocritic
 			isSMIME = att.IsSMIMESignature || att.IsSMIMEEncrypted
 			smimeTrusted = att.SMIMEVerified
 			isEncrypted = att.IsSMIMEEncrypted
@@ -187,7 +187,7 @@ func (m *EmailView) Init() tea.Cmd {
 
 func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmd tea.Cmd
-	var cmds []tea.Cmd
+	cmds := make([]tea.Cmd, 0, 1)
 
 	switch msg := msg.(type) {
 	case tea.KeyPressMsg:
@@ -210,12 +210,12 @@ func (m *EmailView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					m.attachmentCursor = (m.attachmentCursor - 1 + len(m.email.Attachments)) % len(m.email.Attachments)
 				}
 				return m, nil
-			case "down", kb.Global.NavDown:
+			case keyDown, kb.Global.NavDown:
 				if len(m.email.Attachments) > 0 {
 					m.attachmentCursor = (m.attachmentCursor + 1) % len(m.email.Attachments)
 				}
 				return m, nil
-			case "enter":
+			case keyEnter:
 				if len(m.email.Attachments) > 0 {
 					selected := m.email.Attachments[m.attachmentCursor]
 					idx := m.emailIndex
@@ -340,8 +340,8 @@ func (m *EmailView) View() tea.View {
 	// before re-rendering to prevent stacking on scroll. Uses d=a (delete all
 	// placements) instead of d=A (delete all including data) so that images
 	// can be re-displayed by ID without re-uploading.
-	os.Stdout.WriteString("\x1b_Ga=d,d=a\x1b\\")
-	os.Stdout.Sync()
+	os.Stdout.WriteString("\x1b_Ga=d,d=a\x1b\\") //nolint:errcheck,gosec
+	os.Stdout.Sync()                             //nolint:errcheck,gosec
 
 	var cryptoStatus strings.Builder
 
@@ -501,18 +501,18 @@ func renderCalendarInvite(event *calendar.Event) string {
 
 	var b strings.Builder
 	b.WriteString("📅 Meeting Invite\n\n")
-	b.WriteString(fmt.Sprintf("Title:    %s\n", event.Summary))
-	b.WriteString(fmt.Sprintf("When:     %s\n", formatEventTime(event.Start, event.End)))
+	fmt.Fprintf(&b, "Title:    %s\n", event.Summary)
+	fmt.Fprintf(&b, "When:     %s\n", formatEventTime(event.Start, event.End))
 
 	if event.Location != "" {
-		b.WriteString(fmt.Sprintf("Where:    %s\n", event.Location))
+		fmt.Fprintf(&b, "Where:    %s\n", event.Location)
 	}
 
-	b.WriteString(fmt.Sprintf("Organizer: %s\n", event.Organizer))
+	fmt.Fprintf(&b, "Organizer: %s\n", event.Organizer)
 
 	if event.Description != "" {
 		desc := truncateString(event.Description, 100)
-		b.WriteString(fmt.Sprintf("\n%s\n", desc))
+		fmt.Fprintf(&b, "\n%s\n", desc)
 	}
 
 	b.WriteString("\n")

tui/filepicker.go 🔗

@@ -90,7 +90,7 @@ func (m *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		// Path input mode
 		if m.editingPath {
 			switch msg.String() {
-			case "enter":
+			case keyEnter:
 				path := m.pathInput.Value()
 				if path == "" {
 					m.editingPath = false
@@ -135,7 +135,7 @@ func (m *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			if len(m.items) > 0 {
 				m.cursor = (m.cursor - 1 + len(m.items)) % len(m.items)
 			}
-		case "down", "j":
+		case keyDown, "j":
 			if len(m.items) > 0 {
 				m.cursor = (m.cursor + 1) % len(m.items)
 			}
@@ -151,7 +151,7 @@ func (m *FilePicker) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		case "h":
 			m.showHidden = !m.showHidden
 			m.readDir()
-		case "enter":
+		case keyEnter:
 			if len(m.items) == 0 {
 				return m, nil
 			}
@@ -183,7 +183,7 @@ func (m *FilePicker) View() tea.View {
 	var b strings.Builder
 
 	b.WriteString(titleStyle.Render("Select a File") + "\n")
-	b.WriteString(fmt.Sprintf("  %s\n", m.currentPath))
+	fmt.Fprintf(&b, "  %s\n", m.currentPath)
 
 	if m.editingPath {
 		b.WriteString(m.pathInput.View() + "\n")

tui/folder_inbox.go 🔗

@@ -1,7 +1,6 @@
 package tui
 
 import (
-	"fmt"
 	"sort"
 	"strings"
 
@@ -118,10 +117,10 @@ func sortFolders(folders []string) []string {
 	sort.SliceStable(sorted, func(i, j int) bool {
 		iUpper := strings.ToUpper(sorted[i])
 		jUpper := strings.ToUpper(sorted[j])
-		if iUpper == "INBOX" {
+		if iUpper == keyINBOX {
 			return true
 		}
-		if jUpper == "INBOX" {
+		if jUpper == keyINBOX {
 			return false
 		}
 		return sorted[i] < sorted[j]
@@ -159,7 +158,7 @@ func (m *FolderInbox) SetDisableImages(v bool) {
 // NewFolderInbox creates a new FolderInbox with the given folders and accounts.
 func NewFolderInbox(folders []string, accounts []config.Account) *FolderInbox {
 	folders = sortFolders(folders)
-	currentFolder := "INBOX"
+	currentFolder := keyINBOX
 	if len(folders) > 0 {
 		currentFolder = folders[0]
 	}
@@ -182,7 +181,7 @@ func (m *FolderInbox) Init() tea.Cmd {
 	return nil
 }
 
-func (m *FolderInbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *FolderInbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo
 	// If move overlay is active, handle its input
 	if m.movingEmail {
 		return m.updateMoveOverlay(msg)
@@ -260,18 +259,17 @@ func (m *FolderInbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				}
 				m.moveSourceFolder = m.currentFolder
 				return m, nil
-			} else {
-				// Single move
-				selectedItem, ok := m.inbox.list.SelectedItem().(item)
-				if ok {
-					m.movingEmail = true
-					m.moveTargetIdx = 0
-					m.moveUID = selectedItem.uid
-					m.moveUIDs = []uint32{selectedItem.uid}
-					m.moveAccountID = selectedItem.accountID
-					m.moveSourceFolder = m.currentFolder
-					return m, nil
-				}
+			}
+			// Single move
+			selectedItem, ok := m.inbox.list.SelectedItem().(item)
+			if ok {
+				m.movingEmail = true
+				m.moveTargetIdx = 0
+				m.moveUID = selectedItem.uid
+				m.moveUIDs = []uint32{selectedItem.uid}
+				m.moveAccountID = selectedItem.accountID
+				m.moveSourceFolder = m.currentFolder
+				return m, nil
 			}
 		}
 
@@ -417,8 +415,7 @@ func (m *FolderInbox) wrapInboxCmd(cmd tea.Cmd) tea.Cmd {
 
 func (m *FolderInbox) updateMoveOverlay(msg tea.Msg) (tea.Model, tea.Cmd) {
 	kb := config.Keybinds
-	switch msg := msg.(type) {
-	case tea.KeyPressMsg:
+	if msg, ok := msg.(tea.KeyPressMsg); ok {
 		switch msg.String() {
 		case kb.Global.Cancel:
 			m.movingEmail = false
@@ -429,14 +426,14 @@ func (m *FolderInbox) updateMoveOverlay(msg tea.Msg) (tea.Model, tea.Cmd) {
 				m.moveTargetIdx = len(m.moveFolderChoices()) - 1
 			}
 			return m, nil
-		case "down", kb.Global.NavDown:
+		case keyDown, kb.Global.NavDown:
 			m.moveTargetIdx++
 			choices := m.moveFolderChoices()
 			if m.moveTargetIdx >= len(choices) {
 				m.moveTargetIdx = 0
 			}
 			return m, nil
-		case "enter":
+		case keyEnter:
 			choices := m.moveFolderChoices()
 			if len(choices) > 0 && m.moveTargetIdx < len(choices) {
 				destFolder := choices[m.moveTargetIdx]
@@ -461,15 +458,14 @@ func (m *FolderInbox) updateMoveOverlay(msg tea.Msg) (tea.Model, tea.Cmd) {
 							DestFolder:   destFolder,
 						}
 					}
-				} else {
-					// Single move
-					return m, func() tea.Msg {
-						return MoveEmailToFolderMsg{
-							UID:          m.moveUID,
-							AccountID:    m.moveAccountID,
-							SourceFolder: m.moveSourceFolder,
-							DestFolder:   destFolder,
-						}
+				}
+				// Single move
+				return m, func() tea.Msg {
+					return MoveEmailToFolderMsg{
+						UID:          m.moveUID,
+						AccountID:    m.moveAccountID,
+						SourceFolder: m.moveSourceFolder,
+						DestFolder:   destFolder,
 					}
 				}
 			}
@@ -511,17 +507,18 @@ func (m *FolderInbox) View() tea.View {
 
 	var content string
 
-	if m.previewPane != nil {
+	switch {
+	case m.previewPane != nil:
 		// Three-pane layout: folders | inbox | email preview
 		inboxPane := m.renderInboxPane()
 		previewPane := m.renderPreviewPane()
 		content = lipgloss.JoinHorizontal(lipgloss.Top, sidebar, inboxPane, previewPane)
-	} else if m.previewedUID != 0 {
+	case m.previewedUID != 0:
 		// Split pane loading state (body being fetched)
 		inboxPane := m.renderInboxPane()
 		emptyPreview := m.renderEmptyPreview()
 		content = lipgloss.JoinHorizontal(lipgloss.Top, sidebar, inboxPane, emptyPreview)
-	} else {
+	default:
 		// Two-pane layout (original): folders | inbox
 		inboxView := m.inbox.View().Content
 		content = lipgloss.JoinHorizontal(lipgloss.Top, sidebar, inboxView)
@@ -594,7 +591,7 @@ func (m *FolderInbox) renderWithMoveOverlay(content string) string {
 	title := t("folder_inbox.move_to_folder")
 	if len(m.moveUIDs) > 1 {
 		title = tn("folder_inbox.move_multiple", len(m.moveUIDs), map[string]interface{}{
-			"count": len(m.moveUIDs),
+			keyCount: len(m.moveUIDs),
 		})
 	}
 	b.WriteString(moveOverlayTitleStyle.Render(title))
@@ -743,11 +740,6 @@ func (m *FolderInbox) GetFolders() []string {
 	return m.folders
 }
 
-// Helper to get the formatted inbox title
-func folderInboxTitle(folder string) string {
-	return fmt.Sprintf("Folder: %s", folder)
-}
-
 // renderInboxPane renders inbox with border for split pane mode
 func (m *FolderInbox) renderInboxPane() string {
 	inboxWidth := m.calculateInboxWidth()

tui/folder_inbox_test.go 🔗

@@ -17,7 +17,7 @@ func TestFolderInboxSplitPreviewRendersSearchHit(t *testing.T) {
 	accounts := []config.Account{
 		{ID: "account-1", Email: "host.example.com", FetchEmail: "first@example.com"},
 	}
-	fi := NewFolderInbox([]string{"INBOX", "Archive"}, accounts)
+	fi := NewFolderInbox([]string{keyINBOX, "Archive"}, accounts)
 	// Force a non-zero canvas so calculate*Width does not panic on Update.
 	model, _ := fi.Update(tea.WindowSizeMsg{Width: 200, Height: 60})
 	fi = model.(*FolderInbox)
@@ -68,7 +68,7 @@ func TestFolderInboxSplitPreviewPrefersAllEmails(t *testing.T) {
 	accounts := []config.Account{
 		{ID: "account-1", Email: "host.example.com", FetchEmail: "first@example.com"},
 	}
-	fi := NewFolderInbox([]string{"INBOX"}, accounts)
+	fi := NewFolderInbox([]string{keyINBOX}, accounts)
 	model, _ := fi.Update(tea.WindowSizeMsg{Width: 200, Height: 60})
 	fi = model.(*FolderInbox)
 
@@ -96,7 +96,7 @@ func TestSearchOverlayKeysNotIntercepted(t *testing.T) {
 	accounts := []config.Account{
 		{ID: "account-1", Email: "host.example.com", FetchEmail: "first@example.com"},
 	}
-	fi := NewFolderInbox([]string{"INBOX", "Archive"}, accounts)
+	fi := NewFolderInbox([]string{keyINBOX, "Archive"}, accounts)
 	model, _ := fi.Update(tea.WindowSizeMsg{Width: 200, Height: 60})
 	fi = model.(*FolderInbox)
 

tui/inbox.go 🔗

@@ -26,7 +26,6 @@ var (
 	tabBarStyle     = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).PaddingBottom(1).MarginBottom(1)
 )
 
-var dateStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("243"))
 var unreadEmailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Bold(true)
 var readEmailStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
 var visualSelectedStyle lipgloss.Style
@@ -73,7 +72,7 @@ type itemDelegate struct {
 func (d itemDelegate) Height() int                               { return 1 }
 func (d itemDelegate) Spacing() int                              { return 0 }
 func (d itemDelegate) Update(msg tea.Msg, m *list.Model) tea.Cmd { return nil }
-func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) {
+func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { //nolint:gocyclo
 	i, ok := listItem.(item)
 	if !ok {
 		return
@@ -114,7 +113,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
 	listWidth := m.Width() - 2 // account for PaddingLeft(2) in itemStyle
 	isSelected := index == m.Index()
 
-	styledDate := dateStyle.Render(dateStr)
+	var styledDate string
 	if isSelected {
 		styledDate = selectedDateStyle.Render(dateStr)
 	} else {
@@ -214,7 +213,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list
 		}
 	}
 
-	fmt.Fprint(w, fn(str+strings.Repeat(" ", padding)+styledDate))
+	fmt.Fprint(w, fn(str+strings.Repeat(" ", padding)+styledDate)) //nolint:errcheck
 }
 
 // formatInboxDate formats a time as relative unless detailed dates are enabled
@@ -236,13 +235,13 @@ func formatInboxDate(timestamp time.Time, layout string, detailedDates bool) str
 		return t("time.just_now")
 	case d < time.Hour:
 		mins := int(d.Minutes())
-		return tn("time.minute_ago", mins, map[string]interface{}{"count": mins})
+		return tn("time.minute_ago", mins, map[string]interface{}{keyCount: mins})
 	case d < 24*time.Hour:
 		hours := int(d.Hours())
-		return tn("time.hour_ago", hours, map[string]interface{}{"count": hours})
+		return tn("time.hour_ago", hours, map[string]interface{}{keyCount: hours})
 	case d < 7*24*time.Hour:
 		days := int(d.Hours() / 24)
-		return tn("time.day_ago", days, map[string]interface{}{"count": days})
+		return tn("time.day_ago", days, map[string]interface{}{keyCount: days})
 	default:
 		return formatAbsoluteDate(timestamp, layout, now)
 	}
@@ -378,11 +377,11 @@ func NewArchiveInbox(emails []fetcher.Email, accounts []config.Account) *Inbox {
 
 func NewInboxWithMailbox(emails []fetcher.Email, accounts []config.Account, mailbox MailboxKind) *Inbox {
 	// Build tabs: empty for single account, "ALL" + accounts for multiple
-	var tabs []AccountTab
+	tabs := make([]AccountTab, 0, 1+len(accounts))
 	if len(accounts) <= 1 {
 		tabs = []AccountTab{{ID: "", Label: "", Email: ""}}
 	} else {
-		tabs = []AccountTab{{ID: "", Label: "ALL", Email: ""}}
+		tabs = append(tabs, AccountTab{ID: "", Label: "ALL", Email: ""})
 		for _, acc := range accounts {
 			// Use FetchEmail for display, fall back to Email if not set
 			displayEmail := accountDisplayEmail(acc)
@@ -718,11 +717,12 @@ func (m *Inbox) itemForEmail(email fetcher.Email, index int, showAccountLabel bo
 
 func (m *Inbox) getTitle() string {
 	var title string
-	if m.searchActive {
+	switch {
+	case m.searchActive:
 		title = fmt.Sprintf("Search Results - %s", m.searchQuery)
-	} else if m.currentAccountID == "" {
+	case m.currentAccountID == "":
 		title = m.getBaseTitle() + " - " + t("inbox.all_accounts")
-	} else {
+	default:
 		title = m.getBaseTitle()
 		for _, acc := range m.accounts {
 			if acc.ID == m.currentAccountID {
@@ -761,9 +761,10 @@ func (m *Inbox) getBaseTitle() string {
 		return "Trash"
 	case MailboxArchive:
 		return "Archive"
-	default:
+	case MailboxInbox:
 		return "Inbox"
 	}
+	return "Inbox"
 }
 
 func (m *Inbox) folderKey() string {
@@ -821,7 +822,7 @@ func (m *Inbox) Init() tea.Cmd {
 	return nil
 }
 
-func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocyclo
 	var cmds []tea.Cmd
 
 	switch msg := msg.(type) {
@@ -891,7 +892,7 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				m.updateListTitle()
 				return m, nil
 			}
-		case kb.Global.NavDown, "down", kb.Global.NavUp, "up":
+		case kb.Global.NavDown, keyDown, kb.Global.NavUp, "up":
 			if m.visualMode {
 				// Let the list handle navigation first
 				var cmd tea.Cmd
@@ -914,7 +915,7 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				m.updateList()
 				return m, nil
 			}
-		case "right", kb.Inbox.NextTab:
+		case keyRight, kb.Inbox.NextTab:
 			if len(m.tabs) > 1 {
 				m.activeTabIndex++
 				if m.activeTabIndex >= len(m.tabs) {
@@ -948,13 +949,12 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				return m, func() tea.Msg {
 					return BatchDeleteEmailsMsg{UIDs: uids, AccountID: accountID, Mailbox: m.mailbox}
 				}
-			} else {
-				// Single delete
-				selectedItem, ok := m.list.SelectedItem().(item)
-				if ok && selectedItem.uid != 0 {
-					return m, func() tea.Msg {
-						return DeleteEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
-					}
+			}
+			// Single delete
+			selectedItem, ok := m.list.SelectedItem().(item)
+			if ok && selectedItem.uid != 0 {
+				return m, func() tea.Msg {
+					return DeleteEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
 				}
 			}
 		case kb.Inbox.Archive:
@@ -977,13 +977,12 @@ func (m *Inbox) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				return m, func() tea.Msg {
 					return BatchArchiveEmailsMsg{UIDs: uids, AccountID: accountID, Mailbox: m.mailbox}
 				}
-			} else {
-				// Single archive
-				selectedItem, ok := m.list.SelectedItem().(item)
-				if ok && selectedItem.uid != 0 {
-					return m, func() tea.Msg {
-						return ArchiveEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
-					}
+			}
+			// Single archive
+			selectedItem, ok := m.list.SelectedItem().(item)
+			if ok && selectedItem.uid != 0 {
+				return m, func() tea.Msg {
+					return ArchiveEmailMsg{UID: selectedItem.uid, AccountID: selectedItem.accountID, Mailbox: m.mailbox}
 				}
 			}
 		case kb.Inbox.Refresh:
@@ -1388,7 +1387,7 @@ func (m *Inbox) RemoveEmails(uids []uint32, accountID string) {
 	// Remove from all emails list
 	var filteredAll []fetcher.Email
 	for _, e := range m.allEmails {
-		if !(uidSet[e.UID] && e.AccountID == accountID) {
+		if !uidSet[e.UID] || e.AccountID != accountID {
 			filteredAll = append(filteredAll, e)
 		}
 	}
@@ -1413,7 +1412,7 @@ func (m *Inbox) RemoveEmail(uid uint32, accountID string) {
 	// Remove from all emails list
 	var filteredAll []fetcher.Email
 	for _, e := range m.allEmails {
-		if !(e.UID == uid && e.AccountID == accountID) {
+		if e.UID != uid || e.AccountID != accountID {
 			filteredAll = append(filteredAll, e)
 		}
 	}
@@ -1454,11 +1453,11 @@ func (m *Inbox) SetEmails(emails []fetcher.Email, accounts []config.Account) {
 	m.noMoreByAccount = make(map[string]bool)
 
 	// Rebuild tabs: empty for single account, "ALL" + accounts for multiple
-	var tabs []AccountTab
+	tabs := make([]AccountTab, 0, 1+len(accounts))
 	if len(accounts) <= 1 {
 		tabs = []AccountTab{{ID: "", Label: "", Email: ""}}
 	} else {
-		tabs = []AccountTab{{ID: "", Label: "ALL", Email: ""}}
+		tabs = append(tabs, AccountTab{ID: "", Label: "ALL", Email: ""})
 		for _, acc := range accounts {
 			displayEmail := accountDisplayEmail(acc)
 			tabs = append(tabs, AccountTab{ID: acc.ID, Label: displayEmail, Email: displayEmail})

tui/login.go 🔗

@@ -199,7 +199,7 @@ func (m *Login) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				return m, nil
 			}
 
-		case "enter":
+		case keyEnter:
 			m.updateFlags()
 			visible := m.visibleFields()
 			lastField := visible[len(visible)-1]
@@ -209,7 +209,7 @@ func (m *Login) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			}
 			fallthrough
 
-		case "tab", "shift+tab", "up", "down":
+		case "tab", keyShiftTab, "up", keyDown:
 			s := msg.String()
 			m.updateFlags()
 			visible := m.visibleFields()
@@ -223,7 +223,7 @@ func (m *Login) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				}
 			}
 
-			if s == "up" || s == "shift+tab" {
+			if s == "up" || s == keyShiftTab {
 				curPos--
 			} else {
 				curPos++

tui/mailing_list.go 🔗

@@ -73,7 +73,7 @@ func (m *MailingListEditor) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			return m, tea.Quit
 		case kb.Global.Cancel:
 			return m, func() tea.Msg { return GoToSettingsMsg{} }
-		case "tab", "shift+tab", "up", "down":
+		case "tab", keyShiftTab, "up", keyDown:
 			if m.focus == 0 {
 				m.focus = 1
 				m.nameInput.Blur()
@@ -84,24 +84,23 @@ func (m *MailingListEditor) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 				m.nameInput.Focus()
 			}
 			return m, nil
-		case "enter":
+		case keyEnter:
 			if m.focus == 0 {
 				m.focus = 1
 				m.nameInput.Blur()
 				m.addrInput.Focus()
 				return m, nil
-			} else {
-				// Submit on second field
-				name := strings.TrimSpace(m.nameInput.Value())
-				addrs := strings.TrimSpace(m.addrInput.Value())
-				if name != "" && addrs != "" {
-					editIdx := m.editIndex
-					return m, func() tea.Msg {
-						return SaveMailingListMsg{
-							Name:      name,
-							Addresses: addrs,
-							EditIndex: editIdx,
-						}
+			}
+			// Submit on second field
+			name := strings.TrimSpace(m.nameInput.Value())
+			addrs := strings.TrimSpace(m.addrInput.Value())
+			if name != "" && addrs != "" {
+				editIdx := m.editIndex
+				return m, func() tea.Msg {
+					return SaveMailingListMsg{
+						Name:      name,
+						Addresses: addrs,
+						EditIndex: editIdx,
 					}
 				}
 			}

tui/marketplace.go 🔗

@@ -142,7 +142,7 @@ func (m Marketplace) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					m.offset = m.cursor
 				}
 			}
-		case "down", kb.Global.NavDown:
+		case keyDown, kb.Global.NavDown:
 			if m.cursor < len(m.entries)-1 {
 				m.cursor++
 				visible := m.visibleRows()
@@ -150,7 +150,7 @@ func (m Marketplace) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 					m.offset = m.cursor - visible + 1
 				}
 			}
-		case "enter":
+		case keyEnter:
 			if m.cursor < len(m.entries) {
 				entry := m.entries[m.cursor]
 				if m.installed[entry.Name] {
@@ -187,7 +187,7 @@ func installPlugin(entry plugins.PluginEntry) tea.Cmd {
 		}
 
 		dir := filepath.Join(home, ".config", "matcha", "plugins")
-		if err := os.MkdirAll(dir, 0755); err != nil {
+		if err := os.MkdirAll(dir, 0750); err != nil {
 			return PluginInstalledMsg{Name: entry.Name, Err: err}
 		}
 
@@ -257,12 +257,12 @@ func (m Marketplace) View() tea.View {
 				name += " " + mpInstalledStyle.Render("[installed]")
 			}
 
-			b.WriteString(fmt.Sprintf("%s%s\n", cursor, name))
-			b.WriteString(fmt.Sprintf("    %s\n", mpItemDescStyle.Render(entry.Description)))
+			fmt.Fprintf(&b, "%s%s\n", cursor, name)
+			fmt.Fprintf(&b, "    %s\n", mpItemDescStyle.Render(entry.Description))
 		}
 
 		if len(m.entries) > visible {
-			b.WriteString(fmt.Sprintf("\n  %d/%d plugins", m.cursor+1, len(m.entries)))
+			fmt.Fprintf(&b, "\n  %d/%d plugins", m.cursor+1, len(m.entries))
 		}
 	}
 

tui/password_prompt.go 🔗

@@ -47,7 +47,7 @@ func (m *PasswordPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 	case tea.KeyPressMsg:
 		switch msg.String() {
-		case "enter":
+		case keyEnter:
 			password := m.input.Value()
 			if password == "" {
 				m.err = t("password_prompt.error_empty")
@@ -72,7 +72,6 @@ func (m *PasswordPrompt) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		}
 		// Password correct — key is in msg.Key
 		return m, nil
-
 	}
 
 	var cmd tea.Cmd

tui/search.go 🔗

@@ -52,8 +52,7 @@ func (o *SearchOverlay) Update(msg tea.Msg, mailbox MailboxKind, accountID strin
 		o.results = msg.Emails
 		return nil
 	case tea.KeyPressMsg:
-		switch msg.String() {
-		case "enter":
+		if msg.String() == keyEnter {
 			if o.loading {
 				return nil
 			}

tui/settings.go 🔗

@@ -197,9 +197,9 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		// Global shortcut to return to menu from content pane
 		if m.activePane == PaneContent && msg.String() == "esc" {
 			// unless we are in crypto config or encryption editing which have their own esc logic
-			if !(m.activeCategory == CategoryAccounts && m.isCryptoConfig) &&
-				!(m.activeCategory == CategoryEncryption && m.encFocusIndex > -1) &&
-				!(m.activeCategory == CategoryPlugins && (m.pluginEditing || m.pluginSelected != "")) {
+			if (m.activeCategory != CategoryAccounts || !m.isCryptoConfig) &&
+				(m.activeCategory != CategoryEncryption || m.encFocusIndex <= -1) &&
+				(m.activeCategory != CategoryPlugins || (!m.pluginEditing && m.pluginSelected == "")) {
 				m.activePane = PaneMenu
 				return m, nil
 			}
@@ -207,21 +207,20 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 		if m.activePane == PaneMenu {
 			return m.updateMenu(msg)
-		} else {
-			switch m.activeCategory {
-			case CategoryGeneral:
-				return m.updateGeneral(msg)
-			case CategoryAccounts:
-				return m.updateAccounts(msg)
-			case CategoryTheme:
-				return m.updateTheme(msg)
-			case CategoryMailingLists:
-				return m.updateMailingLists(msg)
-			case CategoryEncryption:
-				return m.updateEncryption(msg)
-			case CategoryPlugins:
-				return m.updatePlugins(msg)
-			}
+		}
+		switch m.activeCategory {
+		case CategoryGeneral:
+			return m.updateGeneral(msg)
+		case CategoryAccounts:
+			return m.updateAccounts(msg)
+		case CategoryTheme:
+			return m.updateTheme(msg)
+		case CategoryMailingLists:
+			return m.updateMailingLists(msg)
+		case CategoryEncryption:
+			return m.updateEncryption(msg)
+		case CategoryPlugins:
+			return m.updatePlugins(msg)
 		}
 
 	case SecureModeEnabledMsg:
@@ -245,12 +244,13 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 	// Update text inputs if active
 	if m.activePane == PaneContent {
-		if m.activeCategory == CategoryEncryption {
+		switch {
+		case m.activeCategory == CategoryEncryption:
 			m.encPasswordInput, cmd = m.encPasswordInput.Update(msg)
 			cmds = append(cmds, cmd)
 			m.encConfirmInput, cmd = m.encConfirmInput.Update(msg)
 			cmds = append(cmds, cmd)
-		} else if m.activeCategory == CategoryAccounts && m.isCryptoConfig {
+		case m.activeCategory == CategoryAccounts && m.isCryptoConfig:
 			m.smimeCertInput, cmd = m.smimeCertInput.Update(msg)
 			cmds = append(cmds, cmd)
 			m.smimeKeyInput, cmd = m.smimeKeyInput.Update(msg)
@@ -261,7 +261,7 @@ func (m *Settings) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			cmds = append(cmds, cmd)
 			m.pgpPINInput, cmd = m.pgpPINInput.Update(msg)
 			cmds = append(cmds, cmd)
-		} else if m.activeCategory == CategoryPlugins && m.pluginEditing {
+		case m.activeCategory == CategoryPlugins && m.pluginEditing:
 			m.pluginInput, cmd = m.pluginInput.Update(msg)
 			cmds = append(cmds, cmd)
 		}
@@ -276,9 +276,9 @@ func (m *Settings) updateMenu(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 	switch msg.String() {
 	case "up", "k":
 		m.menuCursor = (m.menuCursor - 1 + categoryCount) % categoryCount
-	case "down", "j":
+	case keyDown, "j":
 		m.menuCursor = (m.menuCursor + 1) % categoryCount
-	case "right", "l", "enter":
+	case keyRight, "l", keyEnter:
 		m.activeCategory = SettingsCategory(m.menuCursor)
 		m.activePane = PaneContent
 

tui/settings_accounts.go 🔗

@@ -39,7 +39,7 @@ func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 	case "up", "k":
 		itemCount := len(m.cfg.Accounts) + 1
 		m.accountsCursor = (m.accountsCursor - 1 + itemCount) % itemCount
-	case "down", "j":
+	case keyDown, "j":
 		itemCount := len(m.cfg.Accounts) + 1
 		m.accountsCursor = (m.accountsCursor + 1) % itemCount
 	case "d":
@@ -80,7 +80,7 @@ func (m *Settings) updateAccounts(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 			m.enterCryptoConfig()
 			return m, textinput.Blink
 		}
-	case "enter":
+	case keyEnter:
 		if m.accountsCursor == len(m.cfg.Accounts) {
 			return m, func() tea.Msg { return GoToAddAccountMsg{} }
 		} else if m.accountsCursor < len(m.cfg.Accounts) {

tui/settings_crypto.go 🔗

@@ -44,8 +44,8 @@ func (m *Settings) updateSMIMEConfig(msg tea.KeyPressMsg) (*Settings, tea.Cmd) {
 	case "esc":
 		m.isCryptoConfig = false
 		return m, nil
-	case "tab", "shift+tab", "up", "down":
-		if msg.String() == "shift+tab" || msg.String() == "up" {
+	case "tab", keyShiftTab, "up", keyDown:
+		if msg.String() == keyShiftTab || msg.String() == "up" {
 			m.cryptoFocusIndex--
 			if m.cryptoFocusIndex < 0 {
 				m.cryptoFocusIndex = cryptoConfigMaxFocus
@@ -56,8 +56,8 @@ func (m *Settings) updateSMIMEConfig(msg tea.KeyPressMsg) (*Settings, tea.Cmd) {
 				m.cryptoFocusIndex = 0
 			}
 		}
-		if m.cryptoFocusIndex == 6 && m.pgpKeySource != "yubikey" {
-			if msg.String() == "shift+tab" || msg.String() == "up" {
+		if m.cryptoFocusIndex == 6 && m.pgpKeySource != keyYubikey {
+			if msg.String() == keyShiftTab || msg.String() == "up" {
 				m.cryptoFocusIndex = 5
 			} else {
 				m.cryptoFocusIndex = 7
@@ -85,7 +85,7 @@ func (m *Settings) updateSMIMEConfig(msg tea.KeyPressMsg) (*Settings, tea.Cmd) {
 		default:
 			// advance to next
 			next := m.cryptoFocusIndex + 1
-			if next == 6 && m.pgpKeySource != "yubikey" {
+			if next == 6 && m.pgpKeySource != keyYubikey {
 				next = 7
 			}
 			cmds = append(cmds, setFocus(next))
@@ -100,7 +100,7 @@ func (m *Settings) updateSMIMEConfig(msg tea.KeyPressMsg) (*Settings, tea.Cmd) {
 			return m, nil
 		case 5:
 			if m.pgpKeySource == "file" {
-				m.pgpKeySource = "yubikey"
+				m.pgpKeySource = keyYubikey
 			} else {
 				m.pgpKeySource = "file"
 			}
@@ -144,12 +144,12 @@ func (m *Settings) viewSMIMEConfig() string {
 	renderField(4, "Private Key Path:", m.pgpPrivateKeyInput.View())
 
 	keySource := "File"
-	if m.pgpKeySource == "yubikey" {
+	if m.pgpKeySource == keyYubikey {
 		keySource = "YubiKey"
 	}
 	renderField(5, "Key Source:", keySource)
 
-	if m.pgpKeySource == "yubikey" {
+	if m.pgpKeySource == keyYubikey {
 		renderField(6, "YubiKey PIN:", m.pgpPINInput.View())
 	}
 

tui/settings_encryption.go 🔗

@@ -28,7 +28,7 @@ func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 			}
 			return m, nil
 		}
-		if msg.String() == "enter" {
+		if msg.String() == keyEnter {
 			m.confirmingDisable = true
 		}
 		return m, nil
@@ -45,8 +45,8 @@ func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 		m.encError = ""
 		m.activePane = PaneMenu
 		return m, nil
-	case "tab", "shift+tab", "down", "up":
-		if msg.String() == "shift+tab" || msg.String() == "up" {
+	case "tab", keyShiftTab, keyDown, "up":
+		if msg.String() == keyShiftTab || msg.String() == "up" {
 			m.encFocusIndex--
 			if m.encFocusIndex < 0 {
 				m.encFocusIndex = 2
@@ -67,7 +67,7 @@ func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 			cmds = append(cmds, m.encConfirmInput.Focus())
 		}
 		return m, tea.Batch(cmds...)
-	case "enter":
+	case keyEnter:
 		switch m.encFocusIndex {
 		case 0:
 			m.encFocusIndex = 1
@@ -99,13 +99,14 @@ func (m *Settings) updateEncryption(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 	default:
 		// Forward input to focused textinput
 		var cmd tea.Cmd
-		if m.encFocusIndex == 0 {
+		switch m.encFocusIndex {
+		case 0:
 			before := m.encPasswordInput.Value()
 			m.encPasswordInput, cmd = m.encPasswordInput.Update(msg)
 			if m.encPasswordInput.Value() != before {
 				m.handlePasswordChanged()
 			}
-		} else if m.encFocusIndex == 1 {
+		case 1:
 			m.encConfirmInput, cmd = m.encConfirmInput.Update(msg)
 		}
 		return m, cmd
@@ -206,7 +207,8 @@ func (m *Settings) renderPasswordStrength() string {
 		return successStyle.Render(t("settings_encryption.strength_label") + " " + t("settings_encryption.strength_strong"))
 	case passwordstrength.Medium:
 		return settingsFocusedStyle.Render(t("settings_encryption.strength_label") + " " + t("settings_encryption.strength_medium"))
-	default:
+	case passwordstrength.Weak:
 		return dangerStyle.Render(t("settings_encryption.strength_label") + " " + t("settings_encryption.strength_weak"))
 	}
+	return dangerStyle.Render(t("settings_encryption.strength_label") + " " + t("settings_encryption.strength_weak"))
 }

tui/settings_general.go 🔗

@@ -18,7 +18,8 @@ type generalOption struct {
 }
 
 func (m *Settings) buildGeneralOptions() []generalOption {
-	opts := []generalOption{
+	opts := make([]generalOption, 0, 9+len(m.cfg.Accounts))
+	opts = append(opts, []generalOption{
 		{"settings_general.disable_images", onOff(m.cfg.DisableImages), "Prevent images from loading automatically in emails.", false, ""},
 		{"settings_general.hide_tips", onOff(m.cfg.HideTips), "Hide helpful hints displayed at the bottom of the screen.", false, ""},
 		{"settings_general.disable_notifications", onOff(m.cfg.DisableNotifications), "Turn off desktop notifications for new mail.", false, ""},
@@ -28,7 +29,7 @@ func (m *Settings) buildGeneralOptions() []generalOption {
 		{"settings_general.date_format", getDateFormatLabel(m.cfg.DateFormat), "Change how dates and times are displayed.", false, ""},
 		{"settings_general.language", getLanguageLabel(m.cfg.GetLanguage()), "Change the interface language. Changes apply instantly.", false, ""},
 		{"settings_general.signature", getSignatureStatus(), "Configure the global signature appended to your outgoing emails.", false, ""},
-	}
+	}...)
 
 	for _, acc := range m.cfg.Accounts {
 		status := t("settings_general.signature_not_configured")
@@ -54,13 +55,13 @@ func (m *Settings) updateGeneral(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 	switch msg.String() {
 	case "up", "k":
 		m.generalCursor = (m.generalCursor - 1 + len(opts)) % len(opts)
-	case "down", "j":
+	case keyDown, "j":
 		m.generalCursor = (m.generalCursor + 1) % len(opts)
-	case "enter", "space", "right", "l":
+	case keyEnter, "space", keyRight, "l":
 		if m.generalCursor < len(opts) {
 			opt := opts[m.generalCursor]
 			if opt.isAccountSig {
-				if msg.String() == "enter" || msg.String() == "right" || msg.String() == "l" {
+				if msg.String() == keyEnter || msg.String() == keyRight || msg.String() == "l" {
 					return m, func() tea.Msg { return GoToSignatureEditorMsg{AccountID: opt.accountID} }
 				}
 				return m, nil
@@ -118,14 +119,14 @@ func (m *Settings) updateGeneral(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 				m.cfg.Language = langs[nextIdx]
 				_ = config.SaveConfig(m.cfg)
 				// Apply language change immediately
-				i18n.GetManager().SetLanguage(m.cfg.Language)
+				i18n.GetManager().SetLanguage(m.cfg.Language) //nolint:errcheck,gosec
 				// Trigger full UI rebuild
 				return m, tea.Batch(
 					func() tea.Msg { return ConfigSavedMsg{} },
 					func() tea.Msg { return LanguageChangedMsg{} },
 				)
 			case 8: // Edit Signature
-				if msg.String() == "enter" || msg.String() == "right" || msg.String() == "l" {
+				if msg.String() == keyEnter || msg.String() == keyRight || msg.String() == "l" {
 					return m, func() tea.Msg { return GoToSignatureEditorMsg{} }
 				}
 			}

tui/settings_lists.go 🔗

@@ -32,7 +32,7 @@ func (m *Settings) updateMailingLists(msg tea.KeyPressMsg) (tea.Model, tea.Cmd)
 	case "up", "k":
 		itemCount := len(m.cfg.MailingLists) + 1
 		m.listsCursor = (m.listsCursor - 1 + itemCount) % itemCount
-	case "down", "j":
+	case keyDown, "j":
 		itemCount := len(m.cfg.MailingLists) + 1
 		m.listsCursor = (m.listsCursor + 1) % itemCount
 	case "d":
@@ -51,7 +51,7 @@ func (m *Settings) updateMailingLists(msg tea.KeyPressMsg) (tea.Model, tea.Cmd)
 				}
 			}
 		}
-	case "enter":
+	case keyEnter:
 		if m.listsCursor == len(m.cfg.MailingLists) {
 			return m, func() tea.Msg { return GoToAddMailingListMsg{} }
 		}
@@ -70,7 +70,7 @@ func (m *Settings) viewMailingLists() string {
 
 	for i, list := range m.cfg.MailingLists {
 		addrCount := tn("settings_mailing_lists.address_count", len(list.Addresses), map[string]interface{}{
-			"count": len(list.Addresses),
+			keyCount: len(list.Addresses),
 		})
 		line := fmt.Sprintf("%s - %s", list.Name, accountEmailStyle.Render(addrCount))
 

tui/settings_plugins.go 🔗

@@ -43,12 +43,12 @@ func (m *Settings) updatePluginList(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 
 	kb := config.Keybinds.Global
 	key := msg.String()
-	switch {
-	case key == "up" || key == kb.NavUp:
+	switch key {
+	case "up", kb.NavUp:
 		m.pluginListCursor = (m.pluginListCursor - 1 + len(schemas)) % len(schemas)
-	case key == "down" || key == kb.NavDown:
+	case keyDown, kb.NavDown:
 		m.pluginListCursor = (m.pluginListCursor + 1) % len(schemas)
-	case key == "enter" || key == "right" || key == "l":
+	case keyEnter, keyRight, "l":
 		m.pluginSelected = schemas[m.pluginListCursor].Plugin
 		m.pluginSettingCursor = 0
 	}
@@ -64,15 +64,15 @@ func (m *Settings) updatePluginSettings(msg tea.KeyPressMsg) (tea.Model, tea.Cmd
 
 	kb := config.Keybinds.Global
 	key := msg.String()
-	switch {
-	case key == "esc" || key == "left" || key == "h" || key == kb.Cancel:
+	switch key {
+	case "esc", "left", "h", kb.Cancel:
 		m.pluginSelected = ""
 		return m, nil
-	case key == "up" || key == kb.NavUp:
+	case "up", kb.NavUp:
 		m.pluginSettingCursor = (m.pluginSettingCursor - 1 + len(defs)) % len(defs)
-	case key == "down" || key == kb.NavDown:
+	case keyDown, kb.NavDown:
 		m.pluginSettingCursor = (m.pluginSettingCursor + 1) % len(defs)
-	case key == "enter" || key == "space" || key == "right" || key == "l":
+	case keyEnter, "space", keyRight, "l":
 		def := defs[m.pluginSettingCursor]
 		switch def.Type {
 		case plugin.SettingBool:
@@ -103,7 +103,7 @@ func (m *Settings) updatePluginEditor(msg tea.KeyPressMsg) (tea.Model, tea.Cmd)
 		m.pluginEditing = false
 		m.pluginInput.Blur()
 		return m, nil
-	case "enter":
+	case keyEnter:
 		raw := m.pluginInput.Value()
 		switch m.pluginEditingType {
 		case plugin.SettingNumber:
@@ -114,6 +114,8 @@ func (m *Settings) updatePluginEditor(msg tea.KeyPressMsg) (tea.Model, tea.Cmd)
 			m.plugins.SetSettingValue(m.pluginSelected, m.pluginEditingKey, n)
 		case plugin.SettingString:
 			m.plugins.SetSettingValue(m.pluginSelected, m.pluginEditingKey, raw)
+		case plugin.SettingBool:
+			// Bool settings are toggled directly, not via text input
 		}
 		m.pluginEditing = false
 		m.pluginInput.Blur()

tui/settings_theme.go 🔗

@@ -17,11 +17,11 @@ func (m *Settings) updateTheme(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
 		if len(themes) > 0 {
 			m.themeCursor = (m.themeCursor - 1 + len(themes)) % len(themes)
 		}
-	case "down", "j":
+	case keyDown, "j":
 		if len(themes) > 0 {
 			m.themeCursor = (m.themeCursor + 1) % len(themes)
 		}
-	case "enter", "space", "right", "l":
+	case keyEnter, "space", keyRight, "l":
 		if m.themeCursor < len(themes) {
 			selected := themes[m.themeCursor]
 			theme.SetTheme(selected.Name)

tui/signature.go 🔗

@@ -65,9 +65,9 @@ func (m *SignatureEditor) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 			// Save and go back to settings
 			signature := m.textarea.Value()
 			if m.accountID != "" {
-				go config.SaveSignatureForAccount(m.accountID, signature)
+				go config.SaveSignatureForAccount(m.accountID, signature) //nolint:errcheck
 			} else {
-				go config.SaveSignature(signature)
+				go config.SaveSignature(signature) //nolint:errcheck
 			}
 			return m, func() tea.Msg { return GoToSettingsMsg{} }
 		}

tui/theme.go 🔗

@@ -59,18 +59,15 @@ func RebuildStyles() {
 	suggestionBoxStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(t.Secondary).Padding(0, 1)
 	focusedStyle = lipgloss.NewStyle().Foreground(t.Accent)
 	blurredStyle = lipgloss.NewStyle().Foreground(t.Secondary)
-	noStyle = lipgloss.NewStyle()
 	helpStyle = lipgloss.NewStyle().Foreground(t.SubtleText)
 	emailRecipientStyle = lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
 	attachmentStyle = lipgloss.NewStyle().PaddingLeft(4).Foreground(t.Secondary)
-	fromSelectorStyle = lipgloss.NewStyle().Foreground(t.Accent)
 	smimeToggleStyle = lipgloss.NewStyle().PaddingLeft(4).Foreground(t.Secondary)
 
 	// inbox.go
 	tabStyle = lipgloss.NewStyle().Padding(0, 2)
 	activeTabStyle = lipgloss.NewStyle().Padding(0, 2).Foreground(t.Accent).Bold(true).Underline(true)
 	tabBarStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderBottom(true).PaddingBottom(1).MarginBottom(1)
-	dateStyle = lipgloss.NewStyle().Foreground(t.MutedText)
 	unreadEmailStyle = lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
 	readEmailStyle = lipgloss.NewStyle().Foreground(t.Secondary)
 	visualSelectedStyle = lipgloss.NewStyle().Background(t.AccentDark).Foreground(t.AccentText).PaddingLeft(2)

view/html.go 🔗

@@ -19,6 +19,8 @@ import (
 	lru "github.com/hashicorp/golang-lru/v2"
 )
 
+const termGhostty = "ghostty"
+
 func linkStyle() lipgloss.Style {
 	return lipgloss.NewStyle().Foreground(theme.ActiveTheme.Link)
 }
@@ -40,7 +42,7 @@ func getTerminalCellSize() int {
 
 	// Try /dev/tty directly - this works even when stdio is redirected (e.g., in Bubble Tea)
 	if tty, err := os.Open("/dev/tty"); err == nil {
-		defer tty.Close()
+		defer tty.Close() //nolint:errcheck
 		if cellHeight := getCellHeightFromFd(int(tty.Fd())); cellHeight > 0 {
 			return cellHeight
 		}
@@ -57,7 +59,7 @@ func hyperlinkSupported() bool {
 	// Terminals known to support OSC 8 hyperlinks
 	supportedTerms := []string{
 		"kitty",
-		"ghostty",
+		termGhostty,
 		"wezterm",
 		"alacritty",
 		"foot",
@@ -77,7 +79,7 @@ func hyperlinkSupported() bool {
 		"iterm.app",
 		"hyper",
 		"vscode",
-		"ghostty",
+		termGhostty,
 		"wezterm",
 	}
 
@@ -114,13 +116,12 @@ func hyperlink(url, text string) string {
 	if supported {
 		// Use OSC 8 hyperlink sequence for supported terminals
 		return fmt.Sprintf("\x1b]8;;%s\x07%s\x1b]8;;\x07", url, linkStyle().Render(text))
-	} else {
-		// Fallback to plain text format for unsupported terminals
-		if text == url {
-			return fmt.Sprintf("<%s>", linkStyle().Render(url))
-		}
-		return fmt.Sprintf("%s <%s>", linkStyle().Render(text), linkStyle().Render(url))
 	}
+	// Fallback to plain text format for unsupported terminals
+	if text == url {
+		return fmt.Sprintf("<%s>", linkStyle().Render(url))
+	}
+	return fmt.Sprintf("%s <%s>", linkStyle().Render(text), linkStyle().Render(url))
 }
 
 func decodeQuotedPrintable(s string) (string, error) {
@@ -148,12 +149,12 @@ func kittySupported() bool {
 func ghosttySupported() bool {
 	// Check for TERM containing ghostty
 	term := strings.ToLower(os.Getenv("TERM"))
-	if strings.Contains(term, "ghostty") {
+	if strings.Contains(term, termGhostty) {
 		return true
 	}
 
 	// Check for Ghostty-specific environment variables
-	if os.Getenv("TERM_PROGRAM") == "ghostty" {
+	if os.Getenv("TERM_PROGRAM") == termGhostty {
 		return true
 	}
 
@@ -187,11 +188,7 @@ func weztermSupported() bool {
 	}
 
 	term := strings.ToLower(os.Getenv("TERM"))
-	if strings.Contains(term, "wezterm") {
-		return true
-	}
-
-	return false
+	return strings.Contains(term, "wezterm")
 }
 
 func waystSupported() bool {
@@ -201,11 +198,7 @@ func waystSupported() bool {
 	}
 
 	termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
-	if termProgram == "wayst" {
-		return true
-	}
-
-	return false
+	return termProgram == "wayst"
 }
 
 func warpSupported() bool {
@@ -229,11 +222,7 @@ func konsoleSupported() bool {
 	}
 
 	termProgram := strings.ToLower(os.Getenv("TERM_PROGRAM"))
-	if termProgram == "konsole" {
-		return true
-	}
-
-	return false
+	return termProgram == "konsole"
 }
 
 func zellijSupported() bool {
@@ -276,12 +265,12 @@ func debugImageProtocol(format string, args ...interface{}) {
 	msg := fmt.Sprintf("[img-protocol] "+format+"\n", args...)
 	loglevel.Infof("%s", strings.TrimSuffix(msg, "\n"))
 	if path := os.Getenv("DEBUG_IMAGE_PROTOCOL_LOG"); path != "" {
-		if f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
+		if f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil { //nolint:gosec
 			_, _ = f.WriteString(msg)
 			_ = f.Close()
 		}
 	} else if path := os.Getenv("DEBUG_KITTY_LOG"); path != "" {
-		if f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
+		if f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil { //nolint:gosec
 			_, _ = f.WriteString(msg)
 			_ = f.Close()
 		}
@@ -326,7 +315,7 @@ func fetchRemoteBase64(url string) string {
 		debugImageProtocol("remote fetch failed url=%s err=%v", url, err)
 		return ""
 	}
-	defer resp.Body.Close()
+	defer resp.Body.Close() //nolint:errcheck
 	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
 		debugImageProtocol("remote fetch non-200 url=%s status=%d", url, resp.StatusCode)
 		return ""
@@ -369,103 +358,6 @@ func dataURIBase64(uri string) string {
 const imageRowPlaceholderPrefix = "[[MATCHA_IMG_ROWS:"
 const imageRowPlaceholderSuffix = "]]"
 
-func kittyInlineImage(payload string) string {
-	if payload == "" {
-		return ""
-	}
-
-	const chunkSize = 4096
-	var b strings.Builder
-
-	// Calculate how many terminal rows the image occupies to advance text after it.
-	rows := 1
-	if data, err := base64.StdEncoding.DecodeString(payload); err == nil {
-		if _, h, ok := clib.ImageDimensions(data); ok {
-			cellHeight := getTerminalCellSize()
-			rows = (h + cellHeight - 1) / cellHeight
-			if rows < 1 {
-				rows = 1
-			}
-			debugImageProtocol("image height: %d pixels, cell height: %d pixels, rows needed: %d", h, cellHeight, rows)
-		}
-	}
-
-	for offset := 0; offset < len(payload); offset += chunkSize {
-		end := offset + chunkSize
-		if end > len(payload) {
-			end = len(payload)
-		}
-		more := "0"
-		if end < len(payload) {
-			more = "1"
-		}
-
-		chunk := payload[offset:end]
-		if offset == 0 {
-			// C=1 means cursor does NOT move after image render (stays at top-left of image position)
-			// This is needed for proper TUI rendering, but we must add newlines to push text below
-			b.WriteString(fmt.Sprintf("\x1b_Gf=100,a=T,q=2,C=1,m=%s;%s\x1b\\", more, chunk))
-		} else {
-			b.WriteString(fmt.Sprintf("\x1b_Gm=%s;%s\x1b\\", more, chunk))
-		}
-	}
-
-	// Add newlines to push cursor below the image.
-	// Use a placeholder that won't be collapsed by the newline regex.
-	b.WriteString(fmt.Sprintf("\n%s%d%s\n", imageRowPlaceholderPrefix, rows, imageRowPlaceholderSuffix))
-
-	return b.String()
-}
-
-// iterm2InlineImage renders an image using iTerm2's image protocol
-func iterm2InlineImage(payload string) string {
-	if payload == "" {
-		return ""
-	}
-
-	// Calculate rows for cursor positioning
-	rows := 1
-	if data, err := base64.StdEncoding.DecodeString(payload); err == nil {
-		if _, h, ok := clib.ImageDimensions(data); ok {
-			cellHeight := getTerminalCellSize()
-			rows = (h + cellHeight - 1) / cellHeight
-			if rows < 1 {
-				rows = 1
-			}
-			debugImageProtocol("image height: %d pixels, cell height: %d pixels, rows needed: %d", h, cellHeight, rows)
-		}
-	}
-
-	// iTerm2 image protocol: ESC]1337;File=inline=1:<base64_data>BEL
-	result := fmt.Sprintf("\x1b]1337;File=inline=1:%s\x07\n", payload)
-
-	// Add placeholder for row spacing
-	result += fmt.Sprintf("%s%d%s\n", imageRowPlaceholderPrefix, rows, imageRowPlaceholderSuffix)
-
-	return result
-}
-
-// sixelInlineImage returns Sixel escape sequence + newline placeholders
-func sixelInlineImage(base64PNG string) string {
-	data, err := base64.StdEncoding.DecodeString(base64PNG)
-	if err != nil {
-		return ""
-	}
-
-	cellHeight := getTerminalCellSize()
-	sixel, rows, err := clib.EncodePNGToSixel(data, cellHeight)
-	if err != nil {
-		debugImageProtocol("Sixel encoding failed: %v", err)
-		return ""
-	}
-
-	debugImageProtocol("Sixel: encoded %d bytes, %d rows", len(sixel), rows)
-
-	// Sixel sequences don't auto-advance cursor
-	// Add newlines to preserve layout
-	return sixel + strings.Repeat("\n", rows)
-}
-
 // sixelImageEscapeOnly returns raw Sixel for out-of-band rendering
 func sixelImageEscapeOnly(base64PNG string) string {
 	data, err := base64.StdEncoding.DecodeString(base64PNG)
@@ -482,28 +374,6 @@ func sixelImageEscapeOnly(base64PNG string) string {
 	return sixel
 }
 
-// renderInlineImage renders an image using the appropriate protocol for the detected terminal
-func renderInlineImage(payload string) string {
-	if payload == "" {
-		return ""
-	}
-
-	// Priority: Sixel in multiplexers overrides native protocols
-	if sixelSupported() {
-		return sixelInlineImage(payload)
-	}
-
-	if kittySupported() || ghosttySupported() || weztermSupported() || waystSupported() || konsoleSupported() {
-		// These terminals use the Kitty graphics protocol
-		return kittyInlineImage(payload)
-	} else if iterm2Supported() || warpSupported() {
-		// iTerm2 and Warp use the iTerm2 image protocol
-		return iterm2InlineImage(payload)
-	}
-
-	return ""
-}
-
 // imageRows calculates the number of terminal rows an image occupies.
 func imageRows(payload string) int {
 	rows := 1
@@ -546,12 +416,12 @@ func kittyUploadImage(payload string, id uint32) {
 		if offset == 0 {
 			// a=t: transmit (upload) only, don't display yet
 			// i=ID: assign this image ID
-			fmt.Fprintf(os.Stdout, "\x1b_Gf=100,a=t,i=%d,q=2,m=%s;%s\x1b\\", id, more, chunk)
+			fmt.Fprintf(os.Stdout, "\x1b_Gf=100,a=t,i=%d,q=2,m=%s;%s\x1b\\", id, more, chunk) //nolint:errcheck
 		} else {
-			fmt.Fprintf(os.Stdout, "\x1b_Gm=%s;%s\x1b\\", more, chunk)
+			fmt.Fprintf(os.Stdout, "\x1b_Gm=%s;%s\x1b\\", more, chunk) //nolint:errcheck
 		}
 	}
-	os.Stdout.Sync()
+	os.Stdout.Sync() //nolint:errcheck,gosec
 }
 
 // kittyDisplayImage displays a previously uploaded image by its ID at the
@@ -602,9 +472,9 @@ func RenderImageToStdout(placement *ImagePlacement, screenRow int, screenCol ...
 
 		debugImageProtocol("Sixel: rendering %d bytes at row=%d col=%d", len(placement.SixelEncoded), screenRow+1, col)
 		// Position cursor + render Sixel
-		fmt.Fprintf(os.Stdout, "\x1b[s\x1b[%d;%dH%s\x1b[u",
+		fmt.Fprintf(os.Stdout, "\x1b[s\x1b[%d;%dH%s\x1b[u", //nolint:errcheck
 			screenRow+1, col, placement.SixelEncoded)
-		os.Stdout.Sync()
+		os.Stdout.Sync() //nolint:errcheck,gosec
 		return
 	}
 
@@ -619,12 +489,12 @@ func RenderImageToStdout(placement *ImagePlacement, screenRow int, screenCol ...
 			placement.Uploaded = true
 		}
 		seq := kittyDisplayImage(placement.ID)
-		fmt.Fprintf(os.Stdout, "\x1b[s\x1b[%d;%dH%s\x1b[u", screenRow+1, col, seq)
-		os.Stdout.Sync()
+		fmt.Fprintf(os.Stdout, "\x1b[s\x1b[%d;%dH%s\x1b[u", screenRow+1, col, seq) //nolint:errcheck
+		os.Stdout.Sync()                                                           //nolint:errcheck,gosec
 	} else if useIterm2 {
 		seq := iterm2ImageEscapeOnly(placement.Base64)
-		fmt.Fprintf(os.Stdout, "\x1b[s\x1b[%d;%dH%s\x1b[u", screenRow+1, col, seq)
-		os.Stdout.Sync()
+		fmt.Fprintf(os.Stdout, "\x1b[s\x1b[%d;%dH%s\x1b[u", screenRow+1, col, seq) //nolint:errcheck
+		os.Stdout.Sync()                                                           //nolint:errcheck,gosec
 	}
 }
 
@@ -773,9 +643,10 @@ func renderHTMLToText(htmlBody []byte, inline map[string]string, h1Style, h2Styl
 
 			if !disableImages && imageProtocolSupported() {
 				var payload string
-				if strings.HasPrefix(src, "data:image/") {
+				switch {
+				case strings.HasPrefix(src, "data:image/"):
 					payload = dataURIBase64(src)
-				} else if strings.HasPrefix(src, "cid:") {
+				case strings.HasPrefix(src, "cid:"):
 					cid := strings.TrimPrefix(src, "cid:")
 					cid = strings.Trim(cid, "<>")
 					if inline != nil {
@@ -784,7 +655,7 @@ func renderHTMLToText(htmlBody []byte, inline map[string]string, h1Style, h2Styl
 					} else {
 						debugImageProtocol("cid lookup skipped inline map nil for %s", cid)
 					}
-				} else if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
+				case strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://"):
 					payload = fetchRemoteBase64(src)
 				}
 
@@ -800,22 +671,22 @@ func renderHTMLToText(htmlBody []byte, inline map[string]string, h1Style, h2Styl
 						rows    int
 					}{idx, payload, rows})
 
-					text.WriteString(fmt.Sprintf("\n[[MATCHA_IMG:%d]]", idx))
-					text.WriteString(fmt.Sprintf("\n%s%d%s\n", imageRowPlaceholderPrefix, rows, imageRowPlaceholderSuffix))
+					fmt.Fprintf(&text, "\n[[MATCHA_IMG:%d]]", idx)
+					fmt.Fprintf(&text, "\n%s%d%s\n", imageRowPlaceholderPrefix, rows, imageRowPlaceholderSuffix)
 					continue
 				}
 				debugImageProtocol("no payload for src=%s", src)
 			}
 			if hyperlinkSupported() {
-				text.WriteString(fmt.Sprintf("\n %s \n", hyperlink(src, fmt.Sprintf("[Click here to view image: %s]", alt))))
+				fmt.Fprintf(&text, "\n %s \n", hyperlink(src, fmt.Sprintf("[Click here to view image: %s]", alt)))
 			} else {
-				text.WriteString(fmt.Sprintf("\n %s \n", linkStyle().Render(fmt.Sprintf("[Image: %s, %s]", alt, src))))
+				fmt.Fprintf(&text, "\n %s \n", linkStyle().Render(fmt.Sprintf("[Image: %s, %s]", alt, src)))
 			}
 
 		case clib.HElemTable:
 			headerRows := 0
 			if elem.Attr1 != "" {
-				fmt.Sscanf(elem.Attr1, "%d", &headerRows)
+				fmt.Sscanf(elem.Attr1, "%d", &headerRows) //nolint:errcheck,gosec
 			}
 			text.WriteString("\n")
 			text.WriteString(renderTable(elem.Text, headerRows))
@@ -855,7 +726,7 @@ func renderHTMLToText(htmlBody []byte, inline map[string]string, h1Style, h2Styl
 		for lineNum, line := range lines {
 			if matches := imgMarkerRegex.FindStringSubmatch(line); matches != nil {
 				var idx int
-				fmt.Sscanf(matches[1], "%d", &idx)
+				fmt.Sscanf(matches[1], "%d", &idx) //nolint:errcheck,gosec
 				for _, pi := range pendingImages {
 					if pi.index == idx {
 						placements = append(placements, ImagePlacement{
@@ -1026,7 +897,7 @@ func styleQuotedReplies(text string) string {
 		}
 
 		// Check if line starts with ">" (quoted text)
-		if strings.HasPrefix(trimmedLine, ">") {
+		if strings.HasPrefix(trimmedLine, ">") { //nolint:gocritic
 			if !inQuote {
 				// Start a new quote block without header info
 				inQuote = true
@@ -1039,7 +910,7 @@ func styleQuotedReplies(text string) string {
 			quoteBlock = append(quoteBlock, quotedContent)
 		} else if inQuote {
 			// End of quote block - check if it's just whitespace
-			if trimmedLine == "" && i+1 < len(lines) && strings.HasPrefix(strings.TrimSpace(lines[i+1]), ">") {
+			if trimmedLine == "" && i+1 < len(lines) && strings.HasPrefix(strings.TrimSpace(lines[i+1]), ">") { //nolint:gocritic
 				// Empty line within quote block, keep it
 				quoteBlock = append(quoteBlock, "")
 			} else if trimmedLine == "" && len(quoteBlock) == 0 {
@@ -1102,11 +973,12 @@ func renderQuoteBox(from, date string, lines []string) string {
 	// Build header with email on left and date on right
 	var header string
 	if from != "" || date != "" {
-		if from != "" && date != "" {
+		switch {
+		case from != "" && date != "":
 			header = quoteHeaderStyle().Render(from + "  " + date)
-		} else if from != "" {
+		case from != "":
 			header = quoteHeaderStyle().Render(from)
-		} else {
+		default:
 			header = quoteHeaderStyle().Render(date)
 		}
 	}