Openship
Back to Mail

Send via code

Send mail from your application using any SMTP library. We show the Node.js / nodemailer flow because it's the most common - every other language uses the exact same fields.

Install nodemailer

terminal
npm install nodemailer
# or
pnpm add nodemailer
# or
bun add nodemailer

Minimal transporter

mailer.ts
import nodemailer from "nodemailer";

export const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST!,        // smtp host from the right rail
  port: Number(process.env.SMTP_PORT), // 587
  secure: false,                       // STARTTLS upgrades after connect
  requireTLS: true,                    // refuse if TLS upgrade is unavailable
  auth: {
    user: process.env.SMTP_USER!,      // your full email address
    pass: process.env.SMTP_PASS!,      // postmaster password - keep in .env
  },
});
Why port 587 + STARTTLS instead of 465 + secure: true? Both work. 587/STARTTLS is the modern submission standard and is what most clients pick. 465 (implicit TLS) is supported too - set port: 465, secure: true and remove requireTLS.

Send a message

send.ts
import { transporter } from "./mailer";

await transporter.sendMail({
  from: '"Your App" <[email protected]>',
  to: "[email protected]",
  subject: "Hello from your self-hosted mail server",
  text: "Plain-text body.",
  html: "<p>HTML body too - clients pick whichever they prefer.</p>",
});

Verify the connection first

On boot, ping the server so a broken config fails fast instead of waiting for the first sendMail to time out:

boot.ts
await transporter.verify();
console.log("SMTP ready");

From other languages

The shape is identical across libraries - host, port, TLS mode, username, password.

python · smtplib
import smtplib
from email.message import EmailMessage

msg = EmailMessage()
msg["From"] = "[email protected]"
msg["To"] = "[email protected]"
msg["Subject"] = "Hello"
msg.set_content("Plain text body")

with smtplib.SMTP("smtp.yourdomain.com", 587) as s:
    s.starttls()
    s.login("[email protected]", "PASSWORD")
    s.send_message(msg)
go · net/smtp
auth := smtp.PlainAuth("",
  "[email protected]",
  "PASSWORD",
  "smtp.yourdomain.com",
)
err := smtp.SendMail(
  "smtp.yourdomain.com:587",
  auth,
  "[email protected]",
  []string{"[email protected]"},
  []byte("Subject: Hello\r\n\r\nbody"),
)

Production checklist

  1. 1
    Never commit the password. Use environment variables loaded at runtime (Vercel/Railway/Fly all do this; locally use .env.local).
  2. 2
    Keep a single transporter instance for the lifetime of your process - opening a fresh TCP+TLS handshake per email kills throughput.
  3. 3
    Set the From address to a mailbox that exists on this server. Foreign From addresses are rejected by your SPF + DMARC policy.
  4. 4
    For transactional volume, run verify() at boot and add retries around sendMail; transient network blips are normal.
  5. 5
    Check the Health tab in your Openship admin if mail stops sending - the outbound queue lives there.