Mail connectors through the API¶
The concept behind tools is in Tools.
A mail connector gives the model access to one mailbox: search, read, mark, move, save a draft
and, when you allow it, send. Core reads the mailbox over IMAP and sends over SMTP, through your
organisation's own mail servers and with its own login. A connector opens your organisation's
mailbox, such as support@, or on each call the own mailbox of the person the turn is for. A
connector can also notice new mail and start a workflow with it.
You set up a mail connector through the API, as below, or in the console under Tools > Mail
connectors. There you add one on a page of its own, read a connector's servers, folders, sending
limits and whether its watch for new mail is working, edit it into a new version, delete it, and
press Test to have Core log in to the IMAP server, and to the SMTP server too when the connector
sends. For a mailbox per person, a connector's page shows whether you connected your own mailbox,
with Connect your account and Disconnect; Test answers that the login was refused for
such a mailbox, because a test has no person to log in as. Secrets are never shown back. Your key needs the tooling capability, and the hosts of the IMAP and SMTP servers
must be on your organisation's allowed hosts.
Connect an organisation mailbox¶
curl -s -X POST "$CORE_URL/mail_connectors" \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"name": "support",
"description": "The Acme support mailbox. Customers mail their questions here.",
"imap": {"host": "imap.acme.example", "port": 993, "security": "implicit_tls"},
"smtp": {"host": "smtp.acme.example", "port": 587, "security": "starttls"},
"folders": ["INBOX", "Done"],
"drafts_folder": "Drafts",
"from_address": "support@acme.example",
"send_allowed_domains": ["acme.example", "customer.example"],
"auth": {"type": "basic", "username": "support@acme.example", "password": "..."}}'
The answer is 201 with the connector, version 1. tools on the answer names the tools this
version offers.
| Field | What you fill in |
|---|---|
name |
lowercase letters, digits, - and _, at most 48 characters; the tools are called mail__<name>__<operation>. It cannot change after creation |
description |
what this mailbox is for; the model reads it above each tool's own description |
imap |
{host, port, security} of the server the tools read through |
smtp |
the same shape, for sending; leave it out to send nothing |
folders |
the folders the tools may use, ["INBOX"] by default; the first is the folder a tool uses when it names none. A folder name is printable ASCII |
drafts_folder |
the folder for drafts; leave it out to offer no draft tool |
from_address |
the sender of every mail sent and every draft; the model never chooses it |
send_allowed_domains |
the domains a send may address, exactly as written; empty means no sending |
max_recipients |
at most this many to and cc per mail, 10 by default, at most 50 |
sends_per_hour |
at most this many mails per clock hour, 20 by default, at most 1,000 |
auth |
how Core logs in, see Logging in |
mailbox |
the mailbox an application's token opens; only with oauth_client_credentials |
ca_pem |
your own servers' certificate authority, as PEM; leave it out for the public authorities |
region |
where the servers are served, as for an API connector |
watch |
notice new mail in one folder, see New mail |
security is implicit_tls (IMAP 993, SMTP 465) or starttls (IMAP 143, SMTP 587). There is no
unencrypted connection, and the certificate must always carry the host's name.
GET /mail_connectors lists your organisation's connectors, GET /mail_connectors/{id} reads one,
PATCH changes one and DELETE removes one, with every mail it has not sent yet and every
person's consent to it. A delete answers
409 naming the agents while an agent's latest version names one of its tools.
Logging in¶
auth is one of three kinds. The secret is stored encrypted and never returned.
auth.type |
Fields | What Core does |
|---|---|---|
basic |
username, password |
logs in to IMAP and SMTP with the same pair |
oauth_client_credentials |
token_url, client_id, client_secret, scopes, audience |
fetches the application's own token and opens the mailbox in mailbox with it, for example a shared mailbox in Microsoft 365 |
oauth_authorization_code |
authorize_url, token_url, client_id, client_secret (optional), scopes, audience; or client, the name of an OAuth application your operator configured, in place of the first four |
opens the own mailbox of the person on each call, see A person's own mailbox |
mailbox is required with oauth_client_credentials and refused with the other two. The token
endpoint must be on your allowed hosts, as the servers are.
A person's own mailbox¶
With oauth_authorization_code every person has their own mailbox behind the same tools. A call
logs in as the person the turn is for, so two people using the same tool read two different
mailboxes and never see each other's mail.
-
Register the connector with your provider's details. Ask for
openidandemailbesides the scope for IMAP: Core reads the mailbox's address from the ID token the provider hands back at sign-in.curl -s -X POST "$CORE_URL/mail_connectors" \ -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \ -d '{"name": "inbox", "description": "The own mailbox of the person you are working for.", "imap": {"host": "imap.provider.example", "port": 993, "security": "implicit_tls"}, "auth": {"type": "oauth_authorization_code", "authorize_url": "https://id.provider.example/authorize", "token_url": "https://id.provider.example/token", "client_id": "artific-core", "scopes": ["openid", "email", "<your provider's IMAP scope>"]}}' -
Register
<Core's public base URL>/api/v1/oauth/callbackas a redirect URI with your provider. -
Name the person with
useron every request, and ask the person to sign in. This works exactly as for an API connector (A person's own login), with the same routes under/mail_connectors/{id}/authorizations:curl -s -X POST "$CORE_URL/mail_connectors/mail_01J.../authorizations" \ -H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \ -d '{"user": "u_9f31", "return_url": "https://your-app.example/settings"}'Give the
urlfrom the answer to the person.GET .../authorizations/{user}says whether Core holds their consent, andDELETE .../authorizations/{user}forgets it.
At sign-in Core keeps the address the provider names in the ID token, encrypted with the consent.
When the provider says the address is not verified, no mailbox is connected and the person reads
Your sign-in did not confirm a verified email address, so no mailbox was connected., or returns to
your return_url with authorization=denied. The mail server checks the address against the
token, so a person never opens a mailbox they could not already open themselves.
Such a mailbox has no person behind work that runs outside a turn. It is therefore never watched
for itself and never sends for a person; saving a draft does work. The one exception is a service
account's own mailbox. When management names the connector as the account's mailbox, switching
mail_watch on watches it for new mail with the version's watch, each new message a
mail.received event of the account carrying service_account; switching mail_send on lets the
account's turns send through it under the version's sending rules, and any other turn reads This
mailbox sends only for its service account. The authorizations routes answer 409 for a
connector of another kind.
The tools¶
Which tools a version offers follows from what it configures. Every folder a tool may name is a
choice from folders.
| Tool | Offered when | What it does |
|---|---|---|
mail__<name>__search |
always | searches one folder by from, to, subject, since, before and unseen; answers the newest matches, 20 by default and at most 50, each with its uid, sender, recipients, subject, date and whether it is seen or flagged |
mail__<name>__read |
always | reads one message by uid: its headers, its text (up to 32 KiB) and its attachments, which are kept as artifacts. A message over 25 MiB is not fetched |
mail__<name>__mark |
always | sets or clears seen (seen) and flagged (flagged) on one message |
mail__<name>__move |
folders names at least two folders |
moves one message to another allowed folder |
mail__<name>__draft |
a drafts_folder and a from_address |
saves a plain-text mail as a draft, for a person to review and send |
mail__<name>__send |
an smtp, a from_address and at least one domain in send_allowed_domains |
sends a plain-text mail, see Sending |
Searching and reading never mark anything seen. A draft and a mail take to, cc, subject and
body, and with reply_to_uid a reply to a message, with the headers that keep a reply with that
message.
Offer the tools by name, {"type": "tool", "name": "mail__support__search"}, or all at once with a
toolset holding the pattern mail__support__*.
Sending¶
Everything that can stop a mail is decided while the model waits: every recipient is one address in
exactly one of the send_allowed_domains, there are at most max_recipients of them, the SMTP host
is allowed, and the mail takes one of the sends_per_hour places of this clock hour.
The model then reads The mail is queued and will be sent. The mail is sent after the turn has
finished; a turn that fails afterwards sends nothing. A place in the hour such a turn already took
stays spent. Core retries while the server is unreachable or answers with a temporary error; every
attempt carries the same Message-ID, so a repeat can be recognised as the same mail. When the
server refuses the mail permanently, Core does not try again.
How it ended is in the audit trail: mail_connector.mail_sent,
mail_connector.mail_refused or mail_connector.mail_dropped, with ids and without addresses or
text. However it ends, Core no longer keeps the mail's text afterwards.
New mail¶
Set watch to notice new mail in one folder:
curl -s -X PATCH "$CORE_URL/mail_connectors/mail_01J..." \
-H "Authorization: Bearer $CORE_KEY" -H "Content-Type: application/json" \
-d '{"watch": {"folder": "INBOX", "poll_seconds": 300}}'
| Field | What it is |
|---|---|
folder |
the folder Core watches; one of folders |
poll_seconds |
how often Core asks for new mail, 300 by default, at least 60 unless your operator set another floor, at most 86,400 (a day) |
enabled |
whether Core watches the folder, true by default |
Each new message in that folder becomes one mail.received event, carrying the connector's id in
mail_connector, the folder and the uid, and never an address, a subject or a text. Mail that
was already in the folder when the watch began does not count as new. One poll announces at most
100 new messages, oldest first; the rest follow on the next poll. How to start a workflow with
it is in New mail.
A read of the connector shows how the watch is doing:
watch_status |
Meaning |
|---|---|
pending |
Core has not read the folder yet |
ok |
the last poll worked |
denied |
the host or its address is not allowed |
auth_failed |
the server refused the login |
unreachable |
the server did not answer |
refused |
the server refused the folder |
watch_polled_at says when Core last looked. After a failure Core waits longer each time, up to an
hour, before it tries again. {"watch": null} stops the watch. Choosing another folder starts
afresh: Core then counts only mail that arrives after that.
Test a connector¶
POST /mail_connectors/{id}/test logs in to the IMAP server and opens the first folder read-only,
and for a connector that sends, logs in to the SMTP server too. Nothing is stored and nothing is
sent.
{"imap": {"ok": true, "code": "ok"}, "smtp": {"ok": false, "code": "auth_failed"}}
code is ok, denied (the host or its address is not allowed), unreachable, auth_failed or
refused; smtp is null for a connector that does not send. A person's mailbox has no person
during the test, and therefore answers auth_failed for IMAP.
Versions¶
A connector is versioned. A PATCH that changes what the connector does makes a new version; a
PATCH of only auth, mailbox or region makes none. An explicit null switches a tool off:
smtp, from_address or "send_allowed_domains": [] stops sending, and drafts_folder or
from_address the draft.
GET /mail_connectors/{id}/versions lists the versions, newest first (?order=asc for oldest first), and
GET /mail_connectors/{id}/versions/{version} reads one. Pin a version as for any connector:
{"type": "tool", "name": "mail__support__read", "version": 2} (Pin a connector
version).
A mail connector does not go into a publication: each organisation connects its own mailbox.
What the model reads when something fails¶
The model reads a fixed sentence, never the server's own words, which can name an address or a subject. The turn goes on.
| The model reads | What happened |
|---|---|
Denied by policy. |
the host is not on your allowed hosts, the key's rules refuse the tool, or the deployment blocks the host or the address it resolves to (a private or loopback address, a name such as localhost or one ending in .internal) |
This mailbox does not allow that folder. |
the folder is not in folders |
Tool authentication failed. |
the server refused the login, or the person's token was revoked |
Mail server unreachable. |
the server did not answer in time |
Mail server refused the request. |
the server refused the command, for example a uid that does not exist |
Message too large to read. |
the message is larger than 25 MiB |
Mail server cannot search this text. |
the server cannot search text outside ASCII |
Mail tools are busy; try again shortly. |
Core has no room to open the mailbox at this moment |
This mailbox may not send to that domain. |
a recipient is outside send_allowed_domains |
Too many recipients for this mailbox. |
more than max_recipients recipients |
This mailbox has sent as many mails this hour as it may. |
this hour's sends_per_hour are spent |
For a person without consent the model reads the sentence with a sign-in URL from Ask the person
to log in, and for a request that names no user the sentence This tool
needs a person's login, and the request named no user.
At registration or on a change:
| Status | When |
|---|---|
409 |
a mail connector with this name already exists; or, on DELETE, an agent still names one of its tools |
422 |
a field does not fit, for example watch.folder outside folders, a poll_seconds below the floor, send_allowed_domains without smtp and from_address, mailbox with the wrong kind of auth, or an auth.client your deployment does not configure |
503 |
the deployment has no key to encrypt the secret with, or, for a person's own mailbox, no public address for the sign-in to return to; ask your operator |
Every route has a twin under /tenants/{tenant_id}/mail_connectors for an administrator working
with an administrator token, with the same fields and answers.