Spaces:
Running
Running
| """Mail Tracker — the public half of Mr AI Marketer's email open/click tracking. | |
| Serves a 1x1 pixel for open tracking and a redirect for click tracking, both | |
| reachable from anywhere (a recipient's mail client is never on the same machine | |
| as the desktop app that sent the mail), plus a secret-gated /events endpoint the | |
| local app polls to pull new hits back down. Storage is a local SQLite file on | |
| this Space's own container disk — survives normal sleep/wake, but not a factory | |
| reboot or a redeploy (rebuilding the image wipes it), an accepted tradeoff for a | |
| free, personal-scale tool; the local app is the durable copy of record. | |
| SYNC_SECRET is a single value shared by every installation of the app (there is | |
| no per-user boundary on a shared public Space), so it only deters casual | |
| scraping of /events, not a determined reader of the shipped app's own source — | |
| the real privacy boundary is client-side: the syncing app discards any event | |
| whose token it doesn't recognize as its own. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import os | |
| import sqlite3 | |
| from contextlib import contextmanager | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Iterator | |
| from fastapi import FastAPI, Query | |
| from fastapi.responses import JSONResponse, RedirectResponse, Response | |
| from urllib.parse import urlparse | |
| DB_PATH = Path(__file__).parent / "tracker.sqlite3" | |
| SYNC_SECRET = os.environ.get("SYNC_SECRET", "") | |
| # The standard minimal 1x1 GIF used by countless open-source tracking pixels — | |
| # decoded from a literal already verified to be a well-formed 34-byte GIF89a | |
| # (header + 1x1 logical screen + 2-color table + one image block + trailer). | |
| _PIXEL_GIF = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==") | |
| _NO_STORE_HEADERS = { | |
| "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", | |
| "Pragma": "no-cache", | |
| "Expires": "0", | |
| } | |
| app = FastAPI() | |
| def _now_iso() -> str: | |
| return datetime.now(timezone.utc).isoformat() | |
| def _connect() -> Iterator[sqlite3.Connection]: | |
| conn = sqlite3.connect(DB_PATH, timeout=15) | |
| conn.row_factory = sqlite3.Row | |
| try: | |
| yield conn | |
| conn.commit() | |
| finally: | |
| conn.close() | |
| def _init_db() -> None: | |
| with _connect() as conn: | |
| conn.execute( | |
| """ | |
| CREATE TABLE IF NOT EXISTS events ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| token TEXT NOT NULL, | |
| type TEXT NOT NULL, | |
| url TEXT, | |
| occurred_at TEXT NOT NULL | |
| ) | |
| """ | |
| ) | |
| _init_db() | |
| def _log_event(token: str, event_type: str, url: str | None = None) -> None: | |
| with _connect() as conn: | |
| conn.execute( | |
| "INSERT INTO events (token, type, url, occurred_at) VALUES (?, ?, ?, ?)", | |
| (token, event_type, url, _now_iso()), | |
| ) | |
| def health() -> dict: | |
| return {"status": "ok"} | |
| def open_pixel(token: str) -> Response: | |
| _log_event(token, "open") | |
| return Response(content=_PIXEL_GIF, media_type="image/gif", headers=_NO_STORE_HEADERS) | |
| def click_redirect(token: str, u: str = Query(...)): | |
| parsed = urlparse(u) | |
| if parsed.scheme not in ("http", "https") or not parsed.netloc or len(u) > 2000: | |
| return JSONResponse({"error": "invalid destination url"}, status_code=400) | |
| _log_event(token, "click", url=u) | |
| return RedirectResponse(url=u, status_code=302, headers=_NO_STORE_HEADERS) | |
| def events(since_id: int = 0, secret: str = "", limit: int = 500): | |
| if not SYNC_SECRET or secret != SYNC_SECRET: | |
| return JSONResponse({"error": "forbidden"}, status_code=403) | |
| limit = max(1, min(limit, 500)) | |
| with _connect() as conn: | |
| rows = conn.execute( | |
| "SELECT id, token, type, url, occurred_at FROM events WHERE id > ? ORDER BY id ASC LIMIT ?", | |
| (since_id, limit), | |
| ).fetchall() | |
| return {"events": [dict(row) for row in rows]} | |