# SMTP relay

Point anything that sends over SMTP at Rasket — Supabase Auth, WordPress, a framework mailer — and every message becomes the same `POST /emails` an API client makes.

## Settings

| Setting | Value |
| --- | --- |
| Host | `smtp.rasket.com` |
| Port | `465` (implicit TLS) or `587` (STARTTLS) |
| Alternate ports | `2465` (implicit TLS) and `2587` (STARTTLS), for networks that block the usual two |
| Username | `rasket` |
| Password | An API key (`rk_…`) with full or sending access |

Use `2465` or `2587` only when a network or a hosting provider blocks the usual port; they behave the same way.

TLS is required: on port `465` the connection starts encrypted, and on `587` the login is offered only after STARTTLS. Port 25 is not offered. The login mechanisms are `PLAIN` and `LOGIN`.

Your team's settings, and a button to create the key, are in the dashboard under Settings → SMTP.

> The password is an API key, so treat the setting that holds it like one. Create a key with sending access, restricted to the domain the app sends from: a leaked SMTP password can then send from one domain and do nothing else.

## What stays the same

The relay is not a second way in. Each message is one request to the API, made with your key, so everything that applies to an API send applies here: the From address has to be on a [verified domain](https://www.rasket.com/docs/domains), a key restricted to one domain can send only from it, suppressed addresses are skipped, and the message counts against your quota and [rate limit](https://www.rasket.com/docs/rate-limits) exactly as an API call would. Delivery, bounce and complaint events reach your [webhooks](https://www.rasket.com/docs/events), and the Emails and Logs pages show the message with the source **SMTP**.

## Setup guides

Every guide below uses port `465` and reads the key from `RASKET_API_KEY`. The From address in each one has to be on a domain you have verified.

### Supabase Auth

In the Supabase dashboard, open Authentication, then the SMTP settings under Emails, and turn on custom SMTP. Set the sender email to an address on your verified domain, the host to `smtp.rasket.com`, the port to `465`, the username to `rasket` and the password to your key, then save. Sign-up confirmations, magic links and password resets now go through Rasket.

Supabase applies an hourly limit of its own to auth email. Raise it under Authentication → Rate Limits if your sign-ups need more. For a project run with the Supabase CLI, the same settings go in `supabase/config.toml`:

Supabase CLI configuration:

```text
[auth.email.smtp]
enabled = true
host = "smtp.rasket.com"
port = 465
user = "rasket"
pass = "env(RASKET_API_KEY)"
admin_email = "auth@send.acme.example"
sender_name = "Acme"
```

### Nodemailer

`secure: true` is what port `465` needs; on `587` set `secure: false` and Nodemailer upgrades with STARTTLS. The two `X-Rasket-` headers are optional: see [headers you can add](https://www.rasket.com/docs/smtp#headers).

Sending with Nodemailer:

```text
import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  host: "smtp.rasket.com",
  port: 465,
  secure: true,
  auth: { user: "rasket", pass: process.env.RASKET_API_KEY },
});

await transporter.sendMail({
  from: "Acme <billing@send.acme.example>",
  to: "customer@example.com",
  subject: "Your receipt",
  text: "Thanks for your order.",
  headers: {
    "X-Rasket-Idempotency-Key": "receipt-1042",
    "X-Rasket-Tags": "category=receipt",
  },
});
```

### WordPress

WordPress sends through PHPMailer. Any SMTP plugin will do: give it the settings above, with SSL as the encryption. Without a plugin, add this to a small plugin of your own or to the theme's `functions.php`. The two filters matter, because WordPress otherwise sends from `wordpress@` your site's domain, which is rarely the domain you verified.

WordPress, without a plugin:

```text
add_action('phpmailer_init', function ($mailer) {
    $mailer->isSMTP();
    $mailer->Host = 'smtp.rasket.com';
    $mailer->Port = 465;
    $mailer->SMTPSecure = 'ssl';
    $mailer->SMTPAuth = true;
    $mailer->Username = 'rasket';
    $mailer->Password = getenv('RASKET_API_KEY');
});

add_filter('wp_mail_from', fn () => 'site@send.acme.example');
add_filter('wp_mail_from_name', fn () => 'Acme');
```

### Laravel

In `.env`. `MAIL_SCHEME` is read by Laravel 11 and later; earlier versions take `MAIL_ENCRYPTION=ssl` instead.

Laravel mail settings:

```text
MAIL_MAILER=smtp
MAIL_SCHEME=smtps
MAIL_HOST=smtp.rasket.com
MAIL_PORT=465
MAIL_USERNAME=rasket
MAIL_PASSWORD="${RASKET_API_KEY}"
MAIL_FROM_ADDRESS="hello@send.acme.example"
MAIL_FROM_NAME="Acme"
```

### Rails Action Mailer

In `config/environments/production.rb`. `tls: true` is implicit TLS, which is what port `465` speaks. On `587`, drop it and set `enable_starttls_auto: true` instead.

Action Mailer settings:

```text
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address: "smtp.rasket.com",
  port: 465,
  tls: true,
  authentication: :plain,
  user_name: "rasket",
  password: ENV.fetch("RASKET_API_KEY")
}
```

### Django

In `settings.py`, with `os` imported. `EMAIL_USE_SSL` is the setting for port `465`. On `587` use `EMAIL_USE_TLS` instead; Django refuses both at once.

Django email settings:

```text
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.rasket.com"
EMAIL_PORT = 465
EMAIL_USE_SSL = True
EMAIL_HOST_USER = "rasket"
EMAIL_HOST_PASSWORD = os.environ["RASKET_API_KEY"]
DEFAULT_FROM_EMAIL = "Acme <hello@send.acme.example>"
```

### Moving from another provider

If an app sends over SMTP today, the move is three settings: the host, the username and the password. Add and verify your domain in Rasket first, and keep the old provider until the domain reads verified, because a message from an unverified domain is refused with `550 5.7.1`. A mailer that already sends an idempotency header for its old provider may keep sending it: one other provider's header is accepted verbatim, as is its SMTP username, and [the comparison pages](https://www.rasket.com/compare) say which.

## How a message becomes a send

One SMTP transaction is one `POST /emails`. The envelope decides who receives the message and the headers decide how it looks. `MAIL FROM` is ignored, because the return path is ours.

| Field | From the message |
| --- | --- |
| `from` | The `From:` header: exactly one address |
| `to` | Addresses in `To:` that are also envelope recipients (`RCPT TO`) |
| `cc` | Addresses in `Cc:` that are also envelope recipients |
| `bcc` | Every other envelope recipient. A `Bcc:` header is never forwarded |
| `reply_to` | `Reply-To:` |
| `subject` | `Subject:` |
| `html`, `text` | The first HTML part and the first plain-text part that are not attachments |
| `attachments` | Every other part. A part with a `Content-ID` becomes an inline attachment |
| `headers` | `In-Reply-To`, `References`, the priority headers and your own `X-` headers. Everything else a mailer adds is dropped, not refused |

`To:` and `Cc:` are rewritten to the addresses the envelope actually reaches, which is what a mailer that sends one copy per recipient expects.

## Headers you can add

- `X-Rasket-Tags: category=receipt, plan=pro` — becomes the email's tags, which come back on the email and on every webhook event for it. An invalid tag refuses the message with `550 5.6.0`.
- `X-Rasket-Idempotency-Key: receipt-1042` — becomes the [idempotency key](https://www.rasket.com/docs/idempotency), so a mailer that sends the same message twice sends it once.

Without a key header, a message with a `Message-ID` gets one derived from it and the envelope, so a mailer that times out after the final `.` and sends the whole message again gets the first answer back rather than a second email. Both headers are removed before the message goes out.

## Limits

| Limit | Value | Over it |
| --- | --- | --- |
| Message size | 4,000,000 bytes of raw message, attachments included (about 4 MB) | `552 5.3.4` |
| Recipients per message | 50, counting To, Cc and Bcc; 10 while a new account is on its starting limits | `452 4.5.3` for each recipient past the limit |
| Sending rate | 10 messages a second per team, shared with the API | `451 4.7.1` |
| Messages per connection | 100, then reconnect | `421 4.7.0` |
| Connections from one address | 10 at a time | `421 4.7.0` |
| Idle connection | 5 minutes between commands | `421 4.4.2` |
| Failed logins | 3 per connection; 10 from one address in 15 minutes locks that address out for 15 minutes | `421 4.7.0` |

The size is counted on the message as your mailer encodes it, and base64 makes an attachment about a third larger, so files add up to a little under 3 MB. That is lower than the API's limit: a larger message has to go through `POST /emails`, or carry its large files as links.

## Replies

A `4xx` reply means try again later, and your mailer's queue will. A `5xx` reply means something about the message or the account has to change first. The text after the code is the API's own error message, so a mailer's log says what the dashboard would.

| Reply | Meaning | What to do |
| --- | --- | --- |
| `250 2.0.0` | Accepted: `Queued as <email id>`. A suppressed recipient is still accepted and shows up as an `email.suppressed` event | Nothing |
| `421` | A connection limit, a lockout after failed logins, or a restart | Reconnect later |
| `451 4.7.1` | The rate limit, the daily or monthly quota, or the spend cap. The text says when to retry | Retry; your mailer's queue holds the message |
| `451 4.3.0`, `4.3.2`, `4.4.0`, `4.4.2` | A fault or a timeout on our side | Retry. The retry replays rather than sending twice |
| `452 4.5.3` | A recipient past the per-message limit | Send the rest in a new message |
| `452 4.3.1` | Too many messages in flight at once | Retry |
| `454 4.7.0` | The login could not be checked just now | Retry |
| `530 5.7.0` | `MAIL FROM` before logging in | Log in first |
| `535 5.7.8` | A wrong username, or a key that is malformed, revoked or suspended | Fix the credentials |
| `538 5.7.11` | A login attempted before TLS | Use port `465`, or turn on STARTTLS |
| `550 5.7.1` | The sending domain is not verified, the key is restricted to another domain or suspended, or the account may not send this message yet | Change the account or the From address |
| `550 5.6.0` | The message was refused: no recipient in To:, signed or encrypted MIME, a missing From: or subject, or an invalid tag | Change the message |
| `550 5.5.3` | Too many recipients, when the account's limit changed while the connection was open | Send fewer recipients per message |
| `552 5.3.4` | Over 4 MB | Send large files as a link, or use the API |
| `553 5.1.3` | A recipient address that could not be read | Fix the address |
| `554 5.6.0` | An idempotency key you already used, on a different message | Use a new key |

## What it does not do

- **It never sends a bounce message.** After `250`, what happened to a message is an event, a webhook and a row on the Emails page — never a message back to your return path.
- **Bcc-only mail is refused** with `550 5.6.0`. A message needs at least one recipient in `To:`; send Bcc-only mail as one message per recipient.
- `Message-ID` is replaced with our own, so a client that matches its sent folder on it will not match. `In-Reply-To` and `References` are kept, so replies still thread.
- Your own `List-Unsubscribe` headers are dropped, the same as over the API.
- Signed or encrypted messages (S/MIME, PGP) are refused, because rebuilding them would break the signature.
- A calendar invite arrives as an `.ics` attachment rather than an invitation.
- Scheduling, templates and topics are not available over SMTP. Use [the API](https://www.rasket.com/docs/api-reference/emails) for those.
- Addresses with non-ASCII characters before the @ are not accepted yet.
