---
title: "openemail.rules"
description: "Every method in this namespace: its signature, its parameters, what it returns and an example."
url: "https://openemail.uk/docs/python/reference/rules"
area: "Python"
category: "Reference"
---

# openemail.rules

Every method in this namespace: its signature, its parameters, what it returns and an example.

## Methods

Conditions and actions evaluated on arriving mail, with dry runs and an audit trail.

### `rules.list()`

List mail rules in evaluation order

```python
def list(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    enabled: bool | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[RuleResource]
```

Returns one page of the mailbox's rules in the order they run on arriving mail: ascending `position`, then `createdAt`, then `id`. This is deliberately not newest first. With `stopProcessing` in play, the same rules in a different order file mail differently, so the order you read is the order that matters.

Paging is keyset on that same `(position, createdAt, id)` tuple, and the cursor is opaque: it holds where the last rule on the page sat in that order. Pass `nextCursor` back as `cursor=` while `hasMore` is `True`, or let `list_all` or `iterate` walk the pages. `enabled=` narrows to enabled or disabled rules, and the SDK sends it as `enabled=true` or `enabled=false` in the query string.

Scopes: `rules:read`.

**Parameters**

- `limit` (`int`): Rows per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`str`): The `nextCursor` from the previous page. Never build one yourself.
- `enabled` (`bool`): Restricts the page to enabled (`True`) or disabled (`False`) rules. Leave it out for both.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[RuleResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each rule has `id`, `name`, `description`, `enabled`, `position`, `match`, `conditions`, `actions`, `stopProcessing`, `lastMatchedAt`, `matchCount`, `createdAt` and `updatedAt`.

**Example**

```python
from openemail import openemail

page = openemail.rules.list(enabled=True, limit=100)

for rule in page['items']:
    print(rule['position'], rule['name'], rule['matchCount'], rule['stopProcessing'])
```

**Notes**

- A rule deleted, moved or renamed between pages never breaks the walk: the next page starts at the first rule that sorts after the cursor. A cursor this list did not hand out raises a 400 `invalid_cursor`.
- A key limited to particular addresses or domains lists only the rules that can act on mail delivered to them: every rule without a `delivered_to` condition, and a rule with one when it can match an address the key holds.
- A mailbox holds at most 100 rules, so `limit=100` always returns them in a single page.
- A `matchCount` still at zero after weeks is the cheap sign that a rule's conditions never hold. `list_runs` has the detail.

Also available in: API [`GET /rules`](https://openemail.uk/docs/api/reference/rules#get-rules); TypeScript [`rules.list()`](https://openemail.uk/docs/sdk/reference/rules#list); Ruby [`rules.list`](https://openemail.uk/docs/ruby/reference/rules#list); CLI [`openemail rules list`](https://openemail.uk/docs/cli/reference/rules#rules-list).

### `rules.list_all()`

Collect every rule into one list

```python
def list_all(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    enabled: bool | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[RuleResource]
```

Walks every page of the rule list and returns all rules as one list in evaluation order, which is the order to read them in when reasoning about what happens to a message. A mailbox holds at most 100 rules, so passing `limit=100` fetches them in a single request, while the server default of 25 takes up to four.

`enabled=` collects only enabled or only disabled rules. A disabled rule keeps its position, so a filtered list hides rules that still sit between the ones you see, and `reorder` needs every id. Collect without the filter whenever you intend to change the order.

Scopes: `rules:read`.

**Parameters**

- `limit` (`int`): Page size for each request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the first page.
- `enabled` (`bool`): Collects only enabled (`True`) or disabled (`False`) rules.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[RuleResource]` holding every matching rule, sorted by `position`, then `createdAt`, then `id`.

**Example**

```python
from openemail import openemail

rules = openemail.rules.list_all(limit=100)

stoppers = [rule for rule in rules if rule['enabled'] and rule['stopProcessing']]
print([f'{rule["position"]}: {rule["name"]}' for rule in stoppers])
```

**Notes**

- If any page fails the call raises, and the rules already fetched are discarded.
- Positions are not contiguous. Deleting a rule leaves a gap, and `update` with `position` can make two rules share a number.
- `timeout=` applies to each page request on its own, not to the whole walk.

Also available in: API [`GET /rules`](https://openemail.uk/docs/api/reference/rules#get-rules); TypeScript [`rules.listAll()`](https://openemail.uk/docs/sdk/reference/rules#listAll); Ruby [`rules.list_all`](https://openemail.uk/docs/ruby/reference/rules#listAll).

### `rules.iterate()`

Stream rules one at a time in evaluation order

```python
def iterate(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    enabled: bool | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[RuleResource]
```

Returns a generator that yields rules in the order arriving mail meets them and fetches the next page only when the current one is drained. Nothing is requested until you consume it, and breaking out of the loop stops further requests.

The walk ends when `hasMore` is `False`, when a page arrives without a `nextCursor`, or when the server repeats a cursor. The cursor points into the `(position, createdAt, id)` order, so moving rules while you iterate can skip or repeat some of them. Finish reading before you call `reorder` or `update` with a `position`.

Scopes: `rules:read`.

**Parameters**

- `limit` (`int`): Page size per request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the first page.
- `enabled` (`bool`): Yields only enabled (`True`) or disabled (`False`) rules.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[RuleResource]`, a generator yielding one rule per step.

**Example**

```python
from openemail import openemail

for rule in openemail.rules.iterate(enabled=True):
    if rule['stopProcessing']:
        print(f'Mail matching {rule["name"]!r} skips every rule after {rule["position"]}')
        break
```

**Notes**

- A page that fails raises out of the `for` loop, after the rules of the earlier pages have been yielded.

Also available in: API [`GET /rules`](https://openemail.uk/docs/api/reference/rules#get-rules); TypeScript [`rules.iterate()`](https://openemail.uk/docs/sdk/reference/rules#iterate); Ruby [`rules.iterate`](https://openemail.uk/docs/ruby/reference/rules#iterate).

### `rules.get()`

Read one mail rule

```python
def get(
    id: str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> RuleResource
```

Fetches a single rule by its `rul_` id. Rules have no slug and the name is editable, so store the id if your code needs to find the rule again.

The stored `conditions` are normalised, so `negate` is always present and `False` unless you set it. `lastMatchedAt` and `matchCount` move each time the rule fires on an arriving message, which makes them the quickest way to see whether a rule is doing anything without reading `list_runs`.

Scopes: `rules:read`.

**Parameters**

- `id` (`str`, required): The rule's `rul_` id.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`RuleResource`, a dict with `id`, `name`, `description`, `enabled`, `position`, `match`, `conditions`, `actions`, `stopProcessing`, `lastMatchedAt`, `matchCount`, `createdAt` and `updatedAt`.

**Example**

```python
from openemail import openemail

rule = openemail.rules.get('rul_4f1c9a2b7d3e8f6a0b5c1d2e')

print(rule['enabled'], rule['match'], len(rule['conditions']))
print(rule['matchCount'], rule['lastMatchedAt'])
```

**Notes**

- A missing rule raises a 404 `resource_not_found`, whether it was deleted or belongs to another workspace, so `is_not_found` is the check to make. A key limited to particular addresses or domains gets the same 404 for a rule that cannot act on mail delivered to them.
- `matchCount` counts messages the rule matched, not actions it carried out. A match whose actions were all refused still counts.

Also available in: API [`GET /rules/{id}`](https://openemail.uk/docs/api/reference/rules#get-rules-id); TypeScript [`rules.get()`](https://openemail.uk/docs/sdk/reference/rules#get); Ruby [`rules.get`](https://openemail.uk/docs/ruby/reference/rules#get); CLI [`openemail rules get`](https://openemail.uk/docs/cli/reference/rules#rules-get).

### `rules.create()`

Create a mail rule at the end of the order

```python
def create(
    body: RuleCreate,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> RuleResource
```

Creates a rule that runs on mail arriving in the mailbox, with no API call involved once it exists. It is appended after every existing rule and is enabled unless you pass `'enabled': False`. There is no `position` on create: move the rule afterwards with `reorder`, the only call that guarantees an exact sequence. Creating it disabled and calling `test` first is the safe order.

Conditions are a flat list. `'match': 'all'` is AND, `'match': 'any'` is OR, and `'negate': True` inverts a single condition, so `(A and B) or C` takes two rules. `value` is always a string. `has_attachment` and `spam` take only `equals` with `'true'` or `'false'`. `attachment_size`, `message_size`, `hour` and `weekday` take a number, written as a string, with `gt`, `lt` or `equals`, and `gt` or `lt` on any other field is refused. Text comparisons ignore case, and `matches` is a whole value glob over `*` and `?` that needs at least two letters or digits. A `header` condition must name the header it reads in `header`.

Actions apply in order. `label` and `remove_label` take a label id, `forward` takes an email address and `reply` takes a template id or slug. A forward is only delivered once the destination has confirmed it accepts mail forwarded from your domain, and a reply goes to each sender at most once per 24 hours and never to bounces, auto-responders or mailing lists. A rule with `reject` must also test `envelope_from`, otherwise the call raises a 422 `reject_needs_envelope`, because refusing mail on the strength of a `From` header bounces the mailing list rather than the author.

Scopes: `rules:write`.

**Parameters**

- `body['name']` (`str`, required): Display name, 1 to 100 characters after trimming and unique per mailbox.
- `body['description']` (`str | None`): Free text note, at most 500 characters.
- `body['enabled']` (`bool`): Whether the rule runs on arriving mail. Defaults to `True`.
- `body['match']` (`RuleMatchMode`): Whether every condition (`'all'`) or any single one (`'any'`) must hold. Defaults to `'all'`.
- `body['conditions']` (`list[RuleConditionInput]`, required): 1 to 20 conditions, each a dict with `field`, `op` and `value`, plus an optional `header` (at most 128 characters) and `negate`. `value` is at most 512 characters.
- `body['actions']` (`list[RuleAction]`, required): 1 to 10 actions, each a dict with `type` and, where the type needs one, a `value` of at most 320 characters.
- `body['stopProcessing']` (`bool`): When `True`, no later rule runs on a message this rule matched. Defaults to `False`.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`RuleResource` with the new `id`, the `position` it was appended at, `matchCount` of 0 and `lastMatchedAt` set to `None`.

**Example**

```python
from openemail import openemail

rule = openemail.rules.create(
    {
        'name': 'Receipts to their own label',
        'enabled': False,
        'conditions': [
            {'field': 'from_domain', 'op': 'equals', 'value': 'stripe.com'},
            {'field': 'subject', 'op': 'contains', 'value': 'receipt'},
        ],
        'actions': [{'type': 'label', 'value': 'USER_RECEIPTS'}, {'type': 'archive'}],
        'stopProcessing': True,
    }
)

print(rule['id'], rule['position'])
```

**Notes**

- Validation failures raise a 422 `invalid_rule` whose `param` names the path, such as `conditions.0.op`. A duplicate name is a 409 `rule_name_taken`.
- A mailbox holds at most 100 rules, and the next create raises a 422 `workspace_limit_reached`.
- `from_domain` also matches subdomains, so `equals` with `stripe.com` holds for mail from `mail.stripe.com`. `hour` and `weekday` are read in UTC, with `weekday` 0 for Sunday.
- Not retried by the SDK and there is no idempotency key, so repeating a create after a lost response makes a second rule unless the name collides.
- A key limited to particular addresses or domains creates only a rule that acts on nothing but mail delivered to them, so it needs a `delivered_to` condition no other address in the workspace matches, such as `equals` with one of its addresses. Anything else is a 422 `capability_unsupported` on `conditions`.

Also available in: API [`POST /rules`](https://openemail.uk/docs/api/reference/rules#post-rules); TypeScript [`rules.create()`](https://openemail.uk/docs/sdk/reference/rules#create); Ruby [`rules.create`](https://openemail.uk/docs/ruby/reference/rules#create); CLI [`openemail rules create`](https://openemail.uk/docs/cli/reference/rules#rules-create).

### `rules.update()`

Change a rule, replacing conditions or actions whole

```python
def update(
    id: str,
    patch: RulePatch,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> RuleResource
```

Merges the patch onto the stored rule and validates the whole result, so an omitted key keeps its stored value instead of collecting a default. A patch that only renames a rule cannot switch a disabled one back on, and a patch that adds a `reject` action is checked against the conditions already stored.

`conditions` and `actions` replace the entire list. There is no way to add or remove a single element: read the rule, change the list, and send all of it. `match` and `stopProcessing` read across the whole set, which is why element level edits are not offered.

`position` moves this one rule without renumbering the others. Positions are not unique and ties break by `createdAt` then `id`, so landing on an occupied position puts the older rule first. Use `reorder` when the exact sequence matters. Setting `'enabled': False` is the reversible alternative to `delete`, and a disabled rule keeps its place in the order.

Scopes: `rules:write`.

**Parameters**

- `id` (`str`, required): The rule's `rul_` id.
- `patch['name']` (`str`): Replacement name, 1 to 100 characters and unique per mailbox.
- `patch['description']` (`str | None`): Replacement note of at most 500 characters, or `None` to clear it.
- `patch['enabled']` (`bool`): Turns the rule on or off without moving it.
- `patch['match']` (`RuleMatchMode`): Whether every condition (`'all'`) or any single one (`'any'`) must hold.
- `patch['conditions']` (`list[RuleConditionInput]`): Complete replacement list of 1 to 20 conditions.
- `patch['actions']` (`list[RuleAction]`): Complete replacement list of 1 to 10 actions.
- `patch['stopProcessing']` (`bool`): When `True`, no later rule runs on a message this rule matched.
- `patch['position']` (`int`): New position, a whole number from 0 to 1,000,000. Other rules keep theirs.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`RuleResource` as saved, with a fresh `updatedAt`. `matchCount` and `lastMatchedAt` are untouched by an edit.

**Example**

```python
from openemail import openemail

updated = openemail.rules.update(
    'rul_4f1c9a2b7d3e8f6a0b5c1d2e',
    {
        'conditions': [
            {'field': 'from_domain', 'op': 'equals', 'value': 'stripe.com'},
            {'field': 'subject', 'op': 'contains', 'value': 'receipt'},
            {'field': 'has_attachment', 'op': 'equals', 'value': 'true'},
        ],
        'enabled': True,
    },
)

print(len(updated['conditions']), updated['enabled'], updated['updatedAt'])
```

**Notes**

- Validation failures raise a 422 `invalid_rule` or `reject_needs_envelope` with `param` naming the path, such as `conditions.1.value`.
- Renaming onto another rule's name is a 409 `rule_name_taken`.
- Not retried by the SDK, since there is no idempotency key on this resource.
- A key limited to particular addresses or domains may patch only a rule that acts on nothing but mail delivered to them, and the patched rule has to stay that way. Anything else is a 422 `capability_unsupported`, and a rule the key cannot list is a 404.

Also available in: API [`PATCH /rules/{id}`](https://openemail.uk/docs/api/reference/rules#patch-rules-id); TypeScript [`rules.update()`](https://openemail.uk/docs/sdk/reference/rules#update); Ruby [`rules.update`](https://openemail.uk/docs/ruby/reference/rules#update); CLI [`openemail rules update`](https://openemail.uk/docs/cli/reference/rules#rules-update).

### `rules.delete()`

Delete a mail rule

```python
def delete(
    id: str,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> DeletedRuleResource
```

Permanently removes a rule. There is no undo, and mail it already filed stays where it put it. If you might want the rule back, `update(id, {'enabled': False})` switches it off and keeps its place in the order.

The run history survives. `list_runs(rule_id=id)` still returns what the rule did, under the name it had at the time. The other rules are not renumbered, so a gap in `position` is normal afterwards.

Scopes: `rules:write`.

**Parameters**

- `id` (`str`, required): The rule's `rul_` id.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`DeletedRuleResource`, the dict `{'object': 'rule', 'id': ..., 'deleted': True}`.

**Example**

```python
from openemail import OpenEmailApiError, openemail

try:
    deleted = openemail.rules.delete('rul_4f1c9a2b7d3e8f6a0b5c1d2e')
    print(deleted['id'], deleted['deleted'])
except OpenEmailApiError as error:
    if not error.is_not_found:
        raise
    print('That rule was already gone')
```

**Notes**

- Deleting frees the name, so a new rule can take it straight away.
- Not retried by the SDK. Repeating a delete that already succeeded raises a 404 about something that worked, which `is_not_found` lets you treat as done.
- A key limited to particular addresses or domains deletes only a rule that acts on nothing but mail delivered to them. Another rule it can list is a 422 `capability_unsupported`, and one it cannot list is a 404.

Also available in: API [`DELETE /rules/{id}`](https://openemail.uk/docs/api/reference/rules#delete-rules-id); TypeScript [`rules.delete()`](https://openemail.uk/docs/sdk/reference/rules#delete); Ruby [`rules.delete`](https://openemail.uk/docs/ruby/reference/rules#delete); CLI [`openemail rules delete`](https://openemail.uk/docs/cli/reference/rules#rules-delete).

### `rules.reorder()`

Set the evaluation order of every rule

```python
def reorder(
    rule_ids: Sequence[str],
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[RuleResource]
```

Replaces the whole order in one transaction. `rule_ids` must name every rule on the mailbox exactly once, and each rule's `position` becomes its index in the list, starting at 0. Either the whole order lands or nothing moves.

A list that leaves a rule out or names one twice raises a 422 `incomplete_order`, because an omitted rule would have to go somewhere and there is no right answer for where. Build the list from `list_all()` without the `enabled` filter so disabled rules are included. The new order applies to the next message that arrives, and mail already filed is not revisited.

Scopes: `rules:write`.

**Parameters**

- `rule_ids` (`Sequence[str]`, required): Every rule id on the mailbox in the order they should run, 1 to 100 entries. Any sequence of strings works, and the SDK sends it as a list named `ruleIds`.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[RuleResource]` holding every rule on the mailbox, sorted by its new `position`. The SDK unwraps the list envelope, so this is a plain list rather than a page.

**Example**

```python
from openemail import openemail

rules = openemail.rules.list_all(limit=100)

first = [rule['id'] for rule in rules if rule['name'] == 'Refuse known spammers']
rest = [rule['id'] for rule in rules if rule['id'] not in first]
ordered = openemail.rules.reorder(first + rest)

print([f'{rule["position"]} {rule["name"]}' for rule in ordered])
```

**Notes**

- An id that is not on this mailbox raises a 404 `resource_not_found`, and nothing moves.
- The SDK retries this call on network errors and retryable statuses, since applying the same order twice lands in the same place.
- A mailbox with no rules cannot call this: an empty `rule_ids` raises a 422 `invalid_parameter`.
- A key limited to particular addresses or domains names every rule `list` returns for it and nothing else. It may move only the rules that act on nothing but mail delivered to its addresses, anywhere among the others, and moving any other rule is a 422 `capability_unsupported`. The rules it cannot list keep their places, and the result lists only the rules it can see.

Also available in: API [`POST /rules/reorder`](https://openemail.uk/docs/api/reference/rules#post-rules-reorder); TypeScript [`rules.reorder()`](https://openemail.uk/docs/sdk/reference/rules#reorder); Ruby [`rules.reorder`](https://openemail.uk/docs/ruby/reference/rules#reorder); CLI [`openemail rules reorder`](https://openemail.uk/docs/cli/reference/rules#rules-reorder).

### `rules.test()`

Dry run a rule against mail already in the mailbox

```python
def test(
    id: str,
    body: RuleTestInput | None = None,
    *,
    api_key: str | None = None,
    timeout: float | None = None,
) -> RuleTestResource
```

Evaluates one rule against stored mail with the same matcher delivery uses and reports what it would have caught and what it would do. It changes nothing, sends nothing and files nothing, which is why it needs only `rules:read`. It works on a disabled rule, so the intended sequence is create with `'enabled': False`, test, then enable.

By default it reads up to 50 inbox threads from the last 30 days, and the body can be left out entirely to do just that. For each thread it tests the newest message the mailbox received rather than sent, and threads that fail to load or hold only your own messages are skipped, so `scanned` can come in below `limit`. Pass `threadIds` to test specific threads from any folder instead, and `days` and `limit` are then ignored. A message an existing rule already moved out of the inbox is not in the default sample, and `list_runs` is the record of what really happened.

Read `warnings` before `matched`. `field_unevaluable` means a condition reads something stored mail no longer carries (`envelope_from`, `header`, `list_id`, `delivered_to` or `message_size`), so it never held here, and under `negate` it holds on everything. `field_approximate` flags attachment, `hour` and `weekday` conditions answered from stored data that can differ from delivery. `body_encrypted` means a `body` condition met mail whose body is sealed. `forward_loop` means a forward target leads back to this mailbox, and `forward_unverified` means the target is not hosted here, so the forwarded copy is rebuilt and loses its original DKIM signature.

Scopes: `rules:read`.

**Parameters**

- `id` (`str`, required): The rule's `rul_` id. Disabled rules can be tested.
- `body['threadIds']` (`list[str]`): Up to 50 specific thread ids to test instead of the recent window. Ids that fail to load are skipped.
- `body['days']` (`int`): How far back the default window reaches, 1 to 365. Defaults to 30.
- `body['limit']` (`int`): How many recent inbox threads to read, 1 to 200. Defaults to 50.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`RuleTestResource`, a dict with `ruleId`, `scanned`, `matched`, `wouldApply` (the actions as `type` or `type:value`), `messages` (each a dict with `threadId`, `from`, `subject` and `receivedAt`) and `warnings` (each a dict with `code` and `value`, where `value` is the field name or the forward address).

**Example**

```python
from openemail import openemail

dry = openemail.rules.test('rul_4f1c9a2b7d3e8f6a0b5c1d2e', {'days': 30, 'limit': 100})

print(dry['scanned'], dry['matched'], dry['wouldApply'])
print([message['subject'] for message in dry['messages']])

if not dry['warnings'] and dry['matched'] > 0:
    openemail.rules.update(dry['ruleId'], {'enabled': True})
```

**Notes**

- There is no call that applies a rule to mail already in the mailbox. Rules only act on mail that arrives while they are enabled.
- `wouldApply` lists what the rule declares. At delivery a forward to an unconfirmed address or a suppressed reply still fails, and only `list_runs` shows that.
- The SDK retries it on network errors and retryable statuses, because a dry run has no side effects.
- A key limited to particular addresses or domains tests only a rule it can list, against conversations delivered to its addresses. `threadIds` naming any other conversation are skipped, and a rule it cannot list is a 404.

Also available in: API [`POST /rules/{id}/test`](https://openemail.uk/docs/api/reference/rules#post-rules-id-test); TypeScript [`rules.test()`](https://openemail.uk/docs/sdk/reference/rules#test); Ruby [`rules.test`](https://openemail.uk/docs/ruby/reference/rules#test); CLI [`openemail rules test`](https://openemail.uk/docs/cli/reference/rules#rules-test).

### `rules.list_runs()`

List what rules actually did to arriving mail

```python
def list_runs(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    rule_id: str | None = None,
    thread_id: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Page[RuleRunResource]
```

Returns one page of the rule audit log, newest first. Each row is one rule matching one arriving message, with the actions that took effect and the ones that were refused.

Each row copies the rule's name at the moment it fired, so renamed and deleted rules still read correctly, and `rule_id=` works for a rule that no longer exists. `thread_id=` answers the other common question: why a particular message ended up where it did.

`actions` lists the action types that were applied, and `failures` lists refusals as strings of the form `type: reason`. A non empty `failures` means the rule matched but the mailbox declined part of it, for example a `reply` suppressed because that sender was already answered in the last day, a `forward` to an address that has not confirmed, or a `reject` that could not be refused at SMTP and was filed as spam instead.

Scopes: `rules:read`.

**Parameters**

- `limit` (`int`): Rows per page, a whole number from 1 to 100. The server defaults to 25.
- `cursor` (`str`): The `nextCursor` from the previous page. Never build one yourself.
- `rule_id` (`str`): Only runs of this rule, including a rule that has since been deleted.
- `thread_id` (`str`): Only runs recorded against this thread.
- `api_key` (`str`): Overrides the client API key for this call only.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Page[RuleRunResource]`, a dict with `items`, `hasMore` and `nextCursor`. Each run has `id`, `ruleId`, `ruleName`, `threadId`, `messageId`, `sender`, `subject`, `actions`, `failures` and `createdAt`.

**Example**

```python
from openemail import openemail

page = openemail.rules.list_runs(rule_id='rul_4f1c9a2b7d3e8f6a0b5c1d2e', limit=50)

for run in page['items']:
    print(run['createdAt'], run['sender'], run['actions'], run['failures'])
```

**Notes**

- `actions` holds bare types such as `label` or `archive`, without the label id or address the rule was configured with. Read the rule itself for those.
- `sender` is lower-cased and trimmed, and `subject` is cut at 500 characters.
- A cursor naming a run that never existed raises a 400 `invalid_cursor`.
- A key limited to particular addresses or domains reads only the runs of rules it can list, so runs of a rule deleted since, whose conditions are gone, are left out for such a key.
- Only mail arriving while the rule is enabled writes here. `test` and edits never do.

Also available in: API [`GET /rules/runs`](https://openemail.uk/docs/api/reference/rules#get-rules-runs); TypeScript [`rules.listRuns()`](https://openemail.uk/docs/sdk/reference/rules#listRuns); Ruby [`rules.list_runs`](https://openemail.uk/docs/ruby/reference/rules#listRuns); CLI [`openemail rules list-runs`](https://openemail.uk/docs/cli/reference/rules#rules-list-runs).

### `rules.list_all_runs()`

Collect every matching rule run into one list

```python
def list_all_runs(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    rule_id: str | None = None,
    thread_id: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> builtins.list[RuleRunResource]
```

Walks every page of the rule audit log and returns all matching runs as one list, newest first. Unlike rules, runs are not capped: a busy rule can leave thousands of rows, so narrow with `rule_id=` or `thread_id=` before collecting, and prefer `iterate_runs` when you can stop early.

Pages are keyset on `createdAt` and `id`, walking backwards in time. Runs recorded after the walk starts are newer than its first page and are not included.

Scopes: `rules:read`.

**Parameters**

- `limit` (`int`): Page size for each request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest run.
- `rule_id` (`str`): Only runs of this rule, including a rule that has since been deleted.
- `thread_id` (`str`): Only runs recorded against this thread.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`list[RuleRunResource]` holding every matching run, newest first.

**Example**

```python
from openemail import openemail

runs = openemail.rules.list_all_runs(rule_id='rul_4f1c9a2b7d3e8f6a0b5c1d2e', limit=100)

refused = [run for run in runs if run['failures']]
print(f'{len(refused)} of {len(runs)} matches were partly refused')
```

**Notes**

- If any page fails the call raises, and the runs already fetched are discarded.
- Without a filter this reads the whole mailbox audit, one request per page.
- `timeout=` applies to each page request on its own, not to the whole walk.

Also available in: API [`GET /rules/runs`](https://openemail.uk/docs/api/reference/rules#get-rules-runs); TypeScript [`rules.listAllRuns()`](https://openemail.uk/docs/sdk/reference/rules#listAllRuns); Ruby [`rules.list_all_runs`](https://openemail.uk/docs/ruby/reference/rules#listAllRuns).

### `rules.iterate_runs()`

Stream rule runs one at a time, newest first

```python
def iterate_runs(
    *,
    limit: int | None = None,
    cursor: str | None = None,
    rule_id: str | None = None,
    thread_id: str | None = None,
    api_key: str | None = None,
    timeout: float | None = None,
) -> Iterator[RuleRunResource]
```

Returns a generator over the rule audit log that yields runs one at a time and fetches the next page only when the current one is drained. Nothing is requested until you consume it, and breaking out of the loop stops further requests, which makes it the right way to find the latest run of some kind without reading the whole history.

The walk ends when `hasMore` is `False`, when a page arrives without a `nextCursor`, or when the server repeats a cursor. Runs recorded after the walk starts are newer than its cursor and are not yielded.

Scopes: `rules:read`.

**Parameters**

- `limit` (`int`): Page size per request, 1 to 100. The server defaults to 25.
- `cursor` (`str`): Starts the walk from this cursor instead of the newest run.
- `rule_id` (`str`): Only runs of this rule, including a rule that has since been deleted.
- `thread_id` (`str`): Only runs recorded against this thread.
- `api_key` (`str`): Overrides the client API key for every page of this walk.
- `timeout` (`float`): Seconds this call may take, the response included, before it raises `OpenEmailNetworkError` with `is_timeout`. It overrides the client's `timeout` for this call, and `0` turns the limit off.

**Returns**

`Iterator[RuleRunResource]`, a generator yielding one run per step.

**Example**

```python
from openemail import openemail

for run in openemail.rules.iterate_runs(rule_id='rul_4f1c9a2b7d3e8f6a0b5c1d2e'):
    if any(failure.startswith('forward:') for failure in run['failures']):
        print(run['createdAt'], run['threadId'], run['failures'])
        break
```

**Notes**

- A page that fails raises out of the `for` loop, after the runs of the earlier pages have been yielded.

Also available in: API [`GET /rules/runs`](https://openemail.uk/docs/api/reference/rules#get-rules-runs); TypeScript [`rules.iterateRuns()`](https://openemail.uk/docs/sdk/reference/rules#iterateRuns); Ruby [`rules.iterate_runs`](https://openemail.uk/docs/ruby/reference/rules#iterateRuns).
