Skip to content

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.

  1. Register the connector with your provider's details. Ask for openid and email besides 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>"]}}'
    
  2. Register <Core's public base URL>/api/v1/oauth/callback as a redirect URI with your provider.

  3. Name the person with user on 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 url from the answer to the person. GET .../authorizations/{user} says whether Core holds their consent, and DELETE .../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.