dsh-plugin-outlook
Verifieddsh-plugin-outlook Β· v0.7.0 Β· MIT
DSH plugin: read and send Outlook email, search the mailbox and manage the calendar through Microsoft Graph (application permissions)
Install
dsh plugin add dsh-plugin-outlook Confirm the layer applied with dsh --profile default --dump-config β see the install guide.
Source
Published to npm without a public repository. Inspect the package contents before installing.
Tags
Creators
Readme
dsh-plugin-outlook
DeepSeek Harness (DSH) plugin for Microsoft 365 / Outlook: read and send email, search the mailbox, and read or create calendar events through Microsoft Graph.
Installed as a DSH bundle, it registers seven model-facing tools:
| Tool | What it does |
|---|---|
outlook_read_emails |
Recent messages from a folder (inbox, sentitems, deleteditems, β¦ or a folder id) |
outlook_search_emails |
Full-text search across the mailbox |
outlook_get_email |
One message in full, including the complete body |
outlook_send_email |
Send an HTML email (To / CC / BCC) |
outlook_list_calendar_events |
Calendar events in a time window (default: next 7 days) |
outlook_create_event |
Create an event, optionally inviting attendees |
outlook_check_connection |
Verify credentials, application roles, mail and calendar access |
Requirements
- DSH
0.2.0-rc.2or newer. - Node.js 20+ (uses the built-in
fetch). - A Microsoft 365 / Outlook mailbox.
- An Azure AD app registration with application permissions and admin consent (see Azure setup).
The plugin uses the OAuth2 client-credentials flow. Application
permissions have no signed-in user, so every request addresses the mailbox
explicitly as /users/{userPrincipalName}/...; Graph's /me endpoints are
never used.
Install
In DSH: Plugins β Add plugin, then enter the package name:
dsh-plugin-outlook
Or a local directory while developing:
/absolute/path/to/dsh-plugin-outlook
DSH recognises the package as a bundle because package.json declares:
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
and cordis.patch.yml inserts the outlook loader row.
Azure setup
Azure Portal β Microsoft Entra ID β App registrations β New registration. Name it e.g.
Outlook Graph connector; no redirect URI is needed for client credentials.API permissions β Add a permission β Microsoft Graph β Application permissions. Add:
Mail.ReadMail.SendCalendars.ReadCalendars.ReadWrite
Then click Grant admin consent β without consent the token request fails or the roles are missing.
Certificates & secrets β Client secrets β New client secret. Copy the secret Value immediately (not the Secret ID); it is shown only once.
Note the Application (client) ID and Directory (tenant) ID from the app's Overview page.
Recommended hardening for corporate use: restrict the app to the one mailbox
with an Exchange Online
application access policy
(New-ApplicationAccessPolicy), so a leaked secret cannot read every mailbox
in the tenant.
Configure
1. Fill in the settings
Open Plugins β dsh-plugin-outlook β Configure and fill the form. The plugin
declares a configuration schema, so DSH validates the values and masks
clientSecret / accessToken as secrets:
| Field | Description |
|---|---|
tenantId |
Directory (tenant) ID or a verified domain |
clientId |
Application (client) ID |
clientSecret |
Client secret value |
userPrincipalName |
Mailbox the plugin acts on, e.g. [email protected] |
accessToken |
Optional pre-acquired token; tests only |
graphBaseUrl |
Optional Graph base URL override |
requestTimeoutMs |
Optional per-request timeout (default 30000) |
These values are stored in the profile's cordis.patch.yml, never in the
published npm package. Equivalently you can edit
$DSH_HOME/profiles/<profile>/cordis.patch.yml directly (a patch replaces the
targeted row's whole config):
- id: outlook
config:
tenantId: 00000000-0000-0000-0000-000000000000
clientId: 00000000-0000-0000-0000-000000000000
clientSecret: "your-client-secret-value"
userPrincipalName: [email protected]
2. Or use environment variables
Every field can also come from the environment, so the secret can stay out of the YAML file altogether:
| Environment variable | Field |
|---|---|
OUTLOOK_TENANT_ID |
tenantId |
OUTLOOK_CLIENT_ID |
clientId |
OUTLOOK_CLIENT_SECRET |
clientSecret |
OUTLOOK_USER_PRINCIPAL_NAME |
userPrincipalName |
OUTLOOK_ACCESS_TOKEN |
accessToken |
OUTLOOK_GRAPH_BASE_URL |
graphBaseUrl |
Explicit configuration wins over the environment. If credentials are missing,
the tools answer with an OutlookConfigError that names the missing fields.
3. Verify the setup
Ask the model to run the connection check, or call the tool directly:
Run
outlook_check_connection.
It reports one line per check: Azure AD token, application roles, mailbox read, calendar read. It sends no email and changes nothing.
Usage examples
- "Show my last 10 unread emails."
- "Search the mailbox for
invoice Q3and summarise the results." - "Open the latest message from Contoso and summarise the full body."
- "What is on my calendar next week?"
- "Create a meeting on 2 November at 10:00 Europe/Nicosia with [email protected]."
Security
- No credentials in the package. The published npm tarball contains only code, the bundle patch and documentation. It has no tenant IDs, client IDs, secrets, tokens, mailbox addresses or internal hostnames; releases are scanned for those before publishing.
- Secrets stay local.
clientSecret/accessTokenlive in the DSH profile patch (cordis.patch.yml, mode600) or inOUTLOOK_*environment variables β never in the repository or the npm package. - Least privilege. Only the four application permissions listed above are used. Combine them with an Exchange application access policy to scope the app to a single mailbox.
- No secret logging. Tokens and secrets are never written to logs or tool
output; error messages are additionally redacted so a credential echoed by
Azure AD is replaced with
***. - Read-only diagnostics.
outlook_check_connectionacquires a token, inspects the token's application roles and performs read calls. It never sends email or changes data. - Rotate secrets. Give the client secret the shortest practical lifetime and rotate it via Azure portal; update the profile patch afterwards.
- Vulnerability reporting. See SECURITY.md for the supported versions and the private reporting process.
Behaviour notes
- Auth: OAuth2 client-credentials, token cached until 60 s before expiry,
one transparent refresh and retry on
401. - Retries:
429/503/504are retried up to three times, honouringRetry-After(both seconds and HTTP-date forms). - Pagination: list tools follow
@odata.nextLinkuntil the requested count is reached (bounded to 10 pages;countis clamped to 1β100). - Errors:
GraphErrorcarries the HTTP status, Graph error code and request id for troubleshooting. - Send: Graph
sendMailanswers202/204with no body; the tool reports the recipients because Graph returns no message id for sent mail. - Time zones:
outlook_create_eventaccepts ISO-8601 local date-times plus an IANAtimeZone(defaultUTC). - HTML: message bodies and sent bodies are HTML. The plugin passes them through unchanged; treat email content as untrusted input when acting on it.
Troubleshooting
"This package declares no bundle, so it cannot be managed as a plugin."
This was a defect of versions β€ 0.4.0, which had no dsh.bundle.patch in
package.json. Upgrade to 0.5.0 or newer, then restart DSH.
AADSTS7000215: Invalid client secret provided β the Secret ID was used
instead of the secret Value, or the secret expired. Create a new secret and
update clientSecret.
AADSTS700016 / AADSTS70008 / missing roles β wrong clientId/tenantId,
or admin consent was not granted. Re-check the app registration and click
Grant admin consent.
ErrorInvalidAuthenticationToken β the token expired or is for the wrong
tenant; the plugin normally refreshes automatically. A static accessToken
expires after about an hour β prefer client credentials.
MailboxNotEnabledForRESTAPI / ResourceNotFound β userPrincipalName
does not point to an Exchange Online mailbox, or the mailbox is on-premises.
ErrorAccessDenied β an application permission is missing or an
application access policy excludes the mailbox.
Development
npm install
npm run build # tsc -> dist/
npm run typecheck
npm test # node:test unit suite, network is mocked
npm run test:coverage # unit suite with coverage gates (95% lines / 90% funcs / 70% branches)
npm run verify # typecheck + unit tests + coverage gates
Live integration checks
npm run test:integration exercises the plugin against a real mailbox through
Microsoft Graph: connection check, list, full message body, search, calendar
list, and create-then-delete of a calendar event. Credentials come from the
same OUTLOOK_* environment variables as the plugin:
OUTLOOK_TENANT_ID=... OUTLOOK_CLIENT_ID=... OUTLOOK_CLIENT_SECRET=... \
[email protected] npm run test:integration
The suite is read-only except for one temporary calendar event it creates and
deletes again. Sending mail is opt-in to avoid side effects; set
OUTLOOK_INTEGRATION_SEND=1 to include it (the message goes to the configured
mailbox itself).
The runtime code has no Node-only dependencies: it uses the global fetch and
atob/TextDecoder, so it also works in environments without Buffer. The
only runtime dependency is @deepseek-ai/schemastery for the config schema.
Project layout:
cordis.patch.yml bundle patch that inserts the `outlook` row
src/index.ts Cordis plugin: Config schema + tool definitions
src/outlook-graph.ts Microsoft Graph client (auth, retries, pagination)
test/*.test.mjs node:test unit suites (mocked fetch, coverage gates)
test/integration/ live Microsoft Graph checks (opt-in, real mailbox)
SECURITY.md supported versions and vulnerability reporting
Roadmap
- Move / archive / delete messages and threads by filter.
- Attachments download and upload.
- Delegated (on-behalf-of a user) authentication.
- Draft creation and reply/forward helpers.
- Configurable HTML-to-text conversion for message bodies.
License
MIT β see LICENSE.