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

# client.contacts

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

## Methods

The workspace address book: people it has written to, plus anybody you save yourself.

### `contacts.list`

List the workspace contacts, most recently seen first

```ruby
list(source: nil, q: nil, limit: nil, cursor: nil, api_key: nil) -> OpenEmail::Page
```

Returns one page of the workspace address book, ordered by `lastSeenAt` with the most recent first and contacts that have never been mailed last. `source` says where a row came from: `manual` for a contact somebody saved, in the app or through `create`, and `auto` for an address recorded when a member sent mail to it from the app composer. A saved contact stays `manual` when it is mailed later.

`source:` narrows the page to one origin, so `source: "manual"` is the contacts somebody saved on purpose and `source: "auto"` the ones the composer recorded. `q:` searches the name and the address.

This lists saved contacts only. `list_people` lists everyone the Contacts page in the app shows, the addresses seen in mail included, with thread counts.

Paging is keyset. `limit:` takes 1 to 200 and defaults to 50, and `next_cursor` goes back as `cursor:` while `has_more?` is true. Never build a cursor yourself.

Contacts belong to the whole workspace rather than to any one address, so a key limited to particular addresses or domains reads and writes the same book as every other key. An app connected by a member who reaches only some addresses is refused with 422 `capability_unsupported` on `addressAllowlist`, on every contacts, audiences and broadcasts route.

Scopes: `contacts:read`.

**Parameters**

- `limit` (`Integer`): Rows per page, a whole number from 1 to 200. The server defaults to 50.
- `cursor` (`String`): The `next_cursor` from the previous page. Never build one yourself.
- `source` (`String`): `manual` or `auto`. Leave it out for the whole book.
- `q` (`String`): Searches the name and the address, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead, and the pages that follow keep matching the same way.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

An `OpenEmail::Page` with `items`, `has_more?` and `next_cursor`. Each contact is a Hash with `email`, `name`, `source`, `notes`, `photoUrl` and `lastSeenAt`.

**Example**

```ruby
saved = client.contacts.list(source: "manual", limit: 200)

p saved.items.map { |contact| contact[:name] || contact[:email] }
```

**Notes**

- Mail sent through this API adds no contacts. Only sends from the app composer record recipients, and `create` is the way to add one deliberately.
- Every member of the workspace reads and writes the same book, so a contact saved by one person is visible to the rest.
- A contact that has never been mailed has `lastSeenAt` nil and sorts last, after every contact with a date.
- Addresses are stored lower cased.

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

### `contacts.list_all`

Collect the whole address book into one array

```ruby
list_all(source: nil, q: nil, limit: nil, cursor: nil, api_key: nil) -> Array<Hash>
```

Follows `next_cursor` from page to page and returns every contact in the workspace, most recently seen first and never-mailed contacts last. It takes the same `source:` and `q:` filters as `list`.

Everything is held in memory before the call returns, and an address book grows with every recipient the composer records. Prefer `iterate` when you can stop early. `limit:` sets the page size of each request, not the total.

Scopes: `contacts:read`.

**Parameters**

- `source` (`String`): `manual` or `auto`. Leave it out for the whole book.
- `q` (`String`): Searches the name and the address, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead, and the pages that follow keep matching the same way.
- `limit` (`Integer`): Page size per request, from 1 to 200, defaulting to 50 on the server.
- `cursor` (`String`): A cursor from an earlier page to start after.
- `api_key` (`String`): Overrides the client's API key for every page of this walk.

**Returns**

An Array of Hashes holding every contact across all pages.

**Example**

```ruby
saved = client.contacts.list_all(source: "manual", limit: 200)

puts "#{saved.size} contacts saved on purpose"
```

**Notes**

- A failure on any page raises out of the whole call, and the contacts already fetched are discarded.

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

### `contacts.iterate`

Stream the address book one contact at a time

```ruby
iterate(source: nil, q: nil, limit: nil, cursor: nil, api_key: nil, &block) -> Enumerator<Hash>
```

Returns an Enumerator that yields contacts individually, most recently seen first, or yields each one to a block when given one, and requests the next page only once the current one is drained. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.

Scopes: `contacts:read`.

**Parameters**

- `source` (`String`): `manual` or `auto`. Leave it out for the whole book.
- `q` (`String`): Searches the name and the address, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead, and the pages that follow keep matching the same way.
- `limit` (`Integer`): Page size per request, from 1 to 200, defaulting to 50 on the server.
- `cursor` (`String`): A cursor from an earlier page to start after.
- `api_key` (`String`): Overrides the client's API key for every page of this walk.

**Returns**

An Enumerator of Hashes, one contact per step, or yields each one to a block.

**Example**

```ruby
client.contacts.iterate(source: "auto") do |contact|
  break if contact[:lastSeenAt].nil?

  puts "#{contact[:email]} #{contact[:lastSeenAt]}"
end
```

**Notes**

- The Enumerator is lazy, so an abandoned loop costs only the pages you consumed.

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

### `contacts.get`

Read one contact by email address

```ruby
get(email, api_key: nil) -> Hash
```

Looks up a contact by address in the workspace address book. The address is trimmed and lower cased before the lookup, so `Grace@Example.com` finds `grace@example.com`, and the SDK URL encodes it for the path.

A 404 means only that the address is not in the book. It says nothing about whether mail has been exchanged with it, since received mail never creates contacts.

Scopes: `contacts:read`.

**Parameters**

- `email` (`String`, required): The contact's address, matched case insensitively.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash with `email`, `name`, `source`, `notes`, `photoUrl` and `lastSeenAt`, plus `audiences`, every audience the contact is in as a Hash with `id`, `name` and `builtin`. `builtin` is `default` on the audience every contact belongs to and nil on one somebody created.

**Example**

```ruby
begin
  contact = client.contacts.get("grace@example.com")
  puts contact[:name], contact[:notes]
rescue OpenEmail::NotFoundError
  puts "not in the address book"
end
```

**Notes**

- `name` is nil for an address recorded automatically without a display name, and `notes` is free text somebody set in the app or through `update`.
- The address is the contact's identity here and on every other contacts route. There is no id in the public contract.

Also available in: API [`GET /contacts/{email}`](https://openemail.uk/docs/api/reference/contacts#get-contacts-email); TypeScript [`contacts.get()`](https://openemail.uk/docs/sdk/reference/contacts#get); Python [`contacts.get()`](https://openemail.uk/docs/python/reference/contacts#get); CLI [`openemail contacts get`](https://openemail.uk/docs/cli/reference/contacts#contacts-get).

### `contacts.create`

Save a contact in the workspace address book

```ruby
create(body = nil, api_key: nil, **fields) -> Hash
```

Adds one address to the workspace address book and returns the saved row. `email` is trimmed and lower cased before it is stored, so `Grace@Example.com` and `grace@example.com` are the same contact.

The contact is saved with `source` set to `manual`, the same value a contact typed into the app carries, and it joins the default audience as it is written. Every contact is in that audience for as long as it exists, so there is nothing to add afterwards. Name audiences of your own in `audienceIds` to put it in them in the same call, or add it later with `audiences.add_contact`. Sending `audienceIds` also requires the `audiences:write` scope, since it writes memberships as well as a contact.

An address can be in the book once. A second create for an address that is already there is refused with 409 `contact_exists` rather than merged, so a retry cannot quietly overwrite a name somebody edited in the app. Read the existing row with `get` and change it with `update`.

Scopes: `contacts:write`.

**Parameters**

- `email` (`String`, required): The address to save, trimmed and lower cased before it is stored.
- `name` (`String`): Display name. Leave it out to save the contact without one.
- `notes` (`String`): Free text kept with the contact and shown beside it in the app.
- `audienceIds` (`Array<String>`): Up to 25 audience ids to put the new contact in. The default audience is joined whether or not it is named. When this is present the key also needs `audiences:write`, or the call is refused with 403 before anything is saved.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash with `email`, `name`, `source` as `manual`, `notes` and `lastSeenAt`, which is nil until mail goes to the address from the app composer, plus `audiences`, every audience the contact is now in, the default one included.

**Example**

```ruby
contact = client.contacts.create(
  email: "grace@example.com",
  name: "Grace Hopper",
  notes: "Met at the compiler workshop"
)

puts contact[:email], contact[:source]
```

**Notes**

- The SDK does not retry a create after a network failure. If you retry by hand after a lost response, a 409 `contact_exists` means the first attempt landed.
- Contacts belong to the workspace, so the contact this creates is the one every member and every other key sees.
- An id in `audienceIds` that is not an audience of this workspace fails the whole create. Nothing is saved.

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

### `contacts.update`

Change a contact's name or notes

```ruby
update(email, patch = nil, api_key: nil, **fields) -> Hash
```

Changes the fields you send and leaves the rest alone. A key you leave out keeps its stored value, and an explicit nil clears it, so `notes: nil` empties the notes while an empty patch changes nothing.

The address cannot be changed. It is the contact's identity, its path segment and the unique key of the book, so moving a contact to a new address is a `delete` and a `create`, and that new contact starts with no audience membership beyond the default one.

`source` and `lastSeenAt` are the server's to set and are not accepted here. A contact recorded automatically stays `auto` after you give it a name.

Scopes: `contacts:write`.

**Parameters**

- `email` (`String`, required): The contact's address, matched case insensitively.
- `name` (`String`): New display name. `nil` clears it.
- `notes` (`String`): New notes. `nil` clears them.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash, the contact as it stands after the change, with `audiences` alongside it.

**Example**

```ruby
contact = client.contacts.update("grace@example.com", name: "Grace Hopper", notes: nil)

p contact[:name], contact[:notes]
```

**Notes**

- The SDK retries this call after a network failure, since the same patch sent twice leaves the same contact.
- An address that is not in the book is a 404, the same as `get`.
- Audience membership is not touched here. Use `set_audiences` to set the whole list, or `audiences.add_contact` and `audiences.remove_contact` for one audience.

Also available in: API [`PATCH /contacts/{email}`](https://openemail.uk/docs/api/reference/contacts#patch-contacts-email); TypeScript [`contacts.update()`](https://openemail.uk/docs/sdk/reference/contacts#update); Python [`contacts.update()`](https://openemail.uk/docs/python/reference/contacts#update); CLI [`openemail contacts update`](https://openemail.uk/docs/cli/reference/contacts#contacts-update).

### `contacts.delete`

Delete somebody from the contacts and hide the address

```ruby
delete(email, api_key: nil) -> Hash
```

Does what Delete does on the Contacts page in the app. A saved contact goes with its notes, its photo and every audience it was in, the default one included. The address is then hidden: `list_people` leaves it out, and mail sent to it from the app composer no longer records it as a contact. The address can be one that was only ever seen in mail, which is how you take somebody off the people list, and `wasSaved` says which it was.

Nothing else moves: the mail exchanged with the address stays in the mailbox, and the address can still be written to. There is no undo. `create` or `save` afterwards brings the address back as a new contact with no name, no notes and no membership beyond the default audience.

Scopes: `contacts:write`.

**Parameters**

- `email` (`String`, required): The contact's address, matched case insensitively.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash with `object` set to `contact`, `email`, `deleted` set to true and `wasSaved`.

**Example**

```ruby
removed = client.contacts.delete("grace@example.com")

puts removed[:email], removed[:deleted]
```

**Notes**

- An address that is not in the book is no longer a 404: it is hidden and answers with `wasSaved` false. Something that is not an address is a 422 `invalid_contact`.
- The SDK does not retry a delete, though a second attempt is harmless: it answers with `wasSaved` false.
- `delete_many` deletes up to 200 addresses in one call.

Also available in: API [`DELETE /contacts/{email}`](https://openemail.uk/docs/api/reference/contacts#delete-contacts-email); TypeScript [`contacts.delete()`](https://openemail.uk/docs/sdk/reference/contacts#delete); Python [`contacts.delete()`](https://openemail.uk/docs/python/reference/contacts#delete); CLI [`openemail contacts delete`](https://openemail.uk/docs/cli/reference/contacts#contacts-delete).

### `contacts.set_audiences`

Set exactly which audiences a contact is in

```ruby
set_audiences(email, body = nil, api_key: nil, **fields) -> Hash
```

Makes the contact's audiences match the list you send, the way the audience picker on a contact does in the app. The contact joins every listed audience it is not in yet and leaves every other one, in one transaction, and memberships it keeps keep their `addedAt`.

The default audience is always kept, whether or not you name it, so `audienceIds: []` leaves the contact in the default audience alone. To add or remove one audience without restating the rest, use `audiences.add_contact` or `audiences.remove_contact`.

The address is trimmed and lower cased, the SDK URL encodes it for the path, and it has to be a contact already: an address that is not in the book is a 404 `contact_not_found` on `email`. Create it with `create`, which takes `audienceIds` too. An id that is not an audience of this workspace is a 404 `audience_not_found` on `audienceIds`, and nothing is changed.

Scopes: `audiences:write`.

**Parameters**

- `email` (`String`, required): The contact's address, matched case insensitively.
- `audienceIds` (`Array<String>`, required): Every audience the contact should be in, up to 100 ids. A repeated id counts once. More than 100 is a 422 `invalid_parameter` on `audienceIds`, and an empty id is the same error on that entry, such as `audienceIds.0`.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash, the contact as it stands after the change, with `audiences`, every audience the contact is now in as a Hash with `id`, `name` and `builtin`, the default one included.

**Example**

```ruby
contact = client.contacts.set_audiences(
  "grace@example.com",
  audienceIds: ["aud_9f2c4b7e1a0d63d84c5f2e7b", "aud_1c4e7a9b2d0f36e85a7c1b4d"]
)

p contact[:audiences].map { |audience| audience[:name] }
```

**Notes**

- `audiences:write` is the only scope checked, because this writes memberships rather than the contact.
- Safe to replay: sending the same list again changes nothing. The SDK retries it after a network failure.
- Read the current list with `get` first when you mean to add one audience to what the contact already has, or use `audiences.add_contact`.

Also available in: API [`PUT /contacts/{email}/audiences`](https://openemail.uk/docs/api/reference/contacts#put-contacts-email-audiences); TypeScript [`contacts.setAudiences()`](https://openemail.uk/docs/sdk/reference/contacts#setAudiences); Python [`contacts.set_audiences()`](https://openemail.uk/docs/python/reference/contacts#setAudiences); CLI [`openemail contacts set-audiences`](https://openemail.uk/docs/cli/reference/contacts#contacts-set-audiences).

### `contacts.list_people`

List everyone on the Contacts page, saved or seen in mail

```ruby
list_people(q: nil, email: nil, sort: nil, blocked: nil, limit: nil, cursor: nil, api_key: nil) -> OpenEmail::PeoplePage
```

Returns one page of the people the Contacts page in the app lists: the saved contacts, and every address seen in mail as the sender or a recipient of a thread's newest message. Each row says whether it is `saved`, how many `threads` it shares with the mailbox and when mail last moved (`lastAt`). A person seen in mail and saved is one row. `list` is the saved contacts alone.

The addresses seen in mail are included only when the key also holds `threads:read`, because they are read out of the mail. Without it the page holds the saved contacts and `seen` is false. Deleted addresses and the mailbox's own addresses are never listed.

`sort:` is `recent`, newest mail first and saved contacts never seen in mail after them, `name`, by name or else by address, ignoring case, or `threads`, most threads first. `blockedBy` on each row names the workspace blocklist rule that blocks it, and `blocked: true` narrows the page to those rows.

Paging is keyset. `limit:` takes 1 to 100 and defaults to 25, and `next_cursor` goes back as `cursor:`, with the same `sort:`, `q:` and `blocked:`, while `has_more?` is true.

Scopes: `contacts:read`.

**Parameters**

- `q` (`String`): Searches names, addresses and notes, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead.
- `email` (`String`): One address only, matched case insensitively: the way to read one person's thread count and last mail.
- `sort` (`String`): `recent` (the default), `name` or `threads`.
- `blocked` (`Boolean`): `true` for only the people the workspace blocklist blocks, whole-domain rules included.
- `limit` (`Integer`): Rows per page, 1 to 100. The server defaults to 25.
- `cursor` (`String`): The `next_cursor` from the previous page. Never build one yourself.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

An `OpenEmail::PeoplePage` with `items`, `has_more?`, `next_cursor` and `seen`. Each person is a Hash with `email`, `displayEmail`, `name`, `saved`, `source`, `notes`, `photoUrl`, `threads`, `lastAt`, `createdAt`, `updatedAt` and `blockedBy`.

**Example**

```ruby
page = client.contacts.list_people(sort: "threads", limit: 50)

page.items.each do |person|
  kind = person[:saved] ? "saved" : "seen in mail"
  puts "#{person[:name] || person[:email]} #{person[:threads]} #{kind}"
end
```

**Notes**

- `email` is lower cased and is what every other contacts method takes. `displayEmail` keeps the capitals the newest message wrote.
- A malformed or stale cursor is a 400 `invalid_cursor`. Start again without one.
- A key limited to particular addresses or domains gets the saved contacts alone, with `seen` false, even when it holds `threads:read`, because the addresses seen in mail would be read from the mail of every address in the workspace.

Also available in: API [`GET /contacts/people`](https://openemail.uk/docs/api/reference/contacts#get-contacts-people); TypeScript [`contacts.listPeople()`](https://openemail.uk/docs/sdk/reference/contacts#listPeople); Python [`contacts.list_people()`](https://openemail.uk/docs/python/reference/contacts#listPeople); CLI [`openemail contacts list-people`](https://openemail.uk/docs/cli/reference/contacts#contacts-list-people).

### `contacts.list_all_people`

Collect everyone on the Contacts page into one array

```ruby
list_all_people(q: nil, email: nil, sort: nil, blocked: nil, limit: nil, cursor: nil, api_key: nil) -> Array<Hash>
```

Follows `next_cursor` from page to page and returns every person `list_people` would list, in the same order and with the same filters. It is how you count the people a blocklist blocks: `client.contacts.list_all_people(blocked: true).size`.

Everything is held in memory before the call returns, and a busy mailbox has seen a great many addresses. Prefer `iterate_people` when you can stop early.

Scopes: `contacts:read`.

**Parameters**

- `q` (`String`): Searches names, addresses and notes, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead.
- `email` (`String`): One address only, matched case insensitively: the way to read one person's thread count and last mail.
- `sort` (`String`): `recent` (the default), `name` or `threads`.
- `blocked` (`Boolean`): `true` for only the people the workspace blocklist blocks, whole-domain rules included.
- `limit` (`Integer`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`String`): A cursor from an earlier page to start after.
- `api_key` (`String`): Overrides the client's API key for every page of this walk.

**Returns**

An Array of Hashes holding every person across all pages.

**Example**

```ruby
blocked = client.contacts.list_all_people(blocked: true, limit: 100)

puts "#{blocked.size} people are blocked"
```

**Notes**

- A failure on any page raises out of the whole call, and the people already fetched are discarded.
- `seen` is not reported here. Read one page with `list_people` to learn whether the key reads the people seen in mail.

Also available in: API [`GET /contacts/people`](https://openemail.uk/docs/api/reference/contacts#get-contacts-people); TypeScript [`contacts.listAllPeople()`](https://openemail.uk/docs/sdk/reference/contacts#listAllPeople); Python [`contacts.list_all_people()`](https://openemail.uk/docs/python/reference/contacts#listAllPeople).

### `contacts.iterate_people`

Stream everyone on the Contacts page one person at a time

```ruby
iterate_people(q: nil, email: nil, sort: nil, blocked: nil, limit: nil, cursor: nil, api_key: nil, &block) -> Enumerator<Hash>
```

Returns an Enumerator that yields the people `list_people` lists, one at a time, or yields each one to a block when given one, and requests the next page only once the current one is drained. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.

Scopes: `contacts:read`.

**Parameters**

- `q` (`String`): Searches names, addresses and notes, up to 200 characters. When nothing matches exactly on the first page, close spellings are returned instead.
- `email` (`String`): One address only, matched case insensitively: the way to read one person's thread count and last mail.
- `sort` (`String`): `recent` (the default), `name` or `threads`.
- `blocked` (`Boolean`): `true` for only the people the workspace blocklist blocks, whole-domain rules included.
- `limit` (`Integer`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`String`): A cursor from an earlier page to start after.
- `api_key` (`String`): Overrides the client's API key for every page of this walk.

**Returns**

An Enumerator of Hashes, one person per step, or yields each one to a block.

**Example**

```ruby
client.contacts.iterate_people(sort: "recent") do |person|
  client.contacts.save(person[:email]) if !person[:saved] && person[:threads].to_i > 5
end
```

**Notes**

- The Enumerator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in: API [`GET /contacts/people`](https://openemail.uk/docs/api/reference/contacts#get-contacts-people); TypeScript [`contacts.iteratePeople()`](https://openemail.uk/docs/sdk/reference/contacts#iteratePeople); Python [`contacts.iterate_people()`](https://openemail.uk/docs/python/reference/contacts#iteratePeople).

### `contacts.save`

Save an address as a contact, or keep one that was recorded

```ruby
save(email, body = nil, api_key: nil, **fields) -> Hash
```

Does what Add to contacts and Keep in contacts do in the app, and is safe to call whatever state the address is in. An address that is not a contact yet becomes one with `source` `manual`. A contact recorded from a send becomes `manual`. A contact already saved keeps what it has. A deleted address is brought back.

`name` replaces the stored name and leaving it out keeps it. `notes` replaces the stored notes and nil clears them. Unlike `create`, an address already in the book is not an error, and unlike `update`, an address not in the book is not one either.

Scopes: `contacts:write`.

**Parameters**

- `email` (`String`, required): The address, trimmed and lower cased on the server.
- `name` (`String`): Up to 200 characters. Left out, the stored name is kept.
- `notes` (`String`): Up to 5,000 characters. `nil` clears the notes, and left out, they are kept.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash, the contact as it stands after the save, with `audiences`.

**Example**

```ruby
contact = client.contacts.save("grace@example.com", name: "Grace Hopper")

puts contact[:source]
```

**Notes**

- The server answers 201 for a new contact and 200 for one that was there. The SDK returns the contact the same way for both, so compare `createdAt` with `updatedAt` if you need to tell them apart.
- Safe to replay, so the SDK retries it after a network failure.
- A new contact joins the default audience, as it does from `create`.

Also available in: API [`PUT /contacts/{email}`](https://openemail.uk/docs/api/reference/contacts#put-contacts-email); TypeScript [`contacts.save()`](https://openemail.uk/docs/sdk/reference/contacts#save); Python [`contacts.save()`](https://openemail.uk/docs/python/reference/contacts#save); CLI [`openemail contacts save`](https://openemail.uk/docs/cli/reference/contacts#contacts-save).

### `contacts.delete_many`

Delete up to 200 contacts in one call

```ruby
delete_many(emails, api_key: nil) -> Hash
```

Deletes every address in `emails` the way `delete` deletes one: a saved contact goes with its notes, its photo and every audience membership, and every address is hidden, so mail sent to it from the app composer does not record it again. Addresses that were only seen in mail are hidden too.

An entry that is not an address comes back in `invalid` and the rest still go through. A repeated address counts once. There is no undo.

Scopes: `contacts:write`.

**Parameters**

- `emails` (`Array<String>`, required): 1 to 200 addresses, matched case insensitively. More than 200, or none, is a 422 on `emails`.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash with `object` set to `contact_batch_delete`, `deleted`, `saved` and `invalid`. `deleted` counts the addresses deleted and hidden, `saved` how many of them were saved contacts.

**Example**

```ruby
result = client.contacts.delete_many(["ada@example.com", "grace@example.com"])

puts "#{result[:deleted]} deleted, #{result[:saved]} of them saved contacts"
```

**Notes**

- Safe to replay: deleting an address twice leaves it deleted and hidden, so the SDK retries it after a network failure.
- No mail is deleted, and nobody is unsubscribed from anything.

Also available in: API [`POST /contacts/batch-delete`](https://openemail.uk/docs/api/reference/contacts#post-contacts-batch-delete); TypeScript [`contacts.deleteMany()`](https://openemail.uk/docs/sdk/reference/contacts#deleteMany); Python [`contacts.delete_many()`](https://openemail.uk/docs/python/reference/contacts#deleteMany); CLI [`openemail contacts delete-many`](https://openemail.uk/docs/cli/reference/contacts#contacts-delete-many).

### `contacts.set_photo`

Upload the photo shown for a contact

```ruby
set_photo(email, data, content_type: nil, api_key: nil) -> Hash
```

Sends the image bytes as the request body, replacing any photo the contact had. PNG, JPEG, WebP and GIF are accepted, up to 5 MB. The server fits the image into a 512 pixel square, stores it as WebP, keeps the first frame of an animation, and answers with the contact and its new `photoUrl`.

`data` is a binary `String`, an `IO` or a `Pathname`. The type is read from `content_type:`, or, when that is left out, from `data` itself: a `content_type` it answers to, or the extension of its file name, as for a `File` or a `Pathname`. Without either the bytes go as `application/octet-stream` and the server refuses them with 422 `invalid_image`.

The address has to be a saved contact already: `save` it first.

Scopes: `contacts:write`.

**Parameters**

- `email` (`String`, required): The contact's address, matched case insensitively.
- `data` (`String, IO or Pathname`, required): The image: a binary `String`, an `IO` or a `Pathname`.
- `content_type` (`String`): `image/png`, `image/jpeg`, `image/webp` or `image/gif`. Required unless `data` carries its own type, such as a `File` or `Pathname` whose name ends in `.png`, `.jpg`, `.jpeg`, `.webp` or `.gif`.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash, the contact with its new `photoUrl`.

**Example**

```ruby
contact = client.contacts.set_photo("grace@example.com", File.binread("photo.jpg"), content_type: "image/jpeg")

puts contact[:photoUrl]
```

**Notes**

- An address that is not a saved contact is a 404 `contact_not_found`.
- A busy image service answers 503 `image_busy`, which the SDK retries like any other 503.
- Every upload gets a new URL, so a cached old photo never shows under the new one.

Also available in: API [`PUT /contacts/{email}/photo`](https://openemail.uk/docs/api/reference/contacts#put-contacts-email-photo); TypeScript [`contacts.setPhoto()`](https://openemail.uk/docs/sdk/reference/contacts#setPhoto); Python [`contacts.set_photo()`](https://openemail.uk/docs/python/reference/contacts#setPhoto); CLI [`openemail contacts set-photo`](https://openemail.uk/docs/cli/reference/contacts#contacts-set-photo).

### `contacts.remove_photo`

Remove a contact photo

```ruby
remove_photo(email, api_key: nil) -> Hash
```

Takes the photo off the contact and deletes the stored image, the way Remove does on a contact in the app. The contact answers with `photoUrl` nil. Removing a photo from a contact that has none changes nothing.

Scopes: `contacts:write`.

**Parameters**

- `email` (`String`, required): The contact's address, matched case insensitively.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash, the contact with `photoUrl` nil.

**Example**

```ruby
client.contacts.remove_photo("grace@example.com")
```

**Notes**

- An address that is not a saved contact is a 404 `contact_not_found`.
- Safe to replay, so the SDK retries it after a network failure.

Also available in: API [`DELETE /contacts/{email}/photo`](https://openemail.uk/docs/api/reference/contacts#delete-contacts-email-photo); TypeScript [`contacts.removePhoto()`](https://openemail.uk/docs/sdk/reference/contacts#removePhoto); Python [`contacts.remove_photo()`](https://openemail.uk/docs/python/reference/contacts#removePhoto); CLI [`openemail contacts remove-photo`](https://openemail.uk/docs/cli/reference/contacts#contacts-remove-photo).

### `contacts.block`

Block an address

```ruby
block(email, api_key: nil) -> Hash
```

Puts the address on the workspace blocklist, the same list `settings.update` edits as `blockedSenders`, so mail from it is refused from then on. This is Block on a contact in the app. A plus tag is dropped: blocking `ada+news@example.com` blocks `ada@example.com`, and every tag of it.

When a rule already blocks the address, a whole-domain rule included, nothing is added: `created` is false and `blockedBy` names that rule. The address does not have to be a contact.

Scopes: `settings:write`.

**Parameters**

- `email` (`String`, required): The address to block.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash with `object` set to `contact_block`, `email`, `blocked` set to true, `blockedBy` and `created`.

**Example**

```ruby
result = client.contacts.block("spammer@example.com")

puts result[:created] ? "blocked" : "already blocked by #{result.dig(:blockedBy, :rule)}"
```

**Notes**

- It needs `settings:write` rather than `contacts:write`, because it writes the blocklist rather than the contact.
- Safe to replay, so the SDK retries it after a network failure.
- An address with fewer than two letters or numbers is refused with 422 `blocklist_entry_too_broad`.
- A key limited to particular addresses or domains gets 422 `capability_unsupported` on `addressAllowlist`, because the blocklist belongs to the whole workspace and filters the mail of every address in it.

Also available in: API [`POST /contacts/{email}/block`](https://openemail.uk/docs/api/reference/contacts#post-contacts-email-block); TypeScript [`contacts.block()`](https://openemail.uk/docs/sdk/reference/contacts#block); Python [`contacts.block()`](https://openemail.uk/docs/python/reference/contacts#block); CLI [`openemail contacts block`](https://openemail.uk/docs/cli/reference/contacts#contacts-block).

### `contacts.unblock`

Unblock an address

```ruby
unblock(email, api_key: nil) -> Hash
```

Takes every workspace blocklist rule that blocks the address off the list and lists them in `removed`. This is Unblock on a contact in the app.

When one of them is a whole-domain rule, in `blockedDomains`, everybody at that domain is unblocked with it, so check `removed` when that matters. Rules set for one address or one domain in the settings are not touched. An address that no rule blocks answers with `removed` empty.

Scopes: `settings:write`.

**Parameters**

- `email` (`String`, required): The address to unblock.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash with `object` set to `contact_block`, `email`, `blocked` set to false and `removed`, each entry of `removed` a Hash with `rule` and `list`.

**Example**

```ruby
result = client.contacts.unblock("friend@example.com")

Array(result[:removed]).each { |hit| puts "#{hit[:list]} #{hit[:rule]}" }
```

**Notes**

- Safe to replay, so the SDK retries it after a network failure.
- A key limited to particular addresses or domains gets 422 `capability_unsupported` on `addressAllowlist`, because the blocklist belongs to the whole workspace and filters the mail of every address in it.

Also available in: API [`DELETE /contacts/{email}/block`](https://openemail.uk/docs/api/reference/contacts#delete-contacts-email-block); TypeScript [`contacts.unblock()`](https://openemail.uk/docs/sdk/reference/contacts#unblock); Python [`contacts.unblock()`](https://openemail.uk/docs/python/reference/contacts#unblock); CLI [`openemail contacts unblock`](https://openemail.uk/docs/cli/reference/contacts#contacts-unblock).

### `contacts.list_threads`

List the conversations with one person

```ruby
list_threads(email, q: nil, sort: nil, limit: nil, cursor: nil, api_key: nil) -> OpenEmail::Page
```

Returns one page of the threads the address wrote or was written to, in every folder: the Mail tab on a contact in the app. Each row is a summary, `subject`, `from`, `receivedAt`, `messageCount`, `hasUnread` and `labels`, and `threads.get` reads the messages behind its `id`.

`limit:` takes 1 to 100 and defaults to 25. Pass `next_cursor` back as `cursor:`, with the same `q:` and `sort:`, while `has_more?` is true.

Scopes: `threads:read`.

**Parameters**

- `email` (`String`, required): The address. It does not have to be a saved contact.
- `q` (`String`): Searches inside those threads, with the mailbox search syntax, up to 200 characters.
- `sort` (`String`): `newest` (the default), `oldest`, `sender` or `subject`.
- `limit` (`Integer`): Threads per page, 1 to 100. The server defaults to 25.
- `cursor` (`String`): The `next_cursor` from the previous page. Never build one yourself.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

An `OpenEmail::Page` of thread Hashes with `items`, `has_more?` and `next_cursor`.

**Example**

```ruby
page = client.contacts.list_threads("ada@example.com", q: "invoice")

page.items.each { |thread| puts "#{thread[:receivedAt]} #{thread[:subject]}" }
```

**Notes**

- It needs `threads:read`, because it reads mail rather than the contact.
- A key limited to particular addresses or domains gets 422 `capability_unsupported` on `addressAllowlist`, because a contact's threads and activity are read from the mail of every address in the workspace.

Also available in: API [`GET /contacts/{email}/threads`](https://openemail.uk/docs/api/reference/contacts#get-contacts-email-threads); TypeScript [`contacts.listThreads()`](https://openemail.uk/docs/sdk/reference/contacts#listThreads); Python [`contacts.list_threads()`](https://openemail.uk/docs/python/reference/contacts#listThreads); CLI [`openemail contacts list-threads`](https://openemail.uk/docs/cli/reference/contacts#contacts-list-threads).

### `contacts.list_all_threads`

Collect every conversation with one person into one array

```ruby
list_all_threads(email, q: nil, sort: nil, limit: nil, cursor: nil, api_key: nil) -> Array<Hash>
```

Follows `next_cursor` from page to page and returns every thread `list_threads` would list for the address, in the same order.

Everything is held in memory before the call returns. Prefer `iterate_threads` when you can stop early.

Scopes: `threads:read`.

**Parameters**

- `email` (`String`, required): The address. It does not have to be a saved contact.
- `q` (`String`): Searches inside those threads, with the mailbox search syntax, up to 200 characters.
- `sort` (`String`): `newest` (the default), `oldest`, `sender` or `subject`.
- `limit` (`Integer`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`String`): A cursor from an earlier page to start after.
- `api_key` (`String`): Overrides the client's API key for every page of this walk.

**Returns**

An Array of Hashes holding every thread across all pages.

**Example**

```ruby
threads = client.contacts.list_all_threads("ada@example.com")

puts "#{threads.size} conversations with Ada"
```

**Notes**

- A failure on any page raises out of the whole call, and the threads already fetched are discarded.

Also available in: API [`GET /contacts/{email}/threads`](https://openemail.uk/docs/api/reference/contacts#get-contacts-email-threads); TypeScript [`contacts.listAllThreads()`](https://openemail.uk/docs/sdk/reference/contacts#listAllThreads); Python [`contacts.list_all_threads()`](https://openemail.uk/docs/python/reference/contacts#listAllThreads).

### `contacts.iterate_threads`

Stream the conversations with one person one thread at a time

```ruby
iterate_threads(email, q: nil, sort: nil, limit: nil, cursor: nil, api_key: nil, &block) -> Enumerator<Hash>
```

Returns an Enumerator that yields the threads `list_threads` lists for the address, one at a time, or yields each one to a block when given one, and requests the next page only once the current one is drained. Nothing is fetched until you consume it, and breaking out of the loop stops the requests.

Scopes: `threads:read`.

**Parameters**

- `email` (`String`, required): The address. It does not have to be a saved contact.
- `q` (`String`): Searches inside those threads, with the mailbox search syntax, up to 200 characters.
- `sort` (`String`): `newest` (the default), `oldest`, `sender` or `subject`.
- `limit` (`Integer`): Page size per request, from 1 to 100, defaulting to 25 on the server.
- `cursor` (`String`): A cursor from an earlier page to start after.
- `api_key` (`String`): Overrides the client's API key for every page of this walk.

**Returns**

An Enumerator of Hashes, one thread per step, or yields each one to a block.

**Example**

```ruby
client.contacts.iterate_threads("ada@example.com") do |thread|
  puts "unread: #{thread[:subject]}" if thread[:hasUnread]
end
```

**Notes**

- The Enumerator is lazy, so an abandoned loop costs only the pages you consumed.

Also available in: API [`GET /contacts/{email}/threads`](https://openemail.uk/docs/api/reference/contacts#get-contacts-email-threads); TypeScript [`contacts.iterateThreads()`](https://openemail.uk/docs/sdk/reference/contacts#iterateThreads); Python [`contacts.iterate_threads()`](https://openemail.uk/docs/python/reference/contacts#iterateThreads).

### `contacts.activity`

Read how mail with one person has gone over a window

```ruby
activity(email, minutes: nil, grain: nil, offset_minutes: nil, api_key: nil) -> Hash
```

Returns the numbers behind the Activity tab on a contact in the app, over a window that ends now: messages received from the address and sent to it per bucket, the threads that moved, the threads whose newest message is theirs and so waits on a reply from the mailbox, when each side last wrote, and the median time each side takes to answer, in milliseconds.

`minutes:` sets how far back the window reaches, 90 days by default. `grain:` sets the bucket width and the key shape, `YYYY-MM-DD`, `YYYY-MM-DDTHH` or `YYYY-MM-DDTHH:MM`, and `offset_minutes:` shifts the boundaries so days break where the reader's day does. `buckets` is sparse and oldest first. Mail in the bin or in spam is left out.

Scopes: `threads:read`.

**Parameters**

- `email` (`String`, required): The address. It does not have to be a saved contact.
- `minutes` (`Integer`): Window length in minutes, from 1 to about 20 years. The server defaults to 90 days.
- `grain` (`String`): Bucket width: `minute`, `hour` or `day`, defaulting to `day`.
- `offset_minutes` (`Integer`): Minutes east of UTC to cut the buckets in, from -840 to 840, defaulting to 0. Pass `Time.now.utc_offset / 60` for the local zone.
- `api_key` (`String`): Overrides the client's API key for this call only.

**Returns**

A Hash with `object` set to `contact_activity`, `email`, `since`, `until`, `grain`, `buckets`, `totals` and `replyTime`. `totals` has `received`, `sent`, `threads`, `waiting`, `lastReceivedAt` and `lastSentAt`, and `replyTime` has `yours` and `theirs`, each nil when there is no reply to measure.

**Example**

```ruby
activity = client.contacts.activity("ada@example.com", minutes: 30 * 24 * 60)
totals = activity[:totals]

puts "#{totals[:received]} in, #{totals[:sent]} out, #{totals[:waiting]} waiting on you"
```

**Notes**

- It needs `threads:read`, because it reads mail rather than the contact.
- Read only, so the SDK retries it after a network failure like any other read.
- A key limited to particular addresses or domains gets 422 `capability_unsupported` on `addressAllowlist`, because a contact's threads and activity are read from the mail of every address in the workspace.

Also available in: API [`GET /contacts/{email}/activity`](https://openemail.uk/docs/api/reference/contacts#get-contacts-email-activity); TypeScript [`contacts.activity()`](https://openemail.uk/docs/sdk/reference/contacts#activity); Python [`contacts.activity()`](https://openemail.uk/docs/python/reference/contacts#activity); CLI [`openemail contacts activity`](https://openemail.uk/docs/cli/reference/contacts#contacts-activity).
