diff --git a/WEB_API_DEPLOYMENT.md b/WEB_API_DEPLOYMENT.md new file mode 100644 index 0000000..8ccb397 --- /dev/null +++ b/WEB_API_DEPLOYMENT.md @@ -0,0 +1,201 @@ +# Stalwart 邮件 Web API 部署指南 + +本文档介绍如何将Stalwart邮件API客户端部署为Web服务,通过HTTP API提供邮箱管理功能。 + +## 特点 + +- RESTful API设计 +- 默认使用API Key认证方式 +- JSON格式请求和响应 +- 支持所有命令行工具的功能 +- 易于集成到现有系统 + +## API端点 + +| 端点 | 方法 | 描述 | +|------|------|------| +| `/api/create_user` | POST | 创建新邮箱用户 | +| `/api/list_users` | GET | 获取用户列表 | +| `/api/get_user` | GET | 获取单个用户信息 | +| `/api/get_emails` | POST | 获取用户邮件 | +| `/health` | GET | 健康检查 | + +## 构建和部署 + +### 1. 编译Web服务器 + +```bash +cd go_stalwart_client +go build -o bin/stalwart-web-server cmd/web_server/main.go +``` + +### 2. 准备配置文件 + +确保`config/config.yaml`文件已正确配置,特别是API Key: + +```yaml +api: + base_url: https://mail.evnmail.com/api + disable_proxy: true +user: + default_domain: evnmail.com + default_quota: 1073741824 +auth: + basic: + username: admin + password: your_password_here + api_key: api_your_api_key_here +``` + +### 3. 部署到Linux服务器 + +#### 3.1 复制文件到服务器 + +```bash +# 创建目录 +ssh user@your-server "mkdir -p /usr/local/bin /etc/stalwart" + +# 复制可执行文件 +scp bin/stalwart-web-server user@your-server:/usr/local/bin/ + +# 复制配置文件 +scp config/config.yaml user@your-server:/etc/stalwart/ + +# 复制服务文件 +scp stalwart-api.service user@your-server:/etc/systemd/system/ +``` + +#### 3.2 设置权限 + +```bash +ssh user@your-server "chmod +x /usr/local/bin/stalwart-web-server" +``` + +#### 3.3 创建用户和组(如果需要) + +```bash +ssh user@your-server "useradd -r -s /bin/false stalwart" +``` + +#### 3.4 生成API Token并更新服务文件 + +在服务器上运行一次Web服务器以生成API Token: + +```bash +/usr/local/bin/stalwart-web-server -c /etc/stalwart/config.yaml +``` + +复制生成的API Token,然后更新服务文件: + +```bash +sudo vim /etc/systemd/system/stalwart-api.service +# 将YOUR_API_TOKEN替换为生成的Token +``` + +#### 3.5 启动服务 + +```bash +sudo systemctl daemon-reload +sudo systemctl enable stalwart-api +sudo systemctl start stalwart-api +sudo systemctl status stalwart-api +``` + +### 4. 部署到Windows服务器 + +#### 4.1 使用NSSM(Non-Sucking Service Manager) + +1. 下载NSSM: https://nssm.cc/download +2. 安装服务: + +``` +nssm install StalwartAPI D:\path\to\stalwart-web-server.exe -p 8080 -c D:\path\to\config.yaml -t YOUR_API_TOKEN +nssm set StalwartAPI AppDirectory D:\path\to\ +nssm start StalwartAPI +``` + +## API使用示例 + +### 创建用户 + +```bash +curl -X POST http://your-server:8080/api/create_user \ + -H "Authorization: Bearer YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "newuser@evnmail.com", + "description": "新用户", + "quota": 1073741824 + }' +``` + +响应: + +```json +{ + "id": 123, + "username": "newuser", + "email": "newuser@evnmail.com", + "password": "A8f2@xLp9q!Z", + "success": true, + "message": "用户创建成功" +} +``` + +### 获取用户列表 + +```bash +curl -X GET "http://your-server:8080/api/list_users?limit=10&page=1" \ + -H "Authorization: Bearer YOUR_API_TOKEN" +``` + +### 获取用户信息 + +```bash +curl -X GET "http://your-server:8080/api/get_user?email=user@evnmail.com" \ + -H "Authorization: Bearer YOUR_API_TOKEN" +``` + +### 获取邮件 + +```bash +curl -X POST http://your-server:8080/api/get_emails \ + -H "Authorization: Bearer YOUR_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "user@evnmail.com", + "password": "userpassword", + "count": 5 + }' +``` + +## 注意事项 + +1. 安全性: + - 使用HTTPS部署Web服务 + - 保护API Token,不要泄露 + - 定期更换API Token + +2. 资源考虑: + - 对于高负载环境,考虑增加服务器资源 + - 对于大规模部署,考虑负载均衡 + +3. 监控: + - 定期检查服务状态 + - 设置日志轮转 + - 监控API调用频率和响应时间 + +## 故障排除 + +1. 服务无法启动 + - 检查配置文件路径是否正确 + - 确认API服务器地址可访问 + - 检查端口是否被占用 + +2. 认证失败 + - 确认API Token是否正确设置 + - 检查Authorization头格式 + +3. API请求失败 + - 检查请求格式和参数 + - 查看服务器日志获取详细错误信息 \ No newline at end of file diff --git a/cmd/create_user/main.go b/cmd/create_user/main.go new file mode 100644 index 0000000..70904cf --- /dev/null +++ b/cmd/create_user/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "flag" + "fmt" + "os" + "stalwart_client/pkg/api" +) + +func main() { + // 解析命令行参数 + emailPtr := flag.String("e", "", "邮箱地址") + passwordPtr := flag.String("p", "", "用户密码") + descriptionPtr := flag.String("d", "", "用户描述") + quotaPtr := flag.Int64("q", 0, "邮箱配额(字节)") + authTypePtr := flag.String("a", "basic", "认证方式: basic(基本认证)或apikey(API Key认证)") + authUserPtr := flag.String("u", "", "基本认证用户名") + authPassPtr := flag.String("P", "", "基本认证密码") + apiKeyPtr := flag.String("k", "", "API Key") + configPtr := flag.String("c", "", "配置文件路径") + + flag.Parse() + + // 加载配置 + config, err := api.LoadConfig(*configPtr) + if err != nil { + fmt.Printf("加载配置失败: %v\n", err) + os.Exit(1) + } + + // 创建客户端 + client := api.NewClient(config) + + // 确定认证类型 + var authType api.AuthType + if *authTypePtr == "apikey" { + authType = api.APIKeyAuth + } else { + authType = api.BasicAuth + } + + // 创建用户 + userID, username, email, password, err := client.CreateEmailUser( + *emailPtr, + *passwordPtr, + *descriptionPtr, + *quotaPtr, + authType, + *authUserPtr, + *authPassPtr, + *apiKeyPtr, + ) + + if err != nil { + fmt.Printf("创建用户失败: %v\n", err) + os.Exit(1) + } + + // 打印结果 + success := api.PrintUserResult(userID, username, email, password) + if !success { + os.Exit(1) + } +} diff --git a/cmd/get_latest_email/main.go b/cmd/get_latest_email/main.go new file mode 100644 index 0000000..9cca3e9 --- /dev/null +++ b/cmd/get_latest_email/main.go @@ -0,0 +1,68 @@ +package main + +import ( + "flag" + "fmt" + "os" + "stalwart_client/pkg/api" + "strings" +) + +func main() { + // 解析命令行参数 + emailPtr := flag.String("e", "hayfzgul@evnmail.com", "邮箱地址") + passwordPtr := flag.String("p", "", "邮箱密码") + serverPtr := flag.String("s", "mail.evnmail.com", "IMAP服务器地址") + portPtr := flag.Int("P", 993, "IMAP服务器端口") + numPtr := flag.Int("n", 1, "获取的邮件数量") + noSSLPtr := flag.Bool("no-ssl", false, "禁用SSL连接") + configPtr := flag.String("c", "", "配置文件路径") + + flag.Parse() + + // 加载配置 + _, err := api.LoadConfig(*configPtr) + if err != nil { + fmt.Printf("加载配置失败: %v\n", err) + os.Exit(1) + } + + // 检查密码 + password := *passwordPtr + if password == "" { + fmt.Println("错误: 必须提供邮箱密码") + flag.Usage() + os.Exit(1) + } + + // 提取域名 + parts := strings.Split(*emailPtr, "@") + domain := "" + if len(parts) > 1 { + domain = parts[1] + } + + // 如果未指定服务器,尝试使用邮箱域名 + server := *serverPtr + if server == "" && domain != "" { + server = fmt.Sprintf("mail.%s", domain) + } + + // 获取邮件 + emails, err := api.GetLatestEmails( + *emailPtr, + password, + server, + *portPtr, + !*noSSLPtr, + *numPtr, + ) + + if err != nil { + fmt.Printf("获取邮件失败: %v\n", err) + os.Exit(1) + } + + // 打印邮件信息 + api.PrintEmails(emails) +} diff --git a/cmd/get_user/main.go b/cmd/get_user/main.go new file mode 100644 index 0000000..1f763f8 --- /dev/null +++ b/cmd/get_user/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "flag" + "fmt" + "os" + "stalwart_client/pkg/api" + "strconv" +) + +func main() { + // 解析命令行参数 + idPtr := flag.String("i", "", "用户ID") + emailPtr := flag.String("e", "", "用户邮箱地址") + authTypePtr := flag.String("a", "basic", "认证方式: basic(基本认证)或apikey(API Key认证)") + authUserPtr := flag.String("u", "", "基本认证用户名") + authPassPtr := flag.String("P", "", "基本认证密码") + apiKeyPtr := flag.String("k", "", "API Key") + configPtr := flag.String("c", "", "配置文件路径") + + flag.Parse() + + // 检查参数 + if *idPtr == "" && *emailPtr == "" { + fmt.Println("错误: 必须指定用户ID或邮箱地址") + flag.Usage() + os.Exit(1) + } + + // 加载配置 + config, err := api.LoadConfig(*configPtr) + if err != nil { + fmt.Printf("加载配置失败: %v\n", err) + os.Exit(1) + } + + // 创建客户端 + client := api.NewClient(config) + + // 确定认证类型 + var authType api.AuthType + if *authTypePtr == "apikey" { + authType = api.APIKeyAuth + } else { + authType = api.BasicAuth + } + + // 获取用户信息 + var userInfo map[string]interface{} + var err2 error + + if *idPtr != "" { + // 通过ID查询 + id, err := strconv.Atoi(*idPtr) + if err != nil { + fmt.Printf("无效的用户ID: %v\n", err) + os.Exit(1) + } + + fmt.Printf("通过ID查询用户: %d\n", id) + userInfo, err2 = client.GetUserDetails(id, authType, *authUserPtr, *authPassPtr, *apiKeyPtr) + } else { + // 通过邮箱查询 + fmt.Printf("通过邮箱查询用户: %s\n", *emailPtr) + userInfo, err2 = client.GetUserByEmail(*emailPtr, authType, *authUserPtr, *authPassPtr, *apiKeyPtr) + } + + if err2 != nil { + fmt.Printf("获取用户信息失败: %v\n", err2) + os.Exit(1) + } + + // 格式化显示结果 + api.FormatUserDetails(userInfo) +} diff --git a/cmd/list_users/main.go b/cmd/list_users/main.go new file mode 100644 index 0000000..2fe4016 --- /dev/null +++ b/cmd/list_users/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "flag" + "fmt" + "os" + "stalwart_client/pkg/api" +) + +func main() { + // 解析命令行参数 + limitPtr := flag.Int("l", 100, "最大返回结果数量") + pagePtr := flag.Int("p", 1, "页码") + _ = flag.String("q", "", "搜索关键词") // 暂未实现搜索功能,保留参数 + authTypePtr := flag.String("a", "basic", "认证方式: basic(基本认证)或apikey(API Key认证)") + authUserPtr := flag.String("u", "", "基本认证用户名") + authPassPtr := flag.String("P", "", "基本认证密码") + apiKeyPtr := flag.String("k", "", "API Key") + configPtr := flag.String("c", "", "配置文件路径") + + flag.Parse() + + // 加载配置 + config, err := api.LoadConfig(*configPtr) + if err != nil { + fmt.Printf("加载配置失败: %v\n", err) + os.Exit(1) + } + + // 创建客户端 + client := api.NewClient(config) + + // 确定认证类型 + var authType api.AuthType + if *authTypePtr == "apikey" { + authType = api.APIKeyAuth + } else { + authType = api.BasicAuth + } + + // 获取用户列表 + userList, err := client.ListUsers( + authType, + *authUserPtr, + *authPassPtr, + *apiKeyPtr, + *limitPtr, + *pagePtr, + ) + + if err != nil { + fmt.Printf("获取用户列表失败: %v\n", err) + os.Exit(1) + } + + // 格式化显示结果 + api.FormatUserList(userList) +} diff --git a/cmd/web_server/main.go b/cmd/web_server/main.go new file mode 100644 index 0000000..b40b2d0 --- /dev/null +++ b/cmd/web_server/main.go @@ -0,0 +1,530 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "math/rand" + "net/http" + "os" + "stalwart_client/pkg/api" + "strconv" + "strings" + "time" +) + +// 请求和响应结构 +type CreateUserRequest struct { + Email string `json:"email"` + Password string `json:"password,omitempty"` + Description string `json:"description,omitempty"` + Quota int64 `json:"quota,omitempty"` +} + +type UserResponse struct { + ID int `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + +type ListUsersRequest struct { + Limit int `json:"limit,omitempty"` + Page int `json:"page,omitempty"` +} + +type ListUsersResponse struct { + Users []api.User `json:"users"` + Total int `json:"total"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + +type GetUserRequest struct { + ID int `json:"id,omitempty"` + Email string `json:"email,omitempty"` +} + +type GetUserResponse struct { + User map[string]interface{} `json:"user"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + +type GetEmailRequest struct { + Email string `json:"email"` + Password string `json:"password"` + Server string `json:"server,omitempty"` + Port int `json:"port,omitempty"` + Count int `json:"count,omitempty"` +} + +type EmailResponse struct { + Emails []*api.EmailMessage `json:"emails"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + +// 全局变量 +var ( + client *api.Client +) + +// API处理函数 + +// 创建用户 +func createUserHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + var req CreateUserRequest + // 尝试解析请求体,但即使失败也继续处理 + _ = json.NewDecoder(r.Body).Decode(&req) + + // 如果email为空,自动生成一个随机用户名和域名 + if req.Email == "" { + randomUsername := api.GenerateRandomPassword(8) + req.Email = fmt.Sprintf("%s@%s", randomUsername, client.Config.User.DefaultDomain) + } + + // 如果密码为空,将由系统自动生成 + // 如果配额为0,使用默认配额 + if req.Quota == 0 { + req.Quota = client.Config.User.DefaultQuota + } + + // 如果描述为空,添加默认描述 + if req.Description == "" { + req.Description = "系统自动创建的用户" + } + + // 创建用户 + userID, username, email, password, err := client.CreateEmailUser( + req.Email, + req.Password, + req.Description, + req.Quota, + api.APIKeyAuth, // 默认使用API Key认证 + "", + "", + client.Config.Auth.APIKey, + ) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{ + "success": "false", + "message": fmt.Sprintf("创建用户失败: %v", err), + }) + return + } + + // 返回响应 + response := UserResponse{ + ID: userID, + Username: username, + Email: email, + Password: password, + Success: true, + Message: "用户创建成功", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// 创建简单用户 - 无需任何参数,直接返回创建的用户信息 +func createSimpleUserHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // 生成随机用户名 - 确保不包含特殊字符 + randomUsername := api.GenerateRandomUsername(8) + // 确保用户名只包含字母和数字 + for i := 0; i < len(randomUsername); i++ { + if !((randomUsername[i] >= 'a' && randomUsername[i] <= 'z') || + (randomUsername[i] >= '0' && randomUsername[i] <= '9')) { + // 替换为字母 + randomUsername = randomUsername[:i] + "x" + randomUsername[i+1:] + } + } + + email := fmt.Sprintf("%s@%s", randomUsername, client.Config.User.DefaultDomain) + description := "自动创建的临时用户" + quota := client.Config.User.DefaultQuota + + // 创建用户 + userID, username, email, password, err := client.CreateEmailUser( + email, + "", // 空密码,系统会自动生成 + description, + quota, + api.APIKeyAuth, + "", + "", + client.Config.Auth.APIKey, + ) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{ + "success": "false", + "message": fmt.Sprintf("创建用户失败: %v", err), + }) + return + } + + // 返回简单格式的响应 + response := map[string]interface{}{ + "id": userID, + "username": username, + "email": email, + "password": password, + "server": fmt.Sprintf("mail.%s", strings.Split(email, "@")[1]), + "success": true, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// 创建模拟用户 - 不实际调用API,只生成模拟数据 +func createMockUserHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // 生成随机用户名 - 确保不包含特殊字符 + randomUsername := api.GenerateRandomUsername(8) + // 确保用户名只包含字母和数字 + for i := 0; i < len(randomUsername); i++ { + if !((randomUsername[i] >= 'a' && randomUsername[i] <= 'z') || + (randomUsername[i] >= '0' && randomUsername[i] <= '9')) { + // 替换为字母 + randomUsername = randomUsername[:i] + "x" + randomUsername[i+1:] + } + } + + domain := client.Config.User.DefaultDomain + email := fmt.Sprintf("%s@%s", randomUsername, domain) + password := api.GenerateRandomPassword(12) + + // 返回模拟响应 + userID := rand.Intn(1000) + 1000 // 随机生成ID + + response := map[string]interface{}{ + "id": userID, + "username": randomUsername, + "email": email, + "password": password, + "server": fmt.Sprintf("mail.%s", domain), + "imap_server": fmt.Sprintf("mail.%s", domain), + "imap_port": 993, + "smtp_server": fmt.Sprintf("mail.%s", domain), + "smtp_port": 465, + "description": "模拟生成的测试用户", + "quota": client.Config.User.DefaultQuota, + "created_time": time.Now().Format(time.RFC3339), + "success": true, + "message": "模拟用户创建成功(仅本地数据,未实际调用API)", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// 列出用户 +func listUsersHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // 解析查询参数 + limitStr := r.URL.Query().Get("limit") + pageStr := r.URL.Query().Get("page") + + limit := 100 + page := 1 + + if limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { + limit = l + } + } + + if pageStr != "" { + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + } + + // 获取用户列表 + userList, err := client.ListUsers( + api.APIKeyAuth, // 默认使用API Key认证 + "", + "", + client.Config.Auth.APIKey, + limit, + page, + ) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{ + "success": "false", + "message": fmt.Sprintf("获取用户列表失败: %v", err), + }) + return + } + + // 返回响应 + response := ListUsersResponse{ + Users: userList.Data.Items, + Total: userList.Data.Total, + Success: true, + Message: "获取用户列表成功", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// 获取单个用户信息 +func getUserHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // 解析参数 + idStr := r.URL.Query().Get("id") + email := r.URL.Query().Get("email") + + // 如果两个参数都为空,尝试从路径中获取 + if idStr == "" && email == "" { + pathParts := strings.Split(r.URL.Path, "/") + if len(pathParts) > 3 { + // 路径形式可能是 /api/get_user/123 或 /api/get_user/user@example.com + lastPart := pathParts[len(pathParts)-1] + if _, err := strconv.Atoi(lastPart); err == nil { + // 是数字,当作ID处理 + idStr = lastPart + } else if strings.Contains(lastPart, "@") { + // 包含@,当作邮箱处理 + email = lastPart + } + } + } + + if idStr == "" && email == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{ + "success": "false", + "message": "必须提供用户ID或邮箱地址", + }) + return + } + + var userInfo map[string]interface{} + var err error + + if idStr != "" { + // 通过ID查询 + id, err := strconv.Atoi(idStr) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{ + "success": "false", + "message": "无效的用户ID", + }) + return + } + + userInfo, err = client.GetUserDetails( + id, + api.APIKeyAuth, // 默认使用API Key认证 + "", + "", + client.Config.Auth.APIKey, + ) + } else { + // 通过邮箱查询 + userInfo, err = client.GetUserByEmail( + email, + api.APIKeyAuth, // 默认使用API Key认证 + "", + "", + client.Config.Auth.APIKey, + ) + } + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{ + "success": "false", + "message": fmt.Sprintf("获取用户信息失败: %v", err), + }) + return + } + + // 返回响应 + response := GetUserResponse{ + User: userInfo, + Success: true, + Message: "获取用户信息成功", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// 获取邮件 +func getEmailsHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + var req GetEmailRequest + // 尝试解析请求体,但即使失败也继续处理 + _ = json.NewDecoder(r.Body).Decode(&req) + + // 检查是否提供了邮箱和密码参数 + if req.Email == "" || req.Password == "" { + // 也可以从URL参数获取 + req.Email = r.URL.Query().Get("email") + req.Password = r.URL.Query().Get("password") + + // 如果仍然为空,则返回错误 + if req.Email == "" || req.Password == "" { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{ + "success": "false", + "message": "邮箱地址和密码必须提供", + }) + return + } + } + + // 设置默认参数 + server := req.Server + if server == "" { + // 尝试从邮箱地址提取域名 + parts := strings.Split(req.Email, "@") + if len(parts) > 1 { + server = fmt.Sprintf("mail.%s", parts[1]) + } else { + server = "mail.evnmail.com" + } + } + + port := req.Port + if port == 0 { + port = 993 + } + + count := req.Count + if count == 0 { + count = 5 // 默认获取5封邮件 + } + + // 获取邮件 + emails, err := api.GetLatestEmails( + req.Email, + req.Password, + server, + port, + true, // 使用SSL + count, + ) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{ + "success": "false", + "message": fmt.Sprintf("获取邮件失败: %v", err), + }) + return + } + + // 返回响应 + response := EmailResponse{ + Emails: emails, + Success: true, + Message: fmt.Sprintf("成功获取 %d 封邮件", len(emails)), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// 启动Web服务器 +func main() { + // 初始化随机数生成器 + rand.Seed(time.Now().UnixNano()) + + // 解析命令行参数 + portPtr := flag.Int("p", 8080, "服务器端口") + configPtr := flag.String("c", "", "配置文件路径") + + flag.Parse() + + // 加载配置 + config, err := api.LoadConfig(*configPtr) + if err != nil { + fmt.Printf("加载配置失败: %v\n", err) + os.Exit(1) + } + + // 创建客户端 + client = api.NewClient(config) + + // 设置路由 - 不再使用authMiddleware + http.HandleFunc("/api/create_user", createUserHandler) + http.HandleFunc("/api/simple_user", createSimpleUserHandler) + http.HandleFunc("/api/mock_user", createMockUserHandler) + http.HandleFunc("/api/list_users", listUsersHandler) + http.HandleFunc("/api/get_user", getUserHandler) + http.HandleFunc("/api/get_emails", getEmailsHandler) + + // 添加一个简单的健康检查端点 + http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) + + // 添加一个简单的根路径处理 + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte("Stalwart 邮件 API 服务运行中")) + }) + + // 启动服务器 + port := *portPtr + addr := fmt.Sprintf(":%d", port) + fmt.Printf("启动Web服务器,监听端口: %d\n", port) + fmt.Printf("API端点 (无需认证):\n") + fmt.Printf(" - POST /api/create_user (创建用户,可自定义参数)\n") + fmt.Printf(" - GET /api/simple_user (创建随机用户,无需任何参数)\n") + fmt.Printf(" - GET /api/mock_user (创建模拟用户,仅本地数据)\n") + fmt.Printf(" - GET /api/list_users (获取用户列表)\n") + fmt.Printf(" - GET /api/get_user (获取用户信息)\n") + fmt.Printf(" - POST /api/get_emails (获取用户邮件)\n") + + if err := http.ListenAndServe(addr, nil); err != nil { + log.Fatalf("启动服务器失败: %v", err) + } +} diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..d14311a --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,11 @@ +api: + base_url: https://mail.evnmail.com/api + disable_proxy: true +user: + default_domain: evnmail.com + default_quota: 10737418 +auth: + basic: + username: admin + password: admin + api_key: api_c3RlYW1yZWcyOnVBVWJmT0xMNElOQWJERGNWTm1aNTRuSk5JZUJsUw== \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..49cc0da --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module stalwart_client + +go 1.24 + +require ( + github.com/emersion/go-imap v1.2.1 + github.com/emersion/go-message v0.17.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect + github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect + golang.org/x/text v0.12.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7ea49de --- /dev/null +++ b/go.sum @@ -0,0 +1,46 @@ +github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= +github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= +github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= +github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04= +github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= +github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= +github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= +github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go_stalwart_client.zip b/go_stalwart_client.zip new file mode 100644 index 0000000..0b5b838 Binary files /dev/null and b/go_stalwart_client.zip differ diff --git a/pkg/api/config.go b/pkg/api/config.go new file mode 100644 index 0000000..63c599f --- /dev/null +++ b/pkg/api/config.go @@ -0,0 +1,90 @@ +package api + +import ( + "fmt" + "io/ioutil" + "os" + + "gopkg.in/yaml.v3" +) + +// Config 保存应用程序配置 +type Config struct { + API struct { + BaseURL string `yaml:"base_url"` + DisableProxy bool `yaml:"disable_proxy"` + } `yaml:"api"` + User struct { + DefaultDomain string `yaml:"default_domain"` + DefaultPassword string `yaml:"default_password"` + DefaultQuota int64 `yaml:"default_quota"` + } `yaml:"user"` + Auth struct { + Basic struct { + AdminUsername string `yaml:"username"` + AdminPassword string `yaml:"password"` + } `yaml:"basic"` + APIKey string `yaml:"api_key"` + } `yaml:"auth"` +} + +// LoadConfig 从指定路径加载配置文件 +func LoadConfig(path string) (*Config, error) { + // 如果未指定路径,使用默认路径 + if path == "" { + // 尝试在当前目录和上一级目录查找配置文件 + candidates := []string{ + "config.yaml", + "config/config.yaml", + "../config/config.yaml", + } + + for _, candidate := range candidates { + if _, err := os.Stat(candidate); err == nil { + path = candidate + break + } + } + + if path == "" { + return nil, fmt.Errorf("未找到配置文件") + } + } + + // 读取配置文件 + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("读取配置文件失败: %w", err) + } + + // 解析YAML + config := &Config{} + if err := yaml.Unmarshal(data, config); err != nil { + return nil, fmt.Errorf("解析配置文件失败: %w", err) + } + + // 设置默认值 + if config.API.BaseURL == "" { + config.API.BaseURL = "https://mail.evnmail.com/api" + } + if config.User.DefaultDomain == "" { + config.User.DefaultDomain = "evnmail.com" + } + if config.User.DefaultPassword == "" { + config.User.DefaultPassword = "123456" + } + if config.User.DefaultQuota == 0 { + config.User.DefaultQuota = 1073741824 // 1GB + } + + fmt.Printf("已从 %s 加载配置\n", path) + return config, nil +} + +// DisableProxy 禁用代理设置 +func DisableProxy() { + os.Setenv("HTTP_PROXY", "") + os.Setenv("HTTPS_PROXY", "") + os.Setenv("http_proxy", "") + os.Setenv("https_proxy", "") +} diff --git a/pkg/api/email_api.go b/pkg/api/email_api.go new file mode 100644 index 0000000..9d485a3 --- /dev/null +++ b/pkg/api/email_api.go @@ -0,0 +1,866 @@ +package api + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "strings" + "time" +) + +// 初始化随机数生成器 +func init() { + rand.Seed(time.Now().UnixNano()) +} + +// Client 表示Stalwart邮件API客户端 +type Client struct { + Config *Config + APIBaseURL string +} + +// NewClient 创建新的API客户端 +func NewClient(config *Config) *Client { + client := &Client{ + Config: config, + APIBaseURL: config.API.BaseURL, + } + + if config.API.DisableProxy { + DisableProxy() + } + + return client +} + +// AuthType 认证类型 +type AuthType string + +const ( + // BasicAuth 表示基本认证 + BasicAuth AuthType = "basic" + // APIKeyAuth 表示API Key认证 + APIKeyAuth AuthType = "apikey" +) + +// User 表示用户信息 +type User struct { + ID int `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Description string `json:"description"` + Quota int64 `json:"quota"` + Emails []string `json:"emails"` + MemberOf []string `json:"memberOf"` + Roles []string `json:"roles"` + Secrets []string `json:"secrets"` +} + +// UserList 表示用户列表响应 +type UserList struct { + Data struct { + Items []User `json:"items"` + Total int `json:"total"` + } `json:"data"` +} + +// APIResponse 表示通用API响应 +type APIResponse struct { + Data json.RawMessage `json:"data"` + Error string `json:"error"` + Message string `json:"message"` + Field string `json:"field"` + Value string `json:"value"` +} + +// GenerateRandomUsername 生成随机用户名 +func GenerateRandomUsername(length int) string { + // 常见的英文名 + firstNames := []string{ + "alex", "bob", "chris", "david", "eric", "frank", "gary", "henry", + "ian", "jack", "kevin", "leo", "mike", "nick", "oliver", "peter", + "ryan", "sam", "tom", "victor", "william", "zack", + "anna", "betty", "cathy", "diana", "emma", "fiona", "grace", "helen", + "irene", "jane", "kate", "lily", "mary", "nina", "olivia", "penny", + "queen", "rose", "sarah", "tina", "uma", "vicky", "wendy", "zoe", + } + + // 常见的姓氏 + lastNames := []string{ + "smith", "johnson", "williams", "jones", "brown", "davis", "miller", + "wilson", "taylor", "clark", "hall", "lee", "allen", "young", "king", + "wright", "hill", "scott", "green", "adams", "baker", "carter", "cook", + } + + // 随机选择名和姓 + firstName := firstNames[rand.Intn(len(firstNames))] + lastName := lastNames[rand.Intn(len(lastNames))] + + // 添加1-999的随机数字 + number := rand.Intn(999) + 1 + + // 组合成用户名 - 不使用特殊字符,直接连接 + username := fmt.Sprintf("%s%s%d", firstName, lastName, number) + + // 如果指定了长度且用户名太长,就截断 + if length > 0 && len(username) > length { + username = username[:length] + } + + return username +} + +// GenerateRandomPassword 生成随机密码 +func GenerateRandomPassword(length int) string { + if length <= 0 { + length = 10 // 默认10位密码 + } + + // 简单词汇列表 + words := []string{ + "apple", "blue", "cat", "dog", "easy", "fish", "good", "home", + "idea", "jump", "king", "love", "moon", "nice", "open", "park", + "queen", "red", "star", "time", "user", "view", "work", "year", + } + + // 随机选择一个词 + word := words[rand.Intn(len(words))] + + // 确保词不超过长度限制的一半 + if len(word) > length/2 { + word = word[:length/2] + } + + // 为剩余长度生成数字和特殊字符 + remainingLength := length - len(word) + numbers := "0123456789" + specials := "@#$%&*!" + + var password strings.Builder + password.WriteString(word) + + // 添加至少一个特殊字符 + password.WriteByte(specials[rand.Intn(len(specials))]) + remainingLength-- + + // 添加至少一个数字 + password.WriteByte(numbers[rand.Intn(len(numbers))]) + remainingLength-- + + // 填充剩余长度 + for i := 0; i < remainingLength; i++ { + // 使用大小写字母、数字组合 + allChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + password.WriteByte(allChars[rand.Intn(len(allChars))]) + } + + // 将密码字符打乱顺序 + passwordBytes := []byte(password.String()) + rand.Shuffle(len(passwordBytes), func(i, j int) { + passwordBytes[i], passwordBytes[j] = passwordBytes[j], passwordBytes[i] + }) + + return string(passwordBytes) +} + +// ParseEmail 解析邮箱地址 +func (c *Client) ParseEmail(email string) (string, string) { + parts := strings.Split(email, "@") + if len(parts) == 2 { + return parts[0], parts[1] + } + return email, c.Config.User.DefaultDomain +} + +// PrepareUserData 准备用户数据 +func (c *Client) PrepareUserData(email, description string, password string, quota int64) map[string]interface{} { + if password == "" { + // 不再使用默认密码,而是生成随机密码 + password = GenerateRandomPassword(12) + } + + if quota == 0 { + quota = c.Config.User.DefaultQuota + } + + if description == "" { + description = fmt.Sprintf("%s account", email) + } + + // 准备用户数据 + return map[string]interface{}{ + "type": "individual", + "name": email, + "description": description, + "quota": quota, + "emails": []string{email}, + "memberOf": []string{}, + "roles": []string{"user"}, + "secrets": []string{password}, + } +} + +// GetAuthHeaders 获取认证头 +func (c *Client) GetAuthHeaders(authType AuthType, username, password, apiKey string) map[string]string { + headers := map[string]string{ + "Accept": "application/json", + "Content-Type": "application/json", + } + + if authType == APIKeyAuth { + // API Key认证 + if apiKey == "" { + apiKey = c.Config.Auth.APIKey + } + + fmt.Printf("使用的API Key: %s\n", apiKey) + + // 检查是否已经是完整的授权头 + if strings.HasPrefix(apiKey, "Bearer ") { + headers["Authorization"] = apiKey + } else if strings.HasPrefix(apiKey, "api_") { + // 使用标准格式 api_XXX + headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) + fmt.Println("使用标准API Key格式: Bearer api_XXX") + } else { + // 可能是纯Token,尝试直接使用 + headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) + fmt.Println("使用Token格式: Bearer XXX") + } + } else { + // 基本认证 + if username == "" { + username = c.Config.Auth.Basic.AdminUsername + } + if password == "" { + password = c.Config.Auth.Basic.AdminPassword + } + + auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", username, password))) + headers["Authorization"] = fmt.Sprintf("Basic %s", auth) + } + + fmt.Println("使用的认证头:", headers["Authorization"]) + return headers +} + +// MakeAPIRequest 发送API请求 +func (c *Client) MakeAPIRequest(method, endpoint string, data interface{}, headers map[string]string) (*http.Response, []byte, error) { + url := fmt.Sprintf("%s/%s", c.APIBaseURL, strings.TrimPrefix(endpoint, "/")) + fmt.Printf("发送请求: %s %s\n", method, url) + + if headers["Authorization"] != "" { + authType := strings.Split(headers["Authorization"], " ")[0] + fmt.Printf("认证方式: %s\n", authType) + } + + var reqBody []byte + var err error + if data != nil { + reqBody, err = json.Marshal(data) + if err != nil { + return nil, nil, fmt.Errorf("序列化请求数据失败: %w", err) + } + fmt.Printf("请求数据: %s\n", string(reqBody)) + } + + req, err := http.NewRequest(method, url, bytes.NewBuffer(reqBody)) + if err != nil { + return nil, nil, fmt.Errorf("创建HTTP请求失败: %w", err) + } + + // 设置请求头 + for key, value := range headers { + req.Header.Set(key, value) + } + + // 发送请求 + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("发送HTTP请求失败: %w", err) + } + + // 读取响应 + respBody, err := ioutil.ReadAll(resp.Body) + defer resp.Body.Close() + if err != nil { + return resp, nil, fmt.Errorf("读取响应失败: %w", err) + } + + fmt.Printf("响应状态码: %d\n", resp.StatusCode) + fmt.Printf("响应内容: %s\n", string(respBody)) + + // 检查认证错误 + if resp.StatusCode == 401 || resp.StatusCode == 403 { + fmt.Println("认证失败,可能需要尝试其他认证方式") + } + + return resp, respBody, nil +} + +// CreateUser 创建邮箱用户 +func (c *Client) CreateUser(email, password, description string, quota int64, authType AuthType, username, authPassword, apiKey string) (int, map[string]interface{}, error) { + // 准备用户数据 + userData := c.PrepareUserData(email, description, password, quota) + + fmt.Printf("创建用户 %s 并设置密码...\n", email) + + // 获取认证头 + headers := c.GetAuthHeaders(authType, username, authPassword, apiKey) + + // 创建用户 + resp, respBody, err := c.MakeAPIRequest("POST", "/principal", userData, headers) + if err != nil { + return 0, nil, fmt.Errorf("创建用户请求失败: %w", err) + } + + if resp == nil { + return 0, nil, fmt.Errorf("创建用户失败:未收到响应") + } + + // 解析响应 + var apiResp APIResponse + if err := json.Unmarshal(respBody, &apiResp); err != nil { + return 0, nil, fmt.Errorf("解析响应失败: %w", err) + } + + // 检查是否返回错误 + if apiResp.Error != "" { + // 特殊处理:如果用户已存在,可以尝试获取已存在用户的ID + if apiResp.Error == "fieldAlreadyExists" && apiResp.Field == "name" { + existingName := apiResp.Value + fmt.Printf("用户 %s 已存在,尝试获取现有用户信息...\n", existingName) + + // 构造一个模拟信息 + userInfo := map[string]interface{}{ + "name": email, + "emails": []string{email}, + "description": description, + "quota": quota, + "_note": "This user already exists", + } + + // 返回一个特殊标记的ID(负数)表示这是一个已存在的用户 + return -1, userInfo, nil + } + + return 0, nil, fmt.Errorf("创建用户失败: %s - %s", apiResp.Error, apiResp.Message) + } + + // 获取用户ID + var userID int + if err := json.Unmarshal(apiResp.Data, &userID); err != nil { + // 尝试解析为对象 + var objData map[string]interface{} + if err := json.Unmarshal(apiResp.Data, &objData); err != nil { + return 0, nil, fmt.Errorf("解析用户ID失败: %w", err) + } + + // 检查是否有id字段 + if id, ok := objData["id"].(float64); ok { + userID = int(id) + } else { + return 0, nil, fmt.Errorf("未能从响应中获取用户ID") + } + } + + if userID == 0 { + return 0, nil, fmt.Errorf("创建用户失败:未获取到用户ID") + } + + fmt.Printf("用户创建成功!ID: %d\n", userID) + + // 尝试获取用户详情进行验证 + userInfo, err := c.GetUserDetails(userID, authType, username, authPassword, apiKey) + if err != nil || userInfo == nil { + fmt.Println("获取用户详情失败,但用户创建成功") + // 创建一个包含基本信息的对象 + userInfo = map[string]interface{}{ + "name": email, + "emails": []string{email}, + "description": description, + "quota": quota, + "_note": "This is a synthetic record as actual details could not be retrieved", + } + } + + return userID, userInfo, nil +} + +// GetUserDetails 获取用户详情 +func (c *Client) GetUserDetails(userID int, authType AuthType, username, authPassword, apiKey string) (map[string]interface{}, error) { + fmt.Println("\n获取用户详情...") + + // 获取认证头 + headers := c.GetAuthHeaders(authType, username, authPassword, apiKey) + + // 获取用户详情 + endpoint := fmt.Sprintf("/principal/%d", userID) + resp, respBody, err := c.MakeAPIRequest("GET", endpoint, nil, headers) + if err != nil { + return nil, fmt.Errorf("获取用户详情请求失败: %w", err) + } + + if resp == nil || resp.StatusCode != 200 { + return nil, fmt.Errorf("获取用户详情失败") + } + + // 解析响应 + var apiResp APIResponse + if err := json.Unmarshal(respBody, &apiResp); err != nil { + return nil, fmt.Errorf("解析响应失败: %w", err) + } + + // 检查是否返回错误 + if apiResp.Error != "" { + return nil, fmt.Errorf("获取用户详情失败: %s - %s", apiResp.Error, apiResp.Message) + } + + // 解析用户数据 + var userData map[string]interface{} + if err := json.Unmarshal(apiResp.Data, &userData); err != nil { + // 如果无法解析为对象,直接返回整个响应 + var rawResp map[string]interface{} + if err := json.Unmarshal(respBody, &rawResp); err != nil { + return nil, fmt.Errorf("解析用户数据失败: %w", err) + } + return rawResp, nil + } + + if userData == nil { + return nil, fmt.Errorf("获取用户详情失败:响应中没有用户数据") + } + + // 打印关键信息 + fmt.Printf("用户名: %v\n", userData["name"]) + emails, _ := userData["emails"].([]interface{}) + if len(emails) > 0 { + fmt.Printf("邮箱: %v\n", emails[0]) + } + + return userData, nil +} + +// ListUsers 获取用户列表 +func (c *Client) ListUsers(authType AuthType, username, authPassword, apiKey string, limit, page int) (*UserList, error) { + fmt.Println("获取用户列表...") + + // 获取认证头 + headers := c.GetAuthHeaders(authType, username, authPassword, apiKey) + + // 构建查询参数 + endpoint := fmt.Sprintf("/principal?limit=%d&page=%d", limit, page) + + // 尝试不同的API端点路径 + endpoints := []string{ + endpoint, // 根据规范文档的主要端点 + "/principals", // 原始尝试 + "/users", // 通用users格式 + "/user", // 单数形式 + "/accounts", // 有些系统用accounts + "/directory", // 目录形式 + } + + var resp *http.Response + var respBody []byte + var err error + var success bool + + for _, ep := range endpoints { + fmt.Printf("尝试端点: %s\n", ep) + resp, respBody, err = c.MakeAPIRequest("GET", ep, nil, headers) + if err == nil && resp != nil && resp.StatusCode == 200 { + fmt.Printf("成功的端点: %s\n", ep) + success = true + break + } + } + + if !success { + return nil, fmt.Errorf("获取用户列表失败,尝试了所有可能的端点") + } + + // 解析响应 + fmt.Println("响应数据结构:") + fmt.Println(string(respBody)) + + var userList UserList + if err := json.Unmarshal(respBody, &userList); err != nil { + // 尝试解析为不同格式 + var rawResp map[string]interface{} + if err := json.Unmarshal(respBody, &rawResp); err != nil { + return nil, fmt.Errorf("解析用户列表失败: %w", err) + } + + // 尝试不同的数据格式 + users := []User{} + + // 检查data.items格式 + if data, ok := rawResp["data"].(map[string]interface{}); ok { + if items, ok := data["items"].([]interface{}); ok { + for _, item := range items { + if user, ok := item.(map[string]interface{}); ok { + userObj := parseUserObject(user) + users = append(users, userObj) + } + } + userList.Data.Items = users + userList.Data.Total = len(users) + return &userList, nil + } + } + + // 检查data数组格式 + if data, ok := rawResp["data"].([]interface{}); ok { + for _, item := range data { + if user, ok := item.(map[string]interface{}); ok { + userObj := parseUserObject(user) + users = append(users, userObj) + } + } + userList.Data.Items = users + userList.Data.Total = len(users) + return &userList, nil + } + + // 检查principals数组格式 + if principals, ok := rawResp["principals"].([]interface{}); ok { + for _, item := range principals { + if user, ok := item.(map[string]interface{}); ok { + userObj := parseUserObject(user) + users = append(users, userObj) + } + } + userList.Data.Items = users + userList.Data.Total = len(users) + return &userList, nil + } + + // 检查users数组格式 + if usersArray, ok := rawResp["users"].([]interface{}); ok { + for _, item := range usersArray { + if user, ok := item.(map[string]interface{}); ok { + userObj := parseUserObject(user) + users = append(users, userObj) + } + } + userList.Data.Items = users + userList.Data.Total = len(users) + return &userList, nil + } + + return nil, fmt.Errorf("未能从响应中提取用户列表") + } + + fmt.Printf("找到 %d 个用户\n", len(userList.Data.Items)) + return &userList, nil +} + +// parseUserObject 解析用户对象 +func parseUserObject(user map[string]interface{}) User { + userObj := User{} + + // ID + if id, ok := user["id"].(float64); ok { + userObj.ID = int(id) + } + + // Type + if typ, ok := user["type"].(string); ok { + userObj.Type = typ + } + + // Name + if name, ok := user["name"].(string); ok { + userObj.Name = name + } + + // Description + if desc, ok := user["description"].(string); ok { + userObj.Description = desc + } + + // Quota + if quota, ok := user["quota"].(float64); ok { + userObj.Quota = int64(quota) + } + + // Emails + if emails, ok := user["emails"].([]interface{}); ok { + for _, email := range emails { + if e, ok := email.(string); ok { + userObj.Emails = append(userObj.Emails, e) + } + } + } else if email, ok := user["emails"].(string); ok { + userObj.Emails = []string{email} + } + + // MemberOf + if memberOf, ok := user["memberOf"].([]interface{}); ok { + for _, member := range memberOf { + if m, ok := member.(string); ok { + userObj.MemberOf = append(userObj.MemberOf, m) + } + } + } + + // Roles + if roles, ok := user["roles"].([]interface{}); ok { + for _, role := range roles { + if r, ok := role.(string); ok { + userObj.Roles = append(userObj.Roles, r) + } + } + } + + return userObj +} + +// GetUserByEmail 通过邮箱地址查询用户信息 +func (c *Client) GetUserByEmail(email string, authType AuthType, username, authPassword, apiKey string) (map[string]interface{}, error) { + fmt.Printf("通过邮箱查询用户: %s\n", email) + + // 获取所有用户,然后过滤 + userList, err := c.ListUsers(authType, username, authPassword, apiKey, 1000, 1) + if err != nil { + return nil, err + } + + // 尝试在列表中查找匹配的邮箱 + for _, user := range userList.Data.Items { + // 检查用户邮箱是否匹配 + for _, userEmail := range user.Emails { + if userEmail == email { + fmt.Printf("找到匹配的用户,ID: %d\n", user.ID) + // 获取详细信息 + return c.GetUserDetails(user.ID, authType, username, authPassword, apiKey) + } + } + if user.Name == email { + fmt.Printf("找到匹配的用户,ID: %d\n", user.ID) + // 获取详细信息 + return c.GetUserDetails(user.ID, authType, username, authPassword, apiKey) + } + } + + fmt.Printf("未找到邮箱为 %s 的用户\n", email) + return nil, fmt.Errorf("未找到用户") +} + +// FormatUserList 格式化输出用户列表 +func FormatUserList(userList *UserList) { + if userList == nil || len(userList.Data.Items) == 0 { + fmt.Println("没有用户数据可显示") + return + } + + fmt.Println("\n=== 用户列表 ===") + fmt.Printf("找到 %d 个用户\n", len(userList.Data.Items)) + fmt.Println(strings.Repeat("-", 60)) + fmt.Printf("%-10s %-30s %-15s %-30s\n", "ID", "名称", "类型", "邮箱") + fmt.Println(strings.Repeat("-", 60)) + + for _, user := range userList.Data.Items { + email := "N/A" + if len(user.Emails) > 0 { + email = user.Emails[0] + } + + // 截断过长的字符串 + name := user.Name + if len(name) > 28 { + name = name[:28] + } + if len(email) > 28 { + email = email[:28] + } + + fmt.Printf("%-10d %-30s %-15s %-30s\n", user.ID, name, user.Type, email) + } + + fmt.Println(strings.Repeat("-", 60)) +} + +// FormatUserDetails 格式化输出单个用户的详细信息 +func FormatUserDetails(userInfo map[string]interface{}) { + if userInfo == nil { + fmt.Println("没有用户数据可显示") + return + } + + fmt.Println("\n=== 用户详细信息 ===") + fmt.Println(strings.Repeat("-", 60)) + + // 获取基本信息 + id, _ := userInfo["id"] + name, _ := userInfo["name"] + description, _ := userInfo["description"] + typ, _ := userInfo["type"] + quota, _ := userInfo["quota"] + + // 显示类型转换 + var quotaFloat float64 + switch q := quota.(type) { + case float64: + quotaFloat = q + case int64: + quotaFloat = float64(q) + case int: + quotaFloat = float64(q) + } + + quotaMB := quotaFloat / (1024 * 1024) + + fmt.Printf("ID: %v\n", id) + fmt.Printf("名称: %v\n", name) + fmt.Printf("描述: %v\n", description) + fmt.Printf("类型: %v\n", typ) + fmt.Printf("配额: %.2f MB (%.0f 字节)\n", quotaMB, quotaFloat) + + // 获取邮箱 + if emails, ok := userInfo["emails"].([]interface{}); ok && len(emails) > 0 { + fmt.Println("\n邮箱地址:") + for _, email := range emails { + fmt.Printf(" - %v\n", email) + } + } else if email, ok := userInfo["emails"].(string); ok { + fmt.Println("\n邮箱地址:") + fmt.Printf(" - %s\n", email) + } + + // 获取成员组 + if memberOf, ok := userInfo["memberOf"].([]interface{}); ok && len(memberOf) > 0 { + fmt.Println("\n所属组:") + for _, group := range memberOf { + fmt.Printf(" - %v\n", group) + } + } + + // 其他属性 + fmt.Println("\n其他属性:") + for key, value := range userInfo { + if key != "id" && key != "name" && key != "description" && key != "type" && key != "quota" && key != "emails" && key != "memberOf" { + // 跳过密码相关字段 + if strings.Contains(strings.ToLower(key), "secret") || strings.Contains(strings.ToLower(key), "password") { + continue + } + fmt.Printf(" - %s: %v\n", key, value) + } + } + + fmt.Println(strings.Repeat("-", 60)) +} + +// CreateEmailUser 创建邮箱用户的统一入口 +func (c *Client) CreateEmailUser(emailAddress, password, description string, quota int64, authType AuthType, username, authPassword, apiKey string) (int, string, string, string, error) { + // 如果没有提供邮箱地址,则生成随机用户名 + if emailAddress == "" { + randomUsername := GenerateRandomUsername(8) + // 确保用户名只包含字母和数字 + for i := 0; i < len(randomUsername); i++ { + if !((randomUsername[i] >= 'a' && randomUsername[i] <= 'z') || + (randomUsername[i] >= '0' && randomUsername[i] <= '9')) { + // 替换为字母 + randomUsername = randomUsername[:i] + "x" + randomUsername[i+1:] + } + } + emailAddress = fmt.Sprintf("%s@%s", randomUsername, c.Config.User.DefaultDomain) + } + + // 解析邮箱地址 + usernameLocal, domain := c.ParseEmail(emailAddress) + + // 确保用户名只包含字母和数字 + cleanUsername := "" + for i := 0; i < len(usernameLocal); i++ { + if (usernameLocal[i] >= 'a' && usernameLocal[i] <= 'z') || + (usernameLocal[i] >= '0' && usernameLocal[i] <= '9') { + cleanUsername += string(usernameLocal[i]) + } else { + cleanUsername += "x" // 替换非法字符为x + } + } + usernameLocal = cleanUsername + + email := fmt.Sprintf("%s@%s", usernameLocal, domain) + + // 如果没有提供密码,生成随机密码 + if password == "" { + password = GenerateRandomPassword(12) + fmt.Println("已生成随机密码") + } + + // 设置认证类型消息 + if authType == APIKeyAuth { + // 从API Key中提取用户名用于显示 + if apiKey != "" && strings.HasPrefix(apiKey, "api_") { + decoded, err := base64.StdEncoding.DecodeString(apiKey[4:]) + if err == nil { + decodedStr := string(decoded) + if strings.Contains(decodedStr, ":") { + parts := strings.SplitN(decodedStr, ":", 2) + fmt.Printf("使用API Key认证 (用户: %s)\n", parts[0]) + } else { + fmt.Println("使用API Key认证") + } + } else { + fmt.Println("使用API Key认证") + } + } else { + fmt.Println("使用API Key认证") + } + } else { + actualUsername := username + if actualUsername == "" { + actualUsername = c.Config.Auth.Basic.AdminUsername + } + fmt.Printf("使用基本认证 (用户: %s)\n", actualUsername) + } + + // 创建用户 + userID, _, err := c.CreateUser(email, password, description, quota, authType, username, authPassword, apiKey) + if err != nil { + return 0, "", "", "", err + } + + // 特殊处理:如果用户已存在(用户ID为负数) + if userID < 0 { + fmt.Printf("用户 %s 已存在,使用现有用户信息\n", email) + userID = -userID // 转为正数便于显示 + } + + // 返回用户信息 + return userID, usernameLocal, email, password, nil +} + +// PrintUserResult 打印用户创建结果 +func PrintUserResult(userID int, username, email, password string) bool { + if userID == 0 { + fmt.Println("\n创建用户失败") + return false + } + + domainParts := strings.Split(email, "@") + domain := "" + if len(domainParts) > 1 { + domain = domainParts[1] + } + + fmt.Println("\n=== 新创建的用户信息 ===") + fmt.Printf("用户ID: %d\n", userID) + fmt.Printf("登录名: %s\n", username) + fmt.Printf("邮箱地址: %s\n", email) + fmt.Printf("密码: %s\n", password) + fmt.Printf("\n登录信息:\n") + fmt.Printf(" - 登录名: %s\n", username) + fmt.Printf(" - 密码: %s\n", password) + fmt.Printf(" - 邮箱服务器: mail.%s\n", domain) + + return true +} diff --git a/pkg/api/email_fetch.go b/pkg/api/email_fetch.go new file mode 100644 index 0000000..0177005 --- /dev/null +++ b/pkg/api/email_fetch.go @@ -0,0 +1,253 @@ +package api + +import ( + "fmt" + "io" + "io/ioutil" + "strings" + "time" + + "github.com/emersion/go-imap" + "github.com/emersion/go-imap/client" + "github.com/emersion/go-message/mail" +) + +// EmailMessage 表示一封邮件 +type EmailMessage struct { + ID string + Subject string + From string + To string + Date time.Time + Body string + MimeType string +} + +// GetLatestEmails 获取指定邮箱的最新邮件 +func GetLatestEmails(emailAddr, password, server string, port int, useSSL bool, numEmails int) ([]*EmailMessage, error) { + fmt.Printf("正在连接到 %s:%d...\n", server, port) + + // 连接到服务器 + var c *client.Client + var err error + + if useSSL { + c, err = client.DialTLS(fmt.Sprintf("%s:%d", server, port), nil) + } else { + c, err = client.Dial(fmt.Sprintf("%s:%d", server, port)) + } + + if err != nil { + return nil, fmt.Errorf("连接到IMAP服务器失败: %w", err) + } + defer c.Logout() + + // 登录 + fmt.Printf("正在登录 %s...\n", emailAddr) + if err := c.Login(emailAddr, password); err != nil { + return nil, fmt.Errorf("登录失败: %w", err) + } + + // 选择收件箱 + mbox, err := c.Select("INBOX", false) + if err != nil { + return nil, fmt.Errorf("选择收件箱失败: %w", err) + } + + if mbox.Messages == 0 { + fmt.Println("收件箱为空") + return nil, nil + } + + // 计算要获取的消息范围 + from := uint32(1) + if mbox.Messages > uint32(numEmails) { + from = mbox.Messages - uint32(numEmails) + 1 + } + to := mbox.Messages + + // 创建搜索条件 + criteria := imap.NewSearchCriteria() + criteria.WithoutFlags = []string{imap.DeletedFlag} + if from > 1 { + criteria.Uid = new(imap.SeqSet) + criteria.Uid.AddRange(from, to) + } + + // 搜索消息 + ids, err := c.Search(criteria) + if err != nil { + return nil, fmt.Errorf("搜索邮件失败: %w", err) + } + + if len(ids) == 0 { + fmt.Println("未找到符合条件的邮件") + return nil, nil + } + + // 限制获取的邮件数量 + if len(ids) > numEmails { + ids = ids[len(ids)-numEmails:] + } + + // 创建序列集 + seqSet := new(imap.SeqSet) + seqSet.AddNum(ids...) + + // 设置获取项 + items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchBodyStructure, imap.FetchFlags, imap.FetchRFC822Text, imap.FetchUid} + + // 获取消息 + messages := make(chan *imap.Message, 10) + done := make(chan error, 1) + go func() { + done <- c.Fetch(seqSet, items, messages) + }() + + // 处理消息 + var emails []*EmailMessage + for msg := range messages { + email := &EmailMessage{ + ID: fmt.Sprintf("%d", msg.Uid), + } + + if msg.Envelope != nil { + email.Subject = msg.Envelope.Subject + if len(msg.Envelope.From) > 0 { + email.From = formatAddress(msg.Envelope.From[0]) + } + if len(msg.Envelope.To) > 0 { + email.To = formatAddress(msg.Envelope.To[0]) + } + email.Date = msg.Envelope.Date + } + + // 获取正文 + var section imap.BodySectionName + section.Specifier = imap.TextSpecifier + + r := msg.GetBody(§ion) + if r == nil { + // 尝试获取全部内容 + section = imap.BodySectionName{} + r = msg.GetBody(§ion) + if r == nil { + continue + } + } + + // 处理正文 + body, err := extractBody(r) + if err != nil { + fmt.Printf("提取邮件正文失败: %v\n", err) + continue + } + + email.Body = body + emails = append(emails, email) + + fmt.Printf("已获取邮件ID: %s\n", email.ID) + } + + if err := <-done; err != nil { + return nil, fmt.Errorf("获取邮件失败: %w", err) + } + + // 反转列表,让最新的邮件在前面 + for i, j := 0, len(emails)-1; i < j; i, j = i+1, j-1 { + emails[i], emails[j] = emails[j], emails[i] + } + + return emails, nil +} + +// formatAddress 格式化地址 +func formatAddress(addr *imap.Address) string { + if addr == nil { + return "" + } + + if addr.PersonalName != "" { + return fmt.Sprintf("%s <%s@%s>", addr.PersonalName, addr.MailboxName, addr.HostName) + } + return fmt.Sprintf("%s@%s", addr.MailboxName, addr.HostName) +} + +// extractBody 提取邮件正文 +func extractBody(r io.Reader) (string, error) { + if r == nil { + return "", fmt.Errorf("无邮件内容") + } + + // 尝试解析为邮件 + mr, err := mail.CreateReader(r) + if err != nil { + // 如果无法解析为邮件,直接读取内容 + data, err := ioutil.ReadAll(r) + if err != nil { + return "", fmt.Errorf("读取邮件内容失败: %w", err) + } + return string(data), nil + } + + var textParts []string + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + return "", fmt.Errorf("读取邮件部分失败: %w", err) + } + + switch h := p.Header.(type) { + case *mail.InlineHeader: + contentType, _, _ := h.ContentType() + if strings.HasPrefix(contentType, "text/plain") { + data, err := ioutil.ReadAll(p.Body) + if err != nil { + return "", fmt.Errorf("读取文本部分失败: %w", err) + } + textParts = append(textParts, string(data)) + } + } + } + + if len(textParts) > 0 { + return strings.Join(textParts, "\n"), nil + } + + // 如果没有找到文本部分,尝试读取原始内容 + data, err := ioutil.ReadAll(r) + if err != nil { + return "", fmt.Errorf("读取原始内容失败: %w", err) + } + return string(data), nil +} + +// PrintEmails 打印邮件信息 +func PrintEmails(emails []*EmailMessage) { + if len(emails) == 0 { + fmt.Println("没有找到邮件") + return + } + + for i, email := range emails { + fmt.Println("\n" + strings.Repeat("=", 60)) + fmt.Printf("邮件 %d/%d\n", i+1, len(emails)) + fmt.Println(strings.Repeat("-", 60)) + fmt.Printf("主题: %s\n", email.Subject) + fmt.Printf("发件人: %s\n", email.From) + fmt.Printf("收件人: %s\n", email.To) + fmt.Printf("日期: %s\n", email.Date.Format(time.RFC1123Z)) + fmt.Println(strings.Repeat("-", 60)) + fmt.Println("邮件正文:") + + body := email.Body + if len(body) > 1000 { + body = body[:1000] + "..." + } + fmt.Println(body) + fmt.Println(strings.Repeat("=", 60)) + } +} diff --git a/当前新加坡西服务器运行得 b/当前新加坡西服务器运行得 new file mode 100644 index 0000000..e69de29