Arcwayv0.3.0

Mail

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 Nodemailer
  • console — 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:

  1. A system job (poll-inbound-mail) polls the IMAP server at the configured interval
  2. New unseen messages are fetched and automatically marked as seen
  3. Each message is stored in the __inbound_mail table (deduplicated by messageId)
  4. A mail/received event is emitted with the parsed email
  5. A separate system job (prune-inbound-mail) cleans up old messages based on retentionDays

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,
    });
  }
};

On this page