Skip to content

feat(email): add email category (SES, Azure ACS, raw SMTP)#4

Merged
abhi-bhat-lyzr merged 3 commits into
mainfrom
feat/email-category
May 28, 2026
Merged

feat(email): add email category (SES, Azure ACS, raw SMTP)#4
abhi-bhat-lyzr merged 3 commits into
mainfrom
feat/email-category

Conversation

@abhi-bhat-lyzr

Copy link
Copy Markdown
Contributor

New cloudrift.email package with a single EmailBackend interface and three native-async backends:

  • ses: AWSSESBackend via aioboto3 SESv2. IAM-role / access-key / profile auth. Switches to Content.Raw MIME when attachments or custom headers are present.
  • azure_acs: AzureACSEmailBackend via azure.communication.email (sync SDK wrapped with asyncio.to_thread). Connection-string, managed-identity, and service-principal auth.
  • smtp: SMTPEmailBackend via aiosmtplib. Three modes: plaintext, STARTTLS (587), implicit TLS (465). Covers SendGrid, Mailgun, Postmark, Office365, and MailHog/Mailpit for local dev.

EmailBackend.send() takes text + HTML bodies, cc/bcc/reply-to, attachments, and custom headers. send_batch() ships as a default loop in the ABC; the lazy-_ensure / atomic-close() pattern from commit 20340f0 carries over to SES and is now covered by
tests/test_ensure_lifecycle.py.

Five new exception types map provider errors at the boundary: EmailError, EmailSendError, RecipientRejectedError, SenderUnverifiedError, EmailThrottledError.

35 unit tests in tests/test_email.py (SES via moto, ACS+SMTP via mocks). Full suite: 135 passed.

New `cloudrift.email` package with a single `EmailBackend` interface and
three native-async backends:

- `ses`: AWSSESBackend via aioboto3 SESv2. IAM-role / access-key /
  profile auth. Switches to `Content.Raw` MIME when attachments or
  custom headers are present.
- `azure_acs`: AzureACSEmailBackend via `azure.communication.email`
  (sync SDK wrapped with `asyncio.to_thread`). Connection-string,
  managed-identity, and service-principal auth.
- `smtp`: SMTPEmailBackend via `aiosmtplib`. Three modes: plaintext,
  STARTTLS (587), implicit TLS (465). Covers SendGrid, Mailgun,
  Postmark, Office365, and MailHog/Mailpit for local dev.

`EmailBackend.send()` takes text + HTML bodies, cc/bcc/reply-to,
attachments, and custom headers. `send_batch()` ships as a default loop
in the ABC; the lazy-`_ensure` / atomic-`close()` pattern from commit
20340f0 carries over to SES and is now covered by
`tests/test_ensure_lifecycle.py`.

Five new exception types map provider errors at the boundary:
EmailError, EmailSendError, RecipientRejectedError,
SenderUnverifiedError, EmailThrottledError.

35 unit tests in `tests/test_email.py` (SES via moto, ACS+SMTP via
mocks). Full suite: 135 passed.
`patch("azure.identity.ClientSecretCredential", ...)` failed under CI
(Python 3.11, fresh interpreter) because `azure` is a PEP 420 namespace
package — `azure.identity` is not discoverable as an attribute on
`azure` until something imports it directly. Locally the import had
already happened transitively, masking the issue.

Switch to an explicit `import azure.identity` followed by
`patch.object(azure.identity, "ClientSecretCredential", ...)`, which
works on every environment regardless of import order.
CI runs 'uv sync --extra dev' which only pulls the [dev] extra.
test_factory_acs_routing_service_principal exercises the
AzureACSEmailBackend.from_service_principal factory, whose lazy
'from azure.identity import ClientSecretCredential' fails when the
module isn't installed.

Other ACS tests dodge this because they use connection-string auth and
stub the client directly. The dev extra already ships
azure-communication-email for the same 'tests touch it' reason -
azure-identity was just missed.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
2 Security Hotspots
3.5% Duplication on New Code (required ≤ 3%)
E Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@abhi-bhat-lyzr abhi-bhat-lyzr merged commit e2d81ee into main May 28, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants