From 5777dbcf5f399160110dd24d1ce0b316f376ddbe Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:01:21 +0000 Subject: [PATCH] chore: remove unused imports and duplicate code block - Removed unused `base64` and `render_template` imports in `app.py`. - Removed a large block of accidentally duplicated private methods in `services/storage.py` that were causing `flake8` `F811` (redefinition) errors. Co-authored-by: genz27 <222551373+genz27@users.noreply.github.com> --- app.py | 3 +- services/storage.py | 911 -------------------------------------------- 2 files changed, 1 insertion(+), 913 deletions(-) diff --git a/app.py b/app.py index be1479a..f9fe398 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,5 @@ from __future__ import annotations -import base64 import csv import hashlib import hmac @@ -14,7 +13,7 @@ from pathlib import Path from typing import Any, Callable -from flask import Flask, jsonify, render_template, request, send_from_directory, session +from flask import Flask, jsonify, request, send_from_directory, session from services.outlook_manager import ( FlagStateUpdateRequest, diff --git a/services/storage.py b/services/storage.py index 603ad66..bedc368 100644 --- a/services/storage.py +++ b/services/storage.py @@ -2968,914 +2968,3 @@ def _normalize_text(value: Any, fallback: str | None = None) -> str | None: @staticmethod def _utc_now() -> str: return datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z") - - def _initialize_schema(self) -> None: - with self._connect() as connection: - connection.executescript( - """ - CREATE TABLE IF NOT EXISTS mailboxes ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - label TEXT NOT NULL, - email TEXT NOT NULL UNIQUE, - client_id TEXT NOT NULL, - refresh_token TEXT NOT NULL, - proxy TEXT DEFAULT '', - preferred_method TEXT NOT NULL DEFAULT 'graph_api', - notes TEXT DEFAULT '', - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS folder_cache ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mailbox_id INTEGER NOT NULL, - method TEXT NOT NULL, - folder_id TEXT NOT NULL, - name TEXT NOT NULL, - display_name TEXT NOT NULL, - kind TEXT NOT NULL DEFAULT 'custom', - total INTEGER NOT NULL DEFAULT 0, - unread INTEGER NOT NULL DEFAULT 0, - is_default INTEGER NOT NULL DEFAULT 0, - cached_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - UNIQUE(mailbox_id, method, folder_id), - FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS message_cache ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mailbox_id INTEGER NOT NULL, - method TEXT NOT NULL, - provider_message_id TEXT NOT NULL, - internet_message_id TEXT NOT NULL DEFAULT '', - conversation_id TEXT NOT NULL DEFAULT '', - folder_id TEXT NOT NULL DEFAULT '', - subject TEXT NOT NULL DEFAULT '', - sender TEXT NOT NULL DEFAULT '', - sender_name TEXT NOT NULL DEFAULT '', - received_at TEXT NOT NULL DEFAULT '', - is_read INTEGER NOT NULL DEFAULT 0, - is_flagged INTEGER NOT NULL DEFAULT 0, - importance TEXT NOT NULL DEFAULT 'normal', - has_attachments INTEGER NOT NULL DEFAULT 0, - preview TEXT NOT NULL DEFAULT '', - body_text TEXT NOT NULL DEFAULT '', - body_html TEXT NOT NULL DEFAULT '', - to_recipients_json TEXT NOT NULL DEFAULT '[]', - cc_recipients_json TEXT NOT NULL DEFAULT '[]', - bcc_recipients_json TEXT NOT NULL DEFAULT '[]', - headers_json TEXT NOT NULL DEFAULT '{}', - in_reply_to TEXT NOT NULL DEFAULT '', - references_json TEXT NOT NULL DEFAULT '[]', - raw_json TEXT NOT NULL DEFAULT '{}', - cached_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - UNIQUE(mailbox_id, method, provider_message_id), - FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS attachment_cache ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mailbox_id INTEGER NOT NULL, - method TEXT NOT NULL, - provider_message_id TEXT NOT NULL, - attachment_id TEXT NOT NULL, - name TEXT NOT NULL, - content_type TEXT NOT NULL DEFAULT '', - size INTEGER NOT NULL DEFAULT 0, - is_inline INTEGER NOT NULL DEFAULT 0, - content_base64 TEXT NOT NULL DEFAULT '', - cached_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - UNIQUE(mailbox_id, method, provider_message_id, attachment_id), - FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS message_meta ( - mailbox_id INTEGER NOT NULL, - method TEXT NOT NULL, - provider_message_id TEXT NOT NULL, - tags_json TEXT NOT NULL DEFAULT '[]', - tags_text TEXT NOT NULL DEFAULT '', - follow_up TEXT NOT NULL DEFAULT '', - notes TEXT NOT NULL DEFAULT '', - snoozed_until TEXT NOT NULL DEFAULT '', - status TEXT NOT NULL DEFAULT 'active', - updated_at TEXT NOT NULL, - PRIMARY KEY(mailbox_id, method, provider_message_id), - FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS saved_rules ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mailbox_id INTEGER NOT NULL, - name TEXT NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1, - priority INTEGER NOT NULL DEFAULT 100, - conditions_json TEXT NOT NULL DEFAULT '{}', - actions_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS audit_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mailbox_id INTEGER, - actor TEXT NOT NULL, - action TEXT NOT NULL, - target_type TEXT NOT NULL, - target_id TEXT NOT NULL DEFAULT '', - status TEXT NOT NULL DEFAULT 'success', - details_json TEXT NOT NULL DEFAULT '{}', - created_at TEXT NOT NULL, - FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id) ON DELETE SET NULL - ); - - CREATE TABLE IF NOT EXISTS sync_jobs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mailbox_id INTEGER NOT NULL, - method TEXT NOT NULL, - requested_by TEXT NOT NULL, - scope_json TEXT NOT NULL DEFAULT '{}', - status TEXT NOT NULL DEFAULT 'queued', - processed_messages INTEGER NOT NULL DEFAULT 0, - cached_messages INTEGER NOT NULL DEFAULT 0, - folders_synced INTEGER NOT NULL DEFAULT 0, - error TEXT NOT NULL DEFAULT '', - started_at TEXT NOT NULL, - finished_at TEXT NOT NULL DEFAULT '', - FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id) ON DELETE CASCADE - ); - - CREATE TABLE IF NOT EXISTS sync_state ( - mailbox_id INTEGER NOT NULL, - method TEXT NOT NULL, - folder_id TEXT NOT NULL, - last_synced_at TEXT NOT NULL, - last_message_at TEXT NOT NULL DEFAULT '', - cached_messages INTEGER NOT NULL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'idle', - error TEXT NOT NULL DEFAULT '', - updated_at TEXT NOT NULL, - PRIMARY KEY(mailbox_id, method, folder_id), - FOREIGN KEY(mailbox_id) REFERENCES mailboxes(id) ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS idx_folder_cache_mailbox_method - ON folder_cache(mailbox_id, method); - CREATE INDEX IF NOT EXISTS idx_message_cache_mailbox_method_received - ON message_cache(mailbox_id, method, received_at DESC); - CREATE INDEX IF NOT EXISTS idx_message_cache_conversation - ON message_cache(mailbox_id, conversation_id); - CREATE INDEX IF NOT EXISTS idx_message_cache_internet_message - ON message_cache(mailbox_id, internet_message_id); - CREATE INDEX IF NOT EXISTS idx_message_cache_folder - ON message_cache(mailbox_id, method, folder_id); - CREATE INDEX IF NOT EXISTS idx_attachment_cache_message - ON attachment_cache(mailbox_id, method, provider_message_id); - CREATE INDEX IF NOT EXISTS idx_saved_rules_mailbox - ON saved_rules(mailbox_id, enabled, priority); - CREATE INDEX IF NOT EXISTS idx_audit_logs_mailbox_created - ON audit_logs(mailbox_id, created_at DESC); - CREATE INDEX IF NOT EXISTS idx_sync_jobs_mailbox_started - ON sync_jobs(mailbox_id, started_at DESC); - """ - ) - try: - connection.execute( - """ - CREATE VIRTUAL TABLE IF NOT EXISTS message_search USING fts5( - document_id UNINDEXED, - mailbox_id UNINDEXED, - method UNINDEXED, - provider_message_id UNINDEXED, - folder_id UNINDEXED, - subject, - sender, - preview, - body_text, - tags, - notes, - tokenize = 'unicode61' - ) - """ - ) - except sqlite3.OperationalError: - self._fts_enabled = False - - def _connect(self) -> sqlite3.Connection: - connection = sqlite3.connect(self.db_path) - connection.row_factory = sqlite3.Row - connection.execute("PRAGMA foreign_keys = ON") - return connection - - def _normalize_payload( - self, - payload: dict[str, Any], - *, - partial: bool, - current: MailboxProfile | None = None, - ) -> dict[str, str]: - if not isinstance(payload, dict): - raise MailboxStoreError("邮箱档案参数必须是对象") - - email = self._normalize_text(payload.get("email"), fallback=current.email if current else None) - client_id = self._normalize_text( - payload.get("client_id"), - fallback=current.client_id if current else None, - ) - refresh_token = self._normalize_text( - payload.get("refresh_token"), - fallback=current.refresh_token if current else None, - ) - - if not partial and not email: - raise MailboxStoreError("邮箱账号不能为空") - if not partial and not client_id: - raise MailboxStoreError("Client ID 不能为空") - if not partial and not refresh_token: - raise MailboxStoreError("Refresh Token 不能为空") - - label = self._normalize_text(payload.get("label"), fallback=current.label if current else None) - label = label or email - proxy = self._normalize_text(payload.get("proxy"), fallback=current.proxy if current else None) or "" - notes = self._normalize_text(payload.get("notes"), fallback=current.notes if current else None) or "" - preferred_method = ( - self._normalize_text( - payload.get("preferred_method"), - fallback=current.preferred_method if current else DEFAULT_METHOD, - ) - or DEFAULT_METHOD - ) - - if preferred_method not in VALID_METHODS: - raise MailboxStoreError("preferred_method 仅支持 graph_api、imap_new、imap_old") - - return { - "label": label or "", - "email": email or "", - "client_id": client_id or "", - "refresh_token": refresh_token or "", - "proxy": proxy, - "preferred_method": preferred_method, - "notes": notes, - } - - def _normalize_folder_record( - self, - mailbox_id: int, - method: str, - raw: Any, - now: str, - ) -> FolderCacheEntry: - item = self._coerce_mapping(raw) - folder_id = self._normalize_optional_string(item.get("id")) or self._normalize_optional_string(item.get("name")) or "custom" - display_name = self._normalize_optional_string(item.get("display_name")) or folder_id - name = self._normalize_optional_string(item.get("name")) or display_name - return FolderCacheEntry( - mailbox_id=mailbox_id, - method=method, - folder_id=folder_id, - name=name, - display_name=display_name, - kind=self._normalize_optional_string(item.get("kind")) or "custom", - total=self._coerce_int(item.get("total"), default=0), - unread=self._coerce_int(item.get("unread"), default=0), - is_default=bool(item.get("is_default", False)), - cached_at=now, - updated_at=now, - ) - - def _normalize_message_record( - self, - mailbox_id: int, - method: str, - raw: Any, - now: str, - ) -> dict[str, Any]: - item = self._coerce_mapping(raw) - provider_message_id = self._normalize_optional_string(item.get("message_id") or item.get("id")) - if not provider_message_id: - raise MailboxStoreError("消息缓存缺少 provider message id") - - headers = self._coerce_headers(item.get("headers")) - attachments_provided = "attachments" in item - attachments = item.get("attachments") if attachments_provided else [] - to_recipients = self._coerce_string_list(item.get("to_recipients") or item.get("to")) - cc_recipients = self._coerce_string_list(item.get("cc_recipients") or item.get("cc")) - bcc_recipients = self._coerce_string_list(item.get("bcc_recipients") or item.get("bcc")) - references = self._parse_reference_header(headers.get("References", "")) - body_text = self._normalize_optional_string(item.get("body_text") or item.get("body")) or "" - preview = self._normalize_optional_string(item.get("preview")) or (body_text[:180] if body_text else "") - - return { - "mailbox_id": mailbox_id, - "method": method, - "provider_message_id": provider_message_id, - "internet_message_id": self._normalize_optional_string(item.get("internet_message_id")) or "", - "conversation_id": self._normalize_optional_string(item.get("conversation_id")) or "", - "folder_id": self._normalize_optional_string(item.get("folder")) or "", - "subject": self._normalize_optional_string(item.get("subject")) or "", - "sender": self._normalize_optional_string(item.get("sender")) or "", - "sender_name": self._normalize_optional_string(item.get("sender_name")) or "", - "received_at": self._normalize_optional_string(item.get("received_at")) or "", - "is_read": int(bool(item.get("is_read", False))), - "is_flagged": int(bool(item.get("is_flagged", False))), - "importance": self._normalize_optional_string(item.get("importance")) or "normal", - "has_attachments": int(bool(item.get("has_attachments", False) or attachments)), - "preview": preview, - "body_text": body_text, - "body_html": self._normalize_optional_string(item.get("body_html")) or "", - "to_recipients_json": self._json_dumps(to_recipients), - "cc_recipients_json": self._json_dumps(cc_recipients), - "bcc_recipients_json": self._json_dumps(bcc_recipients), - "headers_json": self._json_dumps(headers), - "in_reply_to": self._normalize_optional_string(headers.get("In-Reply-To")) or "", - "references_json": self._json_dumps(references), - "raw_json": self._json_dumps(self._jsonable(item)), - "cached_at": now, - "updated_at": now, - "attachments": list(attachments) if isinstance(attachments, list) else [], - "attachments_provided": attachments_provided, - } - - def _replace_attachment_cache( - self, - connection: sqlite3.Connection, - *, - mailbox_id: int, - method: str, - message_id: str, - attachments: Iterable[Any], - now: str, - ) -> None: - connection.execute( - "DELETE FROM attachment_cache WHERE mailbox_id = ? AND method = ? AND provider_message_id = ?", - (mailbox_id, method, message_id), - ) - records = [ - self._normalize_attachment_record(mailbox_id, method, message_id, item, now) - for item in attachments - ] - if not records: - return - connection.executemany( - """ - INSERT INTO attachment_cache ( - mailbox_id, - method, - provider_message_id, - attachment_id, - name, - content_type, - size, - is_inline, - content_base64, - cached_at, - updated_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - [ - ( - record.mailbox_id, - record.method, - record.message_id, - record.attachment_id, - record.name, - record.content_type, - record.size, - int(record.is_inline), - record.content_base64, - record.cached_at, - record.updated_at, - ) - for record in records - ], - ) - - def _normalize_attachment_record( - self, - mailbox_id: int, - method: str, - message_id: str, - raw: Any, - now: str, - ) -> CachedAttachment: - item = self._coerce_mapping(raw) - attachment_id = self._normalize_optional_string(item.get("attachment_id") or item.get("id")) - if not attachment_id: - raise MailboxStoreError("附件缓存缺少 attachment id") - return CachedAttachment( - mailbox_id=mailbox_id, - method=method, - message_id=message_id, - attachment_id=attachment_id, - name=self._normalize_optional_string(item.get("name")) or attachment_id, - content_type=self._normalize_optional_string(item.get("content_type")) or "", - size=self._coerce_int(item.get("size"), default=0), - is_inline=bool(item.get("is_inline", False)), - content_base64=self._normalize_optional_string(item.get("content_base64")) or "", - cached_at=now, - updated_at=now, - ) - - def _refresh_search_document( - self, - connection: sqlite3.Connection, - mailbox_id: int, - method: str, - message_id: str, - ) -> None: - if not self._fts_enabled: - return - - document_id = self._build_document_id(mailbox_id, method, message_id) - connection.execute("DELETE FROM message_search WHERE document_id = ?", (document_id,)) - row = connection.execute( - """ - SELECT - c.mailbox_id, - c.method, - c.provider_message_id, - c.folder_id, - c.subject, - c.sender, - c.preview, - c.body_text, - COALESCE(mm.tags_text, '') AS tags_text, - COALESCE(mm.notes, '') AS notes - FROM message_cache AS c - LEFT JOIN message_meta AS mm - ON mm.mailbox_id = c.mailbox_id - AND mm.method = c.method - AND mm.provider_message_id = c.provider_message_id - WHERE c.mailbox_id = ? AND c.method = ? AND c.provider_message_id = ? - LIMIT 1 - """, - (mailbox_id, method, message_id), - ).fetchone() - if not row: - return - connection.execute( - """ - INSERT INTO message_search ( - document_id, - mailbox_id, - method, - provider_message_id, - folder_id, - subject, - sender, - preview, - body_text, - tags, - notes - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - document_id, - str(row["mailbox_id"]), - str(row["method"]), - str(row["provider_message_id"]), - str(row["folder_id"] or ""), - str(row["subject"] or ""), - str(row["sender"] or ""), - str(row["preview"] or ""), - str(row["body_text"] or ""), - str(row["tags_text"] or ""), - str(row["notes"] or ""), - ), - ) - - def _list_attachments_from_connection( - self, - connection: sqlite3.Connection, - *, - mailbox_id: int, - method: str, - message_id: str, - ) -> list[CachedAttachment]: - rows = connection.execute( - """ - SELECT - mailbox_id, - method, - provider_message_id, - attachment_id, - name, - content_type, - size, - is_inline, - content_base64, - cached_at, - updated_at - FROM attachment_cache - WHERE mailbox_id = ? AND method = ? AND provider_message_id = ? - ORDER BY id ASC - """, - (mailbox_id, method, message_id), - ).fetchall() - return [self._row_to_attachment(row) for row in rows] - - def _list_attachments_map_from_connection( - self, - connection: sqlite3.Connection, - *, - mailbox_id: int, - method: str | None, - message_ids: list[str], - ) -> dict[str, list[CachedAttachment]]: - unique_ids = [item for item in dict.fromkeys(message_ids) if item] - if not unique_ids: - return {} - placeholders = ", ".join("?" for _ in unique_ids) - parameters: list[Any] = [mailbox_id] - method_clause = "" - if method: - method_clause = "AND method = ?" - parameters.append(method) - rows = connection.execute( - f""" - SELECT - mailbox_id, - method, - provider_message_id, - attachment_id, - name, - content_type, - size, - is_inline, - content_base64, - cached_at, - updated_at - FROM attachment_cache - WHERE mailbox_id = ? - {method_clause} - AND provider_message_id IN ({placeholders}) - ORDER BY id ASC - """, - [*parameters, *unique_ids], - ).fetchall() - grouped: dict[str, list[CachedAttachment]] = {} - for row in rows: - grouped.setdefault(str(row["provider_message_id"]), []).append(self._row_to_attachment(row)) - return grouped - - @staticmethod - def _normalize_text(value: Any, fallback: str | None = None) -> str | None: - if value is None: - return fallback - if not isinstance(value, str): - raise MailboxStoreError("邮箱档案字段必须是字符串") - cleaned = value.strip() - return cleaned or None - - @staticmethod - def _normalize_optional_string(value: Any) -> str | None: - if value is None: - return None - if not isinstance(value, str): - raise MailboxStoreError("请求字段必须是字符串") - cleaned = value.strip() - return cleaned or None - - @staticmethod - def _require_non_empty_text(value: Any, message: str) -> str: - if not isinstance(value, str) or not value.strip(): - raise MailboxStoreError(message) - return value.strip() - - @staticmethod - def _normalize_method_value(method: str) -> str: - normalized = str(method or "").strip() - if normalized not in VALID_METHODS: - raise MailboxStoreError("不支持的邮件接入方式") - return normalized - - @staticmethod - def _normalize_tags(value: list[str] | None) -> list[str]: - if value is None: - return [] - if not isinstance(value, list): - raise MailboxStoreError("tags 必须是数组") - tags: list[str] = [] - seen: set[str] = set() - for item in value: - if not isinstance(item, str) or not item.strip(): - raise MailboxStoreError("tags 中的每一项都必须是非空字符串") - normalized = item.strip() - key = normalized.casefold() - if key in seen: - continue - seen.add(key) - tags.append(normalized) - return tags - - @staticmethod - def _coerce_int(value: Any, *, default: int) -> int: - if value in (None, ""): - return default - try: - return int(value) - except (TypeError, ValueError) as exc: - raise MailboxStoreError("请求字段必须是整数") from exc - - def _coerce_mapping(self, value: Any) -> dict[str, Any]: - if is_dataclass(value): - return asdict(value) - if isinstance(value, dict): - return dict(value) - raise MailboxStoreError("缓存对象必须是 dataclass 或 dict") - - @staticmethod - def _coerce_string_list(value: Any) -> list[str]: - if value is None: - return [] - if not isinstance(value, list): - raise MailboxStoreError("收件人字段必须是数组") - items: list[str] = [] - for item in value: - if not isinstance(item, str): - raise MailboxStoreError("收件人列表中的每一项都必须是字符串") - cleaned = item.strip() - if cleaned: - items.append(cleaned) - return items - - @staticmethod - def _coerce_headers(value: Any) -> dict[str, str]: - if value is None: - return {} - if not isinstance(value, dict): - raise MailboxStoreError("headers 必须是对象") - normalized: dict[str, str] = {} - for key, item in value.items(): - if not isinstance(key, str): - raise MailboxStoreError("headers 的键必须是字符串") - if item is None: - continue - normalized[key.strip()] = str(item).strip() - return normalized - - @staticmethod - def _parse_reference_header(value: str) -> list[str]: - if not value: - return [] - return [item for item in value.replace("\r", " ").replace("\n", " ").split(" ") if item] - - @staticmethod - def _jsonable(value: Any) -> Any: - if is_dataclass(value): - return {key: MailboxStore._jsonable(item) for key, item in asdict(value).items()} - if isinstance(value, list): - return [MailboxStore._jsonable(item) for item in value] - if isinstance(value, tuple): - return [MailboxStore._jsonable(item) for item in value] - if isinstance(value, dict): - return {str(key): MailboxStore._jsonable(item) for key, item in value.items()} - return value - - @staticmethod - def _json_dumps(value: Any) -> str: - return json.dumps(value, ensure_ascii=False, sort_keys=True) - - @staticmethod - def _json_loads(value: Any, default: Any) -> Any: - if not value: - return default - try: - return json.loads(str(value)) - except json.JSONDecodeError: - return default - - @staticmethod - def _build_document_id(mailbox_id: int, method: str, message_id: str) -> str: - return f"{mailbox_id}|{method}|{message_id}" - - @staticmethod - def _build_fts_query(query: str) -> str: - tokens = [item.replace('"', "").strip() for item in query.split() if item.strip()] - if not tokens: - return '""' - return " AND ".join(f'"{token}"' for token in tokens) - - @staticmethod - def _row_to_profile(row: sqlite3.Row) -> MailboxProfile: - return MailboxProfile( - id=int(row["id"]), - label=str(row["label"]), - email=str(row["email"]), - client_id=str(row["client_id"]), - refresh_token=str(row["refresh_token"]), - proxy=str(row["proxy"]) if row["proxy"] else None, - preferred_method=str(row["preferred_method"]), - notes=str(row["notes"] or ""), - created_at=str(row["created_at"]), - updated_at=str(row["updated_at"]), - ) - - @staticmethod - def _row_to_summary(row: sqlite3.Row) -> MailboxSummary: - return MailboxSummary( - id=int(row["id"]), - label=str(row["label"]), - email=str(row["email"]), - preferred_method=str(row["preferred_method"]), - notes=str(row["notes"] or ""), - created_at=str(row["created_at"]), - updated_at=str(row["updated_at"]), - ) - - @staticmethod - def _row_to_folder_entry(row: sqlite3.Row) -> FolderCacheEntry: - return FolderCacheEntry( - mailbox_id=int(row["mailbox_id"]), - method=str(row["method"]), - folder_id=str(row["folder_id"]), - name=str(row["name"]), - display_name=str(row["display_name"]), - kind=str(row["kind"]), - total=int(row["total"] or 0), - unread=int(row["unread"] or 0), - is_default=bool(row["is_default"]), - cached_at=str(row["cached_at"]), - updated_at=str(row["updated_at"]), - ) - - def _row_to_attachment(self, row: sqlite3.Row) -> CachedAttachment: - return CachedAttachment( - mailbox_id=int(row["mailbox_id"]), - method=str(row["method"]), - message_id=str(row["provider_message_id"]), - attachment_id=str(row["attachment_id"]), - name=str(row["name"]), - content_type=str(row["content_type"] or ""), - size=int(row["size"] or 0), - is_inline=bool(row["is_inline"]), - content_base64=str(row["content_base64"] or ""), - cached_at=str(row["cached_at"]), - updated_at=str(row["updated_at"]), - ) - - def _row_to_message_meta(self, row: sqlite3.Row) -> MessageMeta: - return MessageMeta( - mailbox_id=int(row["mailbox_id"]), - method=str(row["method"]), - message_id=str(row["provider_message_id"]), - tags=list(self._json_loads(row["tags_json"], [])), - follow_up=str(row["follow_up"] or ""), - notes=str(row["notes"] or ""), - snoozed_until=str(row["snoozed_until"] or ""), - status=str(row["status"] or row["meta_status"] or "active"), - updated_at=str(row["updated_at"] or row["meta_updated_at"] or ""), - ) - - def _row_to_cached_message( - self, - row: sqlite3.Row, - *, - attachments: list[CachedAttachment], - ) -> CachedMessage: - meta = None - if "tags_json" in row.keys(): - meta = MessageMeta( - mailbox_id=int(row["mailbox_id"]), - method=str(row["method"]), - message_id=str(row["provider_message_id"]), - tags=list(self._json_loads(row["tags_json"], [])), - follow_up=str(row["follow_up"] or ""), - notes=str(row["notes"] or ""), - snoozed_until=str(row["snoozed_until"] or ""), - status=str(row["meta_status"] or "active"), - updated_at=str(row["meta_updated_at"] or ""), - ) - return CachedMessage( - mailbox_id=int(row["mailbox_id"]), - mailbox_label=str(row["mailbox_label"] or ""), - mailbox_email=str(row["mailbox_email"] or ""), - method=str(row["method"]), - message_id=str(row["provider_message_id"]), - subject=str(row["subject"] or ""), - sender=str(row["sender"] or ""), - sender_name=str(row["sender_name"] or ""), - received_at=str(row["received_at"] or ""), - is_read=bool(row["is_read"]), - is_flagged=bool(row["is_flagged"]), - importance=str(row["importance"] or "normal"), - has_attachments=bool(row["has_attachments"]), - preview=str(row["preview"] or ""), - body_text=str(row["body_text"] or ""), - body_html=str(row["body_html"] or "") or None, - folder=str(row["folder_id"] or ""), - internet_message_id=str(row["internet_message_id"] or ""), - conversation_id=str(row["conversation_id"] or ""), - to_recipients=list(self._json_loads(row["to_recipients_json"], [])), - cc_recipients=list(self._json_loads(row["cc_recipients_json"], [])), - bcc_recipients=list(self._json_loads(row["bcc_recipients_json"], [])), - headers=dict(self._json_loads(row["headers_json"], {})), - in_reply_to=str(row["in_reply_to"] or ""), - references=list(self._json_loads(row["references_json"], [])), - attachments=attachments, - meta=meta, - cached_at=str(row["cached_at"]), - updated_at=str(row["updated_at"]), - ) - - def _row_to_search_result(self, row: sqlite3.Row) -> MessageSearchResult: - meta = MessageMeta( - mailbox_id=int(row["mailbox_id"]), - method=str(row["method"]), - message_id=str(row["provider_message_id"]), - tags=list(self._json_loads(row["tags_json"], [])), - follow_up=str(row["follow_up"] or ""), - notes=str(row["notes"] or ""), - snoozed_until=str(row["snoozed_until"] or ""), - status=str(row["meta_status"] or "active"), - updated_at=str(row["meta_updated_at"] or ""), - ) - return MessageSearchResult( - mailbox_id=int(row["mailbox_id"]), - mailbox_label=str(row["mailbox_label"] or ""), - mailbox_email=str(row["mailbox_email"] or ""), - method=str(row["method"]), - message_id=str(row["provider_message_id"]), - subject=str(row["subject"] or ""), - sender=str(row["sender"] or ""), - sender_name=str(row["sender_name"] or ""), - received_at=str(row["received_at"] or ""), - is_read=bool(row["is_read"]), - is_flagged=bool(row["is_flagged"]), - importance=str(row["importance"] or "normal"), - has_attachments=bool(row["has_attachments"]), - preview=str(row["preview"] or ""), - folder=str(row["folder_id"] or ""), - internet_message_id=str(row["internet_message_id"] or ""), - conversation_id=str(row["conversation_id"] or ""), - meta=meta, - ) - - def _row_to_rule(self, row: sqlite3.Row) -> SavedRule: - return SavedRule( - id=int(row["id"]), - mailbox_id=int(row["mailbox_id"]), - name=str(row["name"]), - enabled=bool(row["enabled"]), - priority=int(row["priority"] or 0), - conditions=dict(self._json_loads(row["conditions_json"], {})), - actions=dict(self._json_loads(row["actions_json"], {})), - created_at=str(row["created_at"]), - updated_at=str(row["updated_at"]), - ) - - def _row_to_audit_log(self, row: sqlite3.Row) -> AuditLogEntry: - return AuditLogEntry( - id=int(row["id"]), - mailbox_id=int(row["mailbox_id"]) if row["mailbox_id"] is not None else None, - mailbox_label=str(row["mailbox_label"] or ""), - mailbox_email=str(row["mailbox_email"] or ""), - actor=str(row["actor"]), - action=str(row["action"]), - target_type=str(row["target_type"]), - target_id=str(row["target_id"] or ""), - status=str(row["status"] or "success"), - details=dict(self._json_loads(row["details_json"], {})), - created_at=str(row["created_at"]), - ) - - def _row_to_sync_job(self, row: sqlite3.Row) -> SyncJob: - return SyncJob( - id=int(row["id"]), - mailbox_id=int(row["mailbox_id"]), - method=str(row["method"]), - requested_by=str(row["requested_by"]), - scope=dict(self._json_loads(row["scope_json"], {})), - status=str(row["status"]), - processed_messages=int(row["processed_messages"] or 0), - cached_messages=int(row["cached_messages"] or 0), - folders_synced=int(row["folders_synced"] or 0), - error=str(row["error"] or ""), - started_at=str(row["started_at"]), - finished_at=str(row["finished_at"] or ""), - ) - - @staticmethod - def _row_to_sync_state(row: sqlite3.Row) -> SyncState: - return SyncState( - mailbox_id=int(row["mailbox_id"]), - method=str(row["method"]), - folder_id=str(row["folder_id"]), - last_synced_at=str(row["last_synced_at"]), - last_message_at=str(row["last_message_at"] or ""), - cached_messages=int(row["cached_messages"] or 0), - status=str(row["status"] or "idle"), - error=str(row["error"] or ""), - updated_at=str(row["updated_at"]), - ) - - @staticmethod - def _utc_now() -> str: - return datetime.now(UTC).isoformat(timespec="seconds").replace("+00:00", "Z")