Implement internal mail skill MVP

This commit is contained in:
赵义仑
2026-06-06 22:28:36 +08:00
commit 097df73495
22 changed files with 4059 additions and 0 deletions

177
internal/config/config.go Normal file
View File

@@ -0,0 +1,177 @@
package config
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
type Config struct {
AccountID string
POP3 POP3Config
SMTP SMTPConfig
Data DataConfig
HTTP HTTPConfig
}
type POP3Config struct {
Host string
Port int
Username string
Password string
UseTLS bool
TimeoutSeconds int
}
type SMTPConfig struct {
Host string
Port int
Username string
Password string
UseTLS bool
UseSTARTTLS bool
AuthRequired bool
AllowInsecureAuth bool
FromAddress string
FromName string
TimeoutSeconds int
}
type DataConfig struct {
DataDir string
DatabasePath string
RawMailDir string
AttachmentDir string
}
type HTTPConfig struct {
Host string
Port int
}
func (c POP3Config) Timeout() time.Duration {
return time.Duration(c.TimeoutSeconds) * time.Second
}
func (c SMTPConfig) Timeout() time.Duration {
return time.Duration(c.TimeoutSeconds) * time.Second
}
func Load(envPath string) (*Config, error) {
values := map[string]string{}
if envPath == "" {
envPath = ".env"
}
if err := readEnvFile(envPath, values); err != nil {
return nil, err
}
for _, item := range os.Environ() {
key, val, ok := strings.Cut(item, "=")
if ok {
values[key] = val
}
}
dataDir := getString(values, "DATA_DIR", "./data")
cfg := &Config{
AccountID: getString(values, "MAIL_ACCOUNT_ID", "default"),
POP3: POP3Config{
Host: getString(values, "POP3_HOST", ""),
Port: getInt(values, "POP3_PORT", 110),
Username: getString(values, "POP3_USERNAME", ""),
Password: getString(values, "POP3_PASSWORD", ""),
UseTLS: getBool(values, "POP3_USE_TLS", false),
TimeoutSeconds: getInt(values, "POP3_TIMEOUT_SECONDS", 30),
},
SMTP: SMTPConfig{
Host: getString(values, "SMTP_HOST", ""),
Port: getInt(values, "SMTP_PORT", 25),
Username: getString(values, "SMTP_USERNAME", ""),
Password: getString(values, "SMTP_PASSWORD", ""),
UseTLS: getBool(values, "SMTP_USE_TLS", false),
UseSTARTTLS: getBool(values, "SMTP_USE_STARTTLS", false),
AuthRequired: getBool(values, "SMTP_AUTH_REQUIRED", false),
AllowInsecureAuth: getBool(values, "SMTP_ALLOW_INSECURE_AUTH", false),
FromAddress: getString(values, "SMTP_FROM_ADDRESS", ""),
FromName: getString(values, "SMTP_FROM_NAME", ""),
TimeoutSeconds: getInt(values, "SMTP_TIMEOUT_SECONDS", 30),
},
Data: DataConfig{
DataDir: dataDir,
DatabasePath: getString(values, "DATABASE_PATH", filepath.Join(dataDir, "internal_mail.db")),
RawMailDir: getString(values, "RAW_MAIL_DIR", filepath.Join(dataDir, "raw")),
AttachmentDir: getString(values, "ATTACHMENT_DIR", filepath.Join(dataDir, "attachments")),
},
HTTP: HTTPConfig{
Host: getString(values, "HTTP_HOST", "127.0.0.1"),
Port: getInt(values, "HTTP_PORT", 8765),
},
}
return cfg, nil
}
func readEnvFile(path string, values map[string]string) error {
file, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("read env file: %w", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
value = strings.Trim(value, `"'`)
values[key] = value
}
return scanner.Err()
}
func getString(values map[string]string, key, def string) string {
if v, ok := values[key]; ok {
return v
}
return def
}
func getInt(values map[string]string, key string, def int) int {
v, ok := values[key]
if !ok || strings.TrimSpace(v) == "" {
return def
}
n, err := strconv.Atoi(v)
if err != nil {
return def
}
return n
}
func getBool(values map[string]string, key string, def bool) bool {
v, ok := values[key]
if !ok || strings.TrimSpace(v) == "" {
return def
}
switch strings.ToLower(strings.TrimSpace(v)) {
case "1", "true", "yes", "y", "on":
return true
case "0", "false", "no", "n", "off":
return false
default:
return def
}
}

View File

@@ -0,0 +1,42 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func TestLoadDefaultsAndEnvFile(t *testing.T) {
dir := t.TempDir()
envPath := filepath.Join(dir, ".env")
if err := os.WriteFile(envPath, []byte("POP3_HOST=mail.local\nSMTP_PORT=2525\nSMTP_AUTH_REQUIRED=true\nSMTP_ALLOW_INSECURE_AUTH=true\nDATA_DIR="+dir+"\n"), 0600); err != nil {
t.Fatal(err)
}
cfg, err := Load(envPath)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.AccountID != "default" {
t.Fatalf("AccountID = %q, want default", cfg.AccountID)
}
if cfg.POP3.Host != "mail.local" {
t.Fatalf("POP3.Host = %q", cfg.POP3.Host)
}
if cfg.POP3.Port != 110 {
t.Fatalf("POP3.Port = %d, want 110", cfg.POP3.Port)
}
if cfg.SMTP.Port != 2525 {
t.Fatalf("SMTP.Port = %d, want 2525", cfg.SMTP.Port)
}
if !cfg.SMTP.AuthRequired {
t.Fatal("SMTP.AuthRequired = false, want true")
}
if !cfg.SMTP.AllowInsecureAuth {
t.Fatal("SMTP.AllowInsecureAuth = false, want true")
}
if cfg.HTTP.Host != "127.0.0.1" || cfg.HTTP.Port != 8765 {
t.Fatalf("HTTP default = %s:%d", cfg.HTTP.Host, cfg.HTTP.Port)
}
}