Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+192
View File
@@ -0,0 +1,192 @@
# 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
```