Microsoft 365 Backup¶
Mailbox backup workflows, browse messages and attachments, scheduling, exclude lists, logs.
All endpoints below are mounted under /api. Authentication: Authorization: Bearer <token> unless noted as public.
Endpoint count: 14
Endpoints¶
| Method | Path | Handler | Guards |
|---|---|---|---|
GET |
/api/m365-backup/attachment/:email/:attachId |
m365DownloadAttachment |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
GET |
/api/m365-backup/config |
m365GetConfig |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
POST |
/api/m365-backup/config |
m365SaveConfig |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
POST |
/api/m365-backup/exclude |
m365UpdateExcluded |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
GET |
/api/m365-backup/logs |
m365GetLogs |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
GET |
/api/m365-backup/messages/:email |
m365BrowseMessages |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
GET |
/api/m365-backup/messages/:email/:msgId/attachments |
m365ListAttachments |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
GET |
/api/m365-backup/messages/:email/:msgId/download |
m365DownloadMessage |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
POST |
/api/m365-backup/run |
m365TriggerRun |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
POST |
/api/m365-backup/schedule |
m365UpdateSchedule |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
GET |
/api/m365-backup/status |
m365GetStatus |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
POST |
/api/m365-backup/test-azure |
m365TestAzure |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
POST |
/api/m365-backup/test-s3 |
m365TestS3 |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
GET |
/api/m365-backup/users |
m365ListUsers |
RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup") |
Source¶
Handlers for this section live in api/routes/handlers_*.go in the StarkOne source. Each handler binds its request body with gin.ShouldBindJSON(&req) and responds with the standard envelope:
On error:
Note: error bodies for this section carry only
error.message(noerror.code). Several handlers in this section also return a flat, non-enveloped JSON body or stream a file rather than using the{ "data": ... }envelope; this is called out per endpoint below.
Reference¶
GET /api/m365-backup/attachment/:email/:attachId¶
Downloads a single backed-up attachment by retrieving it from S3 via the backup CLI and streaming the file bytes to the client.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Path parameters
| Name | Type | Description |
|---|---|---|
email |
string | Mailbox address whose backup SQLite DB is opened (the @ is rewritten to _at_ to form the DB filename). |
attachId |
string | Primary-key id of the attachment row in the mailbox attachments table. |
Response - 200 OK
Streams the raw attachment bytes (not enveloped). The Content-Type is the attachment's stored content_type, and the response sets Content-Disposition: attachment; filename="<original filename>".
Errors
| Status | Code | Meaning |
|---|---|---|
404 |
(none - error.message only) |
Attachment not found in the mailbox DB. |
500 |
(none - error.message only) |
Failed to open the mailbox DB, S3 download failed, or the downloaded temp file could not be read. |
Side effects
- Opens the mailbox SQLite DB read-only at
/var/lib/starkone/m365-backup/mailboxes/<safeEmail>.db. - Runs
starkone-m365-backup download-attachment --s3-key <key> --output <tmp>to fetch the object from S3. - Writes and then deletes a temp file under the OS temp dir (
m365-attach-<basename>).
Example
curl -X GET https://panel.example.com:2087/api/m365-backup/attachment/[email protected]/att-123 \
-H "Authorization: Bearer $TOKEN" -OJ
GET /api/m365-backup/config¶
Returns the current M365 backup configuration with secret fields masked; returns sensible defaults if no config file exists yet.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Response - 200 OK
Secrets (azure_client_secret, s3_access_key, s3_secret_key) are masked (e.g. abcd••••wxyz, or •••••••• when 8 chars or fewer). When no config file exists, defaults are returned (backup_schedule defaults to 0 2 * * *, retention_days to 365, data_dir to /var/lib/starkone/m365-backup).
{
"data": {
"azure_tenant_id": "00000000-0000-0000-0000-000000000000",
"azure_client_id": "11111111-1111-1111-1111-111111111111",
"azure_client_secret": "abcd••••wxyz",
"s3_endpoint": "https://s3.example.com",
"s3_bucket": "m365-backups",
"s3_access_key": "AKIA••••MPLE",
"s3_secret_key": "••••••••",
"s3_region": "us-east-1",
"s3_encrypt": true,
"attachment_storage": "s3",
"backup_schedule": "0 2 * * *",
"excluded_users": [],
"retention_days": 365,
"data_dir": "/var/lib/starkone/m365-backup"
}
}
Errors
| Status | Code | Meaning |
|---|---|---|
500 |
(none - error.message only) |
Config file exists but could not be read/parsed (errors other than not-found). |
Example
curl -X GET https://panel.example.com:2087/api/m365-backup/config \
-H "Authorization: Bearer $TOKEN"
POST /api/m365-backup/config¶
Saves the full M365 backup configuration to disk, preserving existing secrets when masked placeholder values are submitted and encrypting secrets at rest.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Request body
The full m365Cfg struct. No field has binding:"required". If a secret field contains the masked marker •••• or the literal ********, the previously stored secret is retained instead of overwriting. Empty data_dir defaults to /var/lib/starkone/m365-backup; retention_days of 0 defaults to 365; nil excluded_users becomes [].
{
"azure_tenant_id": "string",
"azure_client_id": "string",
"azure_client_secret": "string",
"s3_endpoint": "string",
"s3_bucket": "string",
"s3_access_key": "string",
"s3_secret_key": "string",
"s3_region": "string",
"s3_encrypt": true,
"attachment_storage": "string",
"backup_schedule": "string",
"excluded_users": ["string"],
"retention_days": 365,
"data_dir": "string"
}
| Field | Type | Required | Description |
|---|---|---|---|
azure_tenant_id |
string | no | Azure AD tenant ID. |
azure_client_id |
string | no | Azure app registration client ID. |
azure_client_secret |
string | no | Azure client secret (encrypted at rest; masked value preserves existing). |
s3_endpoint |
string | no | S3-compatible endpoint URL. |
s3_bucket |
string | no | Destination bucket name. |
s3_access_key |
string | no | S3 access key (encrypted at rest; masked value preserves existing). |
s3_secret_key |
string | no | S3 secret key (encrypted at rest; masked value preserves existing). |
s3_region |
string | no | S3 region. |
s3_encrypt |
bool | no | Whether to encrypt uploaded objects. |
attachment_storage |
string | no | Attachment storage mode. |
backup_schedule |
string | no | Cron expression for scheduled backups. |
excluded_users |
array of string | no | Mailboxes/users to skip. |
retention_days |
int | no | Days to retain backups (defaults to 365 if 0). |
data_dir |
string | no | Local data directory (defaults to /var/lib/starkone/m365-backup). |
Response - 200 OK
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none - error.message only) |
Request body failed JSON binding. |
500 |
(none - error.message only) |
Could not derive encryption key or write the config file. |
Side effects
- Creates directory
/var/lib/starkone/m365-backup(mode0700) if missing. - Writes
/var/lib/starkone/m365-backup/config.json(mode0600) with Azure/S3 secrets AES-GCM encrypted using a key derived from/etc/machine-id.
Example
curl -X POST https://panel.example.com:2087/api/m365-backup/config \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"azure_tenant_id":"00000000-0000-0000-0000-000000000000","s3_bucket":"m365-backups","retention_days":365}'
POST /api/m365-backup/exclude¶
Replaces the list of excluded mailboxes/users in the saved configuration.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Request body
{ "excluded_users": ["[email protected]"] }
| Field | Type | Required | Description |
|---|---|---|---|
excluded_users |
array of string | no | New full exclude list (nil is stored as []). Replaces, not merges. |
Response - 200 OK
{ "data": { "status": "ok", "excluded_users": ["[email protected]"] } }
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none - error.message only) |
Request body failed JSON binding. |
500 |
(none - error.message only) |
Existing config could not be loaded, or saving the config failed. |
Side effects
- Loads and re-writes
/var/lib/starkone/m365-backup/config.jsonwith the updatedexcluded_users.
Example
curl -X POST https://panel.example.com:2087/api/m365-backup/exclude \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"excluded_users":["[email protected]"]}'
GET /api/m365-backup/logs¶
Returns the last 100 lines of the M365 backup log file.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Response - 200 OK
{
"data": {
"log_file": "/var/log/starkone/m365-backup.log",
"lines": ["2026-06-17 02:00:01 starting backup", "..."]
}
}
lines is [] if the log file does not exist.
Example
GET /api/m365-backup/messages/:email¶
Browses backed-up messages for one mailbox with folder facets, optional folder/search filtering, and fixed-size pagination (50 per page).
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Path parameters
| Name | Type | Description |
|---|---|---|
email |
string | Mailbox address; @ is rewritten to _at_ to locate the backup DB. |
Query parameters
| Name | Type | Optional | Description |
|---|---|---|---|
folder |
string | yes | Restrict to a single folder name (default empty = all folders). |
search |
string | yes | Substring matched against subject, from_email, and from_name (default empty). |
page |
int | yes | 1-based page number (defaults to 1; page size is fixed at 50). |
Response - 200 OK
Flat (non-enveloped) body.
{
"email": "[email protected]",
"folders": [{ "name": "Inbox", "count": 1200 }],
"messages": [
{
"id": "msg-1",
"folder": "Inbox",
"subject": "Welcome",
"from_email": "[email protected]",
"from_name": "Sender",
"to_emails": "[email protected]",
"date": "2026-06-01T09:00:00Z",
"has_attachments": false,
"importance": "normal",
"is_read": true,
"status": "active"
}
],
"total": 1200,
"page": 1,
"per_page": 50
}
Errors
| Status | Code | Meaning |
|---|---|---|
404 |
(none - error.message only) |
No backup DB file exists for the given mailbox. |
500 |
(none - error.message only) |
Mailbox DB could not be opened. |
Example
curl -X GET "https://panel.example.com:2087/api/m365-backup/messages/[email protected]?folder=Inbox&search=invoice&page=2" \
-H "Authorization: Bearer $TOKEN"
GET /api/m365-backup/messages/:email/:msgId/attachments¶
Lists attachment metadata for a single backed-up message.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Path parameters
| Name | Type | Description |
|---|---|---|
email |
string | Mailbox address; @ is rewritten to _at_ to locate the backup DB. |
msgId |
string | Message id whose attachments are listed. |
Response - 200 OK
Flat (non-enveloped) body.
{
"attachments": [
{
"id": "att-123",
"filename": "report.pdf",
"size": 48213,
"content_type": "application/pdf",
"s3_key": "mailboxes/user1/att-123"
}
]
}
Errors
| Status | Code | Meaning |
|---|---|---|
500 |
(none - error.message only) |
Mailbox DB could not be opened or queried. |
Example
curl -X GET https://panel.example.com:2087/api/m365-backup/messages/[email protected]/msg-1/attachments \
-H "Authorization: Bearer $TOKEN"
GET /api/m365-backup/messages/:email/:msgId/download¶
Reconstructs a backed-up message as an .eml file (multipart/mixed with base64 attachments when present) and streams it for download.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Path parameters
| Name | Type | Description |
|---|---|---|
email |
string | Mailbox address; @ is rewritten to _at_ to locate the backup DB. |
msgId |
string | Message id to export. |
Response - 200 OK
Streams an RFC 822 message (Content-Type: message/rfc822, not enveloped) with Content-Disposition: attachment; filename="<sanitized subject, max 50 chars>.eml". If the message has attachments, they are fetched from S3 and embedded as base64 MIME parts.
Errors
| Status | Code | Meaning |
|---|---|---|
404 |
(none - error.message only) |
Message not found in the mailbox DB. |
500 |
(none - error.message only) |
Mailbox DB could not be opened. |
Side effects
- Opens the mailbox SQLite DB read-only.
- For each attachment, runs
starkone-m365-backup download-attachment --s3-key <key> --output <tmp>; writes and removes temp files under the OS temp dir (m365-att-<id>). Failed attachment downloads are silently skipped (empty body part).
Example
curl -X GET https://panel.example.com:2087/api/m365-backup/messages/[email protected]/msg-1/download \
-H "Authorization: Bearer $TOKEN" -OJ
POST /api/m365-backup/run¶
Triggers an asynchronous backup run via the CLI binary, guarding against concurrent runs.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Response - 202 Accepted
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none - error.message only) |
starkone-m365-backup binary not found on PATH or at /usr/sbin/. |
409 |
(none - error.message only) |
A backup is already running. |
Side effects
- Sets an in-process
runningflag (cleared when the background run finishes). - Creates
/var/log/starkone(mode0755) if missing and appends to/var/log/starkone/m365-backup.log(mode0644). - Spawns
starkone-m365-backup runin the background, capturing stdout/stderr to the log file.
Example
POST /api/m365-backup/schedule¶
Installs, updates, or disables the backup cron entry and mirrors the schedule into the saved config.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Request body
| Field | Type | Required | Description |
|---|---|---|---|
schedule |
string | conditional | Cron expression. Required (non-empty) unless enabled is false. |
enabled |
bool (nullable) | no | When explicitly false, removes the cron entry and clears the stored schedule. When omitted/true, installs/updates the cron entry. |
Response - 200 OK
When disabling:
When installing/updating:
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none - error.message only) |
Body failed JSON binding, or schedule is empty while not disabling. |
500 |
(none - error.message only) |
Removing or installing the crontab entry failed. |
Side effects
- Rewrites the user crontab via
crontab -l/crontab -to add or remove the line tagged# STARKONE_M365_BACKUP(which runs/usr/sbin/starkone-m365-backup run >> /var/log/starkone/m365-backup.log 2>&1). - Updates
backup_schedulein/var/lib/starkone/m365-backup/config.json(best-effort; ignored if config load fails).
Example
curl -X POST https://panel.example.com:2087/api/m365-backup/schedule \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"schedule":"0 2 * * *"}'
GET /api/m365-backup/status¶
Reports whether a backup is currently running, the active cron schedule, the last run record, and per-mailbox DB size/timestamp stats.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Response - 200 OK
last_run is included only if last_run.json exists and parses; its shape is whatever the backup CLI wrote.
{
"data": {
"running": false,
"schedule": "0 2 * * *",
"user_stats": [
{
"user_id": "user1_at_example.com",
"db_size": 10485760,
"last_mod": "2026-06-17T02:14:00Z"
}
],
"last_run": { "...": "as written by the backup CLI" }
}
}
Side effects
- Reads
/var/lib/starkone/m365-backup/last_run.jsonand lists*.dbfiles under/var/lib/starkone/m365-backup/mailboxes(pure read).
Example
curl -X GET https://panel.example.com:2087/api/m365-backup/status \
-H "Authorization: Bearer $TOKEN"
POST /api/m365-backup/test-azure¶
Validates Azure credentials by acquiring a Graph token and listing users, returning the discovered user count.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Request body
The m365Cfg struct; only the Azure fields are used. If azure_client_secret is exactly ********, the stored secret is substituted. No field has binding:"required".
{
"azure_tenant_id": "00000000-0000-0000-0000-000000000000",
"azure_client_id": "11111111-1111-1111-1111-111111111111",
"azure_client_secret": "string"
}
| Field | Type | Required | Description |
|---|---|---|---|
azure_tenant_id |
string | no | Azure AD tenant ID. |
azure_client_id |
string | no | Azure app client ID. |
azure_client_secret |
string | no | Azure client secret (******** reuses the stored secret). |
Response - 200 OK
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none - error.message only) |
Request body failed JSON binding. |
502 |
(none - error.message only) |
Azure token acquisition failed, or listing users via Graph failed. |
Side effects
- Outbound HTTPS to
login.microsoftonline.com(token) andgraph.microsoft.com(user list). No local writes.
Example
curl -X POST https://panel.example.com:2087/api/m365-backup/test-azure \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"azure_tenant_id":"00000000-0000-0000-0000-000000000000","azure_client_id":"11111111-1111-1111-1111-111111111111","azure_client_secret":"secret"}'
POST /api/m365-backup/test-s3¶
Validates S3 connectivity by writing a temp config and invoking the backup CLI's test-s3 subcommand.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Request body
The m365Cfg struct. Masked secret values (containing •••• or equal to ********) are replaced with the stored secrets before testing. No field has binding:"required".
{
"s3_endpoint": "https://s3.example.com",
"s3_bucket": "m365-backups",
"s3_access_key": "string",
"s3_secret_key": "string",
"s3_region": "us-east-1",
"s3_encrypt": true
}
| Field | Type | Required | Description |
|---|---|---|---|
s3_endpoint |
string | no | S3-compatible endpoint URL. |
s3_bucket |
string | no | Bucket to test against. |
s3_access_key |
string | no | S3 access key (masked value reuses stored). |
s3_secret_key |
string | no | S3 secret key (masked value reuses stored). |
s3_region |
string | no | S3 region. |
s3_encrypt |
bool | no | Encryption flag passed through to the test. |
(Other m365Cfg fields may be supplied but are only used to build the temp config.)
Response - 200 OK
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
(none - error.message only) |
Body failed JSON binding, or the starkone-m365-backup binary was not found. |
500 |
(none - error.message only) |
Failed to create or write the temp config file. |
502 |
(none - error.message only) |
The CLI test-s3 command returned a non-zero exit (its combined output is returned as the message). |
Side effects
- Writes a temp config file (
m365-test-*.json, secrets encrypted) under the OS temp dir and removes it afterward. - Runs
starkone-m365-backup test-s3 --config <tmp>.
Example
curl -X POST https://panel.example.com:2087/api/m365-backup/test-s3 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"s3_endpoint":"https://s3.example.com","s3_bucket":"m365-backups","s3_access_key":"AKIA...","s3_secret_key":"...","s3_region":"us-east-1"}'
GET /api/m365-backup/users¶
Lists Microsoft 365 users via Graph, annotating each with whether it is excluded and, when a backup exists, the local DB size and last-backup time.
Guards: RequireSuperuser(), RequireFeature("m365_backup"), RequirePlugin(db, "m365-backup")
Response - 200 OK
Each user includes id, display_name, mail, and excluded. db_size and last_backup are present only when a backup DB exists for that user's ID.
{
"data": {
"users": [
{
"id": "user-id-1",
"display_name": "User One",
"mail": "[email protected]",
"excluded": false,
"db_size": 10485760,
"last_backup": "2026-06-17T02:14:00Z"
}
]
}
}
Errors
| Status | Code | Meaning |
|---|---|---|
500 |
(none - error.message only) |
Configuration could not be loaded. |
502 |
(none - error.message only) |
Azure authentication failed, or listing users via Graph failed. |
Side effects
- Outbound HTTPS to Azure for token + Graph user list; reads (stats) mailbox DB files under
<data_dir>/mailboxes. No writes.
Example