SMTP
Send over SMTP with a standards-compliant MIME transport. No HTTP API.
Capabilities
| Repeated headers | Idempotency | Scheduling | Personalized |
|---|---|---|---|
| Yes | message_id | No | expanded |
These values come from the adapter's exported capabilities declaration. The field support matrix covers normalized message fields.
The SMTP adapter uses Nodemailer for the SMTP protocol, TLS, authentication, MIME composition, transfer encoding, RFC-safe subject encoding, and attachments. Point it at PurelyMail, a self-hosted server, or any provider's SMTP endpoint, and register it more than once under different names for multiple routes.
Configure
Get the host, port, TLS mode, and credentials from your SMTP provider.
import { createEmailClient } from "@opencoredev/email-sdk";
import { smtp } from "@opencoredev/email-sdk/smtp";
export const email = createEmailClient({
adapters: [
smtp({
host: "smtp.purelymail.com",
port: 587,
auth: {
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!,
},
}),
],
});Prop
Type
TLS and auth semantics
With secure: true the whole session is TLS. Otherwise the adapter connects in plaintext and upgrades with STARTTLS before authenticating whenever requireTLS is set or auth is present, so credentials do not travel unencrypted. Authenticating against a server that cannot do TLS requires an explicit allowInsecureAuth: true; without it the adapter throws instead of sending the password in the clear.
Send
SMTP maps cc, bcc, replyTo, headers, and attachments. Nodemailer builds the MIME tree, chooses safe transfer encodings, encodes non-ASCII subjects, and supports content or path attachments (including inline contentId attachments). When a message has both html and text, the adapter builds a multipart/alternative MIME body. The generated Message-ID header is <{idempotencyKey}@email-sdk.local> when send options include an idempotencyKey, and a random UUID otherwise. An explicit Message-ID in headers overrides it; set one yourself if you need a specific domain for threading or deduplication.
const result = await email.send({
from: "Acme <alerts@acme.com>",
to: "ops@example.com",
cc: "oncall@example.com",
subject: "Disk usage above 90% on db-1",
text: "db-1 is at 92% disk usage.",
html: "<p><strong>db-1</strong> is at 92% disk usage.</p>",
headers: [{ name: "X-Alert-ID", value: "alert_481" }],
});
console.log(result.accepted); // every recipient the server accepted
console.log(result.id); // parsed from the server's "queued as ..." reply, else the idempotency keyrejected is always empty, because a refused recipient fails the SMTP conversation instead. Retryable transport failures surface as EmailAdapterError, so SMTP plays well with retries and fallbacks.
No tags or metadata
SMTP has no tag or metadata concept. Those fields throw an EmailValidationError before
connecting. Attachments are supported and are encoded as MIME parts. See the
field support matrix.
Multiple SMTP routes
name lets you register the adapter twice and use one route as the fallback for the other.
export const email = createEmailClient({
adapters: [
smtp({
name: "purelymail",
host: "smtp.purelymail.com",
auth: { user: process.env.PURELYMAIL_USER!, pass: process.env.PURELYMAIL_PASS! },
}),
smtp({
name: "backup-smtp",
host: "smtp.backup.example",
auth: { user: process.env.BACKUP_SMTP_USER!, pass: process.env.BACKUP_SMTP_PASS! },
}),
],
defaultAdapter: "purelymail",
fallback: { adapters: ["backup-smtp"] },
});Verify from the CLI
doctor only checks SMTP_HOST; the other settings are optional.
SMTP_HOST=smtp.purelymail.com npx --package @opencoredev/email-sdk email-sdk doctor --adapter smtpnpx --package @opencoredev/email-sdk email-sdk send \
--adapter smtp \
--host smtp.purelymail.com \
--port 587 \
--require-tls \
--user hello@acme.com \
--pass "$SMTP_PASS" \
--from "Acme <hello@acme.com>" \
--to user@example.com \
--subject "SMTP smoke test" \
--text "It works" \
--dry-runEach flag falls back to its env var: --host/SMTP_HOST, --port/SMTP_PORT (default 587), --secure/SMTP_SECURE, --require-tls/SMTP_REQUIRE_TLS, --allow-insecure-auth/SMTP_ALLOW_INSECURE_AUTH, --user/SMTP_USER, --pass/SMTP_PASS. Drop --dry-run for one real send to prove the host, TLS mode, and credentials actually deliver.
Frequently asked questions
Does Email SDK support SMTP?
Yes. Email SDK ships a SMTP adapter imported from @opencoredev/email-sdk/smtp. You keep your SMTP account and credentials; the SDK adds message validation, typed errors, and no-network test adapters around the same send() call used for every other provider.
Which message fields does the SMTP adapter support?
SMTP supports CC recipients, BCC recipients, Reply-To address, Custom headers, and Attachments. It does not support Tags, Metadata, and Scheduled sending (sendAt); Email SDK rejects a message that uses those fields before any request is made.
Can SMTP schedule email for later with Email SDK?
No. SMTP has no provider-side scheduling, so a message with sendAt fails validation. Store the job in your own queue and send when it is due.
Does the SMTP adapter support idempotent sends?
Partially. The SMTP adapter derives a stable Message-ID from the idempotency key, but the receiving server decides whether to deduplicate.