6.1 KiB
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 andemail_verified_atreset logicMailingBounce::isSuppressed()— used byUniversalBounceHandleron every outgoing email- IMAP connection, message fetching, DSN parsing (IMAP mode only)
What it does NOT test
- The scheduled
mailings:process-bouncesjob (same IMAP logic, different command) - Queue processing — run
php artisan queue:workseparately - Mailpit SMTP layer — use
copy_to_mailpitin config to verify emails land in Mailpit UI
Related commands
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