465 lines
11 KiB
Go
465 lines
11 KiB
Go
package mail
|
|
|
|
import (
|
|
"bytes"
|
|
"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 {
|
|
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) {
|
|
start := time.Now()
|
|
codeRegex := regexp.MustCompile(`\b(\d{6})\b`)
|
|
|
|
for time.Since(start) < timeout {
|
|
emails, err := m.GetEmails(email, 10)
|
|
if err == nil {
|
|
for _, mail := range emails {
|
|
subject := strings.ToLower(mail.Subject)
|
|
// 匹配多种可能的验证码邮件主题
|
|
// 包括 "Your ChatGPT code is 016547" 格式
|
|
isCodeEmail := strings.Contains(subject, "code") ||
|
|
strings.Contains(subject, "verify") ||
|
|
strings.Contains(subject, "verification") ||
|
|
strings.Contains(subject, "openai") ||
|
|
strings.Contains(subject, "confirm") ||
|
|
strings.Contains(subject, "chatgpt")
|
|
|
|
if !isCodeEmail {
|
|
continue
|
|
}
|
|
|
|
// 优先从标题中提取验证码 (如 "Your ChatGPT code is 016547")
|
|
matches := codeRegex.FindStringSubmatch(mail.Subject)
|
|
if len(matches) >= 2 {
|
|
return matches[1], nil
|
|
}
|
|
|
|
// 如果标题中没有,从内容中提取
|
|
content := mail.Content
|
|
if content == "" {
|
|
content = mail.Text
|
|
}
|
|
matches = codeRegex.FindStringSubmatch(content)
|
|
if len(matches) >= 2 {
|
|
return matches[1], nil
|
|
}
|
|
}
|
|
}
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
|
|
return "", fmt.Errorf("验证码获取超时")
|
|
}
|
|
|
|
// 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)
|
|
}
|