From 5c0f51e272f36878d108e1a754c1fcf69cc25446 Mon Sep 17 00:00:00 2001 From: gooker_young Date: Thu, 25 Jun 2026 12:12:04 +0800 Subject: [PATCH] ~ --- src/pyflowx/cli/emlmanager.py | 276 ++++++++++++++++++++-------------- 1 file changed, 162 insertions(+), 114 deletions(-) diff --git a/src/pyflowx/cli/emlmanager.py b/src/pyflowx/cli/emlmanager.py index 361bf19..110734d 100644 --- a/src/pyflowx/cli/emlmanager.py +++ b/src/pyflowx/cli/emlmanager.py @@ -8,6 +8,7 @@ from __future__ import annotations import argparse import email +import gzip import hashlib import json import sqlite3 @@ -15,7 +16,7 @@ import threading from datetime import datetime from email.header import decode_header from email.utils import parsedate_to_datetime -from http.server import BaseHTTPRequestHandler, HTTPServer +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from typing import Any from urllib.parse import parse_qs, urlparse @@ -86,7 +87,7 @@ class EmailDatabase: cursor = self.conn.cursor() cursor.execute( f""" - INSERT OR REPLACE INTO {TABLE_NAME} + INSERT OR REPLACE INTO {TABLE_NAME} (file_path, file_hash, subject, sender, recipients, date, date_parsed, body_text, body_html, has_attachments, file_size, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) @@ -112,29 +113,35 @@ class EmailDatabase: except sqlite3.Error: return False - def search_emails(self, keyword: str = "", field: str = "all") -> list[dict[str, Any]]: + def search_emails( + self, keyword: str = "", field: str = "all", limit: int = 100, offset: int = 0 + ) -> list[dict[str, Any]]: """搜索邮件.""" with self._lock: cursor = self.conn.cursor() + # 只返回必要字段,减少数据量 + select_fields = "id, subject, sender, date_parsed, has_attachments, file_size" + if not keyword: - cursor.execute(f"SELECT * FROM {TABLE_NAME} ORDER BY date_parsed DESC") + query = f"SELECT {select_fields} FROM {TABLE_NAME} ORDER BY date_parsed DESC LIMIT ? OFFSET ?" + cursor.execute(query, (limit, offset)) elif field == "subject": - query = f"SELECT * FROM {TABLE_NAME} WHERE subject LIKE ? ORDER BY date_parsed DESC" - cursor.execute(query, (f"%{keyword}%",)) + query = f"SELECT {select_fields} FROM {TABLE_NAME} WHERE subject LIKE ? ORDER BY date_parsed DESC LIMIT ? OFFSET ?" + cursor.execute(query, (f"%{keyword}%", limit, offset)) elif field == "sender": - query = f"SELECT * FROM {TABLE_NAME} WHERE sender LIKE ? ORDER BY date_parsed DESC" - cursor.execute(query, (f"%{keyword}%",)) + query = f"SELECT {select_fields} FROM {TABLE_NAME} WHERE sender LIKE ? ORDER BY date_parsed DESC LIMIT ? OFFSET ?" + cursor.execute(query, (f"%{keyword}%", limit, offset)) elif field == "recipients": - query = f"SELECT * FROM {TABLE_NAME} WHERE recipients LIKE ? ORDER BY date_parsed DESC" - cursor.execute(query, (f"%{keyword}%",)) + query = f"SELECT {select_fields} FROM {TABLE_NAME} WHERE recipients LIKE ? ORDER BY date_parsed DESC LIMIT ? OFFSET ?" + cursor.execute(query, (f"%{keyword}%", limit, offset)) else: # all query = f""" - SELECT * FROM {TABLE_NAME} + SELECT {select_fields} FROM {TABLE_NAME} WHERE subject LIKE ? OR sender LIKE ? OR recipients LIKE ? OR body_text LIKE ? - ORDER BY date_parsed DESC + ORDER BY date_parsed DESC LIMIT ? OFFSET ? """ - cursor.execute(query, (f"%{keyword}%", f"%{keyword}%", f"%{keyword}%", f"%{keyword}%")) + cursor.execute(query, (f"%{keyword}%", f"%{keyword}%", f"%{keyword}%", f"%{keyword}%", limit, offset)) columns = [description[0] for description in cursor.description] return [dict(zip(columns, row)) for row in cursor.fetchall()] @@ -312,7 +319,7 @@ class EmlManagerHandler(BaseHTTPRequestHandler): path = parsed_path.path query_params = parse_qs(parsed_path.query) - if path == "/" or path == "/index.html": + if path in {"/", "/index.html"}: self._serve_index() elif path == "/test": self._serve_test_page() @@ -344,13 +351,28 @@ class EmlManagerHandler(BaseHTTPRequestHandler): def _serve_index(self) -> None: """返回主页 HTML.""" html_content = self._get_html_template() - self.send_response(200) - self.send_header("Content-Type", "text/html; charset=utf-8") - self.send_header("Cache-Control", "no-cache, no-store, must-revalidate") - self.send_header("Pragma", "no-cache") - self.send_header("Expires", "0") - self.end_headers() - self.wfile.write(html_content.encode("utf-8")) + html_bytes = html_content.encode("utf-8") + + # 检查客户端是否支持gzip压缩 + accept_encoding = self.headers.get("Accept-Encoding", "") + if "gzip" in accept_encoding.lower(): + # 使用gzip压缩 + compressed = gzip.compress(html_bytes) + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Encoding", "gzip") + self.send_header("Content-Length", str(len(compressed))) + self.send_header("Cache-Control", "public, max-age=3600") + self.end_headers() + self.wfile.write(compressed) + else: + # 不压缩 + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(html_bytes))) + self.send_header("Cache-Control", "public, max-age=3600") + self.end_headers() + self.wfile.write(html_bytes) def _serve_test_page(self) -> None: """返回测试页面 HTML.""" @@ -396,11 +418,11 @@ class EmlManagerHandler(BaseHTTPRequestHandler):

EML 邮件管理器 API 测试

- + @@ -454,9 +476,18 @@ class EmlManagerHandler(BaseHTTPRequestHandler): keyword = query_params.get("keyword", [""])[0] field = query_params.get("field", ["all"])[0] + limit = int(query_params.get("limit", ["100"])[0]) # 默认返回100条 + offset = int(query_params.get("offset", ["0"])[0]) # 默认偏移0 - emails = self.db.search_emails(keyword, field) - self._send_json_response({"emails": emails, "count": len(emails)}) + emails = self.db.search_emails(keyword, field, limit, offset) + total_count = self.db.get_email_count() + self._send_json_response({ + "emails": emails, + "count": len(emails), + "total": total_count, + "limit": limit, + "offset": offset, + }) def _api_get_email(self, query_params: dict[str, list[str]]) -> None: """API: 获取单个邮件详情.""" @@ -561,11 +592,28 @@ class EmlManagerHandler(BaseHTTPRequestHandler): def _send_json_response(self, data: dict[str, Any], status_code: int = 200) -> None: """发送 JSON 响应.""" - self.send_response(status_code) - self.send_header("Content-Type", "application/json; charset=utf-8") - self.send_header("Access-Control-Allow-Origin", "*") - self.end_headers() - self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8")) + json_bytes = json.dumps(data, ensure_ascii=False).encode("utf-8") + + # 检查客户端是否支持gzip压缩 + accept_encoding = self.headers.get("Accept-Encoding", "") + if "gzip" in accept_encoding.lower(): + # 使用gzip压缩 + compressed = gzip.compress(json_bytes) + self.send_response(status_code) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Encoding", "gzip") + self.send_header("Content-Length", str(len(compressed))) + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(compressed) + else: + # 不压缩 + self.send_response(status_code) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(json_bytes))) + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(json_bytes) def _get_html_template(self) -> str: """获取 HTML 模板.""" @@ -581,20 +629,20 @@ class EmlManagerHandler(BaseHTTPRequestHandler): padding: 0; box-sizing: border-box; } - + body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #333; min-height: 100vh; } - + .container { max-width: 1400px; margin: 0 auto; padding: 20px; } - + .header { background: white; padding: 30px; @@ -602,20 +650,20 @@ class EmlManagerHandler(BaseHTTPRequestHandler): margin-bottom: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } - + .header h1 { color: #667eea; font-size: 32px; margin-bottom: 20px; } - + .toolbar { display: flex; gap: 15px; flex-wrap: wrap; align-items: center; } - + .btn { padding: 12px 24px; border: none; @@ -625,32 +673,32 @@ class EmlManagerHandler(BaseHTTPRequestHandler): cursor: pointer; transition: all 0.3s ease; } - + .btn-primary { background: #667eea; color: white; } - + .btn-primary:hover { background: #5568d3; transform: translateY(-2px); } - + .btn-danger { background: #e74c3c; color: white; } - + .btn-danger:hover { background: #c0392b; } - + .search-box { display: flex; gap: 10px; align-items: center; } - + .search-input { padding: 12px 20px; border: 2px solid #ddd; @@ -658,24 +706,24 @@ class EmlManagerHandler(BaseHTTPRequestHandler): font-size: 14px; width: 300px; } - + .search-input:focus { outline: none; border-color: #667eea; } - + .search-select { padding: 12px 15px; border: 2px solid #ddd; border-radius: 8px; font-size: 14px; } - + .view-toggle { display: flex; gap: 10px; } - + .radio-btn { padding: 8px 16px; background: #f8f9fa; @@ -684,20 +732,20 @@ class EmlManagerHandler(BaseHTTPRequestHandler): cursor: pointer; transition: all 0.3s ease; } - + .radio-btn.active { background: #667eea; color: white; border-color: #667eea; } - + .main-content { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; height: calc(100vh - 200px); } - + .email-list { background: white; border-radius: 15px; @@ -705,36 +753,36 @@ class EmlManagerHandler(BaseHTTPRequestHandler): box-shadow: 0 10px 30px rgba(0,0,0,0.1); overflow-y: auto; } - + .email-item { padding: 15px; border-bottom: 1px solid #eee; cursor: pointer; transition: all 0.3s ease; } - + .email-item:hover { background: #f8f9fa; } - + .email-item.active { background: #e8f4f8; border-left: 4px solid #667eea; } - + .email-subject { font-weight: 600; color: #2c3e50; margin-bottom: 5px; } - + .email-meta { display: flex; gap: 15px; color: #7f8c8d; font-size: 12px; } - + .email-group { background: #f8f9fa; padding: 10px 15px; @@ -743,7 +791,7 @@ class EmlManagerHandler(BaseHTTPRequestHandler): border-radius: 8px; margin-bottom: 10px; } - + .email-detail { background: white; border-radius: 15px; @@ -751,38 +799,38 @@ class EmlManagerHandler(BaseHTTPRequestHandler): box-shadow: 0 10px 30px rgba(0,0,0,0.1); overflow-y: auto; } - + .detail-header { border-bottom: 2px solid #eee; padding-bottom: 20px; margin-bottom: 20px; } - + .detail-title { font-size: 24px; color: #2c3e50; margin-bottom: 15px; } - + .detail-meta { display: grid; grid-template-columns: auto 1fr; gap: 10px 20px; color: #7f8c8d; } - + .detail-label { font-weight: 600; color: #2c3e50; } - + .detail-body { white-space: pre-wrap; font-size: 14px; line-height: 1.6; color: #34495e; } - + .status-bar { background: white; padding: 15px 30px; @@ -792,17 +840,17 @@ class EmlManagerHandler(BaseHTTPRequestHandler): text-align: center; color: #7f8c8d; } - + .loading { display: none; text-align: center; padding: 40px; } - + .loading.active { display: block; } - + .spinner { border: 4px solid #f3f3f3; border-top: 4px solid #667eea; @@ -812,18 +860,18 @@ class EmlManagerHandler(BaseHTTPRequestHandler): animation: spin 1s linear infinite; margin: 0 auto 20px; } - + @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } - + .empty-state { text-align: center; padding: 60px 20px; color: #7f8c8d; } - + .empty-state h3 { font-size: 20px; margin-bottom: 10px; @@ -838,7 +886,7 @@ class EmlManagerHandler(BaseHTTPRequestHandler): - +