commit 1a47a4274823cb5aec4619f623b52067f49e667c Author: dvs-dvsxt Date: Fri Aug 28 10:15:24 2026 +0800 Initial commit: Zoho Mail Service v1.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6242ce0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +.venv/ +venv/ +env/ +.env +*.key + +# System files +Thumbs.db +.DS_Store +desktop.ini diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fddd2dd --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DVS (dvs-dvsxt) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..140b4ed --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# ✉️ Zoho Mail Service — SMTP Email Sending API + +> A Flask-based email sending service using **Zoho SMTP**, supporting attachments, authentication, and file upload. + +**Zoho Mail Service** wraps Zoho SMTP into a simple HTTP API. It provides login authentication, file upload, and email sending (with optional attachments) via a Flask server. + +--- + +## ✨ Features + +| Feature | Description | +|---------|-------------| +| 🔐 **Login Auth** | Authenticate before sending emails | +| 📎 **File Upload** | Upload file attachments to the server | +| 📧 **Send Email** | Send email via Zoho SMTP with text + attachments | +| 🔒 **Account Auth** | Hardcoded account validation (edit in config) | +| 🔑 **SMTP TLS** | Zoho SMTP over SSL (port 465) | + +--- + +## 🔌 API Endpoints + +### POST `/login` +Authenticate a user. +- **Params**: credentials (username/password) +- **Returns**: authentication result + +### POST `/upload` +Upload a file attachment. +- **Params**: file data +- **Returns**: saved file info + +### POST `/send` +Send an email. +- **Params**: recipient, subject, body, optional attachment +- **Returns**: send result + +--- + +## 🚀 Quick Start + +### Prerequisites +- Python 3.7+ +- `flask` + +### Configure + +Edit the config section in `zoho_mail_service.py`: +```python +SMTP_SERVER = "smtp.zoho.com.cn" +SMTP_PORT = 465 +SENDER_EMAIL = "yourname@yourname.com" +EMAIL_PASSWORD = "your-app-password" +``` + +### Run + +```bash +python zoho_mail_service.py +``` + +--- + +## 📄 License + +Licensed under the **MIT License**. See [LICENSE](LICENSE). + +--- + +## ⚠️ Security Note + +> Email credentials are **sensitive** — never commit real passwords. This project is for learning/reference; configure credentials securely in production (e.g., environment variables). diff --git a/zoho_mail_service.py b/zoho_mail_service.py new file mode 100644 index 0000000..08b3311 --- /dev/null +++ b/zoho_mail_service.py @@ -0,0 +1,298 @@ +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 '' 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)