-
Notifications
You must be signed in to change notification settings - Fork 1
SMTP and Sending
This page explains how outbound email sending works in Mailbox — including how to configure SMTP, how the job queue processes emails, and how to integrate a third-party email API (e.g. Resend, Postmark) if you don't have an SMTP server.
Mailbox does not send emails instantly when you hit the send button. Instead, it treats every outbound email as a job — persisting it to the database first, then processing it in the background. This design makes sending reliable and fault-tolerant.
User clicks Send in the UI
│
▼
POST /api/emails/send (Next.js Route)
│
▼
email.service.ts → mailClient.sendMail()
│
▼ (bypassQueue = false)
MailQueueService.enqueue()
│ saves job to database
▼
emailJob table → status: PENDING ✅ (user sees "Sent")
│
│ Background Worker wakes up (within 3 seconds)
▼
MailQueueService.processQueue()
│ fetches up to 5 PENDING jobs ordered by priority desc
▼
MailJobRepository.lockJob() → status: PROCESSING 🔒 (atomic lock)
│
▼
mailClient.sendMailSmtp() (or custom provider)
│
┌────┴────┐
│ │
Success Failure
│ │
▼ ▼
status: SENT status: PENDING (retry, up to 3 attempts)
After 3 failures → status: FAILED
| Status | Meaning |
|---|---|
PENDING |
Job is queued and waiting to be picked up by the worker |
PROCESSING |
Worker has locked and is actively sending this job |
SENT |
Email was delivered successfully to the SMTP server |
FAILED |
All 3 send attempts failed — check your SMTP credentials |
When enqueuing, you can set a priority to control the send order:
| Priority | Value | Use Case |
|---|---|---|
HIGH |
3 | Transactional emails (password resets, invites) |
DEFAULT |
2 | Standard user replies and outbound emails |
LOW |
1 | Bulk or campaign emails |
- The worker starts automatically the first time any email is enqueued.
- It polls the
emailJobtable every 3 seconds and processes up to 5 jobs per cycle. - A separate prune interval runs once daily to delete
SENTjobs older than 7 days to keep the database clean.
SMTP is the simplest and most reliable way to send email. Any SMTP provider works — Gmail, Zoho, Mailgun, AWS SES, or your own mail server.
Add the following to your .env file:
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_SECURE=false # Set to true for port 465 (SSL)
SMTP_USER=you@yourdomain.com
SMTP_PASS=your-smtp-password
SMTP_FROM_NAME=MailboxIf you want bulk/campaign emails to go through a separate provider (e.g. Mailgun for campaigns, AWS SES for transactional), configure a second SMTP channel:
SMTP_CAMPAIGN_HOST=smtp.mailgun.org
SMTP_CAMPAIGN_PORT=587
SMTP_CAMPAIGN_SECURE=false
SMTP_CAMPAIGN_USER=campaigns@yourdomain.com
SMTP_CAMPAIGN_PASS=your-campaign-password
SMTP_CAMPAIGN_FROM_NAME=Your Company UpdatesAll emails enqueued with priority: LOW will automatically route to the campaign SMTP. All other priorities use the primary SMTP.
If you want to test the queue and send flow without actually sending emails, set:
SMTP_DRY_RUN=trueInstead of dispatching to SMTP, the system will print the full email payload to the server console. Useful for local development and staging environments.
If you don't have an SMTP server, you can integrate a transactional email API provider such as Resend, Postmark, or SendGrid. These APIs are often easier to set up and have generous free tiers.
Important
This requires writing a small amount of custom code. The system has a blueprint placeholder ready for you in src/clients/mail.ts — you just need to fill it in.
For example, using Resend:
npm install resendEMAIL_PROVIDER=RESEND
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxOpen src/clients/mail.ts and find the sendMailCustom method. Replace the placeholder with your provider's SDK call:
// Example: Resend integration
async sendMailCustom(options: SendMailOptions) {
const { Resend } = await import("resend");
const resend = new Resend(process.env.RESEND_API_KEY);
const { data, error } = await resend.emails.send({
from: options.from || `Mailbox <noreply@yourdomain.com>`,
to: options.to,
cc: options.cc,
bcc: options.bcc,
subject: options.subject,
text: options.text,
html: options.html,
});
if (error) throw new Error(error.message);
return data;
},In the same file, uncomment the provider routing block above the SMTP call:
// ── 2. CUSTOM PROVIDER ─────────────────────────────────────────────────────
if (process.env.EMAIL_PROVIDER === "RESEND") {
return this.sendMailCustom(options);
}That's it — the queue system, retry logic, priority routing, and prune intervals all continue to work exactly the same. Only the final delivery method changes.
If SMTP is not configured and no custom provider is active, the system automatically falls back to console logging instead of throwing an error. This means your application will never crash due to a missing mail config — emails will just be printed to the server logs instead of delivered.
| Issue | Solution |
|---|---|
Emails stuck in PENDING
|
Check that the Next.js server process is running — the worker lives in the same process |
FAILED status after 3 attempts |
Verify SMTP_HOST, SMTP_USER, and SMTP_PASS are correct |
| Emails printing to console instead of sending |
SMTP_DRY_RUN=true is set, or SMTP_HOST / SMTP_USER are missing from .env
|
sendMailCustom not implemented error |
You enabled EMAIL_PROVIDER but haven't filled in the sendMailCustom method yet |