# 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 ```bash 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. ```env 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) ```bash # 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) ```bash # 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 ```bash # 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 ```bash # 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 ## Related commands ```bash 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 ```