349 lines
12 KiB
Python
Executable File
349 lines
12 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
邮件发送器 - 支持附件(基于老流程)
|
||
专门用于IT资讯自动化系统的邮件发送
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import smtplib
|
||
import logging
|
||
import zipfile
|
||
import datetime
|
||
from email.mime.text import MIMEText
|
||
from email.mime.multipart import MIMEMultipart
|
||
from email.mime.base import MIMEBase
|
||
from email import encoders
|
||
from pathlib import Path
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class EmailWithAttachments:
|
||
"""支持附件的邮件发送器(老流程兼容版)"""
|
||
|
||
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()
|
||
|
||
def load_config(self):
|
||
"""加载配置"""
|
||
try:
|
||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||
self.config = json.load(f)
|
||
logger.info("邮件配置加载成功")
|
||
except Exception as e:
|
||
logger.error(f"配置加载失败: {e}")
|
||
self.config = {}
|
||
|
||
def create_zip_package(self, article_data, markdown_content, images_dir=None):
|
||
"""
|
||
创建ZIP包(老流程格式)
|
||
|
||
Args:
|
||
article_data: 文章数据字典
|
||
markdown_content: Markdown内容
|
||
images_dir: 图片目录路径
|
||
|
||
Returns:
|
||
ZIP文件路径,或None(失败时)
|
||
"""
|
||
try:
|
||
# 生成日期格式文件名(老流程格式:2026314.zip)
|
||
today = datetime.datetime.now()
|
||
zip_filename = today.strftime("%Y%m%d") + ".zip"
|
||
|
||
# 创建输出目录
|
||
output_dir = "/home/ubuntu/nanocode/it_automation_enhanced/output"
|
||
os.makedirs(output_dir, exist_ok=True)
|
||
|
||
zip_path = os.path.join(output_dir, zip_filename)
|
||
|
||
# 创建ZIP文件
|
||
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||
# 1. 创建article_7目录结构
|
||
article_id = article_data.get('id', 7) # 默认使用7,老流程格式
|
||
article_dir = f"article_{article_id}"
|
||
|
||
# 2. 添加Markdown文件
|
||
md_filename = f"article_{article_id}.md"
|
||
md_content = self._format_markdown_with_images(markdown_content, article_data)
|
||
|
||
# 在ZIP中创建目录结构
|
||
zipf.writestr(f"{article_dir}/{md_filename}", md_content)
|
||
logger.info(f"添加Markdown文件: {article_dir}/{md_filename}")
|
||
|
||
# 3. 添加图片文件(如果存在)
|
||
image_count = 0
|
||
if images_dir and os.path.exists(images_dir):
|
||
for root, dirs, files in os.walk(images_dir):
|
||
for file in files:
|
||
if file.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.gif')):
|
||
file_path = os.path.join(root, file)
|
||
arcname = f"images/{file}"
|
||
zipf.write(file_path, arcname)
|
||
image_count += 1
|
||
logger.debug(f"添加图片: {arcname}")
|
||
|
||
# 4. 如果没有图片,创建占位图片
|
||
if image_count == 0:
|
||
logger.info("没有实际图片,创建占位图片...")
|
||
placeholder_content = self._create_placeholder_image()
|
||
placeholder_name = "placeholder_image.png"
|
||
zipf.writestr(f"images/{placeholder_name}", placeholder_content)
|
||
image_count = 1
|
||
logger.info(f"添加占位图片: images/{placeholder_name}")
|
||
|
||
logger.info(f"添加 {image_count} 张图片到ZIP包")
|
||
|
||
logger.info(f"ZIP包创建成功: {zip_path} ({os.path.getsize(zip_path):,} 字节)")
|
||
return zip_path
|
||
|
||
except Exception as e:
|
||
logger.error(f"创建ZIP包失败: {e}")
|
||
return None
|
||
|
||
def _create_placeholder_image(self):
|
||
"""创建占位图片"""
|
||
try:
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
import io
|
||
|
||
# 创建简单图片
|
||
img = Image.new('RGB', (400, 300), color=(70, 130, 180))
|
||
draw = ImageDraw.Draw(img)
|
||
|
||
# 添加文字
|
||
try:
|
||
font = ImageFont.truetype("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", 24)
|
||
except:
|
||
font = ImageFont.load_default()
|
||
|
||
draw.text((100, 120), "图片占位符", fill=(255, 255, 255), font=font)
|
||
draw.text((80, 160), "IT资讯自动化系统", fill=(255, 255, 200), font=font)
|
||
|
||
# 保存到字节流
|
||
img_byte_arr = io.BytesIO()
|
||
img.save(img_byte_arr, format='PNG')
|
||
return img_byte_arr.getvalue()
|
||
|
||
except Exception as e:
|
||
logger.warning(f"创建占位图片失败: {e}")
|
||
# 返回空字节,ZIP包仍然可以创建
|
||
return b''
|
||
|
||
def _format_markdown_with_images(self, content, article_data):
|
||
"""格式化Markdown,确保图片链接使用老流程格式"""
|
||
# 确保图片链接使用 ../images/filename.webp 格式
|
||
import re
|
||
|
||
# 替换常见的图片链接格式
|
||
patterns = [
|
||
(r'!\[.*?\]\((.*?)\)', r''), # 简化描述
|
||
(r'!\[图片\]\(images/(.*?)\)', r''), # 确保相对路径
|
||
(r'!\[.*?\]\(\./images/(.*?)\)', r''),
|
||
(r'!\[.*?\]\(.*?/(.*?\.(?:png|jpg|jpeg|webp|gif))\)', r''),
|
||
]
|
||
|
||
formatted = content
|
||
for pattern, replacement in patterns:
|
||
formatted = re.sub(pattern, replacement, formatted)
|
||
|
||
# 添加文章信息头
|
||
header = f"""# {article_data.get('title', 'IT资讯文章')}
|
||
|
||
**来源**: {article_data.get('source', '未知')}
|
||
**日期**: {article_data.get('publish_date', datetime.datetime.now().strftime('%Y-%m-%d'))}
|
||
**字数**: {article_data.get('word_count', 0):,}
|
||
**原文链接**: {article_data.get('url', '#')}
|
||
|
||
---
|
||
|
||
"""
|
||
|
||
return header + formatted
|
||
|
||
def send_article_email(self, article_data, markdown_content, images_dir=None):
|
||
"""
|
||
发送文章邮件(老流程兼容)
|
||
|
||
Args:
|
||
article_data: 文章数据
|
||
markdown_content: Markdown内容
|
||
images_dir: 图片目录
|
||
|
||
Returns:
|
||
bool: 是否成功
|
||
"""
|
||
email_config = self.config.get('email', {})
|
||
|
||
# 检查配置
|
||
if not all([email_config.get('smtp_host'), email_config.get('username'),
|
||
email_config.get('password'), email_config.get('from_email')]):
|
||
logger.error("邮件配置不完整")
|
||
return False
|
||
|
||
# 1. 创建ZIP包
|
||
zip_path = self.create_zip_package(article_data, markdown_content, images_dir)
|
||
if not zip_path:
|
||
logger.error("无法创建ZIP包,邮件发送失败")
|
||
return False
|
||
|
||
# 2. 构建邮件内容(老流程格式)
|
||
email_subject = f"IT资讯摘要 - {article_data.get('source', '未知')}: {article_data.get('title', '')[:30]}..."
|
||
|
||
email_body = self._create_email_body(article_data, zip_path)
|
||
|
||
# 3. 发送邮件
|
||
try:
|
||
# 连接SMTP服务器
|
||
smtp_host = email_config['smtp_host']
|
||
smtp_port = email_config.get('smtp_port', 587)
|
||
username = email_config['username']
|
||
password = email_config['password']
|
||
from_email = email_config['from_email']
|
||
to_email = email_config.get('to_email', from_email) # 默认发给自己
|
||
|
||
# 创建邮件
|
||
msg = MIMEMultipart()
|
||
msg['From'] = from_email
|
||
msg['To'] = to_email
|
||
msg['Subject'] = email_subject
|
||
|
||
# 添加正文
|
||
msg.attach(MIMEText(email_body, 'plain', 'utf-8'))
|
||
|
||
# 添加ZIP附件
|
||
with open(zip_path, 'rb') as f:
|
||
part = MIMEBase('application', 'zip')
|
||
part.set_payload(f.read())
|
||
encoders.encode_base64(part)
|
||
part.add_header('Content-Disposition', f'attachment; filename="{os.path.basename(zip_path)}"')
|
||
msg.attach(part)
|
||
|
||
# 发送邮件
|
||
if smtp_port == 465:
|
||
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
||
else:
|
||
server = smtplib.SMTP(smtp_host, smtp_port)
|
||
server.starttls()
|
||
|
||
server.login(username, password)
|
||
server.send_message(msg)
|
||
server.quit()
|
||
|
||
logger.info(f"邮件发送成功: {email_subject}")
|
||
logger.info(f"ZIP附件: {os.path.basename(zip_path)}")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
logger.error(f"邮件发送失败: {e}")
|
||
return False
|
||
|
||
def _create_email_body(self, article_data, zip_path):
|
||
"""创建邮件正文(老流程格式)"""
|
||
zip_filename = os.path.basename(zip_path)
|
||
article_id = article_data.get('id', 7)
|
||
|
||
return f"""IT资讯自动化处理报告
|
||
|
||
**文章信息:**
|
||
- 标题: {article_data.get('title', '无标题')}
|
||
- 来源: {article_data.get('source', '未知')}
|
||
- 字数: {article_data.get('word_count', 0):,}
|
||
- 日期: {article_data.get('publish_date', '最近')}
|
||
- 处理时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||
|
||
**处理流程:**
|
||
1. 自动抓取(最近7天内文章)
|
||
2. 内容筛选(≥500字)
|
||
3. 图片下载(保存到本地)
|
||
4. DeepSeek智能处理
|
||
5. Markdown格式生成(包含本地图片链接)
|
||
6. 自动邮件发送
|
||
|
||
**附件内容:**
|
||
- 完整ZIP包: {zip_filename}
|
||
- 包含Markdown文件和所有图片
|
||
- 解压后目录结构:
|
||
├── article_{article_id}/article_{article_id}.md
|
||
└── images/ (所有图片文件)
|
||
|
||
**使用说明:**
|
||
1. 下载ZIP附件
|
||
2. 解压到任意目录
|
||
3. 用Markdown编辑器打开 article_{article_id}/article_{article_id}.md
|
||
4. 图片将自动显示(使用相对路径 ../images/filename.webp)
|
||
|
||
**目录结构示例:**
|
||
解压后/
|
||
├── article_{article_id}/
|
||
│ └── article_{article_id}.md
|
||
└── images/
|
||
├── image1.webp
|
||
├── image2.webp
|
||
└── ...
|
||
|
||
原文链接: {article_data.get('url', '#')}
|
||
|
||
此邮件由 nanobot IT资讯自动化系统生成。
|
||
"""
|
||
|
||
def test_email_with_attachments():
|
||
"""测试邮件附件功能"""
|
||
import sys
|
||
|
||
logging.basicConfig(level=logging.INFO)
|
||
|
||
sender = EmailWithAttachments()
|
||
|
||
# 测试数据
|
||
test_article = {
|
||
'id': 7,
|
||
'title': '测试文章:AI芯片技术突破',
|
||
'source': 'TechNews',
|
||
'word_count': 1200,
|
||
'publish_date': '2026-03-19',
|
||
'url': 'https://example.com/test-article'
|
||
}
|
||
|
||
test_markdown = """# 测试文章
|
||
|
||
这是一篇测试文章的内容。
|
||
|
||

|
||
|
||
文章正文继续...
|
||
"""
|
||
|
||
print("测试邮件附件功能...")
|
||
|
||
# 测试ZIP创建
|
||
print("\n1. 测试ZIP包创建...")
|
||
zip_path = sender.create_zip_package(test_article, test_markdown)
|
||
if zip_path:
|
||
print(f"✅ ZIP包创建成功: {zip_path}")
|
||
else:
|
||
print("❌ ZIP包创建失败")
|
||
|
||
# 测试邮件发送(需要实际配置)
|
||
print("\n2. 测试邮件发送...")
|
||
print("⚠️ 需要配置SMTP信息才能实际测试")
|
||
|
||
# 检查配置
|
||
email_config = sender.config.get('email', {})
|
||
if email_config.get('smtp_host') and not email_config.get('smtp_host', '').startswith('{{'):
|
||
print("✅ 邮件配置已设置,可以测试发送")
|
||
# 实际发送测试(可选)
|
||
# success = sender.send_article_email(test_article, test_markdown)
|
||
else:
|
||
print("⏸️ 邮件配置未设置,跳过发送测试")
|
||
|
||
print("\n测试完成")
|
||
|
||
if __name__ == "__main__":
|
||
test_email_with_attachments() |