Setup
On the machine that starts s3mail: nothing. No web server, no database, no Docker, no runtime. s3mail is a single program, brings its own server and binds it to 127.0.0.1. The only outbound connections are HTTPS to s3.<region>.amazonaws.com, email.<region>.amazonaws.com and, if encrypted, kms.<region>.amazonaws.com.
- Package: macOS (Apple Silicon and Intel, signed and notarised), Linux and Windows, each amd64/arm64, from the releases
- Start:
unzip s3mail-macos-arm64.zip && open s3mail.app, on Linux and Windows./s3mailor a double-click - Build it yourself: Go 1.27 or newer, CGO off
In the AWS account
| What | For | Without it … |
|---|---|---|
| Domain verified in SES | receiving at all | no mail arrives |
| MX record pointing to SES | routes the mail to AWS | mail goes to the old server |
| S3 bucket in the same region | the mail lives there | – |
| Bucket policy for SES | SES may write into it | mail is dropped |
| Active receipt rule set with an S3 action | writes the mail into the bucket | mail is accepted and thrown away |
| IAM identity with the policy below | s3mail reads and sorts | s3mail cannot reach the bucket |
| Verified sender address | writing, replying, forwarding | reading works, sending does not (--no-send) |
| KMS key (optional) | only with encryption | see Encrypted buckets |
Not needed: Lambda, EC2, a VPC or a WorkMail organisation.
The short way: CloudFormation
Bucket, bucket policy, IAM user with policy, the device boundary and the SES receipt rule are created by a template (deploy/README.md in the repository says what each resource is for):
aws cloudformation create-stack \
--stack-name s3mail \
--template-body file://deploy/s3mail.json \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=MailDomain,ParameterValue=example.org \
ParameterKey=MailboxLocalPart,ParameterValue=info \
ParameterKey=MailBucket,ParameterValue=my-unique-bucket
The domain must be verified in SES beforehand; that takes DNS records only you can set. Two steps the template cannot finish, and each is one command: activating the rule set (there is no CloudFormation resource for it) and minting an access key. The template deliberately creates none: a key in a stack output is readable by anyone who can read the stack, and stays that way.
aws ses set-active-receipt-rule-set --rule-set-name s3mail-info
aws iam create-access-key --user-name s3mail-infoBy hand, in order
Pick a region
SES accepts mail only in certain regions; bucket and SES belong in the same one.
Verify the domain in SES
And set the DKIM CNAMEs in DNS.
MX record
Of the domain, to inbound-smtp.<region>.amazonaws.com, priority 10.
Create the S3 bucket
With a prefix as root inside it, usually mail/.
Bucket policy
So SES may write; the SES console offers it when creating the rule.
Create and activate the receipt rule set
With a rule inside whose action is S3 → bucket and prefix mail/. Then activate the rule set. A rule set that exists but is inactive is the most common reason nothing arrives.
Access for s3mail
An IAM user with an access key (recommended), an existing profile with keys, or AWS SSO / IAM Identity Center after aws sso login. The wizard writes typed-in keys as a separate, additional profile to ~/.aws/credentials (chmod 600); existing profiles are left alone.
Verify the sender address
If you want to send. While the SES account is in the sandbox it can only send to verified addresses; receiving works there without restriction.
Send a test mail
Then “Reload” in s3mail. You need not guess whether all of it holds: step 3 of the wizard checks, in order, reading the bucket, reading mail, writing, deleting, state from several machines, encryption and the SES sender, and writes next to every missing item the IAM action it would take.
IAM policy
{
"Version": "2012-10-17",
"Statement": [
{ "Effect": "Allow", "Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::MY-BUCKET",
"Condition": {"StringLike": {"s3:prefix": ["mail/*"]}} },
{ "Effect": "Allow", "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::MY-BUCKET/mail/*" },
{ "Effect": "Allow", "Action": "ses:SendRawEmail", "Resource": "*" },
{ "Effect": "Allow", "Action": ["sqs:ReceiveMessage", "sqs:DeleteMessage"],
"Resource": "arn:aws:sqs:REGION:ACCOUNT:MY-QUEUE" },
{ "Effect": "Allow", "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
"Resource": "arn:aws:kms:REGION:ACCOUNT:key/YOUR-KEY" }
]
}
The KMS part goes away when neither the bucket nor the SES rule encrypts. The SQS part goes away when there is no doorbell; the sync then runs on a timer. sqs:GetQueueUrl is deliberately absent: s3mail builds the URL from the ARN it reads from the policy anyway.
Bucket, folder, sender address and queue are already in the policy, as exactly the value AWS later measures access against. If the identity may read its own policy (iam:ListUserPolicies, iam:GetUserPolicy on user/${aws:username}), s3mail fetches them from there instead of asking: all you enter is the access key and secret. Only inline policies are read. The region is what S3 itself names in its response header.
Optional, purely for the wizard's convenience: s3:ListAllMyBuckets (bucket picker; deliberately not in the policy above, because it shows every bucket in the account), ses:ListIdentities and ses:GetIdentityVerificationAttributes (sender picker), s3:GetLifecycleConfiguration and s3:PutLifecycleConfiguration (automatic trash emptying).
First start
./s3mail
On start, s3mail prints an address to the terminal that contains a token: s3mail running on 127.0.0.1.. The browser opens with it on its own. Without that token the server does not answer.
The first time, the setup wizard opens instead of the mailbox, in three steps: AWS access (pick a profile or enter access key ID + secret, plus the region), Mailbox (bucket, prefix from the SES rule, sender address for replies, optionally a signature and a name for the switcher), Check (briefly creates a test object and deletes it again; the result is a checklist, and what is missing is spelled out). Afterwards you can optionally set a lifecycle rule that empties the trash after 7/30/90 days.
“Save and start” writes ~/.config/s3mail/config.json (chmod 600) and loads the mailbox straight away. From then on ./s3mail is enough. Settings at the top right takes you back into the wizard at any time, also for a second mailbox.
Options
| Option | Meaning |
|---|---|
--setup | open the wizard even when already configured |
--bucket, --prefix, --region, --profile, --from | bucket, root prefix, region, profile, sender; with several mailboxes they act on the first |
--no-send | disable SES sending (read-only mode) |
--no-delete | block permanent deletion; only the trash remains |
--port / --host | default 127.0.0.1:8765 |
--no-browser | do not open the browser automatically |
--no-cache | cache nothing on disk |
--cache-plaintext | write the cache unencrypted when no keychain is available |
--debug | log calls to S3 and SES: keys, sizes, duration, no mail content |
--refresh | seconds between automatic syncs, 0 switches them off (default 60) |
--mcp, --mcp-readonly | run as an MCP server over stdin/stdout; readonly without moving, tagging, drafting |
--version | print the version and exit |