# SMTP relay

Point existing mail code at smtp.emails.sh on port 587 or 465. Username emailssh, password your API key. Same log, same suppressions, same quota.

Some mail code is not worth rewriting. A Rails app with ActionMailer configured, a WordPress install, a monitoring tool with an SMTP box in its settings: all of those want a host, a port, and a credential, not an SDK. The gateway takes the message, parses the MIME, and hands it to exactly the same POST /v1/emails your API traffic goes through.

## The credential is one you already have

The username is the fixed string emailssh and the password is your esh_ API key. There is no second credential to mint, rotate, and forget about, and revoking the key revokes the SMTP access in the same moment. Encryption is not optional: port 587 with STARTTLS or port 465 with implicit TLS, AUTH is refused before the connection is secured, and port 25 is not offered at all.

## It is a translator, not a second mail server

The gateway queues nothing, signs nothing, and enforces nothing itself. It parses the message and calls the REST API, which means suppression, quota, idempotency, DKIM signing, and the delivery log are the same code as your API sends rather than a parallel implementation that drifts. A message sent over SMTP appears in the same dashboard, with the same id, answering the same GET /v1/emails/:id.

## Rejections are the SMTP codes a retrying client understands

A quota, spend, or rate limit comes back as a 4xx so your mailer retries later instead of dropping the message. A suppressed recipient is 550 5.1.1 and an unverified From domain is 550 5.7.1, both permanent, because retrying either would never work. If the verifier is unreachable it is 454 4.7.0, which is temporary, because refusing permanently on our outage would lose your mail.

## Bcc works the way it is supposed to

The envelope recipients from RCPT TO are authoritative and the headers only sort those recipients into to, cc, and bcc. A bcc recipient is not leaked into the headers of anybody else's copy, which is a bug plenty of relays have shipped.

## The limits, stated

40 MB of raw MIME per message, advertised as SIZE and enforced during DATA. 50 recipients per transaction. 10 concurrent connections per IP. Five authentication failures in fifteen minutes locks that source out for fifteen minutes. Sockets idle for sixty seconds are closed.

## The API

| Endpoint | What it does |
| --- | --- |
| `smtp.emails.sh:587` | STARTTLS. AUTH PLAIN or LOGIN, after the connection is secured. |
| `smtp.emails.sh:465` | Implicit TLS, for clients that want the connection encrypted from the first byte. |
| `Username` | emailssh, the same for every account. |
| `Password` | Your esh_ API key. Revoking it closes SMTP access too. |

## Rails, with nothing else changed

config/environments/production.rb. Read the key from the environment; do not commit it.

```
Rails.application.configure do
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    address:              "smtp.emails.sh",
    port:                 587,
    user_name:            "emailssh",
    password:             ENV.fetch("EMAILSSH_API_KEY"),
    authentication:       :plain,
    enable_starttls_auto: true
  }
end
```

## What this does not do

### No tags over SMTP

The gateway builds from, to, cc, bcc, subject, text, html, reply_to, headers, and attachments. There is no tags field, so the tag breakdown in analytics is only available to mail sent through the REST API.

### No stored templates over SMTP

A template is referenced by a field on the JSON request, and SMTP has no way to express it. Render it in your application and send the result.

### No scheduling, idempotency key, or topic

send_at, idempotency_key, and topic are request fields on POST /v1/emails and there is no header that carries them here. A duplicate submission over SMTP is a duplicate send.

### Custom headers must start with X-

Anything else is dropped, and X-Emailssh- prefixed headers are refused so a client cannot forge our own metadata.

### The separate SMTP credential in the dashboard does not work yet

The schema carries a revocable send-only SMTP credential and it is not wired up: it authenticates a workspace without yielding a token the gateway could send with, so it would issue a password that cannot send. The API-key-as-password path is what works and what is documented.

## Questions

### Which port should I use?

587 with STARTTLS unless your client only speaks implicit TLS, in which case 465. Port 25 is not offered, and most hosting providers block outbound 25 anyway.

### Does SMTP mail show up in the same log as API mail?

Yes, because it is the same send. The gateway calls POST /v1/emails, so a message sent over SMTP has an id, a delivery timeline, webhooks, and suppression behaviour identical to one sent from the SDK.

### Why does nodemailer with Gmail stop working when I deploy?

Vercel, Netlify, and Cloudflare block outbound connections on port 587 to arbitrary hosts, and Gmail app passwords are rate limited and not meant for application mail. If you are already on a serverless host, use the REST API rather than the relay; the relay is for code that cannot be changed.

### Does the relay have its own quota?

No. It draws on the same quota as the REST API, because it is the REST API. One allowance covers both.

### Can I still send attachments?

Yes, up to the 40 MB raw message limit. Inline parts referenced with cid: are kept as inline rather than listed as visible attachments.

## Related

- [Drop-in migration](https://emails.sh/features/drop-in-migration.md)
- [Deliverability](https://emails.sh/features/deliverability.md)
- [Analytics](https://emails.sh/features/analytics.md)

Docs: https://emails.sh/docs.md