A bot can be connected to one mailbox: Outlook / Microsoft 365 or Gmail. Without either, it uses Testpost, the practice mailbox (see Getting started).
You sign in in the system browser, the way installed apps do it, with PKCE and a
redirect to a random port on this machine. Huskarl uses an OAuth client you
create yourself, in Microsoft Entra or Google Cloud. It is described in a file
under ~/.huskarl.
Outlook / Microsoft 365
Create the app registration
-
Go to Microsoft Entra admin center → App registrations → New registration.
- Name: Huskarl.
- Supported account types: choose what fits.
- "Accounts in any organizational directory and personal Microsoft accounts"
gives
"tenant": "common". - Only your own organization gives
"tenant": "<tenant-id>".
- "Accounts in any organizational directory and personal Microsoft accounts"
gives
- Redirect URI: the platform Public client/native (mobile & desktop) with
http://localhost.
-
Under API permissions → Add a permission → Microsoft Graph → Delegated permissions, add:
Mail.ReadWriteMail.SendCalendars.ReadWrite, for the calendarChat.ReadWrite, only if a bot is to use TeamsUser.Readoffline_access
In a company tenant, an administrator may have to grant consent.
-
Copy the Application (client) ID. You need no secret: Huskarl is a public client.
Tell Huskarl about it
Create ~/.huskarl/microsoft.json:
{"client_id": "…", "tenant": "common"}
tenant can be common (work and personal accounts), organizations or a
tenant ID. If it is missing, common is used. Instead of the file, the daemon can
be given HUSKARL_MICROSOFT_CLIENT_ID and HUSKARL_MICROSOFT_TENANT.
The daemon reads this when it starts. Restart it afterwards (quit and start Huskarl, or turn the login item off and on).
Connect
Open the bot's settings (the gear in the thread's top bar) → Connections → Connect on the Outlook / Microsoft 365 row. The browser opens with an account picker. When you are done, the thread says "✓ Connected to Outlook as
".From the terminal, huskarld connect <bot> outlook prints the address to open.
Microsoft issues a new refresh token at every renewal. The new one replaces the old one in the Keychain.
Gmail
Create the OAuth client
- Go to Google Cloud Console and create or select a project.
- Turn on the Gmail API under APIs & Services → Library.
- Under OAuth consent screen: choose External with status Testing, and add your own Gmail address as a test user.
- Under Credentials → Create credentials → OAuth client ID, choose the type Desktop app.
Tell Huskarl about it
Create ~/.huskarl/google.json:
{"client_id": "….apps.googleusercontent.com", "client_secret": "…"}
Or give the daemon HUSKARL_GOOGLE_CLIENT_ID and HUSKARL_GOOGLE_CLIENT_SECRET.
Restart the daemon.
Connect
Settings → Connections → Connect on the Gmail row, or
huskarld connect <bot> gmail. Google asks for consent every time, so a new
connection always gives a new refresh token. While the app is in testing mode,
Google shows an "unverified app" warning. That is expected.
Huskarl asks for two scopes: https://www.googleapis.com/auth/gmail.modify,
and https://www.googleapis.com/auth/calendar.events for
the calendar. Add the second under Data access in the
OAuth client's consent screen, and turn on the Google Calendar API in the
project.
Google's requirements before Gmail can be handed out
gmail.modify is a restricted scope, like every scope that can read mail.
To let anyone other than test users connect, the app has to go through Google's
OAuth verification and a yearly, paid security assessment (CASA) with an approved
third party. In testing mode there are at most 100 test users, and the login
expires after 7 days. For now, the simplest is for each user to create their own
OAuth client, as above. See
phase-3.md.
Microsoft has no such requirement. To share the app with other organizations, you need publisher verification, which is free. Without it, users get an "unverified publisher" warning, and some tenants block consent.
The tools
All three mailboxes have the same six tools, with the prefix outlook_,
gmail_ or testpost_:
| Tool | Class | What |
|---|---|---|
…_list_inbox |
Read | the inbox, newest first: id, sender, subject and an excerpt. Outlook and Gmail take query (a Gmail search, for example is:unread) and max (1–50, default 20) |
…_read_message |
Read | one message in full, ending with the names of its attachments |
…_read_attachment |
Read | one attachment, by name: a PDF as its text (a scan as a picture of its first page), a picture as a picture, a text file as text |
…_create_draft |
Draft | saves a draft. Nothing is sent |
…_archive |
Change internally | Gmail removes the INBOX label, Outlook moves to Archive |
…_send |
Send | sends in your name, as a proposal |
- Everything that is read is outside text and is wrapped. Plain text is preferred; otherwise HTML, styles and scripts are stripped.
- An attachment starts with
From:and its name, like the mail it came with, so account numbers in an invoice PDF are noted against the sender as those in a mail are (see Approvals and rules). PDFs and pictures are read with macOS's ownsipsand a PDF reader in the daemon; nothing is sent anywhere to be read. Other kinds of file (Word, Excel, zip) are refused for now. - Replies land in the same thread. Gmail uses
In-Reply-To,ReferencesandthreadId. Outlook uses Graph'sreplyandcreateReply. - To, Cc and Bcc all count as recipients, so the domain rules see all of them.
- There are deliberately no tools to delete, create filters or rules, forward, or move to other folders. None of them would wait for approval, and each of them could leak or lose mail.
The calendar
A bot connected to Outlook or Gmail also has that account's calendar:
calendar_list(Read): the meetings from now and the next 1 to 14 days, with subject, time, organizer, attendees, place and the invitation's text. All-day events and cancelled meetings are left out. What an invitation says is outside text: anyone can send one.calendar_book: puts a meeting in the calendar, withsubject,start(your time, such as2026-10-20 10:00),minutesorend,attendees,locationandnotes. With attendees it is Send: Outlook or Google sends them invitations in your name, so it waits for your approval like a mail, and domain rules apply to the attendees. The card says when, with whom and where, and what it overlaps: "⚠ Overlaps «Styremøte», 10:30–11:30". Without attendees it only changes your own calendar (Change internally) and is done at once, logged.
A routine can run before each meeting, with only_when: "before_meeting" and
minutes (see Routines).
Reading the calendar came in 0.4.0 and writing to it in 0.5.0. A connection made before has no permission for it, and gets "Outlook does not let Huskarl read the calendar …" or "… write to the calendar …: the connection was made before it asked for that. Connect Outlook again in the bot's settings." (or the same for Gmail). Mail goes on working as before until then: a token is renewed with what was granted when it was made.
The practice mailbox has a practice calendar too, ~/.huskarl/testpost/calendar.json,
empty until something writes to it.
A bot's own address
A bot does not need the whole inbox. Give it its own address in the settings, Its own address, or from the terminal:
huskarld mail-alias <bot> kh+utlegg@firma.no
huskarld mail-alias <bot> none
It then sees and handles only mail sent to that address, To or Cc:
list_inboxlists only those, and routines on new mail count only those.- Reading, archiving or answering any other message is refused before anything is proposed: "That message was not sent to your address kh+utlegg@firma.no: you only see and handle mail sent there."
- The bot is told its address, so it can say where to send things.
Gmail and Microsoft 365 deliver you+anything@company.com to your own inbox
(plus addressing), so the address needs no new account: write or forward a
receipt to kh+utlegg@firma.no, and it reaches only the expense bot. In
Microsoft 365, plus addressing is on unless an administrator has turned it
off. The bot still sends from your own address.
Teams
Microsoft Teams is a connection of its own, Microsoft Teams in the bot's
connections, or huskarld connect <bot> teams. It uses the same app
registration as Outlook, but asks only for Chat.ReadWrite (with
offline_access and User.Read), and keeps its login apart in the Keychain
(no.huskarl.teams). A tenant that wants an administrator's consent for chats
therefore cannot break the mailbox, and connecting one never replaces the
other. A bot can have Outlook and Teams at once.
| Tool | Class | What |
|---|---|---|
teams_list_chats |
Read | your chats, the latest first: id, who or what it is, when it last changed |
teams_read_chat |
Read | the latest messages in a chat (chat, max up to 50), oldest first, without system messages |
teams_send |
Send | writes in a chat in your name, as a proposal: "Write in Teams: Kari Nordmann" |
What is read is outside text. Channels are not reached: reading them needs an administrator's consent in most tenants.
One mailbox per bot
If you connect a bot to Outlook, it replaces Gmail, and the other way round. The login to the old account is deleted from the Keychain if no other bot uses it. Disconnecting does the same.
When a connection changes, an agent that is not in the middle of a turn is restarted, so it sees the new tools. Testpost is offered only to bots without a real mailbox.
The Keychain
The refresh token is stored in the Keychain, under the service
no.huskarl.microsoft or no.huskarl.google, with the email address as the
account. Only the daemon reads it. It never reaches an agent, a prompt or the
database. Access tokens are kept in memory until they have nearly expired.
huskarld check
huskarld check <bot>
tries the bot's mailbox the way the daemon itself uses it: it lists one message
and runs the search for new mail over the last 24 hours. The answer is, for
example, Gmail: ok (3 mails in the inbox in the last day), an error message, or
no mail connection for a bot without Gmail or Outlook.
It also reads the mailbox's calendar for the next seven days, and lists the chats if the bot has Teams, without changing anything:
Outlook: ok (44 mails in the inbox in the last day)
Outlook's calendar: ok (6 meetings in the next 7 days)
Teams: ok (12 chats)
A connection made before it asked for the calendar or the chats says so here, and needs connecting again.
Run it right after you have connected, or after a new version of the daemon. If macOS asks for access to the Keychain, the question then comes while you are sitting there, and not in the middle of a routine at night. Choose "Always Allow".
Not yet
- Sending attachments, and reading Word and Excel files.
- Contacts, and moving or cancelling meetings.
- More than one mailbox per bot.
- Tried end to end against real Gmail and real Microsoft 365. Both are tested against fake servers with the real daemon (see phase-3.md).