REST API
Every RapidMX client goes through the same HTTP API: the web app, the admin and escrow consoles, the native app, and
the protocol plugins that translate ActiveSync and MAPI into calls on the same data. Most of that API is defined in
the @rapidmx/restapi library; the server repository mounts it, adds a handful of routes of its own and serves the
plugins' routes alongside. If you are fixing a bug in a route, adding an endpoint or building a client, this page tells
you where the code lives and which rules every route follows.
The API is built on RapidREST, so the request pipeline, decorators, CRUD routes and query syntax are the framework's. See Routing and Querying on rapidrest.dev for those.
Where the routes come from
@rapidmx/restapi defines each resource three times:
- an abstract
Base*Routeinsrc/routes/with the logic, independent of the database; - a
*RouteMongoand a*RouteSQLsubclass insrc/routes/mongo/andsrc/routes/sql/that bind the backend's model classes (@Model(MailboxMongo)); - the models themselves,
src/models/mongo/*Mongo.tsandsrc/models/sql/*SQL.ts, behind shared interfaces insrc/models/types.ts.
The library's concrete routes carry no paths. The server gives them one, in src/mongo/routes/ and src/sql/routes/:
@ApiRoute("mail/mailboxes")
export class MailboxRoute extends MailboxRouteMongo {}
So when you add a route to @rapidmx/restapi, you also add the two one-line subclasses to the server. The server picks
the Mongo or SQL set at startup from its entry point (server.mongo.ts or server.sql.ts); both sets must stay in step.
@ApiRoute(path) serves a route under /api/<path>. @Route(path) serves it at exactly path. No route passes an API
version, so there is no /api/v1.
Resource groups
| Prefix | What is there |
|---|---|
/api/mail/ | The mail data: mailboxes (also key vault, key lookup and escrow info, under /:id/), folders, messages, attachments, labels, mail-filter-rules, mail-signatures, search, compose, focused-inbox-overrides, preferences/appearance, directory |
/api/mail/ (PIM) | calendar-events, calendar-share-links, contacts, contact-lists, notes, tasks, task-lists |
/api/mail/ (administration) | domains, distribution-lists, transport-rules, quarantine, ingest-queue, audit-log, data-export-requests, erasure-requests, mailbox-import-requests |
/api/system/ | Deployment-wide settings: branding, encryption-policy, mailbox-policy, retention-policy, plugins, setup, signing-enrollment |
/api/escrow/ | Key escrow and eDiscovery: scopes, access-requests, audit-log, matters, matter-search, matter-export-requests |
/api/admin/ | diagnostics, signing-enrollments, and the framework's admin routes |
/api/ | Framework routes: acls, openapi, status (readiness), metrics (needs a trusted role) |
No /api prefix | /push (WebSocket), /.well-known/rapidmx/keys and /.well-known/rapidmx/server-info (public), /internal/mta (the mail transport's ingest, see Mail flow), the pages (/, /settings, /admin, /escrow), static assets |
Plugins add their own routes, by convention under /api/mail/<plugin>; the protocol plugins add
/Microsoft-Server-ActiveSync, /mapi/emsmdb, /mapi/nspi and /autodiscover (see Protocols).
The OpenAPI document
The server describes its whole API, plugins included, as OpenAPI at /api/openapi (an HTML viewer), /api/openapi/json
and /api/openapi/yaml. It requires a signed-in caller, because the document lists the admin, escrow and plugin routes.
The descriptions come from the @Description decorators on models and routes, so keep those accurate when you change
one.
Authentication
The server never signs anyone in. Users sign in on the separate auth-server, which issues a JWT; the server only verifies it. A request carries the token in one of two ways:
- the
jwtcookie, set by the auth-server for the deployment's domain, which is what the browser apps use; - an
Authorization: Bearerheader, which is what the native app and scripts use.
Tokens in the query string are turned off (auth:allowQueryParam: false). The token must be signed with the secret the
auth-server uses (auth:secret) and carry the configured audience and issuer (auth:options:audience,
auth:options:issuer). When the server and the auth-server disagree on any of these, every signed-in request fails,
which is the first thing to check when a fresh deployment answers 403 to everything.
HTTP Basic is accepted on two paths only, /mapi and /Microsoft-Server-ActiveSync and everything beneath them,
because Outlook and ActiveSync clients can't do anything else. There the server exchanges the username and app password
for a token at the auth-server. Nothing else accepts Basic. See Authentication.
For cookie-authenticated writes, the framework enforces a double-submit CSRF check: the browser echoes the csrf
cookie in an x-csrf-token header. The web client's apiFetch() does this for you.
When you run the server with yarn dev, every request is signed in as a development administrator with an elevated
session, so you can work without an auth-server. Permission bugs therefore don't show up under yarn dev; the route
tests are where they are caught.
Permissions
RapidREST protects data with access control lists stored in the acl datastore: each model class declares a default
with @Protect, and each record gets its own ACL, which can inherit from a parent (a folder's from its mailbox's). See
Access control lists.
RapidMX adds one rule the framework doesn't have: administrators have no access to other people's mail. In
RapidREST a trusted role (admin) passes every ACL check. In RapidMX:
- Mailbox-scoped data is checked with
hasMailAccess()/assertMailAccess()from@rapidmx/restapi(src/util/MailAccessUtils.ts), which strip trusted roles before asking the ACL. Only the owner and the people the owner shared with get in, and a resource the caller can't see answers 404, not 403. - An administrator who needs to see an account impersonates the user, an audited action, rather than reading their data directly.
- The admin console reads mailboxes with
?scope=admin, which returns administrative details only (addresses, owners, quota, never mail or keys), requires a trusted role and an elevated session, and is written to the audit log. /api/aclsdoesn't let a trusted role read or change mailbox and folder ACLs either.
Trusted roles only appear in elevated tokens. Sensitive administration (plugins, signing certificates, data exports,
erasure, retention, transport rules, escrow scopes, ?scope=admin) also requires the elevation to be recent: by default
no more than 900 seconds old (mail:security:elevation_max_age_seconds). A request that fails this answers 403 with code
api-104, which the admin console turns into a trip to the auth-server to confirm the sign-in. See
Authentication.
These rules are tested as a matrix. @rapidmx/restapi's test suite has a table classifying every route class
(test/routes/mailAccessRouteTable.ts), and the build fails when a route class is missing from it or when code in
src/routes calls aclUtils.hasPermission() directly. The server has the same check for its own routes. When you add a
route, add it to the table.
The README of @rapidmx/restapi ("Who can see whose mail") has the full rules.
Rate limits
Routes that answer anonymous callers, and expensive ones, carry @RateLimit(). The server's limiter is tiered:
anonymous callers get the top-level rateLimit settings, signed-in callers the far larger rateLimit.authenticated
ones, and IPv6 addresses are counted per /64. Client addresses come from X-Forwarded-For only through the proxies in
trusted_proxies.
Requests and responses
The conventions are RapidREST's:
- CRUD routes answer
GET /,GET /:id,POST /,PUT /:id,PUT /(bulk),PUT /:id/:property,DELETE /:idandDELETE /.HEAD /returns a count inContent-Length. - Lists take
limit(default 100, at most 1000), a zero-basedpage,sort(field,-fieldor JSON), and other query parameters as field filters. - Updates and deletes take the record's
versionfor optimistic concurrency, and answer 409 when it is stale. - Errors are JSON,
{ "code": "api-1xx", "status": 403, "message": "..." }. Anything that isn't anApiErrorbecomes a generic 500 without internals or stack traces. The codes you will meet most areapi-100(sign-in required),api-102(permission),api-103(trusted role required) andapi-104(elevation required).
Mailbox-scoped routes add their own rules on top (server-minted uids, scoped list queries, quiet 404s for denied reads);
the doc comment on BaseScopedChildRoute in @rapidmx/restapi lists them.
Realtime updates
Clients keep one WebSocket to /push (the framework's push route, extended by @rapidmx/restapi's MailPushRoute).
The flow:
- Connect to
wss://mail.example.com/push. The connection authenticates with thejwtcookie or anAuthorizationheader, or by sending{ "type": "LOGIN", "data": "<token>" }as the first frame. AnOriginthat isn't incors:originsis refused. - Subscribe with
{ "id": 1, "type": "SUBSCRIBE", "data": ["<mailbox uid>", "<folder uid>"] }. The server answersSUBSCRIBEDwith the channels it granted. Channels are plain entity uids, and each is checked for READ access with trusted roles stripped. The user's own uid is always subscribed. - Receive
{ "type": "MESSAGE", "channel": "<uid>", "data": { "type": "...", "action": "create|update|delete", "data": { ... } } }.
For example, a folder's counts change:
{ "type": "FolderMongo", "action": "update", "data": { "uid": "<folder>", "mailboxUid": "<mailbox>", "unreadCount": 3, "totalCount": 27 } }
A mailbox subscription doesn't include its folders' events; subscribe to each folder you show. The server rechecks
access periodically (mail:push:recheck_ms, 60 seconds) and closes the socket when access is lost or the token expires.
Events published while a client is disconnected are not replayed, so a client must reload what it shows when it
reconnects. Between server copies, events travel over the events Redis datastore.
Routes publish with NotificationUtils.sendMessage(uid, type, action, data) from @rapidrest/service-core. The
@rapidmx/restapi README documents each event a route publishes; keep it up to date when you add one.
Server-side code that needs to react to mail (a plugin, say) uses a different mechanism, the mail event stream
(message.delivered, message.sent, send.failed). See
Routes, models and jobs.
Schema
There are no migrations. The server creates and updates its collections, tables and columns from the models at startup
(datastores.*.synchronize, on by default). An added field is just a release; a changed column type may be dropped and
recreated on PostgreSQL, losing its data, so avoid changing a released column's type. The @rapidmx/restapi README's
"Upgrading" section records the schema changes that needed care.