From a8d25054fce12a56bea0a23bad67b0bee8570c5d Mon Sep 17 00:00:00 2001 From: huangzhenpc Date: Fri, 23 May 2025 23:08:02 +0800 Subject: [PATCH] sdd --- WEB_API_DEPLOYMENT.md | 201 ++++++++ cmd/create_user/main.go | 64 +++ cmd/get_latest_email/main.go | 68 +++ cmd/get_user/main.go | 75 +++ cmd/list_users/main.go | 58 +++ cmd/web_server/main.go | 530 +++++++++++++++++++++ config/config.yaml | 11 + go.mod | 15 + go.sum | 46 ++ go_stalwart_client.zip | Bin 0 -> 22171 bytes pkg/api/config.go | 90 ++++ pkg/api/email_api.go | 866 +++++++++++++++++++++++++++++++++++ pkg/api/email_fetch.go | 253 ++++++++++ 当前新加坡西服务器运行得 | 0 14 files changed, 2277 insertions(+) create mode 100644 WEB_API_DEPLOYMENT.md create mode 100644 cmd/create_user/main.go create mode 100644 cmd/get_latest_email/main.go create mode 100644 cmd/get_user/main.go create mode 100644 cmd/list_users/main.go create mode 100644 cmd/web_server/main.go create mode 100644 config/config.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 go_stalwart_client.zip create mode 100644 pkg/api/config.go create mode 100644 pkg/api/email_api.go create mode 100644 pkg/api/email_fetch.go create mode 100644 当前新加坡西服务器运行得 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 0000000000000000000000000000000000000000..0b5b8381bcec1990e24c232255a5b66da8f8a43a GIT binary patch literal 22171 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 Y$1fNO?$;3jpvS#dakHw=bPhiK50^7@WdHyG literal 0 HcmV?d00001 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