This commit is contained in:
huangzhenpc
2025-05-23 23:08:02 +08:00
parent 9444bf4802
commit a8d25054fc
14 changed files with 2277 additions and 0 deletions

201
WEB_API_DEPLOYMENT.md Normal file
View File

@@ -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 使用NSSMNon-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请求失败
- 检查请求格式和参数
- 查看服务器日志获取详细错误信息

64
cmd/create_user/main.go Normal file
View File

@@ -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)
}
}

View File

@@ -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)
}

75
cmd/get_user/main.go Normal file
View File

@@ -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)
}

58
cmd/list_users/main.go Normal file
View File

@@ -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)
}

530
cmd/web_server/main.go Normal file
View File

@@ -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)
}
}

11
config/config.yaml Normal file
View File

@@ -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==

15
go.mod Normal file
View File

@@ -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
)

46
go.sum Normal file
View File

@@ -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=

BIN
go_stalwart_client.zip Normal file

Binary file not shown.

90
pkg/api/config.go Normal file
View File

@@ -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", "")
}

866
pkg/api/email_api.go Normal file
View File

@@ -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
}

253
pkg/api/email_fetch.go Normal file
View File

@@ -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(&section)
if r == nil {
// 尝试获取全部内容
section = imap.BodySectionName{}
r = msg.GetBody(&section)
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))
}
}

View File