From c6001d4eb70872cdd02ecdec35bf2ba3680767d4 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/Dockerfile | 10 +++ email-proxy/README | 13 +++ email-proxy/docker-compose.yml | 11 +++ email-proxy/helper.py | 40 +++++++++ email-proxy/hotels.db | Bin 0 -> 12288 bytes email-proxy/proxy.py | 77 ++++++++++++++++++ email-proxy/requirements.txt | 4 + email-proxy/update.py | 26 ++++++ .../docker-compose.yml | 0 9 files changed, 181 insertions(+) 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/hotels.db 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/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..5e2f411 --- /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=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 + - BREVO_SENDER=proxy@dominikmilacher.com \ 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/hotels.db b/email-proxy/hotels.db new file mode 100644 index 0000000000000000000000000000000000000000..e40a4f6e1da06bdb099a20ecc2299cb8df56cb68 GIT binary patch literal 12288 zcmeI$O-sWt7zgmAtuHH#I&}2<939AHcotS?(c$b|?Zn|h>e{hjyUOfgo@C!{pT(P3 zPo~{^5MRzi{tx6ynxqfmcT28&9g&7KN~W=&(h=KXoU@wp?CIkc^009U<00Izz00bZa0SG`~Jp}HQP4m#?uVNB}Gcmg#Cutb1 zvIz1owdLBDXH(y4_xd)qom2akzHOzhL-}^vU)B&oZC>VErfKr|OX@#FVLqQHWmVF% zPC9n+n_d!utnS%owo6XeBd6c#9FSfr>d}qmw$81ui(%}G=*N{Sw|!~31G=yWv@cJi z*Oqtl1vB1^#d`4L6cB&_1Rwwb2tWV=5P$##AOHaf{3bw(#;V6@_?%{!|7MWHVj>=6 z5&6S$IIRzpc#mtWQX4nKWRx`X_k(O!mjxB$of#i8As_$&2tWV=5P$##AOHafKmY;| T_#Xn>YL#ct1j>e5DUBPSQK4wg literal 0 HcmV?d00001 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