# Starlark Objects This document describes the objects available to Starlark policy scripts in postern. These objects are passed into the policy hooks (`on_message_submit`, `on_message_deliver`, `on_message_moved`) and are returned by the `users` module. - [`conn`](#conn) - [`user`](#user) - [`msg`](#msg) - [`Address`](#address) - [`Attachment`](#attachment) - [`users` module](#users-module) - [`user.gatekeeper`](#usergatekeeper) --- ## `conn` A `conn` represents the incoming network connection that delivered the message. It is passed as the first argument to the `on_message_deliver` hook. ### Attributes | Attribute | Type | Description | |---------------|----------|-----------------------------------------------------------------------------------------------------------------------------------| | `remote_addr` | `string` | The full remote address of the connection (e.g. `1.2.3.4:54321`). | | `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. | | `meta` | `struct` | A free-form, initially empty struct that scripts can use to stash per-connection metadata. | ### Example ```python def on_message_deliver(conn, user, msg): if conn.client_ip.startswith("10."): return "INBOX" return "INBOX" ``` --- ## `user` A `user` represents a postern user account. It is passed as the second argument to `on_message_deliver` and `on_message_submit`, and as the first argument to `on_message_moved`. Users are also obtainable from the [`users` module](#users-module). ### Attributes | Attribute | Type | Description | |---------------|-----------------------------------------------|-------------------------------------------------------------------------------| | `name` | `string` | The username (login) of the user. | | `is_disabled` | `bool` | Whether the user account is disabled. | | `addresses` | `tuple[str]` | The list of email addresses belonging to the user, as a tuple of strings. | | `gatekeeper` | [`UserGatekeeper`](#usergatekeeper) or `None` | The per-user gatekeeper sub-object, or `None` if no gatekeeper is configured. | ### Example ```python def on_message_deliver(conn, user, msg): if user.is_disabled: return None if "vip@example.com" in user.addresses: return "VIP" return "INBOX" ``` --- ## `msg` A `msg` represents the email message being processed. It is passed as the final argument to every policy hook. ### Attributes | Attribute | Type | Description | |---------------|-----------------------|----------------------------------------------------------------------------------| | `header_from` | [`Address`](#address) | Parsed `From` header. Errors (e.g. missing `From`) propagate as Starlark errors. | | `header_to` | [`Address`](#address) | Parsed `To` header. Returns an error if the `To` header is missing or invalid. | | `subject` | `string` | The `Subject` header value, or an empty string if absent. | | `size` | `int` | Size of the raw message in bytes. | | `body` | `bytes` | The full raw message body (headers + content) as bytes. | | `text_body` | `string` | The concatenated plain-text body parts of the message. | | `html_body` | `string` | The concatenated HTML body parts of the message. | | `attachments` | `list[Attachment]` | List of extracted attachments (see [`Attachment`](#attachment)). | Accessing `header_from`, `header_to`, `subject` triggers a one-time parse of the message headers. Likewise, `text_body`, `html_body`, and `attachments` trigger a one-time full parse of the message. These parses are memoized: the message stream is only read once per group of attributes. ### Example ```python def on_message_deliver(conn, user, msg): sender = msg.header_from.address subject = msg.subject for att in msg.attachments: if att.filename.endswith(".pdf"): print("found PDF:", att.filename) body_text = msg.text_body or msg.html_body return "INBOX" ``` --- ## `Address` A parsed email address, returned by `msg.header_from` and `msg.header_to`, and accepted as an argument by some `users.gatekeeper` methods. ### Attributes | Attribute | Type | Description | |-----------|----------|---------------------------------------------------------------------------------------| | `address` | `string` | The bare email address (e.g. `alice@example.com`). | | `name` | `string` | The display name portion of the address, if any (e.g. `Alice`). Empty if not present. | | `raw` | `string` | The original, unparsed header value as it appeared in the message. | ### Example ```python addr = msg.header_from print(addr.raw) # "Alice " print(addr.name) # "Alice" print(addr.address) # "alice@example.com" ``` --- ## `Attachment` Represents a single attachment (or inline binary part) extracted from the message. Returned as elements of `msg.attachments`. ### Attributes | Attribute | Type | Description | |----------------|----------|---------------------------------------------------------------------------------------------------------------------------| | `filename` | `string` | The filename of the attachment, taken from `Content-Disposition` or the `name` parameter of `Content-Type`. May be empty. | | `content_type` | `string` | The MIME media type of the attachment (e.g. `application/pdf`, `image/png`). | | `data` | `bytes` | The decoded payload bytes of the attachment. | ### Example ```python for att in msg.attachments: if att.content_type == "application/pdf": store_pdf(att.filename, att.data) ``` --- ## `users` module The `users` module is a predeclared global that lets scripts look up user accounts by name or by email address. ### Functions #### `users.get(username)` Look up a user by username. - **Args**: `username: string` - **Returns**: a [`User`](#user) object, or `None` if no such user exists. #### `users.get_by_address(address)` Look up the user that owns a given email address. - **Args**: `address: string` - **Returns**: a [`User`](#user) object, or `None` if no user owns the address. ### Example ```python def on_message_deliver(conn, user, msg): recipient = msg.header_to.address owner = users.get_by_address(recipient) if owner is None: return None # reject delivery return "INBOX" ``` --- ## `user.gatekeeper` The gatekeeper is a sub-object of [`User`](#user) that manages persistent routing decisions: where mail from a given sender should be delivered for that user. ### Methods #### `gatekeeper.get_decision(msg)` Retrieve the stored routing destination for the sender of `msg`. - **Args**: `msg` (a [`msg`](#msg) object — only `msg.header_from` is read). - **Returns**: `string` (the destination mailbox/folder), or `None` if no decision is stored for this sender. #### `gatekeeper.route_to(from_address, destination)` Store or update the routing decision: mail coming from `from_address` to this user should be delivered to `destination`. - **Args**: - `from_address`: an [`Address`](#address) object (e.g. `msg.header_from`). - `destination`: a `string` naming the destination mailbox/folder. - **Returns**: `None`. ### Example ```python def on_message_deliver(conn, user, msg): sender = msg.header_from dest = user.gatekeeper.get_decision(msg) if dest is not None: return dest # First contact: route based on subject, and remember it. if "invoice" in msg.subject.lower(): dest = "Invoices" else: dest = "INBOX" user.gatekeeper.route_to(sender, dest) return dest ```