### Install Posthorn from source Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/binary.mdx Install the Posthorn binary using `go install`. Requires Go 1.25+. ```bash go install github.com/craigmccaskill/posthorn/cmd/posthorn@latest ``` -------------------------------- ### Run Posthorn CLI commands Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/binary.mdx Examples of using the `validate` and `serve` subcommands for configuration validation and starting the HTTP listener. ```bash posthorn validate --config /etc/posthorn/config.toml posthorn serve --config /etc/posthorn/config.toml ``` -------------------------------- ### Posthorn Startup Log Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/getting-started/quick-start.mdx Example of structured log output from Posthorn upon successful startup, indicating the version, listening address, and configuration path. ```json {"time":"2026-05-16T20:00:00Z","level":"INFO","msg":"posthorn starting","version":"v1.0.0","listen":":8080","config":"/etc/posthorn/config.toml","endpoints":1} {"time":"2026-05-16T20:00:00Z","level":"INFO","msg":"endpoint registered","path":"/api/contact","transport":"postmark","recipients":1} {"time":"2026-05-16T20:00:00Z","level":"INFO","msg":"http ingress listening","addr":":8080"} ``` -------------------------------- ### Build Docs Site Source: https://github.com/craigmccaskill/posthorn/blob/main/CONTRIBUTING.md Install dependencies and build the documentation site. This command should be run from the 'site' directory. ```bash cd site && npm ci && npm run build ``` -------------------------------- ### Install Posthorn from a release artifact Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/binary.mdx Download, extract, and install the pre-built binary for Linux AMD64 from a GitHub release. ```bash curl -L -o posthorn.tar.gz \ https://github.com/craigmccaskill/posthorn/releases/download/v1.0.0/posthorn-linux-amd64.tar.gz tar -xzf posthorn.tar.gz sudo install -m 0755 posthorn /usr/local/bin/posthorn ``` -------------------------------- ### Serve Command with Config Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/cli.mdx Starts the posthorn listener using a specified configuration file. ```bash posthorn serve --config /etc/posthorn/config.toml ``` -------------------------------- ### Start Posthorn Container (First Endpoint) Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/recipes/newsletter-signup.mdx Start the Posthorn Docker container if this is the first endpoint being configured. ```bash docker compose up -d ``` -------------------------------- ### Verify Standalone Binary Installation Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/getting-started/installation.mdx If you installed Posthorn as a standalone binary, use this command to check its version. ```bash posthorn version ``` -------------------------------- ### API Request Example (cURL) Source: https://github.com/craigmccaskill/posthorn/blob/main/README.md Example of how to send a POST request to the API endpoint using cURL. Includes setting authorization, content type, idempotency key, and JSON payload. ```bash curl -X POST https://posthorn.yourdomain.com/api/transactional \ -H "Authorization: Bearer $WORKER_KEY_PRIMARY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: reset:user-123:$(date -u +%FT%H)" \ --data '{ \ "to_override": "alice@example.com", \ "subject_line": "Reset your password", \ "message": "Click here: https://app.example.com/reset/abc" \ }' ``` -------------------------------- ### Verify Docker Installation Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/getting-started/installation.mdx Run this command to check the version of the Posthorn Docker container and verify the installation. ```bash docker run --rm ghcr.io/craigmccaskill/posthorn:latest version ``` -------------------------------- ### Verify Posthorn installation Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/binary.mdx Check the installed Posthorn version. ```bash posthorn --version # posthorn v1.0.0 ``` -------------------------------- ### Start and Monitor Posthorn Container Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/getting-started/quick-start.mdx Start the Posthorn container in detached mode and follow its logs to observe startup messages and endpoint registration. ```bash docker compose up -d docker compose logs -f posthorn ``` -------------------------------- ### Logrotate Configuration for Posthorn Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/reading-logs.mdx Example logrotate configuration to manage the size and retention of Posthorn log files. ```text # /etc/logrotate.d/posthorn /var/log/posthorn.log { daily rotate 14 compress missingok notifempty copytruncate } ``` -------------------------------- ### Build and Run Posthorn Server Source: https://github.com/craigmccaskill/posthorn/blob/main/docs/manual-test.md Builds the Posthorn application and starts it in the background, serving the configured endpoints. It then validates the configuration and waits for the server to be ready. ```bash cd go build -o /tmp/posthorn-manual/posthorn ./core/cmd/posthorn cd /tmp/posthorn-manual ./posthorn validate --config posthorn.toml # expect: exit 0 ./posthorn serve --config posthorn.toml & SERVE_PID=$! sleep 1 ``` -------------------------------- ### Posthorn environment file Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/binary.mdx Example environment file for Posthorn, used to set environment variables like API keys. ```bash POSTMARK_API_KEY=your-postmark-server-token ``` -------------------------------- ### Set Environment Variables with systemd Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/security/api-keys.mdx Example of setting environment variables for Posthorn using systemd's EnvironmentFile directive. ```bash # systemd EnvironmentFile=/etc/posthorn/posthorn.env ``` -------------------------------- ### Start Docker Compose and Monitor Posthorn Logs Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/recipes/gitea.mdx Brings up the Docker services and tails the Posthorn logs to verify the SMTP listener registration and incoming mail. ```bash docker compose up -d docker compose logs -f posthorn ``` ```json {"msg":"smtp_listener registered","listen":":2525","transport":"postmark","smtp_users":0} {"msg":"smtp ingress listening","addr":":2525"} ``` ```bash docker compose logs -f posthorn | grep -E 'smtp_|submission_' ``` -------------------------------- ### Trusted Proxies Configuration Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/configuration/reference.mdx Example of configuring trusted proxies using a mix of preset names and explicit CIDR ranges. ```toml trusted_proxies = ["cloudflare", "10.0.0.0/8"] ``` -------------------------------- ### Configure Transport API Key Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/security/api-keys.mdx Example of configuring a transport API key using an environment variable placeholder in TOML. ```toml # Transport credential [endpoints.transport.settings] api_key = "${env.POSTMARK_API_KEY}" ``` -------------------------------- ### Run Posthorn Commands Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/getting-started/installation.mdx Commands to validate the configuration file or start the Posthorn HTTP listener. Use the --config flag to specify your TOML configuration. ```bash posthorn validate --config posthorn.toml # check the config without serving posthorn serve --config posthorn.toml # start the HTTP listener ``` -------------------------------- ### Prepare Working Directory Source: https://github.com/craigmccaskill/posthorn/blob/main/docs/manual-test.md Creates a new directory for the manual test and navigates into it. This is the first step in setting up the test environment. ```bash mkdir -p /tmp/posthorn-manual cd /tmp/posthorn-manual ``` -------------------------------- ### Example of From Address with Placeholder Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/configuration/environment-variables.mdx Demonstrates using an environment variable for the 'from' address, allowing it to change across different environments while maintaining a consistent configuration structure. ```toml from = "Contact <${env.NOREPLY_ADDR}>" ``` -------------------------------- ### Build Posthorn from Source Source: https://github.com/craigmccaskill/posthorn/blob/main/README.md Instructions to clone the Posthorn repository, navigate to the core directory, build the binary, and verify the version. Requires Go 1.25+. ```bash git clone https://github.com/craigmccaskill/posthorn cd posthorn/core go build -o /tmp/posthorn ./cmd/posthorn /tmp/posthorn version ``` -------------------------------- ### Run Test Suite and Build Binary Source: https://github.com/craigmccaskill/posthorn/blob/main/CONTRIBUTING.md Execute the test suite with race detection and build the Posthorn binary. Ensure you are in the 'core' directory before running tests. ```bash cd core && go test -race ./... go build -o posthorn ./cmd/posthorn ./posthorn version ``` -------------------------------- ### Example Weekly Umami Digest Email Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/recipes/umami.mdx This is an example of the email digest that will be sent. It includes key metrics from Umami analytics for a specific week. ```text Subject: Weekly Umami digest for example.com (2026-W22) Pageviews: 12,847 Visitors: 3,201 Visits: 4,886 Bounce rate: 41.2% Avg time: 1m 38s ``` -------------------------------- ### Posthorn Starting Event Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/log-format.mdx Log event emitted once at the start of the Posthorn process. Includes version, listening address, configuration path, and number of endpoints. ```json { "level": "INFO", "msg": "posthorn starting", "version": "v1.0.0", "listen": ":8080", "config": "/etc/posthorn/config.toml", "endpoints": 2 } ``` -------------------------------- ### Complete Posthorn Configuration Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/configuration/reference.mdx This TOML configuration demonstrates how to set up multiple HTTP endpoints for contact forms and transactional APIs, as well as an SMTP listener for services like Ghost or Gitea. It includes logging, endpoint-specific settings, transport configurations, rate limiting, and SMTP listener options. ```toml # Two HTTP endpoints (a contact form + a transactional api-mode endpoint) # plus an SMTP listener for Ghost/Gitea/etc. [logging] level = "info" format = "json" [[endpoints]] path = "/api/contact" to = ["alerts@example.com"] from = "Contact Form " required = ["name", "email", "message"] email_field = "email" honeypot = "_gotcha" allowed_origins = ["https://example.com", "https://www.example.com"] max_body_size = "64KB" trusted_proxies = ["cloudflare"] csrf_secret = "${env.CSRF_SECRET}" csrf_token_ttl = "30m" subject = "Contact: {{.name}}" body = """ From: {{.name}} <{{.email}}> {{.message}} """ redirect_success = "/thank-you" redirect_error = "/contact?error=1" [endpoints.transport] type = "postmark" [endpoints.transport.settings] api_key = "${env.POSTMARK_CONTACT_KEY}" [endpoints.rate_limit] count = 5 interval = "1m" # --- [[endpoints]] path = "/api/transactional" to = ["fallback@example.com"] from = "App " auth = "api-key" api_keys = ["${env.WORKER_KEY_PRIMARY}", "${env.WORKER_KEY_BACKUP}"] idempotency_cache_size = 5000 required = ["subject_line", "body"] subject = "{{.subject_line}}" body = "{{.body}}" [endpoints.transport] type = "resend" [endpoints.transport.settings] api_key = "${env.RESEND_API_KEY}" [endpoints.rate_limit] count = 100 interval = "1m" # --- [smtp_listener] listen = ":2525" require_tls = true tls_cert = "/etc/posthorn/cert.pem" tls_key = "/etc/posthorn/key.pem" auth_required = "smtp-auth" allowed_senders = ["*@example.com"] max_recipients_per_session = 10 max_message_size = "1MB" [[smtp_listener.smtp_users]] username = "ghost" password = "${env.GHOST_SMTP_PASSWORD}" [smtp_listener.transport] type = "postmark" [smtp_listener.transport.settings] api_key = "${env.POSTMARK_API_KEY}" ``` -------------------------------- ### Build Posthorn from the repository Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/binary.mdx Clone the Posthorn repository and build the binary locally. ```bash git clone https://github.com/craigmccaskill/posthorn cd posthorn go build -o posthorn ./core/cmd/posthorn ``` -------------------------------- ### 500 Internal Server Error Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/response-codes.mdx This is an example of a 500 Internal Server Error response. It indicates a Posthorn-internal failure, such as a template rendering issue or another server-side error not caused by the upstream service. The body is plain text. ```http HTTP/1.1 500 Internal Server Error Content-Type: text/plain; charset=utf-8 submission could not be processed ``` -------------------------------- ### Create Project Directory Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/getting-started/quick-start.mdx Create a new directory for your Posthorn project and navigate into it. ```bash mkdir posthorn-test && cd posthorn-test ``` -------------------------------- ### send_retry_succeeded Log Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/log-format.mdx Logs when a retried submission attempt is successful. ```json { "level": "INFO", "msg": "send_retry_succeeded", "submission_id": "...", "endpoint": "/api/contact", "transport": "postmark" } ``` -------------------------------- ### Posthorn Startup Log Output Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/recipes/multi-form-site.mdx Example log output from Posthorn after a restart, showing successful registration of endpoints and the HTTP ingress listener. This confirms that Posthorn is running and ready to accept form submissions. ```json {"time":"...","level":"INFO","msg":"endpoint registered","path":"/api/contact","transport":"postmark","recipients":1} {"time":"...","level":"INFO","msg":"endpoint registered","path":"/api/feedback","transport":"postmark","recipients":1} {"time":"...","level":"INFO","msg":"endpoint registered","path":"/api/careers","transport":"postmark","recipients":1} {"time":"...","level":"INFO","msg":"http ingress listening","addr":":8080"} ``` -------------------------------- ### submission_sent Log Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/log-format.mdx Logs when the transport successfully accepts a message submission. ```json { "level": "INFO", "msg": "submission_sent", "submission_id": "...", "endpoint": "/api/contact", "transport": "postmark", "latency_ms": 312, "transport_message_id": "abc123-postmark-id" } ``` -------------------------------- ### Posthorn Application Lifecycle Source: https://github.com/craigmccaskill/posthorn/blob/main/spec/03-architecture.md Describes the sequence of operations from application startup, configuration loading, request handling setup, to graceful shutdown upon receiving termination signals. ```go 1. main.go: parse CLI flags (subcommand: serve | validate) 2. config.Load(path): read TOML, resolve ${env.VAR}, validate schema 3. For each endpoint in config: a. Construct transport (Postmark client with API key) b. Compile templates (subject, body) c. Construct rate limiter d. Construct core.Handler with all of the above 4. Build http.ServeMux mapping each endpoint path to its core.Handler 5. http.Server.ListenAndServe() 6. On SIGTERM/SIGINT: a. Stop accepting new connections (server.Shutdown) b. Drain in-flight requests up to 10s per-request timeout c. Exit 0 On second signal: forced exit ``` -------------------------------- ### HTTP Success Response (JSON) Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/http-api.mdx Example of a successful submission response with a JSON body. ```http HTTP/1.1 200 OK Content-Type: application/json { "status": "ok", "submission_id": "7f2c84d6-9b1e-4c2f-a3b8-1a2b3c4d5e6f" } ``` -------------------------------- ### Posthorn Template Syntax Examples Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/features/templating.mdx Illustrates common Go text/template syntax used in Posthorn for email templating, including variables, pipes, conditionals, and comments. ```text/template {{.name}} ``` ```text/template {{.email | html}} ``` ```text/template {{if .urgent}}!!!{{end}} ``` ```text/template {{if eq .category "bug"}} ``` ```text/template {{/* this is a comment */}} ``` -------------------------------- ### jq Queries for Ad-hoc Log Analysis Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/log-format.mdx Examples of using jq with Docker logs for ad-hoc analysis of Posthorn logs. These queries demonstrate tailing logs, filtering by level, analyzing latency, and searching for specific IDs. ```bash # Tail and pretty-print docker logs -f posthorn | jq . ``` ```bash # All failures from the last hour docker logs --since 1h posthorn | jq 'select(.level=="ERROR")' ``` ```bash # Latency P99 over the last 1000 sent submissions docker logs --tail 10000 posthorn \ | jq -s 'map(select(.msg=="submission_sent")) | sort_by(.latency_ms) | .[-10:]' ``` ```bash # Find a specific submission across all log lines docker logs posthorn | jq 'select(.submission_id=="7f2c84d6-9b1e-4c2f-a3b8-1a2b3c4d5e6f")' ``` ```bash # Find a specific SMTP session across all log lines docker logs posthorn | jq 'select(.session_id=="...")' ``` -------------------------------- ### submission_failed Log Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/log-format.mdx Logs when a submission fails terminally due to errors, timeouts, or exhausted retries. ```json { "level": "ERROR", "msg": "submission_failed", "submission_id": "...", "endpoint": "/api/contact", "transport": "postmark", "error": "postmark: 422 The 'From' address is not a valid Sender Signature.", "form": { "name": "Alice Smith", "email": "alice@example.com", "message": "..." }, "latency_ms": 287 } ``` -------------------------------- ### HTTP Success Response (Redirect) Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/http-api.mdx Example of a successful submission response when a redirect is configured and HTML is preferred. ```http HTTP/1.1 303 See Other Location: /thank-you ``` -------------------------------- ### Path Conflict Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/configuration/endpoints.mdx Illustrates a configuration error where two endpoints are defined with the same path, which will be rejected during validation. ```toml [[endpoints]] path = "/api/contact" [[endpoints]] path = "/api/contact" # ← rejected: duplicate path ``` -------------------------------- ### Kubernetes: All Pods in Deployment (Multi-Replica) Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/reading-logs.mdx Stream logs from all pods in a multi-replica deployment, with prefixes, using `kubectl logs -f --selector app=posthorn --max-log-requests=10 --prefix`. ```bash kubectl logs -f --selector app=posthorn --max-log-requests=10 --prefix ``` -------------------------------- ### Manage Posthorn systemd service Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/binary.mdx Commands to reload systemd, enable and start the Posthorn service, and check its status. ```bash sudo systemctl daemon-reload sudo systemctl enable --now posthorn sudo systemctl status posthorn ``` -------------------------------- ### Registering a Transport Source: https://github.com/craigmccaskill/posthorn/blob/main/spec/03-architecture.md Demonstrates the transport registry pattern where each transport registers itself with a type, validator, and factory function during initialization. ```go transport.Register("resend", resend.ValidateConfig, resend.New) transport.Register("mailgun", mailgun.ValidateConfig, mailgun.New) transport.Register("ses", ses.ValidateConfig, ses.New) transport.Register("smtpout", smtpout.ValidateConfig, smtpout.New) ``` -------------------------------- ### submission_failed Log Example with form_fields Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/log-format.mdx Shows the structure of a submission_failed log when the 'form' field is redacted to 'form_fields' for privacy. ```json { "...": "...", "form_fields": ["name", "email", "message"] } ``` -------------------------------- ### send_retry_failed Log Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/log-format.mdx Indicates that a retry attempt for a submission also failed, marking the end of retries for this submission. ```json { "level": "INFO", "msg": "send_retry_failed", "submission_id": "...", "endpoint": "/api/contact", "transport": "postmark", "error": "postmark: 422 The 'From' address is not a valid Sender Signature." } ``` -------------------------------- ### Standalone Binary: Live JSON Pretty-Print Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/reading-logs.mdx Pipe the output of `posthorn serve` to `jq .` for pretty-printed JSON logs. ```bash posthorn serve --config posthorn.toml | jq . ``` -------------------------------- ### Display Posthorn CLI Help Information Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/cli.mdx Use the `help` command to view general usage instructions and a list of available subcommands. This command is aliased to `--help` and `-h`. Subcommand-specific help is not available separately. ```bash $ posthorn help posthorn — outbound mail gateway Usage: posthorn serve [--config ] [--listen ] posthorn validate [--config ] posthorn version posthorn help Examples: posthorn serve --config /etc/posthorn/config.toml posthorn validate --config ./posthorn.toml ``` -------------------------------- ### send_retry_scheduled Log Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/log-format.mdx Indicates that a transport returned a retryable error and Posthorn is preparing to retry the submission after a delay. ```json { "level": "INFO", "msg": "send_retry_scheduled", "submission_id": "...", "endpoint": "/api/contact", "transport": "postmark", "class": "transient", "status": 503, "delay": 1000000000 } ``` -------------------------------- ### Create Posthorn Directory Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/recipes/contact-form.mdx Creates a directory for the Posthorn deployment and navigates into it. ```bash mkdir -p posthorn && cd posthorn ``` -------------------------------- ### Set Environment Variables Directly Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/security/api-keys.mdx Example of setting an environment variable directly when running Posthorn from the command line. ```bash # direct POSTMARK_API_KEY=abc123 posthorn serve --config ... ``` -------------------------------- ### Set Environment Variables with Docker Compose Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/security/api-keys.mdx Example of setting environment variables for Posthorn using Docker Compose. ```bash # Docker Compose docker compose --env-file .env up -d ``` -------------------------------- ### Standalone Binary: Append to File Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/deployment/reading-logs.mdx Run the Posthorn server and redirect its stdout and stderr to a log file using `>> /var/log/posthorn.log 2>&1 &`. ```bash posthorn serve --config posthorn.toml >> /var/log/posthorn.log 2>&1 & ``` -------------------------------- ### API Mode Response Example Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/reference/http-api.mdx This is a successful response format for API mode requests. It includes the status and submission ID. ```http HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 {"status":"ok","submission_id":"..."} ``` -------------------------------- ### Configure SMTP User Password Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/security/api-keys.mdx Example of configuring an SMTP user password using an environment variable placeholder in TOML. ```toml # SMTP user password [[smtp_listener.smtp_users]] username = "ghost" password = "${env.GHOST_SMTP_PASSWORD}" ``` -------------------------------- ### Common SES Gotchas and Fixes Source: https://github.com/craigmccaskill/posthorn/blob/main/site/src/content/docs/configuration/transports/ses.mdx Lists common issues encountered when using SES, their likely causes, and recommended solutions. ```markdown ## Common gotchas | Symptom | |---|---| | `4xx MessageRejected: Email address not verified` | Sandbox mode + unverified recipient | | `403 SignatureDoesNotMatch` | Clock skew — SigV4 timestamps must be within 15 minutes of AWS's clock | | `403 InvalidClientTokenId` | Access key ID wrong, or IAM user disabled | | Mail goes to recipient's spam | DKIM CNAMEs not published, or DMARC alignment wrong | ```