增加临时邮箱功能

This commit is contained in:
luekongbai committed 2026-01-13 22:19:12 +08:00
1 parent 821b4bfd4f
commit 9bd31c3820
3 files changed
+1043 -37

No files matched your search

+39 -3
View File
@@ -19,6 +19,8 @@
- 📤 **导出功能** - 支持按分组导出邮箱账号信息
- 🎨 **现代化 UI** - 简洁美观的四栏式界面布局
- ⚡ **性能优化** - 邮箱列表缓存,快速切换分组
- 🔥 **临时邮箱** - 集成 GPTMail API,一键生成临时邮箱
- ⚙️ **系统设置** - 支持在线修改登录密码和 API Key
### 界面布局
Web 应用采用四栏式布局设计:
@@ -48,7 +50,7 @@ python web_outlook_app.py
启动后访问:http://127.0.0.1:5001
默认登录密码:`admin123`(可在 [`web_outlook_app.py`](web_outlook_app.py:27) 中修改 `LOGIN_PASSWORD` 变量)
默认登录密码:`admin123`(可在 Web 界面的「设置」中修改)
### 3. 运行命令行测试工具
@@ -112,16 +114,50 @@ Web 应用提供以下 API 端点:
| `/api/email/<email>/<message_id>` | GET | 获取邮件详情 |
| `/api/groups/<id>/export` | GET | 导出分组邮箱 |
| `/api/accounts/export` | GET | 导出所有邮箱 |
| `/api/temp-emails` | GET | 获取所有临时邮箱 |
| `/api/temp-emails/generate` | POST | 生成新的临时邮箱 |
| `/api/temp-emails/<email>` | DELETE | 删除临时邮箱 |
| `/api/temp-emails/<email>/messages` | GET | 获取临时邮箱邮件列表 |
| `/api/temp-emails/<email>/messages/<id>` | GET | 获取临时邮件详情 |
| `/api/temp-emails/<email>/clear` | DELETE | 清空临时邮箱所有邮件 |
| `/api/settings` | GET | 获取系统设置 |
| `/api/settings` | PUT | 更新系统设置 |
### 临时邮箱功能
系统集成了 GPTMail API 提供临时邮箱服务:
1. **生成临时邮箱** - 点击「临时邮箱」分组,然后点击「生成临时邮箱」按钮
2. **查看邮件** - 选择临时邮箱后点击「获取邮件」按钮
3. **清空邮件** - 点击临时邮箱旁的「清空」按钮,清空该邮箱的所有邮件
4. **删除邮箱** - 点击临时邮箱旁的「删除」按钮,删除邮箱及其所有邮件
临时邮箱数据会存储在本地数据库中,方便后续查看。
### 系统设置
点击导航栏的「⚙️ 设置」按钮,可以修改以下配置:
1. **登录密码** - 修改 Web 界面的登录密码
2. **GPTMail API Key** - 设置临时邮箱功能所需的 API Key
设置会保存在数据库中,重启应用后仍然有效。
## ⚙️ 配置说明
### Web 应用配置
在 [`web_outlook_app.py`](web_outlook_app.py) 中可以修改以下配置:
以下配置可以在 Web 界面的「设置」中修改:
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| 登录密码 | `admin123` | Web 界面登录密码 |
| GPTMail API Key | 空 | 临时邮箱功能的 API Key |
以下配置需要在 [`web_outlook_app.py`](web_outlook_app.py) 中修改:
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `LOGIN_PASSWORD` | `admin123` | 登录密码 |
| `DATABASE` | `outlook_accounts.db` | SQLite 数据库文件 |
| `IMAP_SERVER_OLD` | `outlook.office365.com` | 旧版 IMAP 服务器 |
| `IMAP_SERVER_NEW` | `outlook.live.com` | 新版 IMAP 服务器 |
+467 -30
View File
@@ -989,6 +989,7 @@
</div>
<div class="navbar-actions">
<button class="navbar-btn" onclick="showExportModal()">导出邮箱</button>
<button class="navbar-btn" onclick="showSettingsModal()">⚙️ 设置</button>
<button class="navbar-btn" onclick="logout()" style="color: #dc3545;">退出登录</button>
</div>
</nav>
@@ -1219,6 +1220,32 @@
</div>
</div>
<!-- 设置模态框 -->
<div class="modal" id="settingsModal">
<div class="modal-content" style="width: 480px;">
<div class="modal-header">
<h3>⚙️ 系统设置</h3>
<button class="modal-close" onclick="hideSettingsModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">登录密码</label>
<input type="password" class="form-input" id="settingsPassword" placeholder="输入新密码(留空则不修改)">
<div class="form-hint">用于登录系统的密码</div>
</div>
<div class="form-group">
<label class="form-label">GPTMail API Key</label>
<input type="text" class="form-input" id="settingsApiKey" placeholder="输入 GPTMail API Key">
<div class="form-hint">用于临时邮箱功能,可从 <a href="https://mail.chatgpt.org.uk/api" target="_blank" style="color: #0078d4;">gptmail.io</a> 获取</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="hideSettingsModal()">取消</button>
<button class="btn btn-primary" onclick="saveSettings()">保存设置</button>
</div>
</div>
</div>
<!-- 消息提示 -->
<div class="toast" id="toast"></div>
@@ -1233,6 +1260,8 @@
let accountsCache = {}; // 缓存各分组的邮箱列表
let editingGroupId = null;
let selectedColor = '#1a1a1a';
let isTempEmailGroup = false; // 是否是临时邮箱分组
let tempEmailGroupId = null; // 临时邮箱分组 ID
// 初始化
document.addEventListener('DOMContentLoaded', function() {
@@ -1299,6 +1328,13 @@
if (data.success) {
groups = data.groups;
// 找到临时邮箱分组
const tempGroup = groups.find(g => g.name === '临时邮箱');
if (tempGroup) {
tempEmailGroupId = tempGroup.id;
}
renderGroupList(data.groups);
updateGroupSelects();
@@ -1307,7 +1343,11 @@
const group = groups.find(g => g.id === currentGroupId);
if (group) {
// 刷新当前分组的邮箱列表
loadAccountsByGroup(currentGroupId, true);
if (currentGroupId === tempEmailGroupId) {
loadTempEmails(true);
} else {
loadAccountsByGroup(currentGroupId, true);
}
}
}
}
@@ -1330,36 +1370,45 @@
return;
}
container.innerHTML = groups.map(group => `
<div class="group-item ${currentGroupId === group.id ? 'active' : ''}"
data-group-id="${group.id}"
onclick="selectGroup(${group.id})">
<div class="group-row-1">
<div class="group-color" style="background-color: ${group.color || '#666'}"></div>
<span class="group-name">${escapeHtml(group.name)}</span>
</div>
<div class="group-row-2">
<span class="group-count">${group.account_count || 0} 个邮箱</span>
<div class="group-actions">
<button class="group-action-btn" onclick="event.stopPropagation(); editGroup(${group.id})" title="编辑">✏️</button>
${group.id !== 1 ? `<button class="group-action-btn" onclick="event.stopPropagation(); deleteGroup(${group.id})" title="删除">🗑️</button>` : ''}
container.innerHTML = groups.map(group => {
const isSystem = group.is_system === 1 || group.name === '临时邮箱';
const isTempGroup = group.name === '临时邮箱';
const isDefault = group.id === 1;
return `
<div class="group-item ${currentGroupId === group.id ? 'active' : ''} ${isTempGroup ? 'temp-email-group' : ''}"
data-group-id="${group.id}"
onclick="selectGroup(${group.id})">
<div class="group-row-1">
<div class="group-color" style="background-color: ${group.color || '#666'}"></div>
<span class="group-name">${escapeHtml(group.name)}${isTempGroup ? ' ⚡' : ''}</span>
</div>
<div class="group-row-2">
<span class="group-count">${group.account_count || 0} 个邮箱</span>
<div class="group-actions">
${!isSystem ? `<button class="group-action-btn" onclick="event.stopPropagation(); editGroup(${group.id})" title="编辑">✏️</button>` : ''}
${!isDefault && !isSystem ? `<button class="group-action-btn" onclick="event.stopPropagation(); deleteGroup(${group.id})" title="删除">🗑️</button>` : ''}
</div>
</div>
</div>
</div>
`).join('');
`;
}).join('');
}
// 选择分组
async function selectGroup(groupId) {
currentGroupId = groupId;
// 检查是否是临时邮箱分组
const group = groups.find(g => g.id === groupId);
isTempEmailGroup = group && group.name === '临时邮箱';
// 更新分组列表 UI
document.querySelectorAll('.group-item').forEach(item => {
item.classList.toggle('active', parseInt(item.dataset.groupId) === groupId);
});
// 更新邮箱面板标题
const group = groups.find(g => g.id === groupId);
if (group) {
document.getElementById('currentGroupName').textContent = group.name;
document.getElementById('currentGroupColor').style.backgroundColor = group.color || '#666';
@@ -1371,8 +1420,29 @@
}
}
// 更新底部按钮
updateAccountPanelFooter();
// 加载该分组的邮箱
await loadAccountsByGroup(groupId);
if (isTempEmailGroup) {
await loadTempEmails();
} else {
await loadAccountsByGroup(groupId);
}
}
// 更新账号面板底部按钮
function updateAccountPanelFooter() {
const footer = document.querySelector('.account-panel-footer');
if (isTempEmailGroup) {
footer.innerHTML = `
<button class="add-account-btn" onclick="generateTempEmail()">+ 生成临时邮箱</button>
`;
} else {
footer.innerHTML = `
<button class="add-account-btn" onclick="showAddAccountModal()">+ 导入邮箱</button>
`;
}
}
// 加载分组下的账号
@@ -1568,25 +1638,318 @@
}
}
// ==================== 账号相关 ====================
// ==================== 临时邮箱相关 ====================
// 选择账号
function selectAccount(email) {
// 加载临时邮箱列表
async function loadTempEmails(forceRefresh = false) {
const container = document.getElementById('accountList');
if (!forceRefresh && accountsCache['temp']) {
renderTempEmailList(accountsCache['temp']);
return;
}
container.innerHTML = '<div class="loading loading-small"><div class="loading-spinner"></div></div>';
try {
const response = await fetch('/api/temp-emails');
const data = await response.json();
if (data.success) {
accountsCache['temp'] = data.emails;
renderTempEmailList(data.emails);
const group = groups.find(g => g.name === '临时邮箱');
if (group) {
group.account_count = data.emails.length;
renderGroupList(groups);
}
}
} catch (error) {
container.innerHTML = '<div class="empty-state"><div class="empty-state-text">加载失败</div></div>';
}
}
// 渲染临时邮箱列表
function renderTempEmailList(emails) {
const container = document.getElementById('accountList');
if (emails.length === 0) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚡</div>
<div class="empty-state-text">暂无临时邮箱<br>点击下方按钮生成</div>
</div>
`;
return;
}
container.innerHTML = emails.map(email => `
<div class="account-item ${currentAccount === email.email ? 'active' : ''}"
onclick="selectTempEmail('${email.email}')">
<div class="account-email" title="${email.email}">
${email.email}
</div>
<div class="account-actions">
<button class="account-action-btn" onclick="event.stopPropagation(); copyEmail('${email.email}')" title="复制邮箱">复制</button>
<button class="account-action-btn" onclick="event.stopPropagation(); clearTempEmailMessages('${email.email}')" title="清空邮件">清空</button>
<button class="account-action-btn delete" onclick="event.stopPropagation(); deleteTempEmail('${email.email}')" title="删除">删除</button>
</div>
</div>
`).join('');
}
// 生成临时邮箱
async function generateTempEmail() {
// 获取按钮并显示加载状态
const btn = document.querySelector('.account-panel-footer .add-account-btn');
const originalText = btn.textContent;
btn.disabled = true;
btn.textContent = '⏳ 生成中...';
btn.style.opacity = '0.7';
btn.style.cursor = 'not-allowed';
try {
const response = await fetch('/api/temp-emails/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const data = await response.json();
if (data.success) {
showToast(`临时邮箱已生成: ${data.email}`, 'success');
delete accountsCache['temp'];
loadTempEmails(true);
loadGroups();
} else {
showToast(data.error || '生成失败', 'error');
}
} catch (error) {
showToast('生成临时邮箱失败', 'error');
} finally {
// 恢复按钮状态
btn.disabled = false;
btn.textContent = originalText;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
}
// 选择临时邮箱
function selectTempEmail(email) {
currentAccount = email;
isTempEmailGroup = true;
// 更新顶部当前账号显示
document.getElementById('currentAccount').classList.add('show');
document.getElementById('currentAccountEmail').textContent = email;
document.getElementById('currentAccountEmail').textContent = email + ' (临时)';
// 更新邮箱列表 UI
document.querySelectorAll('.account-item').forEach(item => {
item.classList.remove('active');
if (item.querySelector('.account-email').textContent === email) {
const emailEl = item.querySelector('.account-email');
if (emailEl && emailEl.textContent.includes(email)) {
item.classList.add('active');
}
});
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📬</div>
<div class="empty-state-text">点击"获取邮件"按钮获取邮件</div>
</div>
`;
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
`;
document.getElementById('emailDetailToolbar').style.display = 'none';
document.getElementById('emailCount').textContent = '';
document.getElementById('methodTag').style.display = 'none';
}
// 清空临时邮箱的所有邮件
async function clearTempEmailMessages(email) {
if (!confirm(`确定要清空临时邮箱 ${email} 的所有邮件吗?`)) {
return;
}
try {
const response = await fetch(`/api/temp-emails/${encodeURIComponent(email)}/clear`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('邮件已清空', 'success');
// 如果当前选中的就是这个邮箱,清空邮件列表
if (currentAccount === email) {
currentEmails = [];
document.getElementById('emailCount').textContent = '(0)';
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-text">收件箱为空</div>
</div>
`;
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
`;
document.getElementById('emailDetailToolbar').style.display = 'none';
}
} else {
showToast(data.error || '清空失败', 'error');
}
} catch (error) {
showToast('清空失败', 'error');
}
}
// 删除临时邮箱
async function deleteTempEmail(email) {
if (!confirm(`确定要删除临时邮箱 ${email} 吗?\n该邮箱的所有邮件也将被删除。`)) {
return;
}
try {
const response = await fetch(`/api/temp-emails/${encodeURIComponent(email)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showToast('临时邮箱已删除', 'success');
delete accountsCache['temp'];
if (currentAccount === email) {
currentAccount = null;
document.getElementById('currentAccount').classList.remove('show');
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📬</div>
<div class="empty-state-text">请从左侧选择一个邮箱账号</div>
</div>
`;
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
<div class="empty-state-text">选择一封邮件查看详情</div>
</div>
`;
}
loadTempEmails(true);
loadGroups();
} else {
showToast(data.error || '删除失败', 'error');
}
} catch (error) {
showToast('删除失败', 'error');
}
}
// 加载临时邮箱的邮件
async function loadTempEmailMessages(email) {
const container = document.getElementById('emailList');
container.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
try {
const response = await fetch(`/api/temp-emails/${encodeURIComponent(email)}/messages`);
const data = await response.json();
if (data.success) {
currentEmails = data.emails;
currentMethod = 'gptmail';
const methodTag = document.getElementById('methodTag');
methodTag.textContent = 'GPTMail';
methodTag.style.display = 'inline';
methodTag.style.backgroundColor = '#00bcf2';
methodTag.style.color = 'white';
document.getElementById('emailCount').textContent = `(${data.count})`;
renderEmailList(data.emails);
} else {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">${data.error || '加载失败'}</div>
</div>
`;
}
} catch (error) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">网络错误,请重试</div>
</div>
`;
}
}
// 获取临时邮件详情
async function getTempEmailDetail(messageId, index) {
document.querySelectorAll('.email-item').forEach((item, i) => {
item.classList.toggle('active', i === index);
});
document.getElementById('emailDetailToolbar').style.display = 'flex';
const container = document.getElementById('emailDetail');
container.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
try {
const response = await fetch(`/api/temp-emails/${encodeURIComponent(currentAccount)}/messages/${encodeURIComponent(messageId)}`);
const data = await response.json();
if (data.success) {
renderEmailDetail(data.email);
} else {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">${data.error || '加载失败'}</div>
</div>
`;
}
} catch (error) {
container.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">⚠️</div>
<div class="empty-state-text">网络错误,请重试</div>
</div>
`;
}
}
// ==================== 账号相关 ====================
// 选择账号
function selectAccount(email) {
currentAccount = email;
isTempEmailGroup = false;
document.getElementById('currentAccount').classList.add('show');
document.getElementById('currentAccountEmail').textContent = email;
document.querySelectorAll('.account-item').forEach(item => {
item.classList.remove('active');
const emailEl = item.querySelector('.account-email');
if (emailEl && emailEl.textContent.includes(email)) {
item.classList.add('active');
}
});
// 清空邮件列表,提示用户点击获取邮件
document.getElementById('emailList').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📬</div>
@@ -1594,7 +1957,6 @@
</div>
`;
// 清空邮件详情
document.getElementById('emailDetail').innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">📄</div>
@@ -2031,9 +2393,12 @@
return;
}
// 根据是否是临时邮箱选择不同的点击处理函数
const clickHandler = isTempEmailGroup ? 'getTempEmailDetail' : 'selectEmail';
container.innerHTML = emails.map((email, index) => `
<div class="email-item ${email.is_read === false ? 'unread' : ''}"
onclick="selectEmail('${email.id}', ${index})">
<div class="email-item ${email.is_read === false ? 'unread' : ''}"
onclick="${clickHandler}('${email.id}', ${index})">
<div class="email-from">${escapeHtml(email.from)}</div>
<div class="email-subject">${escapeHtml(email.subject || '无主题')}</div>
<div class="email-preview">${escapeHtml(email.body_preview || '')}</div>
@@ -2203,7 +2568,11 @@
// 刷新邮件
function refreshEmails() {
if (currentAccount) {
loadEmails(currentAccount);
if (isTempEmailGroup) {
loadTempEmailMessages(currentAccount);
} else {
loadEmails(currentAccount);
}
} else {
showToast('请先选择一个邮箱账号', 'error');
}
@@ -2263,6 +2632,73 @@
}
}
// ==================== 设置相关 ====================
// 显示设置模态框
async function showSettingsModal() {
document.getElementById('settingsModal').classList.add('show');
await loadSettings();
}
// 隐藏设置模态框
function hideSettingsModal() {
document.getElementById('settingsModal').classList.remove('show');
// 清空密码输入框
document.getElementById('settingsPassword').value = '';
}
// 加载设置
async function loadSettings() {
try {
const response = await fetch('/api/settings');
const data = await response.json();
if (data.success) {
// 密码不显示,只显示 API Key
document.getElementById('settingsApiKey').value = data.settings.gptmail_api_key || '';
// 密码框留空
document.getElementById('settingsPassword').value = '';
}
} catch (error) {
showToast('加载设置失败', 'error');
}
}
// 保存设置
async function saveSettings() {
const password = document.getElementById('settingsPassword').value;
const apiKey = document.getElementById('settingsApiKey').value.trim();
const settings = {};
// 只有输入了密码才更新密码
if (password) {
settings.login_password = password;
}
// API Key 可以为空(清除)
settings.gptmail_api_key = apiKey;
try {
const response = await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
const data = await response.json();
if (data.success) {
showToast('设置已保存', 'success');
hideSettingsModal();
} else {
showToast(data.error || '保存失败', 'error');
}
} catch (error) {
showToast('保存设置失败', 'error');
}
}
// 键盘快捷键
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
@@ -2270,6 +2706,7 @@
hideAddAccountModal();
hideEditAccountModal();
hideExportModal();
hideSettingsModal();
}
});
</script>
+537 -4
View File
@@ -4,6 +4,7 @@
Outlook 邮件 Web 应用
基于 Flask 的 Web 界面,支持多邮箱管理和邮件查看
使用 SQLite 数据库存储邮箱信息,支持分组管理
支持 GPTMail 临时邮箱服务
"""
import email
@@ -40,6 +41,13 @@ IMAP_PORT = 993
# 数据库文件
DATABASE = "outlook_accounts.db"
# GPTMail API 配置
GPTMAIL_BASE_URL = "https://mail.chatgpt.org.uk"
GPTMAIL_API_KEY = "gpt-test" # 测试 API Key,可以修改为正式 Key
# 临时邮箱分组 ID(系统保留)
TEMP_EMAIL_GROUP_ID = -1
# ==================== 数据库操作 ====================
@@ -65,6 +73,15 @@ def init_db():
conn = sqlite3.connect(DATABASE)
cursor = conn.cursor()
# 创建设置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建分组表
cursor.execute('''
CREATE TABLE IF NOT EXISTS groups (
@@ -72,6 +89,7 @@ def init_db():
name TEXT UNIQUE NOT NULL,
description TEXT,
color TEXT DEFAULT '#1a1a1a',
is_system INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
@@ -93,6 +111,35 @@ def init_db():
)
''')
# 创建临时邮箱表
cursor.execute('''
CREATE TABLE IF NOT EXISTS temp_emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
status TEXT DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 创建临时邮件表(存储从 GPTMail 获取的邮件)
cursor.execute('''
CREATE TABLE IF NOT EXISTS temp_email_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id TEXT UNIQUE NOT NULL,
email_address TEXT NOT NULL,
from_address TEXT,
subject TEXT,
content TEXT,
html_content TEXT,
has_html INTEGER DEFAULT 0,
timestamp INTEGER,
raw_content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (email_address) REFERENCES temp_emails (email)
)
''')
# 检查并添加缺失的列(数据库迁移)
cursor.execute("PRAGMA table_info(accounts)")
columns = [col[1] for col in cursor.fetchall()]
@@ -106,22 +153,95 @@ def init_db():
if 'updated_at' not in columns:
cursor.execute('ALTER TABLE accounts ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP')
# 检查 groups 表是否有 is_system 列
cursor.execute("PRAGMA table_info(groups)")
group_columns = [col[1] for col in cursor.fetchall()]
if 'is_system' not in group_columns:
cursor.execute('ALTER TABLE groups ADD COLUMN is_system INTEGER DEFAULT 0')
# 创建默认分组
cursor.execute('''
INSERT OR IGNORE INTO groups (name, description, color)
VALUES ('默认分组', '未分组的邮箱', '#666666')
''')
# 创建临时邮箱分组(系统分组)
cursor.execute('''
INSERT OR IGNORE INTO groups (name, description, color, is_system)
VALUES ('临时邮箱', 'GPTMail 临时邮箱服务', '#00bcf2', 1)
''')
# 初始化默认设置
cursor.execute('''
INSERT OR IGNORE INTO settings (key, value)
VALUES ('login_password', ?)
''', (LOGIN_PASSWORD,))
cursor.execute('''
INSERT OR IGNORE INTO settings (key, value)
VALUES ('gptmail_api_key', ?)
''', (GPTMAIL_API_KEY,))
conn.commit()
conn.close()
# ==================== 设置操作 ====================
def get_setting(key: str, default: str = '') -> str:
"""获取设置值"""
db = get_db()
cursor = db.execute('SELECT value FROM settings WHERE key = ?', (key,))
row = cursor.fetchone()
return row['value'] if row else default
def set_setting(key: str, value: str) -> bool:
"""设置值"""
db = get_db()
try:
db.execute('''
INSERT OR REPLACE INTO settings (key, value, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
''', (key, value))
db.commit()
return True
except Exception:
return False
def get_all_settings() -> Dict[str, str]:
"""获取所有设置"""
db = get_db()
cursor = db.execute('SELECT key, value FROM settings')
rows = cursor.fetchall()
return {row['key']: row['value'] for row in rows}
def get_login_password() -> str:
"""获取登录密码(优先从数据库读取)"""
password = get_setting('login_password')
return password if password else LOGIN_PASSWORD
def get_gptmail_api_key() -> str:
"""获取 GPTMail API Key(优先从数据库读取)"""
api_key = get_setting('gptmail_api_key')
return api_key if api_key else GPTMAIL_API_KEY
# ==================== 分组操作 ====================
def load_groups() -> List[Dict]:
"""加载所有分组"""
"""加载所有分组(临时邮箱分组排在最前面)"""
db = get_db()
cursor = db.execute('SELECT * FROM groups ORDER BY id')
# 使用 CASE 语句让临时邮箱分组排在最前面
cursor = db.execute('''
SELECT * FROM groups
ORDER BY
CASE WHEN name = '临时邮箱' THEN 0 ELSE 1 END,
id
''')
rows = cursor.fetchall()
return [dict(row) for row in rows]
@@ -569,7 +689,10 @@ def login():
data = request.json if request.is_json else request.form
password = data.get('password', '')
if password == LOGIN_PASSWORD:
# 从数据库获取密码,如果没有则使用默认密码
correct_password = get_login_password()
if password == correct_password:
session['logged_in'] = True
session.permanent = True
return jsonify({'success': True, 'message': '登录成功'})
@@ -603,7 +726,11 @@ def api_get_groups():
groups = load_groups()
# 添加每个分组的邮箱数量
for group in groups:
group['account_count'] = get_group_account_count(group['id'])
if group['name'] == '临时邮箱':
# 临时邮箱分组从 temp_emails 表获取数量
group['account_count'] = get_temp_email_count()
else:
group['account_count'] = get_group_account_count(group['id'])
return jsonify({'success': True, 'groups': groups})
@@ -1018,6 +1145,411 @@ def api_get_email_detail(email_addr, message_id):
return jsonify({'success': False, 'error': '获取邮件详情失败'})
# ==================== GPTMail 临时邮箱 API ====================
def gptmail_request(method: str, endpoint: str, params: dict = None, json_data: dict = None) -> Optional[Dict]:
"""发送 GPTMail API 请求"""
try:
url = f"{GPTMAIL_BASE_URL}{endpoint}"
# 从数据库获取 API Key
api_key = get_gptmail_api_key()
headers = {
"X-API-Key": api_key,
"Content-Type": "application/json"
}
if method.upper() == 'GET':
response = requests.get(url, headers=headers, params=params, timeout=30)
elif method.upper() == 'POST':
response = requests.post(url, headers=headers, json=json_data, timeout=30)
elif method.upper() == 'DELETE':
response = requests.delete(url, headers=headers, params=params, timeout=30)
else:
return None
if response.status_code == 200:
return response.json()
else:
return {'success': False, 'error': f'API 请求失败: {response.status_code}'}
except Exception as e:
return {'success': False, 'error': f'请求异常: {str(e)}'}
def generate_temp_email(prefix: str = None, domain: str = None) -> Optional[str]:
"""生成临时邮箱地址"""
json_data = {}
if prefix:
json_data['prefix'] = prefix
if domain:
json_data['domain'] = domain
if json_data:
result = gptmail_request('POST', '/api/generate-email', json_data=json_data)
else:
result = gptmail_request('GET', '/api/generate-email')
if result and result.get('success'):
return result.get('data', {}).get('email')
return None
def get_temp_emails_from_api(email_addr: str) -> Optional[List[Dict]]:
"""从 GPTMail API 获取邮件列表"""
result = gptmail_request('GET', '/api/emails', params={'email': email_addr})
if result and result.get('success'):
return result.get('data', {}).get('emails', [])
return None
def get_temp_email_detail_from_api(message_id: str) -> Optional[Dict]:
"""从 GPTMail API 获取邮件详情"""
result = gptmail_request('GET', f'/api/email/{message_id}')
if result and result.get('success'):
return result.get('data')
return None
def delete_temp_email_from_api(message_id: str) -> bool:
"""从 GPTMail API 删除邮件"""
result = gptmail_request('DELETE', f'/api/email/{message_id}')
return result and result.get('success', False)
def clear_temp_emails_from_api(email_addr: str) -> bool:
"""清空 GPTMail 邮箱的所有邮件"""
result = gptmail_request('DELETE', '/api/emails/clear', params={'email': email_addr})
return result and result.get('success', False)
# ==================== 临时邮箱数据库操作 ====================
def get_temp_email_group_id() -> int:
"""获取临时邮箱分组的 ID"""
db = get_db()
cursor = db.execute("SELECT id FROM groups WHERE name = '临时邮箱'")
row = cursor.fetchone()
return row['id'] if row else 2
def load_temp_emails() -> List[Dict]:
"""加载所有临时邮箱"""
db = get_db()
cursor = db.execute('SELECT * FROM temp_emails ORDER BY created_at DESC')
rows = cursor.fetchall()
return [dict(row) for row in rows]
def get_temp_email_by_address(email_addr: str) -> Optional[Dict]:
"""根据邮箱地址获取临时邮箱"""
db = get_db()
cursor = db.execute('SELECT * FROM temp_emails WHERE email = ?', (email_addr,))
row = cursor.fetchone()
return dict(row) if row else None
def add_temp_email(email_addr: str) -> bool:
"""添加临时邮箱"""
db = get_db()
try:
db.execute('INSERT INTO temp_emails (email) VALUES (?)', (email_addr,))
db.commit()
return True
except sqlite3.IntegrityError:
return False
def delete_temp_email(email_addr: str) -> bool:
"""删除临时邮箱及其所有邮件"""
db = get_db()
try:
db.execute('DELETE FROM temp_email_messages WHERE email_address = ?', (email_addr,))
db.execute('DELETE FROM temp_emails WHERE email = ?', (email_addr,))
db.commit()
return True
except Exception:
return False
def save_temp_email_messages(email_addr: str, messages: List[Dict]) -> int:
"""保存临时邮件到数据库"""
db = get_db()
saved = 0
for msg in messages:
try:
db.execute('''
INSERT OR REPLACE INTO temp_email_messages
(message_id, email_address, from_address, subject, content, html_content, has_html, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
msg.get('id'),
email_addr,
msg.get('from_address', ''),
msg.get('subject', ''),
msg.get('content', ''),
msg.get('html_content', ''),
1 if msg.get('has_html') else 0,
msg.get('timestamp', 0)
))
saved += 1
except Exception:
continue
db.commit()
return saved
def get_temp_email_messages(email_addr: str) -> List[Dict]:
"""获取临时邮箱的所有邮件(从数据库)"""
db = get_db()
cursor = db.execute('''
SELECT * FROM temp_email_messages
WHERE email_address = ?
ORDER BY timestamp DESC
''', (email_addr,))
rows = cursor.fetchall()
return [dict(row) for row in rows]
def get_temp_email_message_by_id(message_id: str) -> Optional[Dict]:
"""根据 ID 获取临时邮件"""
db = get_db()
cursor = db.execute('SELECT * FROM temp_email_messages WHERE message_id = ?', (message_id,))
row = cursor.fetchone()
return dict(row) if row else None
def delete_temp_email_message(message_id: str) -> bool:
"""删除临时邮件"""
db = get_db()
try:
db.execute('DELETE FROM temp_email_messages WHERE message_id = ?', (message_id,))
db.commit()
return True
except Exception:
return False
def get_temp_email_count() -> int:
"""获取临时邮箱数量"""
db = get_db()
cursor = db.execute('SELECT COUNT(*) as count FROM temp_emails')
row = cursor.fetchone()
return row['count'] if row else 0
# ==================== 临时邮箱 API 路由 ====================
@app.route('/api/temp-emails', methods=['GET'])
@login_required
def api_get_temp_emails():
"""获取所有临时邮箱"""
emails = load_temp_emails()
return jsonify({'success': True, 'emails': emails})
@app.route('/api/temp-emails/generate', methods=['POST'])
@login_required
def api_generate_temp_email():
"""生成新的临时邮箱"""
data = request.json or {}
prefix = data.get('prefix')
domain = data.get('domain')
email_addr = generate_temp_email(prefix, domain)
if email_addr:
if add_temp_email(email_addr):
return jsonify({'success': True, 'email': email_addr, 'message': '临时邮箱创建成功'})
else:
return jsonify({'success': False, 'error': '邮箱已存在'})
else:
return jsonify({'success': False, 'error': '生成临时邮箱失败,请稍后重试'})
@app.route('/api/temp-emails/<path:email_addr>', methods=['DELETE'])
@login_required
def api_delete_temp_email(email_addr):
"""删除临时邮箱"""
if delete_temp_email(email_addr):
return jsonify({'success': True, 'message': '临时邮箱已删除'})
else:
return jsonify({'success': False, 'error': '删除失败'})
@app.route('/api/temp-emails/<path:email_addr>/messages', methods=['GET'])
@login_required
def api_get_temp_email_messages(email_addr):
"""获取临时邮箱的邮件列表"""
api_messages = get_temp_emails_from_api(email_addr)
if api_messages:
save_temp_email_messages(email_addr, api_messages)
messages = get_temp_email_messages(email_addr)
formatted = []
for msg in messages:
formatted.append({
'id': msg.get('message_id'),
'from': msg.get('from_address', '未知'),
'subject': msg.get('subject', '无主题'),
'body_preview': (msg.get('content', '') or '')[:200],
'date': msg.get('created_at', ''),
'timestamp': msg.get('timestamp', 0),
'has_html': msg.get('has_html', 0)
})
return jsonify({
'success': True,
'emails': formatted,
'count': len(formatted),
'method': 'GPTMail'
})
@app.route('/api/temp-emails/<path:email_addr>/messages/<path:message_id>', methods=['GET'])
@login_required
def api_get_temp_email_message_detail(email_addr, message_id):
"""获取临时邮件详情"""
msg = get_temp_email_message_by_id(message_id)
if not msg:
api_msg = get_temp_email_detail_from_api(message_id)
if api_msg:
save_temp_email_messages(email_addr, [api_msg])
msg = get_temp_email_message_by_id(message_id)
if msg:
return jsonify({
'success': True,
'email': {
'id': msg.get('message_id'),
'from': msg.get('from_address', '未知'),
'to': email_addr,
'subject': msg.get('subject', '无主题'),
'body': msg.get('html_content') if msg.get('has_html') else msg.get('content', ''),
'body_type': 'html' if msg.get('has_html') else 'text',
'date': msg.get('created_at', ''),
'timestamp': msg.get('timestamp', 0)
}
})
else:
return jsonify({'success': False, 'error': '邮件不存在'})
@app.route('/api/temp-emails/<path:email_addr>/messages/<path:message_id>', methods=['DELETE'])
@login_required
def api_delete_temp_email_message(email_addr, message_id):
"""删除临时邮件"""
delete_temp_email_from_api(message_id)
if delete_temp_email_message(message_id):
return jsonify({'success': True, 'message': '邮件已删除'})
else:
return jsonify({'success': False, 'error': '删除失败'})
@app.route('/api/temp-emails/<path:email_addr>/clear', methods=['DELETE'])
@login_required
def api_clear_temp_email_messages(email_addr):
"""清空临时邮箱的所有邮件"""
clear_temp_emails_from_api(email_addr)
db = get_db()
try:
db.execute('DELETE FROM temp_email_messages WHERE email_address = ?', (email_addr,))
db.commit()
return jsonify({'success': True, 'message': '邮件已清空'})
except Exception:
return jsonify({'success': False, 'error': '清空失败'})
@app.route('/api/temp-emails/<path:email_addr>/refresh', methods=['POST'])
@login_required
def api_refresh_temp_email_messages(email_addr):
"""刷新临时邮箱的邮件"""
api_messages = get_temp_emails_from_api(email_addr)
if api_messages is not None:
saved = save_temp_email_messages(email_addr, api_messages)
messages = get_temp_email_messages(email_addr)
formatted = []
for msg in messages:
formatted.append({
'id': msg.get('message_id'),
'from': msg.get('from_address', '未知'),
'subject': msg.get('subject', '无主题'),
'body_preview': (msg.get('content', '') or '')[:200],
'date': msg.get('created_at', ''),
'timestamp': msg.get('timestamp', 0),
'has_html': msg.get('has_html', 0)
})
return jsonify({
'success': True,
'emails': formatted,
'count': len(formatted),
'new_count': saved,
'method': 'GPTMail'
})
else:
return jsonify({'success': False, 'error': '获取邮件失败'})
# ==================== 设置 API ====================
@app.route('/api/settings', methods=['GET'])
@login_required
def api_get_settings():
"""获取所有设置"""
settings = get_all_settings()
# 隐藏密码的部分字符
if 'login_password' in settings:
pwd = settings['login_password']
if len(pwd) > 2:
settings['login_password_masked'] = pwd[0] + '*' * (len(pwd) - 2) + pwd[-1]
else:
settings['login_password_masked'] = '*' * len(pwd)
return jsonify({'success': True, 'settings': settings})
@app.route('/api/settings', methods=['PUT'])
@login_required
def api_update_settings():
"""更新设置"""
data = request.json
updated = []
errors = []
# 更新登录密码
if 'login_password' in data:
new_password = data['login_password'].strip()
if new_password:
if len(new_password) < 4:
errors.append('密码长度至少为 4 位')
elif set_setting('login_password', new_password):
updated.append('登录密码')
else:
errors.append('更新登录密码失败')
# 更新 GPTMail API Key
if 'gptmail_api_key' in data:
new_api_key = data['gptmail_api_key'].strip()
if new_api_key:
if set_setting('gptmail_api_key', new_api_key):
updated.append('GPTMail API Key')
else:
errors.append('更新 GPTMail API Key 失败')
if errors:
return jsonify({'success': False, 'error': ';'.join(errors)})
if updated:
return jsonify({'success': True, 'message': f'已更新:{", ".join(updated)}'})
else:
return jsonify({'success': False, 'error': '没有需要更新的设置'})
# ==================== 主程序 ====================
if __name__ == '__main__':
@@ -1033,6 +1565,7 @@ if __name__ == '__main__':
print("=" * 60)
print(f"访问地址: http://127.0.0.1:{port}")
print(f"数据库文件: {DATABASE}")
print(f"GPTMail API: {GPTMAIL_BASE_URL}")
print("=" * 60)
app.run(debug=True, host='127.0.0.1', port=port)