# Email Setup From "help me set up email" to an app that sends mail through Amazon SES. Start step 1 as soon as the user asks. This is AWS whatever the app is deployed on; another mail provider only when the user names one, and then it is the four `SMTP_` lines in `production.env` (see `elements man email`). What to say: only the quoted lines on this page, word for word. Nothing else reaches the user: no record names, keys, file names, config, tool names, reasoning, or what comes next. How to run: each block is one command and stands alone. It reads what it needs from `.elements/id`, the env file and AWS, so nothing carries over in shell variables, and every `aws` call names `--profile elements`. No scripts around them, no `set -e`, no `exit`. ## 1. Sign In ```bash Terminal aws sts get-caller-identity --profile elements --query Account --output text ``` An account number means signed in. Anything else: do step 1 of [AWS](aws), then come back here. ## 2. One Question Ask with a question tool when there is one (options "Just testing" and "Real users"). Otherwise send it as its own message: > Is this email for testing, or for real users? Testing sends from the user's own address to the user's own address, and to nobody else. Real users needs a domain the user owns. ## 3. Testing > What email address should the test mail go to? Then, with that address: ```bash Terminal aws sesv2 create-email-identity --profile elements --email-identity
--query VerifiedForSendingStatus --output text ``` > Amazon sent you an email. Click the link in it, then come back. ```bash Terminal aws sesv2 get-email-identity --profile elements --email-identity
--query VerifiedForSendingStatus --output text ``` `True` means verified; `False` means the link is not clicked yet, so say the line above again. The address is also the `from` in step 5. Go to step 5. ## 4. Real Users A domain in `deploy.domain` that is not under elements.online is the domain. Otherwise: > Which domain should your email come from? It needs to be one you own. A user without one gets step 3 instead, with this first: > Without your own domain, mail lands in spam or is refused. Let's set up > testing for now. With the domain, this asks Amazon for its signing records and prints where the domain's DNS is hosted: ```bash Terminal D= aws sesv2 create-email-identity --profile elements --email-identity $D >/dev/null 2>&1 for t in $(aws sesv2 get-email-identity --profile elements --email-identity $D --query 'DkimAttributes.Tokens' --output text); do echo "CNAME $t._domainkey.$D $t.dkim.amazonses.com"; done [ -z "$(dig +short TXT _dmarc.$D)" ] && echo "TXT _dmarc.$D \"v=DMARC1; p=none;\"" Z=$(dig +noall +answer +authority SOA $D | awk '$4=="SOA" {print $1; exit}'); echo "zone ${Z%.} $(dig +short NS $Z | head -1)" ``` The last line names the DNS zone and its name server, which says which of these runs. A record that already exists is left as it is. `awsdns` (Route 53): ```bash Terminal D=; ZN= Z=$(aws route53 list-hosted-zones-by-name --profile elements --dns-name $ZN --query "HostedZones[?Name=='$ZN.'].Id" --output text) for t in $(aws sesv2 get-email-identity --profile elements --email-identity $D --query 'DkimAttributes.Tokens' --output text); do aws route53 change-resource-record-sets --profile elements --hosted-zone-id $Z --change-batch "{\"Changes\":[{\"Action\":\"UPSERT\",\"ResourceRecordSet\":{\"Name\":\"$t._domainkey.$D\",\"Type\":\"CNAME\",\"TTL\":1800,\"ResourceRecords\":[{\"Value\":\"$t.dkim.amazonses.com\"}]}}]}" >/dev/null done [ -z "$(dig +short TXT _dmarc.$D)" ] && aws route53 change-resource-record-sets --profile elements --hosted-zone-id $Z --change-batch "{\"Changes\":[{\"Action\":\"CREATE\",\"ResourceRecordSet\":{\"Name\":\"_dmarc.$D\",\"Type\":\"TXT\",\"TTL\":1800,\"ResourceRecords\":[{\"Value\":\"\\\"v=DMARC1; p=none;\\\"\"}]}}]}" >/dev/null echo done ``` `digitalocean.com`, with `doctl` signed in (see [DigitalOcean](digitalocean)): ```bash Terminal D=; ZN=; n() { x=$1.$D.; echo ${x%.$ZN.}; } for t in $(aws sesv2 get-email-identity --profile elements --email-identity $D --query 'DkimAttributes.Tokens' --output text); do doctl compute domain records create $ZN --context elements --record-type CNAME --record-name $(n $t._domainkey) --record-data $t.dkim.amazonses.com. --record-ttl 1800 >/dev/null done [ -z "$(dig +short TXT _dmarc.$D)" ] && doctl compute domain records create $ZN --context elements --record-type TXT --record-name $(n _dmarc) --record-data 'v=DMARC1; p=none;' --record-ttl 1800 >/dev/null echo done ``` Anything else: the user adds them. Fill in the records the first block printed, as a table of type, name and value: > Add these records where you manage 's DNS, then come back. Then ask Amazon to let the account send to anyone. A new account can only send to addresses it has verified until Amazon reviews it: ```bash Terminal D= aws sesv2 put-account-details --profile elements --production-access-enabled --mail-type TRANSACTIONAL --website-url https://$D --contact-language EN \ --use-case-description "Transactional mail only (sign-up confirmation, password reset, account notifications) to people who sign up at https://$D. No marketing, no purchased lists. Bounces and complaints go to the account suppression list." ``` Wait for the records. Each run waits at most four minutes; run it again until it prints `SUCCESS`: ```bash Terminal D= for i in 1 2 3 4 5 6 7 8; do S=$(aws sesv2 get-email-identity --profile elements --email-identity $D --query 'DkimAttributes.Status' --output text); [ "$S" = SUCCESS ] && break; sleep 30; done echo $S ``` The `from` in step 5 is `no-reply@`. ## 5. Connect the App Set `email.from` in `config.jsoc` to the address. Then this makes a sending key that can do nothing but send mail and writes it to `production.env`: ```bash Terminal APP=$(cat .elements/id); R=$(aws configure get region --profile elements); F=config/env/production.env aws iam create-user --profile elements --user-name elements-$APP-email --tags Key=elements:app,Value=$APP >/dev/null 2>&1 aws iam put-user-policy --profile elements --user-name elements-$APP-email --policy-name send --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"ses:SendRawEmail","Resource":"*"}]}' read -r KEY SECRET <<< "$(aws iam create-access-key --profile elements --user-name elements-$APP-email --query 'AccessKey.[AccessKeyId,SecretAccessKey]' --output text)" k=$(printf %s 11111111 | openssl dgst -sha256 -mac HMAC -macopt "key:AWS4$SECRET" | awk '{print $NF}') for m in $R ses aws4_request SendRawEmail; do k=$(printf %s $m | openssl dgst -sha256 -mac HMAC -macopt hexkey:$k | awk '{print $NF}'); done touch $F; grep -v -e '^EMAIL_LIVE=' -e '^SMTP_' $F > $F.tmp; mv $F.tmp $F printf 'EMAIL_LIVE=true\nSMTP_HOST=email-smtp.%s.amazonaws.com\nSMTP_PORT=587\nSMTP_USER=%s\nSMTP_PASSWORD=%s\n' $R $KEY "$({ printf '\004'; printf %s $k | xxd -r -p; } | base64)" >> $F echo ok ``` The password is derived from the key, the way Amazon's mail servers expect. It lives only in `production.env`. Development keeps logging mail to the server log instead of sending it. ## 6. Look at Them With templates in `app/emails`, each gets a preview route, the way `elements man email` shows, with sample values. Look at each at desktop and phone width and fix what breaks the rules in `elements man style/email`. Then open each one in the user's browser, one at a time: > Here's your email. Anything you'd like changed? Change what the user asks for and reload. With no templates, skip this step. ## 7. Send One Testing only, to the user's address. A new key takes a minute or two to work; on `535` wait 30 seconds and run it again. ```bash Terminal A=
; F=config/env/production.env; v() { grep "^$1=" $F | cut -d= -f2-; } printf 'From: %s\r\nTo: %s\r\nSubject: Email works\r\n\r\nYour app can send email.\r\n' $A $A | curl -sS --url smtp://$(v SMTP_HOST):587 --ssl-reqd --user "$(v SMTP_USER):$(v SMTP_PASSWORD)" --mail-from $A --mail-rcpt $A -T - && echo sent ``` ## 8. Deploy With machines in `config.jsoc`: ```bash Terminal elements deploy -json ``` No errors, testing: > Email is set up. Check your inbox for a test message. Your app sends to > you only, until you set up email for real users. No errors, real users: > Email is set up, from no-reply@. Amazon reviews new senders, > usually within a day, and emails you when it's done. Until then, mail > only reaches addresses you've verified. With no machines yet, the same line without deploying; the next deploy carries it. ## Teardown Confirm with the user first. ```bash Terminal APP=$(cat .elements/id); U=elements-$APP-email for k in $(aws iam list-access-keys --profile elements --user-name $U --query 'AccessKeyMetadata[].AccessKeyId' --output text); do aws iam delete-access-key --profile elements --user-name $U --access-key-id $k; done aws iam delete-user-policy --profile elements --user-name $U --policy-name send aws iam delete-user --profile elements --user-name $U F=config/env/production.env; grep -v -e '^EMAIL_LIVE=' -e '^SMTP_' $F > $F.tmp; mv $F.tmp $F ``` Then `aws sesv2 delete-email-identity --profile elements --email-identity
`, and the DNS records by hand.