|
|
#!/usr/bin/env python3
|
|
|
"""
|
|
|
邮件批量通知工具 - 简易版
|
|
|
直接运行即可发送,无需额外参数
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
import re
|
|
|
import sys
|
|
|
import time
|
|
|
import smtplib
|
|
|
import logging
|
|
|
from datetime import datetime
|
|
|
from email.mime.text import MIMEText
|
|
|
from email.header import Header
|
|
|
|
|
|
# ===================== 配置区(按需修改) =====================
|
|
|
SMTP_SERVER = "smtp.ctvit.com.cn"
|
|
|
SMTP_PORT = 25
|
|
|
SENDER_EMAIL = "zhangguozhong@ctvit.com.cn"
|
|
|
SENDER_PASS = "&TBJ7p^u$HF9hp"
|
|
|
SUBJECT = "账号及口令通知"
|
|
|
REPLY_TO = "" # 回复地址(可选,留空则不填)
|
|
|
DELAY = 1 # 每封间隔(秒)
|
|
|
MAX_RETRIES = 3 # 失败重试次数
|
|
|
USER_FILE = "users.txt" # 用户列表文件
|
|
|
TEMPLATE_FILE = "" # 自定义模板文件(留空用默认模板)
|
|
|
# =============================================================
|
|
|
|
|
|
DEFAULT_TEMPLATE = """亲爱的用户:
|
|
|
|
|
|
您好!这是您的账号配置信息:
|
|
|
|
|
|
您的邮件账号:{email}
|
|
|
您的初始口令:{password}
|
|
|
|
|
|
请务必妥善保管您的账号信息,不要泄露给他人。
|
|
|
如有疑问,请联系技术支持部门。
|
|
|
|
|
|
---
|
|
|
此邮件为系统自动发送,请勿直接回复。"""
|
|
|
|
|
|
|
|
|
# 日志
|
|
|
logging.basicConfig(
|
|
|
level=logging.INFO,
|
|
|
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
|
datefmt="%Y-%m-%d %H:%M:%S",
|
|
|
)
|
|
|
logger = logging.getLogger("MailSender")
|
|
|
|
|
|
|
|
|
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 or line.startswith("#"):
|
|
|
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:
|
|
|
logger.warning(f"跳过无效行: {line}")
|
|
|
continue
|
|
|
users.append({"email": email, "password": password})
|
|
|
return users
|
|
|
|
|
|
|
|
|
def build_smtp():
|
|
|
if SMTP_PORT == 465:
|
|
|
server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=30)
|
|
|
logger.info(f"📡 连接 {SMTP_SERVER}:{SMTP_PORT} (SSL)")
|
|
|
elif SMTP_PORT == 587:
|
|
|
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=30)
|
|
|
server.ehlo()
|
|
|
server.starttls()
|
|
|
server.ehlo()
|
|
|
logger.info(f"📡 连接 {SMTP_SERVER}:{SMTP_PORT} (STARTTLS)")
|
|
|
else:
|
|
|
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=30)
|
|
|
logger.info(f"📡 连接 {SMTP_SERVER}:{SMTP_PORT} (明文)")
|
|
|
|
|
|
server.login(SENDER_EMAIL, SENDER_PASS)
|
|
|
return server
|
|
|
|
|
|
|
|
|
def send_one(server, receiver_email, content):
|
|
|
msg = MIMEText(content, "plain", "utf-8")
|
|
|
msg["From"] = SENDER_EMAIL
|
|
|
msg["To"] = receiver_email
|
|
|
msg["Subject"] = Header(SUBJECT, "utf-8")
|
|
|
if REPLY_TO:
|
|
|
msg["Reply-To"] = REPLY_TO
|
|
|
server.sendmail(SENDER_EMAIL, [receiver_email], msg.as_string())
|
|
|
|
|
|
|
|
|
def send_with_retry(server, receiver_email, content):
|
|
|
for attempt in range(1, MAX_RETRIES + 1):
|
|
|
try:
|
|
|
send_one(server, receiver_email, content)
|
|
|
return True, None
|
|
|
except smtplib.SMTPServerDisconnected:
|
|
|
if attempt < MAX_RETRIES:
|
|
|
wait = 2 ** attempt
|
|
|
logger.warning(f" ⚠ 连接断开,{wait}s 后重试 (第{attempt}次)")
|
|
|
time.sleep(wait)
|
|
|
except smtplib.SMTPResponseException as e:
|
|
|
code = e.smtp_code
|
|
|
if 400 <= code < 500 and attempt < MAX_RETRIES:
|
|
|
wait = 2 ** attempt
|
|
|
logger.warning(f" ⚠ SMTP {code},{wait}s 后重试 (第{attempt}次)")
|
|
|
time.sleep(wait)
|
|
|
else:
|
|
|
return False, f"SMTP {code}: {e}"
|
|
|
except Exception as e:
|
|
|
return False, str(e)
|
|
|
return False, "重试耗尽"
|
|
|
|
|
|
|
|
|
def main():
|
|
|
logger.info("=" * 50)
|
|
|
logger.info("📧 邮件批量通知工具 启动")
|
|
|
logger.info(f" 发件人:{SENDER_EMAIL}")
|
|
|
logger.info(f" SMTP:{SMTP_SERVER}:{SMTP_PORT}")
|
|
|
logger.info(f" 间隔:{DELAY}s 重试:{MAX_RETRIES}次")
|
|
|
logger.info("=" * 50)
|
|
|
|
|
|
# 1. 检查文件
|
|
|
if not os.path.exists(USER_FILE):
|
|
|
logger.error(f"❌ 文件不存在: {USER_FILE}")
|
|
|
sys.exit(1)
|
|
|
|
|
|
# 2. 读取模板
|
|
|
if TEMPLATE_FILE:
|
|
|
with open(TEMPLATE_FILE, "r", encoding="utf-8") as f:
|
|
|
template = f.read()
|
|
|
logger.info(f"📝 自定义模板: {TEMPLATE_FILE}")
|
|
|
else:
|
|
|
template = DEFAULT_TEMPLATE
|
|
|
logger.info("📝 默认模板")
|
|
|
|
|
|
# 3. 解析用户
|
|
|
users = parse_users(USER_FILE)
|
|
|
if not users:
|
|
|
logger.error("❌ 未识别到有效用户")
|
|
|
sys.exit(1)
|
|
|
logger.info(f"📋 共 {len(users)} 个收件人")
|
|
|
|
|
|
# 4. 测试发送(先发给自己验证)
|
|
|
logger.info("🧪 正在测试发送(发给自己验证)...")
|
|
|
server = None
|
|
|
try:
|
|
|
server = build_smtp()
|
|
|
test_content = template.format(email=SENDER_EMAIL, password="123456")
|
|
|
send_one(server, SENDER_EMAIL, test_content)
|
|
|
logger.info(f"✅ 测试成功!已发送至 {SENDER_EMAIL}")
|
|
|
except smtplib.SMTPAuthenticationError:
|
|
|
logger.error("❌ SMTP 认证失败,请检查密码/授权码")
|
|
|
sys.exit(1)
|
|
|
except Exception as e:
|
|
|
logger.error(f"❌ 测试发送失败: {e}")
|
|
|
logger.error(" 请检查 SMTP 服务器和端口是否正确")
|
|
|
sys.exit(1)
|
|
|
finally:
|
|
|
if server:
|
|
|
try:
|
|
|
server.quit()
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
# 5. 正式发送
|
|
|
total = len(users)
|
|
|
success = 0
|
|
|
failed = 0
|
|
|
failed_list = []
|
|
|
|
|
|
print()
|
|
|
logger.info(f"🚀 开始批量发送(共 {total} 封)")
|
|
|
print("─" * 50)
|
|
|
|
|
|
for idx, user in enumerate(users, 1):
|
|
|
email = user["email"]
|
|
|
content = template.format(email=email, password=user["password"])
|
|
|
|
|
|
server = None
|
|
|
try:
|
|
|
server = build_smtp()
|
|
|
ok, err = send_with_retry(server, email, content)
|
|
|
except smtplib.SMTPAuthenticationError:
|
|
|
logger.error("❌ SMTP 认证失败")
|
|
|
sys.exit(1)
|
|
|
except Exception as e:
|
|
|
ok, err = False, str(e)
|
|
|
finally:
|
|
|
if server:
|
|
|
try:
|
|
|
server.quit()
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
if ok:
|
|
|
success += 1
|
|
|
logger.info(f" [{idx}/{total}] ✅ {email}")
|
|
|
else:
|
|
|
failed += 1
|
|
|
failed_list.append({"email": email, "error": err})
|
|
|
logger.error(f" [{idx}/{total}] ❌ {email} → {err}")
|
|
|
|
|
|
if idx < total:
|
|
|
time.sleep(DELAY)
|
|
|
|
|
|
# 6. 汇总
|
|
|
print("═" * 50)
|
|
|
logger.info(f"📊 发送完成")
|
|
|
logger.info(f" 总 数:{total}")
|
|
|
logger.info(f" 成 功:{success}")
|
|
|
logger.info(f" 失 败:{failed}")
|
|
|
|
|
|
if failed_list:
|
|
|
print()
|
|
|
logger.warning("📋 失败人员:")
|
|
|
for f in failed_list:
|
|
|
logger.warning(f" ❌ {f['email']} → {f['error']}")
|
|
|
log_file = f"failed_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
|
|
|
with open(log_file, "w", encoding="utf-8") as f:
|
|
|
for fu in failed_list:
|
|
|
f.write(f"{fu['email']}\t{fu['error']}\n")
|
|
|
logger.info(f"📁 失败详情已保存: {log_file}")
|
|
|
|
|
|
if failed == 0:
|
|
|
logger.info("🎉 全部发送成功!")
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
main()
|