Billionmail/BillionMail · error

SMTP mail

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.

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.

Appendix: 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)