Billionmail/BillionMail · error

failed to import recipients

Error message

failed to import recipients (batch %d/%d): %w

What it means

ImportRecipients inserts recipient rows into recipient_info in chunks using InsertIgnore. When any batch insert fails at the database level, the error is logged and wrapped as "failed to import recipients (batch %d/%d): %w", aborting the whole import. Because earlier batches were already committed, the import is left partially complete.

Solutions

  1. Read the logged underlying DB error for the failing batch number
  2. Validate/normalize CSV rows (email format, field lengths, required columns) before importing
  3. Ensure the recipient_info schema matches the value maps (run migrations)
  4. Make imports resumable/idempotent (skip already-inserted batches via InsertIgnore semantics) and retry from the failed batch

Example fix

// before: abort entire import on first batch failure
if err != nil {
    return fmt.Errorf("failed to import recipients (batch %d/%d): %w", i+1, totalBatches, err)
}
// after: validate rows before insert and report batch context
for _, v := range values {
    if !isValidEmail(v["email"].(string)) {
        return fmt.Errorf("invalid email in batch %d/%d: %s", i+1, totalBatches, v["email"])
    }
}
if _, err := g.DB().Model("recipient_info").InsertIgnore(values); err != nil {
    return fmt.Errorf("failed to import recipients (batch %d/%d): %w", i+1, totalBatches, err)
}
Defensive patterns

Strategy: validation

Validate before calling

for _, r := range recipients {
    if r.Email == "" || len(r.Email) > 254 || !strings.Contains(r.Email, "@") {
        return fmt.Errorf("invalid recipient email: %q", r.Email)
    }
}

Try / catch

if err := ImportRecipients(ctx, taskId, recipients); err != nil {
    log.Printf("import aborted: %v (earlier batches already committed)", err)
    return err
}

Prevention

When it happens

Trigger: Any batch's INSERT into recipient_info errors: schema mismatch between value maps and table columns, invalid/overlong field values (bad email, oversized custom fields), DB connection loss mid-import, or constraint violations not suppressed by InsertIgnore (e.g. NOT NULL without default).

Common situations: Importing a CSV with columns not matching recipient_info schema; multi-byte or malformed emails; Postgres restarted during a large import; duplicates are fine (InsertIgnore) but NOT NULL/length violations are not.

Related errors


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

Appendix: source

Thrown at core/internal/service/batch_mail/batch_mail.go:246

		// Prepare data for current batch
		values := make([]g.Map, len(currentBatch))
		for j, contact := range currentBatch {
			values[j] = g.Map{
				"task_id":     taskId,
				"recipient":   contact.Email,
				"is_sent":     0,
				"sent_time":   0,
				"message_id":  "",
				"create_time": now,
			}
		}

		// Insert current batch
		result, err := g.DB().Model("recipient_info").InsertIgnore(values)
		if err != nil {
			g.Log().Error(ctx, "Failed to import recipient batch %d/%d for task %d: %v",
				i+1, totalBatches, taskId, err)
			return fmt.Errorf("failed to import recipients (batch %d/%d): %w", i+1, totalBatches, err)
		}

		// Count affected rows
		affected, err := result.RowsAffected()
		if err != nil {
			g.Log().Debugf(ctx, "Could not get affected rows for batch %d/%d: %v", i+1, totalBatches, err)
		} else {
			totalImported += int(affected)
		}
	}

	g.Log().Info(ctx, "Task %d: Total %d recipients imported successfully", taskId, totalImported)
	return nil
}

// ImportRecipientsTx
func ImportRecipientsTx(ctx context.Context, tx gdb.TX, taskId int, contacts []*entity.Contact) error {
	if len(contacts) == 0 {

View on GitHub (pinned to fc36c76c05)