Microservice that consumes email_queue from RabbitMQ and delivers emails via SMTP. Any groceror service can publish {recipient, subject, body} to the queue and groceror-email handles delivery.
Published by the groceror main service and any companion microservice that imports client.py.
| Queue | Trigger |
|---|---|
email_queue |
Any service publishes a send_email event |
Messages that fail validation are routed to email_queue.dlq immediately. SMTP failures are retried once; if redelivery also fails, the message goes to email_queue.dlq.
groceror runs as a bare Python process (make run) and expects RabbitMQ on localhost:5672. groceror-email runs in Docker Compose and connects to that same broker via host.docker.internal.
1. Start RabbitMQ on your host (if not already running):
# Linux
sudo systemctl start rabbitmq-server
# or via Docker (standalone)
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management2. Start groceror:
cd /path/to/groceror
make run # starts on localhost:80003. Configure SMTP credentials:
Create a .env file in the project root:
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your@gmail.com
MAIL_PASSWORD=your-app-password
MAIL_FROM=your@gmail.comFor Gmail, generate an App Password (requires 2FA enabled).
4. Start groceror-email:
docker compose up --buildVerify it's working:
# metric counter present
curl -s localhost:8003/metrics | grep groceror_emailOpen Grafana at http://localhost:3003 (admin / admin) — the Email Events dashboard shows delivery rate, total sent, error rate, and consumer status in real time.
Note: If RabbitMQ refuses the connection with a 403 auth error, see the authentication fix section below.
Import EmailClient from client.py in any groceror service:
from client import EmailClient
EmailClient().send(
recipient="user@example.com",
subject="Welcome to groceror",
body="Hello, your account is ready.",
)EmailClient.send() opens a fresh connection, publishes the message to email_queue, and returns. Delivery is asynchronous.
docker compose up --build| Service | URL |
|---|---|
| groceror-email API | http://localhost:8003 |
| Prometheus | http://localhost:9092 |
| Grafana | http://localhost:3003 (admin / admin) |
The Email Events Grafana dashboard is provisioned automatically on startup.
pip install -r requirements.txt
pip install -r requirements-dev.txt # for tests only
python main.py| Variable | Default | Description |
|---|---|---|
RABBITMQ_HOST |
localhost |
RabbitMQ broker hostname |
RABBITMQ_PORT |
5672 |
RabbitMQ broker port |
RABBITMQ_USER |
guest |
RabbitMQ username |
RABBITMQ_PASS |
guest |
RabbitMQ password |
RABBITMQ_VHOST |
/ |
RabbitMQ virtual host |
MAIL_SERVER |
smtp.gmail.com |
SMTP server hostname |
MAIL_PORT |
587 |
SMTP port (STARTTLS) |
MAIL_USERNAME |
(empty) | SMTP login username |
MAIL_PASSWORD |
(empty) | SMTP login password or app password |
MAIL_FROM |
(MAIL_USERNAME) | Sender address in the From header |
API_HOST |
0.0.0.0 |
FastAPI bind address |
API_PORT |
8003 |
FastAPI port |
METRICS_BACKEND |
prometheus |
prometheus (container) or pushgateway (Lambda) |
PUSHGATEWAY_URL |
(empty) | Pushgateway URL, required when METRICS_BACKEND=pushgateway |
MAIL_SERVER=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=your@gmail.com
MAIL_PASSWORD=your-app-password
MAIL_FROM=your@gmail.com
RABBITMQ_USER=groceror
RABBITMQ_PASS=changeme| Endpoint | Description |
|---|---|
GET /health |
Returns {"status": "ok"} |
GET /metrics |
Prometheus metrics (text/plain) |
| Metric | Type | Labels | Description |
|---|---|---|---|
groceror_email_sent_total |
Counter | status (success/failure) |
Emails attempted |
groceror_email_processing_errors_total |
Counter | reason (validation/smtp) |
Validation or delivery errors |
groceror_email_consumer_up |
Gauge | — | 1 when connected to RabbitMQ, 0 otherwise |
groceror-email expects messages in this envelope format:
{
"schema_version": "1.0",
"event": "send_email",
"recipient": "user@example.com",
"subject": "Welcome to groceror",
"body": "Plain text email body"
}Use EmailClient from client.py to build and publish this envelope correctly.
| Failure | Behaviour |
|---|---|
| RabbitMQ unreachable at startup | Reconnect loop with 5s backoff |
| Invalid JSON | NACK requeue=False → DLQ |
| Pydantic or schema validation failure | NACK requeue=False → DLQ |
| SMTP/network failure, first delivery | NACK requeue=True — retry once |
| SMTP/network failure, redelivered | NACK requeue=False → DLQ |
By default, RabbitMQ's guest user only accepts connections from localhost. Since groceror-email runs inside Docker, it connects from a different IP and will get a 403 refused error.
Fix — allow guest from remote hosts (dev only):
# Linux
echo "loopback_users = none" | sudo tee -a /etc/rabbitmq/rabbitmq.conf
sudo systemctl restart rabbitmq-serverThen restart the stack: docker compose restart groceror-email
lambda_handler.py supports both Amazon MQ and SQS triggers. Point your Lambda trigger at lambda_handler.handler.
Set METRICS_BACKEND=pushgateway and configure PUSHGATEWAY_URL to push metrics from Lambda invocations.
pip install -r requirements-dev.txt
pytest tests/ -v