Billionmail/BillionMail · error
SMTP close writer: %w
Error message
SMTP close writer: %w
What it means
This error wraps a failure from w.Close() on the DATA-phase writer. Closing the writer sends the terminating '.' to end the DATA command and awaits the server's final delivery status (e.g. '250 Ok: queued'). A failure here means the server rejected the message at end-of-data or the connection failed before the final reply.
Source
Thrown at core/internal/service/mail_service/sending.go:439
return fmt.Errorf("SMTP rcpt: %w", err)
}
}
// Get a writer for the message body
var w io.WriteCloser
w, err = e.client.Data()
if err != nil {
return fmt.Errorf("SMTP data: %w", err)
}
// Write the message
if _, err = w.Write(msg); err != nil {
return fmt.Errorf("SMTP write: %w", err)
}
// Close the writer
if err = w.Close(); err != nil {
return fmt.Errorf("SMTP close writer: %w", err)
}
return nil
}
// isSecure check if TLS connection is needed
func (e *EmailSender) isSecure() bool {
// usually,
// port 465 is used for SMTP with SSL (implicit TLS)
// port 587 is used for SMTP with STARTTLS (explicit TLS)
if e.Port == "465" {
return true
} else if e.Port == "587" {
// We will use STARTTLS if the port is 587
return false
}
return false
}View on GitHub (pinned to fc36c76c05)
Solutions
- Inspect the wrapped error and mail server logs for the final SMTP reply code (content-rejected vs timeout)
- Reduce message size or remove content triggering the spam filter; check rspamd scores
- Retry the send; end-of-data 4xx replies (greylisting/throttling) are often transient
- Confirm firewall/load-balancer idle timeouts are longer than the DATA transfer duration
Defensive patterns
Strategy: try-catch
Validate before calling
// No pre-call check possible: end-of-data verdict depends on server content policy.
// Pre-screen content with rspamd client if available:
if spamScore, _ := rspamd.Check(msg); spamScore > threshold {
return errors.New("message likely to be rejected at end-of-data")
} Try / catch
err := sender.Send(msg, recipients)
var smtpErr *textproto.Error
if errors.As(err, &smtpErr) && strings.Contains(err.Error(), "SMTP close writer") {
switch {
case smtpErr.Code == 451 || smtpErr.Code == 452: requeueLater(msg)
case smtpErr.Code >= 500: quarantine(msg, smtpErr.Error())
}
} Prevention
- Check rspamd/spam-filter scores before sending content-heavy campaigns
- Treat 4xx end-of-data replies as transient and retry with backoff
- Ensure firewall idle timeouts exceed longest expected DATA transfer
- Log the wrapped textproto.Error code for every close-writer failure
When it happens
Trigger: w.Close() returned an error because the server replied with a non-2xx code at end-of-DATA (e.g. '554 5.7.1 message content rejected' by rspamd), or the connection dropped before the final reply arrived.
Common situations: Spam/content filters rejecting the finished message; oversized message rejected after full upload; server crash or timeout during queueing; connection killed by firewall after long DATA transfer.
Related errors
- failed to send test email to %d recipient(s): %v
- SMTP rcpt: %w
- SMTP data: %w
- SMTP server is required
- SMTP port must be between 1 and 65535
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/47d08eed7d22d600.
Report an issue: GitHub.