Initial commit: Zoho Mail Service v1.0.0

This commit is contained in:
dvs
2026-08-28 10:15:24 +08:00
commit 1a47a42748
4 changed files with 407 additions and 0 deletions
+16
View File
@@ -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
+21
View File
@@ -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.
+72
View File
@@ -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).
+298
View File
@@ -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 '</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)