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
- Test connectivity: nc -zv <host> <port> from the same machine/container the app runs on.
- Check DNS resolution of e.Host (dig/nslookup) and fix the configured hostname if stale.
- If relaying via localhost, verify the local MTA is listening (ss -ltnp | grep :25) and running.
- 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
- Pre-check host:port reachability at startup and before campaigns.
- Verify DNS and firewall/egress rules, especially cloud provider port-25 blocks.
- Ensure the local MTA is running when relaying via localhost.
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
- TLS dial: %w
- %s
- failed to connect to SMTP server: %v
- failed to connect to SMTP server: %v
- base URL not configured
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/0b4b8598b2bafb64.
Report an issue: GitHub.