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
13 changes: 13 additions & 0 deletions sender/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody
certsDir := filepath.Join(cfgDir, "certs")
var certs []*x509.Certificate
var missingCerts []string
var missingCertErr error

for _, em := range allRecipients {
em = strings.TrimSpace(em)
Expand All @@ -574,23 +575,35 @@ func SendEmail(account *config.Account, to, cc, bcc []string, subject, plainBody

certData, err := os.ReadFile(certPath)
if err != nil {
if missingCertErr == nil {
missingCertErr = err
}
missingCerts = append(missingCerts, em)
continue
}
block, _ := pem.Decode(certData)
if block == nil {
if missingCertErr == nil {
missingCertErr = fmt.Errorf("failed to parse S/MIME certificate PEM: %s", certPath)
}
missingCerts = append(missingCerts, em)
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
if missingCertErr == nil {
missingCertErr = err
}
missingCerts = append(missingCerts, em)
continue
}
certs = append(certs, cert)
}

if len(missingCerts) > 0 {
if missingCertErr != nil {
return nil, fmt.Errorf("cannot encrypt: missing or invalid S/MIME certificates for: %s: %w", strings.Join(missingCerts, ", "), missingCertErr)
}
return nil, fmt.Errorf("cannot encrypt: missing or invalid S/MIME certificates for: %s", strings.Join(missingCerts, ", "))
}

Expand Down
38 changes: 38 additions & 0 deletions sender/sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ package sender
import (
"errors"
"io"
"os"
"strings"
"testing"

"github.com/floatpane/matcha/config"
)

type failingReader struct{}
Expand All @@ -29,6 +32,41 @@ func TestWriteQuotedPrintablePropagatesFlushError(t *testing.T) {
}
}

func TestSendEmailSMIMEEncryptionWrapsMissingCertError(t *testing.T) {
t.Setenv("HOME", t.TempDir())

account := &config.Account{
Email: "sender@example.com",
ServiceProvider: "custom",
SMTPServer: "smtp.example.com",
SMTPPort: 587,
}

_, err := SendEmail(
account,
[]string{"recipient@example.com"},
nil,
nil,
"Subject",
"plain body",
"",
nil,
nil,
"",
nil,
false,
true,
false,
false,
)
if err == nil {
t.Fatal("SendEmail() error = nil, want missing S/MIME certificate error")
}
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("SendEmail() error = %v, want errors.Is(os.ErrNotExist)", err)
}
}

// TestSMIMEOuterBoundary_RandFailure ensures that a crypto/rand failure surfaces
// as an error rather than silently producing a predictable, time-based
// boundary that an attacker could collide with (issue #1127).
Expand Down
Loading