Full pipeline: IMAP ingress -> OpenClaw dispatch -> callback -> SMTP reply. SQLite stateful storage with idempotency, threading, and retry logic. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
112 lines
2.7 KiB
Go
112 lines
2.7 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/joho/godotenv"
|
|
)
|
|
|
|
// Config holds all application configuration loaded from environment variables.
|
|
type Config struct {
|
|
// IMAP settings
|
|
IMAPHost string
|
|
IMAPPort string
|
|
IMAPUser string
|
|
IMAPPass string
|
|
|
|
// SMTP settings
|
|
SMTPHost string
|
|
SMTPPort string
|
|
SMTPUser string
|
|
SMTPPass string
|
|
|
|
// OpenClaw settings
|
|
OpenClawURL string
|
|
OpenClawAPIKey string
|
|
|
|
// Bridge settings
|
|
BridgeCallbackToken string
|
|
SystemEmail string
|
|
WhitelistEmails []string
|
|
|
|
// Optional
|
|
IMAPProxyURL string
|
|
|
|
// Server
|
|
ListenAddr string
|
|
}
|
|
|
|
// Load reads .env (if present) and populates Config from environment variables.
|
|
// Returns an error if any required variable is missing.
|
|
func Load() (*Config, error) {
|
|
// Best-effort load .env — ignore error if file doesn't exist.
|
|
_ = godotenv.Load()
|
|
|
|
cfg := &Config{
|
|
IMAPHost: os.Getenv("IMAP_HOST"),
|
|
IMAPPort: os.Getenv("IMAP_PORT"),
|
|
IMAPUser: os.Getenv("IMAP_USER"),
|
|
IMAPPass: os.Getenv("IMAP_PASS"),
|
|
SMTPHost: os.Getenv("SMTP_HOST"),
|
|
SMTPPort: os.Getenv("SMTP_PORT"),
|
|
SMTPUser: os.Getenv("SMTP_USER"),
|
|
SMTPPass: os.Getenv("SMTP_PASS"),
|
|
OpenClawURL: os.Getenv("OPENCLAW_URL"),
|
|
OpenClawAPIKey: os.Getenv("OPENCLAW_API_KEY"),
|
|
BridgeCallbackToken: os.Getenv("BRIDGE_CALLBACK_TOKEN"),
|
|
SystemEmail: os.Getenv("SYSTEM_EMAIL"),
|
|
IMAPProxyURL: os.Getenv("IMAP_PROXY_URL"),
|
|
ListenAddr: os.Getenv("LISTEN_ADDR"),
|
|
}
|
|
|
|
if raw := os.Getenv("WHITELIST_EMAILS"); raw != "" {
|
|
for _, email := range strings.Split(raw, ",") {
|
|
if trimmed := strings.TrimSpace(email); trimmed != "" {
|
|
cfg.WhitelistEmails = append(cfg.WhitelistEmails, trimmed)
|
|
}
|
|
}
|
|
}
|
|
|
|
if cfg.ListenAddr == "" {
|
|
cfg.ListenAddr = ":8080"
|
|
}
|
|
|
|
if err := cfg.validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// validate checks that all required configuration values are present.
|
|
func (c *Config) validate() error {
|
|
required := map[string]string{
|
|
"IMAP_HOST": c.IMAPHost,
|
|
"IMAP_PORT": c.IMAPPort,
|
|
"IMAP_USER": c.IMAPUser,
|
|
"IMAP_PASS": c.IMAPPass,
|
|
"SMTP_HOST": c.SMTPHost,
|
|
"SMTP_PORT": c.SMTPPort,
|
|
"SMTP_USER": c.SMTPUser,
|
|
"SMTP_PASS": c.SMTPPass,
|
|
"OPENCLAW_URL": c.OpenClawURL,
|
|
"BRIDGE_CALLBACK_TOKEN": c.BridgeCallbackToken,
|
|
"SYSTEM_EMAIL": c.SystemEmail,
|
|
}
|
|
|
|
var missing []string
|
|
for name, val := range required {
|
|
if val == "" {
|
|
missing = append(missing, name)
|
|
}
|
|
}
|
|
|
|
if len(missing) > 0 {
|
|
return fmt.Errorf("missing required config: %s", strings.Join(missing, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|