npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ms365

v0.2.4

Published

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

Downloads

598

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": "[email protected]",
    "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 [email protected] \
  --subject "Hello" \
  --body "Just checking in."

# Send to multiple recipients with CC
ms365 email send \
  --to [email protected],[email protected] \
  --cc [email protected] \
  --subject "Team update" \
  --body "Please see the latest notes."

# Send an HTML email marked high importance
ms365 email send \
  --to [email protected] \
  --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 [email protected] \
  --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."
  }
}

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:[email protected] | 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:[email protected]"
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 [email protected],[email protected] \
  --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 [email protected],[email protected] \
  --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 "[email protected]"
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