diff --git a/internal/email/email.go b/internal/email/email.go index 383045e..27048ee 100644 --- a/internal/email/email.go +++ b/internal/email/email.go @@ -4,6 +4,7 @@ package email import ( "fmt" + "os" "time" "github.com/wneessen/go-mail" @@ -85,7 +86,32 @@ func SendFiles(cfg Config, to, subject, body string, attachPaths []string) error return fmt.Errorf("smtp client: %w", err) } if err := client.DialAndSend(m); err != nil { - return fmt.Errorf("send: %w", err) + return fmt.Errorf("send via %s:%d (%s, %s): %w", + cfg.Host, cfg.Port, cfg.Encryption, describeSize(attachPaths), err) } return nil } + +// describeSize reports what was attached, in bytes. +// +// "An existing connection was forcibly closed" during DATA is the same message +// whether the server refused the size, hit a quota, or simply dropped the +// socket — and the first of those is the only one the operator can act on +// directly. Naming the size rules it in or out without a second attempt. +func describeSize(paths []string) string { + var total int64 + n := 0 + for _, p := range paths { + if p == "" { + continue + } + if fi, err := os.Stat(p); err == nil { + total += fi.Size() + n++ + } + } + if n == 0 { + return "no attachment" + } + return fmt.Sprintf("%d attachment(s), %d KB", n, (total+1023)/1024) +} diff --git a/internal/email/size_test.go b/internal/email/size_test.go new file mode 100644 index 0000000..37ba242 --- /dev/null +++ b/internal/email/size_test.go @@ -0,0 +1,34 @@ +package email + +import ( + "os" + "path/filepath" + "testing" +) + +// "An existing connection was forcibly closed" during DATA reads the same +// whether the server refused the SIZE, hit a quota, or just dropped the socket +// — and only the first is something the operator can act on. Naming the size in +// the error rules it in or out without a second attempt. +func TestDescribeSize(t *testing.T) { + dir := t.TempDir() + a := filepath.Join(dir, "card.jpg") + if err := os.WriteFile(a, make([]byte, 2048), 0o644); err != nil { + t.Fatal(err) + } + + if got := describeSize(nil); got != "no attachment" { + t.Errorf("no attachment = %q", got) + } + if got := describeSize([]string{""}); got != "no attachment" { + t.Errorf("blank path = %q, want it ignored", got) + } + if got := describeSize([]string{a}); got != "1 attachment(s), 2 KB" { + t.Errorf("one file = %q", got) + } + // A path that does not exist must not be counted — the mail library skips it, + // so reporting it would describe a message that was never sent. + if got := describeSize([]string{a, filepath.Join(dir, "gone.jpg")}); got != "1 attachment(s), 2 KB" { + t.Errorf("missing file counted: %q", got) + } +}