# SMTP relay

Point any app that already speaks SMTP at emails.sh: swap the host, the port, and the credentials.

Point any app that already speaks SMTP at emails.sh. Nothing in your code changes: you swap the host, the port, and the credentials in your mailer config, and the mail goes out through emails.sh with your verified domain, your delivery log, and your suppression list.

This is the fastest migration path off Postmark, SendGrid, Mailgun, SES, or an old Gmail relay. If your app is Laravel, Rails, Django, WordPress, or anything using PHPMailer or Nodemailer, this is four lines of config.

### Settings

| Setting | Value |
| --- | --- |
| Host | smtp.emails.sh |
| Port | 587 with STARTTLS, recommended. 465 with implicit TLS where it is available. |
| Username | emailssh, the literal string |
| Password | your API key, the string starting esh_ |
| Encryption | Required. TLS on 465, STARTTLS on 587. |
| Authentication | PLAIN or LOGIN |
| Max message size | 40 MB |
| Max recipients per transaction | 50 |
| Simultaneous connections | 10 per source address |

The username is always the literal string emailssh. It is not your email address and not your workspace name. Your API key is the password, so there is no second credential to create: make a key at https://emails.sh/dashboard/api-keys and paste it in.

Use port 587 unless something in your stack blocks it. Some hosts, a few PaaS providers and most residential ISPs, block 587 but allow 465. Port 465 only listens where TLS material is configured, so if a connection to it is refused, use 587.

The From address must be on a domain you have verified, exactly as with the API. While you are still testing, send from onboarding@emails.sh.

### Nodemailer

Node:
```ts
import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: 'smtp.emails.sh',
  port: 587,
  secure: false, // STARTTLS is negotiated on 587; true only for port 465
  auth: {
    user: 'emailssh',
    pass: process.env.EMAILSSH_API_KEY
  }
});

await transporter.sendMail({
  from: 'Acme <hello@acme.com>',
  to: 'someone@example.com',
  subject: 'Welcome to Acme',
  text: 'Thanks for signing up.',
  html: '<p>Thanks for signing up.</p>'
});
```

If you would rather not run SMTP at all from Node, npm install @emails.sh/sdk and call mail.send over HTTPS instead. SMTP exists here for the apps that cannot change. See /docs/node.

### Django

settings.py:
```py
import os

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.emails.sh'
EMAIL_PORT = 587
EMAIL_USE_TLS = True          # STARTTLS on 587
EMAIL_HOST_USER = 'emailssh'
EMAIL_HOST_PASSWORD = os.environ['EMAILSSH_API_KEY']
DEFAULT_FROM_EMAIL = 'Acme <hello@acme.com>'
```

For port 465 use EMAIL_USE_SSL = True with EMAIL_PORT = 465, and leave EMAIL_USE_TLS unset. Django refuses to start if both are true. Sending is then the ordinary Django call.

An ordinary send:
```py
from django.core.mail import send_mail

send_mail(
    subject='Welcome to Acme',
    message='Thanks for signing up.',
    from_email='Acme <hello@acme.com>',
    recipient_list=['someone@example.com'],
)
```

### Laravel

Laravel .env:
```text
# .env. The key comes from https://emails.sh/dashboard/api-keys.
MAIL_MAILER=smtp
MAIL_HOST=smtp.emails.sh
MAIL_PORT=587
MAIL_USERNAME=emailssh
MAIL_PASSWORD=esh_your_key_here
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=hello@acme.com
MAIL_FROM_NAME="Acme"
```

config/mail.php needs no change: those variables are what the shipped config already reads. For port 465, set MAIL_PORT=465.

A send:
```php
<?php

use Illuminate\Support\Facades\Mail;

Mail::raw('Thanks for signing up.', function ($message) {
    $message->to('someone@example.com')->subject('Welcome to Acme');
});
```

### Rails

ActionMailer:
```rb
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address:              'smtp.emails.sh',
  port:                 587,
  user_name:            'emailssh',
  password:             ENV['EMAILSSH_API_KEY'],
  authentication:       :plain,
  enable_starttls_auto: true
}
config.action_mailer.default_options = { from: 'Acme <hello@acme.com>' }
```

For port 465, use port: 465 and tls: true, and drop enable_starttls_auto.

### PHPMailer

PHPMailer:
```php
<?php

use PHPMailer\PHPMailer\PHPMailer;

require 'vendor/autoload.php';

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host       = 'smtp.emails.sh';
$mail->Port       = 587;
$mail->SMTPAuth   = true;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // ENCRYPTION_SMTPS for 465
$mail->Username   = 'emailssh';
$mail->Password   = getenv('EMAILSSH_API_KEY');

$mail->setFrom('hello@acme.com', 'Acme');
$mail->addAddress('someone@example.com');
$mail->Subject = 'Welcome to Acme';
$mail->isHTML(true);
$mail->Body    = '<p>Thanks for signing up.</p>';
$mail->AltBody = 'Thanks for signing up.';

$mail->send();
```

### WordPress

Install WP Mail SMTP, choose Other SMTP as the mailer, and fill in smtp.emails.sh, TLS, port 587, authentication on, username emailssh, and your key as the password. The From Email is an address on your verified domain.

Keep the key out of the database by putting it in wp-config.php instead, which the plugin reads in preference to its stored settings.

wp-config.php:
```php
<?php
// wp-config.php. The key comes from https://emails.sh/dashboard/api-keys.
define( 'WPMS_ON', true );
define( 'WPMS_MAILER', 'smtp' );
define( 'WPMS_SMTP_HOST', 'smtp.emails.sh' );
define( 'WPMS_SMTP_PORT', 587 );
define( 'WPMS_SSL', 'tls' );
define( 'WPMS_SMTP_AUTH', true );
define( 'WPMS_SMTP_USER', 'emailssh' );
define( 'WPMS_SMTP_PASS', 'esh_your_key_here' );
```

### Anything else

msmtp, ssmtp, Postfix relayhost, a printer, a CI job:
```text
host:       smtp.emails.sh
port:       587    (or 465)
security:   STARTTLS on 587, TLS on 465
auth:       PLAIN or LOGIN
username:   emailssh
password:   an esh_ key from https://emails.sh/dashboard/api-keys
```

A one-line check from a shell, which is what to run when a framework is failing and you want to know whether the credentials or the framework is the problem.

swaks:
```bash
swaks --server smtp.emails.sh:587 --tls \
  --auth-user emailssh --auth-password "$EMAILSSH_API_KEY" \
  --from hello@acme.com --to someone@example.com \
  --header 'Subject: SMTP test' --body 'It works.'
```

### What happens to your message

The relay parses your MIME message and sends it through the same POST /v1/emails endpoint the API and the SDKs use, so an SMTP send is not a second-class send. It gets the same domain verification, the same suppression list, the same delivery log, the same webhooks, and the same quota. The gateway holds no database and no sending logic of its own.

- **The envelope decides delivery**: Every address your client issues a RCPT TO for is delivered to, and the To and Cc headers only decide how each one is labelled. Bcc works as you expect: an address in the envelope and in no header is delivered to and shown to nobody.
- **The From header decides which domain must be verified**: The envelope MAIL FROM is only the bounce path.
- **Reply-To, Cc, Bcc, text and HTML parts, and attachments carry through**: They arrive as the equivalent fields on the API call.
- **The 250 reply carries the message id**: 250 2.0.0 Ok: queued as 3f9c1e07-42b8-4d65-9a10-7c53e8b2f491. It is a bare UUID with no prefix, and it is the id GET /v1/emails/:id takes, so your SMTP log lines are enough to look a message up later.

### Four things the relay does that will surprise you

These are consequences of translating MIME into a JSON API, and each one is a thing somebody has been caught by.

- **Inline attachments are dropped**: An attachment referenced from your HTML with cid: does not survive, and the image shows as broken. Host the image and reference it with an https URL. This is the one that most often turns a good-looking template into a broken one.
- **Only string-valued X- headers survive**: Custom X- headers pass through when their value is a string. Anything structured is dropped. X-Emailssh-* names are reserved and always dropped.
- **Recipients can become mutually visible**: If no envelope address matches any header, every recipient is promoted into the To header and they can all see each other. That happens when a client issues RCPT TO for addresses that appear in no To or Cc header at all. If you are sending to a list, send one transaction per person, or use POST /v1/emails/batch.
- **Ten connections per address**: A source address opening an eleventh simultaneous connection gets 421. A pool of workers that each hold a connection open needs to be smaller than ten, or to share.

### Reading the reply codes

Your mail library will surface these. What matters is that a 4xx means try again and a 5xx means the message will never be accepted as sent.

| Reply | What it means | What to do |
| --- | --- | --- |
| 250 2.0.0 | Accepted. The id is in the reply. | Nothing. |
| 421 4.7.0 | Too many connections from your address, or too many failed logins. Five auth failures in fifteen minutes locks you out. | Open fewer connections. Fix the credentials, then wait for the lockout to pass. |
| 451 4.3.0 | emails.sh could not be reached. | Retry. Your client will. |
| 451 4.7.1 | Rate limited, or the workspace is paused. | Retry. Slow down if it repeats. |
| 452 4.3.1 | Your quota, daily cap, or spend cap is used up. | Retry after it resets, or raise the plan. |
| 452 4.5.3 | More than 50 recipients in one transaction. | Split it, or use POST /v1/emails/batch for up to 100 in one call. |
| 454 4.7.0 | Credentials could not be checked right now. | Retry. |
| 535 5.7.8 | The username or the API key is wrong. | Username is emailssh. Password is an esh_ key that has not been revoked. |
| 550 5.1.1 | The recipient is suppressed after an earlier bounce or complaint. | Do not retry. See /docs/suppressions. |
| 550 5.1.3 | A recipient address is not valid. | Fix the address. |
| 550 5.6.0 | The message is missing a subject or a body, or a header is not allowed. | Fix the message. |
| 550 5.7.1 | The sending domain is not verified, or the recipient is blocked. | Verify the domain at https://emails.sh/dashboard/domains, or send from onboarding@emails.sh while testing. |
| 552 5.3.4 | The message is over 40 MB. | Link to the file instead of attaching it. |

### Limits

- **40 MB per message**: Including base64 attachments. The relay advertises SIZE, so a well-behaved client refuses before sending the bytes.
- **50 recipients per transaction**: Counting To, Cc, and Bcc. Past that you get 452 4.5.3, which is temporary: split the send.
- **Ten simultaneous connections per source address**: An eleventh gets 421.
- **Port 25 is not offered**: And never will be. There is no unauthenticated relay.

The API key is the password. There is no separate SMTP credential to create and none to revoke independently, so rotating an SMTP password means rotating the key, which is /docs/api-keys.

---

Base URL: https://emails.sh/v1. Auth: `Authorization: Bearer esh_...`.
Whole API in one file: https://emails.sh/llms.txt. All documentation: https://emails.sh/docs.md.
