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.
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!
Super cool build, Pravesh!
Thanks Parag!
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.
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!
Great, that's interesting.
Thanks!
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
Some comments may only be visible to logged-in visitors. Sign in to view all comments.