mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-08 04:57:36 +00:00
config(git,claude,ssh): templated gitconfig; claude settings/commands/hooks/skills; ssh config
gitconfig: name/email from chezmoi data, gh credential helper by PATH lookup (portable to darwin). claude: no credentials, history, projects, sessions, or plugin caches. ssh config as private_ (0600). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c691e8dc5
commit
20891f3910
476 files changed
+59086
-7
No files matched your search
@@ -0,0 +1,285 @@
|
||||
# Email Deliverability & Best Practices
|
||||
|
||||
For full details, see the [deliverability docs](https://developers.cloudflare.com/email-service/concepts/deliverability/) and [email authentication docs](https://developers.cloudflare.com/email-service/concepts/email-authentication/). All the monitoring endpoints below can be called via the [REST API](rest-api.md), [Wrangler CLI, or the Cloudflare MCP server](cli-and-mcp.md).
|
||||
|
||||
## What Cloudflare Handles
|
||||
|
||||
When you onboard a domain, Cloudflare auto-configures:
|
||||
|
||||
- **SPF** — TXT records authorizing Cloudflare's sending infrastructure
|
||||
- **DKIM** — Records for cryptographic signing of outbound emails
|
||||
- **IP reputation** — Managed sending infrastructure optimized for deliverability
|
||||
- **Soft bounce retries** — Automatic exponential backoff for temporary failures
|
||||
- **Suppression lists** — Hard-bounced addresses automatically blocked
|
||||
- **Feedback loops** — ISP complaint signals processed and acted on
|
||||
|
||||
Consider adding a **DMARC** record if you don't have one: `v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@yourdomain.com`
|
||||
|
||||
## Bounce Handling
|
||||
|
||||
**Hard bounces** — permanent failures (address doesn't exist, domain doesn't exist). Never retried. Address auto-added to suppression list. Sending to suppressed address returns `E_RECIPIENT_SUPPRESSED`.
|
||||
|
||||
**Soft bounces** — temporary failures (mailbox full, server down, greylisting). Cloudflare auto-retries with exponential backoff.
|
||||
|
||||
## Suppression Lists
|
||||
|
||||
**Account list** (your account) — spam complaints from recipients. Cloudflare integrates with Postmasters to auto-suppress. You can manually add/remove addresses in the Dashboard.
|
||||
|
||||
See the [suppressions docs](https://developers.cloudflare.com/email-service/concepts/suppressions/) for details.
|
||||
|
||||
## Your Responsibilities
|
||||
|
||||
### Content
|
||||
- Include both HTML and plain text versions
|
||||
- Use a recognizable sender name: `{ email: "noreply@app.com", name: "My App" }`
|
||||
- Write honest subject lines — avoid ALL CAPS, excessive punctuation
|
||||
- Include `List-Unsubscribe` headers for recurring emails
|
||||
- Use full URLs from your domain — avoid URL shorteners
|
||||
|
||||
### List Quality
|
||||
- Validate email addresses before sending
|
||||
- Implement double opt-in for subscriptions
|
||||
- Honor unsubscribe requests promptly
|
||||
|
||||
### Transactional Only
|
||||
Email Service is for **transactional email** (triggered by user actions: signups, password resets, order confirmations). Marketing/bulk campaigns are not permitted — use a dedicated marketing platform.
|
||||
|
||||
## Monitoring Deliverability
|
||||
|
||||
### Dashboard
|
||||
|
||||
Per-domain and account-wide analytics are available in the Cloudflare dashboard:
|
||||
|
||||
1. Log in to the [Cloudflare dashboard](https://dash.cloudflare.com) and select your account.
|
||||
2. Go to **Compute & AI** > **Email Service**.
|
||||
3. Select a domain or view account-wide metrics.
|
||||
4. Select the **Analytics** tab.
|
||||
|
||||
### Send Response
|
||||
|
||||
Every send (REST API or Workers binding) returns immediate delivery feedback. Check the response to track per-send outcomes:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"delivered": ["user@example.com"],
|
||||
"permanent_bounces": ["bad@nonexistent.com"],
|
||||
"queued": ["slow@recipient.com"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Log these to build your own delivery rate metrics.
|
||||
|
||||
### Sending Limits
|
||||
|
||||
Check your account's daily sending quota:
|
||||
|
||||
```bash
|
||||
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/limits" \
|
||||
--header "Authorization: Bearer <API_TOKEN>"
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"result": {
|
||||
"quota": { "value": 5000, "unit": "day" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Suppression List
|
||||
|
||||
Addresses that hard-bounced or received spam complaints are auto-suppressed. You can query and manage suppressions via the API.
|
||||
|
||||
**List suppressions (account-wide):**
|
||||
|
||||
```bash
|
||||
curl "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression?page=1&per_page=100&order=created_at&direction=desc" \
|
||||
--header "Authorization: Bearer <API_TOKEN>"
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"page": 1,
|
||||
"per_page": 100,
|
||||
"total": 2,
|
||||
"result": [
|
||||
{
|
||||
"id": "396a5436-d4b0-42a6-b3fc-48e8fa522321",
|
||||
"email": "bounced@example.com",
|
||||
"reason": "hard_bounce",
|
||||
"created_at": "2026-03-15T10:00:00Z",
|
||||
"expires_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Query params: `page`, `per_page` (max 1000), `order` (`email` | `expires_at` | `created_at`), `direction` (`asc` | `desc`).
|
||||
|
||||
**Manually suppress an address:**
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression" \
|
||||
--header "Authorization: Bearer <API_TOKEN>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{ "email": "user@example.com", "expires_at": "2026-06-01T00:00:00Z" }'
|
||||
```
|
||||
|
||||
`expires_at` is optional — omit for permanent suppression.
|
||||
|
||||
**Remove a suppression:**
|
||||
|
||||
```bash
|
||||
curl -X DELETE "https://api.cloudflare.com/client/v4/accounts/{account_id}/email/sending/suppression/{suppression_id}" \
|
||||
--header "Authorization: Bearer <API_TOKEN>"
|
||||
```
|
||||
|
||||
Zone-level suppressions are also available at `/zones/{zone_id}/email/sending/suppression` with the same interface.
|
||||
|
||||
### GraphQL Analytics API
|
||||
|
||||
Email Service exposes two zone-level datasets via the [GraphQL Analytics API](https://developers.cloudflare.com/analytics/graphql-api/). You can explore the schema interactively at [graphql.cloudflare.com/explorer](https://graphql.cloudflare.com/explorer). Metrics are retained for 31 days.
|
||||
|
||||
| Dataset | Description |
|
||||
|---------|-------------|
|
||||
| `emailSendingAdaptiveGroups` | Aggregated counts grouped by dimensions (status, date, domain, auth results, etc.) |
|
||||
| `emailSendingAdaptive` | Individual email events with full detail (from, to, subject, messageId, errors, etc.) |
|
||||
|
||||
These are **zone-level** datasets — query under `viewer > zones`, not `accounts`.
|
||||
|
||||
**Aggregated dimensions** (`emailSendingAdaptiveGroups`):
|
||||
|
||||
| Dimension | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `date` | Date | Day-level grouping |
|
||||
| `datetime` | Time | Exact timestamp (also: `datetimeMinute`, `datetimeFiveMinutes`, `datetimeFifteenMinutes`, `datetimeHour`) |
|
||||
| `status` | string | Delivery status |
|
||||
| `eventType` | string | Event type |
|
||||
| `sendingDomain` | string | The sending domain |
|
||||
| `envelopeTo` | string | Recipient address |
|
||||
| `errorCause` | string | Error cause for failed sends |
|
||||
| `arc`, `dkim`, `dmarc`, `spf` | string | Email authentication results |
|
||||
| `isSpam`, `isNDR`, `isLastEvent` | uint8 | Boolean flags |
|
||||
| `spamScore`, `spamThreshold` | uint32 | Spam scoring |
|
||||
|
||||
**Individual event fields** (`emailSendingAdaptive`) additionally include: `from`, `to`, `subject`, `messageId`, `sessionId`, `errorDetail`.
|
||||
|
||||
**Email counts by status and date:**
|
||||
|
||||
```graphql
|
||||
query EmailSendingByStatus(
|
||||
$zoneTag: string!
|
||||
$start: Date!
|
||||
$end: Date!
|
||||
) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
emailSendingAdaptiveGroups(
|
||||
filter: { date_geq: $start, date_leq: $end }
|
||||
limit: 10000
|
||||
orderBy: [date_DESC]
|
||||
) {
|
||||
count
|
||||
dimensions {
|
||||
date
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Filter by status (e.g. only failures):**
|
||||
|
||||
```graphql
|
||||
query EmailFailures(
|
||||
$zoneTag: string!
|
||||
$start: Date!
|
||||
$end: Date!
|
||||
) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
emailSendingAdaptiveGroups(
|
||||
filter: { date_geq: $start, date_leq: $end, status: "deliveryFailed" }
|
||||
limit: 10000
|
||||
orderBy: [date_DESC]
|
||||
) {
|
||||
count
|
||||
dimensions {
|
||||
date
|
||||
errorCause
|
||||
sendingDomain
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Individual email events (troubleshooting):**
|
||||
|
||||
```graphql
|
||||
query RecentEmailEvents(
|
||||
$zoneTag: string!
|
||||
$start: Time!
|
||||
$end: Time!
|
||||
) {
|
||||
viewer {
|
||||
zones(filter: { zoneTag: $zoneTag }) {
|
||||
emailSendingAdaptive(
|
||||
filter: { datetime_geq: $start, datetime_leq: $end }
|
||||
limit: 50
|
||||
orderBy: [datetime_DESC]
|
||||
) {
|
||||
datetime
|
||||
from
|
||||
to
|
||||
subject
|
||||
status
|
||||
eventType
|
||||
sendingDomain
|
||||
messageId
|
||||
errorCause
|
||||
errorDetail
|
||||
dkim
|
||||
dmarc
|
||||
spf
|
||||
isSpam
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: `emailSendingAdaptive` filters use `datetime_geq`/`datetime_leq` (Time type, e.g. `"2026-04-01T00:00:00Z"`), while `emailSendingAdaptiveGroups` uses `date_geq`/`date_leq` (Date type, e.g. `"2026-04-01"`).
|
||||
|
||||
**curl example:**
|
||||
|
||||
```bash
|
||||
curl "https://api.cloudflare.com/client/v4/graphql" \
|
||||
--header "Authorization: Bearer <API_TOKEN>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"query": "query($zoneTag:string!,$start:Date!,$end:Date!){viewer{zones(filter:{zoneTag:$zoneTag}){emailSendingAdaptiveGroups(filter:{date_geq:$start,date_leq:$end},limit:10000,orderBy:[date_DESC]){count,dimensions{date,status}}}}}",
|
||||
"variables": {
|
||||
"zoneTag": "<ZONE_ID>",
|
||||
"start": "2026-03-15",
|
||||
"end": "2026-04-15"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Metrics to Watch
|
||||
|
||||
| Metric | Target | If Out of Range |
|
||||
|--------|--------|-----------------|
|
||||
| Delivery rate | > 95% | Check for invalid addresses; verify DNS records |
|
||||
| Hard bounce rate | < 2% | Clean your email list |
|
||||
| Complaint rate | < 0.1% | Make unsubscribe easier; stop unwanted emails |
|
||||
Reference in new issue
Block a user