chore(email): name the server, encryption and attachment size on a send failure

"An existing connection was forcibly closed by the remote host" during DATA
reads the same whether the server refused the SIZE, hit an hourly quota, or
simply dropped the socket — and only the first is something an operator can act
on. The message now carries host:port, the encryption in use and the total
attachment size, so the size can be ruled in or out without a second attempt.

A path that does not exist is not counted: the mail library skips it, so
reporting it would describe a message that was never sent.
This commit is contained in:
2026-08-13 12:09:57 +02:00
parent eba54344b0
commit 2695747db6
2 changed files with 61 additions and 1 deletions
+27 -1
View File
@@ -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)
}