Initial commit: email-openclaw bridge v1

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>
This commit is contained in:
2026-04-27 18:16:57 +07:00
co-authored by Claude Opus 4.7
commit 8815c20c13
35 changed files with 2808 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
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
}