Inbox
Inboxes: an email address your code or agent receives mail at.
inbox = noclick.Inbox.create(local_part="signup")
print(inbox.address)
mail = inbox.wait_for(sender="noreply@github.com", subject_contains="code", timeout_s=300).wait()
print(mail.text)With agent=, every inbound message wakes that agent, in its email
thread's own conversation (message.conversation), and the agent answers on
the thread with a reply tool bound to the thread's latest message. Replies
go to the sender only; an inbox on a domain you bought also writes new mail to
one person at a time (inbox.send). Inboxes have
names (Inbox.from_name). Mail proven to be from one of your users runs as
that user, and senders says whose mail wakes the agent.
class InboxAn email address that receives mail. inbox.address is where to send it.
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| name | str | None | What your code calls it, unique among its user's inboxes (None for one created without a name). |
| address | str | |
| agent_id | str | None | The agent each inbound message wakes (None if none). |
| senders | str | None | Whose mail wakes its agent: |
| conversation | str | None | The agent's conversation each message lands in, a template over |
| user_id | str | None | |
| created_at | Any |
createclassmethodasync
noclick.Inbox.create(
local_part=None, *, name=None, agent=None, domain=None, senders=None,
conversation=None, user=..., client=None
) -> InboxCreate an inbox. local_part asks for the part before the @
(generated when omitted); agent (an noclick.Agent or id) is woken by
every inbound message. name is what your code calls it (unique among its
user's inboxes, noclick.Conflict with existing_id when taken;
Inbox.from_name finds it). domain (a noclick.Domain, its name, or a subdomain
of it) puts the address on your own domain: any address on one you bought, and on one
you connected a subdomain of it (help@mail.acme.com), so its existing mail keeps
working (the bare connected domain is noclick.Conflict).
On an account inbox, a message from one of your users, known by the email on their
User and proven by the message's authenticated sender (a DKIM signature aligned
with it, or SPF), runs as that user, in their connections, whatever senders says; any
other message runs as your account. A user's own inbox runs every message as that
user. senders only says whose mail wakes the agent: "anyone" (the default),
"users" (only mail proven to be from one of your users) or "owner" (only the
inbox's own user, or your account's owner for an account inbox). Mail from anyone
else is still kept and listed, with agent_error saying why it woke no one.
Each email thread is a conversation of its own with the agent (message.conversation):
a message answering one of the inbox's messages, or one of its replies, from the same
correspondent joins that thread. The agent gets a reply tool bound to the thread's
latest message: it answers that sender, on that thread, and nobody else. So a turn that
resumes after an approval, with no new message, still answers on the same thread.
conversation names another conversation instead, a template over {sender} and
{thread}: "mail-{sender}" is one per correspondent, and "owner" puts every
message in the same conversation as agent.message(..., conversation="owner") and a
number's calls with that key. There the reply tool answers the newest email. Mail from
outside the inbox's scope never lands in your own thread with the agent (where your texts
and calls reach it): it takes its thread's own.
NoneNoneNoneNoneNoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The new inbox; address is where to send its mail.
Give your code an address to receive mail at:
inbox = noclick.Inbox.create(local_part="signup")
print(inbox.address) # signup@noclick.emailHand every message that arrives to an agent:
support = noclick.Agent.from_name("support", create_if_missing=True, instructions="Answer customer email.")
inbox = noclick.Inbox.create(local_part="acme-help", agent=support)Created inside as_user, the inbox belongs to that user. Leave out
local_part for a generated address:
with noclick.as_user(noclick.User.from_id("user_42")):
inbox = noclick.Inbox.create()Put the address on a domain you bought, or a subdomain of one you connected. Mail
arrives once the mail records are live (a connected domain lists them in its
records), and role names like help are yours to use there:
support = noclick.Agent.from_name("support-bot")
inbox = noclick.Inbox.create(local_part="help", domain="mail.acme.test", agent=support)
print(inbox.address) # help@mail.acme.testLet only your users' mail wake an account agent; each turn acts for the user who wrote:
concierge = noclick.Agent.from_name("concierge")
inbox = noclick.Inbox.create(local_part="ask", name="ask", agent=concierge, senders="users")Keep each correspondent in one conversation, whatever the subject:
sales = noclick.Agent.from_name("sales")
inbox = noclick.Inbox.create(local_part="sales", agent=sales, conversation="mail-{sender}")from_nameclassmethod
noclick.Inbox.from_name(
name, *, create_if_missing=False, local_part=None, agent=..., domain=None,
senders=..., conversation=..., user=..., client=None
) -> InboxA lazy reference to the inbox called name in the current (or given) user.
With create_if_missing=True your code is the definition: it's created on first
use with the fields Inbox.create takes, and an existing inbox whose agent,
senders or conversation differ is updated to match. Its address (local_part, domain) is
set when it's created and never changes. A field you leave out is never changed,
and agent=None takes the inbox from its agent, as in update().
FalseNoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)An inbox that loads (or is created, or brought in step with your definition) the first time it's used. Nothing is sent yet.
Refer to an inbox by name:
inbox = noclick.Inbox.from_name("support")
print(inbox.address)Or make your code the definition, run at every deploy:
support = noclick.Agent.from_name("support")
inbox = noclick.Inbox.from_name("support", create_if_missing=True, local_part="acme-help", agent=support)
print(inbox.address)On your own domain, waking the agent for anyone's mail:
support = noclick.Agent.from_name("support")
inbox = noclick.Inbox.from_name(
"support", create_if_missing=True, local_part="help", domain="mail.acme.test",
agent=support, senders="anyone",
)
print(inbox.address) # help@mail.acme.testfrom_idclassmethod
noclick.Inbox.from_id(inbox_id, *, client=None, user=...) -> InboxA lazy reference to an inbox by id.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
An inbox that loads the first time you read a field. Nothing is sent yet.
inbox = noclick.Inbox.from_id("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
for message in inbox.messages():
print(message.sender, message.subject)getclassmethodasync
noclick.Inbox.get(inbox_id, *, user=..., client=None) -> InboxLoad an inbox by id now; an unknown id raises noclick.NotFound.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The inbox, loaded.
inbox = noclick.Inbox.get("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
print(inbox.address, inbox.agent_id)listclassmethodasync
noclick.Inbox.list(
*, agent=None, limit=None, user_filter=None, user=..., client=None
) -> list[Inbox]The current user's inboxes (the account's own outside as_user, or those user_filter covers).
agent (an noclick.Agent or id) keeps the ones whose mail wakes it.
NoneReturn at most this many.
None)Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Every matching inbox, newest first, fetched page by page.
for inbox in noclick.Inbox.list():
print(inbox.address, inbox.agent_id)
everyone = noclick.Inbox.list(user_filter=noclick.ALL_USERS) # every user's, plus your account's
supports = noclick.Inbox.list(agent=noclick.Agent.from_name("support"))list_pageclassmethodasync
noclick.Inbox.list_page(
*, cursor=None, limit=None, agent=None, user_filter=None, user=..., client=None
) -> PageOne page of inboxes; pass next_cursor back as cursor, agent as in list.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)NoneWhich users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A page whose data holds the inboxes and whose next_cursor is None on the last page.
page = noclick.Inbox.list_page(limit=50)
while True:
for inbox in page.data:
print(inbox.address)
if page.next_cursor is None:
break
page = noclick.Inbox.list_page(limit=50, cursor=page.next_cursor)updateasync
inbox.update(*, agent=..., senders=..., conversation=...) -> InboxChange which agent its mail wakes (agent=None: none), whose mail
does (senders), or the conversation it lands in (conversation, a
template; None: each thread's own), from the next message.
This inbox, updated. A field you leave out is unchanged.
inbox = noclick.Inbox.from_name("support")
inbox.update(agent=noclick.Agent.from_name("support-v2"))
inbox.update(senders="users") # from now on only your users' mail wakes it
inbox.update(conversation="mail-{sender}") # one conversation per correspondentdeleteasync
inbox.delete() -> TrashItemDelete the inbox: its address stops receiving mail, and it goes to the trash with its mail, restorable for 30 days.
inbox = noclick.Inbox.from_id("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
inbox.delete()messagesasync
inbox.messages(*, after=None, since=None, wait_s=None) -> list[InboxMessage]Messages oldest first, after a message (or its id) if given.
wait_s long-polls until one arrives. since (a timezone-aware datetime)
keeps those received at or after it.
NoneNoneNoneUp to 50 messages. Pass the last one as after for the ones that follow;
an empty list means none arrived in time.
Read what's there:
inbox = noclick.Inbox.get("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
for message in inbox.messages():
print(message.sender, message.subject)Wait up to 30 seconds (at most 50) for anything newer than the last one you read:
inbox = noclick.Inbox.get("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
seen = inbox.messages()
newer = inbox.messages(after=seen[-1], wait_s=30)Or what arrived today, by time:
from datetime import datetime, timezone
inbox = noclick.Inbox.from_name("support")
midnight = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
for message in inbox.messages(since=midnight):
print(message.received_at, message.subject, message.agent_error)messageasync
inbox.message(message_id) -> InboxMessageOne message in full: text, sender, subject and digested attachments.
The message, loaded. An attachment's text appears once extraction,
which runs just after the message arrives, has finished.
inbox = noclick.Inbox.get("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
mail = inbox.message("b41c7e02-95d3-4f6a-8e1b-0a7d2c5f9e63")
print(mail.subject, mail.text)
for attachment in mail.attachments:
print(attachment["name"], attachment.get("text"))threadsasync
inbox.threads(*, cursor=None, limit=None) -> Page[InboxThread]One page of its email threads, the most recently active first.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)Threads without their messages; pass next_cursor back as cursor for the next page.
inbox = noclick.Inbox.from_name("support")
for thread in inbox.threads().data:
print(thread.last_at, thread.correspondent, thread.subject, thread.message_count)threadasync
inbox.thread(thread_id) -> InboxThreadOne thread whole: what it received and the replies it sent, oldest first.
The thread; each message's direction says which way it went.
inbox = noclick.Inbox.from_name("support")
latest = inbox.threads(limit=1).data[0]
for message in inbox.thread(latest.id).messages:
print(message.direction, message.sender, message.text)sendasync
inbox.send(
text=None, *, to=None, subject=None, html=None, thread=None, idempotency_key=None
) -> InboxMessageNew mail from this inbox to one person: business mail you mean to send, never bulk or cold
outreach. Only an inbox on a domain you bought sends new mail (one on noclick.email
replies); up to 100 new emails per address a day.
Args:
text: The message (html instead, or as well).
to: Who it goes to. Optional with thread: the thread's correspondent.
subject: One line. Optional with thread: Re: its subject.
thread: An InboxThread (or its id) to follow up on: the mail lands in the
recipient's copy of the thread, and their answer in the inbox's.
NoneNoneNoneNoneNoneMakes a retried create safe: the same key returns the first result. Generated when omitted.
None)The message as sent: status is sending until it goes (minutes, the first time an
address sends), then sent; delivery_status follows.
inbox = noclick.Inbox.from_name("hello")
inbox.send("Can you deliver 20 kg of rye flour on Saturday?", to="orders@millbrook.test",
subject="Saturday delivery")
latest = inbox.threads(limit=1).data[0]
inbox.send("Following up on Saturday's delivery: does 7am work?", thread=latest)streamiterator
inbox.stream(*, after=None, since=None, wait_s=25) -> Iterator[InboxMessage]Yield every message (from the start, or after after, or received at or
after since) as it arrives; runs until you stop iterating.
async for m in inbox.stream.aio().
NoneNone25Each message in the order it arrived. It never ends on its own, so
break when you're done.
Handle mail as it comes in:
inbox = noclick.Inbox.get("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
for message in inbox.stream():
print(message.sender, message.subject)In async code, iterate stream.aio(), starting after a message you've already handled:
inbox = noclick.Inbox.from_id("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
async for message in inbox.stream.aio(after="b41c7e02-95d3-4f6a-8e1b-0a7d2c5f9e63"):
print(message.subject)wait_forasync
inbox.wait_for(
*, sender=None, subject_contains=None, since=None, timeout_s=None
) -> OperationWatch for the first message matching sender and/or
subject_contains (a sign-up code, a receipt). since (a timezone-aware
datetime) lets a message that arrived at or after it match too, so a wait
started after you trigger the email can't miss it.
NoneNoneNoneNoneAn operation whose .wait() gives the InboxMessage, or raises
noclick.Expired once timeout_s passes without one.
Only mail that arrives after the call matches, so start the wait before
you trigger the email. sender is an address or a domain, matched
only on authenticated mail:
inbox = noclick.Inbox.create(local_part="signup")
waiting = inbox.wait_for(sender="noreply@github.com", subject_contains="code", timeout_s=300)
# ... sign up with inbox.address ...
mail = waiting.wait()
print(mail.subject, mail.text)timeout_s defaults to 5 minutes and goes up to 15:
inbox = noclick.Inbox.get("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
try:
receipt = inbox.wait_for(sender="stripe.com", subject_contains="receipt", timeout_s=900).wait()
except noclick.Expired:
print("No receipt arrived.")Note the time, trigger the email, then wait: a message that landed in between still counts:
from datetime import datetime, timezone
inbox = noclick.Inbox.from_name("signup")
started = datetime.now(timezone.utc)
# ... sign up with inbox.address ...
mail = inbox.wait_for(sender="noreply@github.com", since=started).wait()InboxMessage
class InboxMessageOne email an inbox received (or, in a thread, sent). message.reply("...") answers the sender.
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| inbox_id | str | None | |
| direction | str | None |
|
| status | str | None |
|
| delivery_status | str | None | How mail the inbox sent landed: |
| delivery_error | str | None | Why sent mail failed or bounced, when it did. |
| sender | str | None | Who sent it: the message's From address when a DKIM signature aligned with it verified (as for mail a sending service delivers under its own bounce address), else the envelope sender. |
| sender_source | str | None | Where |
| envelope_sender | str | None | The envelope sender (SMTP MAIL FROM), often a sending service's bounce address. None for mail the inbox sent. |
| authenticated | bool | None | Whether |
| to | Any | |
| subject | str | None | |
| received_at | Any | |
| text | str | The body as text. |
| html | str | None | The HTML body, if it had one. |
| attachments | list[dict[str, Any]] | Attachments: |
| conversation | str | None | The conversation its email thread is with the inbox's agent: every message of one thread shares it. A message from one of your users is in that user's conversations ( |
| agent_error | str | None | Why the message didn't wake the inbox's agent, or why the turn it woke failed: a sender |
| agent_operation | Operation | None | The agent turn this message woke, if it woke one: |
replyasync
message.reply(text) -> InboxReplyReply to the sender (only the sender; the thread is kept).
The reply as sent; delivery_status is the mail provider's word on it.
inbox = noclick.Inbox.get("3d9b6f1e-8a2c-4e57-b0d4-6c1f2a9e7b35")
mail = inbox.message("b41c7e02-95d3-4f6a-8e1b-0a7d2c5f9e63")
sent = mail.reply("Thanks, we've received your invoice.")
print(sent.status, sent.delivery_status)InboxReply
class InboxReplyWhat message.reply(...) sent: status is ours, delivery_status the mail provider's.
| id | str | None | |
| status | str | None | |
| delivery_status | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
InboxThread
class InboxThreadOne email thread of an inbox: what it received from one correspondent and
the replies it sent them. messages (both ways, oldest first) only when it's
read whole (inbox.thread(id)).
| id | str | |
| conversation | str | None | |
| subject | str | None | |
| correspondent | str | None | |
| message_count | int | |
| last_direction | str | |
| snippet | str | None | |
| last_at | Any | |
| messages | list[InboxMessage] | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |