@kurtbruns/kestrel 1.15.1 → 1.17.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/CHANGELOG.md +33 -1
- package/cli/sweep-ticker.mjs +3 -3
- package/dist/public/dashboard/app.js +1 -1
- package/dist/public/dashboard/app.js.map +1 -1
- package/dist/public/dashboard/index.html +1 -1
- package/dist/worker/index.js +127 -38
- package/docs/get-started/02-deploy.md +8 -2
- package/docs/get-started/03-access.md +5 -3
- package/docs/get-started/04-ses.md +82 -115
- package/docs/get-started/05-verify.md +18 -16
- package/docs/guides/01-allow-claude.md +1 -1
- package/docs/guides/03-notifications.md +2 -2
- package/docs/publishing/02-sending-rules.md +2 -2
- package/docs/publishing/03-name-your-publication.md +1 -1
- package/docs/publishing/06-troubleshooting.md +29 -68
- package/docs/publishing/10-check-your-setup.md +1 -1
- package/docs/reference/01-configuration.md +5 -3
- package/docs/reference/02-sending-domain-dns.md +3 -3
- package/package.json +1 -4
- package/template/claude/CLAUDE.md +2 -0
- package/template/wrangler.jsonc +10 -3
|
@@ -22,7 +22,7 @@ Kestrel uses two hostnames: one for the app, and one your email is sent from.
|
|
|
22
22
|
|
|
23
23
|
These guides put the app on a subdomain, which leaves any website you already have on `example.com` untouched. If `example.com` has no website of its own, the app can live there instead. Use `example.com` wherever these guides say `newsletter.example.com`.
|
|
24
24
|
|
|
25
|
-
It's recommended to send from a subdomain rather than from `example.com` itself. Your apex domain carries the reputation of your regular email, and a newsletter shouldn't put that at risk. Avoid `mail.` as the sending name too, since mail servers read it as an inbound host. [Sending-domain DNS](../reference/02-sending-domain-dns.md#choose-your-sending-name) explains both.
|
|
25
|
+
It's recommended to send from a subdomain rather than from `example.com` itself. Your apex domain carries the reputation of your regular email, and a newsletter shouldn't put that at risk. These guides assume a sending subdomain throughout. Avoid `mail.` as the sending name too, since mail servers read it as an inbound host. [Sending-domain DNS](../reference/02-sending-domain-dns.md#choose-your-sending-name) explains both.
|
|
26
26
|
|
|
27
27
|
Throughout these guides, replace the example names with your own, and any value written as `REPLACE_WITH_…`.
|
|
28
28
|
|
|
@@ -175,9 +175,15 @@ npx wrangler deploy --env production
|
|
|
175
175
|
|
|
176
176
|
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
177
|
|
|
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 named `kestrel` that answers nowhere and never sends. If that happens, remove it:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npx wrangler delete --name kestrel
|
|
182
|
+
```
|
|
183
|
+
|
|
178
184
|
## Check it
|
|
179
185
|
|
|
180
|
-
1. The app answers on your hostname.
|
|
186
|
+
1. The app answers on your hostname. If `curl` reports a certificate error, Cloudflare is still issuing the new hostname's certificate; try again shortly.
|
|
181
187
|
|
|
182
188
|
```bash
|
|
183
189
|
curl https://newsletter.example.com/health
|
|
@@ -53,8 +53,6 @@ Every Access application denies everyone until a policy allows them.
|
|
|
53
53
|
|
|
54
54
|
Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in your `Publishers` policy 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
55
|
|
|
56
|
-
The app accepts only two kinds of policy on this application. One is an Allow policy on people's emails, like this one. The other is a Service Auth policy on a service token, from [Automate with a service token](../guides/02-automate.md). A login through any other kind, such as a certificate, a device check, or an identity provider that gives no email, is refused with a `401`, and the editor says so.
|
|
57
|
-
|
|
58
56
|
## 4. Give the app its two values
|
|
59
57
|
|
|
60
58
|
The app verifies each login against your team domain, and checks that it was issued for this application.
|
|
@@ -107,6 +105,10 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
107
105
|
{"status":"ok","service":"kestrel"}
|
|
108
106
|
```
|
|
109
107
|
|
|
110
|
-
If
|
|
108
|
+
If a check fails:
|
|
109
|
+
|
|
110
|
+
- **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 `Publishers` policy.
|
|
111
113
|
|
|
112
114
|
Next, [connect Amazon SES](04-ses.md).
|
|
@@ -1,93 +1,66 @@
|
|
|
1
1
|
# Connect Amazon SES
|
|
2
2
|
|
|
3
|
-
|
|
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. This page requests that review early, so you can test in the sandbox while you wait.
|
|
4
4
|
|
|
5
|
-
In this guide, you verify your sending hostname
|
|
5
|
+
In this guide, you verify your sending hostname, set a custom MAIL FROM domain, add a DMARC record, and request production access. You then route bounces and complaints to the app, give it an AWS key, and deploy it with SES connected. Last, you forward readers' replies to the publisher's inbox.
|
|
6
6
|
|
|
7
7
|
## Before you begin
|
|
8
8
|
|
|
9
|
-
- An [AWS account](https://portal.aws.amazon.com/billing/signup).
|
|
10
|
-
|
|
11
|
-
To move an instance that already sends through another provider or another SES account, read [Switch an instance that already sends](#switch-an-instance-that-already-sends) first.
|
|
9
|
+
- An [AWS account](https://portal.aws.amazon.com/billing/signup), if you haven't created one already.
|
|
10
|
+
- The publisher's inbox, for readers' replies.
|
|
12
11
|
|
|
13
12
|
## 1. Verify your sending hostname
|
|
14
13
|
|
|
15
|
-
SES sends only from an identity you've verified. Verifying the whole hostname
|
|
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.
|
|
16
15
|
|
|
17
|
-
1. Open the [SES console](https://console.aws.amazon.com/ses/), and choose
|
|
16
|
+
1. Open the [SES console](https://console.aws.amazon.com/ses/), and choose a region near you in the menu at the top right. SES keeps identities, quotas, and the sandbox separately in each region, so do every step on this page in this one. A few newer regions must be [enabled for your account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-regions.html) first.
|
|
18
17
|
|
|
19
|
-
1. Note the region's code
|
|
18
|
+
1. Note the region's code, such as `eu-west-1`. The menu shows only its name, such as Europe (Ireland), and [Amazon SES endpoints](https://docs.aws.amazon.com/general/latest/gr/ses.html) lists each code beside its name. Later steps use this code.
|
|
20
19
|
|
|
21
20
|
1. Under **Configuration**, choose **Identities**, then **Create identity**.
|
|
22
21
|
|
|
23
|
-
1. Select **Domain**, and enter `send.example.com`. Keep the default DKIM settings
|
|
22
|
+
1. Select **Domain**, and enter `send.example.com`. Keep the default DKIM settings, **Easy DKIM** with a 2048-bit key, and choose **Create identity**.
|
|
24
23
|
|
|
25
24
|
1. On the identity's **Authentication** tab, expand **Publish DNS records**. It lists three CNAME records.
|
|
26
25
|
|
|
27
|
-
1. In the Cloudflare dashboard, add each record to your domain's DNS records, exactly as SES lists it
|
|
26
|
+
1. In the Cloudflare dashboard, add each record to your domain's DNS records, exactly as SES lists it, with its proxy status set to **DNS only**. Cloudflare adds your domain to the name, so each name ends in `._domainkey.send`.
|
|
28
27
|
|
|
29
28
|
1. Wait until SES shows **DKIM configuration** as **Successful** and **Identity status** as **Verified**.
|
|
30
29
|
|
|
31
|
-
## 2.
|
|
32
|
-
|
|
33
|
-
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.
|
|
34
|
-
|
|
35
|
-
1. In the Cloudflare dashboard, open your domain's DNS records, and add a record:
|
|
36
|
-
|
|
37
|
-
- **Type:** `TXT`
|
|
38
|
-
- **Name:** `_dmarc.send`
|
|
39
|
-
- **Content:** `v=DMARC1; p=none; rua=mailto:dmarc@example.com`
|
|
40
|
-
|
|
41
|
-
1. Replace `dmarc@example.com` with an address that receives mail. Reports arrive there as attachments.
|
|
42
|
-
|
|
43
|
-
[Sending-domain DNS](../reference/02-sending-domain-dns.md#dmarc-publish-a-policy-and-collect-reports) explains each record, and when to tighten DMARC.
|
|
44
|
-
|
|
45
|
-
## 3. Receive replies at your From address
|
|
46
|
-
|
|
47
|
-
When a reader replies to a post, or to their confirmation email, the reply goes to your From address, `newsletter@send.example.com`. Nothing receives mail there yet, so the reply bounces. Kestrel only sends mail, so you give the address an inbox yourself. Here you forward it to the publisher's inbox, with Cloudflare Email Routing, which is free on every plan. The publisher is whoever writes and sends the newsletter, which may be you.
|
|
30
|
+
## 2. Set a custom MAIL FROM domain
|
|
48
31
|
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
1. In the [Cloudflare dashboard](https://dash.cloudflare.com/), go to **Compute → Email Service → Email Routing**.
|
|
52
|
-
|
|
53
|
-
1. Add `send.example.com` to Email Routing. Cloudflare adds subdomains from your domain's own settings: select `example.com`, then **Settings**, and add `send` under **Subdomains**.
|
|
54
|
-
|
|
55
|
-
1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added.
|
|
32
|
+
By default, SES sends with its own return path, so Gmail shows your mail as "mailed-by `amazonses.com`", and only DKIM ties it to your domain. A custom MAIL FROM domain puts your hostname there too, so SPF also passes for your domain. Your mail then still passes DMARC, the check section 3 sets up, if DKIM ever fails. It also meets Gmail's bulk-sender rule to set up SPF for your domain.
|
|
56
33
|
|
|
57
|
-
1.
|
|
58
|
-
|
|
59
|
-
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.
|
|
60
|
-
|
|
61
|
-
On a subdomain, Email Routing forwards only the addresses you add a rule for, with no catch-all. That's all a From address needs.
|
|
34
|
+
1. In SES, open the `send.example.com` identity. Under **Custom MAIL FROM domain**, choose **Edit**.
|
|
62
35
|
|
|
63
|
-
|
|
64
|
-
- **Your DMARC reports need an inbox too.** Your DMARC record's report address may not receive mail. If so, change the record's `rua` to `mailto:dmarc@send.example.com`, and add a second rule that forwards `dmarc` the same way.
|
|
65
|
-
- **Answers come from the publisher's inbox.** A forwarded reply comes from the reader's own address. An answer to it goes out from the inbox it was forwarded to, not from `newsletter@send.example.com`.
|
|
36
|
+
1. Select **Use a custom MAIL FROM domain**, and enter `bounce.send.example.com`. For **Behavior on MX failure**, keep **Use default MAIL FROM domain**. Choose **Save changes**.
|
|
66
37
|
|
|
67
|
-
|
|
38
|
+
1. In Cloudflare, add the two records SES lists, with your region's code in place of `us-east-1`:
|
|
68
39
|
|
|
69
|
-
|
|
40
|
+
- **Type:** `MX`, **Name:** `bounce.send`, **Mail server:** `feedback-smtp.us-east-1.amazonses.com`, **Priority:** `10`
|
|
41
|
+
- **Type:** `TXT`, **Name:** `bounce.send`, **Content:** `v=spf1 include:amazonses.com ~all`
|
|
70
42
|
|
|
71
|
-
|
|
43
|
+
1. Wait until SES shows the MAIL FROM domain's status as successful.
|
|
72
44
|
|
|
73
|
-
|
|
45
|
+
## 3. Add a DMARC record
|
|
74
46
|
|
|
75
|
-
|
|
47
|
+
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.
|
|
76
48
|
|
|
77
|
-
1.
|
|
49
|
+
1. In the Cloudflare dashboard, open your domain's DNS records, and add a record:
|
|
78
50
|
|
|
79
|
-
|
|
51
|
+
- **Type:** `TXT`
|
|
52
|
+
- **Name:** `_dmarc.send`
|
|
53
|
+
- **Content:** `v=DMARC1; p=none; rua=mailto:dmarc@example.com`
|
|
80
54
|
|
|
81
|
-
|
|
82
|
-
- **Type:** `TXT`, **Name:** `bounce.send`, **Content:** `v=spf1 include:amazonses.com ~all`
|
|
55
|
+
1. Replace `dmarc@example.com` with an address that receives mail. Reports arrive there as attachments.
|
|
83
56
|
|
|
84
|
-
|
|
57
|
+
[Sending-domain DNS](../reference/02-sending-domain-dns.md#dmarc-publish-a-policy-and-collect-reports) explains each record, and when to tighten DMARC.
|
|
85
58
|
|
|
86
|
-
##
|
|
59
|
+
## 4. Request production access
|
|
87
60
|
|
|
88
|
-
A new SES account starts in the sandbox
|
|
61
|
+
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. Ask AWS to lift those limits now, and test while you wait.
|
|
89
62
|
|
|
90
|
-
1. Name the publication
|
|
63
|
+
1. Name the publication, since AWS may visit its website to review the request. In the editor, open **Settings → Publication identity**, and enter the name and tagline, or ask the publisher for them.
|
|
91
64
|
|
|
92
65
|
1. In SES, open the **Account dashboard**, select **View Get set up page**, and then **Request production access**.
|
|
93
66
|
|
|
@@ -96,32 +69,28 @@ A new SES account starts in the sandbox. It sends only to addresses you've verif
|
|
|
96
69
|
- **Mail type:** Marketing
|
|
97
70
|
- **Website URL:** `https://newsletter.example.com/`
|
|
98
71
|
|
|
72
|
+
Leave the other fields as they are.
|
|
73
|
+
|
|
99
74
|
1. Check the acknowledgement, and select **Submit request**. AWS Support answers by email, and may ask how you collect addresses and handle bounces.
|
|
100
75
|
|
|
101
|
-
1.
|
|
76
|
+
1. Verify two addresses you own, such as a personal address and a work address, so the sandbox lets you mail them. Your test emails go to these. For each, under **Identities**, choose **Create identity**, select **Email address**, and follow the link in the email AWS sends.
|
|
102
77
|
|
|
103
|
-
If AWS asks for more detail,
|
|
78
|
+
If AWS asks for more detail, or denies the request, reply in the support case, or open a new request from the **Account dashboard**. Say how Kestrel handles the list:
|
|
104
79
|
|
|
105
80
|
- Signups use double opt-in: each subscriber confirms by email before joining the list.
|
|
106
81
|
- Every post offers one-click unsubscribe.
|
|
107
|
-
- Bounces and complaints reach the app
|
|
82
|
+
- Bounces and complaints reach the app, and it stops mailing those addresses automatically.
|
|
108
83
|
- How many subscribers the newsletter expects, and how often it sends.
|
|
109
84
|
|
|
110
|
-
|
|
85
|
+
In the meantime, continue with the rest of this guide.
|
|
111
86
|
|
|
112
|
-
|
|
113
|
-
- A post goes to your verified addresses and then waits, with the line "The provider is refusing the account" and SES's words. No reader is marked unsent. Once AWS grants access, the post goes to the rest at its next retry, mailing no one twice.
|
|
114
|
-
- A daily quota refusal in the sandbox (200 emails a day) means the same thing: request production access rather than a higher quota.
|
|
87
|
+
## 5. Send bounces and complaints to the app
|
|
115
88
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
## 6. Send bounces and complaints to SNS
|
|
119
|
-
|
|
120
|
-
SES reports what happens to each email through SNS, the AWS notification service. A configuration set says which events to report, and an SNS topic carries them to the app.
|
|
89
|
+
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.
|
|
121
90
|
|
|
122
91
|
1. In SES, under **Configuration**, choose **Configuration sets**, then **Create set**. Name it `kestrel-mail`, and choose **Create set**.
|
|
123
92
|
|
|
124
|
-
1. On the set's **Event destinations** tab, choose **Add destination**. Select **Hard bounces**, **Complaints**, **Deliveries**, and **Rendering failures**, and choose **Next**.
|
|
93
|
+
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.
|
|
125
94
|
|
|
126
95
|
1. For **Destination type**, select **Amazon SNS**. Name it `kestrel-sns`, and keep **Event publishing** enabled. Choose **Next**.
|
|
127
96
|
|
|
@@ -129,7 +98,7 @@ SES reports what happens to each email through SNS, the AWS notification service
|
|
|
129
98
|
|
|
130
99
|
1. Make the set your hostname's default, so SES reports on every email sent from it. Open the `send.example.com` identity, and choose the **Configuration set** tab. Under **Default configuration set**, choose **Edit**, select `kestrel-mail`, and save.
|
|
131
100
|
|
|
132
|
-
1. Open the topic in the [SNS console](https://console.aws.amazon.com/sns/), and
|
|
101
|
+
1. Open the topic in the [SNS console](https://console.aws.amazon.com/sns/), 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.
|
|
133
102
|
|
|
134
103
|
1. Let SES publish to the topic. Choose **Edit**, expand **Access policy**, and add this statement to the `Statement` list:
|
|
135
104
|
|
|
@@ -148,13 +117,23 @@ SES reports what happens to each email through SNS, the AWS notification service
|
|
|
148
117
|
}
|
|
149
118
|
```
|
|
150
119
|
|
|
151
|
-
Replace the topic ARN,
|
|
120
|
+
Replace the topic ARN, both copies of the account ID, and `us-east-1` with the values from your ARN. Choose **Save changes**.
|
|
121
|
+
|
|
122
|
+
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:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx wrangler secret put SNS_TOPIC_ARN --env production
|
|
126
|
+
```
|
|
152
127
|
|
|
153
|
-
|
|
128
|
+
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:
|
|
154
129
|
|
|
155
|
-
|
|
130
|
+
```bash
|
|
131
|
+
npx wrangler secret put SES_CONFIGURATION_SET --env production
|
|
132
|
+
```
|
|
156
133
|
|
|
157
|
-
|
|
134
|
+
## 6. Create the app's AWS key
|
|
135
|
+
|
|
136
|
+
The app sends through an AWS Identity and Access Management (IAM) user whose key can only send email. That limits the harm if the key ever leaks.
|
|
158
137
|
|
|
159
138
|
1. In the [IAM console](https://console.aws.amazon.com/iam/), go to **Users**, and choose **Create user**. Name it `kestrel-sender`, and leave console access off.
|
|
160
139
|
|
|
@@ -183,13 +162,7 @@ The app sends through an IAM user whose key can only send email. That limits the
|
|
|
183
162
|
|
|
184
163
|
1. Open the user, and on the **Security credentials** tab, choose **Create access key**. For the use case, choose **Application running outside AWS**.
|
|
185
164
|
|
|
186
|
-
1.
|
|
187
|
-
|
|
188
|
-
## 8. Set the provider and deploy
|
|
189
|
-
|
|
190
|
-
Until now the app has run with no email provider connected, refusing to send. Setting `PROVIDER` connects SES. Store the secrets before you set it. With `PROVIDER` set to `ses` and a required secret missing, the app refuses to run, and sends nothing, until you store it. `/health` and the editor's **A setting needs fixing** page name the missing secret, and reader pages say the newsletter is temporarily unavailable. `SES_CONFIGURATION_SET` is the one optional secret.
|
|
191
|
-
|
|
192
|
-
1. Store the access key ID, and paste it at the prompt:
|
|
165
|
+
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:
|
|
193
166
|
|
|
194
167
|
```bash
|
|
195
168
|
npx wrangler secret put AWS_ACCESS_KEY_ID --env production
|
|
@@ -201,17 +174,9 @@ Until now the app has run with no email provider connected, refusing to send. Se
|
|
|
201
174
|
npx wrangler secret put AWS_SECRET_ACCESS_KEY --env production
|
|
202
175
|
```
|
|
203
176
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
npx wrangler secret put SNS_TOPIC_ARN --env production
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
1. Store the configuration set's name, `kestrel-mail`. It's optional but recommended: the app names it on every email, so SES reports on each one even without the default. Type it exactly as the set is named in this region: if SES can't find the set, it refuses every email, and a list send is held until the name is fixed or the set created:
|
|
177
|
+
## 7. Set the provider and deploy
|
|
211
178
|
|
|
212
|
-
|
|
213
|
-
npx wrangler secret put SES_CONFIGURATION_SET --env production
|
|
214
|
-
```
|
|
179
|
+
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 5 and 6. If a required one is missing, the app refuses to run until you store it, and `/health` names it.
|
|
215
180
|
|
|
216
181
|
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), and add `SES_MAX_SEND_RATE`. Change `AWS_REGION` to your region's code from section 1. Your other values stay as they are. The block ends up like this:
|
|
217
182
|
|
|
@@ -244,7 +209,7 @@ Until now the app has run with no email provider connected, refusing to send. Se
|
|
|
244
209
|
npx wrangler deploy --env production
|
|
245
210
|
```
|
|
246
211
|
|
|
247
|
-
##
|
|
212
|
+
## 8. Subscribe the app to the topic
|
|
248
213
|
|
|
249
214
|
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.
|
|
250
215
|
|
|
@@ -260,7 +225,9 @@ SNS delivers events only to a subscriber that has confirmed. The app confirms on
|
|
|
260
225
|
|
|
261
226
|
1. Reload the subscription. Its status changes to **Confirmed**, with nothing for you to click.
|
|
262
227
|
|
|
263
|
-
|
|
228
|
+
If it stays **Pending confirmation**, check that your Access application covers only `/dashboard` and `/api`, that `SNS_TOPIC_ARN` matches the topic's ARN, and that section 7'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.
|
|
229
|
+
|
|
230
|
+
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. Open the topic, choose **Edit**, and expand **Delivery retry policy (HTTP/S)**. Turn off the default policy, set these values, and choose **Save changes**:
|
|
264
231
|
|
|
265
232
|
| Field | Value |
|
|
266
233
|
| --- | --- |
|
|
@@ -272,25 +239,35 @@ SNS delivers events only to a subscriber that has confirmed. The app confirms on
|
|
|
272
239
|
| Maximum delay retries | `6` |
|
|
273
240
|
| Retry-backoff function | Exponential |
|
|
274
241
|
|
|
275
|
-
SNS then keeps trying for
|
|
242
|
+
SNS then keeps trying for about 40 minutes. It allows an hour at most.
|
|
243
|
+
|
|
244
|
+
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.
|
|
276
245
|
|
|
277
|
-
|
|
246
|
+
## 9. Receive replies at your From address
|
|
278
247
|
|
|
279
|
-
|
|
248
|
+
Replies to a post go to your From address, `newsletter@send.example.com`, which has no inbox yet, so they bounce. 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.
|
|
280
249
|
|
|
281
|
-
|
|
282
|
-
- **`SNS_TOPIC_ARN` matches the topic's ARN.** If it doesn't, the app's logs show a `webhook.refused` error naming both.
|
|
283
|
-
- **The deploy in section 8 finished.**
|
|
250
|
+
1. Check that your From address has no inbox yet. This prints nothing:
|
|
284
251
|
|
|
285
|
-
|
|
252
|
+
```bash
|
|
253
|
+
dig +short MX send.example.com
|
|
254
|
+
```
|
|
286
255
|
|
|
287
|
-
|
|
256
|
+
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 to [Check it](#check-it).
|
|
288
257
|
|
|
289
|
-
|
|
258
|
+
1. In the [Cloudflare dashboard](https://dash.cloudflare.com/), go to **Compute → Email Service → Email Routing**.
|
|
290
259
|
|
|
291
|
-
|
|
260
|
+
1. Add `send.example.com` to Email Routing: select `example.com`, then **Settings**, and add `send` under **Subdomains**.
|
|
292
261
|
|
|
293
|
-
|
|
262
|
+
1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added.
|
|
263
|
+
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
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.
|
|
267
|
+
|
|
268
|
+
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`.
|
|
269
|
+
|
|
270
|
+
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.
|
|
294
271
|
|
|
295
272
|
## Check it
|
|
296
273
|
|
|
@@ -318,26 +295,16 @@ Kestrel can't tell whether those emails went out. SES's console shows sent messa
|
|
|
318
295
|
|
|
319
296
|
1. The SNS subscription shows **Confirmed**.
|
|
320
297
|
|
|
321
|
-
1. Your From address
|
|
322
|
-
|
|
323
|
-
- **Type:** `MX`, **Name:** `send`, three of them, for `route1.mx.cloudflare.net`, `route2.mx.cloudflare.net`, and `route3.mx.cloudflare.net`
|
|
324
|
-
- **Type:** `TXT`, **Name:** `send`, **Content:** `v=spf1 include:_spf.mx.cloudflare.net ~all`
|
|
325
|
-
- **Type:** `TXT`, **Name:** `cf2024-1._domainkey.send`, Cloudflare's DKIM key
|
|
326
|
-
|
|
327
|
-
The world sees the MX records too:
|
|
298
|
+
1. Your From address receives mail. This lists Cloudflare's three `route` mail servers, or your own mail host's if you skipped section 9's forwarding:
|
|
328
299
|
|
|
329
300
|
```bash
|
|
330
301
|
dig +short MX send.example.com
|
|
331
302
|
```
|
|
332
303
|
|
|
333
|
-
It lists the three `route` servers. Email Routing's **Routing Rules** shows your rule for `newsletter@send.example.com`, and **Destination Addresses** shows the publisher's inbox as verified. A reply to your test email in the next step proves the forwarding works.
|
|
334
|
-
|
|
335
304
|
1. In the editor, **Settings → Email sender** shows Amazon SES and your From address, where it said **No email provider connected** before.
|
|
336
305
|
|
|
337
306
|
1. In a browser, `https://newsletter.example.com/subscribe` shows the subscribe form, where it said signups aren't open yet.
|
|
338
307
|
|
|
339
|
-
The test
|
|
340
|
-
|
|
341
|
-
SES charges for each email it sends, test emails included. [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists the rates.
|
|
308
|
+
The test emails in the next step prove the rest, including a reply. In the sandbox, they go only to the addresses you verified in section 4, and to the SES test addresses that page lists. SES charges for each email it sends, test emails included, as [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists.
|
|
342
309
|
|
|
343
310
|
Next, [verify it works](05-verify.md).
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Verify it works
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This page checks your instance end to end with real email, while the list is empty and every email goes only to you. You confirm that inboxes trust your mail, that posts look right in a mail client, and that bounces, complaints, and unsubscribes reach the app.
|
|
4
4
|
|
|
5
5
|
In this guide, you send yourself a test email, reply to it, and check that it passes authentication. Then you set up notifications, send a real post to yourself as a subscriber, and unsubscribe. Finally, you check that bounces and complaints reach the app, read the app's logs, check that SES can send to anyone, and clean up the test subscribers.
|
|
6
6
|
|
|
7
7
|
## Before you begin
|
|
8
8
|
|
|
9
|
-
-
|
|
9
|
+
- The two addresses you verified in SES in [Connect Amazon SES](04-ses.md#4-request-production-access). Some steps use both.
|
|
10
10
|
- A Gmail address, or another mail client that shows a message's original source and an **Unsubscribe** button.
|
|
11
11
|
|
|
12
12
|
## 1. Send yourself a test email
|
|
@@ -17,9 +17,9 @@ A test email goes through the same code as a real send. If the test looks right,
|
|
|
17
17
|
|
|
18
18
|
1. Select **Send test email**, enter your address, and send it.
|
|
19
19
|
|
|
20
|
-
1. Open the email. It
|
|
20
|
+
1. Open the email. It arrives in your inbox, not in spam, the image loads, and the subject reads as you wrote it. Its links work, but a test email unsubscribes no one; section 5 unsubscribes for real.
|
|
21
21
|
|
|
22
|
-
1.
|
|
22
|
+
1. Send a test email to your second address, not the publisher's inbox, and reply to it from there, as a reader would. The reply arrives in the publisher's inbox, the one you chose in [Connect Amazon SES](04-ses.md#9-receive-replies-at-your-from-address), or in your From address's own mailbox if you skipped forwarding. If it bounces, check the routing rule and the destination address there.
|
|
23
23
|
|
|
24
24
|
## 2. Check that it passes authentication
|
|
25
25
|
|
|
@@ -33,13 +33,13 @@ Inboxes trust mail that passes DKIM and DMARC for the domain it's from. Passing
|
|
|
33
33
|
- `dmarc=pass` for `send.example.com`
|
|
34
34
|
- `spf=pass`, for a hostname under `send.example.com`
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
If SPF passes for `amazonses.com` instead, SES couldn't read your custom MAIL FROM domain's MX record, and used its own. DMARC still passes, but check the records from [Connect Amazon SES](04-ses.md#2-set-a-custom-mail-from-domain).
|
|
37
37
|
|
|
38
38
|
If a check fails, compare your DNS records with the ones in [Connect Amazon SES](04-ses.md#1-verify-your-sending-hostname). [Sending-domain DNS](../reference/02-sending-domain-dns.md#verify-alignment) explains each result.
|
|
39
39
|
|
|
40
40
|
## 3. Set up notifications
|
|
41
41
|
|
|
42
|
-
Kestrel emails the notifications address when a send finishes, and right away when one runs into a problem.
|
|
42
|
+
Kestrel emails the notifications address when a send finishes, and right away when one runs into a problem.
|
|
43
43
|
|
|
44
44
|
1. In the editor, open **Settings → Notifications**.
|
|
45
45
|
|
|
@@ -55,18 +55,16 @@ This is a real send, to a list of one. It proves the whole path: subscribing, co
|
|
|
55
55
|
|
|
56
56
|
1. Open `https://newsletter.example.com/subscribe`, and subscribe with your second address.
|
|
57
57
|
|
|
58
|
-
1. Open the confirmation email, follow its link, and select **Confirm**. In the editor, **Subscribers** shows the address as **Confirmed**.
|
|
58
|
+
1. Open the confirmation email, follow its link, and select **Confirm subscription**. In the editor, **Subscribers** shows the address as **Confirmed**.
|
|
59
59
|
|
|
60
|
-
1. Open the post, select **Schedule**, and then **Send now**.
|
|
60
|
+
1. Open the post, select **Schedule**, and then **Send now**.
|
|
61
61
|
|
|
62
|
-
1.
|
|
62
|
+
1. In the **Send now?** dialog, uncheck **Include in archive**, and select **Send now**. The post goes out as email only, so it doesn't become the publication's first archived post. The dashboard keeps its **Finish setting up** steps for the publisher. Every send waits at least five minutes before it goes out, so you can cancel a mistake.
|
|
63
63
|
|
|
64
|
-
1. Cancel the send, to see that canceling stops it. Then
|
|
64
|
+
1. Cancel the send, to see that canceling stops it. Then send it again the same way, with **Include in archive** unchecked, and let it go.
|
|
65
65
|
|
|
66
66
|
1. When the send finishes, the post arrives at your second address. A notification arrives at your first, with the subject `Sent:` and the post's title.
|
|
67
67
|
|
|
68
|
-
If you verified this instance before Email only existed, the test post is in the archive. Open it under **Sent**, and select **Remove from archive…**.
|
|
69
|
-
|
|
70
68
|
## 5. Unsubscribe
|
|
71
69
|
|
|
72
70
|
Every email carries an unsubscribe link, and a header that lets mail clients show their own **Unsubscribe** button. Both work in one step, with no login.
|
|
@@ -88,11 +86,11 @@ When an address bounces or marks your mail as spam, SES tells the app through SN
|
|
|
88
86
|
|
|
89
87
|
1. In the editor, **Subscribers** lists the bounce address as **Pending** with a **bounced** flag, and the complaint address as **Unsubscribed**. Open its row: its page says it left by a complaint. The app mails neither again.
|
|
90
88
|
|
|
91
|
-
If the bounce address has no flag, or the complaint address is still pending, SES's events aren't reaching the app. Check the configuration set
|
|
89
|
+
If the bounce address has no flag, or the complaint address is still pending, SES's events aren't reaching the app. Check the configuration set in [Send bounces and complaints to the app](04-ses.md#5-send-bounces-and-complaints-to-the-app), and the subscription in [Subscribe the app to the topic](04-ses.md#8-subscribe-the-app-to-the-topic).
|
|
92
90
|
|
|
93
91
|
## 7. Read the logs
|
|
94
92
|
|
|
95
|
-
The app writes a log line for each thing it does, and Cloudflare keeps them for a few days. Nothing else tells you if the once-a-minute schedule that sends posts stops, so it's worth knowing where to look.
|
|
93
|
+
The app writes a log line for each thing it does, and Cloudflare keeps them for a few days, depending on your plan. Nothing else tells you if the once-a-minute schedule that sends posts stops, so it's worth knowing where to look.
|
|
96
94
|
|
|
97
95
|
1. In the Cloudflare dashboard, go to **Workers & Pages**, select `kestrel-production`, and select **Observability**.
|
|
98
96
|
|
|
@@ -100,15 +98,19 @@ The app writes a log line for each thing it does, and Cloudflare keeps them for
|
|
|
100
98
|
|
|
101
99
|
1. Filter on `event` equal to `send.completed`. Your send from section 4 is there.
|
|
102
100
|
|
|
101
|
+
1. Filter on `event` equal to `send.scheduled`. Your two sends from section 4 are there, and `send.canceled` shows the one you canceled. Each line that records an action says who took it. Its `principal` is `human` or `service`, and its `client` names the program a person used, such as `editor`.
|
|
102
|
+
|
|
103
103
|
1. Filter on `level` equal to `error`. A healthy instance shows nothing.
|
|
104
104
|
|
|
105
|
+
To follow one send, narrow the time range and filter on its `sendId`, since a long range may show only a sample. On the Free plan, Cloudflare stops keeping logs for the rest of the day once its daily allowance is used, as [its pricing page](https://developers.cloudflare.com/observability/pricing/) describes.
|
|
106
|
+
|
|
105
107
|
## 8. Check that SES can send to anyone
|
|
106
108
|
|
|
107
|
-
[Connect Amazon SES](04-ses.md#
|
|
109
|
+
[Connect Amazon SES](04-ses.md#4-request-production-access) asked AWS for production access. Until AWS grants it, the sandbox mails only your verified addresses, so check it before you go live.
|
|
108
110
|
|
|
109
111
|
1. In SES, open the **Account dashboard** in the region the app sends from. It no longer says your account is in the sandbox, and it shows your daily quota and maximum send rate.
|
|
110
112
|
|
|
111
|
-
1. Check that `SES_MAX_SEND_RATE` in `wrangler.jsonc` matches that rate, and that you deployed after setting it, as [Connect Amazon SES](04-ses.md#
|
|
113
|
+
1. Check that `SES_MAX_SEND_RATE` in `wrangler.jsonc` matches that rate, and that you deployed after setting it, as [Connect Amazon SES](04-ses.md#7-set-the-provider-and-deploy) describes.
|
|
112
114
|
|
|
113
115
|
If the dashboard still says sandbox, AWS hasn't answered yet. Every other section works in the sandbox, so come back to this one when it does.
|
|
114
116
|
|
|
@@ -17,7 +17,7 @@ You need:
|
|
|
17
17
|
|
|
18
18
|
Managed OAuth lets an app such as Claude act with a person's account, the way the editor does in their browser. Cloudflare marks it as beta, and its [Managed OAuth](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/) page describes it.
|
|
19
19
|
|
|
20
|
-
1. In **Zero Trust**, go to **Access controls → Applications**, and select **
|
|
20
|
+
1. In **Zero Trust**, go to **Access controls → Applications**, and select **Configure** on your application.
|
|
21
21
|
|
|
22
22
|
1. On the **Additional settings** tab, select the **OAuth** filter, and find **Managed OAuth (Beta)**.
|
|
23
23
|
|
|
@@ -21,7 +21,7 @@ Cloudflare's email sends only from a domain set up with Email Routing. Use your
|
|
|
21
21
|
|
|
22
22
|
1. Cloudflare adds MX, SPF, and DKIM records for `newsletter.example.com`, and locks them. Wait until they show as added.
|
|
23
23
|
|
|
24
|
-
Avoid turning on Email Routing for a domain that already receives mail elsewhere, such as `example.com` with Google Workspace. Email Routing takes over that domain's MX records. Don't send notifications from `send.example.com` either, since your email provider sends from it. Email Routing there to forward readers' replies, as [Connect Amazon SES](../get-started/04-ses.md#
|
|
24
|
+
Avoid turning on Email Routing for a domain that already receives mail elsewhere, such as `example.com` with Google Workspace. Email Routing takes over that domain's MX records. Don't send notifications from `send.example.com` either, since your email provider sends from it. Email Routing there to forward readers' replies, as [Connect Amazon SES](../get-started/04-ses.md#9-receive-replies-at-your-from-address) sets up, is fine, since it only receives.
|
|
25
25
|
|
|
26
26
|
## 2. Verify the notifications address
|
|
27
27
|
|
|
@@ -74,7 +74,7 @@ A `send_email` binding named `NOTIFY` moves notifications to Cloudflare's email.
|
|
|
74
74
|
|
|
75
75
|
## Check it
|
|
76
76
|
|
|
77
|
-
1. In the editor, open **Settings → Notifications**. **Sent through** reads "Cloudflare Email, separate from your newsletter's provider", and **From address** shows `kestrel@newsletter.example.com
|
|
77
|
+
1. In the editor, open **Settings → Notifications**. **Sent through** reads "Cloudflare Email, separate from your newsletter's provider", and **From address** shows `Kestrel <kestrel@newsletter.example.com>`.
|
|
78
78
|
|
|
79
79
|
1. Select **Send a test notification**. It arrives at the notifications address, and **Last notification** shows it as delivered.
|
|
80
80
|
|
|
@@ -13,10 +13,10 @@ This page covers what Kestrel handles, what you handle, and how to act on a read
|
|
|
13
13
|
|
|
14
14
|
## What you handle
|
|
15
15
|
|
|
16
|
-
- **An honest From name and subject line.** The From name should say who the email is from, and the subject should match what's inside. The From name and address are set when Kestrel is deployed ([
|
|
16
|
+
- **An honest From name and subject line.** The From name should say who the email is from, and the subject should match what's inside. The From name and address are set when Kestrel is deployed ([Deploy the app](../get-started/02-deploy.md#4-configure-production)).
|
|
17
17
|
- **Marking ads as ads.** When an email is an advertisement, say so clearly. In a newsletter that's mostly writing, labeling a sponsor's section is enough.
|
|
18
18
|
- **A mailing address, when an email is commercial.** Most newsletters don't need one. [When to provide a mailing address](03-name-your-publication.md#when-to-provide-a-mailing-address) says when you do, beside where you add one.
|
|
19
|
-
- **Requests readers send you.** A reader who replies to a post writes to your From address. Replies reach you once that address forwards to an inbox you read, as [Connect Amazon SES](../get-started/04-ses.md#
|
|
19
|
+
- **Requests readers send you.** A reader who replies to a post writes to your From address. Replies reach you once that address forwards to an inbox you read, as [Connect Amazon SES](../get-started/04-ses.md#9-receive-replies-at-your-from-address) sets up. Kestrel never sees a reply, so carrying out what a reader asks is up to you.
|
|
20
20
|
|
|
21
21
|
## When a reader asks to leave or to be deleted
|
|
22
22
|
|
|
@@ -16,5 +16,5 @@ This is general information, not legal advice. The FTC's [CAN-SPAM compliance gu
|
|
|
16
16
|
|
|
17
17
|
## Who sets the From name and address
|
|
18
18
|
|
|
19
|
-
The From name and address your emails come from are set when Kestrel is deployed, so **Settings → Email sender** shows them read-only. To change them, follow [
|
|
19
|
+
The From name and address your emails come from are set when Kestrel is deployed, so **Settings → Email sender** shows them read-only. To change them, follow [Deploy the app](../get-started/02-deploy.md#4-configure-production).
|
|
20
20
|
|