Skip to content

Email Security setup with the CLI

Generally available

Email Security is generally available. Subscribe to the Email Security extension in your organization to purchase and enable it. Paid usage costs \(1 per protected mailbox per month**, billed daily at **\)1/30 per mailbox-day. Free-tier organizations receive a limited 14-day trial. See security product billing for pricing, trial limits and upgrading.

CLI examples require LimaCharlie CLI 5.7.0 or later, which includes the mailsec commands. Install or upgrade, then check:

python -m pip install --upgrade limacharlie
limacharlie mailsec --help

Credential-file examples also require jq.

Prefer the web app? Start with the console walkthrough.

Use a CLI build that includes the commands described above, then configure authentication and select your organization. $OID below means your LimaCharlie organization UUID, not its display name. Use limacharlie org list --output yaml to find it and set OID="<organization-uuid>" for these examples.

This reference takes an organization from zero to a populated Email Security queue: enable the product, connect a mail tenant, verify the connection, and read the first judged message. You can do all of it in the console or entirely as code — both are shown.

1. Enable Email Security

Email Security is enabled per organization by subscribing to the ext-email-security extension. The subscription is the enable gate: without it every /v1/mailsec/* route is refused, and the console shows a subscribe screen instead of the product. Subscribing is the purchase. Paid usage is \(1 per protected mailbox per month**, billed daily at **\)1/30 per mailbox-day on that day's protected-mailbox count. See security product billing.

limacharlie extension subscribe --name ext-email-security --oid $OID

Confirm it:

limacharlie extension list --oid $OID

Subscribing installs the default detection rules in dr-mail. It does not create automation policy records. With no automation policy, automatic actions are off; a new automation rule defaults to alert_only, so it records intent without moving mail. See Policy Reference.

Free trial: 14 days, 25 mailboxes

An organization on the LimaCharlie free tier gets Email Security in full for 14 days and protects up to 25 mailboxes while it does. Every feature is the same as on a paid plan; only the duration and the mailbox count differ. The clock starts the day you subscribe and does not restart if you unsubscribe and resubscribe, so point the 25 at the mailboxes that matter — start with the executives, finance and the abuse mailbox.

When the trial ends, ingestion pauses and nothing is deleted; the data is removed after a 30-day purge grace period unless the organization moves off the free tier before the purge, and you are told before that happens. The full rules, and the exact fields to read the countdown from, are in Plans, the free trial, and the mailbox cap.

The free tier means a configured sensor quota of 2 or less. Raising it above 2 moves the organization to a paid plan, lifts the trial limits, and starts usage billing. Check mailsec coverage and its entitlement block for the trial countdown and any scheduled deletion.

2. Grant the permissions

Email Security ships four permissions. A user or API key that will triage mail typically needs mailsec.get, mailsec.set and mailsec.act; a read-only analyst needs only mailsec.get. mailsec.get.eml is an escalation on top of mailsec.get and should be granted deliberately — see Overview → Permissions.

For setup, ask your administrator for the permissions that match your tasks:

Task Permissions
Subscribe billing.ctrl, user.ctrl
Read, create or edit connections (including --enabled on a data write) mailsec_provider.get, mailsec_provider.set
Enable or disable an existing connection without editing its data Read access to its metadata (mailsec_provider.get.mtd or mailsec_provider.get), and mailsec_provider.set.mtd or mailsec_provider.set
Create and enable a credential secret in one write secret.set
Select existing secrets and read their metadata secret.get.mtd; reading secret values separately needs secret.get
Test a connection mailsec.act
Read messages and coverage mailsec.get
Change mail rules or policy mailsec.set

Deleting a connection additionally needs mailsec_provider.del. Read-only analyst access does not grant permission to change which tenant or mailboxes the product reads.

3. Prepare the provider credential

The credential is created in your mail provider's admin console and stored in the LimaCharlie secret Hive. It is always referenced, never inlined into the connection record.

Provider What you create Full walkthrough
Microsoft 365 An Entra ID app registration with application permissions and a client secret Microsoft 365
Google Workspace A Google Cloud service account with domain-wide delegation, plus a Pub/Sub topic and subscription in the same project Google Workspace

The console renders your own setup guide

The setup steps, OAuth scopes and gcloud commands are served by the product rather than transcribed here, so they cannot go stale: the connection wizard renders them with your project id and service-account address already substituted, and each step names the connection-test check that proves it was done. These pages carry the narrative — what each grant buys and what breaks without it — and the wizard carries the values.

The same guide is available headless:

limacharlie mailsec onboarding --provider gworkspace --oid $OID
limacharlie mailsec onboarding --provider m365 --oid $OID

Fill the Google commands in advance with your project details:

limacharlie mailsec onboarding --provider gworkspace \
  --project-id "$GCP_PROJECT" --sa-email "$SERVICE_ACCOUNT_EMAIL" \
  --topic mailsec-gmail-push --subscription mailsec-gmail-push-sub \
  --oid "$OID" --output yaml

Set the two variables to the project and service-account email from your downloaded key. The CLI uses these values to populate the onboarding instructions.

4. Connect the mail tenant

In the console

Open Email Security → Settings and add a connection. The wizard collects the provider, the saved credential name, the mailbox scope and — for Google Workspace — the project ID used to name its Pub/Sub topic and subscription. It shows the personalized setup guide alongside, and saves the connection. The connection diagnostic then opens if you have permission to test; click Run connection test to verify access. Failed saves are reported inline on the review step, with the validator's own wording rather than a generic error.

Editing an existing connection is patch-preserving: fields the form does not manage are left exactly as they were.

As code

One mailsec_provider Hive record per connection. The record's existence (and its Hive enabled flag) is the connection — there is no separate on switch.

cat > m365-credential.json <<'JSON'
{"tenant_id": "<tenant-id>", "client_id": "<application-client-id>", "client_secret": "<the-secret-value>"}
JSON

jq -Rs '{secret: .}' m365-credential.json \
  | limacharlie secret set --key m365-mail --enabled --oid $OID \
  && rm -f m365-credential.json

jq -Rs builds the secret record's {"secret": "..."} envelope without putting the credential in process arguments. The temporary file is removed only after a successful write.

# m365.yaml
provider: m365
credentials: hive://secret/m365-mail
scope:
  include_addresses:
    - pilot@corp.example
ingest:
  mode: auto  # Microsoft 365 auto is Graph notification push
  backfill_days: 14
features:
  outbound_observation: true

Replace pilot@corp.example with your pilot mailbox addresses. If you later configure features.reports_mailbox, use an existing mailbox and include it in this scope too; otherwise user reports cannot arrive. Omitting scope or leaving its include lists empty covers every discovered mailbox, subject to exclusions and any domain filter. include_addresses and exclude_addresses entries must contain @; domains entries must be bare domains containing a dot, with no @ or slash. Exclusions win over inclusions.

ingest.backfill_days accepts 0–90, default 14. 0 disables the initial connection-setup history pass; it does not delete already indexed mail and is not a privacy cutoff for recovery. If a Gmail history ID or Graph delta token expires, recovery can re-walk the default 14-day window even when this value is 0. See backfill cleanup before changing retention to remove indexed history.

Read the enforcement model before enabling actions: connections start with alert-only automation, and manual actions in an alert-only organization need an explicit --force override.

limacharlie hive set --hive-name mailsec_provider --key m365-prod \
  --input-file m365.yaml --enabled --oid $OID

New Hive records are created disabled

hive set creates a record disabled unless you pass --enabled. A disabled mailsec_provider record is not a connection — nothing is discovered, subscribed or ingested — so a first connection that appears to do nothing is usually this.

The full field reference — scope, provider-specific delivery mode, features — is in Connecting Providers.

5. Verify the connection

The connection test uses the credential the only way a credential can be verified: by using it. Each requirement is reported independently, so a failure names the step to fix rather than saying "connection failed".

limacharlie mailsec connection test m365-prod --oid $OID --output yaml
ok: true
summary: Connection is fully configured.
checks:
  - id: credential
    name: Authenticate to Microsoft Graph
    required: true
    status: passed
  - id: mailbox_read
    name: List mailboxes in the tenant (24 found)
    required: true
    status: passed
  - id: mail_write
    name: "Modify mail (Mail.ReadWrite): quarantine, restore, banner"
    required: true
    status: passed
  - id: mail_send
    name: "Send mail (Mail.Send): reporter auto-replies"
    required: false
    status: skipped
    detail: Mail.Send is not granted; reporter auto-replies will be refused by name until it is

A failed optional check is not an error and ok stays true: a tenant that deliberately declined the optional grant has a working connection, and the product tells you by name which capability it does not have rather than pretending it does. Every failed check carries a remediation string naming the exact fix.

For Google Workspace, add --include-watch to verify notification delivery end to end. It is the one probe with a side effect: it establishes a real Gmail watch, which is idempotent and expires on its own.

6. Watch coverage fill in

limacharlie mailsec coverage --oid $OID --output yaml

Coverage is the product's honesty surface. It reports mailboxes in four separate states — protected, discovered, excluded, error — and never collapses them, because a broken subscription hiding behind a deliberate exclusion is exactly how a coverage number starts lying. connections.state summarizes to the worst connection, and an organization with no connection at all reads unconfigured, never ok.

The same call reports message volume and the verdict funnel over a window, the parse-degradation rate, backfill progress, the emission backlog, open reports, active campaigns, and the effective automation mode.

It also carries an entitlement block: which plan the organization is on, when a trial ends, how many mailboxes it may protect against how many it is protecting, and — if one is scheduled — the date its Email Security data will be deleted and what cancels it. On a trial organization this is where you check that the 25 mailboxes are the 25 you meant:

entitlement:
  plan: trial
  trial_ends_at: "2026-09-20T14:02:11Z"
  trial_days_remaining: 12
  mailbox_cap: 25
  mailboxes_active: 25
  mailboxes_over_cap: 118        # discovered, not protected
  mailbox_cap_reached: true

mailboxes_over_cap is the number that matters: those mailboxes were found and are not being watched. Narrow the connection's scope, or move off the free tier. See Plans, the free trial, and the mailbox cap.

Backfill is judged, and acts on nothing

On connection, the collector walks up to ingest.backfill_days (14 by default) of existing mail. It judges that history with the same rules it judges live mail with, so the queue has real verdicts on your first day and a hunt or a rule backtest has something to run against — and it seeds sender profiles and campaign statistics, so "we have never heard from this sender" is a true statement on day two instead of day ninety.

It takes no actions and emits no telemetry on that history. No EMAIL_MESSAGE, no EMAIL_VERDICT, no policy automation and no remediation: mail delivered eleven days ago has already been read and filed by the person it was addressed to, and quarantining it now — or replaying a fortnight of it into your D&R rules on the day you switch the product on — is not something you asked for. The drawer says so on each such message (judged_via: backfill).

It is also paced, so it cannot compete with live ingestion: a small tenant's fortnight fills in over hours, a very large estate's over days, rather than all at once. Progress is reported in coverage.

7. Read the first judged message

limacharlie mailsec message list --oid $OID --limit 10 --output table
limacharlie mailsec message get <msg_uuid> --oid $OID --output yaml

In the console, Email Security → Messages is the queue and the row opens a drawer with the verdict, the signals that produced it, authentication results, links, attachments, the sender profile, the action timeline and the remediation controls. See Messages & Triage.

8. Decide whether the product may act

Setup grants access to read and modify mail, and establishes notification subscriptions. Default automation does not change messages. Automations ship in alert_only, which means a rule is evaluated, its intent is recorded, and the mailbox is not touched. Manual actions are also withheld in an alert-only organization: force_required: true asks for explicit consent. Repeat with --force in the CLI or JSON force: true in the API to perform that action without enabling organization-wide automation. See manual overrides.

Turning enforcement on is a deliberate edit to a mailsec_policy/automations record. Read Policy Reference before you do, in particular this consequence:

Enforcement is currently an organization-level switch

The remediation executor authorizes automated action when any automation rule in the organization is in enforce mode. Which rule dispatches an action is still decided per rule, but the executor's consent check is not per rule — so putting one rule into enforce enables automated action for the organization's automated paths generally. Enable it when you mean the organization to start moving mail.

Next steps