Files
zoho-mail-service/zoho_mail_service.py
T

299 lines
9.7 KiB
Python
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.
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
import secrets
import os
import shutil
import string
import hashlib
from datetime import datetime
from flask import Flask, request, jsonify
app = Flask(__name__)
# ==================== 配置 ====================
SMTP_SERVER = "smtp.zoho.com.cn"
SMTP_PORT = 465
SENDER_EMAIL = "yourname@yourname"
# ==================== 邮件密码(用户自己填写) ====================
EMAIL_PASSWORD = "不能看."
# ==================== 硬编码账户 ====================
AUTH_ACCOUNTS = "不能看"
# ==================== 存储 Cookie(永久有效) ====================
COOKIE_STORE = {}
# ==================== 工具函数 ====================
def generate_high_entropy_cookie():
"""生成 4096 位高熵 Cookie"""
entropy = secrets.token_bytes(512)
cookie = hashlib.sha3_512(entropy).hexdigest() + secrets.token_hex(256)
return cookie[:4096]
def generate_token():
"""生成 32 位令牌(随机数+字母+特殊字符)"""
chars = string.ascii_letters + string.digits + "!@#$%^&*"
token = ''.join(secrets.choice(chars) for _ in range(32))
return token
def validate_cookie(cookie):
"""验证 Cookie 是否有效(永久有效)"""
if cookie not in COOKIE_STORE:
return None
return COOKIE_STORE[cookie]
def get_upload_dir(token):
"""获取令牌对应的上传目录"""
return os.path.join(os.path.dirname(__file__), 'uploads', token)
def delete_upload_dir(token):
"""删除令牌对应的上传目录"""
upload_dir = get_upload_dir(token)
if os.path.exists(upload_dir):
shutil.rmtree(upload_dir)
return True
return False
# ==================== API 接口 ====================
@app.route('/login', methods=['POST'])
def login():
"""
登录接口
请求体: {
"aid": "w9M3nR8...",
"username": "dvsadmin",
"password": "G7#kLp$..."
}
返回: {"code": 200, "cookie": "xxx"}
"""
try:
data = request.get_json()
if not data:
return jsonify({"code": 400, "message": "请求体不能为空"}), 400
aid = data.get('aid')
username = data.get('username')
password = data.get('password')
if not aid:
return jsonify({"code": 400, "message": "缺少 aid"}), 400
if not username:
return jsonify({"code": 400, "message": "缺少 username"}), 400
if not password:
return jsonify({"code": 400, "message": "缺少 password"}), 400
if aid not in AUTH_ACCOUNTS:
return jsonify({"code": 401, "message": "aid 不存在"}), 401
account = AUTH_ACCOUNTS[aid]
if account['username'] != username:
return jsonify({"code": 401, "message": "用户名错误"}), 401
if account['password'] != password:
return jsonify({"code": 401, "message": "密码错误"}), 401
if not EMAIL_PASSWORD:
return jsonify({"code": 500, "message": "系统未配置邮件密码,请联系管理员"}), 500
cookie = generate_high_entropy_cookie()
COOKIE_STORE[cookie] = {
'aid': aid,
'username': username
}
return jsonify({
"code": 200,
"message": "登录成功",
"cookie": cookie
})
except Exception as e:
return jsonify({"code": 500, "message": f"服务器错误: {str(e)}"}), 500
@app.route('/upload', methods=['POST'])
def upload_file():
"""
上传附件接口
请求: multipart/form-data
- file: 要上传的文件
返回: {"code": 200, "token": "xxx", "filename": "xxx"}
"""
try:
if 'file' not in request.files:
return jsonify({"code": 400, "message": "缺少 file 字段"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"code": 400, "message": "未选择文件"}), 400
token = generate_token()
upload_dir = get_upload_dir(token)
os.makedirs(upload_dir, exist_ok=True)
filename = file.filename
file_path = os.path.join(upload_dir, filename)
file.save(file_path)
return jsonify({
"code": 200,
"message": "上传成功",
"token": token,
"filename": filename
})
except Exception as e:
return jsonify({"code": 500, "message": f"上传失败: {str(e)}"}), 500
@app.route('/send', methods=['POST'])
def send_email():
"""
发件接口
请求体: {
"cookie": "xxx",
"to": "dvs6666@163.com",
"subject": "邮件主题",
"content": "邮件内容(支持HTML)",
"token": "xxx" # 可选
}
"""
try:
data = request.get_json()
if not data:
return jsonify({"code": 400, "message": "请求体不能为空"}), 400
cookie = data.get('cookie')
to_email = data.get('to')
subject = data.get('subject')
content = data.get('content')
token = data.get('token')
if not cookie:
return jsonify({"code": 400, "message": "缺少 cookie"}), 400
if not to_email:
return jsonify({"code": 400, "message": "缺少收件人"}), 400
if not subject:
return jsonify({"code": 400, "message": "缺少主题"}), 400
if not content:
return jsonify({"code": 400, "message": "缺少内容"}), 400
# 验证 Cookie(永久有效)
session_data = validate_cookie(cookie)
if not session_data:
return jsonify({"code": 401, "message": "Cookie 无效,请重新登录"}), 401
if not EMAIL_PASSWORD:
return jsonify({"code": 500, "message": "系统未配置邮件密码,请联系管理员"}), 500
# 获取附件列表(如果有令牌)
attachments = []
if token:
upload_dir = get_upload_dir(token)
if os.path.exists(upload_dir):
for filename in os.listdir(upload_dir):
file_path = os.path.join(upload_dir, filename)
if os.path.isfile(file_path):
attachments.append(file_path)
# 发送邮件
success, result = send_mail(to_email, subject, content, attachments, EMAIL_PASSWORD)
if success:
# 发送成功后删除令牌目录(一次性使用)
if token:
delete_upload_dir(token)
trigger_callback(to_email, subject, "成功")
return jsonify({"code": 200, "message": result})
else:
trigger_callback(to_email, subject, f"失败: {result}")
return jsonify({"code": 500, "message": result}), 500
except Exception as e:
return jsonify({"code": 500, "message": f"服务器错误: {str(e)}"}), 500
def send_mail(to_email, subject, content, attachments, email_password):
"""发送邮件核心函数"""
try:
msg = MIMEMultipart()
msg['From'] = SENDER_EMAIL
msg['To'] = to_email
msg['Subject'] = subject
if content.strip().startswith('<') or '</html>' in content.lower():
msg.attach(MIMEText(content, 'html', 'utf-8'))
else:
msg.attach(MIMEText(content, 'plain', 'utf-8'))
if attachments:
for file_path in attachments:
if not os.path.exists(file_path):
continue
with open(file_path, 'rb') as f:
part = MIMEBase('application', 'octet-stream')
part.set_payload(f.read())
encoders.encode_base64(part)
filename = os.path.basename(file_path)
part.add_header('Content-Disposition', f'attachment; filename="{filename}"')
msg.attach(part)
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server:
server.login(SENDER_EMAIL, email_password)
server.send_message(msg)
return True, "邮件发送成功"
except Exception as e:
return False, f"发送失败: {str(e)}"
def trigger_callback(to_email, subject, status):
"""回调函数"""
print("=" * 60)
print(f"[回调] 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"[回调] 收件人: {to_email}")
print(f"[回调] 主题: {subject}")
print(f"[回调] 状态: {status}")
print("=" * 60)
# ==================== 启动服务 ====================
if __name__ == '__main__':
os.makedirs(os.path.join(os.path.dirname(__file__), 'uploads'), exist_ok=True)
print("=" * 60)
print("Zoho 邮件发送 API 服务")
print("=" * 60)
print(f"SMTP 服务器: {SMTP_SERVER}:{SMTP_PORT}")
print(f"发件邮箱: {SENDER_EMAIL}")
print(f"邮件密码已配置: {'✅ 是' if EMAIL_PASSWORD else '❌ 否(请先配置)'}")
print(f"有效账户数: {len(AUTH_ACCOUNTS)}")
print(f"Cookie 有效期: 永久")
print("=" * 60)
if not EMAIL_PASSWORD:
print("\n⚠️ 警告: 请先在代码中设置 EMAIL_PASSWORD 变量!")
print(" 位置: 第 18 行 EMAIL_PASSWORD = '你的邮箱密码'")
print("=" * 60)
print("\n🚀 服务启动中...")
print(f"📍 登录接口: POST http://localhost:5000/login")
print(f"📍 上传接口: POST http://localhost:5000/upload")
print(f"📍 发件接口: POST http://localhost:5000/send")
print("=" * 60)
app.run(host='0.0.0.0', port=5000, debug=True)