InTouch Hub · Blue Isle Software

Microsoft Exchange Mail

Read Exchange Online mailboxes, folders and attachments, and send mail as a mailbox, through Microsoft Graph.

Provided free and as is, without warranty of any kind — including merchantability, fitness for a particular purpose, and the accuracy or completeness of any result. See the licence. You are responsible for checking what this produces before relying on it.

communicationmsgraphschedulestatus

Microsoft Exchange Mail Tool

Read Exchange Online mailboxes, folders and attachments, and send mail as a mailbox. Built for a monitored or shared mailbox that a job processes on a schedule.

Tool ID

msgraph-mail

Credential Required

Yes — the shared msgraph credential, owned by the msgraph connector. Configure the tenant once and all nine tools use it; rotating the client secret is one edit rather than nine.

It is a server-managed OAuth credential: InTouch mints and refreshes the access token itself and the connector never sees the secret. For unattended work set grant_type to client_credentials (the app-only grant) — an app-only token belongs to the application, so a 06:00 schedule does not run as a person, see only what that person can see, and stop working the day they leave.

Credential Properties

Property Type Default Description
client_id string Required. The Entra app registration's Application (client) ID.
client_secret string Required for app-only. The secret's Value, not its Secret ID.
token_url string Required. https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
grant_type string (blank) client_credentials for unattended app-only use. Blank means the user-delegated refresh-token flow.
scope string https://graph.microsoft.com/.default — resolves to whatever application permissions an admin has consented to.
access_token string Machine-managed. Leave empty; the server mints and persists it.
refresh_token string Delegated credentials only. Not used by the app-only grant.
expires_at string 0 Machine-managed expiry, epoch seconds.

Entra setup

  1. https://entra.microsoft.comApp registrations → your app → API permissions.
  2. Add a permissionMicrosoft GraphApplication permissionsnot Delegated. Picking Delegated is the most common failure: it consents cleanly and then 403s at runtime.
  3. Tick the permissions listed under Permissions below, then Grant admin consent. The Status column must read Granted.
  4. Permissions live in the token, not the portal. A token minted before the grant carries the old claims for about an hour — blank access_token to force a fresh mint.

Permissions

Mail.Read to read, Mail.Send to send.

Operations

1. list_messages — List messages

Messages in a mailbox. Always bound this with a $filter on receivedDateTime. Calls GET /users/{id}/messages.

Properties:

Property Type Default Description
operation string Required. list_messages
id string Required. The MAILBOX: a user id or userPrincipalName (e.g. [email protected]). Required by every operation — there is no 'me' on an app-only credential.
query string Optional. OData query params as a JSON object string. Filter server-side — {"$filter": "receivedDateTime ge 2026-08-01T00:00:00Z", "$select": "subject,from,receivedDateTime", "$top": 50} — because a mailbox is unbounded and draining it page by page is slow and rate-limited.

Published Outputs: - result — the JSON response body - status — HTTP status code

2. get_message — Get one message

One message by id, including its body. Calls GET /users/{id}/messages/{messageId}.

Properties:

Property Type Default Description
operation string Required. get_message
id string Required. The MAILBOX: a user id or userPrincipalName (e.g. [email protected]). Required by every operation — there is no 'me' on an app-only credential.
messageId string Required. Message id. Required by get_message and list_message_attachments.
query string Optional. OData query params as a JSON object string. Filter server-side — {"$filter": "receivedDateTime ge 2026-08-01T00:00:00Z", "$select": "subject,from,receivedDateTime", "$top": 50} — because a mailbox is unbounded and draining it page by page is slow and rate-limited.

Published Outputs: - result — the JSON response body - status — HTTP status code

3. list_message_attachments — List attachments

The attachments on one message. Calls GET /users/{id}/messages/{messageId}/attachments.

Properties:

Property Type Default Description
operation string Required. list_message_attachments
id string Required. The MAILBOX: a user id or userPrincipalName (e.g. [email protected]). Required by every operation — there is no 'me' on an app-only credential.
messageId string Required. Message id. Required by get_message and list_message_attachments.
query string Optional. OData query params as a JSON object string. Filter server-side — {"$filter": "receivedDateTime ge 2026-08-01T00:00:00Z", "$select": "subject,from,receivedDateTime", "$top": 50} — because a mailbox is unbounded and draining it page by page is slow and rate-limited.

Published Outputs: - result — the JSON response body - status — HTTP status code

4. list_mail_folders — List mail folders

The mailbox's folder tree. Calls GET /users/{id}/mailFolders.

Properties:

Property Type Default Description
operation string Required. list_mail_folders
id string Required. The MAILBOX: a user id or userPrincipalName (e.g. [email protected]). Required by every operation — there is no 'me' on an app-only credential.
query string Optional. OData query params as a JSON object string. Filter server-side — {"$filter": "receivedDateTime ge 2026-08-01T00:00:00Z", "$select": "subject,from,receivedDateTime", "$top": 50} — because a mailbox is unbounded and draining it page by page is slow and rate-limited.

Published Outputs: - result — the JSON response body - status — HTTP status code

5. list_folder_messages — List messages in a folder

Messages in one folder. folderId accepts well-known names such as inbox, sentitems, drafts, archive. Calls GET /users/{id}/mailFolders/{folderId}/messages.

Properties:

Property Type Default Description
operation string Required. list_folder_messages
id string Required. The MAILBOX: a user id or userPrincipalName (e.g. [email protected]). Required by every operation — there is no 'me' on an app-only credential.
folderId string Required. Mail folder id, or a well-known name such as inbox, sentitems, drafts, archive. Required by list_folder_messages.
query string Optional. OData query params as a JSON object string. Filter server-side — {"$filter": "receivedDateTime ge 2026-08-01T00:00:00Z", "$select": "subject,from,receivedDateTime", "$top": 50} — because a mailbox is unbounded and draining it page by page is slow and rate-limited.

Published Outputs: - result — the JSON response body - status — HTTP status code

6. send_mail — Send mail

Send as the named mailbox. Returns HTTP 202 with no body. Calls POST /users/{id}/sendMail.

Properties:

Property Type Default Description
operation string Required. send_mail
id string Required. The MAILBOX: a user id or userPrincipalName (e.g. [email protected]). Required by every operation — there is no 'me' on an app-only credential.
body string Required. Message to send, as a JSON object string — {"message": {"subject": "Nightly load", "body": {"contentType": "Text", "content": "finished at 06:12"}, "toRecipients": [{"emailAddress": {"address": "[email protected]"}}]}, "saveToSentItems": true}. Only used by send_mail.
query string Optional. OData query params as a JSON object string. Filter server-side — {"$filter": "receivedDateTime ge 2026-08-01T00:00:00Z", "$select": "subject,from,receivedDateTime", "$top": 50} — because a mailbox is unbounded and draining it page by page is slow and rate-limited.

Published Outputs: - result — the JSON response body - status — HTTP status code

Paging

Graph answers a collection with 100 rows and an @odata.nextLink, so a naive listing returns a fraction of a large tenant with HTTP 200 and no sign anything is missing. This connector follows the continuation link until the collection is exhausted. If it stops at the page cap it says so — the result carries complete: false with a warning, and the step returns WARNING rather than SUCCESS. A page cap must never look like the end of the data.

Notes

The scope is the whole tenant until someone narrows it. Application Mail.Read does not mean one mailbox — it means every mailbox in the organisation, including the executives'. Nothing in Entra narrows it. The control is an Exchange application access policy:

Connect-ExchangeOnline
New-DistributionGroup -Name "InTouch-Mailboxes" -Type Security
New-ApplicationAccessPolicy -AppId <your-app-id> `
  -PolicyScopeGroupId InTouch-Mailboxes@<yourdomain> -AccessRight RestrictAccess `
  -Description "InTouch automation — restricted to the InTouch-Mailboxes group"
Test-ApplicationAccessPolicy -Identity someone@<yourdomain> -AppId <your-app-id>

Grant the consent and skip the policy and you have handed a job server the entire organisation's mail. It works perfectly, which is the problem. Allow up to an hour for the policy to propagate.

Addressed by mailbox, never by "me". /me/messages is delegated-only and answers 400 on an app-only token, so every route here names the mailbox explicitly.

Send returns 202 and no body. That means Exchange accepted it for delivery, not that it was delivered; a later bounce is invisible here.

Filter server-side. A mailbox is unbounded. {"$filter": "receivedDateTime ge 2026-08-01T00:00:00Z", "$select": "subject,from,receivedDateTime", "$top": 50} is the difference between a fast job and a throttled one.

For ordinary outbound notification from a job, prefer InTouch's own messaging over this connector.

Self-Contained JAR

The jar bundles all runtime dependencies; only intouch-tool-api is provided by the server.

Publishes

The keys a later task may reference as {{taskName.key}}. Referencing anything not listed here resolves to nothing at run time.