This commit is contained in:
Your Name
2026-07-20 13:40:05 +08:00
parent 9e34f6f0f9
commit bca0da7c7d
12 changed files with 1609 additions and 525 deletions
+239
View File
@@ -0,0 +1,239 @@
from pydantic_settings import BaseSettings
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import smtplib
from typing import Optional, List
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
class EmailSettings(BaseSettings):
"""邮件配置"""
SMTP_HOST: str = "localhost"
SMTP_PORT: int = 25
SMTP_USER: Optional[str] = None
SMTP_PASSWORD: Optional[str] = None
SMTP_USE_TLS: bool = False
SMTP_USE_SSL: bool = False
SMTP_FROM: str = "ipam@localhost"
class Config:
env_prefix = "EMAIL_"
class EmailService:
"""邮件服务"""
def __init__(self, settings: EmailSettings):
self.settings = settings
def send_email(self, to: str, subject: str, html_content: str) -> bool:
"""
发送HTML邮件
Args:
to: 收件人邮箱
subject: 邮件主题
html_content: HTML内容
Returns:
是否发送成功
"""
try:
msg = MIMEMultipart('alternative')
msg['From'] = self.settings.SMTP_FROM
msg['To'] = to
msg['Subject'] = subject
html_part = MIMEText(html_content, 'html', 'utf-8')
msg.attach(html_part)
# 连接SMTP服务器
if self.settings.SMTP_USE_SSL:
server = smtplib.SMTP_SSL(
host=self.settings.SMTP_HOST,
port=self.settings.SMTP_PORT,
timeout=10
)
else:
server = smtplib.SMTP(
host=self.settings.SMTP_HOST,
port=self.settings.SMTP_PORT,
timeout=10
)
# 如果需要TLS加密
if self.settings.SMTP_USE_TLS and not self.settings.SMTP_USE_SSL:
server.starttls()
# 如果需要认证
if self.settings.SMTP_USER and self.settings.SMTP_PASSWORD:
server.login(self.settings.SMTP_USER, self.settings.SMTP_PASSWORD)
# 发送邮件
server.send_message(msg)
server.quit()
logger.info(f"邮件发送成功: {to} - {subject}")
return True
except Exception as e:
logger.error(f"邮件发送失败: {str(e)}", exc_info=True)
return False
def send_alert_notification(self, to: str, alert_data: dict) -> bool:
"""发送告警通知邮件"""
severity_colors = {
'critical': '#dc2626',
'error': '#ef4444',
'warning': '#f59e0b',
'info': '#3b82f6'
}
severity_texts = {
'critical': '严重',
'error': '错误',
'warning': '警告',
'info': '信息'
}
alert_type_texts = {
'ip_conflict': 'IP 地址冲突',
'unauthorized_access': '未授权设备接入',
'subnet_full': '网段容量不足',
'device_offline': '设备离线',
'new_device_detected': '新设备发现',
'mac_changed': 'MAC 地址变更'
}
severity = alert_data.get('severity', 'warning')
color = severity_colors.get(severity, '#6b7280')
severity_text = severity_texts.get(severity, severity)
alert_type_text = alert_type_texts.get(alert_data.get('alert_type'), alert_data.get('alert_type'))
html_content = f"""
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.header {{ background-color: {color}; color: white; padding: 20px; border-radius: 5px; }}
.content {{ padding: 20px; background-color: #f9fafb; border-radius: 5px; margin-top: 10px; }}
.alert-item {{ margin-bottom: 15px; }}
.label {{ font-weight: bold; color: #6b7280; display: inline-block; width: 100px; }}
.footer {{ margin-top: 20px; padding-top: 20px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #9ca3af; }}
</style>
</head>
<body>
<div class="header">
<h2>⚠️ IPAM 告警通知</h2>
<p>告警级别: <strong>{severity_text}</strong></p>
</div>
<div class="content">
<div class="alert-item">
<span class="label">告警类型:</span> {alert_type_text}
</div>
<div class="alert-item">
<span class="label">告警标题:</span> {alert_data.get('title', '-')}
</div>
<div class="alert-item">
<span class="label">详细信息:</span> {alert_data.get('message', '-')}
</div>
<div class="alert-item">
<span class="label">相关IP:</span> {alert_data.get('ip_address_str', '-')}
</div>
<div class="alert-item">
<span class="label">相关MAC:</span> {alert_data.get('mac_address', '-')}
</div>
<div class="alert-item">
<span class="label">告警时间:</span> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
</div>
</div>
<div class="footer">
<p>此邮件由 IPAM 地址管理系统自动发送,请勿直接回复。</p>
<p>如需处理该告警,请登录 IPAM 管理控制台: <a href="#">点击访问</a></p>
</div>
</body>
</html>
"""
subject = f"{severity_text}】IPAM 告警 - {alert_data.get('title', '新告警')}"
return self.send_email(to, subject, html_content)
def send_scan_report(self, to: str, report_data: dict) -> bool:
"""发送扫描报告邮件"""
html_content = f"""
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.header {{ background-color: #3b82f6; color: white; padding: 20px; border-radius: 5px; }}
.content {{ padding: 20px; background-color: #f9fafb; border-radius: 5px; margin-top: 10px; }}
.stat-item {{ display: inline-block; width: 22%; text-align: center; padding: 15px; background: white; border-radius: 5px; margin: 1%; }}
.stat-number {{ font-size: 28px; font-weight: bold; color: #3b82f6; }}
.stat-label {{ color: #6b7280; font-size: 14px; }}
.success {{ color: #10b981; }}
.warning {{ color: #f59e0b; }}
.footer {{ margin-top: 20px; padding-top: 20px; border-top: 1px solid #e5e7eb; font-size: 12px; color: #9ca3af; }}
</style>
</head>
<body>
<div class="header">
<h2>📊 IPAM 网段扫描报告</h2>
</div>
<div class="content">
<h3>扫描结果概览</h3>
<div class="stat-item">
<div class="stat-number">{report_data.get('total_ips', 0)}</div>
<div class="stat-label">扫描IP总数</div>
</div>
<div class="stat-item">
<div class="stat-number success">{report_data.get('online_ips', 0)}</div>
<div class="stat-label">在线设备</div>
</div>
<div class="stat-item">
<div class="stat-number">{report_data.get('with_mac', 0)}</div>
<div class="stat-label">发现MAC地址</div>
</div>
<div class="stat-item">
<div class="stat-number warning">{report_data.get('new_devices', 0)}</div>
<div class="stat-label">新发现设备</div>
</div>
<div style="clear: both; margin-top: 20px;">
<p><strong>扫描网段:</strong> {report_data.get('network_cidr', '-')}</p>
<p><strong>扫描时间:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
</div>
</div>
<div class="footer">
<p>此邮件由 IPAM 地址管理系统自动发送,请勿直接回复。</p>
</div>
</body>
</html>
"""
subject = f"IPAM 扫描报告 - {report_data.get('network_cidr', '')}"
return self.send_email(to, subject, html_content)
# 全局邮件服务实例
_email_service = None
def get_email_service() -> EmailService:
"""获取邮件服务实例"""
global _email_service
if _email_service is None:
settings = EmailSettings()
_email_service = EmailService(settings)
return _email_service
def init_email_service(settings: EmailSettings):
"""初始化邮件服务"""
global _email_service
_email_service = EmailService(settings)