351 lines
13 KiB
Python
Executable File
351 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
邮件发送器
|
|
用于发送每日资讯摘要
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import smtplib
|
|
import logging
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.application import MIMEApplication
|
|
from datetime import datetime
|
|
from typing import List, Dict, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class EmailSender:
|
|
"""邮件发送器"""
|
|
|
|
def __init__(self, config_path=None):
|
|
"""初始化邮件发送器"""
|
|
if config_path is None:
|
|
config_path = "/home/ubuntu/nanocode/it_automation_enhanced/config/config.json"
|
|
|
|
self.config_path = config_path
|
|
self.load_config()
|
|
|
|
# 检查配置
|
|
email_config = self.config.get('email', {})
|
|
if not email_config.get('smtp_host') or email_config.get('smtp_host', '').startswith('{{'):
|
|
logger.warning("邮件配置不完整,邮件功能将禁用")
|
|
|
|
def load_config(self):
|
|
"""加载配置"""
|
|
try:
|
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
|
self.config = json.load(f)
|
|
logger.info(f"邮件配置加载成功")
|
|
except Exception as e:
|
|
logger.error(f"配置加载失败: {e}")
|
|
self.config = {}
|
|
|
|
def send_email(self, subject: str, content: str, to_emails: List[str] = None,
|
|
attachments: List[str] = None) -> bool:
|
|
"""发送邮件"""
|
|
email_config = self.config.get('email', {})
|
|
|
|
# 检查是否启用
|
|
if not email_config.get('enable_daily_digest', True):
|
|
logger.info("邮件功能已禁用")
|
|
return False
|
|
|
|
# 获取配置
|
|
smtp_host = email_config.get('smtp_host')
|
|
smtp_port = email_config.get('smtp_port', 587)
|
|
username = email_config.get('username')
|
|
password = email_config.get('password')
|
|
from_email = email_config.get('from_email')
|
|
|
|
if not all([smtp_host, username, password, from_email]):
|
|
logger.error("邮件配置不完整")
|
|
return False
|
|
|
|
if to_emails is None:
|
|
to_emails = [email_config.get('to_email')]
|
|
if not to_emails[0]:
|
|
logger.error("收件人邮箱未配置")
|
|
return False
|
|
|
|
try:
|
|
# 创建邮件
|
|
msg = MIMEMultipart()
|
|
msg['From'] = from_email
|
|
msg['To'] = ', '.join(to_emails)
|
|
msg['Subject'] = subject
|
|
msg['Date'] = datetime.now().strftime('%a, %d %b %Y %H:%M:%S %z')
|
|
|
|
# 添加正文
|
|
msg.attach(MIMEText(content, 'html' if '<html>' in content.lower() else 'plain', 'utf-8'))
|
|
|
|
# 添加附件
|
|
if attachments:
|
|
for attachment_path in attachments:
|
|
if os.path.exists(attachment_path):
|
|
with open(attachment_path, 'rb') as f:
|
|
part = MIMEApplication(f.read(), Name=os.path.basename(attachment_path))
|
|
part['Content-Disposition'] = f'attachment; filename="{os.path.basename(attachment_path)}"'
|
|
msg.attach(part)
|
|
logger.info(f"添加附件: {attachment_path}")
|
|
|
|
# 连接SMTP服务器并发送
|
|
logger.info(f"连接SMTP服务器: {smtp_host}:{smtp_port}")
|
|
|
|
if smtp_port == 465:
|
|
# SSL连接
|
|
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
|
else:
|
|
# TLS连接
|
|
server = smtplib.SMTP(smtp_host, smtp_port)
|
|
server.starttls()
|
|
|
|
server.login(username, password)
|
|
server.send_message(msg)
|
|
server.quit()
|
|
|
|
logger.info(f"邮件发送成功: {subject} -> {', '.join(to_emails)}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"邮件发送失败: {e}")
|
|
return False
|
|
|
|
def create_daily_digest(self, articles: List[Dict], date: str = None) -> Dict:
|
|
"""创建每日摘要"""
|
|
if date is None:
|
|
date = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
# 统计信息
|
|
total_articles = len(articles)
|
|
sources = {}
|
|
categories = {}
|
|
|
|
for article in articles:
|
|
source = article.get('source', '未知')
|
|
category = article.get('category', '未分类')
|
|
|
|
sources[source] = sources.get(source, 0) + 1
|
|
categories[category] = categories.get(category, 0) + 1
|
|
|
|
# 创建HTML内容
|
|
html_content = f"""
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>IT资讯每日摘要 - {date}</title>
|
|
<style>
|
|
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 800px; margin: 0 auto; padding: 20px; }}
|
|
.header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 10px; margin-bottom: 30px; }}
|
|
.header h1 {{ margin: 0; font-size: 28px; }}
|
|
.header .date {{ margin-top: 10px; opacity: 0.9; }}
|
|
.stats {{ background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; }}
|
|
.stats-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; }}
|
|
.stat-card {{ background: white; padding: 15px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
|
|
.stat-card h3 {{ margin: 0 0 10px 0; color: #667eea; }}
|
|
.stat-card .number {{ font-size: 32px; font-weight: bold; color: #764ba2; }}
|
|
.articles {{ margin-top: 30px; }}
|
|
.article {{ background: white; padding: 20px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-left: 4px solid #667eea; }}
|
|
.article-title {{ font-size: 18px; font-weight: bold; margin: 0 0 10px 0; color: #333; }}
|
|
.article-meta {{ color: #666; font-size: 14px; margin-bottom: 10px; }}
|
|
.article-meta span {{ margin-right: 15px; }}
|
|
.article-summary {{ color: #444; line-height: 1.6; }}
|
|
.footer {{ margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px; text-align: center; }}
|
|
.tag {{ display: inline-block; background: #e9ecef; color: #495057; padding: 4px 8px; border-radius: 4px; font-size: 12px; margin-right: 5px; margin-bottom: 5px; }}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>📰 IT资讯每日摘要</h1>
|
|
<div class="date">{date} | 共收录 {total_articles} 篇文章</div>
|
|
</div>
|
|
|
|
<div class="stats">
|
|
<h2>📊 今日统计</h2>
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<h3>文章总数</h3>
|
|
<div class="number">{total_articles}</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>来源分布</h3>
|
|
<div>
|
|
"""
|
|
|
|
# 添加来源统计
|
|
for source, count in sources.items():
|
|
html_content += f'<div>{source}: {count}篇</div>'
|
|
|
|
html_content += """
|
|
</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<h3>分类分布</h3>
|
|
<div>
|
|
"""
|
|
|
|
# 添加分类统计
|
|
for category, count in categories.items():
|
|
html_content += f'<div>{category}: {count}篇</div>'
|
|
|
|
html_content += f"""
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="articles">
|
|
<h2>📋 今日精选</h2>
|
|
"""
|
|
|
|
# 添加文章列表(最多10篇)
|
|
for i, article in enumerate(articles[:10]):
|
|
title = article.get('title', '无标题')
|
|
source = article.get('source', '未知')
|
|
category = article.get('category', '未分类')
|
|
url = article.get('url', '#')
|
|
summary = article.get('summary', article.get('description', '暂无摘要'))
|
|
|
|
html_content += f"""
|
|
<div class="article">
|
|
<div class="article-title">
|
|
<a href="{url}" style="color: #667eea; text-decoration: none;">{i+1}. {title}</a>
|
|
</div>
|
|
<div class="article-meta">
|
|
<span>来源: {source}</span>
|
|
<span>分类: <span class="tag">{category}</span></span>
|
|
</div>
|
|
<div class="article-summary">
|
|
{summary[:200]}...
|
|
</div>
|
|
</div>
|
|
"""
|
|
|
|
html_content += """
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p>本邮件由 IT资讯自动化系统 自动生成</p>
|
|
<p>系统版本: 1.0.0 | 生成时间: """ + datetime.now().strftime('%Y-%m-%d %H:%M:%S') + """</p>
|
|
<p>如需退订或调整接收频率,请回复本邮件</p>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
# 创建纯文本版本
|
|
text_content = f"IT资讯每日摘要 - {date}\n"
|
|
text_content += "=" * 50 + "\n\n"
|
|
text_content += f"共收录 {total_articles} 篇文章\n\n"
|
|
|
|
text_content += "来源统计:\n"
|
|
for source, count in sources.items():
|
|
text_content += f" {source}: {count}篇\n"
|
|
|
|
text_content += "\n分类统计:\n"
|
|
for category, count in categories.items():
|
|
text_content += f" {category}: {count}篇\n"
|
|
|
|
text_content += "\n精选文章:\n"
|
|
for i, article in enumerate(articles[:5]):
|
|
title = article.get('title', '无标题')
|
|
source = article.get('source', '未知')
|
|
text_content += f"{i+1}. {title} [{source}]\n"
|
|
|
|
text_content += f"\n\n--\nIT资讯自动化系统 v1.0.0\n{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
|
|
|
return {
|
|
'date': date,
|
|
'total_articles': total_articles,
|
|
'sources': sources,
|
|
'categories': categories,
|
|
'html_content': html_content,
|
|
'text_content': text_content,
|
|
'subject': f"IT资讯每日摘要 - {date} ({total_articles}篇文章)"
|
|
}
|
|
|
|
def send_daily_digest(self, articles: List[Dict], date: str = None) -> bool:
|
|
"""发送每日摘要"""
|
|
logger.info(f"准备发送每日摘要,文章数量: {len(articles)}")
|
|
|
|
if not articles:
|
|
logger.warning("没有文章可发送")
|
|
return False
|
|
|
|
# 创建摘要
|
|
digest = self.create_daily_digest(articles, date)
|
|
|
|
# 发送邮件
|
|
success = self.send_email(
|
|
subject=digest['subject'],
|
|
content=digest['html_content'],
|
|
to_emails=None # 使用配置中的收件人
|
|
)
|
|
|
|
if success:
|
|
logger.info(f"每日摘要发送成功: {digest['subject']}")
|
|
else:
|
|
logger.error("每日摘要发送失败")
|
|
|
|
return success
|
|
|
|
def send_test_email(self):
|
|
"""发送测试邮件"""
|
|
test_articles = [
|
|
{
|
|
'title': 'AI芯片技术的最新突破',
|
|
'source': 'TechNews',
|
|
'category': 'AI',
|
|
'url': 'https://example.com/ai-chip',
|
|
'summary': '研究人员在AI芯片能效比方面取得重大进展'
|
|
},
|
|
{
|
|
'title': 'Python 3.12性能优化',
|
|
'source': 'PythonWeekly',
|
|
'category': '编程',
|
|
'url': 'https://example.com/python-optimize',
|
|
'summary': 'Python 3.12在性能方面有显著提升'
|
|
},
|
|
{
|
|
'title': '云计算安全指南',
|
|
'source': 'CloudTech',
|
|
'category': '云计算',
|
|
'url': 'https://example.com/cloud-security',
|
|
'summary': '最新的云计算安全最佳实践'
|
|
}
|
|
]
|
|
|
|
print("发送测试邮件...")
|
|
success = self.send_daily_digest(test_articles)
|
|
|
|
if success:
|
|
print("✅ 测试邮件发送成功")
|
|
else:
|
|
print("❌ 测试邮件发送失败(可能是配置问题)")
|
|
|
|
return success
|
|
|
|
def test_email_sender():
|
|
"""测试邮件发送器"""
|
|
import sys
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
sender = EmailSender()
|
|
|
|
# 检查配置
|
|
email_config = sender.config.get('email', {})
|
|
print("邮件配置检查:")
|
|
for key, value in email_config.items():
|
|
if 'password' in key:
|
|
print(f" {key}: [已隐藏]")
|
|
else:
|
|
print(f" {key}: {value}")
|
|
|
|
print("\n发送测试邮件...")
|
|
sender.send_test_email()
|
|
|
|
if __name__ == "__main__":
|
|
test_email_sender() |