sdd
This commit is contained in:
201
WEB_API_DEPLOYMENT.md
Normal file
201
WEB_API_DEPLOYMENT.md
Normal 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 使用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请求失败
|
||||
- 检查请求格式和参数
|
||||
- 查看服务器日志获取详细错误信息
|
||||
64
cmd/create_user/main.go
Normal file
64
cmd/create_user/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
68
cmd/get_latest_email/main.go
Normal file
68
cmd/get_latest_email/main.go
Normal 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
75
cmd/get_user/main.go
Normal 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
58
cmd/list_users/main.go
Normal 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
530
cmd/web_server/main.go
Normal 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
11
config/config.yaml
Normal 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
15
go.mod
Normal 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
46
go.sum
Normal 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
BIN
go_stalwart_client.zip
Normal file
Binary file not shown.
90
pkg/api/config.go
Normal file
90
pkg/api/config.go
Normal 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
866
pkg/api/email_api.go
Normal 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
253
pkg/api/email_fetch.go
Normal 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(§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))
|
||||
}
|
||||
}
|
||||
0
当前新加坡西服务器运行得
Normal file
0
当前新加坡西服务器运行得
Normal file
Reference in New Issue
Block a user