├── requirements.txt ├── .github ├── FUNDING.yml └── workflows │ ├── email-sender.yml~ │ └── RepoSync.yml~ ├── README.md ├── send_email.py └── resend.js /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | # .github/FUNDING.yml 3 | 4 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 5 | patreon: # Replace with a single Patreon username 6 | open_collective: # Replace with a single Open Collective username 7 | ko_fi: # Replace with a single Ko-fi username 8 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 9 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 10 | liberapay: # Replace with a single Liberapay username 11 | issuehunt: # Replace with a single IssueHunt username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | polar: # Replace with a single Polar username 14 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 15 | thanks_dev: # Replace with a single thanks.dev username 16 | custom: https://blog.811520.xyz/thanks/ 17 | -------------------------------------------------------------------------------- /.github/workflows/email-sender.yml~: -------------------------------------------------------------------------------- 1 | name: 自动群发邮件 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 1" # 每周一 0 点执行一次 6 | workflow_dispatch: # 支持手动触发 7 | 8 | jobs: 9 | send_email: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | # Step 1: 检出代码仓库 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | # Step 2: 设置 Python 环境 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.9' # 可根据需要调整为具体的版本 22 | 23 | # Step 3: 安装依赖 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt # 使用 requirements.txt 管理依赖 28 | 29 | # Step 4: 运行邮件发送脚本 30 | - name: Run email sender script 31 | env: 32 | EMAIL_CONFIG: ${{ secrets.EMAIL_CONFIG }} 33 | TG_ID: ${{ secrets.TG_ID }} 34 | TG_TOKEN: ${{ secrets.TG_TOKEN }} 35 | run: | 36 | python "send_email.py" 37 | -------------------------------------------------------------------------------- /.github/workflows/RepoSync.yml~: -------------------------------------------------------------------------------- 1 | name: RepoSync 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 1" # 每周一 0 点运行 6 | workflow_dispatch: # 手动触发工作流 7 | 8 | jobs: 9 | sync_latest_from_upstream: 10 | name: 自动同步上游仓库 11 | runs-on: ubuntu-latest 12 | if: ${{ github.event.repository.fork }} # 仅在 fork 仓库中运行 13 | env: 14 | UPSTREAM_REPO: yutian81/auto-email # 在这里定义上游仓库,格式为“用户名/仓库名” 15 | UPSTREAM_REPO_BRANCH: main # 定义上游仓库的分支 16 | TARGET_REPO_BRANCH: main # 定义目标仓库的分支 17 | 18 | steps: 19 | # 检出当前仓库代码 20 | - uses: actions/checkout@v4 21 | 22 | # 同步上游仓库的更改 23 | - name: Sync upstream changes 24 | id: sync 25 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 26 | with: 27 | upstream_sync_repo: ${{ env.UPSTREAM_REPO }} # 使用定义的上游仓库 28 | upstream_sync_branch: ${{ env.UPSTREAM_REPO_BRANCH }} # 使用定义的上游分支 29 | target_sync_branch: ${{ env.TARGET_REPO_BRANCH }} # 使用定义的目标分支 30 | target_repo_token: ${{ secrets.GITHUB_TOKEN }} # 自动生成,无需设置 31 | test_mode: false # 关闭测试模式,实际执行同步操作 32 | continue-on-error: true # 即使同步失败,继续执行后续步骤 33 | 34 | # 输出同步结果 35 | - name: 输出同步结果 36 | run: | 37 | if [ "${{ steps.sync.outputs.changes_pushed }}" = "true" ]; then 38 | echo "✅ 同步成功:上游仓库的最新更改已同步到目标仓库。" 39 | elif [ "${{ steps.sync.outputs.changes_pushed }}" = "false" ]; then 40 | echo "⚠️ 无需同步:没有检测到需要同步的更改。" 41 | else 42 | echo "❌ 同步失败:${{ steps.sync.outputs.error_message:-同步过程中出现了问题。}}" 43 | fi 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 注意 2 | worker方式部署的,如果使用了最新脚本无法访问前端网页,需要先进到绑定的kv空间,清空kv里保存的内容! 3 | 4 | ## 部署方式一:github action(推荐) 5 | 6 | 在你的 GitHub 仓库中,依次点击 Settings -> Secrets -> Actions,然后点击 New repository secret,创建一个名为`EMAIL_CONFIG`的机密变量,内容为你的邮件配置信息。 7 | 8 | `EMAIL_CONFIG`机密变量的`JSON`格式如下: 9 | ``` 10 | { 11 | "smtp_server": "smtp.example.com", 12 | "smtp_port": 587, 13 | "smtp_user": "your_email@example.com", 14 | "smtp_pass": "your_password", 15 | "from_email": "your_email@example.com", 16 | "to_emails": [ 17 | "recipient1@example.com", 18 | "recipient2@example.com", 19 | "recipient3@example.com", 20 | "recipient4@example.com", 21 | "recipient5@example.com" 22 | ], 23 | "subject": "定时邮件通知", 24 | "body": "这是一封来自自动化脚本的邮件。" 25 | } 26 | ``` 27 | **若需要tg通知,则新增以下两个变量** 28 | 29 | - TG_ID = tg机器人用户ID 30 | 31 | - TG_TOKEN = tg机器人token 32 | 33 | **手动运行一次action,之后便可自动运行,默认为每周一次。可自行在action的yml配置文件中修改自动执行频率** 34 | 35 | ## 部署方式二:cf worker 36 | 37 | 到 [resend](https://resend.com/) 注册一个账号,申请 `apitoken`,并且绑定一个域名,根据 resend 的提示到域名托管商处添加相应的 dns 解析记录,有三个 `txt` 和一个 `mx` 记录。 38 | 39 | 在 cf 新建一个 wokrer,粘贴仓库内 `resend.js` 中的内容 40 | 41 | 已完成前端界面 42 | 43 | ![](https://pan.811520.xyz/2025-01/1736779999-%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_20250113224844.webp) 44 | 45 | ### 设置以下环境变量: 46 | 47 | - RESEND_API_KEY = 填刚刚申请的 `apitoken` 48 | - KEY = 设置一个密码,访问`https://你项目的worker域名?key=你设置的密码`,即为前段面板 49 | - FROM_EMAIL = 发件人邮箱,邮箱域名必须与在 resend 中绑定的域名一致,前缀随意 50 | - TO_EMAILS = 收件人邮箱,支持多个邮箱地址,每行一个 51 | - TG_ID = TG 机器人的 chat id 52 | - TG_TOKEN = TG 机器人的 token 53 | - SUBJECT = 邮件主题 54 | - BODY = 邮件正文 55 | 56 | ### 绑定KV 57 | - KV绑定变量名/KV空间名:AUTO_EMAIL 58 | 59 | ### 设置 `corn` 触发器 60 | 实现定时自动群发邮件,建议每周执行一次 61 | -------------------------------------------------------------------------------- /send_email.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | import json 4 | import smtplib 5 | import traceback 6 | from email.mime.multipart import MIMEMultipart 7 | from email.mime.text import MIMEText 8 | from datetime import datetime 9 | 10 | def load_email_config(): 11 | """加载并验证邮件配置""" 12 | config_json = os.getenv('EMAIL_CONFIG') 13 | if not config_json: 14 | raise ValueError("环境变量 'EMAIL_CONFIG' 未设置或为空,请检查环境变量配置。") 15 | 16 | try: 17 | config = json.loads(config_json) 18 | except json.JSONDecodeError as e: 19 | raise ValueError(f"解析 'EMAIL_CONFIG' 时出错: {str(e)}") 20 | 21 | required_fields = ['smtp_server', 'smtp_port', 'smtp_user', 'smtp_pass', 'from_email', 'to_emails', 'subject', 'body'] 22 | for field in required_fields: 23 | if field not in config: 24 | raise ValueError(f"配置中缺少必需字段: {field}") 25 | 26 | # 检查收件人列表 27 | to_emails = config['to_emails'] 28 | if isinstance(to_emails, str): 29 | config['to_emails'] = [email.strip() for email in to_emails.split(",")] 30 | elif not isinstance(to_emails, list): 31 | raise ValueError("配置中的 'to_emails' 应为一个邮件地址列表或逗号分隔的字符串") 32 | if not config['to_emails']: 33 | raise ValueError("收件人列表为空,请检查 'to_emails' 配置。") 34 | 35 | return config 36 | 37 | def load_telegram_config(): 38 | """加载并验证 Telegram 配置""" 39 | tg_id = os.getenv('TG_ID') 40 | tg_token = os.getenv('TG_TOKEN') 41 | 42 | if tg_id and not tg_id.isdigit(): 43 | raise ValueError("变量配置中的 'TG_ID' 应为数字,请检查配置。") 44 | if tg_token and ":" not in tg_token: 45 | raise ValueError("变量配置中的 'TG_TOKEN' 应该包含':',请检查配置。") 46 | 47 | return tg_id, tg_token 48 | 49 | def send_email(smtp_server, smtp_port, smtp_user, smtp_pass, from_email, to_email, subject, body): 50 | """发送邮件""" 51 | if smtp_port not in [465, 587]: 52 | raise ValueError(f"不支持的 SMTP 端口号: {smtp_port},仅支持 465 或 587,请检查配置。") 53 | 54 | msg = MIMEMultipart() 55 | msg['From'] = from_email 56 | msg['To'] = to_email 57 | msg['Subject'] = subject 58 | msg.attach(MIMEText(body, 'html')) 59 | 60 | try: 61 | # 根据端口号选择加密方式 62 | if smtp_port == 465: 63 | # 使用 SMTP_SSL 直接启用 SSL 64 | with smtplib.SMTP_SSL(smtp_server, smtp_port) as server: 65 | server.login(smtp_user, smtp_pass) 66 | server.sendmail(from_email, to_email, msg.as_string()) 67 | elif smtp_port == 587: 68 | # 使用 SMTP 和 starttls() 启用 TLS 69 | with smtplib.SMTP(smtp_server, smtp_port) as server: 70 | server.starttls() 71 | server.login(smtp_user, smtp_pass) 72 | server.sendmail(from_email, to_email, msg.as_string()) 73 | 74 | print(f"邮件已成功发送到 {to_email}") 75 | return True 76 | 77 | except Exception as e: 78 | # 获取异常信息并替换敏感信息 79 | error_message = str(e) 80 | if smtp_user: 81 | error_message = error_message.replace(smtp_user, "[SMTP 账号错误]") 82 | if smtp_pass: 83 | error_message = error_message.replace(smtp_pass, "[SMTP 密码错误]") 84 | 85 | print(f"发送邮件到 {to_email} 失败: {error_message}") 86 | traceback.print_exc() 87 | return False 88 | 89 | def send_telegram_notification(tg_id, tg_token, success_emails, failed_emails_with_reasons): 90 | """发送 Telegram 消息(Markdown 格式)""" 91 | now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 92 | 93 | # 统计成功和失败的数量 94 | success_count = len(success_emails) 95 | failure_count = len(failed_emails_with_reasons) 96 | total_count = success_count + failure_count 97 | 98 | # 构建消息头部 99 | message = ( 100 | "🤖 **邮件群发状态报告**\n" 101 | f"⏰ 时间: `{now}`\n" 102 | f"📊 总计: `{total_count}` 个邮箱\n" 103 | f"✅ 成功: `{success_count}`个 | ❌ 失败: `{failure_count}`个\n\n" 104 | ) 105 | 106 | # 添加成功的邮箱列表 107 | for email in success_emails: 108 | message += f"邮箱:`{email}`\n状态: ✅ 发送成功\n" 109 | 110 | # 添加失败的邮箱列表及原因 111 | for email, reason in failed_emails_with_reasons.items(): 112 | message += f"邮箱:`{email}`\n状态: ❌ 发送失败\n失败原因: {reason}\n" 113 | 114 | # 发送消息 115 | url = f"https://api.telegram.org/bot{tg_token}/sendMessage" 116 | payload = { 117 | "chat_id": tg_id, 118 | "text": message, 119 | "parse_mode": "Markdown", # 使用 Markdown 格式 120 | } 121 | try: 122 | response = requests.post(url, json=payload) 123 | if response.status_code == 200: 124 | print("Telegram 通知发送成功") 125 | else: 126 | print(f"Telegram 通知发送失败: {response.status_code}, {response.text}") 127 | except Exception as e: 128 | print(f"发送 Telegram 通知时出现异常: {str(e)}") 129 | traceback.print_exc() # 打印完整的异常堆栈信息 130 | 131 | if __name__ == "__main__": 132 | try: 133 | # 加载邮件配置 134 | config = load_email_config() 135 | smtp_server = config['smtp_server'] 136 | smtp_port = int(config['smtp_port']) 137 | smtp_user = config['smtp_user'] 138 | smtp_pass = config['smtp_pass'] 139 | from_email = config['from_email'] 140 | to_emails = config['to_emails'] 141 | subject = config['subject'] 142 | body = config['body'] 143 | 144 | # 加载 Telegram 配置 145 | tg_id, tg_token = load_telegram_config() 146 | send_telegram = bool(tg_id and tg_token) 147 | 148 | success_emails = [] 149 | failed_emails_with_reasons = {} 150 | 151 | # 群发邮件 152 | for email in to_emails: 153 | try: 154 | result = send_email(smtp_server, smtp_port, smtp_user, smtp_pass, from_email, email, subject, body) 155 | if result: 156 | success_emails.append(email) 157 | else: 158 | failed_emails_with_reasons[email] = "未知错误" # 如果没有具体原因,可以设置默认值 159 | except Exception as e: 160 | failed_emails_with_reasons[email] = str(e) # 捕获具体的异常信息作为失败原因 161 | 162 | # 仅在 TG_ID 和 TG_TOKEN 存在时发送 Telegram 通知 163 | if send_telegram: 164 | send_telegram_notification(tg_id, tg_token, success_emails, failed_emails_with_reasons) 165 | else: 166 | print("Telegram 通知配置缺失,跳过发送 Telegram 通知。") 167 | 168 | except Exception as e: 169 | print(f"脚本运行时发生异常: {str(e)}") 170 | traceback.print_exc() # 打印完整的异常堆栈信息 171 | -------------------------------------------------------------------------------- /resend.js: -------------------------------------------------------------------------------- 1 | // 检查环境变量 2 | function checkEnvironmentVariables(env) { 3 | const requiredVars = [ 4 | 'KEY', 5 | 'FROM_EMAIL', 6 | 'TO_EMAILS', 7 | 'RESEND_API_KEY' 8 | ]; 9 | const missingVars = requiredVars.filter(varName => !env[varName]); 10 | if (missingVars.length > 0) { 11 | throw new Error(`缺少必需的环境变量: ${missingVars.join(', ')}`); 12 | } 13 | } 14 | 15 | //加载邮件配置 16 | function loadEmailConfig(env) { 17 | const from_email = env.FROM_EMAIL; 18 | const to_emails_raw = env.TO_EMAILS; 19 | const subject = (env && env.SUBJECT) || "测试"; 20 | const body = (env && env.BODY) || "这是一封自动化测试邮件"; 21 | 22 | if (!from_email || !to_emails_raw || !subject || !body) { 23 | throw new Error("邮件配置缺失,请检查环境变量设置。"); 24 | } 25 | 26 | const to_emails = to_emails_raw 27 | .split(/[\n,]+/) 28 | .map(email => email.trim()) 29 | .filter(email => email.length > 0); 30 | 31 | if (to_emails.length === 0) { 32 | throw new Error("收件人列表为空,请检查 TO_EMAILS 配置。"); 33 | } 34 | 35 | return { from_email, to_emails, subject, body }; 36 | } 37 | 38 | // 延迟函数 39 | function delay(ms) { 40 | return new Promise(resolve => setTimeout(resolve, ms)); 41 | } 42 | 43 | // 发送邮件函数,使用 resend API 44 | async function sendEmail(to_email, subject, body, resendApiKey, from_email) { 45 | const url = "https://api.resend.com/emails"; 46 | const payload = { 47 | from: from_email, 48 | to: [to_email], 49 | subject: subject, 50 | html: body, 51 | }; 52 | 53 | const headers = { 54 | "Content-Type": "application/json", 55 | Authorization: `Bearer ${resendApiKey}`, 56 | }; 57 | 58 | const response = await fetch(url, { 59 | method: "POST", 60 | headers: headers, 61 | body: JSON.stringify(payload), 62 | }); 63 | 64 | if (response.ok) { 65 | console.log(`邮件已成功发送到 ${to_email}`); 66 | return true; 67 | } else { 68 | const errorText = await response.text(); 69 | console.error(`发送邮件到 ${to_email} 失败: ${response.status} - ${errorText}`); 70 | throw new Error(`发送邮件失败: ${errorText}`); 71 | } 72 | } 73 | 74 | // TG消息发送函数 75 | async function sendTelegramMessage(env, message) { 76 | const tgToken = env.TG_TOKEN; 77 | const tgId = env.TG_ID; 78 | 79 | const url = `https://api.telegram.org/bot${tgToken}/sendMessage`; 80 | const payload = { 81 | chat_id: tgId, 82 | text: message, 83 | parse_mode: "Markdown", 84 | }; 85 | 86 | const response = await fetch(url, { 87 | method: "POST", 88 | headers: { "Content-Type": "application/json" }, 89 | body: JSON.stringify(payload), 90 | }); 91 | 92 | if (!response.ok) { 93 | const errorText = await response.text(); 94 | console.error(`Telegram 消息发送失败: ${response.status} - ${errorText}`); 95 | throw new Error(`Telegram 消息发送失败: ${errorText}`); 96 | } 97 | } 98 | 99 | // 渲染前端 HTML 页面 100 | function renderHTML(lastExecution) { 101 | const siteIcon = 'https://pan.811520.xyz/icon/email.png'; 102 | const lastExecutionData = lastExecution ? JSON.parse(lastExecution) : null; 103 | const lastExecutionHTML = lastExecutionData 104 | ? ` 105 |
106 |

上次执行结果

107 |
108 |
109 | 发件人邮箱 110 | ${lastExecutionData.from_email} 111 |
112 |
113 | 收件人数量 114 | ${lastExecutionData.totalCount} 115 |
116 |
117 | 发送成功 118 | ${lastExecutionData.successEmails.length} 119 |
120 |
121 | 发送失败 122 | ${Object.keys(lastExecutionData.failedReasons).length} 123 |
124 |
125 |
126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | ${lastExecutionData.successEmails 137 | .map( 138 | email => ` 139 | 140 | 141 | 142 | 143 | 144 | 145 | ` 146 | ) 147 | .join("")} 148 | ${Object.entries(lastExecutionData.failedReasons) 149 | .map( 150 | ([email, reason]) => ` 151 | 152 | 153 | 154 | 155 | 156 | 157 | ` 158 | ) 159 | .join("")} 160 | 161 |
收件邮箱状态消息执行时间
${email}发送成功${lastExecutionData.executionTime}
${email}${reason}${lastExecutionData.executionTime}
162 |
163 |
164 | ` 165 | : '

暂无执行记录

'; 166 | 167 | return ` 168 | 169 | 170 | 171 | 邮件群发工具 172 | 173 | 174 | 175 | 320 | 321 | 322 |
323 |
324 |

邮件群发工具

325 |
326 | 329 |

330 |
331 |
332 |
333 | ${lastExecutionHTML} 334 |
335 | 452 | 453 | 454 | `; 455 | } 456 | 457 | // 执行邮件发送任务 458 | async function executeEmailTask(env, options = {}) { 459 | const { isScheduled = false, writer = null } = options; 460 | 461 | try { 462 | // 加载邮件配置 463 | const emailConfig = loadEmailConfig(env); 464 | const { from_email, to_emails, subject, body } = emailConfig; 465 | 466 | const successEmails = []; 467 | const failedReasons = {}; 468 | const totalCount = to_emails.length; 469 | const executionTime = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }).replace("年", "-").replace("月", "-").replace("日", ""); 470 | 471 | // 发送邮件 472 | for (let i = 0; i < totalCount; i++) { 473 | const email = to_emails[i]; 474 | try { 475 | // 如果有 writer,发送状态更新 476 | if (writer) { 477 | const statusUpdate = JSON.stringify({ 478 | statusUpdates: [`正在发送第 ${i + 1}/${totalCount} 封邮件到 ${email}`] 479 | }) + '\n'; 480 | await writer.write(new TextEncoder().encode(statusUpdate)); 481 | } 482 | 483 | // 添加延迟,确保每秒最多发送2封邮件 484 | if (i > 0) { 485 | await delay(1000); 486 | } 487 | 488 | const result = await sendEmail(email, subject, body, env.RESEND_API_KEY, from_email); 489 | if (result) { 490 | successEmails.push(email); 491 | // 发送成功状态更新 492 | if (writer) { 493 | const successUpdate = JSON.stringify({ 494 | statusUpdates: [`总共 ${totalCount} 个邮箱发送完成!成功:${successEmails.length} 个 | 失败:${Object.keys(failedReasons).length} 个`] 495 | }) + '\n'; 496 | await writer.write(new TextEncoder().encode(successUpdate)); 497 | } 498 | } else { 499 | failedReasons[email] = "未知错误"; 500 | // 发送失败状态更新 501 | if (writer) { 502 | const failureUpdate = JSON.stringify({ 503 | statusUpdates: [`❌ ${email} 发送失败: 未知错误`] 504 | }) + '\n'; 505 | await writer.write(new TextEncoder().encode(failureUpdate)); 506 | } 507 | } 508 | } catch (error) { 509 | failedReasons[email] = error.message; 510 | // 发送错误状态更新 511 | if (writer) { 512 | const errorUpdate = JSON.stringify({ 513 | statusUpdates: [`❌ ${email} 发送失败: ${error.message}`] 514 | }) + '\n'; 515 | await writer.write(new TextEncoder().encode(errorUpdate)); 516 | } 517 | } 518 | } 519 | 520 | // 构建结果数据 521 | const formData = { 522 | from_email, 523 | executionTime, 524 | totalCount, 525 | successEmails, 526 | failedReasons, 527 | }; 528 | 529 | // 存储到 KV 空间 530 | await env.AUTO_EMAIL.put("AUTO_EMAIL", JSON.stringify(formData)); 531 | 532 | // 发送 Telegram 消息通知 533 | // 获取当前时间,并转换为北京时间 534 | const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }).replace("年", "-").replace("月", "-").replace("日", ""); 535 | const successCount = successEmails.length; 536 | const failureCount = Object.keys(failedReasons).length; 537 | 538 | // 构建消息 539 | let message = `🤖 **${isScheduled ? '自动' : '手动'}邮件群发状态报告**\n⏰ 时间: \`${now}\`\n📊 总计: \`${totalCount}\` 个邮箱\n✅ 成功: \`${successCount}\`个 | ❌ 失败: \`${failureCount}\`个\n\n`; 540 | 541 | // 添加成功的邮箱列表 542 | for (const email of successEmails) { 543 | message += `邮箱:\`${email}\`\n状态: ✅ 发送成功\n`; 544 | } 545 | 546 | // 添加失败的邮箱列表及原因 547 | for (const [email, reason] of Object.entries(failedReasons)) { 548 | message += `邮箱:\`${email}\`\n状态: ❌ 发送失败\n失败原因: ${reason}\n`; 549 | } 550 | 551 | // 调用 Telegram API 发送消息 552 | await sendTelegramMessage(env, message); 553 | 554 | return formData; 555 | } catch (error) { 556 | console.error("邮件任务执行失败:", error); 557 | // 发送错误通知到 Telegram 558 | await sendTelegramMessage(env, `❌ **${isScheduled ? '自动' : '手动'}邮件群发失败**\n错误信息: \`${error.message}\``); 559 | throw error; 560 | } 561 | } 562 | 563 | // 导出函数 564 | export default { 565 | // Cron 触发器处理函数 566 | async scheduled(event, env, ctx) { 567 | console.log("Cron 触发开始执行..."); 568 | try { 569 | checkEnvironmentVariables(env); 570 | await executeEmailTask(env, { isScheduled: true }); 571 | console.log("Cron 任务执行完成"); 572 | } catch (error) { 573 | console.error("Cron 任务执行失败:", error); 574 | throw error; 575 | } 576 | }, 577 | 578 | // HTTP 请求处理函数 579 | async fetch(request, env, ctx) { 580 | try { 581 | checkEnvironmentVariables(env); 582 | const url = new URL(request.url); 583 | 584 | // 密钥保护 585 | const key = url.searchParams.get("key"); 586 | if (key !== env.KEY) { 587 | return new Response("访问被拒绝:密钥错误", { status: 401 }); 588 | } 589 | 590 | // 如果是 GET 请求,返回前端网页 591 | if (request.method === "GET") { 592 | const lastExecution = await env.AUTO_EMAIL.get("AUTO_EMAIL"); 593 | return new Response(renderHTML(lastExecution), { 594 | headers: { "Content-Type": "text/html" }, 595 | }); 596 | } 597 | 598 | // 如果是 POST 请求,执行脚本 599 | if (request.method === "POST") { 600 | try { 601 | const encoder = new TextEncoder(); 602 | const stream = new TransformStream(); 603 | const writer = stream.writable.getWriter(); 604 | 605 | const response = new Response(stream.readable, { 606 | headers: { 607 | "Content-Type": "application/json", 608 | "Access-Control-Allow-Origin": "*" 609 | } 610 | }); 611 | 612 | // 异步处理发送邮件的过程 613 | (async () => { 614 | try { 615 | const formData = await executeEmailTask(env, { 616 | isScheduled: false, 617 | writer: writer // 传入 writer 以支持状态更新 618 | }); 619 | await writer.write(encoder.encode(JSON.stringify({ formData }) + '\n')); 620 | } catch (error) { 621 | await writer.write( 622 | encoder.encode( 623 | JSON.stringify({ 624 | error: true, 625 | message: error.message 626 | }) + '\n' 627 | ) 628 | ); 629 | } finally { 630 | await writer.close(); 631 | } 632 | })(); 633 | 634 | return response; 635 | } catch (error) { 636 | return new Response(JSON.stringify({ 637 | error: true, 638 | message: error.message 639 | }), { 640 | status: 500, 641 | headers: { 642 | "Content-Type": "application/json", 643 | "Access-Control-Allow-Origin": "*" 644 | } 645 | }); 646 | } 647 | } 648 | 649 | // 如果不是 GET 或 POST 请求,返回 405 错误 650 | return new Response("方法不允许", { status: 405 }); 651 | } catch (error) { 652 | return new Response(error.message, { status: 500 }); 653 | } 654 | } 655 | }; 656 | --------------------------------------------------------------------------------