A submission for the Postmark Challenge: Inbox Innovators
💡 What I Built
Hey folks! 👋
I built an Email-based AI Assistant powered by FastAPI, Gemini, and Postmark. The assistant allows users to send an email and get an AI-generated response right in their inbox — just like magic 🪄.
Here’s the workflow in simple terms:
User sends an email ➝ Postmark receives it ➝ Webhook (FastAPI backend) is triggered ➝
Gemini processes the email ➝ Response is generated ➝
Reply is sent back to the user via Postmark
🎥 Live Demo
📧 Try it yourself:
Send an email to 👉 assistant@codewithpravesh.tech
Ask a question like “Explain Postmark in brief” and within 30–60 seconds, you’ll get an intelligent reply — straight to your inbox.
▶️ Watch the full walkthrough below
💻 Code Repository
The project is open-source and available on GitHub:
🔗 https://github.com/Pravesh-Sudha/dev-to-challenges
The relevant code lives in the postmark-challenge/
directory, containing:
-
main.py
: Sets up the FastAPI server and webhook endpoint -
utils.py
: Handles Gemini integration and Postmark email sending logic
main.py
from fastapi import FastAPI
from pydantic import BaseModel
from fastapi.responses import JSONResponse
from utils import get_response, send_email_postmark
app = FastAPI()
class PostmarkInbound(BaseModel):
From: str
Subject: str
TextBody: str
@app.post("/inbound-email")
async def receive_email(payload: PostmarkInbound):
sender = payload.From
subject = payload.Subject
body = payload.TextBody
# Prevent infinite loop
if sender == "assistant@codewithpravesh.tech":
return {"message": "Self-email detected, skipping."}
response = get_response(body)
try:
send_email_postmark(
to_email=sender,
subject=f"Re: {subject}",
body=response
)
except Exception as e:
print("Email send failed, but continuing:", e)
return JSONResponse(content={"message": "Processed"}, status_code=200)
utils.py
import os
import requests
import google.generativeai as genai
from dotenv import load_dotenv
load_dotenv()
genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
model = genai.GenerativeModel("models/gemini-2.5-flash-preview-04-17-thinking")
def get_response(prompt: str) -> str:
try:
response = model.generate_content(prompt)
return response.text.strip()
except Exception as e:
return f"Error: {e}"
def send_email_postmark(to_email, subject, body):
postmark_token = os.getenv('POSTMARK_API_TOKEN')
payload = {
"From": "assistant@codewithpravesh.tech",
"To": to_email,
"Subject": subject or "No Subject",
"TextBody": body or "Empty Response",
}
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"X-Postmark-Server-Token": postmark_token
}
try:
r = requests.post("https://api.postmarkapp.com/email", json=payload, headers=headers)
r.raise_for_status()
except Exception as e:
print("Failed to send email via Postmark:", e)
🛠️ How I Built It
This project has been a rewarding rollercoaster 🎢 — full of debugging, email loops, and a bit of DNS sorcery.
🚫 Problem: No Private Email
When I first registered on Postmark, I realized they don’t allow public email domains (like Gmail) for sending. I didn’t have a private email. 😓
✅ Solution: Dev++ to the Rescue
I reached out to the Dev.to team, and they kindly gifted me a DEV++ membership 💛 — which included a domain and two private emails!
I registered:
🔗 codewithpravesh.tech
📬 Created user@codewithpravesh.tech
Using this, I successfully created a Postmark account. ✅
🧠 Choosing the LLM
I wanted a fast, reliable, and free LLM. I tested:
- ❌ OpenAI — Paid
- ❌ Grok — Complicated setup
- ✅ Gemini — Free via Google API, simple to use, fast response
The winner? 🏆 Gemini 2.5 Flash
🧪 Local Testing with Ngrok
To test the webhook, I spun up the FastAPI app locally and exposed it using ngrok.
Webhook URL used:
https://<ngrok-url>/inbound-email
Then I set up Inbound Domain Forwarding on Postmark:
- Added an MX Record pointing to
inbound.postmarkapp.com
in my domain DNS
- Used
assistant@codewithpravesh.tech
as the receiver email - Faced
422 Error
because my account approval was in pending state.
😅 The Loop Disaster
For testing, I tried sending an email from user@codewithpravesh.tech
➝ assistant@codewithpravesh.tech
.
Result? Infinite loop 🔁
Why?
My webhook was triggered, and it responded to itself over and over.
Outcome:
- Burned through 100 free emails/month
- Had to upgrade with promo code
DEVCHALLENGE25
Fix:
if sender == "assistant@codewithpravesh.tech":
return {"message": "Self-email detected, skipping."}
- Now application is working fine locally.
☁️ Deploying on AWS EC2
To make it public, I chose AWS EC2:
- Instance type:
t2.small
- Storage: 16 GB
- Elastic IP assigned
- Security group: Open HTTP, HTTPS (0.0.0.0/0), SSH (my IP)
Then:
- 🧾 Cloned my GitHub repo
- 🧰 Installed nginx
- 🔧 Configured DNS A record to point
app.codewithpravesh.tech
➝ EC2 IP
🔁 Nginx Reverse Proxy Setup
I created a file /etc/nginx/sites-available/email-ai-assistant
:
server {
listen 80;
server_name app.codewithpravesh.tech;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Enabled it:
sudo ln -s /etc/nginx/sites-available/email-ai-assistant /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Updated Postmark’s webhook URL to:
http://app.codewithpravesh.tech/inbound-email
🧬 Making It Production-Ready
To keep the app alive after reboot, I created a systemd service:
[Unit]
Description=Email AI Assistant FastAPI App
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/dev-to-challenges/postmark-challenge
Environment="PATH=/home/ubuntu/dev-to-challenges/postmark-challenge/app-venv/bin"
ExecStart=/home/ubuntu/dev-to-challenges/postmark-challenge/app-venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000
Restart=always
[Install]
WantedBy=multi-user.target
Enabled it using:
sudo systemctl daemon-reexec
sudo systemctl daemon-reload
sudo systemctl enable email-assistant
sudo systemctl start email-assistant
Last Minute things 😅
After posting the article, I got a lovely comment as shown below:
"Very Interesting!
But Privacy"
To fix this, I get inside the instance and generate a SSL/TLS certificate for the Webhook URL using the following command:
sudo certbot --nginx -d app.codewithpravesh.tech
and Voila!, everything got setup, it changes the Nginx config file (email-assistant) accordingly.
The only thing left to do was just just http to https in the webhook URL.
🙌 Final Thoughts
This was such a fun and technically challenging project!
Big thanks to Postmark and the Dev.to team for organizing this challenge and giving us a platform to innovate. 💛
I learned a ton about:
- Webhooks & mail routing
- FastAPI production setups
- DNS + Postmark integration
- Using LLMs in real-world tools
🧠 Try the app → assistant@codewithpravesh.tech
🎥 Watch the demo → YouTube Walkthrough
If you liked this project, leave a ❤️, star the repo, and feel free to reach out on Twitter or LinkedIn.
Pretty cool seeing how you actually battled through those mail loops and DNS headaches – respect for just sticking with it and getting it all to work.