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.comin my domain DNS
- Used
assistant@codewithpravesh.techas the receiver email - Faced
422 Errorbecause 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.



















Top comments (14)
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.
Thanks buddy, Mail loop was actually a big one!
Loved the story about debugging that email loop - felt that pain before! Any cool use cases for this beyond quick answers or summaries?
Thanks! That loop had me sweating 😅. Beyond summaries, we can extend the program by adding features like auto-reply for customer support, newsletter digests, or even daily briefings—basically turning email into a lightweight AI interface.
Super cool build, Pravesh!
Thanks Parag!
Thanks for the post Pravesh, very interesting stuff, learned quite a bit reading it.
Just a note: Viola is an instrument, I think you meant Voila :)
I did a typo XD, fixing it now
Great, that's interesting.
Thanks!
Very interesting!
But... Privacy?
For security, the only traffic we allow is HTTP access (all over) and SSH (from my IP), the only thing I forgot to add is SSL/TLS Certificate. Will do it soon!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.