DEV Community

Cover image for πŸ“¨ Email-AI Assistant using FastAPI, Gemini & Postmark
Pravesh Sudha
Pravesh Sudha Subscriber

Posted on Edited on

πŸ“¨ Email-AI Assistant using FastAPI, Gemini & Postmark

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
Enter fullscreen mode Exit fullscreen mode


πŸŽ₯ 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
Watch the video


πŸ’» 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

πŸ› οΈ 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
Enter fullscreen mode Exit fullscreen mode

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."}
Enter fullscreen mode Exit fullscreen mode

  • 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:

  1. 🧾 Cloned my GitHub repo
  2. 🧰 Installed nginx
  3. πŸ”§ 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Enabled it:

sudo ln -s /etc/nginx/sites-available/email-ai-assistant /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Updated Postmark’s webhook URL to:

http://app.codewithpravesh.tech/inbound-email
Enter fullscreen mode Exit fullscreen mode


🧬 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
Enter fullscreen mode Exit fullscreen mode

Enabled it using:

sudo systemctl daemon-reexec
sudo systemctl daemon-reload
sudo systemctl enable email-assistant
sudo systemctl start email-assistant
Enter fullscreen mode Exit fullscreen mode

Cloud Testing


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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
nevodavid profile image
Nevo David •

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.

Collapse
 
pravesh_sudha_3c2b0c2b5e0 profile image
Pravesh Sudha •

Thanks buddy, Mail loop was actually a big one!

Collapse
 
dotallio profile image
Dotallio •

Loved the story about debugging that email loop - felt that pain before! Any cool use cases for this beyond quick answers or summaries?

Collapse
 
pravesh_sudha_3c2b0c2b5e0 profile image
Pravesh Sudha •

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.

Collapse
 
parag_nandy_roy profile image
Parag Nandy Roy •

Super cool build, Pravesh!

Collapse
 
pravesh_sudha_3c2b0c2b5e0 profile image
Pravesh Sudha •

Thanks Parag!

Collapse
 
jakovg1 profile image
Jakov •

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 :)

Collapse
 
pravesh_sudha_3c2b0c2b5e0 profile image
Pravesh Sudha •

I did a typo XD, fixing it now

Collapse
 
dupe profile image
Dupe •

Great, that's interesting.

Collapse
 
pravesh_sudha_3c2b0c2b5e0 profile image
Pravesh Sudha •

Thanks!

Collapse
 
hunterdev profile image
Francesco Larossa •

Very interesting!
But... Privacy?

Collapse
 
pravesh_sudha_3c2b0c2b5e0 profile image
Pravesh Sudha • • Edited

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.