From 26ec7ace9e769e44af49d0ec20ff73eeb310b06f Mon Sep 17 00:00:00 2001 From: Dominik Milacher Date: Sat, 14 Jun 2025 11:38:56 +0200 Subject: [PATCH] Add email proxy implementation --- email-proxy/.gitignore | 1 + email-proxy/Dockerfile | 10 +++ email-proxy/README | 13 ++++ email-proxy/docker-compose.yml | 11 +++ email-proxy/helper.py | 40 ++++++++++ email-proxy/proxy.py | 77 +++++++++++++++++++ email-proxy/requirements.txt | 4 + email-proxy/update.py | 26 +++++++ .../docker-compose.yml | 0 9 files changed, 182 insertions(+) create mode 100644 email-proxy/.gitignore create mode 100644 email-proxy/Dockerfile create mode 100644 email-proxy/README create mode 100644 email-proxy/docker-compose.yml create mode 100644 email-proxy/helper.py create mode 100644 email-proxy/proxy.py create mode 100644 email-proxy/requirements.txt create mode 100644 email-proxy/update.py rename gitea-runner.yaml => gitea-runner/docker-compose.yml (100%) diff --git a/email-proxy/.gitignore b/email-proxy/.gitignore new file mode 100644 index 0000000..1d7b718 --- /dev/null +++ b/email-proxy/.gitignore @@ -0,0 +1 @@ +hotels.db \ No newline at end of file diff --git a/email-proxy/Dockerfile b/email-proxy/Dockerfile new file mode 100644 index 0000000..d98acbf --- /dev/null +++ b/email-proxy/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "proxy:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/email-proxy/README b/email-proxy/README new file mode 100644 index 0000000..be89326 --- /dev/null +++ b/email-proxy/README @@ -0,0 +1,13 @@ +INITIAL SETUP + +cd email-proxy +touch hotels.db # IMPORTANT +docker compose up -d --build + + +In the current directory, to update the image&container run +docker compose up --build +this does not change the volume (db) + +For resetting everything, also the db, before run +Use docker compose down -v \ No newline at end of file diff --git a/email-proxy/docker-compose.yml b/email-proxy/docker-compose.yml new file mode 100644 index 0000000..ad4c70c --- /dev/null +++ b/email-proxy/docker-compose.yml @@ -0,0 +1,11 @@ +services: + email-proxy: + restart: unless-stopped + build: . + ports: + - "8000:8000" + volumes: + - ./hotels.db:/app/hotels.db + environment: + - BREVO_API_KEY= + - BREVO_SENDER= \ No newline at end of file diff --git a/email-proxy/helper.py b/email-proxy/helper.py new file mode 100644 index 0000000..7c9cacc --- /dev/null +++ b/email-proxy/helper.py @@ -0,0 +1,40 @@ +# helper.py +from sqlmodel import SQLModel, Session, create_engine, select +from proxy import Hotel, DATABASE_URL + +engine = create_engine(DATABASE_URL, echo=False) + +def add_hotel(hotel_id, email): + with Session(engine) as session: + existing = session.exec(select(Hotel).where(Hotel.hotel_id == hotel_id)).first() + if existing: + print(f"Hotel '{hotel_id}' already exists with email: {existing.email}") + return + hotel = Hotel(hotel_id=hotel_id, email=email) + session.add(hotel) + session.commit() + print(f"Added: {hotel_id} -> {email}") + +def remove_hotel(hotel_id): + with Session(engine) as session: + hotel = session.exec(select(Hotel).where(Hotel.hotel_id == hotel_id)).first() + if not hotel: + print(f"Hotel '{hotel_id}' not found.") + return + session.delete(hotel) + session.commit() + print(f"Removed: {hotel_id}") + +if __name__ == "__main__": + import sys + if len(sys.argv) < 3: + print("Usage: python helper.py add ") + print(" or: python helper.py remove ") + exit(1) + command = sys.argv[1].lower() + if command == "add" and len(sys.argv) == 4: + add_hotel(sys.argv[2], sys.argv[3]) + elif command == "remove" and len(sys.argv) == 3: + remove_hotel(sys.argv[2]) + else: + print("Invalid command or arguments.") diff --git a/email-proxy/proxy.py b/email-proxy/proxy.py new file mode 100644 index 0000000..3269158 --- /dev/null +++ b/email-proxy/proxy.py @@ -0,0 +1,77 @@ +from fastapi import FastAPI, Request, HTTPException +from sqlmodel import Field, SQLModel, Session, create_engine, select +from typing import Optional +import os +import httpx + +# --- Database Models --- + +class Hotel(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + hotel_id: str = Field(index=True, unique=True) + email: str + +# --- App Setup --- + +DATABASE_URL = "sqlite:////app/hotels.db" +engine = create_engine(DATABASE_URL, echo=False) + +def create_db_and_tables(): + SQLModel.metadata.create_all(engine) + +app = FastAPI() + +@app.on_event("startup") +def on_startup(): + create_db_and_tables() + +# --- API Endpoints --- + +@app.post("/api/contact") +async def contact(request: Request): + data = await request.json() + hotel_id = data.get("hotel") + name = data.get("name") + sender_email = data.get("email") + subject = data.get("subject") + message = data.get("message") + + if not hotel_id or not message or not sender_email or not subject: + raise HTTPException(status_code=400, detail="Missing required fields.") + + # Lookup hotel + with Session(engine) as session: + hotel = session.exec(select(Hotel).where(Hotel.hotel_id == hotel_id)).first() + if not hotel: + raise HTTPException(status_code=404, detail="Unknown hotel.") + + await send_brevo_email( + to_email=hotel.email, + from_name=name, + reply_to_email=sender_email, + subject=subject, + message=message, + ) + + return {"ok": True} + +async def send_brevo_email(to_email, from_name, reply_to_email, subject, message): + api_key = os.getenv("BREVO_API_KEY") + sender_email = os.getenv("BREVO_SENDER") + payload = { + "sender": {"name": from_name, "email": sender_email}, + "replyTo": {"name": from_name, "email": reply_to_email}, + "to": [{"email": to_email}], + "subject": subject, + "textContent": message, + } + async with httpx.AsyncClient() as client: + r = await client.post( + "https://api.brevo.com/v3/smtp/email", + headers={"api-key": api_key, "accept": "application/json", "Content-Type": "application/json"}, + json=payload, + timeout=10, + ) + if r.status_code >= 400: + raise HTTPException(status_code=500, detail=f"Email failed: {r.text}") + diff --git a/email-proxy/requirements.txt b/email-proxy/requirements.txt new file mode 100644 index 0000000..3db81dc --- /dev/null +++ b/email-proxy/requirements.txt @@ -0,0 +1,4 @@ +fastapi +uvicorn +sqlmodel +httpx diff --git a/email-proxy/update.py b/email-proxy/update.py new file mode 100644 index 0000000..13ed7ba --- /dev/null +++ b/email-proxy/update.py @@ -0,0 +1,26 @@ +# update.py +import sys +import subprocess + +if len(sys.argv) < 3: + print("Usage:") + print(" python update.py add ") + print(" python update.py remove ") + sys.exit(1) + +command = sys.argv[1].lower() +if command == "add" and len(sys.argv) == 4: + # Run helper.py inside container with add + subprocess.run([ + "docker-compose", "run", "--rm", "email-proxy", + "python", "helper.py", "add", sys.argv[2], sys.argv[3] + ]) +elif command == "remove" and len(sys.argv) == 3: + # Run helper.py inside container with remove + subprocess.run([ + "docker-compose", "run", "--rm", "email-proxy", + "python", "helper.py", "remove", sys.argv[2] + ]) +else: + print("Invalid arguments.") + sys.exit(1) diff --git a/gitea-runner.yaml b/gitea-runner/docker-compose.yml similarity index 100% rename from gitea-runner.yaml rename to gitea-runner/docker-compose.yml