first commit
This commit is contained in:
220
notifier.py
Normal file
220
notifier.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import smtplib
|
||||
import json
|
||||
import logging
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.header import Header
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Dict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class EmailNotifier:
|
||||
"""邮件通知类"""
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
self.config = config.get('notification', {})
|
||||
self.email_config = self.config.get('email', {})
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.enabled = self.config.get('enabled', False) and self.email_config.get('enabled', False)
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
"""检查邮件通知是否启用"""
|
||||
return self.enabled
|
||||
|
||||
def send_email(
|
||||
self,
|
||||
subject: str,
|
||||
body: str,
|
||||
to_addrs: Optional[List[str]] = None,
|
||||
attachments: Optional[List[str]] = None,
|
||||
is_html: bool = False
|
||||
) -> bool:
|
||||
"""
|
||||
发送邮件
|
||||
|
||||
Args:
|
||||
subject: 邮件主题
|
||||
body: 邮件正文
|
||||
to_addrs: 收件人列表,None时使用配置中的默认收件人
|
||||
attachments: 附件路径列表
|
||||
is_html: 是否为HTML格式
|
||||
|
||||
Returns:
|
||||
bool: 发送成功返回True,否则返回False
|
||||
"""
|
||||
if not self.enabled:
|
||||
self.logger.info("邮件通知未启用")
|
||||
return False
|
||||
|
||||
to_addrs = to_addrs or self.email_config.get('to_addrs', [])
|
||||
if not to_addrs:
|
||||
self.logger.warning("未配置收件人地址")
|
||||
return False
|
||||
|
||||
smtp_host = self.email_config.get('smtp_host', '')
|
||||
smtp_port = self.email_config.get('smtp_port', 587)
|
||||
smtp_user = self.email_config.get('smtp_user', '')
|
||||
smtp_password = self.email_config.get('smtp_password', '')
|
||||
from_addr = self.email_config.get('from_addr', smtp_user)
|
||||
|
||||
if not smtp_host or not smtp_user or not smtp_password:
|
||||
self.logger.error("邮件配置不完整")
|
||||
return False
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['From'] = from_addr
|
||||
msg['To'] = ','.join(to_addrs)
|
||||
msg['Subject'] = Header(subject, 'utf-8')
|
||||
|
||||
if is_html:
|
||||
msg.attach(MIMEText(body, 'html', 'utf-8'))
|
||||
else:
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
if attachments:
|
||||
for attachment_path in attachments:
|
||||
attachment_file = Path(attachment_path)
|
||||
if attachment_file.exists():
|
||||
with open(attachment_file, 'rb') as f:
|
||||
part = MIMEApplication(f.read())
|
||||
part.add_header(
|
||||
'Content-Disposition',
|
||||
'attachment',
|
||||
filename=attachment_file.name
|
||||
)
|
||||
msg.attach(part)
|
||||
else:
|
||||
self.logger.warning(f"附件不存在: {attachment_path}")
|
||||
|
||||
server = smtplib.SMTP(smtp_host, smtp_port)
|
||||
server.starttls()
|
||||
server.login(smtp_user, smtp_password)
|
||||
server.sendmail(from_addr, to_addrs, msg.as_string())
|
||||
server.quit()
|
||||
|
||||
self.logger.info(f"邮件发送成功: {subject} -> {to_addrs}")
|
||||
return True
|
||||
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
self.logger.error("邮件认证失败,请检查用户名和密码")
|
||||
except smtplib.SMTPConnectError:
|
||||
self.logger.error("无法连接到SMTP服务器")
|
||||
except smtplib.SMTPSenderRefused:
|
||||
self.logger.error("发件人地址被拒绝")
|
||||
except Exception as e:
|
||||
self.logger.error(f"邮件发送失败: {e}")
|
||||
|
||||
return False
|
||||
|
||||
def send_policy_report(
|
||||
self,
|
||||
articles: List[Dict],
|
||||
to_addrs: Optional[List[str]] = None,
|
||||
report_file: Optional[str] = None
|
||||
) -> bool:
|
||||
"""
|
||||
发送政策检索报告邮件
|
||||
|
||||
Args:
|
||||
articles: 文章列表
|
||||
to_addrs: 收件人列表
|
||||
report_file: Excel报告文件路径
|
||||
|
||||
Returns:
|
||||
bool: 发送成功返回True
|
||||
"""
|
||||
if not articles:
|
||||
return False
|
||||
|
||||
subject = f"政策法规检索报告 - {datetime.now().strftime('%Y-%m-%d')}"
|
||||
|
||||
category_stats = {}
|
||||
for article in articles:
|
||||
category = article.get('category', '其他')
|
||||
category_stats[category] = category_stats.get(category, 0) + 1
|
||||
|
||||
source_stats = {}
|
||||
for article in articles:
|
||||
source = article.get('source', '未知')
|
||||
source_stats[source] = source_stats.get(source, 0) + 1
|
||||
|
||||
body_lines = [
|
||||
f"<h2>政策法规检索报告</h2>",
|
||||
f"<p><strong>检索时间:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>",
|
||||
f"<p><strong>检索结果:</strong> 共 {len(articles)} 条</p>",
|
||||
f"<h3>按类别统计</h3>",
|
||||
f"<ul>",
|
||||
]
|
||||
|
||||
for category, count in sorted(category_stats.items(), key=lambda x: -x[1]):
|
||||
body_lines.append(f" <li>{category}: {count} 条</li>")
|
||||
body_lines.append(f"</ul>")
|
||||
|
||||
body_lines.append(f"<h3>按来源统计</h3>")
|
||||
body_lines.append(f"<ul>")
|
||||
for source, count in sorted(source_stats.items(), key=lambda x: -x[1]):
|
||||
body_lines.append(f" <li>{source}: {count} 条</li>")
|
||||
body_lines.append(f"</ul>")
|
||||
|
||||
body_lines.append(f"<h3>最新政策列表</h3>")
|
||||
body_lines.append(f"<table border='1' cellpadding='5' style='border-collapse: collapse;'>")
|
||||
body_lines.append(f"<tr><th>标题</th><th>来源</th><th>类别</th><th>发布时间</th></tr>")
|
||||
|
||||
for i, article in enumerate(articles[:20]):
|
||||
title = article.get('title', '')[:50]
|
||||
source = article.get('source', '')
|
||||
category = article.get('category', '')
|
||||
publish_date = article.get('publish_date', '')
|
||||
body_lines.append(
|
||||
f"<tr><td>{title}</td><td>{source}</td><td>{category}</td><td>{publish_date}</td></tr>"
|
||||
)
|
||||
|
||||
body_lines.append(f"</table>")
|
||||
|
||||
if len(articles) > 20:
|
||||
body_lines.append(f"<p>... 共 {len(articles)} 条记录,仅显示前20条</p>")
|
||||
|
||||
body_lines.append(f"<hr>")
|
||||
body_lines.append(f"<p style='color: #666; font-size: 12px;'>")
|
||||
body_lines.append(f"本报告由政策法规检索系统自动生成<br>")
|
||||
body_lines.append(f"</p>")
|
||||
|
||||
body = ''.join(body_lines)
|
||||
attachments = [report_file] if report_file else None
|
||||
|
||||
return self.send_email(subject, body, to_addrs, attachments, is_html=True)
|
||||
|
||||
def send_error_alert(
|
||||
self,
|
||||
error_message: str,
|
||||
to_addrs: Optional[List[str]] = None
|
||||
) -> bool:
|
||||
"""
|
||||
发送错误告警邮件
|
||||
|
||||
Args:
|
||||
error_message: 错误信息
|
||||
to_addrs: 收件人列表
|
||||
|
||||
Returns:
|
||||
bool: 发送成功返回True
|
||||
"""
|
||||
subject = f"[警告] 政策法规检索任务执行失败 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
body = f"""
|
||||
<h2>任务执行失败告警</h2>
|
||||
<p><strong>发生时间:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
|
||||
<p><strong>错误信息:</strong></p>
|
||||
<pre style="background-color: #f5f5f5; padding: 10px; border-radius: 5px;">{error_message}</pre>
|
||||
<p>请及时检查系统运行状态。</p>
|
||||
"""
|
||||
|
||||
return self.send_email(subject, body, to_addrs, is_html=True)
|
||||
|
||||
|
||||
def create_notifier(config: Dict) -> EmailNotifier:
|
||||
"""创建邮件通知器工厂函数"""
|
||||
return EmailNotifier(config)
|
||||
Reference in New Issue
Block a user