Cartero¶
| Field | Value |
|---|---|
| Owner | Alquemis (DRI) — original author Olger Ávila (IT) |
| Status | live |
| Runs on | Office Windows host (C:\Trabajo\cartero); Azure-capable (managed identity → Key Vault) |
| Connectivity | Internal only — unauthenticated POST /send, must stay on a trusted network. Outbound to Exchange Online (SMTP 587). Not exposed via Cloudflare Tunnel. |
| Hostname | internal only, no public endpoint |
| Repository | Alquemis-Automation/Cartero (private) |
What it is¶
Cartero is Alquemis' internal transactional email service: it sends mail as
cartero@alquemis.com through Microsoft 365 / Exchange Online using SMTP AUTH + OAuth 2.0
(client-credentials, app-only) — deliberately avoiding Basic Auth, which Microsoft is retiring
(deprecation through 2026, retirement H2 2027).
It ships in two forms:
MailSender— a reusable Python class (import and call.send(...)); caches the MSAL token across sends, so create one instance and reuse it.mail_service.py— a Starlette / Uvicorn REST microservice wrappingMailSender, so other automations can send mail over HTTP (same mold as the internalpdf_service).
The From address is always cartero@alquemis.com (fixed policy — callers can't override the
sender); a caller-supplied from is used only as the Reply-To, which otherwise defaults to
soporte@bio-land.com. Built with msal + azure-identity, Python 3.
HTTP API¶
The microservice listens on port 8182 (configurable) and exposes:
| Method + path | Purpose |
|---|---|
POST /send |
Send one email (JSON body below). Returns {ok: true, from, reply_to} or {ok: false, error}. |
GET /health |
Liveness — returns service name, version, sender, default reply-to, timestamp. |
POST /send JSON body:
| Field | Type | Notes |
|---|---|---|
to |
str | list | required — recipient(s); ; and , separators accepted (PKCORREO format) |
subject |
str | required |
body |
str | plain-text body (optional) |
html |
str | HTML body (optional) |
cc / bcc |
str | list | optional |
from |
str | ignored for the From; used as Reply-To if reply_to is absent |
reply_to |
str | optional; default soporte@bio-land.com |
attachments |
list | each item {filename, content_base64, mime} or {filename, path, mime} (path/UNC reachable by the service) |
Error codes: 400 bad request (missing to/subject), 502 send failure (MailSendError),
500 unexpected.
How it's reached¶
Internal callers hit http://<host>:8182/send on the office LAN. There is no authentication on
the endpoint and it is not published through the Cloudflare Tunnel — it must stay on a
trusted network (see security notes). Outbound, the service connects to smtp.office365.com:587
(STARTTLS) to submit mail.
Configuration¶
All settings have defaults in config.py and are overridable via environment variables (see
.env.example; .env is never committed). Key ones:
| Variable | Default | Purpose |
|---|---|---|
CARTERO_CLIENT_SECRET |
— | Entra app secret. If unset, read from Key Vault. |
CARTERO_TENANT_ID / CARTERO_CLIENT_ID |
Bioland1 tenant / app (in config.py) |
Entra app identity |
CARTERO_SENDER |
cartero@alquemis.com |
Fixed From (UPN) |
CARTERO_REPLY_TO |
soporte@bio-land.com |
Default Reply-To |
CARTERO_KEYVAULT_URL / _SECRET |
https://generales.vault.azure.net/ / cartero-smtp-client-secret |
Key Vault fallback for the secret |
CARTERO_SMTP_HOST / _PORT / _TIMEOUT |
smtp.office365.com / 587 / 30 |
SMTP endpoint |
CARTERO_MAX_RETRIES / BACKOFF_BASE / _CAP |
3 / 2.0 / 30.0 |
Transient-error retry / backoff (seconds) |
MAIL_SERVICE_HOST / _PORT / MAIL_LOG_LEVEL |
0.0.0.0 / 8182 / INFO |
Microservice bind + logging |
Secret resolution (config.obtener_client_secret()): CARTERO_CLIENT_SECRET from the
environment first, else Azure Key Vault (Generales vault) via DefaultAzureCredential — a
managed identity on Azure (role Key Vault Secrets User), or a service-principal / signed-in dev
identity on-prem. The tenant/client IDs in config.py are non-secret identifiers; the secret
itself is never in code.
Security notes¶
- Unauthenticated endpoint.
POST /sendhas no auth or rate limiting — anyone who can reach the port can send mail ascartero@alquemis.com. Keep it on a trusted network only; add auth before any wider exposure. - Fixed sender. From is locked to
cartero@alquemis.com; callers cannot spoof another From. - No secrets in code/repo. The app secret comes from env or Key Vault; only non-secret
tenant/client IDs live in
config.py. - Retries fire only on transient SMTP errors (timeouts, disconnects,
4.x) with exponential backoff; permanent errors (5.x, auth535) fail fast withMailSendError.
Dependencies¶
- Microsoft 365 / Exchange Online —
smtp.office365.com:587; requiresSMTP.SendAsAppadmin consent, a service principal withFullAccess+SendAson the mailbox, and SMTP AUTH enabled on the mailbox. Already configured in the Bioland1 tenant. - Entra ID app registration (client-credentials, single tenant).
- Azure Key Vault (
Generales, secretcartero-smtp-client-secret) — fallback secret source. - Python:
msal,azure-identity,azure-keyvault-secrets,python-dotenv,starlette,uvicorn. - Consumers: internal Alquemis automations that need to send mail over HTTP.
Known limitations¶
- Not a bulk / marketing mailer. Exchange throttles SMTP client submission; at volume, move to
Microsoft Graph
sendMailor High Volume Email. - Outbound only. Sends mail; does not read or receive.
- Secret expiry. The Entra app secret expires — delivery breaks if it isn't rotated (see the runbook). Migrating to certificate auth is under consideration.
- Single sender. Only
cartero@alquemis.com.