all repos — postern

@ d1a9230eb6918613c1748e78ba80ba486973663a

Modern mail management

clone
git clone https:///postern.git

internal/policy/parse.go (view raw)

  1package policy
  2
  3import (
  4	"context"
  5	"fmt"
  6	"log"
  7	"net/netip"
  8	"os"
  9
 10	"postern/internal/db"
 11
 12	"go.starlark.net/lib/json"
 13	sltime "go.starlark.net/lib/time"
 14	"go.starlark.net/starlark"
 15	"go.starlark.net/starlarkstruct"
 16	"go.starlark.net/syntax"
 17)
 18
 19type RelayConfig struct {
 20	Host     string
 21	Port     int
 22	Username string
 23	Password string
 24}
 25
 26type Engine struct {
 27	Listeners   []ListenerConfig
 28	UsersModule *UsersModule
 29	globals     starlark.StringDict
 30	thread      *starlark.Thread
 31}
 32
 33type ListenerConfig struct {
 34	Address     netip.AddrPort
 35	Role        string
 36	TLSCertFile string
 37	TLSKeyFile  string
 38}
 39
 40func NewEngine(policyFile string, database *db.DB) (Engine, error) {
 41	script, err := os.ReadFile(policyFile)
 42	if err != nil {
 43		log.Fatal(err)
 44	}
 45
 46	// Create an 'os' module containing our getenv function
 47	osModule := &starlarkstruct.Module{
 48		Name: "os",
 49		Members: starlark.StringDict{
 50			"getenv": starlark.NewBuiltin("getenv", starlarkGetenv),
 51		},
 52	}
 53
 54	// Create the users module
 55	usersModule := NewUsersModule(database)
 56
 57	// Expose the "os", "mda", "users", and "http" modules to the Starlark global environment
 58	m := &MDA{
 59		parsedListeners: make([]ListenerConfig, 0),
 60	}
 61
 62	predeclared := starlark.StringDict{
 63		"http":  &HTTPModule{},
 64		"json":  json.Module,
 65		"kv":    NewKVModule(database),
 66		"mda":   m,
 67		"os":    osModule,
 68		"time":  sltime.Module,
 69		"users": usersModule,
 70	}
 71
 72	// Create a Starlark thread. We customize the Print function
 73	// so the Starlark `print()` built-in logs cleanly in Go.
 74	thread := &starlark.Thread{
 75		Name: "mda-main",
 76		Print: func(_ *starlark.Thread, msg string) {
 77			fmt.Printf("[init.star]: %s\n", msg)
 78		},
 79	}
 80	thread.SetLocal("context", context.Background())
 81
 82	// 1. Evaluate the script
 83	globals, err := starlark.ExecFileOptions(&syntax.FileOptions{}, thread, "policy.star", script, predeclared)
 84	if err != nil {
 85		return Engine{}, err
 86	}
 87
 88	// 2. Call the init() function
 89	initVal, ok := globals["init"]
 90	if !ok {
 91		return Engine{}, fmt.Errorf("init not found in policy")
 92	}
 93
 94	_, err = starlark.Call(thread, initVal, nil, nil)
 95	if err != nil {
 96		return Engine{}, err
 97	}
 98
 99	engine := Engine{
100		Listeners:   m.parsedListeners,
101		UsersModule: usersModule,
102		globals:     globals,
103		thread:      thread,
104	}
105	return engine, err
106}
107
108func (e *Engine) OnMessageSubmit(user User, msg *Message) (RelayConfig, error) {
109	hookVal, ok := e.globals["on_message_submit"]
110	if !ok {
111		return RelayConfig{}, nil
112	}
113
114	result, err := starlark.Call(e.thread, hookVal, starlark.Tuple{user, msg}, nil)
115	if err != nil {
116		return RelayConfig{}, err
117	}
118
119	if result == starlark.None {
120		return RelayConfig{}, nil
121	}
122
123	dict, ok := result.(*starlark.Dict)
124	if !ok {
125		return RelayConfig{}, fmt.Errorf("on_message_submit must return a dict, got %s", result.Type())
126	}
127
128	relay := RelayConfig{
129		Host:     dictString(dict, "host"),
130		Port:     dictInt(dict, "port"),
131		Username: dictString(dict, "username"),
132		Password: dictString(dict, "password"),
133	}
134	return relay, nil
135}
136
137func (e *Engine) OnMessageDeliver(conn Conn, user User, msg *Message) (string, error) {
138	defaultMailbox := "INBOX"
139	hookVal, ok := e.globals["on_message_deliver"]
140	if !ok {
141		return defaultMailbox, nil
142	}
143
144	result, err := starlark.Call(e.thread, hookVal, starlark.Tuple{conn, user, msg}, nil)
145	if err != nil {
146		return defaultMailbox, err
147	}
148
149	mailbox := defaultMailbox
150	if result != starlark.None {
151		if s, ok := result.(starlark.String); ok {
152			mailbox = string(s)
153		}
154	}
155
156	return mailbox, nil
157}
158
159func (e *Engine) OnMessageMoved(user User, msg *Message, sourceFolder, destFolder string) error {
160	hookVal, ok := e.globals["on_message_moved"]
161	if !ok {
162		return nil
163	}
164
165	_, err := starlark.Call(e.thread, hookVal, starlark.Tuple{user, msg, starlark.String(sourceFolder), starlark.String(destFolder)}, nil)
166	return err
167}
168
169// starlarkGetenv implements Python's os.getenv("KEY", "default")
170func starlarkGetenv(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
171	var key string
172	var def starlark.Value = starlark.None
173
174	if err := starlark.UnpackArgs(b.Name(), args, kwargs, "key", &key, "default?", &def); err != nil {
175		return nil, err
176	}
177
178	if val, exists := os.LookupEnv(key); exists {
179		return starlark.String(val), nil
180	}
181	return def, nil
182}
183
184func dictString(dict *starlark.Dict, key string) string {
185	v, _, err := dict.Get(starlark.String(key))
186	if err != nil || v == starlark.None {
187		return ""
188	}
189	if s, ok := v.(starlark.String); ok {
190		return string(s)
191	}
192	return ""
193}
194
195func dictInt(dict *starlark.Dict, key string) int {
196	v, _, err := dict.Get(starlark.String(key))
197	if err != nil || v == starlark.None {
198		return 0
199	}
200	if i, ok := v.(starlark.Int); ok {
201		if n, ok := i.Int64(); ok {
202			return int(n)
203		}
204	}
205	return 0
206}