Purple Mail API OpenAPI

Purple Mail is the suite's email system, the category that tools like Microsoft Outlook and Gmail sit in. Every person on the shared roster has a mailbox with Inbox, Sent, Drafts, Archive, Junk and Trash plus custom folders, inbox rules, a signature and automatic replies. Mail is stored per mailbox: sending writes a sent copy for the sender and an inbox copy for each To, Cc and Bcc recipient, linked by conversation_id (the thread) and internet_message_id (the RFC 5322 Message-ID). Start with the current user's own mail: list-my-conversations for threads, list-my-messages for individual emails with OData filters and _search, get-my-conversation for a whole thread. --- **Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.

This reference is readable without JavaScript. Download the OpenAPI document.

Runtime

/api/purple-suite/mail

Purple Mail is the suite's email system, the category that tools like Microsoft Outlook and Gmail sit in. Every person on the shared roster has a mailbox with Inbox, Sent, Drafts, Archive, Junk and Trash plus custom folders, inbox rules, a signature and automatic replies. Mail is stored per mailbox: sending writes a sent copy for the sender and an inbox copy for each To, Cc and Bcc recipient, linked by conversation_id (the thread) and internet_message_id (the RFC 5322 Message-ID). Start with the current user's own mail: list-my-conversations for threads, list-my-messages for individual emails with OData filters and _search, get-my-conversation for a whole thread. --- **Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`.

Servers

[
  {
    "url": "https://marketplace.moveworks.com/api/purple-suite/mail"
  }
]

Authentication schemes

{
  "patAuth": {
    "type": "http",
    "scheme": "bearer"
  }
}

Each operation lists its security requirements. An empty security array permits an unauthenticated request. Use the scheme and headers declared for the selected contract.

Operations

POST /messages/send

Send an email

Composes and delivers a NEW email: the sender gets a copy in sent, each To, Cc and Bcc recipient gets a copy in inbox (Bcc stays hidden from recipients). Recipients' inbox rules run; recipients with automatic replies on answer once. Attachments are metadata only. To answer an existing email use reply so the thread stays linked; to pass one on use forward.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__send__message",
  "summary": "Send an email",
  "description": "Composes and delivers a NEW email: the sender gets a copy in sent, each To, Cc and Bcc recipient gets a copy in inbox (Bcc stays hidden from recipients). Recipients' inbox rules run; recipients with automatic replies on answer once. Attachments are metadata only. To answer an existing email use reply so the thread stays linked; to pass one on use forward.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/SendMailRequest"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "The sender's copy of the delivered email",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailMessage"
          }
        }
      }
    },
    "400": {
      "description": "Missing sender, recipients or subject, or an invalid address"
    }
  }
}

POST /messages/{id}/reply

Reply to an email

Replies from the mailbox that holds the message (its owner_email) to the sender (or its Reply-To), or to everyone on it with reply_all. The subject gets a Re: prefix and the reply joins the same conversation_id with in_reply_to_message_id and references set. Pass the id of a copy in the current user's mailbox (from list-my-messages or get-my-conversation).

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__reply__message",
  "summary": "Reply to an email",
  "description": "Replies from the mailbox that holds the message (its owner_email) to the sender (or its Reply-To), or to everyone on it with reply_all. The subject gets a Re: prefix and the reply joins the same conversation_id with in_reply_to_message_id and references set. Pass the id of a copy in the current user's mailbox (from list-my-messages or get-my-conversation).",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string",
        "example": "MSG-00001"
      },
      "description": "Message ID"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/ReplyMailRequest"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "The sender's copy of the reply",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailMessage"
          }
        }
      }
    },
    "400": {
      "description": "body_content missing, or no one to reply to"
    },
    "404": {
      "description": "Message not found"
    },
    "409": {
      "description": "The message is a draft"
    }
  }
}

POST /messages/{id}/forward

Forward an email

Forwards a message from the mailbox that holds it to new recipients, with an optional comment above the forwarded text. Attachments are carried along. Starts a new conversation.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__forward__message",
  "summary": "Forward an email",
  "description": "Forwards a message from the mailbox that holds it to new recipients, with an optional comment above the forwarded text. Attachments are carried along. Starts a new conversation.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string",
        "example": "MSG-00001"
      },
      "description": "Message ID"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/ForwardMailRequest"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "The sender's copy of the forwarded email",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailMessage"
          }
        }
      }
    },
    "400": {
      "description": "Missing or invalid recipients"
    },
    "404": {
      "description": "Message not found"
    }
  }
}

POST /messages/{id}/send

Send a draft

Delivers a message from the drafts folder to its To, Cc and Bcc recipients and removes the draft. Create a draft first with the store-message tool (folder drafts) or edit one with update.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__send__draft",
  "summary": "Send a draft",
  "description": "Delivers a message from the drafts folder to its To, Cc and Bcc recipients and removes the draft. Create a draft first with the store-message tool (folder drafts) or edit one with update.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string",
        "example": "MSG-00001"
      },
      "description": "Message ID"
    }
  ],
  "responses": {
    "201": {
      "description": "The sender's copy of the delivered email",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailMessage"
          }
        }
      }
    },
    "400": {
      "description": "The draft has no recipients or subject"
    },
    "404": {
      "description": "Draft not found"
    },
    "409": {
      "description": "The message is not a draft, or was already sent"
    }
  }
}

GET /me/messages

List my email

Individual emails in the current user's mailbox (the X-PS-Impersonate-User header, or the instance's default MCP user), every folder unless you filter. Common calls: folder=inbox for the inbox; _filter "isRead eq false" for unread; _filter "isFlagged eq true"; _filter "importance eq 'high'"; _filter "receivedDateTime ge 2026-11-01T00:00:00Z"; _filter "contains(from_email,'vendor')"; _search "budget" (subject, body, sender, recipients, categories); _orderby "receivedDateTime desc"; _top 10. For threads use list-my-conversations instead. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__list__my_messages",
  "summary": "List my email",
  "description": "Individual emails in the current user's mailbox (the X-PS-Impersonate-User header, or the instance's default MCP user), every folder unless you filter. Common calls: folder=inbox for the inbox; _filter \"isRead eq false\" for unread; _filter \"isFlagged eq true\"; _filter \"importance eq 'high'\"; _filter \"receivedDateTime ge 2026-11-01T00:00:00Z\"; _filter \"contains(from_email,'vendor')\"; _search \"budget\" (subject, body, sender, recipients, categories); _orderby \"receivedDateTime desc\"; _top 10. For threads use list-my-conversations instead. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "query",
      "name": "folder",
      "schema": {
        "type": "string",
        "example": "inbox",
        "description": "One of inbox, sent, drafts, archive, junk, trash, or a custom folder id (fld_…) from the folders tool."
      },
      "description": "Only this folder"
    },
    {
      "in": "query",
      "name": "conversation_id",
      "schema": {
        "type": "string"
      },
      "description": "Only one conversation"
    },
    {
      "in": "query",
      "name": "importance",
      "schema": {
        "type": "string",
        "enum": [
          "low",
          "normal",
          "high"
        ]
      },
      "description": "Only this importance"
    },
    {
      "in": "query",
      "name": "isRead",
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ]
      },
      "description": "Only read (true) or unread (false) mail"
    },
    {
      "in": "query",
      "name": "isFlagged",
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ]
      },
      "description": "Only flagged mail"
    },
    {
      "in": "query",
      "name": "has_attachments",
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ]
      },
      "description": "Only mail with attachments"
    },
    {
      "in": "query",
      "name": "inference_classification",
      "schema": {
        "type": "string",
        "enum": [
          "focused",
          "other"
        ]
      },
      "description": "Focused Inbox tab"
    },
    {
      "in": "query",
      "name": "limit",
      "schema": {
        "type": "integer",
        "example": 50
      },
      "description": "Page size (legacy)"
    },
    {
      "in": "query",
      "name": "cursor",
      "schema": {
        "type": "string"
      },
      "description": "Pagination cursor (legacy)"
    },
    {
      "in": "query",
      "name": "search",
      "schema": {
        "type": "string"
      },
      "description": "Free-text search (legacy; prefer _search)"
    },
    {
      "in": "query",
      "name": "$filter",
      "schema": {
        "type": "string"
      },
      "description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
    },
    {
      "in": "query",
      "name": "$select",
      "schema": {
        "type": "string"
      },
      "description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
    },
    {
      "in": "query",
      "name": "$orderby",
      "schema": {
        "type": "string"
      },
      "description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
    },
    {
      "in": "query",
      "name": "$search",
      "schema": {
        "type": "string"
      },
      "description": "Free-text, case-insensitive substring match across the resource's searchable fields."
    },
    {
      "in": "query",
      "name": "$top",
      "schema": {
        "type": "integer"
      },
      "description": "Maximum number of items to return (page size)."
    },
    {
      "in": "query",
      "name": "$skip",
      "schema": {
        "type": "integer"
      },
      "description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
    },
    {
      "in": "query",
      "name": "$count",
      "schema": {
        "type": "boolean"
      },
      "description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated list",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MailMessage"
                }
              },
              "nextCursor": {
                "type": "string",
                "nullable": true,
                "description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
              },
              "total": {
                "type": "integer",
                "description": "Total documents in the collection."
              },
              "@odata.count": {
                "type": "integer",
                "description": "Present only when $count=true: total matches after $filter/$search."
              },
              "@odata.truncated": {
                "type": "boolean",
                "description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "No current user, or an invalid OData query"
    }
  }
}

GET /me/conversations

List my conversations (threads)

The current user's mailbox grouped into threads, newest activity first, each with subject, participants, message_count, unread_count, latest_preview and the folders its copies are in. This is what a mail client's list shows; use it to answer "what's in my inbox" and then get-my-conversation for the whole thread. folder=inbox limits to threads with a copy in the inbox; _filter "unread_count gt 0" for unread threads; _search matches subject, participants and the latest preview. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__list__my_conversations",
  "summary": "List my conversations (threads)",
  "description": "The current user's mailbox grouped into threads, newest activity first, each with subject, participants, message_count, unread_count, latest_preview and the folders its copies are in. This is what a mail client's list shows; use it to answer \"what's in my inbox\" and then get-my-conversation for the whole thread. folder=inbox limits to threads with a copy in the inbox; _filter \"unread_count gt 0\" for unread threads; _search matches subject, participants and the latest preview. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "query",
      "name": "folder",
      "schema": {
        "type": "string",
        "example": "inbox",
        "description": "One of inbox, sent, drafts, archive, junk, trash, or a custom folder id (fld_…) from the folders tool."
      },
      "description": "Only threads with a copy in this folder"
    },
    {
      "in": "query",
      "name": "$filter",
      "schema": {
        "type": "string"
      },
      "description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
    },
    {
      "in": "query",
      "name": "$select",
      "schema": {
        "type": "string"
      },
      "description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
    },
    {
      "in": "query",
      "name": "$orderby",
      "schema": {
        "type": "string"
      },
      "description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
    },
    {
      "in": "query",
      "name": "$search",
      "schema": {
        "type": "string"
      },
      "description": "Free-text, case-insensitive substring match across the resource's searchable fields."
    },
    {
      "in": "query",
      "name": "$top",
      "schema": {
        "type": "integer"
      },
      "description": "Maximum number of items to return (page size)."
    },
    {
      "in": "query",
      "name": "$skip",
      "schema": {
        "type": "integer"
      },
      "description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
    },
    {
      "in": "query",
      "name": "$count",
      "schema": {
        "type": "boolean"
      },
      "description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated list",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MailConversation"
                }
              },
              "nextCursor": {
                "type": "string",
                "nullable": true,
                "description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
              },
              "total": {
                "type": "integer",
                "description": "Total documents in the collection."
              },
              "@odata.count": {
                "type": "integer",
                "description": "Present only when $count=true: total matches after $filter/$search."
              },
              "@odata.truncated": {
                "type": "boolean",
                "description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "No current user, or an invalid OData query"
    }
  }
}

GET /me/conversations/{conversationId}

Get a conversation (whole thread)

Every copy of one thread in the current user's mailbox, oldest first, with full headers and bodies. Use the latest message's id with reply.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__get__my_conversation",
  "summary": "Get a conversation (whole thread)",
  "description": "Every copy of one thread in the current user's mailbox, oldest first, with full headers and bodies. Use the latest message's id with reply.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "conversationId",
      "required": true,
      "schema": {
        "type": "string",
        "example": "CONV-0001"
      },
      "description": "Conversation ID (conversation_id on any message in the thread)"
    }
  ],
  "responses": {
    "200": {
      "description": "The thread",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailConversationDetail"
          }
        }
      }
    },
    "400": {
      "description": "No current user"
    },
    "404": {
      "description": "No such conversation in this mailbox"
    }
  }
}

PATCH /me/conversations/{conversationId}

Archive, move, flag or mark a whole thread read

Applies isRead, isFlagged and/or folder to every copy of the thread in the current user's mailbox in one call (drafts and sent copies keep their folder). Use folder archive to archive a thread, trash to delete it, inbox to restore it.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__update__my_conversation",
  "summary": "Archive, move, flag or mark a whole thread read",
  "description": "Applies isRead, isFlagged and/or folder to every copy of the thread in the current user's mailbox in one call (drafts and sent copies keep their folder). Use folder archive to archive a thread, trash to delete it, inbox to restore it.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "conversationId",
      "required": true,
      "schema": {
        "type": "string",
        "example": "CONV-0001"
      },
      "description": "Conversation ID (conversation_id on any message in the thread)"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/ConversationPatch"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "How many copies changed, and the thread after the change",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "conversation_id": {
                "type": "string"
              },
              "updated": {
                "type": "integer"
              },
              "messages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MailMessage"
                }
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "Invalid change, or no current user"
    },
    "404": {
      "description": "No such conversation in this mailbox"
    }
  }
}

GET /me/folders

List my folders with counts

The current user's folders: inbox, sent, drafts, archive, junk, trash and their custom folders, each with total_item_count and unread_item_count. Custom folder ids (fld_…) are what you pass as folder when moving mail or writing a rule.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__list__my_folders",
  "x-odata": {
    "enabled": false,
    "reason": "A fixed set of six well-known folders plus the mailbox's custom folders, with live counts; not a queryable collection."
  },
  "summary": "List my folders with counts",
  "description": "The current user's folders: inbox, sent, drafts, archive, junk, trash and their custom folders, each with total_item_count and unread_item_count. Custom folder ids (fld_…) are what you pass as folder when moving mail or writing a rule.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "responses": {
    "200": {
      "description": "Folders with counts",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MailFolderCount"
                }
              },
              "total": {
                "type": "integer"
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "No current user"
    }
  }
}

POST /me/folders

Create a folder

Creates a custom folder in the current user's mailbox. Names are unique per mailbox; built-in folder names are reserved.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__create__my_folder",
  "summary": "Create a folder",
  "description": "Creates a custom folder in the current user's mailbox. Names are unique per mailbox; built-in folder names are reserved.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/CreateFolderRequest"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "The new folder",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailFolder"
          }
        }
      }
    },
    "400": {
      "description": "Missing or invalid display_name, or no current user"
    },
    "409": {
      "description": "A folder with that name already exists"
    }
  }
}

GET /me/rules

List my inbox rules

The current user's inbox rules in run order. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__list__my_rules",
  "summary": "List my inbox rules",
  "description": "The current user's inbox rules in run order. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "query",
      "name": "is_enabled",
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ]
      },
      "description": "Only enabled or disabled rules"
    },
    {
      "in": "query",
      "name": "$filter",
      "schema": {
        "type": "string"
      },
      "description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
    },
    {
      "in": "query",
      "name": "$select",
      "schema": {
        "type": "string"
      },
      "description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
    },
    {
      "in": "query",
      "name": "$orderby",
      "schema": {
        "type": "string"
      },
      "description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
    },
    {
      "in": "query",
      "name": "$search",
      "schema": {
        "type": "string"
      },
      "description": "Free-text, case-insensitive substring match across the resource's searchable fields."
    },
    {
      "in": "query",
      "name": "$top",
      "schema": {
        "type": "integer"
      },
      "description": "Maximum number of items to return (page size)."
    },
    {
      "in": "query",
      "name": "$skip",
      "schema": {
        "type": "integer"
      },
      "description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
    },
    {
      "in": "query",
      "name": "$count",
      "schema": {
        "type": "boolean"
      },
      "description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated list",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MailRule"
                }
              },
              "nextCursor": {
                "type": "string",
                "nullable": true,
                "description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
              },
              "total": {
                "type": "integer",
                "description": "Total documents in the collection."
              },
              "@odata.count": {
                "type": "integer",
                "description": "Present only when $count=true: total matches after $filter/$search."
              },
              "@odata.truncated": {
                "type": "boolean",
                "description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "No current user, or an invalid OData query"
    }
  }
}

POST /me/rules

Create an inbox rule

Adds a rule to the current user's mailbox. It runs on every new email that lands in their inbox from now on; call apply-rule to run it over mail already there. conditions are matched case-insensitively (all given conditions must hold); actions need at least one of move_to_folder, mark_as_read, flag, assign_categories.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__create__my_rule",
  "summary": "Create an inbox rule",
  "description": "Adds a rule to the current user's mailbox. It runs on every new email that lands in their inbox from now on; call apply-rule to run it over mail already there. conditions are matched case-insensitively (all given conditions must hold); actions need at least one of move_to_folder, mark_as_read, flag, assign_categories.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/CreateRuleRequest"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "The new rule",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailRule"
          }
        }
      }
    },
    "400": {
      "description": "Invalid conditions or actions, or no current user"
    }
  }
}

POST /rules/{id}/apply

Run a rule over the existing inbox

Runs one rule over every message currently in its owner's inbox and applies its actions to the matches. Returns how many matched and how many changed.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__apply__rule",
  "summary": "Run a rule over the existing inbox",
  "description": "Runs one rule over every message currently in its owner's inbox and applies its actions to the matches. Returns how many matched and how many changed.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string",
        "example": "RULE-0001"
      },
      "description": "Rule ID"
    }
  ],
  "responses": {
    "200": {
      "description": "Counts of matched and changed messages",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "rule_id": {
                "type": "string"
              },
              "matched": {
                "type": "integer"
              },
              "updated": {
                "type": "integer"
              },
              "message_ids": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "404": {
      "description": "Rule not found"
    }
  }
}

POST /admin/reseed

Reset mail to the seed (admin)

Replaces every mailbox (or one, with mailbox) with the default seed content, or with the content posted in the same format as seed-content.json: messages, settings, folders and rules. Instance owners only; not exposed as an MCP tool.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__reseed__mailboxes",
  "x-mcp": false,
  "summary": "Reset mail to the seed (admin)",
  "description": "Replaces every mailbox (or one, with mailbox) with the default seed content, or with the content posted in the same format as seed-content.json: messages, settings, folders and rules. Instance owners only; not exposed as an MCP tool.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "requestBody": {
    "required": false,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "properties": {
            "mailbox": {
              "type": "string",
              "format": "email",
              "description": "Reset only this mailbox"
            },
            "content": {
              "type": "object",
              "description": "Mail content (see seed-content.json); default seed when omitted"
            }
          }
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Counts of what was written"
    },
    "400": {
      "description": "Invalid mailbox or content"
    }
  }
}

POST /admin/import

Import a thread (admin)

Adds conversations to the mailboxes of everyone on them (or only to mailbox) without touching existing mail. format json takes the seed-content.json shape; format eml takes one or more RFC 822 messages, grouped into threads by subject. Instance owners only; not exposed as an MCP tool.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__import__messages",
  "x-mcp": false,
  "summary": "Import a thread (admin)",
  "description": "Adds conversations to the mailboxes of everyone on them (or only to mailbox) without touching existing mail. format json takes the seed-content.json shape; format eml takes one or more RFC 822 messages, grouped into threads by subject. Instance owners only; not exposed as an MCP tool.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "required": [
            "content"
          ],
          "properties": {
            "format": {
              "type": "string",
              "enum": [
                "json",
                "eml"
              ],
              "example": "json"
            },
            "content": {
              "description": "Mail content object (json) or .eml text / list of .eml texts (eml)"
            },
            "mailbox": {
              "type": "string",
              "format": "email",
              "description": "Store copies only in this mailbox"
            }
          }
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "How many mailbox copies were written, and their conversation ids"
    },
    "400": {
      "description": "Invalid content"
    }
  }
}

GET /me/settings

Get my mailbox settings

The current user's display name, signature, time zone and automatic reply (out of office) settings. owner_email here is also the address to use as a filter value elsewhere.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__get__my_settings",
  "summary": "Get my mailbox settings",
  "description": "The current user's display name, signature, time zone and automatic reply (out of office) settings. owner_email here is also the address to use as a filter value elsewhere.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "responses": {
    "200": {
      "description": "Mailbox settings",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailboxSettings"
          }
        }
      }
    },
    "400": {
      "description": "No current user"
    },
    "404": {
      "description": "The current user has no mailbox"
    }
  }
}

PATCH /me/settings

Update my mailbox settings

Turn automatic replies (out of office) on or off, set their message and window (auto_reply_start / auto_reply_end, ISO date-times), or change the signature, display name or time zone.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__update__my_settings",
  "summary": "Update my mailbox settings",
  "description": "Turn automatic replies (out of office) on or off, set their message and window (auto_reply_start / auto_reply_end, ISO date-times), or change the signature, display name or time zone.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/MailboxSettings"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated settings",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailboxSettings"
          }
        }
      }
    },
    "400": {
      "description": "Invalid update, or no current user"
    },
    "404": {
      "description": "The current user has no mailbox"
    }
  }
}

GET /messages

List email across mailboxes

Email in every Purple Mail mailbox. Each copy has owner_email (whose mailbox) and folder; pass owner_email to read one person's mail. For the current user's own mail prefer list-my-messages or list-my-conversations. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "List email across mailboxes",
  "description": "Email in every Purple Mail mailbox. Each copy has owner_email (whose mailbox) and folder; pass owner_email to read one person's mail. For the current user's own mail prefer list-my-messages or list-my-conversations. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "query",
      "name": "limit",
      "schema": {
        "type": "integer",
        "default": 50
      },
      "description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
    },
    {
      "in": "query",
      "name": "cursor",
      "schema": {
        "type": "string"
      },
      "description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
    },
    {
      "in": "query",
      "name": "$filter",
      "schema": {
        "type": "string"
      },
      "description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
    },
    {
      "in": "query",
      "name": "$select",
      "schema": {
        "type": "string"
      },
      "description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
    },
    {
      "in": "query",
      "name": "$orderby",
      "schema": {
        "type": "string"
      },
      "description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
    },
    {
      "in": "query",
      "name": "$search",
      "schema": {
        "type": "string"
      },
      "description": "Free-text, case-insensitive substring match across the resource's searchable fields."
    },
    {
      "in": "query",
      "name": "$top",
      "schema": {
        "type": "integer"
      },
      "description": "Maximum number of items to return (page size)."
    },
    {
      "in": "query",
      "name": "$skip",
      "schema": {
        "type": "integer"
      },
      "description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
    },
    {
      "in": "query",
      "name": "$count",
      "schema": {
        "type": "boolean"
      },
      "description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
    },
    {
      "in": "query",
      "name": "owner_email",
      "schema": {
        "type": "string",
        "format": "email"
      },
      "description": "Only this mailbox"
    },
    {
      "in": "query",
      "name": "folder",
      "schema": {
        "type": "string",
        "example": "inbox",
        "description": "One of inbox, sent, drafts, archive, junk, trash, or a custom folder id (fld_…) from the folders tool."
      },
      "description": "Only this folder"
    },
    {
      "in": "query",
      "name": "conversation_id",
      "schema": {
        "type": "string"
      },
      "description": "Only one conversation"
    },
    {
      "in": "query",
      "name": "from_email",
      "schema": {
        "type": "string",
        "format": "email"
      },
      "description": "Only mail from this sender"
    },
    {
      "in": "query",
      "name": "importance",
      "schema": {
        "type": "string",
        "enum": [
          "low",
          "normal",
          "high"
        ]
      },
      "description": "Only this importance"
    },
    {
      "in": "query",
      "name": "isRead",
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ]
      },
      "description": "Only read (true) or unread (false) mail"
    },
    {
      "in": "query",
      "name": "isFlagged",
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ]
      },
      "description": "Only flagged mail"
    },
    {
      "in": "query",
      "name": "has_attachments",
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ]
      },
      "description": "Only mail with attachments"
    },
    {
      "in": "query",
      "name": "inference_classification",
      "schema": {
        "type": "string",
        "enum": [
          "focused",
          "other"
        ]
      },
      "description": "Focused Inbox tab"
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated list",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MailMessage"
                }
              },
              "nextCursor": {
                "type": "string",
                "nullable": true,
                "description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
              },
              "total": {
                "type": "integer",
                "description": "Total documents in the collection."
              },
              "@odata.count": {
                "type": "integer",
                "description": "Present only when $count=true: total matches after $filter/$search."
              },
              "@odata.truncated": {
                "type": "boolean",
                "description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "Invalid OData query (e.g. malformed $filter)"
    }
  }
}

POST /messages

Store a message in a mailbox

Writes a message straight into one mailbox without delivering it: a draft (folder drafts, then send-draft) or, for demo set-up, a received email. Requires owner_email and folder; everything else defaults. To actually send an email use send.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Store a message in a mailbox",
  "description": "Writes a message straight into one mailbox without delivering it: a draft (folder drafts, then send-draft) or, for demo set-up, a received email. Requires owner_email and folder; everything else defaults. To actually send an email use send.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/MailMessage"
        }
      }
    }
  },
  "responses": {
    "201": {
      "description": "Created",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailMessage"
          }
        }
      }
    }
  }
}

GET /messages/{id}

Get an email

One mailbox copy by id, with full headers, body and attachment metadata.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Get an email",
  "description": "One mailbox copy by id, with full headers, body and attachment metadata.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "responses": {
    "200": {
      "description": "Found",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailMessage"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

PATCH /messages/{id}

Update an email

Mark read or unread (isRead), flag (isFlagged), move between folders (folder: inbox, archive, junk, trash or a custom fld_… id), set categories or importance, or edit a draft's recipients, subject, body and attachments.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Update an email",
  "description": "Mark read or unread (isRead), flag (isFlagged), move between folders (folder: inbox, archive, junk, trash or a custom fld_… id), set categories or importance, or edit a draft's recipients, subject, body and attachments.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "requestBody": {
    "description": "Fields to update (partial)",
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/MailMessage"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailMessage"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

DELETE /messages/{id}

Permanently delete an email

Removes one mailbox's copy for good. To delete the way a mail client does, move it to trash with update instead.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Permanently delete an email",
  "description": "Removes one mailbox's copy for good. To delete the way a mail client does, move it to trash with update instead.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "responses": {
    "200": {
      "description": "Deleted",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "success": {
                "type": "boolean"
              }
            }
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

GET /settings

List mailbox settings

Display name, signature, time zone and out-of-office settings for every mailbox, i.e. the directory of who has a mailbox. Filter auto_reply_enabled eq true to see who is out of office. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "List mailbox settings",
  "description": "Display name, signature, time zone and out-of-office settings for every mailbox, i.e. the directory of who has a mailbox. Filter auto_reply_enabled eq true to see who is out of office. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "query",
      "name": "limit",
      "schema": {
        "type": "integer",
        "default": 50
      },
      "description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
    },
    {
      "in": "query",
      "name": "cursor",
      "schema": {
        "type": "string"
      },
      "description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
    },
    {
      "in": "query",
      "name": "$filter",
      "schema": {
        "type": "string"
      },
      "description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
    },
    {
      "in": "query",
      "name": "$select",
      "schema": {
        "type": "string"
      },
      "description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
    },
    {
      "in": "query",
      "name": "$orderby",
      "schema": {
        "type": "string"
      },
      "description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
    },
    {
      "in": "query",
      "name": "$search",
      "schema": {
        "type": "string"
      },
      "description": "Free-text, case-insensitive substring match across the resource's searchable fields."
    },
    {
      "in": "query",
      "name": "$top",
      "schema": {
        "type": "integer"
      },
      "description": "Maximum number of items to return (page size)."
    },
    {
      "in": "query",
      "name": "$skip",
      "schema": {
        "type": "integer"
      },
      "description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
    },
    {
      "in": "query",
      "name": "$count",
      "schema": {
        "type": "boolean"
      },
      "description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
    },
    {
      "in": "query",
      "name": "owner_email",
      "schema": {
        "type": "string",
        "format": "email"
      },
      "description": "Only this mailbox"
    },
    {
      "in": "query",
      "name": "auto_reply_enabled",
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ]
      },
      "description": "Only mailboxes with automatic replies on or off"
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated list",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MailboxSettings"
                }
              },
              "nextCursor": {
                "type": "string",
                "nullable": true,
                "description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
              },
              "total": {
                "type": "integer",
                "description": "Total documents in the collection."
              },
              "@odata.count": {
                "type": "integer",
                "description": "Present only when $count=true: total matches after $filter/$search."
              },
              "@odata.truncated": {
                "type": "boolean",
                "description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "Invalid OData query (e.g. malformed $filter)"
    }
  }
}

GET /settings/{id}

Get a mailbox's settings

By owner email.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Get a mailbox's settings",
  "description": "By owner email.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "responses": {
    "200": {
      "description": "Found",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailboxSettings"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

GET /folders

List custom folders across mailboxes

Custom folders in every mailbox (well-known folders aren't stored). For the current user's folders with counts use list-my-folders. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "List custom folders across mailboxes",
  "description": "Custom folders in every mailbox (well-known folders aren't stored). For the current user's folders with counts use list-my-folders. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "query",
      "name": "limit",
      "schema": {
        "type": "integer",
        "default": 50
      },
      "description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
    },
    {
      "in": "query",
      "name": "cursor",
      "schema": {
        "type": "string"
      },
      "description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
    },
    {
      "in": "query",
      "name": "$filter",
      "schema": {
        "type": "string"
      },
      "description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
    },
    {
      "in": "query",
      "name": "$select",
      "schema": {
        "type": "string"
      },
      "description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
    },
    {
      "in": "query",
      "name": "$orderby",
      "schema": {
        "type": "string"
      },
      "description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
    },
    {
      "in": "query",
      "name": "$search",
      "schema": {
        "type": "string"
      },
      "description": "Free-text, case-insensitive substring match across the resource's searchable fields."
    },
    {
      "in": "query",
      "name": "$top",
      "schema": {
        "type": "integer"
      },
      "description": "Maximum number of items to return (page size)."
    },
    {
      "in": "query",
      "name": "$skip",
      "schema": {
        "type": "integer"
      },
      "description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
    },
    {
      "in": "query",
      "name": "$count",
      "schema": {
        "type": "boolean"
      },
      "description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
    },
    {
      "in": "query",
      "name": "owner_email",
      "schema": {
        "type": "string",
        "format": "email"
      },
      "description": "Only this mailbox's folders"
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated list",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MailFolder"
                }
              },
              "nextCursor": {
                "type": "string",
                "nullable": true,
                "description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
              },
              "total": {
                "type": "integer",
                "description": "Total documents in the collection."
              },
              "@odata.count": {
                "type": "integer",
                "description": "Present only when $count=true: total matches after $filter/$search."
              },
              "@odata.truncated": {
                "type": "boolean",
                "description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "Invalid OData query (e.g. malformed $filter)"
    }
  }
}

GET /folders/{id}

Get a folder

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Get a folder",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "responses": {
    "200": {
      "description": "Found",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailFolder"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

PATCH /folders/{id}

Rename a folder

Only display_name can change.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Rename a folder",
  "description": "Only display_name can change.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "requestBody": {
    "description": "Fields to update (partial)",
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/MailFolder"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailFolder"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

DELETE /folders/{id}

Delete a folder

Deletes a custom folder and moves the mail in it back to the inbox.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "operationId": "mail__delete__folder",
  "summary": "Delete a folder",
  "description": "Deletes a custom folder and moves the mail in it back to the inbox.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string",
        "example": "fld_3f2a9c1b0d4e"
      },
      "description": "Folder ID"
    }
  ],
  "responses": {
    "200": {
      "description": "Deleted; how many messages moved to the inbox",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "success": {
                "type": "boolean"
              },
              "moved": {
                "type": "integer"
              }
            }
          }
        }
      }
    },
    "404": {
      "description": "Folder not found"
    }
  }
}

GET /rules

List inbox rules across mailboxes

Inbox rules in every mailbox. For the current user's rules use list-my-rules; create with create-my-rule. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "List inbox rules across mailboxes",
  "description": "Inbox rules in every mailbox. For the current user's rules use list-my-rules; create with create-my-rule. Supports server-side querying with _filter/_search/_orderby/_select/_top/_skip/_count (OData $-params, renamed with a leading underscore for MCP).",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "query",
      "name": "limit",
      "schema": {
        "type": "integer",
        "default": 50
      },
      "description": "Legacy page size (default 50). Prefer $top. Ignored when $top/$skip are used."
    },
    {
      "in": "query",
      "name": "cursor",
      "schema": {
        "type": "string"
      },
      "description": "Legacy keyset cursor — pass the previous response's nextCursor. Prefer $skip/$top."
    },
    {
      "in": "query",
      "name": "$filter",
      "schema": {
        "type": "string"
      },
      "description": "OData filter expression. Comparison operators: eq, ne, gt, ge, lt, le. Logical: and, or, not, with parentheses for grouping. String functions: contains(field,'x'), startswith(field,'x'), endswith(field,'x'). Membership: field in ('a','b'). Case-insensitive matching via tolower(field) / toupper(field). Nested fields use a slash, e.g. pto_balance/vacation_days. String literals use single quotes; dates are ISO-8601. Examples: \"status eq 'active'\"; \"salary gt 50000 and department eq 'Engineering'\"; \"contains(tolower(name),'acme')\"; \"closeDate gt 2026-01-01\"."
    },
    {
      "in": "query",
      "name": "$select",
      "schema": {
        "type": "string"
      },
      "description": "Comma-separated list of fields to return, e.g. \"id,firstName,email\". Nested fields via slash (\"pto_balance/vacation_days\"). The id field is always included."
    },
    {
      "in": "query",
      "name": "$orderby",
      "schema": {
        "type": "string"
      },
      "description": "Sort clause(s), comma-separated, each \"field [asc|desc]\" (asc is the default). Example: \"createdAt desc, lastName asc\". Null values sort last."
    },
    {
      "in": "query",
      "name": "$search",
      "schema": {
        "type": "string"
      },
      "description": "Free-text, case-insensitive substring match across the resource's searchable fields."
    },
    {
      "in": "query",
      "name": "$top",
      "schema": {
        "type": "integer"
      },
      "description": "Maximum number of items to return (page size)."
    },
    {
      "in": "query",
      "name": "$skip",
      "schema": {
        "type": "integer"
      },
      "description": "Number of matching items to skip before returning results (offset paging). Combine with $top."
    },
    {
      "in": "query",
      "name": "$count",
      "schema": {
        "type": "boolean"
      },
      "description": "When true, include @odata.count (total matches after $filter/$search, before paging) in the response."
    },
    {
      "in": "query",
      "name": "owner_email",
      "schema": {
        "type": "string",
        "format": "email"
      },
      "description": "Only this mailbox's rules"
    },
    {
      "in": "query",
      "name": "is_enabled",
      "schema": {
        "type": "string",
        "enum": [
          "true",
          "false"
        ]
      },
      "description": "Only enabled or disabled rules"
    }
  ],
  "responses": {
    "200": {
      "description": "Paginated list",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "data": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MailRule"
                }
              },
              "nextCursor": {
                "type": "string",
                "nullable": true,
                "description": "Cursor for the next page, or null on the last page. With $top/$skip this is the next $skip value; otherwise a keyset cursor."
              },
              "total": {
                "type": "integer",
                "description": "Total documents in the collection."
              },
              "@odata.count": {
                "type": "integer",
                "description": "Present only when $count=true: total matches after $filter/$search."
              },
              "@odata.truncated": {
                "type": "boolean",
                "description": "Present and true only when the collection exceeded the in-memory query cap and results may be incomplete."
              }
            }
          }
        }
      }
    },
    "400": {
      "description": "Invalid OData query (e.g. malformed $filter)"
    }
  }
}

GET /rules/{id}

Get a rule

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Get a rule",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "responses": {
    "200": {
      "description": "Found",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailRule"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

PATCH /rules/{id}

Update a rule

Change display_name, sequence, is_enabled, conditions or actions. Set is_enabled false to pause a rule.

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Update a rule",
  "description": "Change display_name, sequence, is_enabled, conditions or actions. Set is_enabled false to pause a rule.",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "requestBody": {
    "description": "Fields to update (partial)",
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/MailRule"
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Updated",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MailRule"
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

DELETE /rules/{id}

Delete a rule

Authentication

[
  {
    "patAuth": []
  }
]

Request, responses and operation details

{
  "summary": "Delete a rule",
  "parameters": [
    {
      "$ref": "#/components/parameters/InstanceId"
    },
    {
      "in": "path",
      "name": "id",
      "required": true,
      "schema": {
        "type": "string"
      }
    }
  ],
  "responses": {
    "200": {
      "description": "Deleted",
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "properties": {
              "success": {
                "type": "boolean"
              }
            }
          }
        }
      }
    },
    "404": {
      "description": "Not found"
    }
  }
}

Schemas and reusable components

securitySchemes

patAuth

{
  "type": "http",
  "scheme": "bearer"
}

parameters

InstanceId

{
  "in": "header",
  "name": "X-Instance-ID",
  "required": true,
  "schema": {
    "type": "string",
    "example": "inst_abc123"
  },
  "description": "The Purple Suite instance ID that scopes all data"
}

ImpersonateUser

{
  "in": "header",
  "name": "X-PS-Impersonate-User",
  "required": false,
  "schema": {
    "type": "string",
    "format": "email"
  },
  "description": "Optional. Act as a specific user in the target instance — the value is that user's email (must exist in the instance). Resolution order for the effective user: (1) this header, if set; (2) the instance's configured default MCP user; (3) otherwise no user is bound. User-scoped paths (e.g. /users/{email}/…) also accept the literal \"me\", and Graph-style apps serve /me/… twins of those paths (Graph: /me ≡ /users/{signed-in-user}) — both resolve to whatever this header (or the default MCP user) points at. An explicit path email always wins over the header. The unified MCP server does not send this header — it relies on the per-instance default — so it is primarily for direct REST/HTTP callers."
}

schemas

MailAttachment

{
  "type": "object",
  "description": "Attachment metadata (name, type, size). Purple Mail stores no file bytes.",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true,
      "example": "ATT-00001"
    },
    "name": {
      "type": "string",
      "example": "Q3-offsite-agenda-v2.pdf"
    },
    "content_type": {
      "type": "string",
      "example": "application/pdf"
    },
    "size_bytes": {
      "type": "integer",
      "example": 184320
    }
  }
}

MailMessage

{
  "type": "object",
  "description": "One email as stored in one mailbox. The same email appears once per mailbox (sender's sent copy, each recipient's inbox copy) with the same internet_message_id and conversation_id but its own id, folder, isRead and isFlagged.",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true,
      "example": "MSG-00001"
    },
    "owner_email": {
      "type": "string",
      "format": "email",
      "example": "alice.chen@acme.com",
      "description": "Whose mailbox this copy lives in."
    },
    "folder": {
      "type": "string",
      "example": "inbox",
      "description": "Mailbox folder. PATCH it to move: archive, junk, trash, inbox or a custom folder id (fld_…)."
    },
    "is_draft": {
      "type": "boolean",
      "readOnly": true,
      "example": false,
      "description": "True when folder is drafts."
    },
    "conversation_id": {
      "type": "string",
      "example": "CONV-0001",
      "description": "The thread: shared by every copy of an email and all replies to it."
    },
    "internet_message_id": {
      "type": "string",
      "readOnly": true,
      "example": "<msg-00001@acme.com>",
      "description": "RFC 5322 Message-ID. Identical on every mailbox's copy of one email."
    },
    "in_reply_to": {
      "type": "string",
      "nullable": true,
      "example": null,
      "description": "ID of the mailbox copy that was replied from (only meaningful in that mailbox). Use in_reply_to_message_id for threading."
    },
    "in_reply_to_message_id": {
      "type": "string",
      "nullable": true,
      "example": null,
      "description": "internet_message_id of the email this one answers."
    },
    "references": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "example": [],
      "description": "internet_message_ids of the thread so far, oldest first."
    },
    "from_email": {
      "type": "string",
      "format": "email",
      "example": "bob.martinez@acme.com"
    },
    "from_name": {
      "type": "string",
      "example": "Bob Martinez"
    },
    "to_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": [
        "alice.chen@acme.com"
      ]
    },
    "cc_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": []
    },
    "bcc_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": [],
      "description": "Blind copies. Populated only on the sender's own copy; recipients never see it."
    },
    "reply_to_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": [],
      "description": "Where replies go instead of from_email, when set."
    },
    "subject": {
      "type": "string",
      "example": "Q3 planning offsite agenda"
    },
    "body_content": {
      "type": "string",
      "example": "Hi Alice, here is the first pass at the agenda..."
    },
    "body_content_type": {
      "type": "string",
      "enum": [
        "text",
        "html"
      ],
      "example": "text"
    },
    "body_preview": {
      "type": "string",
      "readOnly": true,
      "description": "First 140 characters of the body, tags stripped."
    },
    "importance": {
      "type": "string",
      "enum": [
        "low",
        "normal",
        "high"
      ],
      "example": "normal"
    },
    "inference_classification": {
      "type": "string",
      "enum": [
        "focused",
        "other"
      ],
      "example": "focused",
      "description": "Focused Inbox: mail from people is focused; automated senders (notifications, newsletters) are other."
    },
    "isRead": {
      "type": "boolean",
      "example": false
    },
    "isFlagged": {
      "type": "boolean",
      "example": false
    },
    "categories": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "example": [
        "Planning"
      ]
    },
    "has_attachments": {
      "type": "boolean",
      "readOnly": true,
      "example": false
    },
    "attachments": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/MailAttachment"
      },
      "description": "Attachment metadata; there are no file bytes."
    },
    "sentDateTime": {
      "type": "string",
      "format": "date-time",
      "nullable": true
    },
    "receivedDateTime": {
      "type": "string",
      "format": "date-time",
      "nullable": true
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

MailConversation

{
  "type": "object",
  "description": "One thread in the current user's mailbox: every copy they hold that shares a conversation_id, summarised.",
  "properties": {
    "conversation_id": {
      "type": "string",
      "example": "CONV-0001"
    },
    "subject": {
      "type": "string",
      "example": "Q3 planning offsite agenda",
      "description": "The thread's subject without Re:/Fwd: prefixes."
    },
    "first_received": {
      "type": "string",
      "format": "date-time"
    },
    "latest_received": {
      "type": "string",
      "format": "date-time"
    },
    "message_count": {
      "type": "integer",
      "example": 3
    },
    "unread_count": {
      "type": "integer",
      "example": 1
    },
    "participants": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": [
        "alice.chen@acme.com",
        "bob.martinez@acme.com"
      ]
    },
    "participant_names": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "example": [
        "Alice Chen",
        "Bob Martinez"
      ]
    },
    "latest_from_email": {
      "type": "string",
      "format": "email"
    },
    "latest_from_name": {
      "type": "string",
      "example": "Bob Martinez"
    },
    "latest_preview": {
      "type": "string"
    },
    "has_attachments": {
      "type": "boolean"
    },
    "is_flagged": {
      "type": "boolean"
    },
    "importance": {
      "type": "string",
      "enum": [
        "low",
        "normal",
        "high"
      ]
    },
    "folders": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "example": [
        "inbox",
        "sent"
      ],
      "description": "Folders this mailbox's copies of the thread are in."
    }
  }
}

MailConversationDetail

{
  "type": "object",
  "description": "A thread with its messages, oldest first.",
  "properties": {
    "conversation_id": {
      "type": "string",
      "example": "CONV-0001"
    },
    "subject": {
      "type": "string"
    },
    "participants": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": [
        "alice.chen@acme.com"
      ]
    },
    "participant_names": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "message_count": {
      "type": "integer"
    },
    "unread_count": {
      "type": "integer"
    },
    "has_attachments": {
      "type": "boolean"
    },
    "folders": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "messages": {
      "type": "array",
      "items": {
        "$ref": "#/components/schemas/MailMessage"
      }
    }
  }
}

MailFolder

{
  "type": "object",
  "description": "A custom mail folder. Well-known folders (inbox, sent, drafts, archive, junk, trash) are not stored; list-my-folders returns both kinds with counts.",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true,
      "example": "fld_3f2a9c1b0d4e"
    },
    "owner_email": {
      "type": "string",
      "format": "email",
      "readOnly": true,
      "example": "alice.chen@acme.com"
    },
    "display_name": {
      "type": "string",
      "example": "Vendors"
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

MailFolderCount

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string",
      "example": "inbox"
    },
    "display_name": {
      "type": "string",
      "example": "Inbox"
    },
    "well_known": {
      "type": "boolean",
      "example": true
    },
    "total_item_count": {
      "type": "integer",
      "example": 42
    },
    "unread_item_count": {
      "type": "integer",
      "example": 5
    }
  }
}

MailRuleConditions

{
  "type": "object",
  "description": "All given conditions must match (each list matches if any entry is contained, case-insensitive). No conditions matches every email.",
  "properties": {
    "from_contains": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "example": [
        "vendor.example.com"
      ],
      "description": "Substrings of the sender's address or name."
    },
    "subject_contains": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "example": [
        "invoice"
      ]
    },
    "body_contains": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "example": []
    },
    "importance": {
      "type": "string",
      "enum": [
        "low",
        "normal",
        "high"
      ]
    },
    "has_attachments": {
      "type": "boolean"
    },
    "sender_is_external": {
      "type": "boolean",
      "description": "Sender's domain differs from the mailbox owner's."
    }
  }
}

MailRuleActions

{
  "type": "object",
  "description": "At least one of move_to_folder, mark_as_read, flag or assign_categories.",
  "properties": {
    "move_to_folder": {
      "type": "string",
      "example": "archive",
      "description": "A well-known folder or a custom folder id (fld_…)."
    },
    "mark_as_read": {
      "type": "boolean"
    },
    "flag": {
      "type": "boolean"
    },
    "assign_categories": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "example": [
        "Vendors"
      ]
    },
    "stop_processing": {
      "type": "boolean",
      "description": "Skip later rules once this one matches."
    }
  }
}

MailRule

{
  "type": "object",
  "description": "An inbox rule. Rules run in sequence order on every email that arrives in the owner's inbox; apply runs one over the existing inbox.",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true,
      "example": "RULE-0001"
    },
    "owner_email": {
      "type": "string",
      "format": "email",
      "readOnly": true,
      "example": "alice.chen@acme.com"
    },
    "display_name": {
      "type": "string",
      "example": "File vendor invoices"
    },
    "sequence": {
      "type": "integer",
      "example": 1
    },
    "is_enabled": {
      "type": "boolean",
      "example": true
    },
    "conditions": {
      "$ref": "#/components/schemas/MailRuleConditions"
    },
    "actions": {
      "$ref": "#/components/schemas/MailRuleActions"
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

MailboxSettings

{
  "type": "object",
  "description": "A person's mailbox settings. The id is the owner's email address, lowercased.",
  "properties": {
    "id": {
      "type": "string",
      "readOnly": true,
      "example": "alice.chen@acme.com"
    },
    "owner_email": {
      "type": "string",
      "format": "email",
      "readOnly": true,
      "example": "alice.chen@acme.com"
    },
    "display_name": {
      "type": "string",
      "example": "Alice Chen"
    },
    "signature": {
      "type": "string",
      "example": "Alice Chen\nSenior Engineer, Engineering\nAcme"
    },
    "time_zone": {
      "type": "string",
      "example": "America/Los_Angeles",
      "description": "IANA time zone the owner reads mail in."
    },
    "auto_reply_enabled": {
      "type": "boolean",
      "example": false,
      "description": "Out of office: answer new mail automatically."
    },
    "auto_reply_message": {
      "type": "string",
      "example": "I'm out of office until Monday."
    },
    "auto_reply_start": {
      "type": "string",
      "format": "date-time",
      "nullable": true
    },
    "auto_reply_end": {
      "type": "string",
      "format": "date-time",
      "nullable": true
    },
    "createdAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time",
      "readOnly": true
    }
  }
}

SendMailRequest

{
  "type": "object",
  "required": [
    "to_emails",
    "subject"
  ],
  "properties": {
    "from_email": {
      "type": "string",
      "example": "me",
      "description": "Sender. Omit or pass \"me\" to send as the current user."
    },
    "to_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": [
        "alice.chen@acme.com"
      ]
    },
    "cc_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": []
    },
    "bcc_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": [],
      "description": "Blind copies; recipients don't see them."
    },
    "reply_to_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": [],
      "description": "Where replies should go instead of the sender."
    },
    "subject": {
      "type": "string",
      "example": "Draft launch announcement for review"
    },
    "body_content": {
      "type": "string",
      "example": "Hi Alice, here's the draft. Any edits?"
    },
    "body_content_type": {
      "type": "string",
      "enum": [
        "text",
        "html"
      ],
      "example": "text"
    },
    "importance": {
      "type": "string",
      "enum": [
        "low",
        "normal",
        "high"
      ],
      "example": "normal"
    },
    "categories": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "example": []
    },
    "attachments": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "example": "launch-announcement-v3.docx"
          },
          "content_type": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer"
          }
        }
      },
      "description": "Attachment metadata only; no file bytes are stored."
    },
    "include_signature": {
      "type": "boolean",
      "example": false,
      "description": "Append the sender's mailbox signature to the body."
    }
  }
}

ReplyMailRequest

{
  "type": "object",
  "required": [
    "body_content"
  ],
  "properties": {
    "body_content": {
      "type": "string",
      "example": "Thanks, that works for me."
    },
    "reply_all": {
      "type": "boolean",
      "example": false,
      "description": "Also reply to the other To and Cc recipients."
    },
    "include_signature": {
      "type": "boolean",
      "example": false,
      "description": "Append the replier's mailbox signature."
    },
    "include_original": {
      "type": "boolean",
      "example": false,
      "description": "Quote the original message under the reply."
    }
  }
}

ForwardMailRequest

{
  "type": "object",
  "required": [
    "to_emails"
  ],
  "properties": {
    "to_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": [
        "carol.johnson@acme.com"
      ]
    },
    "cc_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": []
    },
    "bcc_emails": {
      "type": "array",
      "items": {
        "type": "string",
        "format": "email"
      },
      "example": []
    },
    "comment": {
      "type": "string",
      "example": "FYI, see below."
    },
    "include_signature": {
      "type": "boolean",
      "example": false
    }
  }
}

CreateFolderRequest

{
  "type": "object",
  "required": [
    "display_name"
  ],
  "properties": {
    "display_name": {
      "type": "string",
      "example": "Vendors"
    }
  }
}

CreateRuleRequest

{
  "type": "object",
  "required": [
    "display_name",
    "actions"
  ],
  "properties": {
    "display_name": {
      "type": "string",
      "example": "File vendor invoices"
    },
    "sequence": {
      "type": "integer",
      "description": "Run order; defaults to after the existing rules."
    },
    "is_enabled": {
      "type": "boolean",
      "example": true
    },
    "conditions": {
      "$ref": "#/components/schemas/MailRuleConditions"
    },
    "actions": {
      "$ref": "#/components/schemas/MailRuleActions"
    }
  }
}

ConversationPatch

{
  "type": "object",
  "description": "Applies to every copy of the thread in the current user's mailbox (drafts and sent copies keep their folder).",
  "properties": {
    "isRead": {
      "type": "boolean",
      "example": true
    },
    "isFlagged": {
      "type": "boolean"
    },
    "folder": {
      "type": "string",
      "example": "archive",
      "description": "Move the thread: inbox, archive, junk, trash or a custom folder id."
    }
  }
}

Document metadata and extensions

{
  "openapi": "3.0.0",
  "info": {
    "title": "Purple Mail API",
    "version": "2.0.0",
    "description": "Purple Mail is the suite's email system, the category that tools like Microsoft Outlook and Gmail sit in. Every person on the shared roster has a mailbox with Inbox, Sent, Drafts, Archive, Junk and Trash plus custom folders, inbox rules, a signature and automatic replies. Mail is stored per mailbox: sending writes a sent copy for the sender and an inbox copy for each To, Cc and Bcc recipient, linked by conversation_id (the thread) and internet_message_id (the RFC 5322 Message-ID). Start with the current user's own mail: list-my-conversations for threads, list-my-messages for individual emails with OData filters and _search, get-my-conversation for a whole thread.\n\n---\n\n**Acting as a user (`X-PS-Impersonate-User`).** Every endpoint accepts an optional `X-PS-Impersonate-User` request header whose value is the email of the instance user to act as. The effective user resolves in this order: the header if present, else the instance's configured default MCP user, else none. User-scoped paths additionally accept the literal `me` in place of an email, and Graph-style apps also serve `/me/…` twins of their `/users/{email}/…` paths that resolve the same way. This header is honored by all operations but is intentionally omitted from each operation's parameter list so it is not surfaced as a per-tool argument by MCP clients; see the `ImpersonateUser` entry under `components.parameters`."
  },
  "security": [
    {
      "patAuth": []
    }
  ]
}