Mail access: IMAP + app password (IPv4 only), not Gmail API
This commit is contained in:
160
internal/config/config.go
Normal file
160
internal/config/config.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// Package config loads the Email Agent runtime configuration from the
|
||||
// environment.
|
||||
//
|
||||
// Values are read from process environment variables. In production those
|
||||
// variables are provided by the host (for example by
|
||||
// ~/.config/environment.d/10-secrets.conf) and passed into the container by
|
||||
// docker-compose. Secret values are never written to disk by this package and
|
||||
// must never be committed to the repository.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Defaults for the service. Port 8790 is recorded in AGENTS.md §9.
|
||||
const (
|
||||
DefaultPort = 8790
|
||||
DefaultEnv = "development"
|
||||
DefaultAddr = ":" + "8790"
|
||||
DefaultDB = "data/email_agent.db"
|
||||
)
|
||||
|
||||
// Config is the fully resolved runtime configuration.
|
||||
type Config struct {
|
||||
// Env is the deployment environment: "development" or "production".
|
||||
Env string
|
||||
|
||||
// Addr is the TCP address the HTTP server listens on, e.g. ":8790".
|
||||
Addr string
|
||||
|
||||
// Port is the TCP port the HTTP server listens on.
|
||||
Port int
|
||||
|
||||
// DBPath is the filesystem path to the SQLite database file.
|
||||
DBPath string
|
||||
|
||||
// SessionSecret signs session cookies for the login screen (FR auth).
|
||||
// Login itself is implemented in Phase 1.2; the value is loaded here so the
|
||||
// service has a single configuration surface.
|
||||
SessionSecret string
|
||||
|
||||
// LogLevel controls slog verbosity: debug, info, warn, error.
|
||||
LogLevel string
|
||||
|
||||
// PublicBaseURL is the externally visible base URL of the service.
|
||||
PublicBaseURL string
|
||||
|
||||
// Integration settings. These are loaded for later phases so that secret
|
||||
// names are defined in one place. Phase 1 does not use them.
|
||||
|
||||
// JEVBaseURL is the base URL of the JEV/TypeSafe HTTP API.
|
||||
JEVBaseURL string
|
||||
// JEVAPIKey authenticates to JEV. Secret.
|
||||
JEVAPIKey string
|
||||
|
||||
// OmniRouteBaseURL is the base URL of the OmniRoute gateway.
|
||||
OmniRouteBaseURL string
|
||||
// OmniRouteAPIKey authenticates to OmniRoute. Secret.
|
||||
OmniRouteAPIKey string
|
||||
|
||||
// GmailCredentialsFile is the OAuth client credentials JSON path.
|
||||
GmailCredentialsFile string
|
||||
// GmailTokenFile is the path where the OAuth token is cached.
|
||||
GmailTokenFile string
|
||||
|
||||
// HubToken authenticates the read-only Hub API. Secret.
|
||||
HubToken string
|
||||
}
|
||||
|
||||
// Load reads configuration from the environment, applying defaults.
|
||||
func Load() (*Config, error) {
|
||||
c := &Config{
|
||||
Env: getenvDefault("EMAIL_AGENT_ENV", DefaultEnv),
|
||||
SessionSecret: os.Getenv("EMAIL_AGENT_SESSION_SECRET"),
|
||||
LogLevel: strings.ToLower(getenvDefault("EMAIL_AGENT_LOG_LEVEL", "info")),
|
||||
PublicBaseURL: strings.TrimRight(getenvDefault("EMAIL_AGENT_PUBLIC_BASE_URL", "http://localhost:8790"), "/"),
|
||||
JEVBaseURL: strings.TrimRight(os.Getenv("JEV_BASE_URL"), "/"),
|
||||
JEVAPIKey: os.Getenv("JEV_API_KEY"),
|
||||
OmniRouteBaseURL: strings.TrimRight(os.Getenv("OMNIROUTE_BASE_URL"), "/"),
|
||||
OmniRouteAPIKey: os.Getenv("OMNIROUTE_API_KEY"),
|
||||
GmailCredentialsFile: os.Getenv("EMAIL_AGENT_GMAIL_CREDENTIALS_FILE"),
|
||||
GmailTokenFile: os.Getenv("EMAIL_AGENT_GMAIL_TOKEN_FILE"),
|
||||
HubToken: os.Getenv("HUB_TOKEN"),
|
||||
}
|
||||
|
||||
port, err := resolvePort()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.Port = port
|
||||
c.Addr = resolveAddr(port)
|
||||
c.DBPath = getenvDefault("EMAIL_AGENT_DB_PATH", DefaultDB)
|
||||
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// resolvePort determines the listening port from EMAIL_AGENT_PORT or the port
|
||||
// component of EMAIL_AGENT_ADDR, falling back to DefaultPort.
|
||||
func resolvePort() (int, error) {
|
||||
if raw := strings.TrimSpace(os.Getenv("EMAIL_AGENT_PORT")); raw != "" {
|
||||
p, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("EMAIL_AGENT_PORT %q is not a number: %w", raw, err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
if addr := strings.TrimSpace(os.Getenv("EMAIL_AGENT_ADDR")); addr != "" {
|
||||
if i := strings.LastIndex(addr, ":"); i >= 0 && i < len(addr)-1 {
|
||||
if p, err := strconv.Atoi(addr[i+1:]); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return DefaultPort, nil
|
||||
}
|
||||
|
||||
// resolveAddr prefers an explicit EMAIL_AGENT_ADDR, otherwise builds one from
|
||||
// the resolved port.
|
||||
func resolveAddr(port int) string {
|
||||
if addr := strings.TrimSpace(os.Getenv("EMAIL_AGENT_ADDR")); addr != "" {
|
||||
return addr
|
||||
}
|
||||
return fmt.Sprintf(":%d", port)
|
||||
}
|
||||
|
||||
func (c *Config) validate() error {
|
||||
if c.Port < 1 || c.Port > 65535 {
|
||||
return fmt.Errorf("port %d is out of range", c.Port)
|
||||
}
|
||||
if strings.TrimSpace(c.DBPath) == "" {
|
||||
return fmt.Errorf("EMAIL_AGENT_DB_PATH must not be empty")
|
||||
}
|
||||
switch c.Env {
|
||||
case "development", "production", "test":
|
||||
default:
|
||||
return fmt.Errorf("EMAIL_AGENT_ENV %q must be development, production, or test", c.Env)
|
||||
}
|
||||
switch c.LogLevel {
|
||||
case "debug", "info", "warn", "error":
|
||||
default:
|
||||
return fmt.Errorf("EMAIL_AGENT_LOG_LEVEL %q must be debug, info, warn, or error", c.LogLevel)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsProduction reports whether the service runs in the production environment.
|
||||
func (c *Config) IsProduction() bool { return c.Env == "production" }
|
||||
|
||||
func getenvDefault(key, def string) string {
|
||||
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
Reference in New Issue
Block a user