JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 46
  • Score
    100M100P100Q74679F
  • License MIT

CLI tool for Microsoft 365 services — email, calendar, and contacts via Microsoft Graph API

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

  • 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 ms365

Azure App Setup

You need an Azure AD app registration before using this tool. If you already have one, skip ahead to Configuration.

  1. Go to portal.azure.comAzure Active DirectoryApp registrationsNew registration

  2. Give it a name (e.g. ms365-cli), select the appropriate account type, and register

  3. Under Authentication, add a platform: choose Mobile and desktop applications and enable the https://login.microsoftonline.com/common/oauth2/nativeclient redirect URI. Enable Allow public client flows.

  4. Under API permissions, add the following delegated Microsoft Graph permissions:

    Permission Used for
    User.Read Auth status (current user info)
    Mail.Read Email list, read, search
    Mail.Send Email send
    Mail.ReadWrite Email draft, move, delete
    Calendars.ReadWrite Calendar list, create, delete
    Contacts.Read Contacts list, search
    offline_access Refresh tokens (stay logged in)
  5. 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 common

Authentication

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 login

Example 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 logout

Email

email 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 high

email 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>" \
  --html

Example response:

{
  "success": true,
  "data": {
    "id": "AAMkAGI2...",
    "subject": "Ideas for Q3",
    "isDraft": true,
    "createdDateTime": "2024-07-01T08:00:00Z"
  },
  "meta": {
    "message": "Draft created successfully."
  }
}

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 50

email 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... --permanent

email 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-folders flag to discover available folders
  • --all-folders to 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 50

email 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-folders to 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-folders

calendar 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:59

calendar 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-day

Example 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-following

To 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"

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 10

Output 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
fi

Error Handling

  • If you are not logged in, every command (except auth configure, auth login, auth status) exits immediately with auth_required or auth_failed.
  • There is no automatic re-login. If your token expires, run ms365 auth login again.
  • 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 libsecret

A 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 --help

Project 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