Skip to content

dsh-plugin-outlook

Verified

dsh-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.2 or 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

  1. Azure Portal β†’ Microsoft Entra ID β†’ App registrations β†’ New registration. Name it e.g. Outlook Graph connector; no redirect URI is needed for client credentials.

  2. API permissions β†’ Add a permission β†’ Microsoft Graph β†’ Application permissions. Add:

    • Mail.Read
    • Mail.Send
    • Calendars.Read
    • Calendars.ReadWrite

    Then click Grant admin consent β€” without consent the token request fails or the roles are missing.

  3. Certificates & secrets β†’ Client secrets β†’ New client secret. Copy the secret Value immediately (not the Secret ID); it is shown only once.

  4. 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 Q3 and 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 / accessToken live in the DSH profile patch (cordis.patch.yml, mode 600) or in OUTLOOK_* 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_connection acquires 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 / 504 are retried up to three times, honouring Retry-After (both seconds and HTTP-date forms).
  • Pagination: list tools follow @odata.nextLink until the requested count is reached (bounded to 10 pages; count is clamped to 1–100).
  • Errors: GraphError carries the HTTP status, Graph error code and request id for troubleshooting.
  • Send: Graph sendMail answers 202/204 with no body; the tool reports the recipients because Graph returns no message id for sent mail.
  • Time zones: outlook_create_event accepts ISO-8601 local date-times plus an IANA timeZone (default UTC).
  • 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.