Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 31 additions & 11 deletions fetcher/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/emersion/go-message/mail"
"github.com/emersion/go-pgpmail"
"github.com/floatpane/matcha/config"
"github.com/floatpane/matcha/internal/loglevel"
"go.mozilla.org/pkcs7"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/ianaindex"
Expand All @@ -43,6 +44,18 @@ var (
debugIMAPOnce sync.Once
)

type debugKittyLogFile interface {
WriteString(string) (int, error)
Close() error
}

var (
debugKittyOpenLogFile = func(path string) (debugKittyLogFile, error) {
return os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
}
debugKittyLogErrorf = loglevel.Infof
)

func getDebugIMAPWriter() io.Writer {
debugIMAPOnce.Do(func() {
if path := os.Getenv("DEBUG_IMAP"); path != "" {
Expand All @@ -58,6 +71,22 @@ func getDebugIMAPWriter() io.Writer {
return nil
}

func writeDebugKittyLog(path, msg string) {
f, err := debugKittyOpenLogFile(path)
if err != nil {
debugKittyLogErrorf("failed to open debug kitty log %s: %v", path, err)
return
}
defer func() {
if err := f.Close(); err != nil {
debugKittyLogErrorf("failed to close debug kitty log %s: %v", path, err)
}
}()
if _, err := f.WriteString(msg); err != nil {
debugKittyLogErrorf("failed to write debug kitty log %s: %v", path, err)
}
}

// Attachment holds data for an email attachment.
type Attachment struct {
Filename string
Expand Down Expand Up @@ -1162,18 +1191,9 @@ func FetchEmailBodyFromMailbox(account *config.Account, mailbox string, uid uint
}
if os.Getenv("DEBUG_KITTY_IMAGES") != "" {
msg := fmt.Sprintf("[kitty-img] body selection html=%s plain=%s chosen=%s\n", htmlPartID, plainPartID, textPartID)
log.Print(msg)
loglevel.Infof("%s", strings.TrimSuffix(msg, "\n"))
if path := os.Getenv("DEBUG_KITTY_LOG"); path != "" {
// 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)
if err != nil {
return
}
defer f.Close()
_, _ = f.WriteString(msg)
}()
writeDebugKittyLog(path, msg)
}
}
if textPartID != "" {
Expand Down
62 changes: 62 additions & 0 deletions fetcher/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package fetcher

import (
"bytes"
"errors"
"fmt"
"strings"
"testing"

Expand All @@ -26,6 +28,66 @@ func (h testPartHeader) Set(key, value string) {
h[key] = value
}

type failingDebugKittyLogFile struct {
writeErr error
closeErr error
}

func (f failingDebugKittyLogFile) WriteString(s string) (int, error) {
if f.writeErr != nil {
return 0, f.writeErr
}
return len(s), nil
}

func (f failingDebugKittyLogFile) Close() error {
return f.closeErr
}

func TestWriteDebugKittyLogReportsOpenWriteAndCloseErrors(t *testing.T) {
originalOpenLogFile := debugKittyOpenLogFile
originalLogErrorf := debugKittyLogErrorf
defer func() {
debugKittyOpenLogFile = originalOpenLogFile
debugKittyLogErrorf = originalLogErrorf
}()

var logged []string
debugKittyLogErrorf = func(format string, args ...interface{}) {
logged = append(logged, fmt.Sprintf(format, args...))
}

openErr := errors.New("open failed")
debugKittyOpenLogFile = func(string) (debugKittyLogFile, error) {
return nil, openErr
}
writeDebugKittyLog("/tmp/matcha-kitty.log", "hello")

writeErr := errors.New("write failed")
closeErr := errors.New("close failed")
debugKittyOpenLogFile = func(string) (debugKittyLogFile, error) {
return failingDebugKittyLogFile{
writeErr: writeErr,
closeErr: closeErr,
}, nil
}
writeDebugKittyLog("/tmp/matcha-kitty.log", "hello")

joined := strings.Join(logged, "\n")
for _, want := range []string{
"failed to open debug kitty log",
openErr.Error(),
"failed to write debug kitty log",
writeErr.Error(),
"failed to close debug kitty log",
closeErr.Error(),
} {
if !strings.Contains(joined, want) {
t.Fatalf("expected logged output to contain %q, got %q", want, joined)
}
}
}

func TestDecodePartUsesCharsetWhenContentTypeIsMalformed(t *testing.T) {
header := testPartHeader{}
header.Set("Content-Type", "text/plain; charset=iso-8859-1; broken")
Expand Down
39 changes: 30 additions & 9 deletions view/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"charm.land/lipgloss/v2"
"github.com/floatpane/matcha/clib"
"github.com/floatpane/matcha/internal/httpclient"
"github.com/floatpane/matcha/internal/loglevel"
"github.com/floatpane/matcha/theme"
lru "github.com/hashicorp/golang-lru/v2"
)
Expand Down Expand Up @@ -268,22 +269,42 @@ func imageProtocolSupported() bool {
weztermSupported() || waystSupported() || warpSupported() || konsoleSupported()
}

type debugImageProtocolLogFile interface {
WriteString(string) (int, error)
Close() error
}

var (
debugImageProtocolOpenLogFile = func(path string) (debugImageProtocolLogFile, error) {
Comment thread
FromSi marked this conversation as resolved.
return os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
}
debugImageProtocolLogErrorf = loglevel.Infof
)

func debugImageProtocol(format string, args ...interface{}) {
if os.Getenv("DEBUG_IMAGE_PROTOCOL") == "" && os.Getenv("DEBUG_KITTY_IMAGES") == "" {
return
}
msg := fmt.Sprintf("[img-protocol] "+format+"\n", args...)
fmt.Print(msg)
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 {
_, _ = f.WriteString(msg)
_ = f.Close()
}
writeDebugImageProtocolLog(path, msg)
} 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 {
_, _ = f.WriteString(msg)
_ = f.Close()
}
writeDebugImageProtocolLog(path, msg)
}
}

func writeDebugImageProtocolLog(path, msg string) {
f, err := debugImageProtocolOpenLogFile(path)
if err != nil {
debugImageProtocolLogErrorf("failed to open debug image protocol log %s: %v", path, err)
return
Comment thread
FromSi marked this conversation as resolved.
}
if _, err := f.WriteString(msg); err != nil {
debugImageProtocolLogErrorf("failed to write debug image protocol log %s: %v", path, err)
}
if err := f.Close(); err != nil {
debugImageProtocolLogErrorf("failed to close debug image protocol log %s: %v", path, err)
}
}

Expand Down
113 changes: 113 additions & 0 deletions view/html_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package view

import (
"errors"
"fmt"
"os"
"regexp"
Expand Down Expand Up @@ -32,6 +33,118 @@ func clearAllTerminalEnv() {
os.Setenv("TERM_PROGRAM", "basic")
}

type failingDebugImageProtocolLogFile struct {
writeErr error
closeErr error
}

func (f failingDebugImageProtocolLogFile) WriteString(s string) (int, error) {
if f.writeErr != nil {
return 0, f.writeErr
}
return len(s), nil
}

func (f failingDebugImageProtocolLogFile) Close() error {
return f.closeErr
}

func TestDebugImageProtocolReportsLogWriteError(t *testing.T) {
originalOpenLogFile := debugImageProtocolOpenLogFile
originalLogErrorf := debugImageProtocolLogErrorf
defer func() {
debugImageProtocolOpenLogFile = originalOpenLogFile
debugImageProtocolLogErrorf = originalLogErrorf
}()

writeErr := errors.New("write failed")
debugImageProtocolOpenLogFile = func(string) (debugImageProtocolLogFile, error) {
return failingDebugImageProtocolLogFile{writeErr: writeErr}, nil
}

var logged []string
debugImageProtocolLogErrorf = func(format string, args ...interface{}) {
logged = append(logged, fmt.Sprintf(format, args...))
}

t.Setenv("DEBUG_IMAGE_PROTOCOL", "1")
t.Setenv("DEBUG_IMAGE_PROTOCOL_LOG", "/tmp/matcha-debug.log")

debugImageProtocol("hello")

joined := strings.Join(logged, "\n")
if !strings.Contains(joined, "failed to write debug image protocol log") {
t.Fatalf("expected write error to be logged, got %q", joined)
}
if !strings.Contains(joined, writeErr.Error()) {
t.Fatalf("expected logged error to contain %q, got %q", writeErr, joined)
}
}

func TestDebugImageProtocolReportsLogCloseError(t *testing.T) {
originalOpenLogFile := debugImageProtocolOpenLogFile
originalLogErrorf := debugImageProtocolLogErrorf
defer func() {
debugImageProtocolOpenLogFile = originalOpenLogFile
debugImageProtocolLogErrorf = originalLogErrorf
}()

closeErr := errors.New("close failed")
debugImageProtocolOpenLogFile = func(string) (debugImageProtocolLogFile, error) {
return failingDebugImageProtocolLogFile{closeErr: closeErr}, nil
}

var logged []string
debugImageProtocolLogErrorf = func(format string, args ...interface{}) {
logged = append(logged, fmt.Sprintf(format, args...))
}

t.Setenv("DEBUG_IMAGE_PROTOCOL", "1")
t.Setenv("DEBUG_KITTY_LOG", "/tmp/matcha-kitty.log")

debugImageProtocol("hello")

joined := strings.Join(logged, "\n")
if !strings.Contains(joined, "failed to close debug image protocol log") {
t.Fatalf("expected close error to be logged, got %q", joined)
}
if !strings.Contains(joined, closeErr.Error()) {
t.Fatalf("expected logged error to contain %q, got %q", closeErr, joined)
}
}

func TestDebugImageProtocolReportsLogOpenError(t *testing.T) {
originalOpenLogFile := debugImageProtocolOpenLogFile
originalLogErrorf := debugImageProtocolLogErrorf
defer func() {
debugImageProtocolOpenLogFile = originalOpenLogFile
debugImageProtocolLogErrorf = originalLogErrorf
}()

openErr := errors.New("open failed")
debugImageProtocolOpenLogFile = func(string) (debugImageProtocolLogFile, error) {
return nil, openErr
}

var logged []string
debugImageProtocolLogErrorf = func(format string, args ...interface{}) {
logged = append(logged, fmt.Sprintf(format, args...))
}

t.Setenv("DEBUG_IMAGE_PROTOCOL", "1")
t.Setenv("DEBUG_IMAGE_PROTOCOL_LOG", "/tmp/matcha-debug.log")

debugImageProtocol("hello")

joined := strings.Join(logged, "\n")
if !strings.Contains(joined, "failed to open debug image protocol log") {
t.Fatalf("expected open error to be logged, got %q", joined)
}
if !strings.Contains(joined, openErr.Error()) {
t.Fatalf("expected logged error to contain %q, got %q", openErr, joined)
}
}

func TestDecodeQuotedPrintable(t *testing.T) {
testCases := []struct {
name string
Expand Down
Loading