初始化提交,包含完整的邮件系统代码

This commit is contained in:
huangzhenpc
2025-02-25 19:50:00 +08:00
commit aeffc4f8b8
52 changed files with 6673 additions and 0 deletions

46
app/models/__init__.py Normal file
View File

@@ -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']

71
app/models/attachment.py Normal file
View File

@@ -0,0 +1,71 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, LargeBinary
from sqlalchemy.orm import relationship
from datetime import datetime
import os
from . import Base
class Attachment(Base):
"""附件模型"""
__tablename__ = 'attachments'
id = Column(Integer, primary_key=True)
email_id = Column(Integer, ForeignKey('emails.id'), nullable=False, index=True)
filename = Column(String(255), nullable=False)
content_type = Column(String(100), nullable=True)
size = Column(Integer, nullable=False, default=0)
storage_path = Column(String(500), nullable=True) # 用于文件系统存储
content = Column(LargeBinary, nullable=True) # 用于小型附件的直接存储
created_at = Column(DateTime, default=datetime.utcnow)
# 关联关系
email = relationship("Email", back_populates="attachments")
@property
def is_stored_in_fs(self):
"""判断附件是否存储在文件系统中"""
return bool(self.storage_path and not self.content)
def save_to_filesystem(self, content, base_path):
"""将附件保存到文件系统"""
# 确保目录存在
os.makedirs(base_path, exist_ok=True)
# 创建文件路径
file_path = os.path.join(
base_path,
f"{self.email_id}_{self.id}_{self.filename}"
)
# 写入文件
with open(file_path, 'wb') as f:
f.write(content)
# 更新对象属性
self.storage_path = file_path
self.size = len(content)
self.content = None # 清空内存中的内容
return file_path
def get_content(self, attachments_dir=None):
"""获取附件内容,无论是从数据库还是文件系统"""
if self.content:
return self.content
if self.storage_path and os.path.exists(self.storage_path):
with open(self.storage_path, 'rb') as f:
return f.read()
return None
def to_dict(self):
"""转换为字典用于API响应"""
return {
"id": self.id,
"email_id": self.email_id,
"filename": self.filename,
"content_type": self.content_type,
"size": self.size,
"created_at": self.created_at.isoformat() if self.created_at else None
}

35
app/models/domain.py Normal file
View File

@@ -0,0 +1,35 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
from . import Base
class Domain(Base):
"""邮件域名模型"""
__tablename__ = 'domains'
id = Column(Integer, primary_key=True)
name = Column(String(255), unique=True, nullable=False, index=True)
description = Column(String(500), nullable=True)
active = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 关系
mailboxes = relationship("Mailbox", back_populates="domain", cascade="all, delete-orphan")
def __repr__(self):
return f"<Domain {self.name}>"
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
}

98
app/models/email.py Normal file
View File

@@ -0,0 +1,98 @@
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
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):
"""
尝试从邮件内容中提取验证码和验证链接
这个方法会在邮件保存时自动调用
"""
# 合并文本和HTML内容用于搜索
content = f"{self.subject} {self.body_text or ''}"
# 提取可能的验证码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})', # 英文格式
]
for pattern in code_patterns:
matches = re.findall(pattern, content, re.IGNORECASE)
if matches:
self.verification_code = matches[0]
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]
break
def __repr__(self):
return f"<Email {self.id}: {self.subject}>"
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
}

50
app/models/mailbox.py Normal file
View File

@@ -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"<Mailbox {self.full_address}>"
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
}