Files
timebank-cc-public/references/BOUNCE_SIMULATE_COMMAND.md
T

6.1 KiB

bounce:simulate — Bounce Testing Command

Single Artisan command for testing the full bounce/suppression pipeline in any environment. Works in two modes:

  • Inject mode (local dev): injects a fake bounce directly into the DB — no mailbox needed
  • IMAP mode (production): connects to the real bounce mailbox configured in .env, parses and records actual DSN messages

Both modes call MailingBounce::recordBounce() — the same code path, so threshold logic, suppression, and email_verified_at reset behave identically.

Usage

php artisan bounce:simulate [options]

Options

Inject mode

Option Description
--email= Email address to simulate bounce for (required in inject mode)
--type= Bounce type: hard, soft, complaint (default: hard)
--reason= Bounce reason string. Defaults to a definitive pattern per type
--mailing-id= Optional: link bounce to a specific mailing ID (validated)
--status Show current bounce state only — record nothing
--clear Delete all existing bounce records for this address first
--suppress Directly suppress the address, bypassing threshold logic

IMAP mode

Option Description
--imap Read real bounce emails from the IMAP mailbox configured in .env
--dry-run Parse and display bounces without recording them
--delete Delete processed emails from the mailbox after recording

Default reasons (inject mode)

Type Default reason
hard 550 5.1.1 user unknown — matches a definitive pattern, counts toward thresholds
soft 452 mailbox full — does NOT match a definitive pattern, no threshold action
complaint Spam complaint

IMAP configuration (.env)

IMAP mode reads these keys from .env. If any of BOUNCE_HOST, BOUNCE_USERNAME, or BOUNCE_PASSWORD are missing, the command skips gracefully with a clear message.

BOUNCE_HOST=imap.yourdomain.org
BOUNCE_PORT=993
BOUNCE_USERNAME=bounces@yourdomain.org
BOUNCE_PASSWORD=your-password
BOUNCE_SSL=true
BOUNCE_PROTOCOL=imap

Examples

Inject mode (local dev)

# Check current state without recording anything
php artisan bounce:simulate --email=user@example.com --status

# Record a hard bounce (triggers suppression if suppression_threshold=1)
php artisan bounce:simulate --email=user@example.com

# Record a soft bounce — recorded but never triggers suppression
php artisan bounce:simulate --email=user@example.com --type=soft

# Reset records between tests
php artisan bounce:simulate --email=user@example.com --clear --status

# Hard bounce with custom reason, linked to mailing #3
php artisan bounce:simulate --email=user@example.com --clear --reason="user unknown" --mailing-id=3

# Directly suppress without threshold logic
php artisan bounce:simulate --email=user@example.com --suppress

IMAP mode (production)

# Verify connection and parsing — record nothing
php artisan bounce:simulate --imap --dry-run

# Connect, parse, and record all bounces found
php artisan bounce:simulate --imap

# Connect, parse, record, and delete processed emails from mailbox
php artisan bounce:simulate --imap --delete

If IMAP credentials are not set in .env, the command prints a warning and exits cleanly — safe to run on local dev.

Output (inject mode)

=== Before: user@example.com ===
  Total bounces           : 0
  Recent bounces          : 0 (window: 365 days)
  Recent definitive hard  : 0
  Suppressed              : no
  Suppression threshold   : 1
  Verif. reset threshold  : 1

Recording hard bounce for user@example.com
Reason: 550 5.1.1 user unknown
Matches definitive hard-bounce pattern: yes

Bounce recorded.

=== After: user@example.com ===
  Total bounces           : 1
  Recent bounces          : 1 (window: 365 days)
  Recent definitive hard  : 1
  Suppressed              : YES
  Suppression threshold   : 1
  Verif. reset threshold  : 1

Output (IMAP mode)

Connecting to IMAP mailbox: imap.yourdomain.org as bounces@yourdomain.org
Found 2 bounce email(s).

  [#1] hard bounce → faileduser@somedomain.com
       Reason: 550 5.1.1 User unknown
       Definitive hard-bounce pattern: yes

=== Before: faileduser@somedomain.com ===
  ...

=== After: faileduser@somedomain.com ===
  ...

Done. Processed: 2, skipped: 0, deleted from mailbox.

Typical test flows

Local dev

# 1. Check baseline
php artisan bounce:simulate --email=user@example.com --status

# 2. Simulate a hard bounce
php artisan bounce:simulate --email=user@example.com

# 3. Confirm suppression
php artisan mailings:manage-bounces stats

# 4. Reset for next test
php artisan bounce:simulate --email=user@example.com --clear

Production

# 1. Dry run — verify IMAP connection and bounce parsing
php artisan bounce:simulate --imap --dry-run

# 2. Process and clear mailbox
php artisan bounce:simulate --imap --delete

# 3. Inspect results
php artisan mailings:manage-bounces stats

What the command exercises

  • MailingBounce::recordBounce() — creates the DB record
  • Definitive hard-bounce pattern matching — determines whether threshold logic runs
  • checkAndApplyThresholds() — suppression and email_verified_at reset logic
  • MailingBounce::isSuppressed() — used by UniversalBounceHandler on every outgoing email
  • IMAP connection, message fetching, DSN parsing (IMAP mode only)

What it does NOT test

  • The scheduled mailings:process-bounces job (same IMAP logic, different command)
  • Queue processing — run php artisan queue:work separately
  • Mailpit SMTP layer — use copy_to_mailpit in config to verify emails land in Mailpit UI
php artisan mailings:manage-bounces stats               # Full suppression list and stats
php artisan mailings:manage-bounces list                # List all bounced addresses
php artisan mailings:manage-bounces check-thresholds    # Check all addresses against thresholds
php artisan mailings:manage-bounces unsuppress --email= # Remove suppression
php artisan mailings:manage-bounces cleanup --days=90   # Delete old soft bounces