Email sending via SMTP and receiving via IMAP with background queue support
The ctx.mail service provides email sending with support for immediate delivery and background queuing.
Sending Immediately
const result = await ctx.mail.send({
to: 'user@example.com', // string or string[]
subject: 'Welcome!',
html: '<h1>Hello</h1>',
text: 'Hello', // Optional plain-text fallback
from: 'custom@app.com', // Optional, overrides config default
cc: 'cc@example.com', // Optional, string or string[]
bcc: 'bcc@example.com', // Optional, string or string[]
replyTo: 'reply@app.com', // Optional
});
// result: { accepted: ['user@example.com'], rejected: [], messageId: '...' }Queuing for Background Delivery
await ctx.mail.queue({
to: 'user@example.com',
subject: 'Your report is ready',
html: '<p>Download your report...</p>',
});Queued emails are pushed to the mail:outbound topic and processed by a system job (send-mail) in batches of 10. This is useful for sending many emails without blocking the request.
Mail Configuration
// arcway.config.js
export default {
mail: {
driver: 'smtp',
from: 'noreply@myapp.com', // Default sender
smtp: {
host: 'smtp.example.com',
port: 587,
secure: false, // true for port 465
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
},
};Mail Drivers
smtp— sends via SMTP server using Nodemailerconsole— logs emails to terminal (development). Stores messages in an internal array for testing.
Inbound Mail
Configure IMAP polling to receive incoming emails:
// arcway.config.js
export default {
mail: {
inbound: {
driver: 'imap',
imap: {
host: 'imap.example.com',
port: 993, // Default
tls: true, // Default
auth: {
user: process.env.IMAP_USER,
pass: process.env.IMAP_PASS,
},
mailbox: 'INBOX', // Default
pollIntervalSeconds: 30, // Default
},
retentionDays: 30, // Default. Set to 0 to disable pruning.
},
},
};How it works:
- A system job (
poll-inbound-mail) polls the IMAP server at the configured interval - New unseen messages are fetched and automatically marked as seen
- Each message is stored in the
__inbound_mailtable (deduplicated bymessageId) - A
mail/receivedevent is emitted with the parsed email - A separate system job (
prune-inbound-mail) cleans up old messages based onretentionDays
Parsed email payload:
{
messageId, // IMAP message ID
from, // sender address
to, // recipient(s)
cc, // CC recipients
subject, // email subject
text, // plain text body
html, // HTML body
hasAttachments,// boolean
date, // Date object
headers, // raw headers
}Listen for incoming mail:
// listeners/mail/received.js
export default async (ctx) => {
const { db, log } = ctx;
const email = ctx.event.payload;
log.info('Received email', { from: email.from, subject: email.subject });
if (email.subject.startsWith('[SUPPORT]')) {
await db('support_tickets').insert({
email_from: email.from,
subject: email.subject,
body: email.text,
});
}
};