Repository navigation
Add a Microsoft 365 mail provider (Microsoft Graph) - #12
Merged
Merged
Conversation
Exchange Online no longer accepts passwords over IMAP and has SMTP AUTH off by default, so the smtp provider can't read an M365 inbox. Without reading replies Harvey can't stop sequences, honor opt-outs or catch bounces. channels.email.provider: microsoft talks to Graph instead: - Signs in once with OAuth authorization code + PKCE (harvey microsoft auth): a public client, so no client secret and no stored password. The rotating refresh token is saved to data/m365_token.json (mode 600). - Drafts, then sends, so the RFC Message-ID is known for bounce and reply matching. Replies use createReply on the prospect's own message, so In-Reply-To/References are right. - Reads the last 7 days of the inbox without marking anything read; dedup stays with the handler. - Refuses to read or send as any mailbox but persona.email, which catches signing in as the admin who approved the app. - get_replies raises when the inbox can't be read, so the heartbeat holds sending instead of treating an outage as "no replies". - The loopback callback listens on 127.0.0.1 and ::1: localhost resolves to both, and browsers differ in which one they try. Setup and behaviour are documented in docs/microsoft365.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # harvey/config.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Exchange Online no longer accepts passwords over IMAP, and SMTP AUTH is off by default in most tenants. So the
smtpprovider can't read a Microsoft 365 inbox. Without reading replies, Harvey can't stop sequences, honor opt-outs or catch bounces, and it keeps emailing people who already answered.What
A new native mail provider,
channels.email.provider: microsoft, built on Microsoft Graph. It follows the same pattern as the Gmail one: raw REST withhttpx, no SDK.harvey microsoft authsigns in once with OAuth authorization code + PKCE. It's a public client, so there's no client secret and no stored password. The rotating refresh token is saved todata/m365_token.json(mode 600 on macOS/Linux; Windows relies on folder ACLs).harvey microsoft testchecks the connection.Message-IDis known for bounce and reply matching.createReplyon the prospect's own message, soIn-Reply-To/Referencesare right. If that message was deleted, the reply goes out as a new email.persona.email. This catches signing in as the admin who approved the app, which is easy to do when a tenant requires admin consent.get_repliesraisesGraphErrorwhen the inbox can't be read, instead of returning[], so the heartbeat holds sending rather than treating an outage as "no replies".127.0.0.1and::1.localhostresolves to both, and a browser that tries::1first never reaches an IPv4-only listener (this hit the first real sign-in on macOS).Setup, the admin-consent path, behaviour and troubleshooting are in
docs/microsoft365.md.Files
harvey/integrations/microsoft.pyMicrosoftProvider+run_auth_flowharvey/integrations/mail_provider.pymicrosoftharvey/config.py,.env.exampleM365_CLIENT_ID,M365_TENANT_IDharvey/cli.pyharvey microsoft auth|testharvey/dashboard.pydocs/microsoft365.md,README.md,CHANGELOG.mdTesting
tests/test_microsoft.py: 12 tests with Graph and the login endpoint faked throughhttpx.MockTransport, no network. They cover:test_brain_provider.py::test_existing_usage_rows_become_claude, fails the same way onmainwithout this change.🤖 Generated with Claude Code
Maintainer follow-up and validation
The original Microsoft 365 implementation and commit by Camilo Cruz (@cvmilocrz) are preserved. Maintainer fixes are separate commits.
Local validation: 65 Microsoft-provider tests passed; the full integrated suite passed with 531 passed and 22 skipped. Regression tests were also run against the original implementation and reproduced failures. Coverage includes later-page unsubscribe handling, malformed pages, failed partial reads, unsafe pagination links, loops, and successful empty send/delete responses. No real emails or live Microsoft Graph calls were made. Live tenant OAuth was not independently retested by the maintainer. The first CI run passed all macOS/Linux jobs and exposed a POSIX-only file-mode assertion on Windows. The assertion is now platform-aware while token rotation/save coverage remains enabled everywhere; documentation explains Windows ACL requirements. All 65 focused tests passed again after this correction. Final GitHub CI results are tracked in the PR checks.