Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,13 @@ export default defineConfig({
{ text: 'Wireguard', link: '/services/vpn/wireguard' },
]
},
{
text: 'Transactional Mail',
collapsed: true,
items: [
{ text: 'Amazon SES', link: '/services/transactional-mail/amazon-ses' },
]
},
]
},
{
Expand Down
189 changes: 189 additions & 0 deletions docs/services/transactional-mail/amazon-ses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
---
outline: deep
---

# Amazon SES — Sending from Python and JavaScript

## Project Configuration

- SES Region: `eu-north-1` (Europe, Stockholm)
- Verified domain: `fastcomputing.net`
- Example sender: `noreply@fastcomputing.net`
- Recommended API: Amazon SES v2

## Authentication

Do not put AWS credentials in the code or in the repository.

### Local Development

Log in with the AWS CLI:

```sh
aws login
```

This opens a browser to authenticate and caches temporary credentials locally. Boto3 reads them automatically through the AWS CLI credential chain, so there is no need to export `AWS_ACCESS_KEY_ID` or `AWS_SECRET_ACCESS_KEY`.

Set the region for the session:

```sh
export AWS_DEFAULT_REGION="eu-north-1"
```

On Windows PowerShell:

```powershell
$env:AWS_DEFAULT_REGION="eu-north-1"
```

### EC2

Attach an IAM role authorized to send with SES to the instance. Do not copy the local access keys onto the server: the SDKs automatically retrieve temporary credentials from the EC2 role.

Minimal IAM permissions:

```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ses:SendEmail",
"ses:SendRawEmail"
],
"Resource": "arn:aws:ses:eu-north-1:ACCOUNT_ID:identity/fastcomputing.net"
}
]
}
```

Replace `ACCOUNT_ID` with the numeric ID of the AWS account.

If a configuration set is specified or applied during sending, also add its ARN to `Resource`, for example:

```
arn:aws:ses:eu-north-1:ACCOUNT_ID:configuration-set/CONFIGURATION_SET_NAME
```

## Python

Installation:

```sh
pip install boto3
```

Sending:

```python
import boto3

ses = boto3.client("sesv2", region_name="eu-north-1")

response = ses.send_email(
FromEmailAddress="Fast Computing <noreply@fastcomputing.net>",
Destination={
"ToAddresses": ["recipient@example.com"],
},
ReplyToAddresses=["nicola.demo@fastcomputing.net"],
Content={
"Simple": {
"Subject": {
"Data": "Test email",
"Charset": "UTF-8",
},
"Body": {
"Text": {
"Data": "This email was sent via Amazon SES.",
"Charset": "UTF-8",
},
"Html": {
"Data": (
"<h1>Test email</h1>"
"<p>This email was sent via Amazon SES.</p>"
),
"Charset": "UTF-8",
},
},
}
},
)

print("Email sent:", response["MessageId"])
```

If Botocore reports that the login provider dependency is missing:

```sh
pip install "botocore[crt]"
```

## JavaScript / Node.js

Installation:

```sh
npm install @aws-sdk/client-sesv2
```

Sending:

```js
import {
SESv2Client,
SendEmailCommand,
} from "@aws-sdk/client-sesv2";

const ses = new SESv2Client({
region: "eu-north-1",
});

const response = await ses.send(new SendEmailCommand({
FromEmailAddress: "Fast Computing <noreply@fastcomputing.net>",
Destination: {
ToAddresses: ["recipient@example.com"],
},
ReplyToAddresses: ["nicola.demo@fastcomputing.net"],
Content: {
Simple: {
Subject: {
Data: "Test email",
Charset: "UTF-8",
},
Body: {
Text: {
Data: "This email was sent via Amazon SES.",
Charset: "UTF-8",
},
Html: {
Data: `
<h1>Test email</h1>
<p>This email was sent via Amazon SES.</p>
`,
Charset: "UTF-8",
},
},
},
},
}));

console.log("Email sent:", response.MessageId);
```

To use `import`, add the following to `package.json`:

```json
{
"type": "module"
}
```

## Notes

- The sender must belong to the verified domain.
- The region in the code and in the IAM policy must be `eu-north-1`.
- Do not pass `ConfigurationSetName` unless it is necessary.
- Store and monitor the `MessageId` returned by SES.
- Handle bounces and complaints separately before increasing volumes.