SMTP — Email Testing
Your app sends emails — registration confirmations, password resets, notifications. You need to verify they look correct without actually delivering them. Buggregator includes a fake SMTP server: point your app's mail config at port 1025 and every email shows up in the UI. No Mailhog, no Mailtrap, no external services — just another module in the same Buggregator instance where you already see your logs and exceptions.

Use cases
- Email template testing — preview HTML emails with a desktop/mobile viewport switcher to check responsive layouts.
- Transactional email verification — make sure the right recipients, subject, and content are set before going to production.
- Attachment testing — verify that attachments are generated correctly and can be downloaded.
- All in one place — see emails in the same UI as your exceptions, logs, and dumps.
What you see in the UI
- HTML preview — rendered email with desktop/mobile device viewport switcher.
- Plain text — the plain text version of the email.
- Addresses — From, To, CC, BCC, Reply-To organized by type.
- Attachments — download or preview any attached file. Inline images display correctly in the HTML preview.
- Raw source — the raw MIME source for debugging encoding or header issues.
Configuration
Point your app's SMTP settings to 127.0.0.1:1025. No authentication required.
In Docker Compose, replace
127.0.0.1with the Buggregator service name (e.g.,buggregator).
Laravel
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025Spiral Framework
MAILER_DSN=smtp://127.0.0.1:1025Symfony
MAILER_DSN=smtp://127.0.0.1:1025Magento 2
CONFIG__DEFAULT__SYSTEM__SMTP__TRANSPORT="smtp"
CONFIG__DEFAULT__SYSTEM__SMTP__HOST="127.0.0.1"
CONFIG__DEFAULT__SYSTEM__SMTP__PORT="1025"or via CLI:
bin/magento config:set system/smtp/transport smtp
bin/magento config:set system/smtp/host 127.0.0.1
bin/magento config:set system/smtp/port 1025WordPress
Save to the mu-plugins directory (e.g., smtp.php):
<?php
/**
* Plugin Name: MU Buggregator SMTP
*/
if (! defined('WP_ENVIRONMENT_TYPE') || 'local' !== WP_ENVIRONMENT_TYPE) {
return;
}
add_action( 'phpmailer_init', function( $phpmailer ) : void {
$phpmailer->isSMTP();
$phpmailer->Host = '127.0.0.1';
$phpmailer->Port = 1025;
} );Any other app
Any application that can send email via SMTP works. Set host to 127.0.0.1 and port to 1025.
Testing API
End-to-end tests often need to check an email: "did the reset mail arrive, and what is the code in it?" The testing API answers that over plain HTTP. Example: your test clicks "Forgot password", waits for the email to alice@example.com, and reads the 6-digit code from it.
All routes below need the same login as the rest of /api when SSO is on: send the X-Auth-Token header (see Calling the API from scripts).
| Method | Path | Answer |
|---|---|---|
GET | /api/smtp/cursor | {"cursor": "2026-11-07T08:30:09.123456789Z"}: the server time now |
GET | /api/smtp/messages | messages that match the filters: {"data": [event, …], "meta": {"total", "limit", "offset"}} |
GET | /api/smtp/messages/wait | the first message that matches, as soon as there is one; 408 on timeout |
GET | /api/smtp/stats?project= | {"count": 3, "last_received_at": "2026-11-07T08:30:09Z"}; last_received_at is null with no mail |
DELETE | /api/smtp/messages?project= | deletes the messages (of one project, or all): {"status": true} |
GET | /api/smtp/message/{uuid}/raw | the original message source (Content-Type: message/rfc822) |
GET | /api/smtp/message/{uuid}/links | links from the HTML and text parts: {"data": [{"url", "text", "source"}]}; source is html or text |
GET | /api/smtp/message/{uuid}/codes?pattern= | codes found in the text and HTML parts: {"data": ["482913"], "pattern": "\\b\\d{4,8}\\b"} |
An event is a normal Buggregator event: uuid, type (smtp), timestamp, project and payload. The payload has subject, from, to, cc, reply_to (lists of {"email", "name"}), bcc, text, html and raw.
Errors are {"message", "code"}. An unknown uuid gets 404.
Filters
/messages and /messages/wait take the same filters. Every filter you pass must match.
| Parameter | Matches when |
|---|---|
to, from, cc | any address or name in that field contains the value, ignoring case (to=alice finds Alice <alice@example.com>) |
subject | the subject is exactly the value |
subject_contains | the subject contains the value (case matters) |
subject_regex | the subject matches the Go regular expression; an invalid one matches nothing |
body_contains | the text or the HTML part contains the value (case matters) |
project | the message belongs to that project |
since, until | received at or after / at or before this time: RFC 3339 (2026-11-07T08:30:00Z), unix seconds, or unix milliseconds. A value that does not parse is ignored |
/messages also takes:
order:desc(newest first, the default) orasc.limitandoffsetfor pages. Nolimitreturns all matches.meta.totalcounts all matches before the page.
Waiting for an email
/messages/wait holds the request open until a matching message arrives. It also takes timeout: a Go duration such as 10s or 1m, default 30s, at most 60s.
A message that is already stored and matches comes back at once. So take a cursor before the action, and pass it as since. Otherwise an old email from an earlier test can match.
Example — a password reset test:
# 1. Remember "now"
CURSOR=$(curl -s http://127.0.0.1:8000/api/smtp/cursor | jq -r .cursor)
# 2. Your test asks the app to send the reset email to alice@example.com
# 3. Wait up to 20 seconds for it
UUID=$(curl -s -G http://127.0.0.1:8000/api/smtp/messages/wait \
--data-urlencode "since=$CURSOR" \
--data-urlencode "to=alice@example.com" \
--data-urlencode "subject_contains=Reset" \
--data-urlencode "timeout=20s" | jq -r .uuid)
# 4. Read the code from it
curl -s http://127.0.0.1:8000/api/smtp/message/$UUID/codes
# {"data":["482913"],"pattern":"\\b\\d{4,8}\\b"}Codes
/codes looks for a regular expression in the text part and in the HTML part (tags removed). The default pattern finds numbers of 4 to 8 digits. Each code is listed once. Pass your own pattern for other codes, for example pattern=[A-Z0-9]{6}. An invalid pattern gets 400.
Attachments
GET /api/smtp/attachments/{uuid} lists the attachments of a message: {"data": [{"uuid", "name", "path", "size", "mime"}]}. It needs the login like the routes above. The download (/api/smtp/attachments/{event uuid}/{attachment uuid}) and the preview (/api/smtp/attachments/{event uuid}/preview/{attachment uuid}) links stay open: they work only with both ids, which only the protected API gives out.