package policy import ( "context" "fmt" "log" "net/netip" "os" "postern/internal/db" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" "go.starlark.net/syntax" ) type RelayConfig struct { Host string Port int Username string Password string } type Engine struct { Listeners []ListenerConfig UsersModule *UsersModule globals starlark.StringDict thread *starlark.Thread } type ListenerConfig struct { Address netip.AddrPort Role string TLSCertFile string TLSKeyFile string } func NewEngine(policyFile string, database *db.DB) (Engine, error) { script, err := os.ReadFile(policyFile) if err != nil { log.Fatal(err) } // Create an 'os' module containing our getenv function osModule := &starlarkstruct.Module{ Name: "os", Members: starlark.StringDict{ "getenv": starlark.NewBuiltin("getenv", starlarkGetenv), }, } // Create the users module usersModule := NewUsersModule(database) kvModule := NewKVModule(database) // Expose the "os", "mda", and "users" modules to the Starlark global environment m := &MDA{ parsedListeners: make([]ListenerConfig, 0), } predeclared := starlark.StringDict{ "kv": kvModule, "mda": m, "os": osModule, "users": usersModule, } // Create a Starlark thread. We customize the Print function // so the Starlark `print()` built-in logs cleanly in Go. thread := &starlark.Thread{ Name: "mda-main", Print: func(_ *starlark.Thread, msg string) { fmt.Printf("[init.star]: %s\n", msg) }, } thread.SetLocal("context", context.Background()) // 1. Evaluate the script globals, err := starlark.ExecFileOptions(&syntax.FileOptions{}, thread, "policy.star", script, predeclared) if err != nil { return Engine{}, err } // 2. Call the init() function initVal, ok := globals["init"] if !ok { return Engine{}, fmt.Errorf("init not found in policy") } _, err = starlark.Call(thread, initVal, nil, nil) if err != nil { return Engine{}, err } engine := Engine{ Listeners: m.parsedListeners, UsersModule: usersModule, globals: globals, thread: thread, } return engine, err } func (e *Engine) OnMessageSubmit(user User, msg MessageContext) (RelayConfig, error) { hookVal, ok := e.globals["on_message_submit"] if !ok { return RelayConfig{}, nil } result, err := starlark.Call(e.thread, hookVal, starlark.Tuple{user, msg}, nil) if err != nil { return RelayConfig{}, err } if result == starlark.None { return RelayConfig{}, nil } dict, ok := result.(*starlark.Dict) if !ok { return RelayConfig{}, fmt.Errorf("on_message_submit must return a dict, got %s", result.Type()) } relay := RelayConfig{ Host: dictString(dict, "host"), Port: dictInt(dict, "port"), Username: dictString(dict, "username"), Password: dictString(dict, "password"), } return relay, nil } func (e *Engine) OnMessageDeliver(conn Conn, user User, msg MessageContext) (string, error) { defaultMailbox := "INBOX" hookVal, ok := e.globals["on_message_deliver"] if !ok { return defaultMailbox, nil } result, err := starlark.Call(e.thread, hookVal, starlark.Tuple{conn, user, msg}, nil) if err != nil { return defaultMailbox, err } mailbox := defaultMailbox if result != starlark.None { if s, ok := result.(starlark.String); ok { mailbox = string(s) } } return mailbox, nil } func (e *Engine) OnMessageMoved(user User, msg MessageContext, sourceFolder, destFolder string) error { hookVal, ok := e.globals["on_message_moved"] if !ok { return nil } _, err := starlark.Call(e.thread, hookVal, starlark.Tuple{user, msg, starlark.String(sourceFolder), starlark.String(destFolder)}, nil) return err } // starlarkGetenv implements Python's os.getenv("KEY", "default") func starlarkGetenv(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var key string var def starlark.Value = starlark.None if err := starlark.UnpackArgs(b.Name(), args, kwargs, "key", &key, "default?", &def); err != nil { return nil, err } if val, exists := os.LookupEnv(key); exists { return starlark.String(val), nil } return def, nil } func dictString(dict *starlark.Dict, key string) string { v, _, err := dict.Get(starlark.String(key)) if err != nil || v == starlark.None { return "" } if s, ok := v.(starlark.String); ok { return string(s) } return "" } func dictInt(dict *starlark.Dict, key string) int { v, _, err := dict.Get(starlark.String(key)) if err != nil || v == starlark.None { return 0 } if i, ok := v.(starlark.Int); ok { if n, ok := i.Int64(); ok { return int(n) } } return 0 }