Email Header Validator
communication.email_validate_headers
Overview
Nothing here is sent and nothing is connected to. We read the headers you hand us and check their syntax and your policy, and that is the whole of it: we cannot tell you whether a domain exists, whether a mailbox is real, or whether your message will be delivered. The word validate invites exactly that assumption, so we say it plainly — no name is resolved, no mail exchanger is looked up, and a clean report means the headers are well formed, not that the mail will arrive. What you do get is three kinds of finding. What is missing, judged against a profile you pick or a list you supply. What is malformed: a value that carries a carriage return and therefore injects a second header, a date nothing can read, a message identifier with no domain half, a header that appears twice when it may appear once. And what is risky: a display name carrying a different address from the real one, a reply that would leave your domain, a blind copy alongside visible recipients, more recipients than you allow, and bulk mail with no way to unsubscribe. Every finding carries a severity, and a report is hygiene and policy — never a spam verdict and never an authentication result.
A header value that carries a carriage return does not look wrong until a receiving server reads everything after it as a header you never wrote, which is how a blind copy gets added to somebody else's message. Around that one check sit the quieter problems that cost you delivery rather than security: a date nothing can parse, a message identifier with no domain so threading breaks, two From headers so nobody agrees who sent it, bulk mail with no way out. All of them are decidable from the headers alone, and all of them are cheaper to catch here than in a postmaster's reply. Address syntax comes from the same engine our email-syntax skill uses, so an address judged valid here is valid everywhere on the platform.
Reliability
The same headers and the same rules always produce the same report, in the same order, and no clock or network is involved anywhere. Names are matched without regard to case. A header name longer than 256 characters or a value longer than 8192 is reported as a finding rather than refused, so you still get the rest of the report. Findings never carry an address, a subject or a body — only a header name, a position, a count and a limit — and nothing you send is written to a log. Four things stop the call instead of answering it. HEADERS_INVALID means the set could not be read as headers at all: an entry that is not an object, an entry with a property other than name and value, an empty name, a value that is not a string, or an empty set — fix the shape named in the message and retry, because the same payload will always be refused; HEADERS_LIMIT_EXCEEDED means more than 256 headers, or more header text in total than one request may carry — send the headers you actually want checked; HEADER_PROFILE_UNSUPPORTED means profile was something other than TRANSACTIONAL, MARKETING or MINIMAL, and we refuse rather than quietly fall back to the default, because a message checked under rules nobody chose is worse than no check — correct the value and retry; HEADER_RULES_INVALID means the rules object named a property we do not implement, a rule was not a boolean, maxRecipients was outside 1 to 100000, or one of the two header-name lists was too long or held something that is not a usable name — correct the rule and retry. A malformed or risky header set is never one of these: it is a normal success with valid false and the findings inside.
Context footprint
A clean set answers in well under a kilobyte. A report grows with the number of findings and stops at maxIssues, and the counts in summary stay complete even when the list is cut, so you always know how much you did not see. A successful call costs 1 execution credit.
When to use it
Before a message is handed to a mail server, whenever any part of a header was built from something a person typed, and in the build that ships your outbound templates.
When you need to know an address is deliverable, when you want a spam or authentication verdict, or when you have a raw message rather than a header set.
Scope
Non-goals
- Does not send, queue or relay a message, and opens no connection of any kind
- Does not resolve a domain, look up a mail exchanger, or confirm a mailbox exists, so a clean report never means the mail will be delivered
- Does not check SPF, DKIM or DMARC — those need name resolution and cryptographic verification, and we do neither
- Does not score a message for spam, and a clean report is not a reputation verdict
- Does not repair a header, reorder a set, or hand back a corrected message
- Does not read a message body or an attachment
Properties
Capabilities
Cost & billing
Flat rate per invocation
File handling
How we handle your data
What this means
This skill may receive sensitive content, but it does not target sensitive data and returns only a structured non-sensitive result.
Squillify guarantees
- Runs inside Squillify
- This skill runs inside Squillify.
- Payloads are not sent to an external service by this skill.
- Squillify does not permanently store submitted payloads.
- Telemetry records metadata only, not submitted content.
- Submitted payloads are not used for training.
- Submitted payloads are not reviewed by humans.
- This skill does not target sensitive data.
- The response does not contain submitted content.
Your responsibility
Squillify does not determine whether submitted content is sensitive. Your application is responsible for deciding what data it is allowed to submit. Your application is responsible for securing the response.
Input schema
| Field | Type | Required | Description |
|---|---|---|---|
| headers | array | Yes | Bounded ordered array of header name/value declarations. The item shape is defined by the nested schema and the array contains from 0 through 10000 items. |
| profile | string | No | Named header profile; omit it for standard. |
| requiredHeaders | array | No | Bounded optional required-header names; omit it to use profile requirements. The item shape is defined by the nested schema and the array contains from 0 through 10000 items. |
| forbiddenHeaders | array | No | Bounded optional forbidden-header names; omit it to use profile restrictions. The item shape is defined by the nested schema and the array contains from 0 through 10000 items. |
| rules | object | No | Closed header-policy option object; omit it for profile defaults. Its documented communication properties are bounded to 256 entries — unconstrained RFC 8259 JSON items. |
| maxIssues | integer | No | Issue cap; omit it for one thousand. |
Output schema
| Field | Type | Required | Description |
|---|---|---|---|
| valid | boolean | Yes | Whether no error-severity header issue exists. |
| issues | array | Yes | Bounded header issue receipts in deterministic rule order. The item shape is defined by the nested schema and the array contains from 0 through 10000 items. |
| summary | object | Yes | Closed header, error, warning, and truncation summary. Its documented communication properties are bounded to 256 entries — unconstrained RFC 8259 JSON items. |
| resolvedRules | object | Yes | Closed applied header rules with defaults filled in. Its documented communication properties are bounded to 256 entries — unconstrained RFC 8259 JSON items. |
API contract
POST /v1/executecurl -sS -X POST "$BASE/v1/execute" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -d '{"skillKey":"communication.email_validate_headers","input":{"headers":[{"name":"From","value":"Northwind Orders <[email protected]>"},{"name":"To","value":"[email protected]"},{"name":"Date","value":"Tue, 28 Jul 2026 09:15:00 +0200"},{"name":"Message-ID","value":"<[email protected]>"}],"profile":"TRANSACTIONAL"}}'import { Squillify } from "@squillify/sdk";
const sq = new Squillify({ apiKey: process.env.SQUILLIFY_API_KEY });
const { output } = await sq.execute("communication.email_validate_headers", {"headers":[{"name":"From","value":"Northwind Orders <[email protected]>"},{"name":"To","value":"Alice Whitfield <[email protected]>"},{"name":"Subject","value":"Your order 4417 has shipped"},{"name":"Date","value":"Tue, 28 Jul 2026 09:15:00 +0200"},{"name":"Message-ID","value":"<[email protected]>"}],"profile":"TRANSACTIONAL"});Errors
| Code | HTTP | Retryable | Description |
|---|---|---|---|
| VALIDATION_ERROR | 400 | No | The request payload failed schema validation. |
| UNAUTHORIZED | 401 | No | Missing or invalid API key. |
| SKILL_NOT_ACCESSIBLE | 403 | No | The API key is not granted access to this skill. |
| SKILL_NOT_AVAILABLE_FOR_PLAN | 403 | No | This skill is not included in your current plan. |
| PLAN_ELIGIBILITY_UNAVAILABLE | 503 | Yes | Plan eligibility could not be decided. Retry after backoff. |
| SKILL_NOT_FOUND | 404 | No | The requested skill or version does not exist. |
| TENANT_RATE_LIMITED | 429 | Yes | Request rate limit exceeded. Retry after backoff. |
| QUOTA_EXCEEDED | 429 | Yes | Plan quota exhausted. Retryable once the quota period resets. |
| EXECUTION_TIMEOUT | 504 | Yes | Execution exceeded the allowed time budget. |
| HEADERS_INVALID | 400 | No | The request payload failed this skill's input validation. |
| HEADERS_LIMIT_EXCEEDED | 429 | No | A size, quota, rate, or time limit was exceeded. |
| HEADER_PROFILE_UNSUPPORTED | 400 | No | The skill does not support the requested variant. |
| HEADER_RULES_INVALID | 400 | No | The request payload failed this skill's input validation. |
Other limits
| Limit | Value |
|---|---|
| Recommended inline bytes | 32 KiB |
Limitations
- Does not send, queue or relay a message, and opens no connection of any kind
- Does not resolve a domain, look up a mail exchanger, or confirm a mailbox exists, so a clean report never means the mail will be delivered
- Does not check SPF, DKIM or DMARC — those need name resolution and cryptographic verification, and we do neither
- Does not score a message for spam, and a clean report is not a reputation verdict
- Does not repair a header, reorder a set, or hand back a corrected message
- Does not read a message body or an attachment
- Deciding whether an address can receive mail, which needs the network
- Judging whether a message is spam or whether its sender is authenticated
- Parsing a whole raw message into headers, body and attachments
How to call
POST /v1/execute (requires authentication)
{
"skillKey": "communication.email_validate_headers",
"input": {
"headers": [
{
"name": "From",
"value": "Northwind Orders <[email protected]>"
},
{
"name": "To",
"value": "Alice Whitfield <[email protected]>"
},
{
"name": "Subject",
"value": "Your order 4417 has shipped"
},
{
"name": "Date",
"value": "Tue, 28 Jul 2026 09:15:00 +0200"
},
{
"name": "Message-ID",
"value": "<[email protected]>"
}
],
"profile": "TRANSACTIONAL"
}
}Examples
A clean transactional header set passes with nothing to report
{
"headers": [
{
"name": "From",
"value": "Northwind Orders <[email protected]>"
},
{
"name": "To",
"value": "Alice Whitfield <[email protected]>"
},
{
"name": "Subject",
"value": "Your order 4417 has shipped"
},
{
"name": "Date",
"value": "Tue, 28 Jul 2026 09:15:00 +0200"
},
{
"name": "Message-ID",
"value": "<[email protected]>"
}
],
"profile": "TRANSACTIONAL"
}{
"valid": true,
"summary": {
"profile": "TRANSACTIONAL",
"truncated": false,
"bySeverity": {},
"headerCount": 5,
"recipientCount": 1,
"issueCount": 0
},
"resolvedRules": {
"maxIssues": 1000,
"checkListUnsubscribe": false,
"requireTo": true,
"requireFrom": true,
"checkAddressSyntax": true,
"forbidBccWithRecipients": false,
"forbiddenHeaders": [],
"requireDate": true,
"requiredHeaders": [
"From",
"To",
"Date",
"Message-ID"
],
"requireMessageId": true
},
"issues": []
}A marketing set with an injected header, a spoofed display name and a reply that leaves the domain
{
"profile": "MARKETING",
"rules": {
"maxRecipients": 50
},
"headers": [
{
"name": "From",
"value": "\"Northwind Billing <[email protected]>\" <[email protected]>"
},
{
"name": "To",
"value": "[email protected]"
},
{
"name": "Reply-To",
"value": "[email protected]"
},
{
"name": "Subject",
"value": "Your July statement is ready\r\nBcc: [email protected]"
},
{
"name": "Date",
"value": "yesterday afternoon"
},
{
"name": "Message-ID",
"value": "<statement-july-2026>"
}
]
}{
"valid": false,
"summary": {
"profile": "MARKETING",
"truncated": false,
"bySeverity": {
"ERROR": 3,
"WARNING": 3
},
"headerCount": 6,
"recipientCount": 1,
"issueCount": 6
},
"resolvedRules": {
"requireDate": true,
"requireTo": true,
"requireFrom": true,
"requiredHeaders": [
"From",
"To",
"Date",
"Message-ID"
],
"requireMessageId": true,
"checkListUnsubscribe": true,
"forbiddenHeaders": [],
"forbidBccWithRecipients": false,
"maxRecipients": 50,
"checkAddressSyntax": true,
"maxIssues": 1000
},
"issues": [
{
"index": 3,
"code": "HEADER_CONTROL_CHARACTER",
"header": "Subject",
"severity": "ERROR",
"detail": "A carriage return, line feed or other control character sits at position 28 of the value. A receiving server reads that as the end of the header and the start of another one, which is how a header is injected."
},
{
"index": 0,
"code": "DISPLAY_NAME_SPOOF",
"header": "From",
"severity": "WARNING",
"detail": "The display name contains an address of its own that is not the address the message is really from. A reader sees the display name and almost never the real one."
},
{
"index": 2,
"code": "REPLY_TO_DOMAIN_MISMATCH",
"header": "Reply-To",
"severity": "WARNING",
"detail": "A reply to this message leaves the domain the message came from. That is normal for a helpdesk and it is also what a redirected reply looks like, so it is worth a look."
},
{
"index": 5,
"code": "MESSAGE_ID_INVALID",
"header": "Message-ID",
"severity": "ERROR",
"detail": "A message identifier is a local part and a domain inside angle brackets. Without the domain half it is not globally unique, so threading and duplicate detection stop working."
},
{
"index": 4,
"code": "DATE_UNPARSEABLE",
"header": "Date",
"severity": "ERROR",
"detail": "The value is not a date in the form a message uses, which looks like Tue, 28 Jul 2026 09:15:00 +0200. We never fall back to the current time, so an unreadable date stays unreadable."
},
{
"code": "LIST_UNSUBSCRIBE_MISSING",
"severity": "WARNING",
"header": "List-Unsubscribe",
"detail": "Bulk mail without a one-click way out is filtered harder by the large mailbox providers, and in several places it is also a legal requirement."
}
]
}Recommended use cases
- Stop a header-injection attempt before a message is handed to a mail server, by refusing any value that carries a carriage return or a line feed
- Gate an outbound template in your build, so a missing Message-ID or an unparseable Date is caught long before a customer sees it
- Check a bulk campaign carries a List-Unsubscribe and stays inside your recipient policy
- Spot a display name carrying somebody else's address while reviewing suspicious mail
Avoid for
- Deciding whether an address can receive mail, which needs the network
- Judging whether a message is spam or whether its sender is authenticated
- Parsing a whole raw message into headers, body and attachments
Related skills
Composable with
Safety & privacy
- Runs without calling any external service.
- Your payloads are never stored permanently.
- Telemetry captures metadata only — never your payload contents.
- We never train models on your payloads.
- No human reviews your payloads.