Files
codexautopool/backend/internal/mail/service.go

589 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mail
import (
"bytes"
"context"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"regexp"
"strings"
"sync"
"time"
"codex-pool/internal/config"
)
// 默认邮箱配置
var defaultMailServices = []config.MailServiceConfig{
{
Name: "esyteam",
APIBase: "https://mail.esyteam.edu.kg",
APIToken: "005d6f3e-5312-4c37-8125-e1f71243e1ba",
Domain: "esyteam.edu.kg",
EmailPath: "/api/public/emailList",
AddUserAPI: "/api/public/addUser",
},
}
// 全局变量
var (
currentMailServices []config.MailServiceConfig
mailServicesMutex sync.RWMutex
currentServiceIndex int
)
func init() {
currentMailServices = defaultMailServices
}
// Init 初始化邮箱服务配置
func Init(services []config.MailServiceConfig) {
mailServicesMutex.Lock()
defer mailServicesMutex.Unlock()
if len(services) > 0 {
for i := range services {
if services[i].EmailPath == "" {
services[i].EmailPath = "/api/public/emailList"
}
if services[i].AddUserAPI == "" {
services[i].AddUserAPI = "/api/public/addUser"
}
if services[i].Name == "" {
services[i].Name = fmt.Sprintf("mail-service-%d", i+1)
}
}
currentMailServices = services
fmt.Printf("[邮箱] 已加载 %d 个邮箱服务配置:\n", len(services))
for _, s := range services {
fmt.Printf(" - %s (%s) @ %s\n", s.Name, s.Domain, s.APIBase)
}
} else {
currentMailServices = defaultMailServices
fmt.Println("[邮箱] 使用默认邮箱服务配置")
}
currentServiceIndex = 0
}
// GetServices 获取当前邮箱服务配置
func GetServices() []config.MailServiceConfig {
mailServicesMutex.RLock()
defer mailServicesMutex.RUnlock()
return currentMailServices
}
// GetNextService 轮询获取下一个邮箱服务
func GetNextService() config.MailServiceConfig {
mailServicesMutex.Lock()
defer mailServicesMutex.Unlock()
if len(currentMailServices) == 0 {
return defaultMailServices[0]
}
service := currentMailServices[currentServiceIndex]
currentServiceIndex = (currentServiceIndex + 1) % len(currentMailServices)
return service
}
// GetRandomService 随机获取一个邮箱服务
func GetRandomService() config.MailServiceConfig {
mailServicesMutex.RLock()
defer mailServicesMutex.RUnlock()
if len(currentMailServices) == 0 {
return defaultMailServices[0]
}
return currentMailServices[rand.Intn(len(currentMailServices))]
}
// GetServiceByDomain 根据域名获取对应的邮箱服务
func GetServiceByDomain(domain string) *config.MailServiceConfig {
mailServicesMutex.RLock()
defer mailServicesMutex.RUnlock()
for _, s := range currentMailServices {
if s.Domain == domain || strings.HasSuffix(domain, "."+s.Domain) {
return &s
}
}
return nil
}
// ==================== 邮件结构 ====================
// EmailListRequest 邮件列表请求
type EmailListRequest struct {
ToEmail string `json:"toEmail"`
TimeSort string `json:"timeSort"`
Size int `json:"size"`
}
// EmailListResponse 邮件列表响应
type EmailListResponse struct {
Code int `json:"code"`
Data []EmailItem `json:"data"`
}
// EmailItem 邮件项
type EmailItem struct {
EmailID int `json:"emailId"`
Content string `json:"content"`
Text string `json:"text"`
Subject string `json:"subject"`
}
// AddUserRequest 创建用户请求
type AddUserRequest struct {
List []AddUserItem `json:"list"`
}
// AddUserItem 用户项
type AddUserItem struct {
Email string `json:"email"`
Password string `json:"password"`
}
// AddUserResponse 创建用户响应
type AddUserResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
// ==================== 邮箱生成 ====================
// GenerateEmail 生成随机邮箱并在邮件系统中创建
func GenerateEmail() string {
return GenerateEmailWithService(GetNextService())
}
// GenerateEmailWithService 使用指定服务生成随机邮箱
func GenerateEmailWithService(service config.MailServiceConfig) string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 10)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
email := string(b) + "@" + service.Domain
if err := CreateMailboxWithService(email, service); err != nil {
fmt.Printf(" [!] 创建邮箱失败 (%s): %v (继续尝试)\n", service.Name, err)
}
return email
}
// CreateMailbox 在邮件系统中创建邮箱
func CreateMailbox(email string) error {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return fmt.Errorf("无效的邮箱地址: %s", email)
}
domain := parts[1]
service := GetServiceByDomain(domain)
if service == nil {
services := GetServices()
if len(services) > 0 {
service = &services[0]
} else {
return fmt.Errorf("没有可用的邮箱服务")
}
}
return CreateMailboxWithService(email, *service)
}
// CreateMailboxWithService 使用指定服务在邮件系统中创建邮箱
func CreateMailboxWithService(email string, service config.MailServiceConfig) error {
client := &http.Client{Timeout: 10 * time.Second}
parts := strings.Split(email, "@")
if len(parts) == 2 {
domain := parts[1]
if strings.HasSuffix(domain, "."+service.Domain) {
email = parts[0] + "@" + service.Domain
}
}
payload := AddUserRequest{
List: []AddUserItem{
{Email: email, Password: GeneratePassword()},
},
}
jsonData, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", service.APIBase+service.AddUserAPI, bytes.NewBuffer(jsonData))
if err != nil {
return err
}
req.Header.Set("Authorization", service.APIToken)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
var result AddUserResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
if result.Code != 200 {
if strings.Contains(result.Message, "exist") {
return nil
}
return fmt.Errorf("API 错误: %s", result.Message)
}
return nil
}
// GeneratePassword 生成随机密码
func GeneratePassword() string {
const (
upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lower = "abcdefghijklmnopqrstuvwxyz"
digits = "0123456789"
special = "#$%@!"
)
password := make([]byte, 12)
password[0] = upper[rand.Intn(len(upper))]
password[1] = lower[rand.Intn(len(lower))]
password[10] = digits[rand.Intn(len(digits))]
password[11] = special[rand.Intn(len(special))]
charset := upper + lower
for i := 2; i < 10; i++ {
password[i] = charset[rand.Intn(len(charset))]
}
return string(password)
}
// ==================== 邮件客户端 ====================
// Client 邮件客户端
type Client struct {
client *http.Client
service *config.MailServiceConfig
}
// NewClient 创建邮件客户端
func NewClient() *Client {
services := GetServices()
var service *config.MailServiceConfig
if len(services) > 0 {
service = &services[0]
} else {
service = &defaultMailServices[0]
}
return &Client{
client: &http.Client{Timeout: 10 * time.Second},
service: service,
}
}
// NewClientWithService 创建指定服务的邮件客户端
func NewClientWithService(service config.MailServiceConfig) *Client {
return &Client{
client: &http.Client{Timeout: 10 * time.Second},
service: &service,
}
}
// NewClientForEmail 根据邮箱地址创建对应的邮件客户端
func NewClientForEmail(email string) *Client {
parts := strings.Split(email, "@")
if len(parts) == 2 {
if service := GetServiceByDomain(parts[1]); service != nil {
return NewClientWithService(*service)
}
}
return NewClient()
}
// GetEmails 获取邮件列表
func (m *Client) GetEmails(email string, size int) ([]EmailItem, error) {
service := m.service
parts := strings.Split(email, "@")
if len(parts) == 2 {
if s := GetServiceByDomain(parts[1]); s != nil {
service = s
}
}
url := service.APIBase + service.EmailPath
payload := EmailListRequest{
ToEmail: email,
TimeSort: "desc",
Size: size,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", service.APIToken)
req.Header.Set("Content-Type", "application/json")
resp, err := m.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
}
var result EmailListResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
if result.Code != 200 {
return nil, fmt.Errorf("API 错误: %d", result.Code)
}
return result.Data, nil
}
// WaitForCode 等待验证码邮件
func (m *Client) WaitForCode(email string, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return m.WaitForCodeWithContext(ctx, email)
}
// WaitForCodeWithContext 等待验证码邮件(支持 context 取消)
func (m *Client) WaitForCodeWithContext(ctx context.Context, email string) (string, error) {
// 匹配6位数字验证码
codeRegex := regexp.MustCompile(`\b(\d{6})\b`)
// 专门匹配 OpenAI 验证码邮件标题格式: "Your ChatGPT code is 016547" 或 "OpenAI - Verify your email"
titleCodeRegex := regexp.MustCompile(`(?i)(?:code\s+is\s+|code:\s*|验证码[:]\s*)(\d{6})`)
// 记录已经看到的验证码,避免重复返回旧验证码
seenCodes := make(map[string]bool)
// 第一次获取邮件,记录已有的验证码(这些是旧的)
initialEmails, _ := m.GetEmails(email, 10)
for _, mail := range initialEmails {
// 从标题提取
if matches := codeRegex.FindStringSubmatch(mail.Subject); len(matches) >= 2 {
seenCodes[matches[1]] = true
}
// 从内容提取
content := mail.Content
if content == "" {
content = mail.Text
}
if matches := codeRegex.FindStringSubmatch(content); len(matches) >= 2 {
seenCodes[matches[1]] = true
}
}
for {
select {
case <-ctx.Done():
return "", fmt.Errorf("验证码获取超时")
default:
}
emails, err := m.GetEmails(email, 10)
if err == nil {
for _, mail := range emails {
subject := strings.ToLower(mail.Subject)
// 匹配 OpenAI/ChatGPT 验证码邮件
// 标题格式: "Your ChatGPT code is 016547" 或包含 "verify", "code" 等
isCodeEmail := strings.Contains(subject, "chatgpt code") ||
strings.Contains(subject, "openai") ||
strings.Contains(subject, "verify your email") ||
(strings.Contains(subject, "code") && strings.Contains(subject, "is"))
if !isCodeEmail {
continue
}
// 优先使用专门的标题正则匹配 (如 "Your ChatGPT code is 016547")
if matches := titleCodeRegex.FindStringSubmatch(mail.Subject); len(matches) >= 2 {
code := matches[1]
if !seenCodes[code] {
return code, nil
}
}
// 从标题中提取6位数字
if matches := codeRegex.FindStringSubmatch(mail.Subject); len(matches) >= 2 {
code := matches[1]
if !seenCodes[code] {
return code, nil
}
}
// 如果标题中没有,从内容中提取
content := mail.Content
if content == "" {
content = mail.Text
}
// 在内容中查找验证码,优先匹配 "enter this code:" 后面的数字
contentCodeRegex := regexp.MustCompile(`(?i)(?:enter\s+this\s+code[:]\s*|code[:]\s*)(\d{6})`)
if matches := contentCodeRegex.FindStringSubmatch(content); len(matches) >= 2 {
code := matches[1]
if !seenCodes[code] {
return code, nil
}
}
// 最后尝试普通6位数字匹配
if matches := codeRegex.FindStringSubmatch(content); len(matches) >= 2 {
code := matches[1]
if !seenCodes[code] {
return code, nil
}
}
}
}
select {
case <-ctx.Done():
return "", fmt.Errorf("验证码获取超时")
case <-time.After(1 * time.Second):
}
}
}
// WaitForInviteLink 等待邀请邮件并提取链接
func (m *Client) WaitForInviteLink(email string, timeout time.Duration) (string, error) {
start := time.Now()
for time.Since(start) < timeout {
emails, err := m.GetEmails(email, 10)
if err == nil {
for _, mail := range emails {
content := mail.Content
if content == "" {
content = mail.Text
}
if strings.Contains(mail.Subject, "invite") ||
strings.Contains(mail.Subject, "Team") ||
strings.Contains(mail.Subject, "ChatGPT") ||
strings.Contains(content, "invite") {
link := extractInviteLink(content)
if link != "" {
return link, nil
}
}
}
}
time.Sleep(1 * time.Second)
}
return "", fmt.Errorf("等待邀请邮件超时")
}
// extractInviteLink 从邮件内容提取邀请链接
func extractInviteLink(content string) string {
patterns := []string{
`https://chatgpt\.com/invite/[^\s"'<>]+`,
`https://chat\.openai\.com/invite/[^\s"'<>]+`,
`https://chatgpt\.com/[^\s"'<>]*accept[^\s"'<>]*`,
}
for _, pattern := range patterns {
re := regexp.MustCompile(pattern)
match := re.FindString(content)
if match != "" {
match = strings.ReplaceAll(match, "&amp;", "&")
return match
}
}
return ""
}
// ==================== 便捷函数 ====================
// WaitForInviteEmail 等待邀请邮件
func WaitForInviteEmail(email string, timeout time.Duration) (string, error) {
client := NewClientForEmail(email)
return client.WaitForInviteLink(email, timeout)
}
// GetVerificationCode 获取验证码
func GetVerificationCode(email string, timeout time.Duration) (string, error) {
client := NewClientForEmail(email)
return client.WaitForCode(email, timeout)
}
// GetLatestEmailID 获取邮箱最新邮件的ID
// 基于 get_code.go 的实现用于在发送验证码请求前记录最新邮件ID
func GetLatestEmailID(email string) int {
client := NewClientForEmail(email)
emails, err := client.GetEmails(email, 1)
if err != nil || len(emails) == 0 {
return 0
}
return emails[0].EmailID
}
// GetEmailOTPAfterID 获取指定邮件ID之后的OTP验证码
// 基于 get_code.go 的实现,只获取 afterEmailID 之后的新邮件中的验证码
func GetEmailOTPAfterID(email string, afterEmailID int, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return GetEmailOTPAfterIDWithContext(ctx, email, afterEmailID)
}
// GetEmailOTPAfterIDWithContext 获取指定邮件ID之后的OTP验证码支持 context 取消)
func GetEmailOTPAfterIDWithContext(ctx context.Context, email string, afterEmailID int) (string, error) {
client := NewClientForEmail(email)
codeRegex := regexp.MustCompile(`\b(\d{6})\b`)
for {
select {
case <-ctx.Done():
return "", fmt.Errorf("验证码获取超时 (afterEmailID=%d)", afterEmailID)
default:
}
emails, err := client.GetEmails(email, 5)
if err == nil {
for _, mail := range emails {
// 只处理 afterEmailID 之后的新邮件
if mail.EmailID <= afterEmailID {
continue
}
subject := strings.ToLower(mail.Subject)
// 匹配 OpenAI/ChatGPT 验证码邮件
if !strings.Contains(subject, "code") {
continue
}
// 从标题提取验证码
if matches := codeRegex.FindStringSubmatch(mail.Subject); len(matches) >= 2 {
return matches[1], nil
}
// 从内容提取验证码
content := mail.Content
if content == "" {
content = mail.Text
}
if matches := codeRegex.FindStringSubmatch(content); len(matches) >= 2 {
return matches[1], nil
}
}
}
select {
case <-ctx.Done():
return "", fmt.Errorf("验证码获取超时 (afterEmailID=%d)", afterEmailID)
case <-time.After(1 * time.Second):
}
}
}