Add PostgreSQL-backed DuckMail GUI

This commit is contained in:
52tv
2026-03-20 17:24:48 +08:00
commit 3fe6390250
7 changed files with 1165 additions and 0 deletions

157
account_store.py Normal file
View File

@@ -0,0 +1,157 @@
import json
import os
import threading
try:
import psycopg
except ImportError:
psycopg = None
try:
import psycopg2
except ImportError:
psycopg2 = None
_LOCK = threading.Lock()
def _safe_read_json(path):
if not os.path.exists(path):
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def _safe_write_json(path, data):
tmp_path = f"{path}.tmp"
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, path)
def save_config(path, config):
with _LOCK:
_safe_write_json(path, config)
def load_config(path):
data = _safe_read_json(path)
if not isinstance(data, dict):
return {}
return data
def _to_text(value):
if value is None:
return ""
return str(value)
def _normalize_optional_text(value):
text = _to_text(value).strip()
return text or None
def _get_pg_driver():
if psycopg is not None:
return "psycopg"
if psycopg2 is not None:
return "psycopg2"
raise RuntimeError("未安装 PostgreSQL 驱动,请先安装 psycopg[binary] 或 psycopg2-binary")
def _get_pg_connection(config):
driver = _get_pg_driver()
host = str(config.get("pg_host", "")).strip()
port = int(config.get("pg_port", 5432) or 5432)
dbname = str(config.get("pg_db", "")).strip()
user = str(config.get("pg_user", "")).strip()
password = str(config.get("pg_password", "")).strip()
connect_timeout = int(config.get("pg_connect_timeout", 10) or 10)
if not host or not dbname or not user:
raise ValueError("PostgreSQL 配置不完整,请填写 Host、DB、User")
kwargs = {
"host": host,
"port": port,
"dbname": dbname,
"user": user,
"password": password,
"connect_timeout": connect_timeout,
}
if driver == "psycopg":
return psycopg.connect(**kwargs)
return psycopg2.connect(**kwargs)
def load_accounts(config):
query = (
"select email, mail_password, mail_token, chatgpt_password, name, birthdate, created_at "
"from registered_accounts order by created_at desc nulls last, email asc"
)
accounts = []
with _get_pg_connection(config) as conn:
with conn.cursor() as cur:
cur.execute(query)
rows = cur.fetchall()
for row in rows:
email, mail_password, mail_token, chatgpt_password, name, birthdate, created_at = row
if not email:
continue
accounts.append(
{
"email": _to_text(email).strip(),
"mail_password": _to_text(mail_password).strip(),
"mail_token": _to_text(mail_token).strip(),
"chatgpt_password": _to_text(chatgpt_password).strip(),
"name": _to_text(name).strip(),
"birthdate": _to_text(birthdate).strip(),
"created_at": _to_text(created_at).strip(),
"source": "postgres",
}
)
return accounts
def save_account(
config,
email,
mail_password,
mail_token,
chatgpt_password=None,
name=None,
birthdate=None,
):
query = (
"insert into registered_accounts "
"(email, mail_password, mail_token, chatgpt_password, name, birthdate) "
"values (%s, %s, %s, %s, %s, %s) "
"on conflict (email) do update set "
"mail_password = excluded.mail_password, "
"mail_token = excluded.mail_token, "
"chatgpt_password = excluded.chatgpt_password, "
"name = excluded.name, "
"birthdate = excluded.birthdate"
)
params = (
email,
mail_password,
mail_token,
_normalize_optional_text(chatgpt_password),
_normalize_optional_text(name),
_normalize_optional_text(birthdate),
)
with _LOCK:
with _get_pg_connection(config) as conn:
with conn.cursor() as cur:
cur.execute(query, params)
conn.commit()