New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@strav/mail

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@strav/mail

Strav signal layer — mail (core + array/log transports + Mailable + queue-dispatch); notifications + SSE + broadcast follow in later slices

latest
npmnpm
Version
1.0.3
Version published
Maintainers
1
Created
Source

@strav/mail

Outbound communication for Strav 1.0. Mail layer covers sync send + queued delivery + three production HTTP transports:

  • Message + MailRecipient + MailAddress + MessageAttachment — the plain-data envelope.
  • Transport interface — what every backend implements (send, optional close).
  • ArrayTransport — in-memory recorder for tests.
  • LogTransport — writes mail.sent records to a Logger channel; local-dev default.
  • ResendTransport + SendGridTransport + MailgunTransport + AlibabaDmTransport — production HTTP transports. Pure-fetch, no SDK deps, no nodemailer. Alibaba Cloud DirectMail covers China + SEA deliverability.
  • MailTransportError — typed StravError raised by transports on send failure; carries provider / status / retryable / providerError in context.
  • PostmarkInboundParser + MailgunInboundParser — normalize provider webhooks to ParsedInboundMail. Mailgun verifies HMAC-SHA256 + timestamp; Postmark relies on HTTP-level auth (Basic / IP allow-list).
  • isAutoGeneratedMessage — mail-loop guard. Honour it before auto-responding.
  • MailInboundError — raised when an inbound webhook payload is malformed.
  • MailManager — multi-transport orchestration with default-from substitution + Mailable-aware send overload + lazy/cached transport build.
  • MailProvider — wires config.mail into the container.
  • Mailable<TPayload> — typed Job subclass; override build(payload), dispatch via queue.dispatch(YourMailable, payload) for async delivery with retries / dead-letter.

Status: 1.0.0-alpha — outbound mail layer + Resend + SendGrid + Mailgun + Alibaba DirectMail transports shipped, plus Postmark + Mailgun inbound webhook parsers. Multi-channel fan-out lives in @strav/notification. No SMTP transport — see docs/mail/README.md for the rationale.

Install

bun add @strav/mail

Peer: @strav/kernel.

Minimal example

// config/mail.ts
import type { MailConfig } from '@strav/mail'

export default {
  default: 'array',                       // or 'log' in dev, 'smtp' once it ships
  from: { email: 'noreply@acme.com', name: 'Acme' },
  transports: {
    array: { driver: 'array' },
    log: { driver: 'log', channel: 'mail' },
  },
} satisfies MailConfig
// in a controller or service
@inject()
class SignupController {
  constructor(private readonly mail: MailManager) {}

  async send(email: string): Promise<void> {
    await this.mail.send({
      to: email,
      subject: 'Welcome',
      html: '<h1>Welcome</h1>',
      text: 'Welcome',
    })
  }
}

Test integration

await mail.send({ to: 'a@x', subject: 'hi', text: 'h' })
expect((mail.via() as ArrayTransport).messages[0]?.subject).toBe('hi')

ArrayTransport.messages is a frozen view of every send since the last clear().

Mailable + queue

import { Mailable, type Message } from '@strav/mail'

class WelcomeEmail extends Mailable<{ name: string }> {
  static override readonly jobName = 'mail.welcome'
  build(payload: { name: string }): Message {
    return { to: `${payload.name}@x`, subject: 'Welcome', text: `Hi ${payload.name}` }
  }
}

// Register with JobRegistry (same as any other Job).
registry.register(WelcomeEmail)

// Dispatch:
await queue.dispatch(WelcomeEmail, { name: 'Alice' })  // async, retried
await mail.send(WelcomeEmail, { name: 'Alice' })       // sync, inline

Mailables participate in the full @strav/queue lifecycle (retries, backoff, strav_failed_jobs dead-letter).

Inbound webhooks

import { MailgunInboundParser, PostmarkInboundParser } from '@strav/mail'

const postmark = new PostmarkInboundParser()
const mailgun = new MailgunInboundParser({ webhookSigningKey: env.MAILGUN_SIGNING_KEY })

// In your HTTP handler — pass the raw body + lowercased headers:
const mail = await mailgun.parse({ body: rawBody, headers: req.headers })
if (mail.isAutoGenerated) return        // mail-loop guard — must honor.
await onIncoming(mail)

The parsed shape (ParsedInboundMail) is identical across providers: from/to/cc/bcc, subject/text/html, RFC-5322 messageId / inReplyTo / references, decoded attachments as Buffer, and isAutoGenerated derived from Auto-Submitted / Precedence / X-Auto-Response-Suppress.

What's NOT here yet

  • Notifications + channel drivers (in @strav/notification).
  • Broadcast pub/sub + SSE handler.

Full reference: docs/mail/api.md.

FAQs

Package last updated on 01 Jun 2026

Related posts