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

  1. Inspect the wrapped error and mail server logs for the final SMTP reply code (content-rejected vs timeout)
  2. Reduce message size or remove content triggering the spam filter; check rspamd scores
  3. Retry the send; end-of-data 4xx replies (greylisting/throttling) are often transient
  4. 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

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


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/47d08eed7d22d600. Report an issue: GitHub.