Billionmail/BillionMail · error

reconnect failed: %w

Error message

reconnect failed: %w

What it means

After doSend failed (likely a stale connection), Send closes the client, nulls the connection, and retries Connect() once; this error wraps that reconnect failure. It means the fresh full connect (dial/TLS/auth) also failed, so the original send error is superseded by the reconnect error. Check the wrapped chain for the underlying cause.

Source

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

		e.mutex.Lock()
	}

	// Try to send the message, with reconnect on failure
	err := e.doSend(message, recipients)
	if err != nil {
		// Connection might be stale, try to reconnect once
		g.Log().Debug(context.Background(), "SMTP send failed, attempting reconnection")

		// Clean up
		e.client.Close()
		e.connected = false
		e.client = nil

		// Try to reconnect
		e.mutex.Unlock()
		if err := e.Connect(); err != nil {
			e.mutex.Lock()
			return fmt.Errorf("reconnect failed: %w", err)
		}
		e.mutex.Lock()

		// Try sending again after reconnect
		return e.doSend(message, recipients)
	}

	return nil
}

// doSend performs the actual message sending
func (e *EmailSender) doSend(message Message, recipients []string) error {
	// Add default headers if not present
	if message.Headers == nil {
		message.Headers = make(map[string]string)
	}

	// Add Message-ID if not already set

View on GitHub (pinned to fc36c76c05)

Solutions

  1. Unwrap to the underlying Connect error (dial vs TLS vs auth) and fix accordingly — do not assume the original doSend error was a network issue.
  2. Add backoff between the doSend failure and the reconnect attempt to ride out transient outages and avoid rate-limit loops.
  3. Verify credentials are still valid if the reconnect fails specifically at the AUTH stage.
  4. Consider a connection-health check (NOOP) before each send instead of relying on failure-then-reconnect.

Example fix

// before
e.mutex.Unlock()
if err := e.Connect(); err != nil {
    e.mutex.Lock()
    return fmt.Errorf("reconnect failed: %w", err)
}
e.mutex.Lock()
// after
e.mutex.Unlock()
time.Sleep(time.Second) // brief backoff before reconnect
var connErr error
for attempt := 0; attempt < 3; attempt++ {
    if connErr = e.Connect(); connErr == nil {
        break
    }
    time.Sleep(time.Duration(attempt+1) * 2 * time.Second)
}
e.mutex.Lock()
if connErr != nil {
    return fmt.Errorf("reconnect failed: %w", connErr)
}
Defensive patterns

Strategy: retry

Validate before calling

func sendWithGuard(sender *EmailSender, msg Message, rcpts []string) error {
    err := sender.Send(msg, rcpts)
    if err != nil && strings.Contains(err.Error(), "reconnect failed") {
        time.Sleep(5 * time.Second) // server likely down: wait before giving up
        err = sender.Send(msg, rcpts)
    }
    return err
}

Type guard

func isReconnectErr(err error) bool {
    return strings.Contains(err.Error(), "reconnect failed")
}

Try / catch

if err := sender.Send(msg, rcpts); err != nil {
    if isReconnectErr(err) {
        // schedule the message for a later retry queue rather than failing hard
        enqueueForRetry(msg, rcpts, backoff=1*time.Minute)
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: Send() on an existing connection whose doSend errored (server closed connection, timeout, reset), then the automatic re-connect also fails — network outage, server down, or credentials no longer accepted.

Common situations: SMTP server restarted or idle-killed the connection and is temporarily refusing new ones; a network blip spanning both the send failure and reconnect; password rotated between the original connect and reconnect; server rate-limiting repeated connections from the same IP.

Related errors


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