Kai Ole Hartwig
s3mail · documentation

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 ./s3mail or a double-click
  • Build it yourself: Go 1.27 or newer, CGO off

In the AWS account

WhatForWithout it …
Domain verified in SESreceiving at allno mail arrives
MX record pointing to SESroutes the mail to AWSmail goes to the old server
S3 bucket in the same regionthe mail lives there–
Bucket policy for SESSES may write into itmail is dropped
Active receipt rule set with an S3 actionwrites the mail into the bucketmail is accepted and thrown away
IAM identity with the policy belows3mail reads and sortss3mail cannot reach the bucket
Verified sender addresswriting, replying, forwardingreading works, sending does not (--no-send)
KMS key (optional)only with encryptionsee 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-info

By hand, in order

1

Pick a region

SES accepts mail only in certain regions; bucket and SES belong in the same one.

2

Verify the domain in SES

And set the DKIM CNAMEs in DNS.

3

MX record

Of the domain, to inbound-smtp.<region>.amazonaws.com, priority 10.

4

Create the S3 bucket

With a prefix as root inside it, usually mail/.

5

Bucket policy

So SES may write; the SES console offers it when creating the rule.

6

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.

7

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.

8

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.

9

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

OptionMeaning
--setupopen the wizard even when already configured
--bucket, --prefix, --region, --profile, --frombucket, root prefix, region, profile, sender; with several mailboxes they act on the first
--no-senddisable SES sending (read-only mode)
--no-deleteblock permanent deletion; only the trash remains
--port / --hostdefault 127.0.0.1:8765
--no-browserdo not open the browser automatically
--no-cachecache nothing on disk
--cache-plaintextwrite the cache unencrypted when no keychain is available
--debuglog calls to S3 and SES: keys, sizes, duration, no mail content
--refreshseconds between automatic syncs, 0 switches them off (default 60)
--mcp, --mcp-readonlyrun as an MCP server over stdin/stdout; readonly without moving, tagging, drafting
--versionprint the version and exit

Next

Back to s3mail

The overview: what makes s3mail s3mail, getting started, every chapter.

s3mail →
Back to s3mail

Features

Folders as S3 prefixes, reading, writing, rules, search, several mailboxes.

Read →
Features