Sample Submission¶
Private beta
Email Security is in private beta. It is not generally available, and
access is enabled per organization — if the ext-email-security extension
is not in your catalog, this product is not turned on for you yet.
While it is in beta, expect the surface described here to move: commands, fields and event shapes may change between releases, and they may change in ways that are not backwards compatible. Install or upgrade the CLI before running the examples:
Credential-file examples also require jq. Re-read these pages after upgrading.
Talk to us before relying on it in production.
Sample submission lets your analysts send LimaCharlie a copy of a message the engine got wrong, so detection can improve. It is off by default, it is never automatic, and it works one message at a time.
Submitting sends the message to LimaCharlie
When an analyst submits a message, LimaCharlie keeps a copy of the original message, including its attachments. The sections below state exactly what is kept, where, for how long and how access to it is recorded. You can withdraw any submission at any time, which deletes the copy.
Why it exists¶
Two kinds of mistakes are worth telling us about:
- a threat the engine called benign or unknown (a missed threat), and
- a legitimate message the engine flagged (a false positive).
Sample submission is the way to hand us one of those, with a short reason, without turning on anything broader. It copies the message to LimaCharlie. It does not change the verdict on your message.
Turning it on¶
Sample submission is opt-in per organization. Only the organization Owner can turn it on. Until you opt in, submit requests are refused and nothing is ever copied.
Opt in with a mailsec_policy record of type sample_sharing:
echo '{"policy_type": "sample_sharing", "enabled": true}' > opt-in.json
limacharlie hive set --hive-name mailsec_policy --key sample-sharing \
--input-file opt-in.json --enabled
| Field | Default | |
|---|---|---|
enabled |
false |
Without a record the feature is off. Set it to false on an active record without expiry to turn it off again |
The record has only that one field. Unknown fields are refused, and a record that
sets nothing is refused. When several records set enabled, the last one in
record-name order wins, as with reporter_reply.
See Policy Reference. Enabling sharing requires
mailsec.set and Owner authority (billing.ctrl and user.ctrl) for the organization.
Anyone with mailsec.set can turn it off by writing enabled: false on an active
record without expiry. Deleting, expiring or disabling a sample-sharing record requires
Owner authority because removing an override can reveal an earlier enabled record.
Turning sharing off prevents new submissions; existing copies remain until withdrawal
or retention expiry.
Who can submit¶
Only a person. Submitting and withdrawing need mailsec.act, and the request
must come from an analyst in the console or from an API key.
D&R rules, automations and the AI agent cannot submit or withdraw. The backend
refuses them, records the refusal as a failed action, and returns:
submit_sample is an analyst action: automation, D&R rules and the AI agent may not send mail to LimaCharlie
Listing and reading submissions needs mailsec.get.
Alert-only mode does not apply¶
Alert-only mode governs actions that write to a
mailbox, and submitting or withdrawing a sample touches none. So an organization in
alert-only mode can still submit and withdraw samples, and --force is not needed
or used. What governs these two actions is the opt-in and the rule that only a
person can run them.
What you choose when you submit¶
Every submission needs a category and a reason.
| Category | Use it when |
|---|---|
missed_threat |
We called the message benign or unknown, and it is a threat |
false_positive |
We flagged the message, and it is legitimate |
other |
Any other detection issue with this message |
The reason is free text, 1 to 1024 characters after trimming, and is kept with the submission. Unlike the other message actions, it is required.
What is kept¶
For each submission, LimaCharlie keeps:
- The message itself: the original raw bytes (RFC 822, including attachments), compressed and encrypted with AES-256-GCM. The encryption key is derived per organization from a dedicated LimaCharlie key that is separate from the key protecting your own stored mail.
- A metadata row: the message id, the category, the reason your analyst typed, your analyst's identity, the time, the verdict, score and matched detection rule ids at that time, the sender, the subject, the mailbox address and the size.
Only the one message you chose is copied.
Where it is kept and for how long¶
- Where: in a LimaCharlie-owned bucket in the same datacenter and region as your organization's Email Security data. It is a separate bucket from the one that holds your raw messages.
- How long: 400 days from the submission. After that the copy is no longer available for
access and is deleted automatically by background cleanup. Your organization's mail retention settings (
message_daysandflagged_daysin theretentionrecord) do not apply to submissions: they neither shorten nor extend that period. - Earlier: any time you withdraw it, or when your organization's Email Security data is deleted (see below).
Access and how you can tell¶
Submitted messages are used by LimaCharlie to improve detection. Access is restricted and every access is recorded. No other customer can see it.
You can see that record. Each submission carries a review_count and a
last_reviewed_at, and GET /submissions/{submission_id} returns the
up to 200 access timestamps, oldest first; reviews_truncated says when
more exist. The total review_count and latest last_reviewed_at stay accurate. Access is recorded before decryption,
so the count can include attempts that failed to open the copy. The record is a count and
timestamps only: it never names the identity that accessed it.
Withdrawing¶
You can withdraw at any time, from the console, the CLI or the API, even after turning sample sharing off, disconnecting the mailbox provider, or expiry of the message index. Withdrawing deletes the stored copy and its metadata (a hard delete), then writes the withdrawal to the audit trail. It cannot be undone; to share the message again, submit it again.
- Withdrawing by message:
withdraw_sampleon the message, orlimacharlie mailsec message withdraw-sample <msg_uuid>. - Withdrawing by submission id:
DELETE /submissions/{submission_id}, orlimacharlie mailsec submission withdraw <submission_id>.
Withdrawing an unknown or already-deleted submission is harmless: the submission
routes answer withdrawn: false and nothing is deleted a second time. An expired
submission can still be withdrawn while background cleanup is pending; any
surviving copy and metadata are deleted and the response says withdrawn: true.
If the organization is deleted¶
Deleting the organization (the Email Security tenant purge) deletes all of its submissions too. The automatic deletion that follows an unsubscribe or a trial lapse is the same tenant purge, so it deletes submissions as well. See Data retention and deletion.
From the command line¶
# Submit one message. Both --category and --reason are required.
limacharlie mailsec message submit-sample <MSG_UUID> \
--category missed_threat --reason "credential phish we did not flag"
# Withdraw by message, or by submission id
limacharlie mailsec message withdraw-sample <MSG_UUID>
limacharlie mailsec submission withdraw <SUBMISSION_ID>
# See what you have sent
limacharlie mailsec submission list
limacharlie mailsec submission list --category false_positive --since 2026-09-01T00:00:00Z --limit 100
# One submission, including when LimaCharlie accessed it
limacharlie mailsec submission get <SUBMISSION_ID>
The CLI checks the category and the reason before sending anything. A refused submission exits non-zero and prints the reason. See Command Line Interface.
Routes¶
All routes are under /v1/mailsec/{oid}. See API Reference
for the shared conventions.
| Route | Does |
|---|---|
POST /messages/{msg_uuid}/actions with {"action": "submit_sample", "category": ..., "reason": ...} |
Submit one message. category and reason are required; attempt is an optional idempotency token. Requires mailsec.act |
POST /messages/{msg_uuid}/actions with {"action": "withdraw_sample"} |
Withdraw the submission made from this message. reason (up to 1024 characters) and attempt are optional. Requires mailsec.act |
GET /submissions |
{enabled, available, submissions, next_cursor}. Filters: category, since, until (RFC 3339), limit (1-200, default 50), cursor. Requires mailsec.get |
GET /submissions/{submission_id} |
{submission, reviews}, where reviews is [{ts}], up to 200 recorded accesses. reviews_truncated identifies a partial history; count and latest time remain complete. An unknown id is not an error: it returns {"submission": null, "reviews": []}, so branch on null. Requires mailsec.get |
DELETE /submissions/{submission_id} |
{withdrawn: true, submission_id, action_id}. Hard-deletes the stored copy and its metadata. An unknown or already-deleted id returns {withdrawn: false, submission_id} with no action_id. An expired id is still cleaned up if its metadata remains. Requires mailsec.act |
GET /submissions always returns two flags, so an empty list is never ambiguous:
enabled says your organization has opted in, and available says your
datacenter has a submissions store. Pagination works as for the other lists: pass
next_cursor back as cursor, verbatim, until it is empty.
A submission looks like this:
{
"submission_id": "3f1c9b7e5a2d4c8e9a0b1c2d3e4f5a6b",
"msg_uuid": "0057db2b-0000-4000-8000-000000000001",
"category": "missed_threat",
"reason": "credential phish we did not flag",
"actor": "analyst@corp.example",
"ts": "2026-09-30T12:00:00Z",
"expires_at": "2027-11-04T12:00:00Z",
"provider": "m365",
"mailbox_address": "user@corp.example",
"sender_email": "sender@example.net",
"subject": "Invoice overdue",
"verdict": "benign",
"score": 0,
"matched_rules": [],
"size_bytes": 12345,
"review_count": 0
}
last_reviewed_at is present once an access has been recorded and
omitted before that.
Action results and refusals¶
A submission goes through the same action route as other message actions and
returns the same result shape. ok and skipped carry a submission_id;
skipped means the message already has an active submission, so submitting twice
is safe. A refusal is reported like any other failed action, with one of these
texts in error:
| Error | Meaning |
|---|---|
sample submission is not enabled for this organization |
You have not opted in |
sample submission is not available in this datacenter |
Your datacenter has no submissions store |
the message's raw copy is no longer stored, so it cannot be submitted |
The raw copy aged out or was never stored |
submit_sample is an analyst action: automation, D&R rules and the AI agent may not send mail to LimaCharlie |
The request came from automation, a D&R rule or the AI agent |
A missing or unknown category, or a missing or over-long reason, is refused
with an HTTP 400 before anything is sent.
Actions that reach execution are written to the action audit trail. While the
mailbox connection is active, an EMAIL_ACTION event also carries category and
submission_id. Withdrawal after disconnect remains in the action and platform
audit trails. Submitting and withdrawing also write mailsec_sample_submitted and mailsec_sample_withdrawn
platform audit events.
FAQ¶
Does submitting change my verdicts? No. It copies the message to LimaCharlie; it does not revise the verdict or move the message.
Is anything submitted automatically? No. Nothing is submitted unless a person runs the action on a message, and only after your organization has opted in.
Is it per message? Yes. Each submission is one message. There is no bulk or campaign form.
Can I see when the copy was accessed? Yes: the access count and timestamps are shown on every submission. The accessing identity is not shown.
Can I take it back? Yes, at any time. Withdrawing deletes LimaCharlie's copy and its metadata.
Can another customer see it? No.