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))] } // matchDomain 检查邮箱域名是否匹配服务域名(精确匹配或子域名匹配) func matchDomain(emailDomain, serviceDomain string) bool { if serviceDomain == "" { return false } return emailDomain == serviceDomain || strings.HasSuffix(emailDomain, "."+serviceDomain) } // GetServiceByDomain 根据域名获取对应的邮箱服务 // 会同时检查 Domain(主域名)和 Domains(附加域名列表) func GetServiceByDomain(domain string) *config.MailServiceConfig { mailServicesMutex.RLock() defer mailServicesMutex.RUnlock() for i := range currentMailServices { s := ¤tMailServices[i] // 检查主域名 if matchDomain(domain, s.Domain) { return s } // 检查附加域名列表 for _, d := range s.Domains { if matchDomain(domain, d) { 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, "&", "&") 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): } } }