@kurtbruns/kestrel 1.17.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dev.vars.example +24 -2
- package/CHANGELOG.md +73 -1
- package/bin/kestrel.mjs +1 -1
- package/cli/dev.mjs +3 -3
- package/cli/init.mjs +98 -4
- package/dist/public/dashboard/app.js +327 -327
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +6 -3
- package/dist/public/dashboard/styles.css +133 -21
- package/dist/worker/index.js +5940 -4434
- package/docs/README.md +5 -3
- package/docs/get-started/02-deploy.md +25 -21
- package/docs/get-started/03-access.md +20 -8
- package/docs/get-started/04-aws-account.md +68 -0
- package/docs/get-started/05-ses.md +326 -0
- package/docs/get-started/06-production-access.md +67 -0
- package/docs/get-started/{05-verify.md → 07-verify.md} +16 -26
- package/docs/get-started/{06-go-live.md → 08-go-live.md} +4 -2
- package/docs/guides/01-allow-claude.md +2 -2
- package/docs/guides/02-automate.md +1 -1
- package/docs/guides/03-notifications.md +12 -12
- package/docs/guides/07-upgrade.md +4 -4
- package/docs/guides/09-as-your-list-grows.md +8 -18
- package/docs/publishing/02-sending-rules.md +2 -2
- package/docs/publishing/03-name-your-publication.md +2 -0
- package/docs/publishing/04-write-a-post.md +1 -1
- package/docs/publishing/06-troubleshooting.md +37 -9
- package/docs/publishing/08-connect-claude.md +1 -1
- package/docs/publishing/09-share-your-subscribe-link.md +3 -1
- package/docs/publishing/10-check-your-setup.md +15 -9
- package/docs/publishing/README.md +8 -3
- package/docs/reference/01-configuration.md +18 -16
- package/docs/reference/02-sending-domain-dns.md +4 -4
- package/migrations/0015_provider_account.sql +18 -0
- package/migrations/0016_held_receipts.sql +26 -0
- package/migrations/0017_provider_account_rejected.sql +24 -0
- package/migrations/0018_notify_addresses.sql +13 -0
- package/migrations/0019_sending.sql +67 -0
- package/migrations/0020_notifications_not_sending.sql +40 -0
- package/package.json +1 -1
- package/packages/kestrel/lib/api.mjs +28 -3
- package/packages/kestrel/lib/oauth.mjs +5 -3
- package/template/claude/CLAUDE.md +5 -3
- package/template/wrangler.jsonc +6 -6
- package/docs/get-started/04-ses.md +0 -310
package/docs/README.md
CHANGED
|
@@ -8,9 +8,11 @@ These are the steps to deploy a Kestrel instance and get it ready to send. Each
|
|
|
8
8
|
|
|
9
9
|
1. [Deploy the app](get-started/02-deploy.md)
|
|
10
10
|
1. [Lock the dashboard with Access](get-started/03-access.md)
|
|
11
|
-
1. [
|
|
12
|
-
1. [
|
|
13
|
-
1. [
|
|
11
|
+
1. [Create an AWS account](get-started/04-aws-account.md)
|
|
12
|
+
1. [Connect Amazon SES](get-started/05-ses.md)
|
|
13
|
+
1. [Request production access](get-started/06-production-access.md)
|
|
14
|
+
1. [Verify it works](get-started/07-verify.md)
|
|
15
|
+
1. [Go live](get-started/08-go-live.md)
|
|
14
16
|
|
|
15
17
|
## Guides
|
|
16
18
|
|
|
@@ -9,7 +9,7 @@ In this guide, you choose your hostnames, create your instance's repository, cre
|
|
|
9
9
|
- A [Cloudflare account](https://dash.cloudflare.com/sign-up), with your domain's DNS on Cloudflare. Your app's hostname and its login both live there.
|
|
10
10
|
- [Node.js](https://nodejs.org/) 22 or later, and [Git](https://git-scm.com/).
|
|
11
11
|
- A GitHub account, or another Git host, for your instance's repository.
|
|
12
|
-
- An [AWS account](
|
|
12
|
+
- An AWS account for the newsletter, before [Connect Amazon SES](05-ses.md). [Create an AWS account](04-aws-account.md) walks through it. You can start it now. Signing up needs an email address that receives mail, and AWS can take a while to activate a new account.
|
|
13
13
|
|
|
14
14
|
## Choose your hostnames
|
|
15
15
|
|
|
@@ -30,9 +30,9 @@ Throughout these guides, replace the example names with your own, and any value
|
|
|
30
30
|
|
|
31
31
|
Your instance's repository holds its configuration, its database migrations, and the release of Kestrel it runs. Kestrel itself installs from npm as `@kurtbruns/kestrel`, so the repository stays small, and an upgrade changes the version it names.
|
|
32
32
|
|
|
33
|
-
1. On GitHub, [create a new repository](https://github.com/new) named `newsletter`. It can be private. Leave it empty, with no README, license, or `.gitignore`.
|
|
33
|
+
1. On GitHub, [create a new repository](https://github.com/new) named after your publication. These guides use `newsletter`. It can be private. Leave it empty, with no README, license, or `.gitignore`.
|
|
34
34
|
|
|
35
|
-
1. Clone it, and write a new instance into it. Replace `YOUR_USERNAME` with your GitHub username:
|
|
35
|
+
1. Clone it, and write a new instance into it. Replace `YOUR_USERNAME` with your GitHub username, and `newsletter` with your repository's name:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
git clone https://github.com/YOUR_USERNAME/newsletter.git
|
|
@@ -48,14 +48,6 @@ Your instance's repository holds its configuration, its database migrations, and
|
|
|
48
48
|
npm install
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
1. Commit the new instance, and push it:
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
git add -A
|
|
55
|
-
git commit -m "Start a Kestrel instance"
|
|
56
|
-
git push -u origin HEAD
|
|
57
|
-
```
|
|
58
|
-
|
|
59
51
|
1. **(Optional)** Try Kestrel locally before you deploy it. Copy the local settings, and start the app:
|
|
60
52
|
|
|
61
53
|
```bash
|
|
@@ -63,7 +55,15 @@ Your instance's repository holds its configuration, its database migrations, and
|
|
|
63
55
|
npm run dev
|
|
64
56
|
```
|
|
65
57
|
|
|
66
|
-
Open http://localhost:8787/dashboard/, and run `npm run seed` in a second terminal to load a demo publication. Email goes to a stand-in, so nothing reaches a real inbox. Press Ctrl+C to stop the server when you're done.
|
|
58
|
+
Open http://localhost:8787/dashboard/, and run `npm run seed` in a second terminal to load a demo publication. Email goes to a stand-in, so nothing reaches a real inbox. Press Ctrl+C to stop the server when you're done. `init` prints these steps too, in this order.
|
|
59
|
+
|
|
60
|
+
1. Commit the new instance, and push it. Your local settings and data stay out of the commit, since the instance's `.gitignore` lists them:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
git add -A
|
|
64
|
+
git commit -m "Initialize Kestrel instance"
|
|
65
|
+
git push -u origin HEAD
|
|
66
|
+
```
|
|
67
67
|
|
|
68
68
|
## 2. Sign in to Cloudflare
|
|
69
69
|
|
|
@@ -87,16 +87,18 @@ Every command from here on acts on your Cloudflare account through the Wrangler
|
|
|
87
87
|
|
|
88
88
|
Each command creates the resource and writes it into the `production` block of `wrangler.jsonc`, under the binding the app already uses. Name `--env production` on each one, or Wrangler writes to the top level, which is local development.
|
|
89
89
|
|
|
90
|
+
The commands below use `newsletter`. Use your instance's names instead: `kestrel init` printed them, and they're in the `production` block of `wrangler.jsonc`.
|
|
91
|
+
|
|
90
92
|
1. Create the database:
|
|
91
93
|
|
|
92
94
|
```bash
|
|
93
|
-
npx wrangler d1 create
|
|
95
|
+
npx wrangler d1 create newsletter-production --env production --binding DB --update-config
|
|
94
96
|
```
|
|
95
97
|
|
|
96
98
|
1. Create the bucket for images:
|
|
97
99
|
|
|
98
100
|
```bash
|
|
99
|
-
npx wrangler r2 bucket create
|
|
101
|
+
npx wrangler r2 bucket create newsletter-media-production --env production --binding MEDIA --update-config
|
|
100
102
|
```
|
|
101
103
|
|
|
102
104
|
Each prints a snippet to add to your configuration. There's nothing to paste: `--update-config` has filled in the database's id, and the `MEDIA` binding already names the bucket. Wrangler also rewrites `wrangler.jsonc` in its own layout, with tabs and one value per line, and your settings stay.
|
|
@@ -118,7 +120,7 @@ Each prints a snippet to add to your configuration. There's nothing to paste: `-
|
|
|
118
120
|
"d1_databases": [
|
|
119
121
|
{
|
|
120
122
|
"binding": "DB",
|
|
121
|
-
"database_name": "
|
|
123
|
+
"database_name": "newsletter-production", // named by init; leave it
|
|
122
124
|
"database_id": "…", // already filled in by section 3; leave it
|
|
123
125
|
"migrations_dir": "migrations"
|
|
124
126
|
}
|
|
@@ -136,9 +138,9 @@ Each prints a snippet to add to your configuration. There's nothing to paste: `-
|
|
|
136
138
|
What each one does:
|
|
137
139
|
|
|
138
140
|
- `routes` puts the app on your hostname when you deploy. Cloudflare creates the DNS record and the certificate. Your domain must be in the same Cloudflare account, and the hostname can't already have a CNAME record; delete one if it does. The block also turns off the `workers.dev` addresses, so the app answers only on your hostname.
|
|
139
|
-
- `PROVIDER` stays empty until [Connect Amazon SES](
|
|
141
|
+
- `PROVIDER` stays empty until [Connect Amazon SES](05-ses.md) sets it. Until then the app runs without an email provider: posts can be written and previewed, but scheduling, test emails, and signups are refused. Never set it to `fake`, the top level's value, even when a wrangler warning suggests copying it: `fake` delivers nothing, and the app refuses it on your hostname.
|
|
140
142
|
- `ARCHIVE_BASE_PATH` is permanent once the first post goes out. Every post sent carries its `/archive/…` link, and changing the prefix later breaks the links already mailed. Keep `/archive` unless you have a reason not to.
|
|
141
|
-
- `FROM_ADDRESS` is the address every email comes from. Keep it on your sending hostname, and set `SENDING_DOMAIN` to the part after the `@`.
|
|
143
|
+
- `FROM_ADDRESS` is the address every email comes from. Keep it on your sending hostname, and set `SENDING_DOMAIN` to the part after the `@`. The name before the `<` is what readers see as the sender. It also names the publication on its public pages, until the publisher sets a name under **Settings → Publication identity**. You can change the name at any time, and deploy again.
|
|
142
144
|
|
|
143
145
|
Change every `example.com` value, the route included. The app refuses to run while any of its settings still names `example.com`.
|
|
144
146
|
|
|
@@ -175,15 +177,15 @@ npx wrangler deploy --env production
|
|
|
175
177
|
|
|
176
178
|
This deploys the release your instance runs, with your settings. The output ends with the hostname the app now answers on. Deploying also registers the once-a-minute schedule that sends posts, which you can see under the Worker's **Triggers** tab in the Cloudflare dashboard.
|
|
177
179
|
|
|
178
|
-
Always name `--env production`, here and on every wrangler command that stores a secret. Without it, wrangler deploys the top level, local development, as a second Worker
|
|
180
|
+
Always name `--env production`, here and on every wrangler command that stores a secret. Without it, wrangler deploys the top level, local development, as a second Worker that answers nowhere and never sends. It takes the top level's `name` in `wrangler.jsonc`, such as `newsletter-dev`. If that happens, remove it by that name:
|
|
179
181
|
|
|
180
182
|
```bash
|
|
181
|
-
npx wrangler delete --name
|
|
183
|
+
npx wrangler delete --name newsletter-dev
|
|
182
184
|
```
|
|
183
185
|
|
|
184
186
|
## Check it
|
|
185
187
|
|
|
186
|
-
1. The app answers on your hostname
|
|
188
|
+
1. The app answers on your hostname:
|
|
187
189
|
|
|
188
190
|
```bash
|
|
189
191
|
curl https://newsletter.example.com/health
|
|
@@ -193,6 +195,8 @@ npx wrangler delete --name kestrel
|
|
|
193
195
|
{"status":"ok","service":"kestrel"}
|
|
194
196
|
```
|
|
195
197
|
|
|
198
|
+
If `curl` reports a certificate error, or says it could not resolve the host, Cloudflare is still setting up the new hostname. Try again in a few minutes.
|
|
199
|
+
|
|
196
200
|
1. The editor is locked. Until you set up Access in the next step, the app can't tell who is asking, so it lets no one in:
|
|
197
201
|
|
|
198
202
|
```bash
|
|
@@ -205,7 +209,7 @@ npx wrangler delete --name kestrel
|
|
|
205
209
|
|
|
206
210
|
In a browser, `https://newsletter.example.com/dashboard/` says the editor is locked until you set up Access. That's expected.
|
|
207
211
|
|
|
208
|
-
1. In a browser, `https://newsletter.example.com/` shows the newsletter's public home page. Its subscribe page, `/subscribe`, says signups aren't open yet, until Connect Amazon SES.
|
|
212
|
+
1. In a browser, `https://newsletter.example.com/` shows the newsletter's public home page. Its subscribe page, `/subscribe`, says signups aren't open yet, until Connect Amazon SES has sent its first test email.
|
|
209
213
|
|
|
210
214
|
If the public pages instead say the newsletter is temporarily unavailable, and `/health` answers `500` with a body like `{"error":"invalid_config","variable":"APP_ORIGIN",…}`, the setting it names is missing, malformed, or still on `example.com`. Fix it in `wrangler.jsonc`, commit, and deploy again.
|
|
211
215
|
|
|
@@ -37,7 +37,11 @@ The editor lives under `/dashboard`, and the API under `/api`. One application c
|
|
|
37
37
|
|
|
38
38
|
Each path covers everything under it. Leave every other path out. The home page, the archive, subscribing, unsubscribing, images, and your email provider's webhooks must stay public.
|
|
39
39
|
|
|
40
|
-
1.
|
|
40
|
+
1. Add only these two public hostnames. If the form also shows an empty private hostname or IP row, remove it, or saving fails with an error naming `use_clientless_isolation_app_launcher_url`.
|
|
41
|
+
|
|
42
|
+
1. Name the application after your app's hostname, `newsletter.example.com`. Cloudflare suggests a name made from the subdomain, which won't tell two newsletters apart.
|
|
43
|
+
|
|
44
|
+
1. **(Optional)** Choose how long a sign-in lasts. The application's session duration defaults to 24 hours, so everyone signs in again each day. You can set anything from immediately to one month, as Cloudflare's [session management](https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/session-management/) describes. For example, a week may suit a publisher on their own computer.
|
|
41
45
|
|
|
42
46
|
## 3. Allow yourself in
|
|
43
47
|
|
|
@@ -45,13 +49,13 @@ Every Access application denies everyone until a policy allows them.
|
|
|
45
49
|
|
|
46
50
|
1. Under **Access policies**, create a new policy:
|
|
47
51
|
|
|
48
|
-
- **Policy name:** `
|
|
52
|
+
- **Policy name:** the publication's name and `publishers`, such as `Example publishers`
|
|
49
53
|
- **Action:** Allow
|
|
50
54
|
- **Include:** the **Emails** selector, with your email address
|
|
51
55
|
|
|
52
56
|
1. Select **Create**.
|
|
53
57
|
|
|
54
|
-
Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in
|
|
58
|
+
Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in the policy you made should be one. To let someone sign in another way, such as a one-time code by email, add it under **Zero Trust → Integrations → Identity providers**.
|
|
55
59
|
|
|
56
60
|
## 4. Give the app its two values
|
|
57
61
|
|
|
@@ -75,13 +79,21 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
75
79
|
|
|
76
80
|
A secret takes effect as soon as you store it, with no redeploy.
|
|
77
81
|
|
|
78
|
-
1. **(Optional, recommended)** Limit the editor to named people. The Access policy decides who can sign in. This list keeps that true if the policy is widened later, for another app or by mistake.
|
|
82
|
+
1. **(Optional, recommended)** Limit the editor to named people. The Access policy decides who can sign in. This list keeps that true if the policy is widened later, for another app or by mistake. Store the list of addresses allowed in:
|
|
79
83
|
|
|
80
84
|
```bash
|
|
81
85
|
npx wrangler secret put ACCESS_ALLOWED_EMAILS --env production
|
|
82
86
|
```
|
|
83
87
|
|
|
84
|
-
|
|
88
|
+
At the prompt, enter the addresses separated by commas:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
you@example.com, publisher@example.com
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Spaces and capital letters don't matter. Without the list, the app lets in anyone your Access policy allows.
|
|
95
|
+
|
|
96
|
+
To let someone else into the editor, such as the publisher or a co-editor, add their email address to the policy from [Allow yourself in](#3-allow-yourself-in). If you set `ACCESS_ALLOWED_EMAILS`, run the command again and enter the whole list with their address added, since the new value replaces the old one.
|
|
85
97
|
|
|
86
98
|
## Check it
|
|
87
99
|
|
|
@@ -108,7 +120,7 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
108
120
|
If a check fails:
|
|
109
121
|
|
|
110
122
|
- **The editor says the configuration needs a fix.** Store the secret it names. While only one of the two is stored, `/health` answers `500` naming the other, and the public pages say the newsletter is temporarily unavailable.
|
|
111
|
-
- **Cloudflare lets you in, but the editor answers `401`.** Check the two secrets for a typo, and that `ACCESS_ALLOWED_EMAILS`, if you set it, includes your address.
|
|
112
|
-
- **The editor says Access let you in but didn't say who you are.** Your login carried no email, which happens through a kind of policy the app doesn't accept, such as a device check. Sign out, and sign in through the
|
|
123
|
+
- **Cloudflare lets you in, but the editor answers `401`.** Check the two secrets for a typo, and that `ACCESS_ALLOWED_EMAILS`, if you set it, includes your address. The same goes for anyone else you let in.
|
|
124
|
+
- **The editor says Access let you in but didn't say who you are.** Your login carried no email, which happens through a kind of policy the app doesn't accept, such as a device check. Sign out, and sign in through the policy you made above.
|
|
113
125
|
|
|
114
|
-
Next, [
|
|
126
|
+
Next, [create an AWS account](04-aws-account.md).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Create an AWS account
|
|
2
|
+
|
|
3
|
+
Kestrel sends the newsletter's email through Amazon SES, which runs in an Amazon Web Services (AWS) account. This page assumes a brand-new account, made for the newsletter. If you'd rather use one you already have, skip to [Connect Amazon SES](05-ses.md).
|
|
4
|
+
|
|
5
|
+
If you plan to run multiple publications, you may want to consider setting up one account for each publication. With this setup, you can use [AWS Organizations](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html) to keep the accounts under one bill. Multiple publications can be run through one account. However, they share its sandbox, sending quotas, and suppression list, and AWS can pause sending for the whole account if one publication's bounces or complaints run high.
|
|
6
|
+
|
|
7
|
+
In this guide, you choose the account's email address, sign up, and secure the account's root user. Then you set a monthly budget, so AWS emails you if the account ever costs more than you expect.
|
|
8
|
+
|
|
9
|
+
## Before you begin
|
|
10
|
+
|
|
11
|
+
- A phone number, and a credit or debit card for AWS's charges.
|
|
12
|
+
- Your domain's DNS on Cloudflare, if the account's email address will be forwarded with Cloudflare Email Routing.
|
|
13
|
+
|
|
14
|
+
## 1. Choose the account's email address
|
|
15
|
+
|
|
16
|
+
The address you sign up with belongs to the account's root user, the identity that owns the account and everything in it. You sign in as the root user with it, AWS contacts you there about the account, and it's how you recover the account. AWS recommends an address used for nothing else, which forwards to whoever looks after the account, as its [root user best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/root-user-best-practices.html) describe.
|
|
17
|
+
|
|
18
|
+
1. Choose an address named for its purpose, on your own domain, such as `aws@example.com`. Use one you'll keep, since recovering the account depends on it.
|
|
19
|
+
|
|
20
|
+
1. Make sure it receives mail before you sign up, since AWS emails it a code right away. If `example.com` receives no mail yet, forward the address to your inbox with Cloudflare Email Routing, as [Enable Email Routing](https://developers.cloudflare.com/email-routing/get-started/enable-email-routing/) describes. If `example.com` already receives mail at a mail host, create the address there instead, since Email Routing replaces a domain's mail servers.
|
|
21
|
+
|
|
22
|
+
## 2. Sign up
|
|
23
|
+
|
|
24
|
+
AWS may offer two ways to sign up. Choose **Sign up for AWS (advanced)**. The other way, **Sign up for AWS (new)**, makes an account with no root user, and chooses its region from your address. Its console differs from the one this guide describes. AWS's [Compare sign-up options](https://docs.aws.amazon.com/accounts/latest/reference/sign-up-for-aws.html) explains both.
|
|
25
|
+
|
|
26
|
+
1. Open [Sign up for AWS](https://signin.aws.amazon.com/signup?request_type=register). If it offers a choice, select **Sign up for AWS (advanced)**.
|
|
27
|
+
|
|
28
|
+
1. Enter the address from section 1 as the root user's email address, and an account name, such as your publication's name. Verify the address with the code AWS sends to it, and choose a password for the root user.
|
|
29
|
+
|
|
30
|
+
1. Choose the paid plan. The free plan closes the account automatically after six months, or sooner once its credits are used, unless you upgrade it before then. The paid plan gets the same sign-up credits, and keeps running once they're used. [AWS Free Tier plans](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/free-tier-plans.html) describes both.
|
|
31
|
+
|
|
32
|
+
1. Enter your contact information and payment details, and confirm your phone number if AWS asks.
|
|
33
|
+
|
|
34
|
+
1. Choose the free Basic support plan. It's enough for everything the setup guide does. Asking AWS to let SES send to anyone is a form in the SES console, with no paid support plan needed.
|
|
35
|
+
|
|
36
|
+
1. Complete the sign-up. The console opens, signed in as the root user, and AWS emails the account's address when the account is ready.
|
|
37
|
+
|
|
38
|
+
## 3. Secure the root user
|
|
39
|
+
|
|
40
|
+
The root user can do anything in the account, including close it, so it needs more than a password. AWS requires multi-factor authentication (MFA) for it, and asks you to set it up soon after your first sign-in.
|
|
41
|
+
|
|
42
|
+
1. In the console, select your account name at the top right, and then **Security credentials**.
|
|
43
|
+
|
|
44
|
+
1. Under **Multi-factor authentication (MFA)**, choose **Assign MFA device**. Enter a device name, select **Passkey or Security Key**, and follow the prompts. A passkey on your computer or phone works.
|
|
45
|
+
|
|
46
|
+
1. Leave **Access keys** empty. A root user's key could do anything the root user can. [Connect Amazon SES](05-ses.md#5-create-the-apps-aws-key) makes a separate key that can only send email and read the account's sending status.
|
|
47
|
+
|
|
48
|
+
## 4. Set a budget alert
|
|
49
|
+
|
|
50
|
+
AWS charges for what you use, with no spending limit, so a mistake or a leaked key could run up a bill before you notice. A budget emails you as the month's costs near an amount you choose. SES charges for each email it sends, as [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists.
|
|
51
|
+
|
|
52
|
+
1. In the console, open **Billing and Cost Management**, then **Budgets**, and choose **Create budget**.
|
|
53
|
+
|
|
54
|
+
1. Under **Use a template (simplified)**, select **Monthly cost budget**.
|
|
55
|
+
|
|
56
|
+
1. Enter a name, and an amount a little above what you expect to spend each month. Add the account's email address, or your own, as an email recipient. Choose **Create budget**.
|
|
57
|
+
|
|
58
|
+
1. AWS emails each recipient to verify it, and sends alerts only to a verified address. Open that email while you're signed in to the account, and verify the address within 12 hours, as [Budgets email recipients](https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-email-recipients.html) describes.
|
|
59
|
+
|
|
60
|
+
## Check it
|
|
61
|
+
|
|
62
|
+
1. Sign out, and sign in again as the root user, with the address from section 1. After the password, AWS asks for your passkey or security key.
|
|
63
|
+
|
|
64
|
+
1. Under **Security credentials**, the MFA list shows your device, and **Access keys** lists none.
|
|
65
|
+
|
|
66
|
+
1. Under **Budgets**, your budget is listed, and its email recipient shows as **Active**.
|
|
67
|
+
|
|
68
|
+
Next, [connect Amazon SES](05-ses.md).
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# Connect Amazon SES
|
|
2
|
+
|
|
3
|
+
This page connects the app to Amazon Simple Email Service (Amazon SES), which delivers the newsletter's email. A new SES account starts in a sandbox, where it can send only to addresses you've verified, until AWS reviews it. See [Request production access](06-production-access.md) to take it out of the sandbox.
|
|
4
|
+
|
|
5
|
+
In this guide, you set up how the app sends mail through SES and receives bounces and complaints, and how readers' replies reach the publisher.
|
|
6
|
+
|
|
7
|
+
## Before you begin
|
|
8
|
+
|
|
9
|
+
- An AWS account for the newsletter, from [Create an AWS account](04-aws-account.md).
|
|
10
|
+
- The publisher's inbox, for readers' replies.
|
|
11
|
+
|
|
12
|
+
## 1. Verify your sending hostname
|
|
13
|
+
|
|
14
|
+
SES sends only from an identity you've verified. Verifying the whole hostname lets any address on it send, and sets up DKIM, which lets inboxes trust your mail.
|
|
15
|
+
|
|
16
|
+
The same form sets a custom MAIL FROM domain, so SPF also passes for your domain. [SPF: authorize the return path](../reference/02-sending-domain-dns.md#spf-authorize-the-return-path) explains why it matters.
|
|
17
|
+
|
|
18
|
+
1. Open the [SES console](https://console.aws.amazon.com/ses/), and choose a region near you in the region menu at the top right.
|
|
19
|
+
|
|
20
|
+
SES keeps identities, quotas, and the sandbox separately in each region, so do every step on this page in the region you chose. [Configuration](../reference/01-configuration.md#vars) says more about choosing one, under `AWS_REGION`.
|
|
21
|
+
|
|
22
|
+
1. Note the region's code, such as `us-west-2`. The menu shows it beside the region's name, and [Amazon SES endpoints](https://docs.aws.amazon.com/general/latest/gr/ses.html) lists each one too. AWS names a region in more than one way, such as United States (Oregon) and US West (Oregon), so go by the code. Later steps use it.
|
|
23
|
+
|
|
24
|
+
1. Under **Configuration**, choose **Identities**, then **Create identity**.
|
|
25
|
+
|
|
26
|
+
1. Select **Domain**, and enter `send.example.com`. Leave **Assign a default configuration set** and **Assign to a tenant** unchecked. Section 4 makes the configuration set, and makes it the default then.
|
|
27
|
+
|
|
28
|
+
1. Select **Use a custom MAIL FROM domain**. In **MAIL FROM domain**, enter `bounce`; the form shows `.send.example.com` after it. For **Behavior on MX failure**, keep **Use default MAIL FROM domain**.
|
|
29
|
+
|
|
30
|
+
1. Expand **Advanced DKIM settings**, and select **Easy DKIM**. Keep **RSA_2048_BIT** as the key length. Choose **Create identity**.
|
|
31
|
+
|
|
32
|
+
1. The identity opens on its **Authentication** tab, which lists five DNS records to publish. Under DKIM, three CNAME records. Under **Custom MAIL FROM domain**, expand **Publish DNS records** for an MX record and a TXT record.
|
|
33
|
+
|
|
34
|
+
1. Add all five to your domain's DNS records in Cloudflare, with their proxy status set to **DNS only**. SES can't read a proxied record, so check each one's status. You can import them in one go, or add them by hand:
|
|
35
|
+
|
|
36
|
+
- **Import them.** Copy this into a text file. Replace each `REPLACE_WITH_NAME_…` and `REPLACE_WITH_VALUE_…` with a DKIM record's name and value, exactly as SES lists them, and keep the dot after each. Replace `us-east-1` with your region's code. In the Cloudflare dashboard, open your domain's **DNS → Records**, select **Import and Export**, and import the file with **Proxy imported DNS records** unchecked. SES's own **Download .csv record set** is in a format Cloudflare doesn't import.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
REPLACE_WITH_NAME_1. 3600 IN CNAME REPLACE_WITH_VALUE_1.
|
|
40
|
+
REPLACE_WITH_NAME_2. 3600 IN CNAME REPLACE_WITH_VALUE_2.
|
|
41
|
+
REPLACE_WITH_NAME_3. 3600 IN CNAME REPLACE_WITH_VALUE_3.
|
|
42
|
+
bounce.send.example.com. 3600 IN MX 10 feedback-smtp.us-east-1.amazonses.com.
|
|
43
|
+
bounce.send.example.com. 3600 IN TXT "v=spf1 include:amazonses.com ~all"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **Add them by hand.** Add each record exactly as SES lists it. Cloudflare adds your domain to the name, so each DKIM name ends in `._domainkey.send`, and the MAIL FROM records are named `bounce.send`.
|
|
47
|
+
|
|
48
|
+
1. Wait until SES shows **Identity status** as **Verified**, and both **DKIM configuration** and **MAIL FROM configuration** as **Successful**.
|
|
49
|
+
|
|
50
|
+
## 2. Add a DMARC record
|
|
51
|
+
|
|
52
|
+
DMARC tells inboxes what to do with mail that fails its checks, and sends you reports about it. SES doesn't add this record, so you add it yourself. It starts in monitor mode, which reports without blocking anything.
|
|
53
|
+
|
|
54
|
+
1. In the Cloudflare dashboard, open your domain's DNS records, and add a record:
|
|
55
|
+
|
|
56
|
+
- **Type:** `TXT`
|
|
57
|
+
- **Name:** `_dmarc.send`
|
|
58
|
+
- **Content:** `v=DMARC1; p=none; rua=mailto:dmarc@example.com`
|
|
59
|
+
|
|
60
|
+
1. Replace `dmarc@example.com` with an address on your own domain that receives mail, such as one forwarded to your inbox with Cloudflare Email Routing. Reports arrive there as attachments. An address on another domain, such as a Gmail address, gets no reports unless that domain publishes a record allowing them, which Gmail doesn't.
|
|
61
|
+
|
|
62
|
+
1. Check that the record is published. This prints it:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
dig +short TXT _dmarc.send.example.com
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
[Sending-domain DNS](../reference/02-sending-domain-dns.md#dmarc-publish-a-policy-and-collect-reports) explains each record, and when to tighten DMARC.
|
|
69
|
+
|
|
70
|
+
## 3. Verify your test addresses
|
|
71
|
+
|
|
72
|
+
A new SES account starts in the sandbox: it sends only to addresses you've verified, up to 200 emails a day and one a second. The test emails in [Verify it works](07-verify.md) go to two addresses of your own, so verify those now. [Request production access](06-production-access.md) lifts the limit, once AWS grants it.
|
|
73
|
+
|
|
74
|
+
1. Choose two addresses you own, such as a personal address and a work address. Verify it works uses both.
|
|
75
|
+
|
|
76
|
+
1. For each, under **Identities**, choose **Create identity**, select **Email address**, and enter it. Leave **Assign a default configuration set** and **Assign to a tenant** unchecked. The form's notice about DMARC is about sending from the address, and the app only sends to it. Choose **Create identity**.
|
|
77
|
+
|
|
78
|
+
1. Open the email AWS sends to each address, and follow its link. The identity's status changes to **Verified**.
|
|
79
|
+
|
|
80
|
+
AWS's email and confirmation page say you can now send from the address. That's expected: SES has one kind of verified address, and the same verification lets the sandbox send to it.
|
|
81
|
+
|
|
82
|
+
## 4. Send bounces and complaints to the app
|
|
83
|
+
|
|
84
|
+
SES reports bounces and complaints through Amazon Simple Notification Service (SNS), which delivers them to the app. The app then stops mailing an address that bounced or complained. A configuration set says which events SES reports, and an SNS topic carries them.
|
|
85
|
+
|
|
86
|
+
1. In SES, under **Configuration**, choose **Configuration sets**, then **Create set**. Name it `kestrel-mail`, keep every other option as it is, and choose **Create set**.
|
|
87
|
+
|
|
88
|
+
1. On the set's **Event destinations** tab, choose **Add destination**. Select **Hard bounces**, **Complaints**, **Deliveries**, and **Rendering failures**, and choose **Next**. The app logs a rendering failure, an email SES couldn't build, as an error.
|
|
89
|
+
|
|
90
|
+
Leave the other six off, since the app already knows about them or doesn't act on them. **Sends** repeats what the send records, and **Delivery delays** end as a bounce or a delivery. **Rejects** (a virus) and **Subscriptions** (SES's own unsubscribe) don't arise, since the app's emails carry no attachments and their own unsubscribe link. **Opens** and **Clicks** would add tracking the app doesn't record.
|
|
91
|
+
|
|
92
|
+
1. For **Destination type**, select **Amazon SNS**. Name it `kestrel-sns`, and keep **Event publishing** enabled.
|
|
93
|
+
|
|
94
|
+
1. On the same screen, under **SNS topic**, choose **Create SNS topic**. Name it `kestrel-events`, leave the display name empty, and choose **Create topic**. Then select `kestrel-events` in the topic menu, if it isn't already. Choose **Next**, then **Add destination**.
|
|
95
|
+
|
|
96
|
+
1. Make the set your hostname's default, so SES reports on every email sent from it. Under **Identities**, open `send.example.com`, and choose its **Configuration set** tab, below the summary. Edit the **Default configuration set**, select **Assign a default configuration set**, select `kestrel-mail`, and save.
|
|
97
|
+
|
|
98
|
+
1. Open the topic in the [SNS console](https://console.aws.amazon.com/sns/), in the same region, and find its ARN. It looks like `arn:aws:sns:us-east-1:111122223333:kestrel-events`, where `us-east-1` is your region's code and `111122223333` is your account ID.
|
|
99
|
+
|
|
100
|
+
1. Check that SES may publish to the topic, on its **Access policy** tab. A topic created from SES's **Add destination**, as above, already lets SES publish to it from your account. Leave that statement as it is. For a topic you made in the SNS console instead, choose **Edit**, expand **Access policy**, and add this statement to the `Statement` list:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"Effect": "Allow",
|
|
105
|
+
"Principal": { "Service": "ses.amazonaws.com" },
|
|
106
|
+
"Action": "sns:Publish",
|
|
107
|
+
"Resource": "REPLACE_WITH_TOPIC_ARN",
|
|
108
|
+
"Condition": {
|
|
109
|
+
"StringEquals": {
|
|
110
|
+
"AWS:SourceAccount": "REPLACE_WITH_ACCOUNT_ID",
|
|
111
|
+
"AWS:SourceArn": "arn:aws:ses:us-east-1:REPLACE_WITH_ACCOUNT_ID:configuration-set/kestrel-mail"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Replace the topic ARN, both copies of the account ID, and `us-east-1` with the values from your ARN. Choose **Save changes**.
|
|
118
|
+
|
|
119
|
+
1. Store the topic's ARN in the app. In your instance's repository, run this, and paste the ARN at the prompt. The app accepts events only from this topic:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx wrangler secret put SNS_TOPIC_ARN --env production
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
1. Store the configuration set's name, `kestrel-mail`. It's optional but recommended: the app names the set on every email, so SES reports on each one even without the hostname's default. Type it exactly as the set is named in this region, since SES refuses every email that names a set it can't find:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx wrangler secret put SES_CONFIGURATION_SET --env production
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 5. Create the app's AWS key
|
|
132
|
+
|
|
133
|
+
The app sends through an AWS Identity and Access Management (IAM) user whose key can only send email and read the account's sending status. That limits the harm if the key ever leaks. Reading the status lets the app show whether SES is still in the sandbox and take its send rate from the account. It also warns, without sending anything, when the configuration set is missing or paused, the sender isn't verified, or bounces and complaints can't reach the app.
|
|
134
|
+
|
|
135
|
+
If `kestrel-sender` already exists, from an earlier release, don't create a second user or key. Update its policy instead, as the end of this section shows.
|
|
136
|
+
|
|
137
|
+
1. In the [IAM console](https://console.aws.amazon.com/iam/), choose **Policies**, then **Create policy**.
|
|
138
|
+
|
|
139
|
+
1. Choose the **JSON** editor, and replace its contents with this policy:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"Version": "2012-10-17",
|
|
144
|
+
"Statement": [
|
|
145
|
+
{
|
|
146
|
+
"Effect": "Allow",
|
|
147
|
+
"Action": [
|
|
148
|
+
"ses:SendEmail",
|
|
149
|
+
"ses:SendBulkEmail",
|
|
150
|
+
"ses:SendTemplatedEmail",
|
|
151
|
+
"ses:SendBulkTemplatedEmail",
|
|
152
|
+
"ses:GetAccount",
|
|
153
|
+
"ses:GetEmailIdentity",
|
|
154
|
+
"ses:GetConfigurationSet"
|
|
155
|
+
],
|
|
156
|
+
"Resource": "*"
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
1. Choose **Next**, name the policy `kestrel-send-email`, and choose **Create policy**. The review lists Pinpoint Email, SES, and SES v2, which is how IAM shows these SES actions.
|
|
163
|
+
|
|
164
|
+
1. Choose **IAM users**, then **Create user**. Name it `kestrel-sender`, leave console access off, and choose **Next**.
|
|
165
|
+
|
|
166
|
+
1. Choose **Attach policies directly**, search for `kestrel-send-email`, and select it. AWS recommends groups, which help manage many people, and one app's user doesn't need one. Choose **Next**, then **Create user**.
|
|
167
|
+
|
|
168
|
+
1. Open `kestrel-sender`, and choose **Create access key**. It's an access key, not an API key. For the use case, choose **Application running outside AWS**, and choose **Next**.
|
|
169
|
+
|
|
170
|
+
AWS suggests IAM Roles Anywhere instead. It needs a certificate authority of your own, so use the access key.
|
|
171
|
+
|
|
172
|
+
1. For the description tag, enter the app's hostname, such as `newsletter.example.com`, so you can tell its key apart later. Choose **Create access key**.
|
|
173
|
+
|
|
174
|
+
1. Keep the page open: AWS shows the secret access key only once. Store the access key ID in the app, and paste it at the prompt:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx wrangler secret put AWS_ACCESS_KEY_ID --env production
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
1. Store the secret access key:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
npx wrangler secret put AWS_SECRET_ACCESS_KEY --env production
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
To update an existing key's policy, open **Policies** in the IAM console, then `kestrel-send-email`, and choose **Edit**. In the JSON editor, make its `Action` list match the policy above, and save. Without `ses:SendTemplatedEmail`, SES refuses even a test email. Once the key may call `ses:GetAccount`, the app's next check, within an hour, reads the account. Without `ses:GetConfigurationSet`, the app can't tell whether the configuration set exists, so it says nothing about it rather than guessing.
|
|
187
|
+
|
|
188
|
+
## 6. Connect the app and deploy
|
|
189
|
+
|
|
190
|
+
Until now the app has run with no email provider connected, refusing to send. Setting `PROVIDER` to `ses` connects SES, with the secrets you stored in sections 4 and 5. If a required one is missing, the app refuses to run until you store it, and `/health` names it.
|
|
191
|
+
|
|
192
|
+
1. In `wrangler.jsonc`, edit the `production` block's `vars`. Change `PROVIDER` to `ses` (it's `""` on a new instance; add it if your block has none). Change `AWS_REGION` to your region's code from section 1. Your other values stay as they are. The block ends up like this:
|
|
193
|
+
|
|
194
|
+
```jsonc
|
|
195
|
+
"vars": {
|
|
196
|
+
"PROVIDER": "ses", // ← set to ses
|
|
197
|
+
"APP_ORIGIN": "https://newsletter.example.com",
|
|
198
|
+
"ARCHIVE_BASE_PATH": "/archive",
|
|
199
|
+
"SENDING_DOMAIN": "send.example.com",
|
|
200
|
+
"FROM_ADDRESS": "Newsletter <newsletter@send.example.com>",
|
|
201
|
+
"AWS_REGION": "us-east-1" // ← change: your region's code
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
On Workers Paid, also add `"SUBREQUEST_BUDGET": "10000"`, the plan's limit, so each minute sends more. Leave it out on Workers Free. [As your list grows](../guides/09-as-your-list-grows.md) says when Workers Paid is worth it: for faster sends at any size, and for the daily database writes of a list past about 1,000 subscribers.
|
|
206
|
+
|
|
207
|
+
The app reads its send rate from your SES account, so there's nothing to change when AWS grants production access. **Settings → Email sender** shows the rate and where it comes from.
|
|
208
|
+
|
|
209
|
+
1. Commit the change, and push it:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
git commit -am "Send through Amazon SES"
|
|
213
|
+
git push
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
1. Deploy:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
npx wrangler deploy --env production
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
1. Check that the app answers on your hostname:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
curl https://newsletter.example.com/health
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
{"status":"ok","service":"kestrel"}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Any other answer names the setting the app is missing.
|
|
233
|
+
|
|
234
|
+
1. In the editor, **Settings → Email sender** shows Amazon SES and your From address, where it said **No email provider connected** before. Within a couple of minutes, a **Sandbox** pill appears beside it, once the app has read your SES account.
|
|
235
|
+
|
|
236
|
+
1. In the editor, open **Template** and select **Send test email**, to an address you verified in [Verify your test addresses](#3-verify-your-test-addresses). It arrives. Signups stay closed until SES takes an email, so the **Not proven yet** pill beside Amazon SES in **Settings → Email sender** is gone once it has. If SES refuses it, the test's line says why and what fixes it.
|
|
237
|
+
|
|
238
|
+
1. In a browser, `https://newsletter.example.com/subscribe` shows the subscribe form, where it said signups aren't open yet.
|
|
239
|
+
|
|
240
|
+
## 7. Subscribe the app to the topic
|
|
241
|
+
|
|
242
|
+
SNS delivers events only to a subscriber that has confirmed. The app confirms on its own, once it checks that the request came from your topic.
|
|
243
|
+
|
|
244
|
+
1. Check that the app's webhook is public. It refuses a request that isn't from SNS:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://newsletter.example.com/webhooks/ses
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
400
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Any other answer, such as a redirect to your Cloudflare login, means Access covers the webhook. Remove that path from your Access application.
|
|
255
|
+
|
|
256
|
+
1. In the SNS console, open your topic, and choose **Create subscription**. Check that **Topic ARN** shows the `kestrel-events` topic, and choose it if it's empty.
|
|
257
|
+
|
|
258
|
+
1. Set **Protocol** to **HTTPS**, and enter the endpoint:
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
https://newsletter.example.com/webhooks/ses
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
1. Leave raw message delivery off.
|
|
265
|
+
|
|
266
|
+
1. Give SNS longer to retry. By default it retries a failed delivery 3 times over about a minute, then discards the event, so a bounce that arrives mid-deploy is lost. Expand **Delivery policy (HTTP/S)**, turn off **Use the default delivery policy**, and set these values:
|
|
267
|
+
|
|
268
|
+
| Field | Value |
|
|
269
|
+
| --- | --- |
|
|
270
|
+
| Number of retries | `13` |
|
|
271
|
+
| Retries without delay | `0` |
|
|
272
|
+
| Minimum delay | `20` |
|
|
273
|
+
| Minimum delay retries | `2` |
|
|
274
|
+
| Maximum delay | `300` |
|
|
275
|
+
| Maximum delay retries | `6` |
|
|
276
|
+
| Retry-backoff function | Exponential |
|
|
277
|
+
|
|
278
|
+
SNS then keeps trying for about 40 minutes. It allows an hour at most. The topic has a delivery policy of its own, under **Edit**. Setting it there instead also covers any subscription you add later.
|
|
279
|
+
|
|
280
|
+
1. Choose **Create subscription**.
|
|
281
|
+
|
|
282
|
+
1. Reload the subscription. Its status changes to **Confirmed**, with nothing for you to click.
|
|
283
|
+
|
|
284
|
+
If it stays **Pending confirmation**, check that your Access application covers only `/dashboard` and `/api`. Check that `SNS_TOPIC_ARN` matches the topic's ARN, and that section 6's deploy finished. A mismatched ARN shows in the app's logs as a `webhook.refused` error naming both. Then request confirmation again from the topic's subscriptions. SNS doesn't let you delete a pending subscription, and removes it on its own after 48 hours.
|
|
285
|
+
|
|
286
|
+
1. **(Optional)** Keep what SNS still can't deliver. On the subscription, choose **Edit**, and set a **Redrive policy (dead-letter queue)** to an SQS queue of your own. SNS then moves an event there instead of discarding it. [Amazon SNS dead-letter queues](https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html) covers the queue and its permissions.
|
|
287
|
+
|
|
288
|
+
## 8. Receive replies at your From address
|
|
289
|
+
|
|
290
|
+
Replies to a post go to your From address, `newsletter@send.example.com`, which has no inbox yet, so they bounce. The reader is told your domain couldn't be found. This section forwards that address to the publisher's inbox with Cloudflare Email Routing. Forwarding only receives mail, so it doesn't change how SES sends.
|
|
291
|
+
|
|
292
|
+
1. Check that your From address has no inbox yet. This prints nothing:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
dig +short MX send.example.com
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
If it lists mail servers, `send.example.com` already receives mail elsewhere, and Email Routing would replace those servers. Check that your mail host has a mailbox for `newsletter@send.example.com`, and skip the rest of this section.
|
|
299
|
+
|
|
300
|
+
1. In the [Cloudflare dashboard](https://dash.cloudflare.com/), go to **Compute → Email Service → Email Routing**.
|
|
301
|
+
|
|
302
|
+
1. Add `send.example.com` to Email Routing: select `example.com`, then **Settings**, and add `send` under **Subdomains**.
|
|
303
|
+
|
|
304
|
+
1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added. Its SPF record is for forwarding: SES's mail passes SPF at `bounce.send.example.com`, from section 1.
|
|
305
|
+
|
|
306
|
+
1. Under **Destination Addresses**, add the publisher's inbox. Open the email Cloudflare sends to it, and select **Verify email address**. If it isn't your inbox, ask the publisher to do this step.
|
|
307
|
+
|
|
308
|
+
1. Under **Routing Rules**, select **Create routing rule**. For **Email pattern**, enter `newsletter`, the part of your From address before the `@`, and select `send.example.com`. Set **Action** to **Send to an email**, select the publisher's inbox as the **Destination**, and save.
|
|
309
|
+
|
|
310
|
+
1. **(Optional)** Give your DMARC reports an inbox too. If the address in your DMARC record doesn't receive mail, add a rule that forwards `dmarc` the same way. Then change the record's `rua` to `mailto:dmarc@send.example.com`.
|
|
311
|
+
|
|
312
|
+
1. Check that your From address receives mail. This now lists Cloudflare's three `route` mail servers:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
dig +short MX send.example.com
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
1. Send an email to `newsletter@send.example.com` from an address whose mail doesn't go through the publisher's inbox. Gmail, for one, hides a forwarded copy of mail you sent yourself. In Email Routing's **Activity log**, the email shows as **Forwarded**.
|
|
319
|
+
|
|
320
|
+
If it bounces saying the domain couldn't be found, the sender's provider is still remembering that `send.example.com` had no mail servers. That happens after an email to it before this section, and lasts up to 30 minutes on Cloudflare's DNS. Wait, and send it again.
|
|
321
|
+
|
|
322
|
+
Out-of-office and other automatic replies reach the publisher's inbox too. Bounces don't: they go to SES, which reports them to the app. The publisher answers from their own inbox, not from `newsletter@send.example.com`. To answer from the newsletter's address instead, give it a mailbox at a mail host such as [Fastmail](https://www.fastmail.help/hc/en-us/articles/360060591133) or [Google Workspace](https://support.google.com/a/answer/140034), in place of forwarding.
|
|
323
|
+
|
|
324
|
+
SES is now connected in the sandbox. Until AWS grants production access, it sends only to the addresses you verified in section 3. It also sends to the SES test addresses Verify it works uses. SES charges for each email it sends, test emails included, as [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists.
|
|
325
|
+
|
|
326
|
+
Next, [request production access](06-production-access.md).
|