Billionmail/BillionMail · error

SMTP mail: %w

Error message

SMTP mail: %w

What it means

doSend's first SMTP transaction command, client.Mail(e.Email) (MAIL FROM), failed — the server rejected the envelope sender or the connection was in a bad state. The wrapped error contains the server's 4xx/5xx reply (e.g. 501 bad address syntax, 550 relay denied, 553 sender not permitted). Note e.Email is the configured envelope sender, distinct from any From header.

Source

Thrown at core/internal/service/mail_service/sending.go:414

		"\r\n"

	msg := []byte(headerString)

	var err error

	defer func() {
		// Reset the connection state if sending fails
		if err != nil {
			if err = e.client.Reset(); err != nil {
				g.Log().Warning(context.Background(), "SMTP reset: %w", err)
			}
		}
	}()

	// Set the sender
	// MAIL FROM
	if err = e.client.Mail(e.Email); err != nil {
		return fmt.Errorf("SMTP mail: %w", err)
	}

	// Set the recipients
	// RCPT TO
	for _, to := range recipients {
		if err = e.client.Rcpt(to); err != nil {
			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

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Read the wrapped server reply to distinguish syntax errors (501), policy rejection (550/553), or connection errors (EOF — treat as the reconnect case).
  2. Make e.Email a valid address on a domain the account is authorized to send from; match the authenticated username when the relay requires it.
  3. Verify the sending domain is configured/verified on the mail server (SPF/DKIM/relay allowlist).
  4. If the error is an EOF/protocol error, reconnect and retry once — the session was stale rather than the address rejected.

Example fix

// before
if err = e.client.Mail(e.Email); err != nil {
    return fmt.Errorf("SMTP mail: %w", err)
}
// after
if err = e.client.Mail(e.Email); err != nil {
    if errors.Is(err, io.EOF) || errors.Is(err, syscall.EPIPE) {
        return fmt.Errorf("SMTP mail: stale connection: %w", err) // triggers Send's reconnect path
    }
    return fmt.Errorf("SMTP mail (from %s): %w", e.Email, err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

from, err := mail.ParseAddress(senderEmail)
if err != nil {
    return fmt.Errorf("invalid envelope sender %q: %w", senderEmail, err)
}
// also verify the domain is one the relay account is authorized to send from

Type guard

func isMailFromRejected(err error) bool {
    var protoErr *textproto.Error
    if !errors.As(err, &protoErr) {
        return false
    }
    switch {
    case protoErr.Code == 501:
        return true // syntax
    case protoErr.Code >= 550 && protoErr.Code <= 553:
        return true // policy / sender rejected
    }
    return false
}

Try / catch

if err := sender.Send(msg, rcpts); err != nil {
    if strings.Contains(err.Error(), "SMTP mail") {
        if isMailFromRejected(err) {
            return fmt.Errorf("envelope sender %q rejected by relay (verify domain/SPF): %w", fromAddr, err)
        }
        if errors.Is(err, io.EOF) {
            time.Sleep(2 * time.Second)
            return sender.Send(msg, rcpts) // stale session: retry reconnects
        }
    }
    return err
}

Prevention

When it happens

Trigger: doSend called with an e.Email the server refuses: malformed address, domain not allowed for that relay account, sender must match the authenticated user, or the SMTP session was already broken so the command got an EOF/protocol error.

Common situations: Relay enforces MAIL FROM equals the authenticated user (mismatch in configured e.Email); sending domain not verified at the provider (550 sender rejected); e.Email empty or containing invalid characters; server dropped the connection after long idle before MAIL FROM; rate/relay limits (452/454).

Related errors


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