all repos — postern

@ main

Modern mail management

clone
git clone https:///postern.git

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```