You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

378 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import os
import re
import json
import time
import uuid
import smtplib
import threading
import traceback
from email.mime.text import MIMEText
from email.header import Header
from flask import render_template, request, jsonify
from common.logger import system_logger
# =====================================
# Path
# =====================================
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
UPLOAD_FOLDER = os.path.join(BASE_DIR, "uploads")
PROGRESS_FOLDER = os.path.join(BASE_DIR, "logs", "progress")
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(PROGRESS_FOLDER, exist_ok=True)
# =====================================
# 进度文件存储(跨进程共享)
# =====================================
def progress_path(task_id):
return os.path.join(PROGRESS_FOLDER, f"{task_id}.json")
def write_progress(task_id, **kwargs):
path = progress_path(task_id)
data = {}
if os.path.exists(path):
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
# results 列表累积
old_results = data.get("results", [])
except Exception:
old_results = []
else:
old_results = []
data.update(kwargs)
# 处理 results 追加
results_append = data.pop("results_append", None)
if results_append:
old_results.append(results_append)
data["results"] = old_results
elif "results" not in data:
data["results"] = old_results
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
def read_progress(task_id):
path = progress_path(task_id)
if not os.path.exists(path):
return None
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def clean_progress(task_id):
path = progress_path(task_id)
if os.path.exists(path):
os.remove(path)
# =====================================
# TXT 解析
# =====================================
def parse_users(file_path):
users = []
with open(file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
parts = re.split(r"[\s\t\|:;]+", line)
if len(parts) < 2:
continue
email = parts[0].strip()
password = parts[1].strip()
if not email or "@" not in email:
continue
users.append({"email": email, "password": password})
return users
# =====================================
# 清理 textarea 缩进
# =====================================
def normalize_template(text):
if not text:
return ""
lines = text.splitlines()
while lines and not lines[0].strip():
lines.pop(0)
while lines and not lines[-1].strip():
lines.pop()
return "\n".join(lines)
# =====================================
# 模板变量替换
# =====================================
def render_template_text(template, email, password):
template = normalize_template(template)
return (
str(template)
.replace("{{email}}", str(email or ""))
.replace("{{password}}", str(password or ""))
)
# =====================================
# SMTP 连接
# =====================================
def build_smtp(smtp_server, smtp_port, sender_email, sender_pass):
smtp_port = int(smtp_port)
try:
if smtp_port == 465:
server = smtplib.SMTP_SSL(smtp_server, smtp_port, timeout=30)
else:
server = smtplib.SMTP(smtp_server, smtp_port, timeout=30)
server.ehlo()
if smtp_port == 587:
server.starttls()
server.ehlo()
server.login(sender_email, sender_pass)
return server
except Exception as e:
raise Exception(f"SMTP连接失败{smtp_server}:{smtp_port} {str(e)}")
# =====================================
# 发单封邮件
# =====================================
def send_single_mail(server, sender_email, receiver_email, subject, content, reply_to=None):
msg = MIMEText(content, "plain", "utf-8")
msg["From"] = sender_email
msg["To"] = receiver_email
msg["Subject"] = Header(subject or "邮件通知", "utf-8")
if reply_to:
msg["Reply-To"] = reply_to
server.sendmail(sender_email, [receiver_email], msg.as_string())
def send_with_retry(server, sender_email, receiver_email, subject, content, reply_to=None, max_retries=3):
last_error = None
for attempt in range(1, max_retries + 1):
try:
send_single_mail(server, sender_email, receiver_email, subject, content, reply_to)
return True, None
except smtplib.SMTPServerDisconnected:
last_error = "SMTPServerDisconnected"
if attempt < max_retries:
time.sleep(2 ** attempt)
except smtplib.SMTPResponseException as e:
code = e.smtp_code
if 400 <= code < 500 and attempt < max_retries:
last_error = f"SMTP {code}"
time.sleep(2 ** attempt)
else:
return False, str(e)
except Exception as e:
return False, str(e)
return False, last_error
# =====================================
# 异步后台任务:批量发送
# =====================================
def batch_send_worker(
task_id,
smtp_server,
smtp_port,
sender_email,
sender_pass,
subject,
template_content,
users,
reply_to,
):
server = None
total = len(users)
success_count = 0
failed_count = 0
write_progress(task_id, status="running", total=total, success=0, failed=0, current=0, results=[])
try:
server = build_smtp(smtp_server, smtp_port, sender_email, sender_pass)
for idx, user in enumerate(users):
email = user.get("email", "")
password = user.get("password", "")
ok, err = send_with_retry(
server, sender_email, email,
subject, render_template_text(template_content, email, password),
reply_to=reply_to
)
if ok:
success_count += 1
result = {"email": email, "status": "success"}
write_progress(task_id,
success=success_count,
current=idx + 1,
results_append=result
)
else:
failed_count += 1
result = {"email": email, "status": f"failed: {err}"}
write_progress(task_id,
failed=failed_count,
current=idx + 1,
results_append=result
)
# 每封间隔 1s 防垃圾标记
time.sleep(1)
except Exception as e:
write_progress(task_id, error=str(e))
finally:
if server:
try:
server.quit()
except Exception:
pass
write_progress(task_id, status="done")
# =====================================
# 进度查询接口
# =====================================
def mail_notify_progress(task_id):
data = read_progress(task_id)
if data is None:
return jsonify({"error": "task_id not found"}), 404
# 只返回最近 50 条结果
if "results" in data:
data["results"] = data["results"][-50:]
return jsonify(data)
# =====================================
# 页面入口
# =====================================
def mail_notify_page():
message = None
preview_content = ""
task_id = None
smtp_server = "smtp.ctvit.com.cn"
smtp_port = "25"
reply_to = ""
sender_email = ""
sender_pass = ""
subject = "账号及口令通知"
template_content = """尊敬的老师/同事:
您好!
根据系统账号开通安排,您的账号已创建完成。现将相关登录信息通知如下,请妥善保存并注意账号安全。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
账号(邮箱):{{email}}
初始口令:{{password}}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
请在首次登录后及时修改初始口令,并妥善保管个人账号信息。
此致
敬礼!
信息技术支持中心"""
try:
if request.method == "POST":
action = request.form.get("action", "send")
smtp_server = (request.form.get("smtp_server") or smtp_server).strip()
smtp_port = (request.form.get("smtp_port") or smtp_port).strip()
sender_email = (request.form.get("sender_email") or "").strip()
sender_pass = (request.form.get("sender_pass") or "").strip()
subject = (request.form.get("subject") or subject).strip()
reply_to = (request.form.get("reply_to") or "").strip()
template_content = request.form.get("template_content") or template_content
upload_file = request.files.get("user_file") or request.files.get("file")
if not sender_email:
raise Exception("请输入发件邮箱")
if not sender_pass:
raise Exception("请输入SMTP密码")
if action == "test":
preview_content = render_template_text(template_content, sender_email, "123456")
server = None
try:
server = build_smtp(smtp_server, smtp_port, sender_email, sender_pass)
send_single_mail(server, sender_email, sender_email, f"[测试] {subject}", preview_content, reply_to=reply_to)
finally:
if server:
server.quit()
message = "✅ 测试邮件发送成功(已发送至自己邮箱)"
system_logger.info(f"[MailNotify-Test] sender={sender_email}")
elif action == "send":
if not upload_file or not upload_file.filename:
raise Exception("请上传 TXT 文件")
ext = os.path.splitext(upload_file.filename)[1].lower()
if ext != ".txt":
raise Exception("仅支持 txt 文件")
temp_name = f"{uuid.uuid4().hex}.txt"
save_path = os.path.join(UPLOAD_FOLDER, temp_name)
upload_file.save(save_path)
users = parse_users(save_path)
if not users:
raise Exception("未识别到有效用户数据")
task_id = uuid.uuid4().hex
write_progress(task_id,
status="queued", total=len(users),
success=0, failed=0, current=0,
results=[], error=None
)
thread = threading.Thread(
target=batch_send_worker,
args=(
task_id,
smtp_server, smtp_port,
sender_email, sender_pass,
subject, template_content,
users, reply_to,
),
daemon=True,
)
thread.start()
message = f"⏳ 批量发送已启动任务ID{task_id}(共 {len(users)}每封间隔1s"
system_logger.info(f"[MailNotify] task={task_id} total={len(users)} sender={sender_email}")
preview_content = render_template_text(template_content, "demo@test.com", "123456")
except Exception as e:
message = f"❌ 错误:{str(e)}"
system_logger.exception("[MailNotify ERROR]")
# 不回传密码
return render_template(
"mail_notify.html",
message=message,
smtp_server=smtp_server,
smtp_port=smtp_port,
sender_email=sender_email,
sender_pass="",
reply_to=reply_to,
subject=subject,
template_content=template_content,
preview_content=preview_content,
task_id=task_id,
)