Files
nanocode/it_automation_enhanced/scripts/email_with_attachments.py
T
Ubuntu 32b43f85bd a
2026-07-03 16:58:28 +08:00

349 lines
12 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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'![图片](\1)'), # 简化描述
(r'!\[图片\]\(images/(.*?)\)', r'![图片](../images/\1)'), # 确保相对路径
(r'!\[.*?\]\(\./images/(.*?)\)', r'![图片](../images/\1)'),
(r'!\[.*?\]\(.*?/(.*?\.(?:png|jpg|jpeg|webp|gif))\)', r'![图片](../images/\1)'),
]
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 = """# 测试文章
这是一篇测试文章的内容。
![测试图片](../images/test_image.webp)
文章正文继续...
"""
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()