Package Exports
- ms365
- ms365/dist/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (ms365) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
ms365
A command-line tool for Microsoft 365 services — email, calendar, and contacts — built on the Microsoft Graph API. All output is structured JSON, making it easy to chain into scripts or AI agent skills.
Table of Contents
- Requirements
- Installation
- Azure App Setup
- Configuration
- Authentication
- Calendar
- Contacts
- Output Format
- Error Handling
- Linux Keychain Setup
- Building from Source
Requirements
- Node.js 18 or later
- A Microsoft 365 account (work, school, or personal)
- An Azure AD app registration with the delegated permissions listed in Azure App Setup
Installation
npm install -g ms365Azure App Setup
You need an Azure AD app registration before using this tool. If you already have one, skip ahead to Configuration.
Go to portal.azure.com → Azure Active Directory → App registrations → New registration
Give it a name (e.g.
ms365-cli), select the appropriate account type, and registerUnder Authentication, add a platform: choose Mobile and desktop applications and enable the
https://login.microsoftonline.com/common/oauth2/nativeclientredirect URI. Enable Allow public client flows.Under API permissions, add the following delegated Microsoft Graph permissions:
Permission Used for User.ReadAuth status (current user info) Mail.ReadEmail list, read, search Mail.SendEmail send Mail.ReadWriteEmail draft, move, delete Calendars.ReadWriteCalendar list, create, delete Contacts.ReadContacts list, search offline_accessRefresh tokens (stay logged in) Copy your Application (client) ID and Directory (tenant) ID — you will need them in the next step.
Configuration
Store your Azure app credentials locally. This only needs to be done once.
ms365 auth configure --client-id <your-client-id> --tenant-id <your-tenant-id>Credentials are saved to ~/.ms365/config.json (mode 0600, readable only by your user).
For a multi-tenant app or a personal Microsoft account, use common as the tenant ID:
ms365 auth configure --client-id <your-client-id> --tenant-id commonAuthentication
Login
Authenticate via the OAuth 2.0 device code flow. No browser automation required — you visit a URL and enter a short code.
ms365 auth loginExample output:
{
"success": true,
"data": {
"message": "To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code ABCD1234 to authenticate.",
"userCode": "ABCD1234",
"verificationUri": "https://microsoft.com/devicelogin",
"expiresIn": 900
}
}Tokens are stored securely in the OS keychain (macOS Keychain, Windows Credential Manager, or Linux libsecret). See Linux Keychain Setup if you are on Linux.
Status
Check whether you are currently authenticated and see account details.
ms365 auth status{
"success": true,
"data": {
"authenticated": true,
"username": "you@example.com",
"name": "Your Name",
"tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"environment": "login.microsoftonline.com"
}
}Logout
Clear all stored tokens.
ms365 auth logoutemail list
List emails in a mailbox folder. Defaults to the inbox, most recent first.
ms365 email list [options]| Option | Description | Default |
|---|---|---|
-c, --count <n> |
Number of messages to return | 20 |
-f, --folder <name> |
Folder name (well-known or folder ID) | inbox |
--select <fields> |
Comma-separated fields to include in response | see below |
Well-known folder names: inbox, drafts, sentitems, deleteditems, junkemail, archive, outbox
Default fields returned: id, subject, from, receivedDateTime, isRead, isDraft, bodyPreview, hasAttachments
Examples:
# List 10 most recent inbox emails
ms365 email list --count 10
# List emails from the Sent Items folder
ms365 email list --folder sentitems
# List drafts
ms365 email list --folder drafts
# Return only id, subject, and from fields
ms365 email list --select "id,subject,from"email read
Read the full content of a specific message by its ID.
ms365 email read <id>Fields returned: id, subject, from, toRecipients, ccRecipients, receivedDateTime, sentDateTime, isRead, isDraft, body, hasAttachments, importance
Example:
ms365 email read AAMkAGI2...email send
Send an email immediately. The message is saved to Sent Items.
ms365 email send --to <addresses> --subject <subject> --body <body> [options]| Option | Description | Required |
|---|---|---|
--to <addresses> |
Comma-separated recipient addresses | Yes |
--subject <subject> |
Email subject | Yes |
--body <body> |
Email body | Yes |
--cc <addresses> |
Comma-separated CC addresses | No |
--bcc <addresses> |
Comma-separated BCC addresses | No |
--html |
Treat body as HTML (default: plain text) | No |
--importance <level> |
low, normal, or high |
No (default: normal) |
Examples:
# Send a plain text email
ms365 email send \
--to alice@example.com \
--subject "Hello" \
--body "Just checking in."
# Send to multiple recipients with CC
ms365 email send \
--to alice@example.com,bob@example.com \
--cc charlie@example.com \
--subject "Team update" \
--body "Please see the latest notes."
# Send an HTML email marked high importance
ms365 email send \
--to alice@example.com \
--subject "Action Required" \
--body "<h1>Urgent</h1><p>Please respond today.</p>" \
--html \
--importance highemail draft
Create a draft email saved to the Drafts folder. Recipients are optional — useful for composing a message to finish later.
ms365 email draft --subject <subject> --body <body> [options]| Option | Description | Required |
|---|---|---|
--subject <subject> |
Email subject | Yes |
--body <body> |
Email body | Yes |
--to <addresses> |
Comma-separated recipient addresses | No |
--cc <addresses> |
Comma-separated CC addresses | No |
--bcc <addresses> |
Comma-separated BCC addresses | No |
--html |
Treat body as HTML | No |
--importance <level> |
low, normal, or high |
No (default: normal) |
The response includes the draft's id, which you can use later to move or delete it.
Examples:
# Create a draft with no recipients yet
ms365 email draft \
--subject "Ideas for Q3" \
--body "Draft notes here..."
# Create a draft addressed to someone, ready to review before sending
ms365 email draft \
--to alice@example.com \
--subject "Proposal" \
--body "<p>Please find the proposal below.</p>" \
--htmlExample response:
{
"success": true,
"data": {
"id": "AAMkAGI2...",
"subject": "Ideas for Q3",
"isDraft": true,
"createdDateTime": "2024-07-01T08:00:00Z"
},
"meta": {
"message": "Draft created successfully."
}
}email search
Search across all mailbox messages using a KQL (Keyword Query Language) query.
ms365 email search <query> [options]| Option | Description | Default |
|---|---|---|
-c, --count <n> |
Number of results to return | 20 |
--select <fields> |
Comma-separated fields to include | see email list defaults |
KQL examples:
| Query | Meaning |
|---|---|
from:alice@example.com |
Emails from Alice |
subject:invoice |
Emails with "invoice" in the subject |
hasAttachments:true |
Emails with attachments |
received>=2024-01-01 |
Emails received on or after Jan 1 2024 |
Examples:
ms365 email search "from:boss@example.com"
ms365 email search "subject:quarterly report" --count 5
ms365 email search "hasAttachments:true" --count 50email move
Move an email to a different folder. Accepts well-known folder names or a folder ID.
ms365 email move <id> <destination>Well-known destinations: inbox, drafts, sentitems, deleteditems, junkemail, archive, outbox
You can also pass a raw folder ID obtained from the Microsoft Graph API.
Examples:
# Move to archive
ms365 email move AAMkAGI2... archive
# Move to junk
ms365 email move AAMkAGI2... junkemail
# Move back to inbox
ms365 email move AAMkAGI2... inbox
# Move to a custom folder by ID
ms365 email move AAMkAGI2... AQMkADYAAAIBDAAAAA==email delete
Delete an email. By default this is a soft delete — the message is moved to the Deleted Items folder, matching standard email client behaviour. Use --permanent to bypass this and hard-delete immediately.
ms365 email delete <id> [options]| Option | Description |
|---|---|
--permanent |
Permanently delete, bypassing Deleted Items |
Examples:
# Soft delete (move to Deleted Items)
ms365 email delete AAMkAGI2...
# Hard delete (cannot be recovered from Deleted Items)
ms365 email delete AAMkAGI2... --permanentemail folders
Manage mail folders — list all available folders, get folder details, or create new custom folders.
ms365 email folders <subcommand> [options]email folders list
List all available mail folders with metadata including unread counts and item totals.
ms365 email folders list [options]| Option | Description | Default |
|---|---|---|
-c, --count <n> |
Max number of folders to return | 50 |
--select <fields> |
Comma-separated fields to include | see below |
Default fields returned: id, displayName, unreadItemCount, totalItemCount, parentFolderId, childFolderCount
Examples:
# List all available folders
ms365 email folders list
# Get folder IDs for use in other commands
ms365 email folders list | jq '.data[] | {id, displayName, unreadItemCount}'email folders info
Get detailed information about a specific folder by name or ID.
ms365 email folders info <folder>Arguments:
<folder>- Folder display name or folder ID
Examples:
# Get info about inbox
ms365 email folders info inbox
# Get info about a custom folder
ms365 email folders info "Archive 2024"
# Get info by folder ID
ms365 email folders info "AQMkADYAAAIBDAAAAA=="email folders create
Create a new custom mail folder.
ms365 email folders create <name> [options]Arguments:
<name>- Display name for the new folder
| Option | Description |
|---|---|
--parent <parentFolderId> |
Parent folder ID (creates at root level if omitted) |
Examples:
# Create a folder at root level
ms365 email folders create "Archive 2024"
# Create a nested folder
ms365 email folders create "Q3 Reports" --parent "AQMkADYAAAIBDAAAAA=="email list (Enhanced)
Enhanced with folder discovery capabilities.
ms365 email list [options]| Option | Description | Default |
|---|---|---|
-c, --count <n> |
Number of messages to return | 20 |
-f, --folder <name> |
Folder name, display name, or folder ID | inbox |
--select <fields> |
Comma-separated fields to include | see below |
--list-folders |
Show available folders instead of listing emails | N/A |
--all-folders |
List emails from all folders | N/A |
New features:
- Auto-resolves folder display names (case-insensitive)
--list-foldersflag to discover available folders--all-foldersto search across all folders at once
Examples:
# List emails using folder display name (case-insensitive)
ms365 email list --folder "Sent Items"
ms365 email list --folder sent
# Discover available folders before listing
ms365 email list --list-folders
# List emails from all folders
ms365 email list --all-folders --count 50email move (Enhanced)
Move an email to a folder — now supports folder display names and discovery.
ms365 email move <id> <destination> [options]| Option | Description |
|---|---|
--list-folders |
Show available folders before moving |
New features:
- Supports well-known folder names (
inbox,sent,drafts, etc.) - Supports folder display names (case-insensitive)
- Supports folder IDs
--list-foldersto discover available folders before moving
Examples:
# Move to a well-known folder
ms365 email move AAMkAGI2... inbox
# Move to a custom folder by display name
ms365 email move AAMkAGI2... "Archive 2024"
# Show available folders first
ms365 email move AAMkAGI2... inbox --list-folderscalendar list
List calendar events within a time window. Defaults to the next 7 days.
ms365 calendar list [options]| Option | Description | Default |
|---|---|---|
-d, --days <n> |
Number of days ahead to look | 7 |
--start <datetime> |
Start datetime in ISO 8601 (overrides --days) |
now |
--end <datetime> |
End datetime in ISO 8601 (overrides --days) |
— |
-c, --count <n> |
Max events to return | 50 |
Events are returned in ascending chronological order.
Fields returned: id, subject, start, end, location, organizer, attendees, isAllDay, isCancelled, bodyPreview, onlineMeeting, webLink, recurrence, seriesMasterId
Examples:
# Next 7 days (default)
ms365 calendar list
# Next 30 days, up to 100 events
ms365 calendar list --days 30 --count 100
# Custom date range
ms365 calendar list \
--start 2024-07-01T00:00:00 \
--end 2024-07-31T23:59:59calendar create
Create a new calendar event.
ms365 calendar create --subject <subject> --start <datetime> --end <datetime> [options]| Option | Description | Required |
|---|---|---|
--subject <subject> |
Event title | Yes |
--start <datetime> |
Start time in ISO 8601 | Yes |
--end <datetime> |
End time in ISO 8601 | Yes |
--timezone <tz> |
IANA timezone name | No (default: UTC) |
--attendees <emails> |
Comma-separated attendee emails | No |
--body <content> |
Event description | No |
--html |
Treat body as HTML | No |
--location <location> |
Location display name | No |
--all-day |
Mark as all-day event | No |
--online-meeting |
Generate a Microsoft Teams meeting link | No |
Examples:
# Simple 1-hour meeting
ms365 calendar create \
--subject "Sync with Alice" \
--start 2024-07-10T09:00:00 \
--end 2024-07-10T10:00:00 \
--timezone "America/New_York"
# Meeting with attendees and Teams link
ms365 calendar create \
--subject "Quarterly Review" \
--start 2024-07-15T14:00:00 \
--end 2024-07-15T15:00:00 \
--timezone "Europe/London" \
--attendees alice@example.com,bob@example.com \
--body "Please review the attached slides before joining." \
--online-meeting
# All-day event
ms365 calendar create \
--subject "Company Holiday" \
--start 2024-07-04T00:00:00 \
--end 2024-07-04T23:59:59 \
--all-dayExample response:
{
"success": true,
"data": {
"id": "AAMkAGI2...",
"subject": "Sync with Alice",
"start": { "dateTime": "2024-07-10T09:00:00.0000000", "timeZone": "America/New_York" },
"end": { "dateTime": "2024-07-10T10:00:00.0000000", "timeZone": "America/New_York" },
"webLink": "https://outlook.office365.com/calendar/item/...",
"onlineMeeting": null
},
"meta": { "message": "Event created successfully." }
}calendar delete
Delete a calendar event by its ID. By default, if the ID belongs to a recurring series instance or series master, the entire series is deleted. Use --single or --this-and-following to narrow the scope.
ms365 calendar delete <id> [options]| Option | Description |
|---|---|
--single |
Delete only this single occurrence, leaving the rest of the series intact |
--this-and-following |
Truncate the series so it ends the day before this occurrence |
Behaviour by case:
| Scenario | Default (no flag) | --single |
--this-and-following |
|---|---|---|---|
| Single non-recurring event | Deleted | Deleted | Deleted |
Series instance (has seriesMasterId) |
Entire series deleted | Only this occurrence deleted | Series truncated before this occurrence |
Series master (has recurrence) |
Entire series deleted | Entire series deleted | N/A (pass an instance ID) |
Examples:
# Delete entire series (pass any instance or the master ID)
ms365 calendar delete AAMkAGI2...
# Delete only this one occurrence
ms365 calendar delete AAMkAGI2... --single
# Delete this and all following occurrences (truncates the series)
ms365 calendar delete AAMkAGI2... --this-and-followingTo find the event ID, use ms365 calendar list and copy the id field from the desired event.
calendar series-create
Create a recurring calendar event series.
ms365 calendar series-create --subject <subject> --start <datetime> --end <datetime> --pattern <pattern> [options]| Option | Description | Required |
|---|---|---|
--subject <subject> |
Event title | Yes |
--start <datetime> |
Start time in ISO 8601 | Yes |
--end <datetime> |
End time in ISO 8601 | Yes |
--pattern <pattern> |
Recurrence pattern | Yes |
--interval <n> |
Interval between occurrences | No (default: 1) |
--days-of-week <days> |
Days for weekly patterns (Mo,Tu,We,Th,Fr,Sa,Su) | No |
--range <type> |
How recurrence ends (endDate, noEnd, numbered) | No (default: noEnd) |
--end-date <date> |
End date for recurrence (ISO 8601) | Conditional* |
--occurrences <n> |
Number of occurrences | Conditional* |
--timezone <tz> |
IANA timezone name | No (default: UTC) |
--attendees <emails> |
Comma-separated attendee emails | No |
--body <content> |
Event description | No |
--html |
Treat body as HTML | No |
--location <location> |
Location display name | No |
--all-day |
Mark as all-day event | No |
--online-meeting |
Generate a Microsoft Teams meeting link | No |
Recurrence patterns: daily, weekly, absoluteMonthly, relativeMonthly, absoluteYearly, relativeYearly
*Conditional: --end-date required if --range is endDate; --occurrences required if --range is numbered
Examples:
# Daily standup for 10 occurrences
ms365 calendar series-create \
--subject "Daily Standup" \
--start 2024-07-10T09:00:00 \
--end 2024-07-10T09:30:00 \
--pattern daily \
--range numbered \
--occurrences 10 \
--timezone "America/New_York"
# Weekly meetings on Mon/Wed/Fri until end of year
ms365 calendar series-create \
--subject "Weekly Sync" \
--start 2024-07-10T14:00:00 \
--end 2024-07-10T15:00:00 \
--pattern weekly \
--days-of-week "Mo,We,Fr" \
--range endDate \
--end-date 2024-12-31 \
--timezone "Europe/London" \
--attendees alice@example.com,bob@example.com \
--online-meeting
# Monthly meeting, recurring indefinitely
ms365 calendar series-create \
--subject "Monthly Review" \
--start 2024-07-15T10:00:00 \
--end 2024-07-15T11:00:00 \
--pattern absoluteMonthly \
--range noEnd \
--timezone "America/New_York" \
--location "Conference Room A"Example response:
{
"success": true,
"data": {
"id": "AAMkAGI2...",
"subject": "Daily Standup",
"start": { "dateTime": "2024-07-10T09:00:00.0000000", "timeZone": "America/New_York" },
"end": { "dateTime": "2024-07-10T09:30:00.0000000", "timeZone": "America/New_York" },
"recurrence": {
"pattern": { "type": "daily", "interval": 1 },
"range": { "type": "numbered", "numberOfOccurrences": 10 }
},
"webLink": "https://outlook.office365.com/calendar/item/..."
},
"meta": { "message": "Recurring event series created successfully." }
}Contacts
contacts list
List contacts from your personal contacts folder, sorted alphabetically.
ms365 contacts list [options]| Option | Description | Default |
|---|---|---|
-c, --count <n> |
Number of contacts to return | 50 |
--select <fields> |
Comma-separated fields to include | see below |
Default fields returned: id, displayName, emailAddresses, mobilePhone, businessPhones, jobTitle, companyName
Examples:
ms365 contacts list
ms365 contacts list --count 100
ms365 contacts list --select "id,displayName,emailAddresses"contacts search
Search contacts by name or email address.
ms365 contacts search <query> [options]| Option | Description | Default |
|---|---|---|
-c, --count <n> |
Max results to return | 25 |
Examples:
ms365 contacts search "Alice"
ms365 contacts search "alice@example.com"
ms365 contacts search "Smith" --count 10Output Format
Every command writes a single JSON object to stdout. Errors are written to stderr and the process exits with code 1.
Success
{
"success": true,
"data": { ... },
"meta": { "count": 5, "nextLink": null }
}The meta field is present on commands that return lists and contains:
| Field | Description |
|---|---|
count |
Number of items returned |
nextLink |
OData next-page URL (present when more results are available), otherwise null |
Error
{
"success": false,
"error": {
"code": "auth_required",
"message": "Not authenticated. Run: ms365 auth login"
}
}Common error codes
| Code | Meaning |
|---|---|
config_missing |
~/.ms365/config.json not found — run ms365 auth configure |
auth_required |
No token in keychain — run ms365 auth login |
auth_failed |
Token expired or invalid — run ms365 auth login |
graph_error |
Microsoft Graph API returned an error |
invalid_argument |
A flag value is invalid |
unknown_command |
Unrecognised command |
Parsing output in scripts
# Get the ID of the first unread inbox message using jq
ms365 email list --count 1 | jq -r '.data[0].id'
# Extract all event subjects for the next 7 days
ms365 calendar list | jq '[.data[].subject]'
# Check if authenticated before running a command
if ms365 auth status | jq -e '.data.authenticated' > /dev/null 2>&1; then
ms365 email list
fiError Handling
- If you are not logged in, every command (except
auth configure,auth login,auth status) exits immediately withauth_requiredorauth_failed. - There is no automatic re-login. If your token expires, run
ms365 auth loginagain. - The device code login session expires after 15 minutes if you do not complete it in the browser.
Linux Keychain Setup
On Linux, keytar requires the libsecret system library. Install it before running npm install -g ms365-cli:
# Debian / Ubuntu
sudo apt-get install libsecret-1-dev
# Fedora / RHEL
sudo yum install libsecret-devel
# Arch Linux
sudo pacman -S libsecretA running secret service daemon (e.g. GNOME Keyring or KWallet) is also required for token storage to work.
Building from Source
# Clone and install dependencies
git clone https://github.com/thecfguy/ms365.git
cd ms365
npm install
# Build
npm run build
# Watch mode during development
npm run dev
# Link globally for local testing
npm link
# Run without linking
node dist/index.js --helpProject structure:
src/
├── index.ts # CLI entrypoint
├── auth/
│ ├── auth.ts # MSAL device-code flow, token management
│ └── keychain.ts # OS keychain read/write wrapper
├── graph/
│ └── client.ts # Authenticated Graph client factory
├── utils/
│ ├── config.ts # ~/.ms365/config.json read/write
│ ├── output.ts # printSuccess / printError helpers
│ └── mailFolders.ts # Reusable mail folder utilities
└── commands/
├── auth.ts # auth command group
├── email.ts # email command group
├── email/
│ ├── list.ts
│ ├── read.ts
│ ├── send.ts
│ ├── draft.ts
│ ├── search.ts
│ ├── move.ts
│ ├── delete.ts
│ └── folders.ts # NEW: folder discovery & management
├── calendar.ts # calendar command group
├── calendar/
│ ├── list.ts
│ ├── create.ts
│ ├── delete.ts
│ ├── series-create.ts # NEW: recurring events
│ └── series-delete.ts # NEW: series deletion
├── contacts.ts # contacts command group
└── contacts/
├── list.ts
└── search.ts