Billionmail/BillionMail · error

reconnect failed

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.

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.

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