Пагинация
Одна страница, все страницы или по одному элементу за раз в каждом списке с постраничной выдачей.
list, list_all и iterate
У каждого списка с постраничной выдачей есть три метода. list получает одну страницу и возвращает OpenEmail::Page. list_all следует за курсором через все страницы и возвращает один Array. iterate обходит те же страницы по одному элементу: передаёт каждый элемент в блок или возвращает Enumerator, если блок не передан. Все три принимают фильтры списка, limit:, cursor: и api_key:.
page = client.emails.list(status: "failed", limit: 50)page.items.each { |email| puts "#{email[:id]} #{email[:lastError]}" } failures = client.emails.list_all(status: "failed") client.emails.iterate(status: "failed") do |email| puts email[:id]end puts failures.size, page.has_more?Те же три имени повторяются везде, где у пространства имён больше одного списка, и называются по списку, который обходят: list_events, list_all_events и iterate_events в emails, list_deliveries, list_all_deliveries и iterate_deliveries в webhooks и так далее.
OpenEmail::Page
itemsArray<Hash>- Строки этой страницы, извлечённые из конверта `data` API, каждая в виде Hash с ключами типа Symbol. Пусто, если на странице ничего нет.
has_more?Boolean- Есть ли следующая страница. `has_more` без вопросительного знака читает то же значение. Если API не присылает `hasMore`, значение истинно ровно тогда, когда есть `next_cursor`.
next_cursorString or nil- Что передать обратно как `cursor:` для следующей страницы. На последней странице nil.
Страница является объектом Ruby Data, поэтому она заморожена, сравнивается по значению и превращается в Hash через to_h.
Блок или Enumerator
С блоком iterate сразу обходит все страницы и передаёт в блок каждый элемент. Без блока он возвращает Enumerator и ничего не запрашивает, пока вы его не потребите. В обоих случаях он запрашивает следующую страницу, только когда все элементы текущей уже переданы, поэтому всё, что останавливается раньше, останавливает и запросы: first(10) читает столько страниц, сколько нужно для десяти элементов, find останавливается на совпадении, а break в блоке завершает обход.
latest = client.emails.iterate(status: "failed", limit: 100).first(10) invoice = client.emails.iterate(status: "failed").find do |email| email.dig(:tags, :invoice) == "inv_2026_09_4192"end from_api = client.emails.iterate(status: "bounced").lazy.select { |email| email[:source] == "api" }.first(5) p latest.size, invoice&.fetch(:id), from_api.map { |email| email[:id] }Метод Enumerable, которому нужны все элементы, например select, map или count, вызванный прямо на Enumerator, читает все страницы, прежде чем вернуть результат, как это делает list_all. Поставьте перед ними lazy, чтобы выстроить цепочку и всё равно остановиться раньше.
Enumerator начинает обход заново при каждом потреблении, поэтому двойной вызов first(10) на одном и том же Enumerator дважды получает первую страницу. Если результат нужен снова, сохраняйте результат, а не Enumerator.
Продолжение с курсора
Курсор непрозрачен. Сохраните next_cursor последней прочитанной страницы и передайте его обратно как cursor:, чтобы продолжить с этого места в более позднем запросе или в другом процессе. list_all и iterate тоже принимают cursor: и начинают обход после него.
first_page = client.emails.list(status: "failed", limit: 25)saved = first_page.next_cursor if saved rest = client.emails.list_all(status: "failed", cursor: saved) puts rest.sizeendКурсор принадлежит списку и фильтрам, из которых он получен, поэтому отправляйте вместе с ним те же фильтры. Курсор, который список не может распознать, отклоняется с invalid_cursor, и тогда остаётся начать заново без курсора.
limit:
limit: является размером каждой страницы, а не общим количеством. В list это число возвращаемых строк. В list_all и iterate это число строк, которое запрашивает каждый запрос, поэтому большее значение означает меньше обращений к серверу за те же строки. У каждого списка свой диапазон и значение по умолчанию, чаще всего от 1 до 100 и 25, если ничего не передано, а значение вне диапазона отклоняется, а не подрезается. Диапазон указан на странице каждого списка.
Когда обход останавливается
- Когда страница сообщает, что
has_more?равно false. - Когда страница не несёт
next_cursor, поскольку страница, которая заявляет о продолжении, но не называет курсора, зациклилась бы навсегда. - Когда API возвращает тот же курсор, который ему только что передали, по той же причине.
Каждая страница является GET, поэтому, как любое чтение, она сама повторяется, прежде чем что-либо выбросит исключение. Сбой, переживший повторы, выбрасывается из list_all, а уже полученные элементы отбрасываются. В iterate элементы предыдущих страниц к этому моменту уже переданы в блок, поэтому сделайте действия блока безопасными для повторного выполнения или листайте через list и сохраняйте каждый next_cursor, чтобы вторая попытка могла начать с того места, где остановилась первая.
Цепочки и черновики
threads.list и drafts.list вместе со своими list_all и iterate листают с помощью pageToken и nextPageToken из API, а не курсора. Гем скрывает эту разницу: передавайте токен как cursor: и читайте его из next_cursor.
page = client.threads.list(folder: "inbox", limit: 50)later = client.threads.list(folder: "inbox", limit: 50, cursor: page.next_cursor) if page.has_more? p page.items.size, later&.items&.sizeСервер предлагает токен всякий раз, когда страница возвращается заполненной, поэтому has_more? может быть true на странице, которая окажется последней, и тогда следующий вызов не вернёт ни одного элемента.
Страницы с дополнительными данными
Несколько списков отвечают не только строками и возвращают собственный объект Data вместо OpenEmail::Page.
| Метод | Возвращает | Что добавляет |
|---|---|---|
| addresses.list | OpenEmail::AddressBookPage | addresses вместо items, а также unrestricted и domains, с has_more? и next_cursor. |
| addresses.list_all | OpenEmail::AddressBook | Все адреса в addresses, с unrestricted и domains в том виде, в каком их сообщила последняя страница. Это единственный list_all, который возвращает всю адресную книгу, а не Array. addresses.iterate передаёт только адреса. |
| contacts.list_people | OpenEmail::PeoplePage | seen, равное false, когда ключ не может читать адреса, встреченные в почте. list_all_people и iterate_people возвращают только людей. |
| temp_mail.list_messages | OpenEmail::TempMessagesPage | expires_at, момент, когда срок ящика истекает. list_all_messages и iterate_messages возвращают только сообщения. |
| templates.list_sends | OpenEmail::TemplateSends | Постраничная выдача по номеру, а не по курсору: items, total, page и page_size. Следующую страницу запрашивайте через page:. |
| emails.send_batch | OpenEmail::BatchResult | Не страница: items, по одному на каждое отправленное вами сообщение, со счётчиками sent и failed. |
Списки, которые возвращают Hash из API
Некоторые списки листают по смещению, по номеру страницы или по собственному числовому курсору и возвращают разобранное тело как есть, Hash с data, а не OpenEmail::Page. У них нет list_all или iterate, поэтому листать их приходится самостоятельно.
| Метод | Листается через | Что возвращается |
|---|---|---|
| exports.list | limit: и offset: | data, total и hasMore. |
| imports.list_failures | after: и limit: | data и nextCursor, Integer для передачи обратно как after:, равный nil на последней странице. |
| subscriptions.list и subscriptions.list_domains | limit: и offset: | data, total, counts и hasMore. |
| billing.list_invoices | page: и limit: | data, total, page, limit, hasMore и metered. |
offset = 0 loop do batch = client.subscriptions.list(status: "active", limit: 50, offset:) batch[:data].each { |row| puts "#{row[:senderEmail]} #{row[:total]}" } break unless batch[:hasMore] offset += batch[:data].sizeendСписок без постраничной выдачи, например languages.list, labels.list_colors или roles.list_permissions, сразу возвращает Array.