internal/policy/parse.go (view raw)
1package policy
2
3import (
4 "fmt"
5 "log"
6 "net"
7 "net/mail"
8 "net/netip"
9 "os"
10
11 "postern/internal/db"
12
13 "go.starlark.net/starlark"
14 "go.starlark.net/starlarkstruct"
15 "go.starlark.net/syntax"
16)
17
18type RelayConfig struct {
19 Host string
20 Port int
21 Username string
22 Password string
23}
24
25type Engine struct {
26 Listeners []ListenerConfig
27 UsersModule *UsersModule
28 globals starlark.StringDict
29 thread *starlark.Thread
30}
31
32type ListenerConfig struct {
33 Address netip.AddrPort
34 Role string
35 TLSCertFile string
36 TLSKeyFile string
37}
38
39type Address struct {
40 Address, Name, Raw string
41}
42
43func (a Address) String() string { return a.Raw }
44func (a Address) Type() string { return "Address" }
45func (a Address) Freeze() {}
46func (a Address) Truth() starlark.Bool { return true }
47func (a Address) Hash() (uint32, error) { return 0, nil }
48
49func (a Address) Attr(name string) (starlark.Value, error) {
50 switch name {
51 case "address":
52 return starlark.String(a.Address), nil
53 case "name":
54 return starlark.String(a.Name), nil
55 case "raw":
56 return starlark.String(a.Raw), nil
57 }
58 return nil, nil
59}
60
61func (a Address) AttrNames() []string {
62 return []string{"address", "name", "raw"}
63}
64
65type MessageContext struct {
66 HeaderFrom Address
67 HeaderTo Address
68 Subject string
69 Headers mail.Header
70 Size int64
71}
72
73func (m MessageContext) String() string { return "<MessageContext>" }
74func (m MessageContext) Type() string { return "MessageContext" }
75func (m MessageContext) Freeze() {
76 m.HeaderFrom.Freeze()
77 m.HeaderTo.Freeze()
78}
79func (m MessageContext) Truth() starlark.Bool { return true }
80func (m MessageContext) Hash() (uint32, error) { return 0, nil }
81
82func (m MessageContext) Attr(name string) (starlark.Value, error) {
83 switch name {
84 case "header_from":
85 return m.HeaderFrom, nil
86 case "header_to":
87 return m.HeaderTo, nil
88 case "subject":
89 return starlark.String(m.Subject), nil
90 case "size":
91 return starlark.MakeInt64(m.Size), nil
92 }
93 return nil, nil
94}
95
96func (m MessageContext) AttrNames() []string {
97 return []string{"header_from", "subject", "size"}
98}
99
100// MDA implements starlark.Value and starlark.HasAttrs
101// so it can be exposed as the `mda` global object in the script.
102type MDA struct {
103 parsedListeners []ListenerConfig
104}
105
106func (m *MDA) String() string { return "<module mda>" }
107func (m *MDA) Type() string { return "module" }
108func (m *MDA) Freeze() {}
109func (m *MDA) Truth() starlark.Bool { return true }
110func (m *MDA) Hash() (uint32, error) { return 0, nil }
111
112// Attr resolves mda.listen, mda.accept, mda.reject, mda.deliver_to, and mda.quarantine when called in Starlark
113func (m *MDA) Attr(name string) (starlark.Value, error) {
114 switch name {
115 case "listen":
116 return starlark.NewBuiltin("listen", m.mdaListen), nil
117 }
118 return nil, nil
119}
120func (m *MDA) AttrNames() []string {
121 return []string{"listen"}
122}
123
124type Conn struct {
125 RemoteAddr starlark.String
126 ClientIP starlark.String
127 Meta *starlarkstruct.Struct
128}
129
130func (c Conn) String() string { return "<Conn>" }
131func (c Conn) Type() string { return "Conn" }
132func (c Conn) Freeze() { c.Meta.Freeze() }
133func (c Conn) Truth() starlark.Bool { return true }
134func (c Conn) Hash() (uint32, error) { return 0, nil }
135
136func (c Conn) Attr(name string) (starlark.Value, error) {
137 switch name {
138 case "remote_addr":
139 return c.RemoteAddr, nil
140 case "client_ip":
141 return c.ClientIP, nil
142 case "meta":
143 return c.Meta, nil
144 }
145 return nil, nil
146}
147
148func (c Conn) AttrNames() []string {
149 return []string{"remote_addr", "client_ip", "meta"}
150}
151
152func ConnToStarlark(conn net.Conn) Conn {
153 remoteAddr := conn.RemoteAddr().String()
154 var clientIP string
155 if tcpAddr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
156 clientIP = tcpAddr.IP.String()
157 } else {
158 clientIP = remoteAddr
159 }
160
161 return Conn{
162 RemoteAddr: starlark.String(remoteAddr),
163 ClientIP: starlark.String(clientIP),
164 Meta: starlarkstruct.FromStringDict(starlark.None, starlark.StringDict{}),
165 }
166}
167
168// mdaListen handles mda.listen(address="...", role="...")
169func (m *MDA) mdaListen(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
170 var address, role, tlsCertFile, tlsKeyFile string
171
172 // starlark.UnpackArgs maps Starlark arguments to Go variables safely
173 err := starlark.UnpackArgs(
174 b.Name(),
175 args,
176 kwargs,
177 "address", &address,
178 "role", &role,
179 "tls_cert_file", &tlsCertFile,
180 "tls_key_file", &tlsKeyFile)
181 if err != nil {
182 return nil, err
183 }
184
185 addrPort, err := netip.ParseAddrPort(address)
186 if err != nil {
187 return nil, err
188 }
189
190 // Save to our Go state
191 m.parsedListeners = append(m.parsedListeners, ListenerConfig{
192 Address: addrPort,
193 Role: role,
194 TLSCertFile: tlsCertFile,
195 TLSKeyFile: tlsKeyFile,
196 })
197
198 return starlark.None, nil
199}
200
201func NewEngine(policyFile string, database *db.DB) (Engine, error) {
202 script, err := os.ReadFile(policyFile)
203 if err != nil {
204 log.Fatal(err)
205 }
206
207 // Create an 'os' module containing our getenv function
208 osModule := &starlarkstruct.Module{
209 Name: "os",
210 Members: starlark.StringDict{
211 "getenv": starlark.NewBuiltin("getenv", starlarkGetenv),
212 },
213 }
214
215 // Create the users module
216 usersModule := NewUsersModule(database)
217
218 // Expose the "os", "mda", and "users" modules to the Starlark global environment
219 m := &MDA{
220 parsedListeners: make([]ListenerConfig, 0),
221 }
222 predeclared := starlark.StringDict{
223 "mda": m,
224 "os": osModule,
225 "users": usersModule,
226 }
227
228 // Create a Starlark thread. We customize the Print function
229 // so the Starlark `print()` built-in logs cleanly in Go.
230 thread := &starlark.Thread{
231 Name: "mda-main",
232 Print: func(_ *starlark.Thread, msg string) {
233 fmt.Printf("[init.star]: %s\n", msg)
234 },
235 }
236
237 // 1. Evaluate the script
238 globals, err := starlark.ExecFileOptions(&syntax.FileOptions{}, thread, "policy.star", script, predeclared)
239 if err != nil {
240 return Engine{}, err
241 }
242
243 // 2. Call the init() function
244 initVal, ok := globals["init"]
245 if !ok {
246 return Engine{}, fmt.Errorf("init not found in policy")
247 }
248
249 _, err = starlark.Call(thread, initVal, nil, nil)
250 if err != nil {
251 return Engine{}, err
252 }
253
254 engine := Engine{
255 Listeners: m.parsedListeners,
256 UsersModule: usersModule,
257 globals: globals,
258 thread: thread,
259 }
260 return engine, err
261}
262
263func (e *Engine) OnMessageSubmit(user User, msg MessageContext) (RelayConfig, error) {
264 hookVal, ok := e.globals["on_message_submit"]
265 if !ok {
266 return RelayConfig{}, nil
267 }
268
269 result, err := starlark.Call(e.thread, hookVal, starlark.Tuple{user, msg}, nil)
270 if err != nil {
271 return RelayConfig{}, err
272 }
273
274 if result == starlark.None {
275 return RelayConfig{}, nil
276 }
277
278 dict, ok := result.(*starlark.Dict)
279 if !ok {
280 return RelayConfig{}, fmt.Errorf("on_message_submit must return a dict, got %s", result.Type())
281 }
282
283 relay := RelayConfig{
284 Host: dictString(dict, "host"),
285 Port: dictInt(dict, "port"),
286 Username: dictString(dict, "username"),
287 Password: dictString(dict, "password"),
288 }
289 return relay, nil
290}
291
292func dictString(dict *starlark.Dict, key string) string {
293 v, _, err := dict.Get(starlark.String(key))
294 if err != nil || v == starlark.None {
295 return ""
296 }
297 if s, ok := v.(starlark.String); ok {
298 return string(s)
299 }
300 return ""
301}
302
303func dictInt(dict *starlark.Dict, key string) int {
304 v, _, err := dict.Get(starlark.String(key))
305 if err != nil || v == starlark.None {
306 return 0
307 }
308 if i, ok := v.(starlark.Int); ok {
309 if n, ok := i.Int64(); ok {
310 return int(n)
311 }
312 }
313 return 0
314}
315
316func (e *Engine) OnMessageDeliver(conn Conn, user User, msg MessageContext) (string, error) {
317 defaultMailbox := "INBOX"
318 hookVal, ok := e.globals["on_message_deliver"]
319 if !ok {
320 return defaultMailbox, nil
321 }
322
323 result, err := starlark.Call(e.thread, hookVal, starlark.Tuple{conn, user, msg}, nil)
324 if err != nil {
325 return defaultMailbox, err
326 }
327
328 mailbox := defaultMailbox
329 if result != starlark.None {
330 if s, ok := result.(starlark.String); ok {
331 mailbox = string(s)
332 }
333 }
334
335 return mailbox, nil
336}
337
338func (e *Engine) OnMessageMoved(user User, msg MessageContext, sourceFolder, destFolder string) error {
339 hookVal, ok := e.globals["on_message_moved"]
340 if !ok {
341 return nil
342 }
343
344 _, err := starlark.Call(e.thread, hookVal, starlark.Tuple{user, msg, starlark.String(sourceFolder), starlark.String(destFolder)}, nil)
345 return err
346}
347
348// MessageContextFromEnvelope builds a policy.MessageContext from the envelope
349// fields of a stored message. The header_from address is parsed into an
350// Address; if parsing fails the raw string is preserved.
351func MessageContextFromEnvelope(from, subject string, size int64) MessageContext {
352 fromAddr, err := ParseAddress(from)
353 if err != nil || from == "" {
354 fromAddr = Address{Raw: from, Address: from}
355 }
356 return MessageContext{
357 HeaderFrom: fromAddr,
358 Subject: subject,
359 Size: size,
360 }
361}
362
363func ParseAddress(email string) (Address, error) {
364 a, err := mail.ParseAddress(email)
365 if err != nil {
366 return Address{}, err
367 }
368
369 addr := Address{
370 Raw: email,
371 Address: a.Address,
372 Name: a.Name,
373 }
374
375 return addr, nil
376}
377
378// starlarkGetenv implements Python's os.getenv("KEY", "default")
379func starlarkGetenv(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
380 var key string
381 var def starlark.Value = starlark.None
382
383 if err := starlark.UnpackArgs(b.Name(), args, kwargs, "key", &key, "default?", &def); err != nil {
384 return nil, err
385 }
386
387 if val, exists := os.LookupEnv(key); exists {
388 return starlark.String(val), nil
389 }
390 return def, nil
391}