Skip to content

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:

{ "data": { ... } }

On error:

{ "error": { "code": "string", "message": "string" } }

Note: error bodies for this section carry only error.message (no error.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

{ "data": { "status": "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 (mode 0700) if missing.
  • Writes /var/lib/starkone/m365-backup/config.json (mode 0600) 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.json with the updated excluded_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

curl -X GET https://panel.example.com:2087/api/m365-backup/logs \
  -H "Authorization: Bearer $TOKEN"

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

{ "data": { "status": "started" } }

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 running flag (cleared when the background run finishes).
  • Creates /var/log/starkone (mode 0755) if missing and appends to /var/log/starkone/m365-backup.log (mode 0644).
  • Spawns starkone-m365-backup run in the background, capturing stdout/stderr to the log file.

Example

curl -X POST https://panel.example.com:2087/api/m365-backup/run \
  -H "Authorization: Bearer $TOKEN"

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

{ "schedule": "0 2 * * *", "enabled": true }
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:

{ "data": { "status": "ok", "enabled": false } }

When installing/updating:

{ "data": { "status": "ok", "schedule": "0 2 * * *" } }

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_schedule in /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.json and lists *.db files 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

{ "data": { "status": "ok", "user_count": 42 } }

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) and graph.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

{ "data": { "status": "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

curl -X GET https://panel.example.com:2087/api/m365-backup/users \
  -H "Authorization: Bearer $TOKEN"