Skip to content
Back to skills

Mailer

ASecurity

How to use @owlmeans/mailer — MailerService interface + console/dev transport. Use when sending emails or setting up a mailer in tests. Applies to files matching **/context.ts, **/services/mail*.

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 22, 2026
testingapi

Works with

  • api

Security analysis

A100/100

Scanned October 6, 2026

npx -y skills add owlmeans/common --skill mailer --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Mailer?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Mailer
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/owlmeans-mailer-common/badge)](https://www.skillsdirectory.com/skills/owlmeans-mailer-common)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: mailer
description: "How to use @owlmeans/mailer — MailerService interface + console/dev transport. Use when sending emails or setting up a mailer in tests. Applies to files matching **/context.ts, **/services/mail*."
metadata:
  applyTo: "**/context.ts, **/services/mail*"
---
<!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->

# Using `@owlmeans/mailer`

**Install:** `"@owlmeans/mailer": "^0.1.18-rc.36"` in `dependencies`

Provider-agnostic email dispatch service. It defines the `MailerService` interface and ships one
transport of its own — the console/dev one. Real delivery is a separate package:
`@owlmeans/mailer-smtp` (SMTP, works against any relay) or `@owlmeans/server-mailer-mailgun`
(Mailgun's HTTP API).

## Public API surface

| Symbol | Kind | Purpose |
|--------|------|---------|
| `MailerService` | interface | Extends `InitializedService` (`@owlmeans/context`) with `send(message): Promise<void>` |
| `MailMessage` | interface | `{ to, subject, text?, html?, from?, replyTo?, headers? }` |
| `MAILER_SERVICE` | const | Default service alias `'mailer-service'` |
| `makeConsoleMailerService(alias?)` | fn | Dev transport: logs to console and stores messages in `.captured[]` |
| `makeDefaultConsoleMailerService()` | fn | Alias of `makeConsoleMailerService(MAILER_SERVICE)` |
| `CONSOLE_MAILER` | const | Default alias for the console transport `'console-mailer'` |

## Console transport (dev & tests)

```ts
import { makeConsoleMailerService, MAILER_SERVICE } from '@owlmeans/mailer'

const mailer = makeConsoleMailerService(MAILER_SERVICE)
context.registerService(mailer)

// In tests: inspect captured messages
await mailer.send({ to: 'user@example.com', subject: 'Code', text: '123456' })
console.log(mailer.captured[0].text) // '123456'
```

The console transport:
- Writes each message through `@owlmeans/log` at `info`, scope `mailer`, event `mail.console` — visible at the default level (printing the mail IS its delivery; e2e runs read login codes from it) and silenced by `configureLog({ level: 'warn' })` in a noisy test. Never register it where real mail is sent.
- Accumulates all sent messages in `mailer.captured: MailMessage[]`.
- Never throws: it has no transport to fail, which is what makes it the right double in a test.

## Writing a transport

The package's whole purpose is a contract other packages implement. A transport is a service built
with `createService` from `@owlmeans/context` under the alias the caller asks for, whose one
required member is `send`:

```ts
import { createService } from '@owlmeans/context'
import type { MailerService, MailMessage } from '@owlmeans/mailer'

export const makeMyMailerService = (alias = 'my-mailer'): MailerService =>
  createService<MailerService>(alias, {
    send: async (message: MailMessage): Promise<void> => { /* reach the provider */ },
  })
```

`MailerService` extends `InitializedService`, so the context runs the service's lifecycle like any
other; a transport that has to be torn down declares its own members on top (see the SMTP one's
`verify()` / `close()`).

## Production wiring

Every transport registers under the same `MAILER_SERVICE` alias, so swapping one for another
changes no caller. Select on config rather than on a build flag, so one image serves every
environment:

```ts
// Dev / tests:
context.registerService(makeConsoleMailerService(MAILER_SERVICE))
// Production, SMTP relay:
context.registerService(makeSmtpMailerService(MAILER_SERVICE))
// Production, Mailgun HTTP API:
context.registerService(makeMailgunMailerService(MAILER_SERVICE))
```

## Rules

- Register under `MAILER_SERVICE` alias when called from `OtpService` or other platform code.
- Never import a concrete transport directly in domain services — always inject via the alias.
- The `MailMessage.html` field is optional; supply either `text` or `html` (or both).
- `from`, `replyTo` and `headers` are per-message overrides of whatever the transport was
  configured with. A transport that cannot carry headers ignores the field rather than failing —
  the contract is the smallest thing every transport can honour, so nothing here is guaranteed to
  reach the wire.

## Related

- [[mailer-smtp]] — the SMTP transport and its `cfg.smtp` block
- [[server-mailer-mailgun]] — the Mailgun HTTP transport and its `cfg.mailgun` block

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…