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):
-
+
-
+
-
+
@@ -865,7 +913,7 @@ class EmlManagerHandler(BaseHTTPRequestHandler):
请先打开目录导入邮件
-
+
邮件详情
@@ -873,16 +921,16 @@ class EmlManagerHandler(BaseHTTPRequestHandler):
-
+
就绪 | 邮件总数: 0
-
+