wraps-email
Send email via AWS SES from Python — your AWS account, no vendor lock-in.
A thin, typed wrapper over SES: raw html/text, attachments, SES-stored
templates, and suppression management. Built on httpx + botocore signing (no
boto3), so it stays lightweight and a sync and async client can share one
transport core.
pip install wraps-email # or: uv add wraps-email
from wraps.email import WrapsEmail
email = WrapsEmail() # region and credentials from your AWS environment
result = email.send(
from_="you@yourdomain.com",
to="user@example.com",
subject="Hello from Python",
html="<h1>It works</h1>",
text="It works",
)
print(result.message_id)
Import as
wraps.email; the distribution iswraps-email.from_(notfrom, which is a Python keyword) is the sender.
Before your first send
Three things decide whether a send lands, and all three fail with confusing errors when they are wrong:
- A verified sender identity.
from_must be an SES identity you have verified. Prefer a domain identity with DKIM over a bare address so mail authenticates at Gmail and Yahoo. - The right region. SES identities are per-region. A domain verified in
eu-west-1does not exist inus-east-1, and sending to the wrong one is reported as "Email address is not verified" — pointing you at verification when the real problem is the region. - The SES sandbox. Every new AWS account starts sandboxed and can only send to verified recipients. Getting out is an AWS support review.
You do not need production access to prove your setup works. Send to the AWS mailbox simulator — AWS pre-verifies it, so it needs no recipient verification and produces a real Delivery event:
from wraps.email import SES_SIMULATOR_SUCCESS
email.send(
from_="you@yourdomain.com",
to=SES_SIMULATOR_SUCCESS, # success@simulator.amazonses.com
subject="Proving the pipeline",
text="Hello",
)
When SES rejects a send as unverified, this SDK raises SandboxError with both
causes named, the region it used, where that region came from, and the ranked
ways out. err.original_message still holds AWS's untouched text.
Region
Omit region and it resolves the way every other AWS tool resolves it, highest
priority first:
WrapsEmail(region="eu-west-1")AWS_REGIONAWS_DEFAULT_REGION- the active profile's
regionin~/.aws/config us-east-1as a last resort
WrapsEmail() # AWS_REGION / AWS_DEFAULT_REGION / profile
WrapsEmail(region="eu-west-1") # explicit, wins over everything
email.region # -> "eu-west-1"
email.region_source # -> "$AWS_REGION" — where it came from
Credentials
Resolved via the standard AWS chain — environment variables, shared config, SSO, OIDC/web-identity, assume-role, and IMDS. Override explicitly when you need to:
WrapsEmail() # default chain
WrapsEmail(profile="wraps-dogfood") # named profile
WrapsEmail(credentials={"access_key_id": "...", "secret_access_key": "..."})
WrapsEmail(role_arn="arn:aws:iam::123456789012:role/MyRole") # assume-role / OIDC
Credentials are resolved on the first request, not in the constructor, so
building a client does no I/O and CredentialsError surfaces from send(). An
expired SSO session or an unknown profile raises CredentialsError too, rather
than a raw botocore exception.
Sending
Attachments
Providing attachments switches the send to a raw MIME message automatically.
content is bytes, or a string decoded per encoding ("utf-8" or "base64");
content_type is guessed from the filename when omitted. Bcc always rides the
SES envelope, never the visible headers.
from wraps.email import Attachment
email.send(
from_="you@yourdomain.com",
to="user@example.com",
bcc="audit@yourdomain.com",
subject="Your report",
html="<p>Attached.</p>",
attachments=[Attachment(filename="report.csv", content="a,b\n1,2\n", content_type="text/csv")],
)
Batch
Send many independent messages concurrently. A failed message never aborts the batch; a malformed entry raises before anything is sent.
result = email.send_batch(
[
{"from_": "you@x.com", "to": "a@y.com", "subject": "Hi", "text": "1"},
{"from_": "you@x.com", "to": "b@y.com", "subject": "Hi", "text": "2"},
],
max_concurrency=10,
)
print(result.success_count, result.failure_count)
for entry in result.results: # aligned to input order
if not entry.success:
print(entry.index, entry.error_code, entry.error)
Templates
Manage SES-stored templates and let SES render them at send time.
email.templates.create(name="welcome", subject="Hi {{name}}", html="<h1>{{name}}</h1>")
email.templates.get("welcome")
email.templates.list(page_size=20) # .next_token to paginate
email.templates.update(name="welcome", subject="Hey {{name}}", html="<h1>{{name}}</h1>")
email.templates.delete("welcome")
email.send_template(
template="welcome",
from_="you@yourdomain.com",
to="user@example.com",
data={"name": "Sam"},
)
Suppression
The account-level SES suppression list (bounces and complaints).
email.suppression.add("bad@example.com", "COMPLAINT")
email.suppression.get("bad@example.com") # -> SuppressionEntry | None
email.suppression.list(reason="BOUNCE") # .next_token to paginate
email.suppression.remove("bad@example.com")
Errors
Every error derives from WrapsEmailError, so one except WrapsEmailError
covers all of them.
from wraps.email import CredentialsError, SandboxError, SESError, ValidationError
try:
email.send(from_="you@x.com", to="user@y.com", subject="Hi", html="<p>Hi</p>")
except ValidationError as err:
... # bad input, caught before any AWS call (err.field)
except CredentialsError as err:
... # no credentials, expired SSO, or an unknown profile
except SandboxError as err:
... # unverified recipient: SES sandbox, or wrong region
# err.original_message is AWS's untouched text
except SESError as err:
... # err.code, err.request_id, err.retryable, err.status
SandboxError subclasses SESError, so an existing except SESError keeps
catching it.
Typed
Ships a PEP 561 py.typed marker; every public method has an explicit typed
signature, so mypy / ty / Pyright check your calls and editors autocomplete them.
Status
0.1.0 — email SDK. Inbound (inbox), event history, reply threading, local
template rendering, and an async client are planned but not implemented
yet. See the repo roadmap.
Every SES request carries a wraps-email-py/<version> user-agent so Wraps
traffic is distinguishable from anything else calling SES in your account. The
SDK sends no telemetry and phones nothing home.
MIT licensed.
Metadata
Release files for wraps-email 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| wraps_email-0.2.0.tar.gz | 25.7 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| wraps_email-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 50.9 kB
Release files / wraps_email-0.2.0.tar.gz
| Download URL | wraps_email-0.2.0.tar.gz |
|---|---|
| Size | 25.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fc1fafd3928dc66cb4a90c96eb7108e4922fdeaabdab649de4deaff184edd53a
|
|
BLAKE2b-256 checksum How to use checksums |
e845af1a9f0bf5c0942d8bd4b7675bcb47eb536e86318179cfca43c7388fcc5b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / wraps_email-0.2.0-py3-none-any.whl
| Download URL | wraps_email-0.2.0-py3-none-any.whl |
|---|---|
| Size | 25.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6cbe925eb52aa66e71dd4f3c9d9466e9a134e91290e325db47f2b8c7a3fb93e2
|
|
BLAKE2b-256 checksum How to use checksums |
ee50115b21f9ad80d70230ee46a176ccb2626c6091e4579698b00fc8402634dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for wraps-email 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.