Billionmail/BillionMail · error

SMTP dial: %w

Error message

SMTP dial: %w

What it means

This error wraps smtp.Dial failing in connectPlain — a plain TCP connection to host:port could not be established. It happens at the TCP layer before any SMTP exchange, so causes are DNS failure, connection refused, timeouts, or network unreachability. It is returned for any non-secure sender (ports 25/587).

Source

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

		return fmt.Errorf("new SMTP client: %w", err)
	}

	auth := smtp.PlainAuth("", e.UserName, e.Password, e.Host)
	if err = client.Auth(auth); err != nil {
		client.Close()
		return fmt.Errorf("SMTP auth: %w", err)
	}

	e.client = client

	return nil
}

// connectPlain establishes a plain SMTP connection
func (e *EmailSender) connectPlain() error {
	client, err := smtp.Dial(net.JoinHostPort(e.Host, e.Port))
	if err != nil {
		return fmt.Errorf("SMTP dial: %w", err)
	}

	// Check if STARTTLS is needed
	if e.Port == "587" {
		if err = client.StartTLS(&tls.Config{
			MinVersion:         tls.VersionTLS12,
			InsecureSkipVerify: true,
			ServerName:         e.SNI,
		}); err != nil {
			client.Close()
			return fmt.Errorf("SMTP STARTTLS: %w", err)
		}
	}

	var auth smtp.Auth

	if e.Port == "25" {
		auth = &customAuth{e.UserName, e.Password}

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Test connectivity: nc -zv <host> <port> from the same machine/container the app runs on.
  2. Check DNS resolution of e.Host (dig/nslookup) and fix the configured hostname if stale.
  3. If relaying via localhost, verify the local MTA is listening (ss -ltnp | grep :25) and running.
  4. If hosted on a cloud provider, confirm outbound port 25 is not blocked; use the provider's relay or port 587/465 instead.

Example fix

// before
client, err := smtp.Dial(net.JoinHostPort(e.Host, e.Port))
// after
d := net.Dialer{Timeout: 10 * time.Second}
conn, err := d.Dial("tcp", net.JoinHostPort(e.Host, e.Port))
if err != nil {
    return fmt.Errorf("SMTP dial: %w (check host %q reachability and firewall rules)", err, e.Host)
}
client, err := smtp.NewClient(conn, e.Host)
Defensive patterns

Strategy: validation

Validate before calling

func plainSMTPReachable(host, port string) error {
    conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), 10*time.Second)
    if err != nil {
        return fmt.Errorf("cannot reach %s:%s: %w", host, port, err)
    }
    conn.Close()
    return nil
}
// e.g. plainSMTPReachable("relay.example.com", "587")

Type guard

func isDialFailure(err error) bool {
    var opErr *net.OpError
    var dnsErr *net.DNSError
    return errors.As(err, &opErr) || errors.As(err, &dnsErr)
}

Try / catch

if err := sender.Send(msg, rcpts); err != nil {
    var dnsErr *net.DNSError
    if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
        return fmt.Errorf("SMTP host %q does not resolve — check config", host)
    }
    if errors.Is(err, syscall.ECONNREFUSED) {
        // retry later or fall back to an alternate relay
    }
}

Prevention

When it happens

Trigger: EmailSender.Connect() with isSecure()==false and smtp.Dial(net.JoinHostPort(e.Host, e.Port)) fails: host not resolvable, port closed/refused, or connection timed out.

Common situations: Mail relay hostname typo or stale DNS; local Postfix relay not running (connection refused on 127.0.0.1:25); cloud providers blocking outbound port 25 (AWS/GCP/Azure default); Docker container lacking egress; firewalls dropping 587.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


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