Conversas
`threads.list`, `list_all`, `iterate`, `get`, `update`, `trash`, `snooze`, `unsnooze` e `list_attachments`.
Leitura
from openemail import openemail page = openemail.threads.list( folder='inbox', query='from:ada', label_ids=['INBOX', 'IMPORTANT'], limit=25,) next_page = ( openemail.threads.list(folder='inbox', cursor=page['nextCursor']) if page['nextCursor'] else None) thread = openemail.threads.get('thread_…')print(thread['messageCount'], thread['hasUnread'], thread['totalReplies'])A API pagina as threads com um pageToken. O cliente entrega-lho como nextCursor e recebe-o de volta como cursor, tal como em todas as outras listagens, e list_all e iterate seguem-no por si. É opaco: devolva o que lhe foi dado e nunca construa um.
Os filtros da lista são argumentos nomeados em snake_case (label_ids=, date_from=), enquanto as chaves do corpo de um pedido mantêm os nomes em camelCase da API (addLabelIds em update). Uma página e uma conversa voltam como dicionários, por isso leem-se com page['nextCursor'] e thread['messageCount'].
from datetime import datetime, timedelta, timezone from openemail import openemail now = datetime.now(timezone.utc) last_week = openemail.threads.list_all( sort='oldest', date_from=now - timedelta(days=7), date_to=now, from_contacts=True,) for thread in openemail.threads.iterate(sort='sender'): print(thread['id'])sort, date_from, date_to e from_contacts são os controlos próprios da lista de conversas. sort é newest, oldest, sender ou subject, as datas aceitam um datetime ou uma string ISO 8601 e ambos os extremos estão incluídos, e from_contacts mantém o correio cuja mensagem mais recente veio de um contacto guardado. Cada ordem pagina até ao fim sem saltar nem repetir uma conversa. Um datetime sem tzinfo é lido como hora local.
Organização
from datetime import datetime, timedelta, timezone from openemail import openemail openemail.threads.update('thread_…', { 'read': True, 'addLabelIds': ['USER_DONE'], 'removeLabelIds': ['INBOX'],}) openemail.threads.trash('thread_…')openemail.threads.snooze('thread_…', datetime.now(timezone.utc) + timedelta(days=1))openemail.threads.unsnooze('thread_…')O estado de leitura É uma label em todos os backends aqui, por isso viaja com as listas de labels e a ordem é determinista quando define ambas. Pelo menos um dos três campos tem de estar presente.
addLabelIds aceita ids de labels.list e os ids de sistema como ARCHIVE e STARRED. Um id que não indica nenhuma etiqueta é recusado com um 422 label_not_found em vez de ser criado, por isso crie primeiro a etiqueta com labels.create. threads.list(folder='USER_DONE') lista todas as conversas com uma etiqueta, em qualquer pasta.
Anexos de uma mensagem
import base64from pathlib import Path from openemail import openemail files = openemail.threads.list_attachments('thread_…', 'message_…') for file in files: print(file['filename'], file['contentType'], file['size']) if file['content']: name = Path(file['filename']).name Path(name).write_bytes(base64.b64decode(file['content']))content é base64, e uma string vazia quando não foi possível encontrar os bytes armazenados, por isso verifique o seu comprimento antes de descodificar. O texto cifrado de uma mensagem encriptada ESTÁ nesta lista e descarrega-se como qualquer outro ficheiro; a parte de versão PGP/MIME e qualquer assinatura destacada não estão. Guardam os seus ids em encryption.parts e mais nada.
Uma mensagem que chegou encriptada
Este SDK não encripta nem desencripta: não consegue abrir uma mensagem que outra pessoa encriptou, e não consegue enviar uma encriptada. O pedido de envio é recusado se levar um marcador de encriptação, porque um cliente sem chave não tem nada que afirmar uma. As chaves geradas na aplicação OpenEmail vivem no navegador que as criou e não chegam aqui, e quando esse navegador abre uma mensagem selada o texto simples fica nele, e a mensagem armazenada que esta chamada lê continua a ser texto cifrado. O que threads.get lhe dá é o envelope, reconhecido. Uma mensagem que chegou embrulhada em PGP ou S/MIME traz um dicionário encryption, para que um decodedBody vazio deixe de ser a única coisa que lhe é entregue. É a única chave do dicionário cuja ausência não se pode dar ao luxo de adivinhar, e MessageEncryption em openemail.types descreve-a.
import sys from openemail import is_sealed, openemail thread = openemail.threads.get('thread_…') for message in thread['messages']: if not message.get('encryption'): continue if not is_sealed(message): continue print('cannot read this one:', message['encryption']['format'], file=sys.stderr)Decida com is_sealed, nunca pela presença do campo. Dois dos cinco formatos, pgp-signed e smime-signed, descrevem um corpo que chegou EM CLARO ao lado de uma assinatura destacada, por isso condicionar pela presença esconde correio que ninguém precisava de esconder, e o utilizador não o consegue ver nem explicar. is_sealed existe exatamente por essa razão: o servidor declara o conjunto selado uma vez, e uma terceira cópia escrita a partir da união é a cópia que se desvia.
A ausência não é texto simples. encryption falta em todas as mensagens armazenadas antes de a deteção existir, e em tudo o que chegou à caixa de correio por um caminho onde o detetor nunca correu. Regista que ninguém olhou, um facto sobre a nossa cobertura e não sobre o correio, e nada o preenche retroativamente.
Em que é que estes diferem dos restantes
- Cada entrada em
ThreadResource.messagesé umMessageResource, umdict[str, Any]simples cujo tipo não nomeia nenhum campo, nem sequerencryption. Tipar os campos seria o cliente a afirmar uma normalização que ninguém faz. Leiaencryptioncommessage.get('encryption')e decida comis_sealed, porque um cliente que não consegue decidir com base nele lê uma mensagem selada como uma mensagem vazia. - Um pedido que não possa ser servido fielmente é um 422
capability_unsupported, e não uma resposta que parece certa e está silenciosamente errada.
Parâmetros: threads.list
folderstr- Que pasta listar. O servidor assume `inbox` por omissão, por isso omiti-la restringe a listagem em vez de a alargar a tudo. Aplica-se também a uma pesquisa por `query`, a não ser que a própria query nomeie uma pasta com `in:` ou com um `is:` de pasta, como `is:sent`.
querystr- A sintaxe de pesquisa da caixa de correio. As palavras soltas têm de aparecer todas, e cada uma corresponde de forma solta: maiúsculas, acentos e separadores são ignorados e parte de uma palavra maior conta, por isso tanto `min` como `ben jamin` encontram "Benjamin". Uma frase entre aspas é procurada tal como foi escrita, salvo maiúsculas e acentos, por isso `"ben jamin"` não encontra "Ben-Jamin", e as palavras de enchimento são descartadas quando sobra outra coisa para pesquisar. Quando nada corresponde exatamente, são devolvidas em vez disso grafias próximas, pelo que `benjimin` encontra "Benjamin": uma palavra simples, ou o valor de `from:`, `to:`, `cc:`, `subject:`, `body:`, `filename:` ou `label:`, pode diferir do início de uma palavra por um erro de escrita (uma letra trocada, em falta, a mais ou invertida) quando tem de quatro a sete letras, e por dois quando tem oito ou mais, enquanto uma frase entre aspas, uma palavra com um algarismo, uma palavra mais curta e uma palavra excluída continuam a corresponder só de forma exata, e as páginas seguintes pesquisam da mesma forma. Restrinja com operadores como `from:ada`, `label:Invoices`, `is:unread`, `has:pdf`, `before:2026/01/31` e `older_than:1y`, e combine-os com `OR`, parênteses e um `-` à frente; um valor que a pesquisa não consiga usar é ignorado em vez de restringir. As palavras e os operadores `from:`, `to:`, `cc:`, `subject:` e `body:` leem o remetente, os destinatários, o assunto e os primeiros 4000 caracteres do corpo da mensagem mais recente com a marcação removida, enquanto `filename:` e `has:` leem todos os anexos de toda a conversa e as labels e as pastas leem toda a conversa. Restringe o mesmo índice que a listagem sem filtros lê. As mensagens seladas não armazenam texto do corpo, por isso só o seu remetente, destinatários e assunto podem corresponder. Uma palavra simples também corresponde ao nome de qualquer anexo da conversa, seja qual for a mensagem que o trouxe.
label_idsstr | Sequence[str]- Restringe a listagem às conversas que tenham estas etiquetas. O endpoint recebe uma string separada por vírgulas, e o cliente junta por si uma lista ou um tuplo numa só. Não há limite para quantas indica.
limitint- Quantas threads devolver, de 1 a 100. Omitido, o handler usa 25. O valor por omissão vive no handler e não no schema, por isso um valor ausente e um 25 explícito comportam-se da mesma maneira.
cursorstr- O `nextCursor` da página anterior, devolvido tal e qual. É o `pageToken` da API com o nome que todas as outras listagens usam, e é opaco, por isso nunca construa nem edite um.
Resposta: Page[ThreadSummaryResource]
itemslist[ThreadSummaryResource]- Uma entrada por thread nesta página, extraída do envelope `data` da API. Cada entrada é apenas um marcador de objeto e um id. A listagem não leva assunto, excerto, participantes nem labels, por isso qualquer coisa mais implica chamar `threads.get` nas threads que quiser.
items[].idstr- O id da thread, para entregar a `threads.get`, `threads.update` e aos restantes sem alterações. É o mesmo id quer a linha tenha vindo de uma listagem filtrada quer de uma pesquisa por `query`.
hasMorebool- Se existe mais uma página, derivado de `nextCursor` nos casos em que a API não o declara.
nextCursorstr | None- O `nextPageToken` da API, a devolver como `cursor` para a página seguinte, ou `None` quando não há mais páginas. Um token vazio é normalizado para `None`, por isso uma verificação de falsy e uma verificação de `None` concordam.
Referência
threads.list()Referência completathreads.list_all()Referência completathreads.iterate()Referência completathreads.get()Referência completathreads.update()Referência completathreads.trash()Referência completathreads.snooze()Referência completathreads.unsnooze()Referência completathreads.list_attachments()Referência completa