From a8d1b4138136f56a1ad7bbef7c0df5199a74711d Mon Sep 17 00:00:00 2001 From: huangzhenpc Date: Wed, 26 Feb 2025 18:29:10 +0800 Subject: [PATCH] first commit --- app/__init__.py | 15 + app/config.py | 10 + app/models.py | 11 + app/utils.py | 194 +++++ old/app/__init__.py | 74 ++ old/app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 2877 bytes old/app/api/__init__.py | 25 + .../api/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 814 bytes .../__pycache__/email_routes.cpython-312.pyc | Bin 0 -> 10379 bytes .../mailbox_routes.cpython-312.pyc | Bin 0 -> 10339 bytes .../api/__pycache__/routes.cpython-312.pyc | Bin 0 -> 17955 bytes old/app/api/decoded_email_routes.py | 708 ++++++++++++++++++ old/app/api/domain_routes.py | Bin 0 -> 6540 bytes old/app/api/email_routes.py | 376 ++++++++++ old/app/api/mailbox_routes.py | 206 +++++ old/app/api/routes.py | 341 +++++++++ old/app/models/__init__.py | 46 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1836 bytes .../__pycache__/attachment.cpython-312.pyc | Bin 0 -> 3626 bytes .../models/__pycache__/domain.cpython-312.pyc | Bin 0 -> 2134 bytes .../models/__pycache__/email.cpython-312.pyc | Bin 0 -> 4702 bytes .../__pycache__/mailbox.cpython-312.pyc | Bin 0 -> 3303 bytes old/app/models/attachment.py | 71 ++ old/app/models/domain.py | 35 + old/app/models/email.py | 148 ++++ old/app/models/mailbox.py | 50 ++ old/app/services/__init__.py | 44 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1410 bytes .../email_processor.cpython-312.pyc | Bin 0 -> 5086 bytes .../__pycache__/mail_store.cpython-312.pyc | Bin 0 -> 11617 bytes .../__pycache__/smtp_server.cpython-312.pyc | Bin 0 -> 6214 bytes old/app/services/email_processor.py | 123 +++ old/app/services/mail_store.py | 140 ++++ old/app/services/smtp_server.py | 230 ++++++ old/app/templates/index.html | 0 old/app/utils/__init__.py | 9 + requirements.txt | 5 + run.py | 17 + 38 files changed, 2878 insertions(+) create mode 100644 app/__init__.py create mode 100644 app/config.py create mode 100644 app/models.py create mode 100644 app/utils.py create mode 100644 old/app/__init__.py create mode 100644 old/app/__pycache__/__init__.cpython-312.pyc create mode 100644 old/app/api/__init__.py create mode 100644 old/app/api/__pycache__/__init__.cpython-312.pyc create mode 100644 old/app/api/__pycache__/email_routes.cpython-312.pyc create mode 100644 old/app/api/__pycache__/mailbox_routes.cpython-312.pyc create mode 100644 old/app/api/__pycache__/routes.cpython-312.pyc create mode 100644 old/app/api/decoded_email_routes.py create mode 100644 old/app/api/domain_routes.py create mode 100644 old/app/api/email_routes.py create mode 100644 old/app/api/mailbox_routes.py create mode 100644 old/app/api/routes.py create mode 100644 old/app/models/__init__.py create mode 100644 old/app/models/__pycache__/__init__.cpython-312.pyc create mode 100644 old/app/models/__pycache__/attachment.cpython-312.pyc create mode 100644 old/app/models/__pycache__/domain.cpython-312.pyc create mode 100644 old/app/models/__pycache__/email.cpython-312.pyc create mode 100644 old/app/models/__pycache__/mailbox.cpython-312.pyc create mode 100644 old/app/models/attachment.py create mode 100644 old/app/models/domain.py create mode 100644 old/app/models/email.py create mode 100644 old/app/models/mailbox.py create mode 100644 old/app/services/__init__.py create mode 100644 old/app/services/__pycache__/__init__.cpython-312.pyc create mode 100644 old/app/services/__pycache__/email_processor.cpython-312.pyc create mode 100644 old/app/services/__pycache__/mail_store.cpython-312.pyc create mode 100644 old/app/services/__pycache__/smtp_server.cpython-312.pyc create mode 100644 old/app/services/email_processor.py create mode 100644 old/app/services/mail_store.py create mode 100644 old/app/services/smtp_server.py create mode 100644 old/app/templates/index.html create mode 100644 old/app/utils/__init__.py create mode 100644 requirements.txt create mode 100644 run.py diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..9e5a2cc --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,15 @@ +from flask import Flask +from .config import Config +from .models import db + + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + # 初始化数据库 + db.init_app(app) + with app.app_context(): + db.create_all() + + return app \ No newline at end of file diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..030f876 --- /dev/null +++ b/app/config.py @@ -0,0 +1,10 @@ +class Config: + # 使用 1Panel MySQL 内部连接 + SQLALCHEMY_DATABASE_URI = 'mysql://gitea_HN5jYh:mysql_KbBZTN@1Panel-mysql-vjz9:3306/gitea_2l82ep' + # 如果内部连接不通,可以尝试使用外部连接 + # SQLALCHEMY_DATABASE_URI = 'mysql://gitea_HN5jYh:mysql_KbBZTN@rnpanel.586vip.cn:3306/gitea_2l82ep' + SQLALCHEMY_TRACK_MODIFICATIONS = False + # 使用 1Panel Redis 内部连接 + REDIS_URL = "redis://1Panel-redis-r3Pz:6379/0" + # 如果内部连接不通,可以尝试使用外部连接 + # REDIS_URL = "redis://rnpanel.586vip.cn:6379/0" \ No newline at end of file diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..ecffcf6 --- /dev/null +++ b/app/models.py @@ -0,0 +1,11 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() + +class Email(db.Model): + id = db.Column(db.Integer, primary_key=True) + subject = db.Column(db.String(255)) + sender = db.Column(db.String(255)) + recipient = db.Column(db.String(255)) + body = db.Column(db.Text) + timestamp = db.Column(db.DateTime, server_default=db.func.now()) \ No newline at end of file diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..7c7251a --- /dev/null +++ b/app/utils.py @@ -0,0 +1,194 @@ +import smtplib +import os +import json +import logging +from email.parser import BytesParser +from email.policy import default +from datetime import datetime +from .models import db, Email +import redis +import smtpd +import asyncore +import base64 +from .config import Config + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger('smtp_server') + +# 初始化 Redis 客户端 +redis_client = redis.from_url(Config.REDIS_URL) + + +def receive_email(): + # 这里实现邮件接收逻辑 + # 假设我们从某个 SMTP 服务器接收邮件 + # 解析邮件并存储到 Redis + # 示例: + raw_email = b'...' # 这里应该是接收到的原始邮件内容 + email = BytesParser(policy=default).parsebytes(raw_email) + email_data = { + 'subject': email['subject'], + 'sender': email['from'], + 'recipient': email['to'], + 'body': email.get_body(preferencelist=('plain')).get_content() + } + # 将邮件信息存储到 Redis + redis_client.hmset(f'email:{email_data['subject']}', email_data) + + +class CustomSMTPServer(smtpd.SMTPServer): + def process_message(self, peer, mailfrom, rcpttos, data): + try: + # 记录接收到的邮件基本信息 + logger.info(f'Received mail from {mailfrom} to {rcpttos}') + + # 验证收件人域名 + for rcpt in rcpttos: + if not rcpt.endswith('@nosqli.com'): + logger.warning(f'Rejected mail to {rcpt}: invalid domain') + continue + + # 解析邮件 + email = BytesParser(policy=default).parsebytes(data) + + # 获取邮件正文 + body = self._get_email_body(email) + + # 处理附件 + attachments = self._process_attachments(email) + + # 构建邮件数据 + timestamp = datetime.now().isoformat() + message_id = email.get('Message-ID', f'<{timestamp}@nosqli.com>') + + email_data = { + 'message_id': message_id, + 'subject': email.get('subject', ''), + 'sender': mailfrom, + 'recipients': json.dumps(rcpttos), + 'body': body, + 'timestamp': timestamp, + 'attachments': json.dumps(attachments), + 'headers': json.dumps(dict(email.items())), + 'peer': json.dumps(peer) + } + + # 存储邮件 + self._store_email(email_data) + + logger.info(f'Successfully processed mail: {message_id}') + + except Exception as e: + logger.error(f'Error processing email: {str(e)}', exc_info=True) + raise + + def _get_email_body(self, email): + """提取邮件正文""" + try: + if email.is_multipart(): + for part in email.walk(): + if part.get_content_type() == "text/plain": + return part.get_payload(decode=True).decode() + else: + return email.get_payload(decode=True).decode() + return "" + except Exception as e: + logger.error(f'Error extracting email body: {str(e)}') + return "" + + def _process_attachments(self, email): + """处理邮件附件""" + attachments = [] + try: + if email.is_multipart(): + for part in email.walk(): + if part.get_content_maintype() == 'multipart': + continue + if part.get('Content-Disposition') is None: + continue + + filename = part.get_filename() + if filename: + attachment_data = part.get_payload(decode=True) + attachments.append({ + 'filename': filename, + 'content': base64.b64encode(attachment_data).decode(), + 'content_type': part.get_content_type(), + 'size': len(attachment_data) + }) + except Exception as e: + logger.error(f'Error processing attachments: {str(e)}') + return attachments + + def _store_email(self, email_data): + """存储邮件到 Redis""" + try: + # 使用 message_id 作为主键 + email_key = f'email:{email_data["message_id"]}' + redis_client.hmset(email_key, email_data) + + # 为每个收件人创建索引 + recipients = json.loads(email_data['recipients']) + for recipient in recipients: + recipient_key = f'recipient:{recipient}' + redis_client.lpush(recipient_key, email_key) + + # 创建时间索引 + time_key = f'time:{email_data["timestamp"]}' + redis_client.set(time_key, email_key) + + # 设置过期时间(可选,这里设置为30天) + redis_client.expire(email_key, 30 * 24 * 60 * 60) + + except Exception as e: + logger.error(f'Error storing email: {str(e)}') + raise + + +def start_smtp_server(host='0.0.0.0', port=25): + """启动 SMTP 服务器""" + try: + logger.info(f'Starting SMTP server on {host}:{port}') + server = CustomSMTPServer((host, port), None) + asyncore.loop() + except Exception as e: + logger.error(f'Error starting SMTP server: {str(e)}') + raise + + +def get_emails_by_recipient(recipient, limit=10): + """获取指定收件人的最新邮件""" + try: + recipient_key = f'recipient:{recipient}' + email_keys = redis_client.lrange(recipient_key, 0, limit - 1) + + emails = [] + for key in email_keys: + email_data = redis_client.hgetall(key.decode()) + if email_data: + # 转换数据为字符串 + email_data = {k.decode(): v.decode() for k, v in email_data.items()} + emails.append(email_data) + + return emails + except Exception as e: + print(f'Error fetching emails: {e}') + return [] + + +def get_attachment(email_key, attachment_index): + """获取指定邮件的附件""" + try: + email_data = redis_client.hgetall(email_key) + if email_data: + attachments = json.loads(email_data[b'attachments'].decode()) + if 0 <= attachment_index < len(attachments): + return attachments[attachment_index] + return None + except Exception as e: + print(f'Error fetching attachment: {e}') + return None \ No newline at end of file diff --git a/old/app/__init__.py b/old/app/__init__.py new file mode 100644 index 0000000..6197538 --- /dev/null +++ b/old/app/__init__.py @@ -0,0 +1,74 @@ +import os +import logging +from flask import Flask +from flask_cors import CORS + +# 修改相对导入为绝对导入 +import sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from config import active_config + + +def setup_logging(app): + """设置日志""" + log_level = getattr(logging, active_config.LOG_LEVEL.upper(), logging.INFO) + + # 确保日志目录存在 + log_dir = os.path.dirname(active_config.LOG_FILE) + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # 配置日志 + logging.basicConfig( + level=log_level, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler(active_config.LOG_FILE), + logging.StreamHandler() + ] + ) + + app.logger.setLevel(log_level) + return app + + +def create_app(config=None): + """创建并配置Flask应用""" + app = Flask(__name__) + + # 加载配置 + app.config.from_object(active_config) + + # 如果提供了自定义配置,加载它 + if config: + app.config.from_object(config) + + # 允许跨域请求 + CORS(app) + + # 设置日志 + app = setup_logging(app) + + # 确保存储邮件的目录存在 + os.makedirs(active_config.MAIL_STORAGE_PATH, exist_ok=True) + + # 初始化数据库 + from .models import init_db + init_db() + + # 注册蓝图 + from .api import api_bp + app.register_blueprint(api_bp) + + # 首页路由 + @app.route("/") + def index(): + return { + "name": "Email System", + "version": "1.0.0", + "status": "running" + } + + app.logger.info('应用初始化完成') + + return app \ No newline at end of file diff --git a/old/app/__pycache__/__init__.cpython-312.pyc b/old/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..26ef549cb82ba15890bce20db3b5b903b2419b52 GIT binary patch literal 2877 zcmbtWYiv|S6rQ_#U;8fQQRGp!JcJEcFu_6$R7fpQsMJuRVbgeXyZ3gv+q;j>sELU`{NrY8whoY($b$SU7XJ9lGj}_^mDEV$Bzw;{ zGiT1sIcLr{`%@_7N6>Ce+)UO;2>nhwtodfa%nA;naik%QGf})Exi|-XkLgK!<6e%A zc}+eo#03X)rWhA>F%@Q}T$x(rVup0IpypyW@5=wJxzx4AkRG@U{?Ybt((#EmgXvH_ z1XvhC@vtUo;t&^)oaAEiL%R5Ij3dH6Q?$Qv< z&*slO8>`&446&#uJ)e14Y&q12QngIOm8m)xGt|d1&*`dm6!Ye3UjDdp>%q+{_s2gj ze>>j0eZwZzg3FZFV^;Kq=!Q)hbq<<(k8aNEr*+Fxlb{&$62X}yVpkTYRh!7i)r@B9 z*orM8Qc|~7+r~sPv&p29Nn!zZCBD|S{Yq=|8_lgm$meo8CVb1`eQm^(wFsY6?c+q! z3{1BpGF9Ez(%MW!{e)rJ7Ln6xw+_=533RHKk!W%*J_+nIO#MJ*YZ7X=v96|BSERec zF0!TDt<)1ZwO7sMhz}-|%JJz56>64dI~tAt)%5Di9dCBza8}DFY^%evj=!D7-5prh z(ndx%GM25HCX9CIY1J^Tla{TgJHTj%q8J&&R+ReONfLq`^EriiPqRhiZha1)0d%jb z=Dc)qb*XM=p>AiXuCY+pSghOg!^R)Ce7~hw)i%)bSVWOUW69CvSavjfK3%HaUZ~w( z48Jt6|4tw@wqtb1SmS8p#Ky_M+B>p;EHD~4vwu=v1*?Q$iLWW}HB;f5sYvs^;Nntn zO(D4EVyF~tC`22I!5w$#SY07lR}8M37QEH|Cz2$H)740jhYt=OtSq1KP4a7}0^xy! zGjL$^4|bu<;jqgs;FX2hh26P+u8&L2F%7j;X(`H3Pc>U+18gBpL=lblcVs^$WHw!+3tt*9f1MKx#n6YKvB&ZN@tF6TNCas z2+@?&Cw7Lu((t@FQMxey|%ZdRcSxkc4Y7VX65z0M-LD& zoz-;H!ZbOQiVVD-qH3%sf#h_obeefRhmDL)1f0#o2z`18Z)Cc%F#*>@2h)@}2As8c z3v?|?uRvbWXeBa!Hzc?Lrs#Iom+%4Z@kh?dr?w6>K}1-|uPE><&hMV&V~<26ho|Jg zn19q?l4}cc?L^O{{5&NV-^k)2%1eRe6W*eK)j$&+l!gxt9y%8)Rc$R)ZT))vFZ?S{ zU^MzG9~f$(Xw?)im3V4?q5&8MwiJEC#|Dp$Sns^`$Rh~;87~r-Oj9%+o;}WR+hE(z zOEypPTYjSiWz3e%+d<5;_#XFavXOX`EH}|T~u=y zt(*}N7nnlcVR=v4J1Aav}1C=bCg?x)!+_`A#lwYbiz# Z7OM_TqSm`8`lq;ri#)F1$VD7~{{}6xLOcKf literal 0 HcmV?d00001 diff --git a/old/app/api/__init__.py b/old/app/api/__init__.py new file mode 100644 index 0000000..5d7fbc4 --- /dev/null +++ b/old/app/api/__init__.py @@ -0,0 +1,25 @@ +# API模块初始化文件 +from flask import Blueprint +import logging + +# 创建API蓝图 +api_bp = Blueprint('api', __name__, url_prefix='/api') + +# 注册默认路由 +@api_bp.route('/') +def index(): + return { + 'name': 'Email System API', + 'version': '1.0.0', + 'status': 'running' + } + +# 导入并合并所有API路由 +# 为避免可能的文件读取问题,改为从routes.py模块中导入所有路由定义 +try: + from .routes import * + # 导入解码邮件路由模块 + from .decoded_email_routes import * +except Exception as e: + logging.error(f"导入API路由时出错: {str(e)}") + raise \ No newline at end of file diff --git a/old/app/api/__pycache__/__init__.cpython-312.pyc b/old/app/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45b4f3647a9020f386b466a5364f864f1c28b290 GIT binary patch literal 814 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 M)=_bq>w-D{7b>Bty#N3J literal 0 HcmV?d00001 diff --git a/old/app/api/__pycache__/routes.cpython-312.pyc b/old/app/api/__pycache__/routes.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5211fb8baaa5a52e447b949564dea7c17254a830 GIT binary patch literal 17955 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 literal 0 HcmV?d00001 diff --git a/old/app/api/decoded_email_routes.py b/old/app/api/decoded_email_routes.py new file mode 100644 index 0000000..08d0137 --- /dev/null +++ b/old/app/api/decoded_email_routes.py @@ -0,0 +1,708 @@ +import base64 +import re +import os +import email +from email import policy +from datetime import datetime, timedelta +import psutil +import time + +from flask import jsonify, request, current_app +from sqlalchemy import or_, func, desc + +from . import api_bp +from ..models import Email, Domain, Mailbox, get_session +from ..services import get_mail_store + +# 调试接口 - 检查邮件接收状态 +@api_bp.route('/debug_email', methods=['GET']) +def debug_email(): + """ + 调试接口:检查某个邮箱的邮件状态并提供详细信息 + + 查询参数: + - email: 邮箱地址 (例如: newsadd1test@nosqli.com) + """ + try: + # 获取查询参数 + email_address = request.args.get('email') + + # 验证邮箱地址是否有效 + if not email_address or '@' not in email_address: + return jsonify({ + 'success': False, + 'error': '无效的邮箱地址', + 'message': '请提供有效的邮箱地址,格式为user@domain.com' + }), 400 + + # 解析邮箱地址 + username, domain_name = email_address.split('@', 1) + + result = { + 'email_address': email_address, + 'username': username, + 'domain': domain_name, + 'system_info': {}, + 'logs': [], + 'files': [] + } + + # 查询数据库 + db = get_session() + try: + # 查找域名 + domain = db.query(Domain).filter_by(name=domain_name).first() + + if domain: + result['system_info']['domain'] = { + 'id': domain.id, + 'name': domain.name, + 'active': domain.active, + 'created_at': str(domain.created_at) if hasattr(domain, 'created_at') else None + } + + # 查找邮箱 + mailbox = db.query(Mailbox).filter_by( + domain_id=domain.id, + address=username + ).first() + + if mailbox: + result['system_info']['mailbox'] = { + 'id': mailbox.id, + 'address': mailbox.address, + 'full_address': f"{mailbox.address}@{domain_name}", + 'created_at': str(mailbox.created_at) if hasattr(mailbox, 'created_at') else None + } + + # 获取邮件 + emails = db.query(Email).filter_by(mailbox_id=mailbox.id).all() + + result['system_info']['emails_count'] = len(emails) + result['system_info']['emails'] = [] + + for email_obj in emails: + email_info = { + 'id': email_obj.id, + 'subject': email_obj.subject, + 'sender': email_obj.sender, + 'received_at': str(email_obj.received_at), + 'verification_code': email_obj.verification_code if hasattr(email_obj, 'verification_code') else None + } + result['system_info']['emails'].append(email_info) + else: + result['system_info']['mailbox'] = "未找到邮箱记录" + else: + result['system_info']['domain'] = "未找到域名记录" + + # 查找文件 + email_data_dir = current_app.config.get('MAIL_STORAGE_PATH', 'email_data') + emails_dir = os.path.join(email_data_dir, 'emails') + + if os.path.exists(emails_dir): + for file_name in os.listdir(emails_dir): + if file_name.endswith('.eml'): + file_path = os.path.join(emails_dir, file_name) + file_info = { + 'name': file_name, + 'path': file_path, + 'size': os.path.getsize(file_path), + 'modified': datetime.fromtimestamp(os.path.getmtime(file_path)).isoformat() + } + + # 尝试读取文件内容并检查是否包含收件人地址 + try: + with open(file_path, 'r', encoding='utf-8', errors='replace') as f: + content = f.read(10000) # 只读取前10000个字符用于检查 + if email_address.lower() in content.lower(): + file_info['contains_address'] = True + result['files'].append(file_info) + except Exception as e: + file_info['error'] = str(e) + result['files'].append(file_info) + + # 检查日志 + log_file = current_app.config.get('LOG_FILE', os.path.join('logs', 'email_system.log')) + if os.path.exists(log_file): + try: + with open(log_file, 'r', encoding='utf-8', errors='replace') as f: + # 从日志尾部读取最后200行 + lines = f.readlines()[-200:] + for line in lines: + if email_address.lower() in line.lower(): + result['logs'].append(line.strip()) + except Exception as e: + result['logs'] = [f"读取日志出错: {str(e)}"] + + return jsonify({ + 'success': True, + 'debug_info': result + }), 200 + + finally: + db.close() + + except Exception as e: + current_app.logger.error(f"调试邮件出错: {str(e)}") + return jsonify({ + 'success': False, + 'error': '服务器错误', + 'message': str(e) + }), 500 + +# 创建邮箱接口 +@api_bp.route('/add_mailbox', methods=['POST', 'GET']) +def add_mailbox(): + """ + 创建新邮箱,如果域名不存在则自动创建 + + 查询参数(GET方式)或表单参数(POST方式): + - email: 邮箱地址 (例如: testaa@nosqli.com) + - description: 邮箱描述 (可选) + """ + try: + # 获取参数 + if request.method == 'POST': + data = request.json or {} + email_address = data.get('email') + description = data.get('description', '') + else: # GET方式 + email_address = request.args.get('email') + description = request.args.get('description', '') + + # 验证邮箱地址 + if not email_address or '@' not in email_address: + return jsonify({ + 'success': False, + 'error': '无效的邮箱地址', + 'message': '请提供有效的邮箱地址,格式为user@domain.com' + }), 400 + + # 解析邮箱地址 + username, domain_name = email_address.split('@', 1) + + # 创建或查找域名和邮箱 + db = get_session() + try: + # 查找域名 + domain = db.query(Domain).filter_by(name=domain_name).first() + + # 如果域名不存在,创建域名 + if not domain: + current_app.logger.info(f"域名 {domain_name} 不存在,开始创建") + domain = Domain( + name=domain_name, + description=f"自动创建的域名 {domain_name}", + active=True + ) + db.add(domain) + db.commit() + current_app.logger.info(f"域名 {domain_name} 创建成功,ID: {domain.id}") + + # 查询邮箱是否已存在 + mailbox = db.query(Mailbox).filter_by( + domain_id=domain.id, + address=username + ).first() + + # 如果邮箱已存在,返回已存在信息 + if mailbox: + return jsonify({ + 'success': True, + 'message': f'邮箱 {email_address} 已存在', + 'mailbox': { + 'id': mailbox.id, + 'address': mailbox.address, + 'domain_id': mailbox.domain_id, + 'full_address': f"{mailbox.address}@{domain_name}", + 'description': mailbox.description + } + }), 200 + + # 创建邮箱 + mailbox = Mailbox( + domain_id=domain.id, + address=username, + description=description or f"自动创建的邮箱 {email_address}" + ) + db.add(mailbox) + db.commit() + + # 返回成功信息 + return jsonify({ + 'success': True, + 'message': f'邮箱 {email_address} 创建成功', + 'mailbox': { + 'id': mailbox.id, + 'address': mailbox.address, + 'domain_id': mailbox.domain_id, + 'full_address': f"{mailbox.address}@{domain_name}", + 'description': mailbox.description + } + }), 201 + + finally: + db.close() + + except Exception as e: + current_app.logger.error(f"创建邮箱出错: {str(e)}") + return jsonify({ + 'success': False, + 'error': '服务器错误', + 'message': str(e) + }), 500 + +# 简化的URL路径,直接通过邮箱地址获取邮件 +@api_bp.route('/email', methods=['GET']) +def get_email_by_address(): + """ + 通过邮箱地址获取邮件的简化URL + 等同于 /decoded_emails?email={email_address}&latest=0 + + 查询参数: + - email: 邮箱地址 (必填) + - latest: 是否只返回最新的邮件 (1表示是,0表示否,默认0) + """ + # 重用已有的解码邮件接口 + return get_decoded_emails() + +@api_bp.route('/decoded_emails', methods=['GET']) +def get_decoded_emails(): + """ + 获取指定邮箱地址的所有邮件,并返回解码后的内容 + + 查询参数: + - email: 邮箱地址 (例如: testaa@nosqli.com) + - latest: 是否只返回最新的邮件 (1表示是,0表示否,默认0) + - limit: 返回邮件数量 (默认10) + - offset: 查询起始位置 (默认0) + """ + try: + # 获取查询参数 + email_address = request.args.get('email') + latest = request.args.get('latest', '0') == '1' + limit = int(request.args.get('limit', 10)) + offset = int(request.args.get('offset', 0)) + + # 验证邮箱地址是否有效 + if not email_address or '@' not in email_address: + return jsonify({ + 'success': False, + 'error': '无效的邮箱地址', + 'message': '请提供有效的邮箱地址,格式为user@domain.com' + }), 400 + + # 解析邮箱地址 + username, domain_name = email_address.split('@', 1) + + # 查询数据库 + db = get_session() + try: + # 查找域名 + domain = db.query(Domain).filter_by(name=domain_name).first() + if not domain: + return jsonify({ + 'success': False, + 'error': '域名不存在', + 'message': f'域名 {domain_name} 不存在' + }), 404 + + # 查找邮箱 + mailbox = db.query(Mailbox).filter_by( + domain_id=domain.id, + address=username + ).first() + + if not mailbox: + return jsonify({ + 'success': False, + 'error': '邮箱不存在', + 'message': f'邮箱 {email_address} 不存在' + }), 404 + + # 获取邮件 + query = db.query(Email).filter_by(mailbox_id=mailbox.id) + + # 按接收时间排序,最新的在前 + query = query.order_by(Email.received_at.desc()) + + # 如果只要最新的一封 + if latest: + emails = query.limit(1).all() + else: + emails = query.limit(limit).offset(offset).all() + + # 处理结果 + result_emails = [] + for email_obj in emails: + # 获取原始邮件文件路径 + email_file_path = os.path.join( + current_app.config.get('MAIL_STORAGE_PATH', 'email_data'), + 'emails', + f'email_{email_obj.id}.eml' + ) + + # 解码邮件内容 + decoded_email = decode_email(email_obj, email_file_path) + result_emails.append(decoded_email) + + # 返回结果 + return jsonify({ + 'success': True, + 'email_address': email_address, + 'total_emails': query.count(), + 'emails': result_emails + }), 200 + + finally: + db.close() + + except Exception as e: + current_app.logger.error(f"获取解码邮件出错: {str(e)}") + return jsonify({ + 'success': False, + 'error': '服务器错误', + 'message': str(e) + }), 500 + + +@api_bp.route('/decoded_email/', methods=['GET']) +def get_decoded_email_by_id(email_id): + """获取指定ID的解码邮件内容""" + try: + db = get_session() + try: + # 获取邮件对象 + email_obj = db.query(Email).filter_by(id=email_id).first() + + if not email_obj: + return jsonify({ + 'success': False, + 'error': '邮件不存在', + 'message': f'ID为{email_id}的邮件不存在' + }), 404 + + # 获取原始邮件文件路径 + email_file_path = os.path.join( + current_app.config.get('MAIL_STORAGE_PATH', 'email_data'), + 'emails', + f'email_{email_obj.id}.eml' + ) + + # 解码邮件内容 + decoded_email = decode_email(email_obj, email_file_path) + + # 返回结果 + return jsonify({ + 'success': True, + 'email': decoded_email + }), 200 + + finally: + db.close() + + except Exception as e: + current_app.logger.error(f"获取解码邮件出错: {str(e)}") + return jsonify({ + 'success': False, + 'error': '服务器错误', + 'message': str(e) + }), 500 + + +def decode_email(email_obj, email_file_path): + """解析并解码邮件内容""" + # 创建基本邮件信息 + result = { + 'id': email_obj.id, + 'subject': email_obj.subject, + 'sender': email_obj.sender, + 'recipients': email_obj.recipients, + 'received_at': email_obj.received_at.isoformat() if email_obj.received_at else None, + 'read': email_obj.read, + 'has_attachments': len(email_obj.attachments) > 0 if hasattr(email_obj, 'attachments') else False + } + + # 从数据库中直接获取验证码 + if hasattr(email_obj, 'verification_code') and email_obj.verification_code: + result['verification_code'] = email_obj.verification_code + + if hasattr(email_obj, 'verification_link') and email_obj.verification_link: + result['verification_link'] = email_obj.verification_link + + # 如果邮件对象有文本内容或HTML内容,直接使用 + if hasattr(email_obj, 'body_text') and email_obj.body_text: + result['body_text'] = email_obj.body_text + + if hasattr(email_obj, 'body_html') and email_obj.body_html: + result['body_html'] = email_obj.body_html + + # 如果有原始邮件文件,尝试解析 + if os.path.exists(email_file_path): + try: + # 解析.eml文件 + with open(email_file_path, 'r', encoding='utf-8', errors='replace') as f: + msg = email.message_from_file(f, policy=policy.default) + + # 如果没有从数据库获取到内容,尝试从文件解析 + if 'body_text' not in result or 'body_html' not in result: + body_text = "" + body_html = "" + + # 处理多部分邮件 + if msg.is_multipart(): + for part in msg.iter_parts(): + content_type = part.get_content_type() + + if content_type == "text/plain": + try: + body_text = part.get_content() + except Exception: + payload = part.get_payload(decode=True) + if payload: + charset = part.get_content_charset() or 'utf-8' + try: + body_text = payload.decode(charset, errors='replace') + except: + body_text = payload.decode('utf-8', errors='replace') + + elif content_type == "text/html": + try: + body_html = part.get_content() + except Exception: + payload = part.get_payload(decode=True) + if payload: + charset = part.get_content_charset() or 'utf-8' + try: + body_html = payload.decode(charset, errors='replace') + except: + body_html = payload.decode('utf-8', errors='replace') + else: + # 处理单部分邮件 + content_type = msg.get_content_type() + try: + if content_type == "text/plain": + body_text = msg.get_content() + elif content_type == "text/html": + body_html = msg.get_content() + except Exception: + payload = msg.get_payload(decode=True) + if payload: + charset = msg.get_content_charset() or 'utf-8' + try: + decoded = payload.decode(charset, errors='replace') + if content_type == "text/plain": + body_text = decoded + elif content_type == "text/html": + body_html = decoded + except: + pass + + # 如果找到了内容,添加到结果中 + if body_text and 'body_text' not in result: + result['body_text'] = body_text + + if body_html and 'body_html' not in result: + result['body_html'] = body_html + + # 如果仍然没有提取到验证码,尝试从内容中提取 + if 'verification_code' not in result: + verification_code = extract_verification_code(result.get('body_text', ''), result.get('body_html', '')) + if verification_code: + result['verification_code'] = verification_code + + except Exception as e: + current_app.logger.error(f"解析邮件文件出错: {str(e)}") + + return result + + +def extract_verification_code(body_text, body_html): + """从邮件内容中提取验证码""" + # 首先尝试从HTML中提取 + if body_html: + # 常用的验证码模式 + patterns = [ + r'letter-spacing:\s*\d+px[^>]*>([^<]+)<', # 特殊样式的验证码 + r']*>(\d{6})', # 6位数字验证码在div中 + r'验证码[::]\s*([A-Z0-9]{4,8})', # 中文标记的验证码 + r'code[^\d]+(\d{4,8})', # 英文标记的验证码 + r'\b([A-Z0-9]{6})\b' # 6位大写字母或数字 + ] + + for pattern in patterns: + matches = re.findall(pattern, body_html) + if matches: + return matches[0].strip() + + # 如果HTML中没找到,尝试从纯文本中提取 + if body_text: + patterns = [ + r'验证码[::]\s*([A-Z0-9]{4,8})', # 中文格式 + r'code[^\d]+(\d{4,8})', # 英文格式 + r'\b(\d{6})\b' # 6位数字 + ] + + for pattern in patterns: + matches = re.findall(pattern, body_text) + if matches: + return matches[0].strip() + + return None + + +# 系统诊断接口 +@api_bp.route('/system_check', methods=['GET']) +def system_check(): + """ + 系统诊断接口:检查邮件系统各组件状态 + """ + try: + result = { + 'timestamp': datetime.now().isoformat(), + 'system_status': 'normal', + 'components': {}, + 'recent_activity': {}, + 'mailboxes': [], + 'storage': {} + } + + # 检查系统资源 + try: + result['components']['system'] = { + 'cpu_percent': psutil.cpu_percent(), + 'memory_percent': psutil.virtual_memory().percent, + 'disk_usage': psutil.disk_usage('/').percent + } + except Exception as e: + result['components']['system'] = {'error': str(e)} + + # 检查数据库状态 + db = get_session() + try: + # 获取域名数量 + domain_count = db.query(Domain).count() + + # 获取邮箱数量 + mailbox_count = db.query(Mailbox).count() + + # 获取邮件数量 + email_count = db.query(Email).count() + + # 获取最新邮件 + latest_emails = db.query(Email).order_by(Email.received_at.desc()).limit(5).all() + + result['components']['database'] = { + 'status': 'connected', + 'domain_count': domain_count, + 'mailbox_count': mailbox_count, + 'email_count': email_count + } + + # 最近活动 + result['recent_activity']['latest_emails'] = [ + { + 'id': email.id, + 'subject': email.subject, + 'sender': email.sender, + 'received_at': email.received_at.isoformat() if email.received_at else None + } for email in latest_emails + ] + + # 获取所有活跃邮箱 + active_mailboxes = db.query(Mailbox).order_by(Mailbox.id).limit(10).all() + result['mailboxes'] = [ + { + 'id': mb.id, + 'address': mb.address, + 'domain_id': mb.domain_id, + 'full_address': f"{mb.address}@{mb.domain.name}" if hasattr(mb, 'domain') and mb.domain else f"{mb.address}@unknown", + 'email_count': db.query(Email).filter_by(mailbox_id=mb.id).count() + } for mb in active_mailboxes + ] + + except Exception as e: + result['components']['database'] = {'status': 'error', 'error': str(e)} + finally: + db.close() + + # 检查存储状态 + email_data_dir = current_app.config.get('MAIL_STORAGE_PATH', 'email_data') + try: + emails_dir = os.path.join(email_data_dir, 'emails') + attachments_dir = os.path.join(email_data_dir, 'attachments') + + # 检查目录是否存在 + emails_dir_exists = os.path.exists(emails_dir) + attachments_dir_exists = os.path.exists(attachments_dir) + + # 计算文件数量和大小 + email_files_count = 0 + email_files_size = 0 + if emails_dir_exists: + for file_name in os.listdir(emails_dir): + if file_name.endswith('.eml'): + email_files_count += 1 + email_files_size += os.path.getsize(os.path.join(emails_dir, file_name)) + + attachment_files_count = 0 + attachment_files_size = 0 + if attachments_dir_exists: + for file_name in os.listdir(attachments_dir): + attachment_files_count += 1 + attachment_files_size += os.path.getsize(os.path.join(attachments_dir, file_name)) + + result['storage'] = { + 'emails_dir': { + 'exists': emails_dir_exists, + 'path': emails_dir, + 'file_count': email_files_count, + 'size_bytes': email_files_size, + 'size_mb': round(email_files_size / (1024 * 1024), 2) if email_files_size > 0 else 0 + }, + 'attachments_dir': { + 'exists': attachments_dir_exists, + 'path': attachments_dir, + 'file_count': attachment_files_count, + 'size_bytes': attachment_files_size, + 'size_mb': round(attachment_files_size / (1024 * 1024), 2) if attachment_files_size > 0 else 0 + } + } + + # 检查最近的邮件文件 + if emails_dir_exists and email_files_count > 0: + files = [(os.path.getmtime(os.path.join(emails_dir, f)), f) + for f in os.listdir(emails_dir) if f.endswith('.eml')] + files.sort(reverse=True) + + result['recent_activity']['latest_files'] = [ + { + 'filename': f, + 'modified': datetime.fromtimestamp(t).isoformat(), + 'age_seconds': int(time.time() - t) + } for t, f in files[:5] + ] + + except Exception as e: + result['storage'] = {'error': str(e)} + + # 整体状态评估 + if ('database' in result['components'] and result['components']['database'].get('status') != 'connected'): + result['system_status'] = 'warning' + + if not emails_dir_exists or not attachments_dir_exists: + result['system_status'] = 'warning' + + return jsonify({ + 'success': True, + 'status': result['system_status'], + 'diagnostics': result + }), 200 + + except Exception as e: + current_app.logger.error(f"系统诊断出错: {str(e)}") + return jsonify({ + 'success': False, + 'error': '系统诊断失败', + 'message': str(e) + }), 500 \ No newline at end of file diff --git a/old/app/api/domain_routes.py b/old/app/api/domain_routes.py new file mode 100644 index 0000000000000000000000000000000000000000..a05687c5d460b3929544aee7459d067b2ae517b6 GIT binary patch literal 6540 zcmeHL&1+Ow6hE${LZIDLq?_RdVFL0-i(NFB;zB9ew3z4_wQPgNMCwV zl9qI(DyjT}-yN)MU_Frm{w30sJ2J#~ivMS@avQ(;vJSeIjLb?M?-N704w_YP+Q4T@ zPJlusq~JmsU8&0&xb?6%#`9PFFUp(;_W|CuvAcs`Be`t&QzvqDImbKpQ+@|})OKhQ zLuL=!H^99MJBpIqSf>Rko+&;ZV=1k^gkL@A-+^wb!!*{PKX|oxvG!T}_4TPGu3rRA zCOtOx4~;ctiKGI_8J`p@G{E{@cxVnjOH8(sn%UL`g1axHd0^_f2R^1rXQ(64V+ZY-CtyIAt=KI}Vqzg0~SYae?Z zPiVd<{rArm%x}hH5@!mPHJmlt~I>pJK#e z(QN96ooII6ix?_NpFUL4cVTVve)M$}dq(>|%tU|x`SR8z@JnYR${P=c-5-^Q6{#%Q zdj#gf!EQSCLa_?!sgE;F8y%e!!)};%MXq^gsQm8Y43*D6tz);!EP~OxFKe`v`1T3e zvRfjpbA5)e;NwEJY0R$27i#rq$+24@tMfU=o2Tqf%J!jbO3skpr3-WC%~;*^qoYqE z>|LC{hv;HfJKRhn_c^EA=!LAYH>XX|M#5T&g54wB88(A^HQW4C5AT{-qi/emails', methods=['GET']) +def get_mailbox_emails(mailbox_id): + """获取指定邮箱的所有邮件""" + try: + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 50)) + unread_only = request.args.get('unread_only', 'false').lower() == 'true' + offset = (page - 1) * limit + + db = get_session() + try: + # 检查邮箱是否存在 + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + if not mailbox: + return jsonify({'error': '邮箱不存在'}), 404 + + # 查询邮件 + query = db.query(Email).filter(Email.mailbox_id == mailbox_id) + + if unread_only: + query = query.filter(Email.read == False) + + # 获取总数 + total = query.count() + + # 分页获取邮件 + emails = query.order_by(Email.received_at.desc()) \ + .limit(limit) \ + .offset(offset) \ + .all() + + # 返回结果 + result = { + 'success': True, + 'total': total, + 'page': page, + 'limit': limit, + 'emails': [email.to_dict() for email in emails] + } + + return jsonify(result), 200 + finally: + db.close() + except Exception as e: + current_app.logger.error(f"获取邮件列表出错: {str(e)}") + return jsonify({'success': False, 'error': '获取邮件列表失败', 'details': str(e)}), 500 + +# 获取特定邮件详情 +@api_bp.route('/emails/', methods=['GET']) +def get_email(email_id): + """ + 获取单个邮件的详细信息 + """ + try: + email_id = int(email_id) + session = get_session() + email = session.query(Email).filter(Email.id == email_id).first() + + if not email: + return jsonify({ + 'success': False, + 'error': f'未找到ID为{email_id}的邮件' + }), 404 + + # 获取邮件正文内容 + body_text = None + body_html = None + + try: + # 尝试从文件中读取邮件内容 + if email.id: + email_path = os.path.join(config.DATA_DIR, 'emails', f'email_{email.id}.eml') + if os.path.exists(email_path): + logging.info(f"从文件读取邮件内容: {email_path}") + with open(email_path, 'r', encoding='utf-8', errors='ignore') as f: + try: + raw_email = f.read() + msg = email_parser.parsestr(raw_email) + + if msg.is_multipart(): + # 处理多部分邮件 + for part in msg.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + + # 跳过附件 + if "attachment" in content_disposition: + continue + + # 处理文本内容 + if content_type == "text/plain": + payload = part.get_payload(decode=True) + charset = part.get_content_charset() or 'utf-8' + try: + body_text = payload.decode(charset, errors='replace') + except Exception as e: + logging.error(f"解码纯文本内容失败: {e}") + body_text = payload.decode('utf-8', errors='replace') + + # 处理HTML内容 + elif content_type == "text/html": + payload = part.get_payload(decode=True) + charset = part.get_content_charset() or 'utf-8' + try: + body_html = payload.decode(charset, errors='replace') + except Exception as e: + logging.error(f"解码HTML内容失败: {e}") + body_html = payload.decode('utf-8', errors='replace') + else: + # 处理单部分邮件 + content_type = msg.get_content_type() + payload = msg.get_payload(decode=True) + charset = msg.get_content_charset() or 'utf-8' + + try: + decoded_content = payload.decode(charset, errors='replace') + except Exception as e: + logging.error(f"解码内容失败: {e}") + decoded_content = payload.decode('utf-8', errors='replace') + + if content_type == "text/plain": + body_text = decoded_content + elif content_type == "text/html": + body_html = decoded_content + except Exception as e: + logging.error(f"解析邮件文件失败: {e}") + except Exception as e: + logging.error(f"读取邮件内容时出错: {e}") + + # 如果文件读取失败,使用数据库中的内容 + if body_text is None: + body_text = email.body_text + + if body_html is None: + body_html = email.body_html + + logging.info(f"邮件ID={email_id} 正文长度: text={len(body_text or '')}字节, html={len(body_html or '')}字节") + + # 返回邮件信息,包括正文内容 + return jsonify({ + 'success': True, + 'email': { + 'id': email.id, + 'subject': email.subject, + 'sender': email.sender, + 'recipients': email.recipients, + 'received_at': email.received_at.isoformat(), + 'verification_code': email.verification_code, + 'verification_link': email.verification_link, + 'body_text': body_text, + 'body_html': body_html + } + }) + except Exception as e: + logging.error(f"获取邮件时出错: {str(e)}") + return jsonify({ + 'success': False, + 'error': f'获取邮件时发生错误: {str(e)}' + }), 500 + finally: + session.close() + +# 删除邮件 +@api_bp.route('/emails/', methods=['DELETE']) +def delete_email(email_id): + """删除特定邮件""" + try: + db = get_session() + try: + email = db.query(Email).filter_by(id=email_id).first() + + if not email: + return jsonify({'error': '邮件不存在'}), 404 + + db.delete(email) + db.commit() + + return jsonify({'message': '邮件已删除'}), 200 + except Exception as e: + db.rollback() + raise + finally: + db.close() + except Exception as e: + current_app.logger.error(f"删除邮件出错: {str(e)}") + return jsonify({'error': '删除邮件失败', 'details': str(e)}), 500 + +# 下载附件 +@api_bp.route('/attachments/', methods=['GET']) +def download_attachment(attachment_id): + """下载特定的附件""" + try: + from ..models import Attachment + + db = get_session() + try: + attachment = db.query(Attachment).filter_by(id=attachment_id).first() + + if not attachment: + return jsonify({'error': '附件不存在'}), 404 + + # 获取附件内容 + content = attachment.get_content() + if not content: + return jsonify({'error': '附件内容不可用'}), 404 + + # 创建内存文件对象 + file_obj = BytesIO(content) + + # 返回文件下载响应 + return send_file( + file_obj, + mimetype=attachment.content_type, + as_attachment=True, + download_name=attachment.filename + ) + finally: + db.close() + except Exception as e: + current_app.logger.error(f"下载附件出错: {str(e)}") + return jsonify({'error': '下载附件失败', 'details': str(e)}), 500 + +# 获取最新邮件 (轮询API) +@api_bp.route('/mailboxes//poll', methods=['GET']) +def poll_new_emails(mailbox_id): + """轮询指定邮箱的新邮件""" + try: + # 获取上次检查时间 + last_check = request.args.get('last_check') + if last_check: + try: + last_check_time = float(last_check) + except ValueError: + return jsonify({'error': '无效的last_check参数'}), 400 + else: + last_check_time = time.time() - 300 # 默认检查最近5分钟 + + db = get_session() + try: + # 检查邮箱是否存在 + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + if not mailbox: + return jsonify({'error': '邮箱不存在'}), 404 + + # 查询新邮件 + new_emails = db.query(Email).filter( + Email.mailbox_id == mailbox_id, + Email.received_at >= time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(last_check_time)) + ).order_by(Email.received_at.desc()).all() + + # 返回结果 + result = { + 'mailbox_id': mailbox_id, + 'count': len(new_emails), + 'emails': [email.to_dict() for email in new_emails], + 'timestamp': time.time() + } + + return jsonify(result), 200 + finally: + db.close() + except Exception as e: + current_app.logger.error(f"轮询新邮件出错: {str(e)}") + return jsonify({'error': '轮询新邮件失败', 'details': str(e)}), 500 + +# 通过邮箱地址获取最新邮件 +@api_bp.route('/emails/by-address', methods=['GET']) +def get_emails_by_address(): + """ + 通过邮箱地址获取最新邮件 + + 参数: + email_address: 完整邮箱地址 (例如: user@example.com) + limit: 返回的邮件数量 (默认: 10) + since: 从指定时间戳后获取邮件 (可选) + unread_only: 是否只返回未读邮件 (默认: false) + + 返回: + 最新的邮件列表 + """ + try: + email_address = request.args.get('email_address') + if not email_address or '@' not in email_address: + return jsonify({'success': False, 'error': '无效的邮箱地址'}), 400 + + limit = int(request.args.get('limit', 10)) + unread_only = request.args.get('unread_only', 'false').lower() == 'true' + since = request.args.get('since') + + # 解析邮箱地址 + try: + username, domain_name = email_address.split('@', 1) + except ValueError: + return jsonify({ + 'success': False, + 'error': '邮箱地址格式无效' + }), 400 + + db = get_session() + try: + # 查找域名 + domain = db.query(Domain).filter_by(name=domain_name, active=True).first() + if not domain: + return jsonify({ + 'success': False, + 'error': f'域名 {domain_name} 不存在或未激活' + }), 404 + + # 查找邮箱 + mailbox = db.query(Mailbox).filter_by(address=username, domain_id=domain.id).first() + if not mailbox: + # 自动创建邮箱 - 批量注册场景 + mailbox = Mailbox( + address=username, + domain_id=domain.id, + description=f"自动创建 ({email_address})", + active=True + ) + db.add(mailbox) + db.flush() # 获取新创建邮箱的ID + logging.info(f"自动创建邮箱: {email_address}, ID={mailbox.id}") + + # 查询邮件 + query = db.query(Email).filter(Email.mailbox_id == mailbox.id) + + if unread_only: + query = query.filter(Email.read == False) + + if since: + try: + since_time = float(since) + query = query.filter( + Email.received_at >= time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(since_time)) + ) + except ValueError: + logging.warning(f"无效的since参数: {since}") + + # 获取最新的邮件 + emails = query.order_by(Email.received_at.desc()).limit(limit).all() + + # 获取总数 + total = query.count() + + # 提交数据库变更 + db.commit() + + # 返回结果 + return jsonify({ + 'success': True, + 'email_address': email_address, + 'mailbox_id': mailbox.id, + 'total': total, + 'count': len(emails), + 'emails': [email.to_dict() for email in emails], + 'timestamp': time.time() + }), 200 + except Exception as e: + db.rollback() + raise + finally: + db.close() + except Exception as e: + current_app.logger.error(f"获取邮件时出错: {str(e)}") + return jsonify({'success': False, 'error': '获取邮件失败'}), 500 \ No newline at end of file diff --git a/old/app/api/mailbox_routes.py b/old/app/api/mailbox_routes.py new file mode 100644 index 0000000..84b416d --- /dev/null +++ b/old/app/api/mailbox_routes.py @@ -0,0 +1,206 @@ +from flask import request, jsonify, current_app +from sqlalchemy.exc import IntegrityError +import random +import string + +from . import api_bp +from ..models import get_session, Domain, Mailbox + +# 获取所有邮箱 +@api_bp.route('/mailboxes', methods=['GET']) +def get_mailboxes(): + """获取所有邮箱列表""" + try: + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 50)) + offset = (page - 1) * limit + + db = get_session() + try: + # 查询总数 + total = db.query(Mailbox).count() + + # 获取分页数据 + mailboxes = db.query(Mailbox).order_by(Mailbox.created_at.desc()) \ + .limit(limit) \ + .offset(offset) \ + .all() + + # 转换为字典列表 + result = { + 'total': total, + 'page': page, + 'limit': limit, + 'mailboxes': [mailbox.to_dict() for mailbox in mailboxes] + } + + return jsonify(result), 200 + finally: + db.close() + except Exception as e: + current_app.logger.error(f"获取邮箱列表出错: {str(e)}") + return jsonify({'error': '获取邮箱列表失败', 'details': str(e)}), 500 + +# 创建邮箱 +@api_bp.route('/mailboxes', methods=['POST']) +def create_mailbox(): + """创建新邮箱""" + try: + data = request.json + + # 验证必要参数 + if not data or 'domain_id' not in data: + return jsonify({'error': '缺少必要参数'}), 400 + + db = get_session() + try: + # 查询域名是否存在 + domain = db.query(Domain).filter_by(id=data['domain_id'], active=True).first() + if not domain: + return jsonify({'error': '指定的域名不存在或未激活'}), 404 + + # 生成或使用给定地址 + if 'address' not in data or not data['address']: + # 生成随机地址 + address = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + else: + address = data['address'] + + # 创建邮箱 + mailbox = Mailbox( + address=address, + domain_id=domain.id, + description=data.get('description', ''), + active=True + ) + + db.add(mailbox) + db.commit() + + return jsonify({ + 'message': '邮箱创建成功', + 'mailbox': mailbox.to_dict() + }), 201 + except IntegrityError: + db.rollback() + return jsonify({'error': '邮箱地址已存在'}), 409 + except Exception as e: + db.rollback() + raise + finally: + db.close() + except Exception as e: + current_app.logger.error(f"创建邮箱出错: {str(e)}") + return jsonify({'error': '创建邮箱失败', 'details': str(e)}), 500 + +# 批量创建邮箱 +@api_bp.route('/mailboxes/batch', methods=['POST']) +def batch_create_mailboxes(): + """批量创建邮箱""" + try: + data = request.json + + # 验证必要参数 + if not data or 'domain_id' not in data or 'count' not in data: + return jsonify({'error': '缺少必要参数'}), 400 + + domain_id = data['domain_id'] + count = min(int(data['count']), 100) # 限制最大数量为100 + prefix = data.get('prefix', '') + + db = get_session() + try: + # 查询域名是否存在 + domain = db.query(Domain).filter_by(id=domain_id, active=True).first() + if not domain: + return jsonify({'error': '指定的域名不存在或未激活'}), 404 + + created_mailboxes = [] + + # 批量创建 + for _ in range(count): + # 生成随机地址 + if prefix: + address = f"{prefix}{random.randint(1000, 9999)}" + else: + address = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + + # 尝试创建,如果地址已存在则重试 + retries = 0 + while retries < 3: # 最多尝试3次 + try: + mailbox = Mailbox( + address=address, + domain_id=domain.id, + active=True + ) + + db.add(mailbox) + db.flush() # 验证但不提交 + created_mailboxes.append(mailbox) + break + except IntegrityError: + db.rollback() + # 地址已存在,重新生成 + address = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10)) + retries += 1 + + # 提交所有更改 + db.commit() + + return jsonify({ + 'message': f'成功创建 {len(created_mailboxes)} 个邮箱', + 'mailboxes': [mailbox.to_dict() for mailbox in created_mailboxes] + }), 201 + except Exception as e: + db.rollback() + raise + finally: + db.close() + except Exception as e: + current_app.logger.error(f"批量创建邮箱出错: {str(e)}") + return jsonify({'error': '批量创建邮箱失败', 'details': str(e)}), 500 + +# 获取特定邮箱 +@api_bp.route('/mailboxes/', methods=['GET']) +def get_mailbox(mailbox_id): + """获取指定ID的邮箱信息""" + try: + db = get_session() + try: + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + + if not mailbox: + return jsonify({'error': '邮箱不存在'}), 404 + + return jsonify(mailbox.to_dict()), 200 + finally: + db.close() + except Exception as e: + current_app.logger.error(f"获取邮箱详情出错: {str(e)}") + return jsonify({'error': '获取邮箱详情失败', 'details': str(e)}), 500 + +# 删除邮箱 +@api_bp.route('/mailboxes/', methods=['DELETE']) +def delete_mailbox(mailbox_id): + """删除指定ID的邮箱""" + try: + db = get_session() + try: + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + + if not mailbox: + return jsonify({'error': '邮箱不存在'}), 404 + + db.delete(mailbox) + db.commit() + + return jsonify({'message': '邮箱已删除'}), 200 + except Exception as e: + db.rollback() + raise + finally: + db.close() + except Exception as e: + current_app.logger.error(f"删除邮箱出错: {str(e)}") + return jsonify({'error': '删除邮箱失败', 'details': str(e)}), 500 \ No newline at end of file diff --git a/old/app/api/routes.py b/old/app/api/routes.py new file mode 100644 index 0000000..a0eabab --- /dev/null +++ b/old/app/api/routes.py @@ -0,0 +1,341 @@ +from flask import Blueprint, request, jsonify, current_app +import json +from datetime import datetime, timedelta +import os +import time +import psutil +import sys +import platform +from sqlalchemy import func +from ..models import get_session, Domain, Mailbox, Email +from ..services import get_smtp_server, get_email_processor + +api_bp = Blueprint('api', __name__, url_prefix='/api') + + +@api_bp.route('/domains', methods=['GET']) +def get_domains(): + """获取所有可用域名""" + db = get_session() + try: + domains = db.query(Domain).filter_by(active=True).all() + return jsonify({ + 'success': True, + 'domains': [domain.to_dict() for domain in domains] + }) + except Exception as e: + current_app.logger.exception(f"获取域名失败: {str(e)}") + return jsonify({'success': False, 'error': '获取域名失败'}), 500 + finally: + db.close() + + +@api_bp.route('/domains', methods=['POST']) +def create_domain(): + """创建新域名""" + data = request.json + if not data or 'name' not in data: + return jsonify({'success': False, 'error': '缺少必要字段'}), 400 + + db = get_session() + try: + # 检查域名是否已存在 + domain_exists = db.query(Domain).filter_by(name=data['name']).first() + if domain_exists: + return jsonify({'success': False, 'error': '域名已存在'}), 400 + + # 创建新域名 + domain = Domain( + name=data['name'], + description=data.get('description', ''), + active=data.get('active', True) + ) + db.add(domain) + db.commit() + + return jsonify({ + 'success': True, + 'message': '域名创建成功', + 'domain': domain.to_dict() + }) + except Exception as e: + db.rollback() + current_app.logger.exception(f"创建域名失败: {str(e)}") + return jsonify({'success': False, 'error': '创建域名失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes', methods=['GET']) +def get_mailboxes(): + """获取所有邮箱""" + db = get_session() + try: + mailboxes = db.query(Mailbox).all() + return jsonify({ + 'success': True, + 'mailboxes': [mailbox.to_dict() for mailbox in mailboxes] + }) + except Exception as e: + current_app.logger.exception(f"获取邮箱失败: {str(e)}") + return jsonify({'success': False, 'error': '获取邮箱失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes', methods=['POST']) +def create_mailbox(): + """创建新邮箱""" + data = request.json + if not data or 'address' not in data or 'domain_id' not in data: + return jsonify({'success': False, 'error': '缺少必要字段'}), 400 + + db = get_session() + try: + # 检查域名是否存在 + domain = db.query(Domain).filter_by(id=data['domain_id'], active=True).first() + if not domain: + return jsonify({'success': False, 'error': '域名不存在或未激活'}), 400 + + # 检查邮箱是否已存在 + mailbox_exists = db.query(Mailbox).filter_by( + address=data['address'], domain_id=data['domain_id']).first() + if mailbox_exists: + return jsonify({'success': False, 'error': '邮箱已存在'}), 400 + + # 创建新邮箱 + mailbox = Mailbox( + address=data['address'], + domain_id=data['domain_id'], + description=data.get('description', ''), + active=data.get('active', True) + ) + db.add(mailbox) + db.commit() + + return jsonify({ + 'success': True, + 'message': '邮箱创建成功', + 'mailbox': mailbox.to_dict() + }) + except Exception as e: + db.rollback() + current_app.logger.exception(f"创建邮箱失败: {str(e)}") + return jsonify({'success': False, 'error': '创建邮箱失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes/batch', methods=['POST']) +def batch_create_mailboxes(): + """批量创建邮箱""" + data = request.json + if not data or 'domain_id' not in data or 'usernames' not in data or not isinstance(data['usernames'], list): + return jsonify({'success': False, 'error': '缺少必要字段或格式不正确'}), 400 + + domain_id = data['domain_id'] + usernames = data['usernames'] + description = data.get('description', '') + + db = get_session() + try: + # 检查域名是否存在 + domain = db.query(Domain).filter_by(id=domain_id, active=True).first() + if not domain: + return jsonify({'success': False, 'error': '域名不存在或未激活'}), 400 + + created_mailboxes = [] + existed_mailboxes = [] + + for username in usernames: + # 检查邮箱是否已存在 + mailbox_exists = db.query(Mailbox).filter_by( + username=username, domain_id=domain_id).first() + if mailbox_exists: + existed_mailboxes.append(username) + continue + + # 创建新邮箱 + mailbox = Mailbox( + username=username, + domain_id=domain_id, + description=description, + active=True + ) + db.add(mailbox) + created_mailboxes.append(username) + + db.commit() + + return jsonify({ + 'success': True, + 'message': f'成功创建 {len(created_mailboxes)} 个邮箱,{len(existed_mailboxes)} 个已存在', + 'created': created_mailboxes, + 'existed': existed_mailboxes + }) + except Exception as e: + db.rollback() + current_app.logger.exception(f"批量创建邮箱失败: {str(e)}") + return jsonify({'success': False, 'error': '批量创建邮箱失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes/', methods=['GET']) +def get_mailbox(mailbox_id): + """获取特定邮箱的信息""" + db = get_session() + try: + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + if not mailbox: + return jsonify({'success': False, 'error': '邮箱不存在'}), 404 + + # 更新最后访问时间 + mailbox.last_accessed = datetime.utcnow() + db.commit() + + return jsonify({ + 'success': True, + 'mailbox': mailbox.to_dict() + }) + except Exception as e: + current_app.logger.exception(f"获取邮箱信息失败: {str(e)}") + return jsonify({'success': False, 'error': '获取邮箱信息失败'}), 500 + finally: + db.close() + + +@api_bp.route('/mailboxes//emails', methods=['GET']) +def get_emails(mailbox_id): + """获取邮箱中的所有邮件""" + db = get_session() + try: + mailbox = db.query(Mailbox).filter_by(id=mailbox_id).first() + if not mailbox: + return jsonify({'success': False, 'error': '邮箱不存在'}), 404 + + # 更新最后访问时间 + mailbox.last_accessed = datetime.utcnow() + db.commit() + + emails = db.query(Email).filter_by(mailbox_id=mailbox_id).order_by(Email.received_at.desc()).all() + + return jsonify({ + 'success': True, + 'emails': [email.to_dict() for email in emails] + }) + except Exception as e: + current_app.logger.exception(f"获取邮件失败: {str(e)}") + return jsonify({'success': False, 'error': '获取邮件失败'}), 500 + finally: + db.close() + + +@api_bp.route('/emails/', methods=['GET']) +def get_email(email_id): + """获取特定邮件的详细内容""" + db = get_session() + try: + email = db.query(Email).filter_by(id=email_id).first() + if not email: + return jsonify({'success': False, 'error': '邮件不存在'}), 404 + + # 标记为已读 + if not email.read: + email.read = True + db.commit() + + return jsonify({ + 'success': True, + 'email': email.to_dict() + }) + except Exception as e: + current_app.logger.exception(f"获取邮件详情失败: {str(e)}") + return jsonify({'success': False, 'error': '获取邮件详情失败'}), 500 + finally: + db.close() + + +@api_bp.route('/emails//verification', methods=['GET']) +def get_verification_info(email_id): + """获取邮件中的验证信息(链接和验证码)""" + db = get_session() + try: + email = db.query(Email).filter_by(id=email_id).first() + if not email: + return jsonify({'success': False, 'error': '邮件不存在'}), 404 + + verification_links = json.loads(email.verification_links) if email.verification_links else [] + verification_codes = json.loads(email.verification_codes) if email.verification_codes else [] + + return jsonify({ + 'success': True, + 'email_id': email_id, + 'verification_links': verification_links, + 'verification_codes': verification_codes + }) + except Exception as e: + current_app.logger.exception(f"获取验证信息失败: {str(e)}") + return jsonify({'success': False, 'error': '获取验证信息失败'}), 500 + finally: + db.close() + + +@api_bp.route('/status', methods=['GET']) +def system_status(): + """获取系统状态""" + session = get_session() + + # 获取基本统计信息 + domain_count = session.query(func.count(Domain.id)).scalar() + mailbox_count = session.query(func.count(Mailbox.id)).scalar() + email_count = session.query(func.count(Email.id)).scalar() + + # 获取最近24小时的邮件数量 + recent_emails = session.query(func.count(Email.id)).filter( + Email.received_at > datetime.now() - timedelta(hours=24) + ).scalar() + + # 获取系统资源信息 + cpu_percent = psutil.cpu_percent(interval=0.5) + memory = psutil.virtual_memory() + disk = psutil.disk_usage('/') + + # 获取服务状态 + smtp_server = get_smtp_server() + email_processor = get_email_processor() + + smtp_status = "running" if smtp_server and smtp_server.controller else "stopped" + processor_status = "running" if email_processor and email_processor.is_running else "stopped" + + # 构建响应 + status = { + "system": { + "uptime": round(time.time() - psutil.boot_time()), + "time": datetime.now().isoformat(), + "platform": platform.platform(), + "python_version": sys.version + }, + "resources": { + "cpu_percent": cpu_percent, + "memory_percent": memory.percent, + "memory_used": memory.used, + "memory_total": memory.total, + "disk_percent": disk.percent, + "disk_used": disk.used, + "disk_total": disk.total + }, + "application": { + "domain_count": domain_count, + "mailbox_count": mailbox_count, + "email_count": email_count, + "recent_emails_24h": recent_emails, + "storage_path": os.path.abspath("email_data"), + "services": { + "smtp_server": smtp_status, + "email_processor": processor_status + } + } + } + + return jsonify(status) \ No newline at end of file diff --git a/old/app/models/__init__.py b/old/app/models/__init__.py new file mode 100644 index 0000000..6d8980e --- /dev/null +++ b/old/app/models/__init__.py @@ -0,0 +1,46 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, scoped_session +import os +import sys + +# 修改相对导入为绝对导入 +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +from config import active_config + +# 创建数据库引擎 +engine = create_engine(active_config.SQLALCHEMY_DATABASE_URI) + +# 创建会话工厂 +session_factory = sessionmaker(bind=engine) +Session = scoped_session(session_factory) + +# 创建模型基类 +Base = declarative_base() + +# 获取数据库会话 +def get_session(): + """获取数据库会话""" + return Session() + +# 初始化数据库 +def init_db(): + """初始化数据库,创建所有表""" + # 导入所有模型以确保它们被注册 + from .domain import Domain + from .mailbox import Mailbox + from .email import Email + from .attachment import Attachment + + # 创建表 + Base.metadata.create_all(engine) + + return get_session() + +# 导出模型类 +from .domain import Domain +from .mailbox import Mailbox +from .email import Email +from .attachment import Attachment + +__all__ = ['Base', 'get_session', 'init_db', 'Domain', 'Mailbox', 'Email', 'Attachment'] \ No newline at end of file diff --git a/old/app/models/__pycache__/__init__.cpython-312.pyc b/old/app/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f85915278d18bb1ed4dc6607f64daa68c14fbba3 GIT binary patch literal 1836 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 MDgS>EJParQ17DbAT>t<8 literal 0 HcmV?d00001 diff --git a/old/app/models/__pycache__/domain.cpython-312.pyc b/old/app/models/__pycache__/domain.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..67f986cbe8198dc7332ba9a23409ae15c5733344 GIT binary patch literal 2134 zcma)7ZEO@p7@ob4-MziuwRbHAT1u;8>jF{}Fe*WkVgnq8V$vGp=5({%40P#!9J71E zH6~sRMia%*K++OGiZQXIRf#{8REhXY6aPRwL%RtvMh@*Si^V_w(RX&QA4)M!a__wF zGw-}R^UO1I-z5?fg7(4Cm4o$Bgnnkhe1q6;a|(3B2qVlH$mA5x3@ITqtc1;o5;3Dn zlw)HdgEs|5@cpnMnxZ0tAHh*0W=e|0p%B`IFn+qLJ-*kax^>VKcG$K-G|LmaHJ9}1CXqR>h7v<_b=z|K^*qEPhA|h$W;j-k z^CCMmhvb3}1(vS{bFehn5kU%vkrKk362>7V5<(wJwWuf6P zW;w!mB#4#RLXNnYGZ9;;A^COVFh^RzYJ4m~l84p?RhHT%d3FDlcc0nu1$I*~JB@2_ z0w*VG7tnfKcMK^FI0be^F)i17e79|Cx>ZQSDrR2)_|_MnRnDKUoH%}a>_X+ew><&- z!%iQN8qZVR)M!y1AjO%#Ab+PE_V|LOA1)A2vUV)NnWL;O%oQ*> zpcM>P4$%fcJkhoad3I`^uvgQK{q|9h4?Z=R)(m4~8zu&E$)h&S_iL6M@{;?t+<=<5 z^94A0;&?(%b8;Fcx$4nCQG}SZ*l+GZFd$p*Rdfivf44Ir*>mAuB!_zj#ccrrZ63Fs z3CGe*auIrm7zZ3yyY0{2-CMISWb@R!{%ObJb;R*$SZFQ1l2nn$~? z)wW*~+vnI0{;|^-0RB$9VU5FR`F#Rf6*HSZ-vF)3cOGFqpz~K`2@I&XB&@o$X?grm z{NCL0Dr9l~Y1G`C;a6J*{~y(o%T!zGrsh#tr3rfIY*2|H?12FH`D-*Ym zzkloUWaXpb${Uw|`|_P%PL17~e7EblOy%V9%H*jY$|#iuFQj9-23#+WiIbyxp1nsN zujO3*72-)bNsl#5X4+ZVnC{pIY-(yQ z?eOF3OwfMVAQo)_Fc2thW#fWtt60yugUPD2s_|-;_ZpZIn&nab%HfWoj;V%K!=CBP|@JZ0()cw>K4OUPpa4j2@e$LByR?hL+OapJKHqgweioY<+3RbUZcGK2m>j!zjM= z{MB{k_^wj-bb8h4#?kPZ<}tUN-covYI#GXO7vv|_mv&C4R*r~ck9^X3v9p}oTH3QD zzPkIHeP8b@r}|2J?(itpGV!=LJ!AS%b}PW*$49KuO}Z;8eELCjf)@1vzbCX2TrE e{fN4LKwURc$4%5yLFv1EGbg>(_XmQ`ukbgqsrat| literal 0 HcmV?d00001 diff --git a/old/app/models/__pycache__/email.cpython-312.pyc b/old/app/models/__pycache__/email.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb1ab7ed14359de2b9b2a342f1136dbaefd4272c GIT binary patch literal 4702 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" + + def to_dict(self): + """转换为字典,用于API响应""" + return { + "id": self.id, + "name": self.name, + "description": self.description, + "active": self.active, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + "mailbox_count": len(self.mailboxes) if self.mailboxes else 0 + } \ No newline at end of file diff --git a/old/app/models/email.py b/old/app/models/email.py new file mode 100644 index 0000000..429e5c9 --- /dev/null +++ b/old/app/models/email.py @@ -0,0 +1,148 @@ +import os +import json +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, JSON +from sqlalchemy.orm import relationship +from datetime import datetime +import re +import sys +import logging + +from . import Base +import config +active_config = config.active_config + + +class Email(Base): + """电子邮件模型""" + __tablename__ = 'emails' + + id = Column(Integer, primary_key=True) + mailbox_id = Column(Integer, ForeignKey('mailboxes.id'), nullable=False, index=True) + sender = Column(String(255), nullable=False) + recipients = Column(String(1000), nullable=False) + subject = Column(String(500), nullable=True) + body_text = Column(Text, nullable=True) + body_html = Column(Text, nullable=True) + received_at = Column(DateTime, default=datetime.utcnow) + read = Column(Boolean, default=False) + headers = Column(JSON, nullable=True) + + # 提取的验证码和链接 + verification_code = Column(String(100), nullable=True) + verification_link = Column(String(1000), nullable=True) + + # 关联关系 + mailbox = relationship("Mailbox", back_populates="emails") + attachments = relationship("Attachment", back_populates="email", cascade="all, delete-orphan") + + def save_raw_email(self, raw_content): + """保存原始邮件内容到文件""" + storage_path = active_config.MAIL_STORAGE_PATH + mailbox_dir = os.path.join(storage_path, str(self.mailbox_id)) + os.makedirs(mailbox_dir, exist_ok=True) + + # 保存原始邮件内容 + file_path = os.path.join(mailbox_dir, f"{self.id}.eml") + with open(file_path, 'wb') as f: + f.write(raw_content) + + def extract_verification_data(self): + """ + 尝试从邮件内容中提取验证码和验证链接 + 这个方法会在邮件保存时自动调用 + """ + logger = logging.getLogger(__name__) + + # 合并文本和HTML内容用于搜索 + content = f"{self.subject or ''} {self.body_text or ''} {self.body_html or ''}" + logger.info(f"开始提取邮件ID={self.id}的验证信息,内容长度={len(content)}") + + # 首先检查是否是Cursor验证邮件 + if "Verify your email" in self.subject and ( + "cursor.sh" in self.sender.lower() or + "cursor" in self.sender.lower() + ): + logger.info("检测到Cursor验证邮件") + # 从HTML中提取6位数字验证码 + cursor_patterns = [ + r'(\d{6})', # 匹配Cursor邮件中的6位数字验证码格式 + r']*>(\d{6})', # 更宽松的匹配 + r'>(\d{6})<', # 最简单的形式 + r'(\d{6})' # 任何6位数字 + ] + + for pattern in cursor_patterns: + matches = re.findall(pattern, content) + if matches: + self.verification_code = matches[0] + logger.info(f"从Cursor邮件中提取到验证码: {self.verification_code}") + break + + return + + # 提取可能的验证码(4-8位数字或字母组合) + code_patterns = [ + r'\b([A-Z0-9]{4,8})\b', # 大写字母和数字 + r'验证码[::]\s*([A-Z0-9]{4,8})', # 中文格式 + r'验证码是[::]\s*([A-Z0-9]{4,8})', # 中文格式2 + r'code[::]\s*([A-Z0-9]{4,8})', # 英文格式 + r'code is[::]\s*([A-Z0-9]{4,8})', # 英文格式2 + r'code[::]\s*<[^>]*>([A-Z0-9]{4,8})', # HTML格式 + r']*>([0-9]{4,8})', # HTML分隔的数字 + r']*>([A-Z0-9]{4,8})', # 粗体验证码 + ] + + for pattern in code_patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + if matches: + # 过滤掉明显不是验证码的结果 + filtered_matches = [m for m in matches if len(m) >= 4 and not m.lower() in ['code', 'verify', 'http', 'https']] + if filtered_matches: + self.verification_code = filtered_matches[0] + logger.info(f"提取到验证码: {self.verification_code}") + break + + # 提取验证链接 + link_patterns = [ + r'https?://\S+(?:verify|confirm|activate)\S+', + r'https?://\S+(?:token|auth|account)\S+', + ] + + for pattern in link_patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + if matches: + self.verification_link = matches[0] + logger.info(f"提取到验证链接: {self.verification_link}") + break + + # 如果没有找到验证码,但邮件主题暗示这是验证邮件 + verify_subjects = ['verify', 'confirmation', 'activate', 'validation', '验证', '确认'] + if not self.verification_code and any(subj in self.subject.lower() for subj in verify_subjects): + logger.info("根据主题判断这可能是验证邮件,但未能提取到验证码") + # 尝试从HTML中提取明显的数字序列 + if self.body_html: + number_matches = re.findall(r'(\d{4,8})', self.body_html) + filtered_numbers = [n for n in number_matches if len(n) >= 4 and len(n) <= 8] + if filtered_numbers: + self.verification_code = filtered_numbers[0] + logger.info(f"从HTML中提取到可能的验证码: {self.verification_code}") + + logger.info(f"验证信息提取完成: code={self.verification_code}, link={self.verification_link}") + + def __repr__(self): + return f"" + + def to_dict(self): + """转换为字典,用于API响应""" + return { + "id": self.id, + "mailbox_id": self.mailbox_id, + "sender": self.sender, + "recipients": self.recipients, + "subject": self.subject, + "received_at": self.received_at.isoformat() if self.received_at else None, + "read": self.read, + "verification_code": self.verification_code, + "verification_link": self.verification_link, + "has_attachments": len(self.attachments) > 0 if self.attachments else False + } \ No newline at end of file diff --git a/old/app/models/mailbox.py b/old/app/models/mailbox.py new file mode 100644 index 0000000..9d577c1 --- /dev/null +++ b/old/app/models/mailbox.py @@ -0,0 +1,50 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from datetime import datetime +import secrets + +from . import Base + + +class Mailbox(Base): + """邮箱模型""" + __tablename__ = 'mailboxes' + + id = Column(Integer, primary_key=True) + address = Column(String(255), unique=True, nullable=False, index=True) + domain_id = Column(Integer, ForeignKey('domains.id'), nullable=False) + password_hash = Column(String(255), nullable=True) + description = Column(String(500), nullable=True) + active = Column(Boolean, default=True) + api_key = Column(String(64), unique=True, default=lambda: secrets.token_hex(16)) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + last_accessed = Column(DateTime, nullable=True) + + # 关系 + domain = relationship("Domain", back_populates="mailboxes") + emails = relationship("Email", back_populates="mailbox", cascade="all, delete-orphan") + + @property + def full_address(self): + """获取完整邮箱地址 (包含域名)""" + return f"{self.address}@{self.domain.name}" + + def __repr__(self): + return f"" + + def to_dict(self): + """转换为字典,用于API响应""" + return { + "id": self.id, + "address": self.address, + "domain_id": self.domain_id, + "domain_name": self.domain.name if self.domain else None, + "full_address": self.full_address, + "description": self.description, + "active": self.active, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + "last_accessed": self.last_accessed.isoformat() if self.last_accessed else None, + "email_count": len(self.emails) if self.emails else 0 + } \ No newline at end of file diff --git a/old/app/services/__init__.py b/old/app/services/__init__.py new file mode 100644 index 0000000..91004a1 --- /dev/null +++ b/old/app/services/__init__.py @@ -0,0 +1,44 @@ +# 服务层初始化文件 +# 这里将导入所有服务模块以便于统一调用 + +from .smtp_server import SMTPServer +from .email_processor import EmailProcessor +from .mail_store import MailStore + +# 全局服务实例 +_smtp_server = None +_email_processor = None +_mail_store = None + +def register_smtp_server(instance): + """注册SMTP服务器实例""" + global _smtp_server + _smtp_server = instance + +def register_email_processor(instance): + """注册邮件处理器实例""" + global _email_processor + _email_processor = instance + +def register_mail_store(instance): + """注册邮件存储实例""" + global _mail_store + _mail_store = instance + +def get_smtp_server(): + """获取SMTP服务器实例""" + return _smtp_server + +def get_email_processor(): + """获取邮件处理器实例""" + return _email_processor + +def get_mail_store(): + """获取邮件存储实例""" + return _mail_store + +__all__ = [ + 'SMTPServer', 'EmailProcessor', 'MailStore', + 'register_smtp_server', 'register_email_processor', 'register_mail_store', + 'get_smtp_server', 'get_email_processor', 'get_mail_store' +] \ No newline at end of file diff --git a/old/app/services/__pycache__/__init__.cpython-312.pyc b/old/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1697e67a2548fe14e430467585176c6c9873be6e GIT binary patch literal 1410 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 literal 0 HcmV?d00001 diff --git a/old/app/services/__pycache__/mail_store.cpython-312.pyc b/old/app/services/__pycache__/mail_store.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b0efe1bf2f0aa576d2b12fe37d9d3c01c882fee GIT binary patch literal 11617 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 xcbN5eSn2PuLP*9CQ6}V6-qPgtn!N`5!K^O{M?< literal 0 HcmV?d00001 diff --git a/old/app/services/__pycache__/smtp_server.cpython-312.pyc b/old/app/services/__pycache__/smtp_server.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e06ad927dfec4e66fb874fad3ef04d9d31c196ed GIT binary patch literal 6214 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