starlark-objects.md (view raw)
1# Starlark Objects
2
3This document describes the objects available to Starlark policy scripts in
4postern. These objects are passed into the policy hooks (`on_message_submit`,
5`on_message_deliver`, `on_message_moved`) and are returned by the `users`
6module.
7
8- [`conn`](#conn)
9- [`user`](#user)
10- [`msg`](#msg)
11 - [`Address`](#address)
12 - [`Attachment`](#attachment)
13- [`users` module](#users-module)
14 - [`user.gatekeeper`](#usergatekeeper)
15
16---
17
18## `conn`
19
20A `conn` represents the incoming network connection that delivered the message.
21It is passed as the first argument to the `on_message_deliver` hook.
22
23### Attributes
24
25| Attribute | Type | Description |
26|---------------|----------|-----------------------------------------------------------------------------------------------------------------------------------|
27| `remote_addr` | `string` | The full remote address of the connection (e.g. `1.2.3.4:54321`). |
28| `client_ip` | `string` | The remote IP address. For TCP connections this is the bare IP without port. Falls back to `remote_addr` for non-TCP connections. |
29| `meta` | `struct` | A free-form, initially empty struct that scripts can use to stash per-connection metadata. |
30
31### Example
32
33```python
34def on_message_deliver(conn, user, msg):
35 if conn.client_ip.startswith("10."):
36 return "INBOX"
37 return "INBOX"
38```
39
40---
41
42## `user`
43
44A `user` represents a postern user account. It is passed as the second
45argument to `on_message_deliver` and `on_message_submit`, and as the first
46argument to `on_message_moved`. Users are also obtainable from the
47[`users` module](#users-module).
48
49### Attributes
50
51| Attribute | Type | Description |
52|---------------|-----------------------------------------------|-------------------------------------------------------------------------------|
53| `name` | `string` | The username (login) of the user. |
54| `is_disabled` | `bool` | Whether the user account is disabled. |
55| `addresses` | `tuple[str]` | The list of email addresses belonging to the user, as a tuple of strings. |
56| `gatekeeper` | [`UserGatekeeper`](#usergatekeeper) or `None` | The per-user gatekeeper sub-object, or `None` if no gatekeeper is configured. |
57
58### Example
59
60```python
61def on_message_deliver(conn, user, msg):
62 if user.is_disabled:
63 return None
64 if "vip@example.com" in user.addresses:
65 return "VIP"
66 return "INBOX"
67```
68
69---
70
71## `msg`
72
73A `msg` represents the email message being processed. It is passed as the
74final argument to every policy hook.
75
76### Attributes
77
78| Attribute | Type | Description |
79|---------------|-----------------------|----------------------------------------------------------------------------------|
80| `header_from` | [`Address`](#address) | Parsed `From` header. Errors (e.g. missing `From`) propagate as Starlark errors. |
81| `header_to` | [`Address`](#address) | Parsed `To` header. Returns an error if the `To` header is missing or invalid. |
82| `subject` | `string` | The `Subject` header value, or an empty string if absent. |
83| `size` | `int` | Size of the raw message in bytes. |
84| `body` | `bytes` | The full raw message body (headers + content) as bytes. |
85| `text_body` | `string` | The concatenated plain-text body parts of the message. |
86| `html_body` | `string` | The concatenated HTML body parts of the message. |
87| `attachments` | `list[Attachment]` | List of extracted attachments (see [`Attachment`](#attachment)). |
88
89Accessing `header_from`, `header_to`, `subject` triggers a one-time parse of
90the message headers. Likewise, `text_body`, `html_body`, and `attachments`
91trigger a one-time full parse of the message. These parses are memoized: the
92message stream is only read once per group of attributes.
93
94### Example
95
96```python
97def on_message_deliver(conn, user, msg):
98 sender = msg.header_from.address
99 subject = msg.subject
100
101 for att in msg.attachments:
102 if att.filename.endswith(".pdf"):
103 print("found PDF:", att.filename)
104
105 body_text = msg.text_body or msg.html_body
106 return "INBOX"
107```
108
109---
110
111## `Address`
112
113A parsed email address, returned by `msg.header_from` and `msg.header_to`, and
114accepted as an argument by some `users.gatekeeper` methods.
115
116### Attributes
117
118| Attribute | Type | Description |
119|-----------|----------|---------------------------------------------------------------------------------------|
120| `address` | `string` | The bare email address (e.g. `alice@example.com`). |
121| `name` | `string` | The display name portion of the address, if any (e.g. `Alice`). Empty if not present. |
122| `raw` | `string` | The original, unparsed header value as it appeared in the message. |
123
124### Example
125
126```python
127addr = msg.header_from
128print(addr.raw) # "Alice <alice@example.com>"
129print(addr.name) # "Alice"
130print(addr.address) # "alice@example.com"
131```
132
133---
134
135## `Attachment`
136
137Represents a single attachment (or inline binary part) extracted from the
138message. Returned as elements of `msg.attachments`.
139
140### Attributes
141
142| Attribute | Type | Description |
143|----------------|----------|---------------------------------------------------------------------------------------------------------------------------|
144| `filename` | `string` | The filename of the attachment, taken from `Content-Disposition` or the `name` parameter of `Content-Type`. May be empty. |
145| `content_type` | `string` | The MIME media type of the attachment (e.g. `application/pdf`, `image/png`). |
146| `data` | `bytes` | The decoded payload bytes of the attachment. |
147
148### Example
149
150```python
151for att in msg.attachments:
152 if att.content_type == "application/pdf":
153 store_pdf(att.filename, att.data)
154```
155
156---
157
158## `users` module
159
160The `users` module is a predeclared global that lets scripts look up user
161accounts by name or by email address.
162
163### Functions
164
165#### `users.get(username)`
166
167Look up a user by username.
168
169- **Args**: `username: string`
170- **Returns**: a [`User`](#user) object, or `None` if no such user exists.
171
172#### `users.get_by_address(address)`
173
174Look up the user that owns a given email address.
175
176- **Args**: `address: string`
177- **Returns**: a [`User`](#user) object, or `None` if no user owns the address.
178
179### Example
180
181```python
182def on_message_deliver(conn, user, msg):
183 recipient = msg.header_to.address
184 owner = users.get_by_address(recipient)
185 if owner is None:
186 return None # reject delivery
187 return "INBOX"
188```
189
190---
191
192## `user.gatekeeper`
193
194The gatekeeper is a sub-object of [`User`](#user) that manages persistent
195routing decisions: where mail from a given sender should be delivered for
196that user.
197
198### Methods
199
200#### `gatekeeper.get_decision(msg)`
201
202Retrieve the stored routing destination for the sender of `msg`.
203
204- **Args**: `msg` (a [`msg`](#msg) object — only `msg.header_from` is read).
205- **Returns**: `string` (the destination mailbox/folder), or `None` if no
206 decision is stored for this sender.
207
208#### `gatekeeper.route_to(from_address, destination)`
209
210Store or update the routing decision: mail coming from `from_address` to this
211user should be delivered to `destination`.
212
213- **Args**:
214 - `from_address`: an [`Address`](#address) object (e.g. `msg.header_from`).
215 - `destination`: a `string` naming the destination mailbox/folder.
216- **Returns**: `None`.
217
218### Example
219
220```python
221def on_message_deliver(conn, user, msg):
222 sender = msg.header_from
223 dest = user.gatekeeper.get_decision(msg)
224 if dest is not None:
225 return dest
226 # First contact: route based on subject, and remember it.
227 if "invoice" in msg.subject.lower():
228 dest = "Invoices"
229 else:
230 dest = "INBOX"
231 user.gatekeeper.route_to(sender, dest)
232 return dest
233```