Skip to content

Controllers

The API has 12 controllers organized by domain. All controllers use attribute routing and return JSON responses. Unless noted otherwise, endpoints require authentication (either OIDC/JWT or API key).

ConfigController

Route: /config/config.jsonAuth: None (public)

Serves runtime configuration for the web frontend, allowing OIDC settings to be configured at deployment time rather than build time.

MethodPathDescription
GET/config/config.jsonReturns OIDC configuration (authority, client ID, redirect URI, scope)

The response is cached for 5 minutes (ResponseCache(Duration = 300)). The web frontend fetches this on startup to configure its OIDC client.

Response example:

json
{
  "oidcAuthority": "https://auth.example.com",
  "oidcClientId": "relate-mail",
  "oidcRedirectUri": "https://mail.example.com/callback",
  "oidcScope": "openid profile email"
}

DiscoveryController

Route: /api/discoveryAuth: None ([AllowAnonymous])

Advertises server capabilities so mobile and desktop clients can auto-configure themselves during setup.

MethodPathDescription
GET/api/discoveryReturns server version, API version, enabled features, and OIDC status

Response example:

json
{
  "version": "1.0.0",
  "apiVersion": "v1",
  "oidcEnabled": true,
  "features": ["smtp", "pop3", "imap", "api-keys", "labels", "filters", "preferences", "oidc"]
}

Features reflect the server's runtime configuration. Protocols that are disabled via configuration (e.g., Smtp:Enabled=false) are omitted from the features list.


EmailsController

Route: /api/emailsAuth: Required (JWT or API key) Rate limit: api (100/min), write on mutating endpoints (30/min)

The primary inbox controller for authenticated users. Handles listing, searching, reading, updating, deleting, and exporting emails.

MethodPathDescription
GET/api/emailsList inbox emails (paginated, default 20/page, max 100)
GET/api/emails/searchFull-text search with filters (query, date range, attachments, read status)
GET/api/emails/{id}Get email by ID with full details
PATCH/api/emails/{id}Update email (mark read/unread)
DELETE/api/emails/{id}Delete email
GET/api/emails/{id}/attachments/{attachmentId}Download attachment as file
GET/api/emails/{id}/export/emlExport single email as .eml (RFC 822)
GET/api/emails/export/mboxStream export as MBOX format (50k email limit, 10min rate limit per user)
GET/api/emails/threads/{threadId}Get all emails in a thread
POST/api/emails/bulk/mark-readBulk mark emails as read/unread
POST/api/emails/bulk/deleteBulk delete emails (returns deleted count)
GET/api/emails/sentList sent emails (optional fromAddress filter)
GET/api/emails/sent/addressesList distinct "from" addresses used in sent mail

Search query parameters:

ParameterTypeDescription
qstringFull-text search across subject, body, sender
fromDateDateTimeOffsetFilter emails received after this date
toDateDateTimeOffsetFilter emails received before this date
hasAttachmentsboolFilter by attachment presence
isReadboolFilter by read/unread status
pageintPage number (default 1)
pageSizeintItems per page (default 20, max 100)

MBOX export details:

The MBOX export endpoint streams emails directly to the response body, avoiding large memory allocations. It enforces a hard limit of 50,000 emails and a per-user rate limit of one export every 10 minutes (tracked via an in-memory cache). Supports optional fromDate and toDate query parameters to narrow the export range.

Attachment downloads validate the MIME type against a safelist of known types. Unrecognized types are served as application/octet-stream with a Content-Disposition: attachment header to prevent browser execution.


ExternalEmailsController

Route: /api/external/emailsAuth: API key only (ApiKey scheme), scope-gated

Provides the same inbox operations as EmailsController but scoped to API key authentication with explicit scope requirements. This is the endpoint third-party integrations use to access mailbox data.

MethodPathScopeDescription
GET/api/external/emailsapi:readList inbox emails (paginated)
GET/api/external/emails/searchapi:readSearch with filters
GET/api/external/emails/sentapi:readList sent emails
GET/api/external/emails/{id}api:readGet email by ID
PATCH/api/external/emails/{id}api:writeMark read/unread
DELETE/api/external/emails/{id}api:writeDelete email

The controller uses [RequireScope] attributes to enforce that the API key has the appropriate read or write scope.


FiltersController

Route: /api/filtersAuth: Required

Manages email filter rules that automatically process incoming emails. Filters have conditions (from, subject, body, attachments) and actions (mark as read, assign label, delete).

MethodPathDescription
GET/api/filtersList all filters for the authenticated user
POST/api/filtersCreate a new filter
PUT/api/filters/{id}Update an existing filter
DELETE/api/filters/{id}Delete a filter
POST/api/filters/{id}/testTest filter against recent emails (returns match count and IDs)

Create/Update request fields:

FieldTypeDescription
namestringFilter name
isEnabledboolWhether the filter is active (default true)
priorityintExecution order (lower = first, default 100)
fromAddressContainsstring?Match sender address or display name
subjectContainsstring?Match subject line
bodyContainsstring?Match text or HTML body
hasAttachmentsbool?Match by attachment presence
markAsReadboolAction: auto-mark as read
assignLabelIdGuid?Action: assign this label
deleteboolAction: delete the email

Test endpoint: The test endpoint (POST /api/filters/{id}/test?limit=10) runs the filter's conditions against the user's most recent emails (up to limit, max 100) and returns the count and IDs of matching emails without actually applying any actions.


InternalNotificationsController

Route: /api/internal/notificationsAuth: API key with internal scope

Service-to-service endpoint used by the SMTP, POP3, and IMAP hosts to trigger real-time notifications in the API. This is not intended for external use.

MethodPathDescription
POST/api/internal/notifications/new-emailNotify that new email(s) arrived for specific users

Request body:

json
{
  "userIds": ["guid1", "guid2"],
  "email": {
    "id": "guid",
    "from": "sender@example.com",
    "fromDisplay": "Sender Name",
    "subject": "Email subject",
    "receivedAt": "2026-01-01T00:00:00Z",
    "hasAttachments": false
  }
}

When called, the API broadcasts NewEmail events via SignalR and sends web push notifications to all specified users.


LabelsController

Route: /api/labelsAuth: Required

Manages user-defined labels with colors and sort ordering, and handles assigning/removing labels on emails.

MethodPathDescription
GET/api/labelsList all labels for the authenticated user
POST/api/labelsCreate a new label (name, color hex, sort order)
PUT/api/labels/{id}Update label properties
DELETE/api/labels/{id}Delete a label
POST/api/labels/emails/{emailId}Add a label to an email (body: { "labelId": "guid" })
DELETE/api/labels/emails/{emailId}/{labelId}Remove a label from an email
GET/api/labels/{labelId}/emailsList emails with a specific label (paginated)

Label ownership is verified on every operation -- users can only manage their own labels and can only label emails they have access to.


OutboundEmailsController

Route: /api/outboundAuth: Required Rate limit: api (100/min), write on mutating endpoints (30/min)

Handles email composition with a draft workflow, sending, replying, and forwarding. Outbound emails go through a status lifecycle: Draft -> Queued -> Sending -> Sent / Failed.

Draft CRUD

MethodPathDescription
POST/api/outbound/draftsCreate a new draft
GET/api/outbound/draftsList drafts (paginated)
GET/api/outbound/drafts/{id}Get draft by ID
PUT/api/outbound/drafts/{id}Update a draft (only if status is Draft)
DELETE/api/outbound/drafts/{id}Delete a draft (only if status is Draft)

Sending

MethodPathDescription
POST/api/outbound/sendCompose and immediately queue an email for delivery
POST/api/outbound/drafts/{id}/sendSend an existing draft (transitions Draft -> Queued)

Validation on send:

  • At least 1 recipient required, maximum 100
  • Valid email address format for sender and all recipients
  • Subject limited to 998 characters (RFC 2822)

The send endpoint generates an RFC-compliant Message-Id using MimeKit and queues the email for background delivery.

Reply and Forward

MethodPathDescription
POST/api/outbound/reply/{emailId}Reply to an email (with replyAll option in body)
POST/api/outbound/forward/{emailId}Forward an email to new recipients (copies attachments)

Reply automatically:

  • Adds "Re:" prefix to subject (if not already present)
  • Sets In-Reply-To and References headers for threading
  • Addresses the reply to the original sender (and all recipients if replyAll: true, excluding the current user and Bcc -> Cc promotion)

Forward automatically:

  • Adds "Fwd:" prefix to subject
  • Sets References header
  • Copies all attachments from the original email

Outbox and Sent Mail

MethodPathDescription
GET/api/outbound/outboxList queued/sending emails (paginated)
GET/api/outbound/sentList sent emails (paginated)
GET/api/outbound/{id}Get any outbound email by ID

PreferencesController

Route: /api/preferencesAuth: Required

Manages per-user display and notification preferences with sensible defaults.

MethodPathDescription
GET/api/preferencesGet user preferences (returns defaults if none saved)
PUT/api/preferencesUpdate preferences (upsert -- creates if none exist)

Preference fields:

FieldTypeDefaultDescription
themestring"system"UI theme (system, light, dark)
displayDensitystring"comfortable"Row density (comfortable, compact)
emailsPerPageint20Pagination size
defaultSortstring"receivedAt-desc"Default sort order
showPreviewbooltrueShow email preview text in list
groupByDateboolfalseGroup emails by date
desktopNotificationsboolfalseEnable desktop notifications
emailDigestboolfalseEnable email digest
digestFrequencystring"daily"Digest frequency
digestTimeTimeOnly09:00Time to send digest

ProfileController

Route: /api/profileAuth: Required

Manages user profile information and additional email addresses.

MethodPathDescription
GET/api/profileGet user profile
PUT/api/profileUpdate display name
POST/api/profile/addressesAdd an additional email address
DELETE/api/profile/addresses/{addressId}Remove an additional email address
POST/api/profile/addresses/{addressId}/send-verificationSend verification email (501 -- not yet implemented)
POST/api/profile/addresses/{addressId}/verifyVerify email address with code (501 -- not yet implemented)

When an additional address is added, the system immediately links any existing unlinked emails for that address to the user. Verification tokens are 8-character alphanumeric codes (ambiguous characters removed) with a 24-hour expiry.


PushSubscriptionsController

Route: /api/push-subscriptionsAuth: Required (except VAPID key endpoint) Rate limit: api (100/min)

Manages Web Push notification subscriptions using the VAPID protocol.

MethodPathAuthDescription
GET/api/push-subscriptions/vapid-public-keyNoneGet VAPID public key for client subscription
POST/api/push-subscriptionsRequiredSubscribe to push notifications
DELETE/api/push-subscriptions/{id}RequiredUnsubscribe from push notifications

The VAPID public key endpoint is intentionally anonymous because clients need the key before they can authenticate. If push notifications are not configured on the server, it returns a 400 error.

Subscribe request body:

json
{
  "endpoint": "https://fcm.googleapis.com/fcm/send/...",
  "p256dhKey": "base64-encoded-key",
  "authKey": "base64-encoded-auth"
}

Duplicate subscriptions (same endpoint + user) are detected and return the existing subscription.


SmtpCredentialsController

Route: /api/smtp-credentialsAuth: Required Rate limit: auth (10/min)

Manages API keys used for SMTP/POP3/IMAP authentication and third-party API access. Also returns server connection information.

MethodPathDescription
GET/api/smtp-credentialsGet connection info + list active API keys
POST/api/smtp-credentialsCreate a new API key (returns plaintext key once)
DELETE/api/smtp-credentials/{keyId}Revoke an API key
POST/api/smtp-credentials/{keyId}/rotateRotate a key (creates new, revokes old, preserves scopes)
POST/api/smtp-credentials/mobileCreate a mobile app API key (app scope, requires device name + platform)

GET response includes full connection details for all protocols:

json
{
  "connectionInfo": {
    "smtpServer": "mail.example.com",
    "smtpPort": 587,
    "smtpSecurePort": 465,
    "smtpEnabled": true,
    "pop3Server": "mail.example.com",
    "pop3Port": 110,
    "pop3SecurePort": 995,
    "pop3Enabled": true,
    "imapServer": "mail.example.com",
    "imapPort": 143,
    "imapSecurePort": 993,
    "imapEnabled": true,
    "username": "user@example.com",
    "activeKeyCount": 2
  },
  "apiKeys": [...]
}

Key creation generates a 32-byte random key (Base64-encoded), stores a BCrypt hash, and returns the plaintext key exactly once. The 12-character prefix is stored for efficient database lookup.

Valid scopes: smtp, pop3, imap, api:read, api:write, app, internal

Mobile key creation requires deviceName and platform (ios, android, windows, macos, web). The generated key automatically gets the app scope and a descriptive name like "Mobile App - iPhone 15 (ios)".

Key rotation atomically creates a new key with the same name and scopes, then revokes the old key. The new plaintext key is returned in the response.

Released under the MIT License.