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}