FA-76396 / Email MIME structure / Open access
Resolve a part's effective content type and charset: charset normalization · case 01
Charset names keep their original casing, so "UTF-8" and "utf-8" are treated as different charsets.
ROOT CAUSE
The charset normalization decision evaluates `charset = v.strip().strip('"')` where the contract requires `charset = v.strip().strip('"').lower()`.
VERIFIED REPAIR
Use `charset = v.strip().strip('"').lower()` for the charset normalization decision and keep every other rule of the model unchanged.
Unsuccessful approach: Lower-casing without removing the quotes reports the quoted token. The attempted `charset = v.strip().lower()` still disagrees with a fixture.
Case contract
header is the raw Content-Type value or None. The type is the lower-cased text before ";" with surrounding spaces removed; it is valid only with a non-empty major and minor separated by "/" and no inner space. Missing or invalid types default to message/rfc822 inside multipart/digest and text/plain everywhere else (parameters of an invalid header are ignored). text/* types get the charset parameter (quotes removed, lower-cased) or us-ascii; other types have charset None. Result [type, charset].
Why this case matters
Default and fallback types decide whether a part is rendered as text, as a nested message or as an attachment.
1 / The failure
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(parent_type, header):
default = 'message/rfc822' if parent_type == 'multipart/digest' else 'text/plain'
mtype = default
charset = None
if header is not None:
head, _, rest = header.partition(';')
cand = head.strip().lower()
major, slash, minor = cand.partition('/')
if slash and major and minor and ' ' not in cand:
mtype = cand
for piece in rest.split(';'):
k, eq, v = piece.partition('=')
if eq and k.strip().lower() == 'charset':
charset = v.strip().strip('"')
if mtype.startswith('text/'):
charset = charset or 'us-ascii'
else:
charset = None
return [mtype, charset]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
_CASES = {1: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="UTF-8"'), ['text/html', 'utf-8']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-1'), ['text/plain', 'iso-8859-1']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 2: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="Windows-1252"'), ['text/html', 'windows-1252']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-2'), ['text/plain', 'iso-8859-2']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 3: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="KOI8-R"'), ['text/html', 'koi8-r']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-3'), ['text/plain', 'iso-8859-3']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 4: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="Shift_JIS"'), ['text/html', 'shift_jis']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-4'), ['text/plain', 'iso-8859-4']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 5: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="EUC-KR"'), ['text/html', 'euc-kr']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-5'), ['text/plain', 'iso-8859-5']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])]}
for _label, _args, _expected in _CASES[N]:
check(_label, solve(*_args), _expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| digest child without header | ['message/rfc822', None] | ['message/rfc822', None] | Passed |
| mixed child without header | ['text/plain', 'us-ascii'] | ['text/plain', 'us-ascii'] | Passed |
| truncated type in digest | ['message/rfc822', None] | ['message/rfc822', None] | Passed |
| missing major type | ['text/plain', 'us-ascii'] | ['text/plain', 'us-ascii'] | Passed |
| quoted upper-case charset | ['text/html', 'UTF-8'] | ['text/html', 'utf-8'] | Failed |
| charset on a binary type | ['image/png', None] | ['image/png', None] | Passed |
| padded header | ['text/plain', 'ISO-8859-1'] | ['text/plain', 'iso-8859-1'] | Failed |
| nested message parent | ['text/plain', 'us-ascii'] | ['text/plain', 'us-ascii'] | Passed |
SHA-256 / 4c6d795abf030655482e7945504d48d5724be6f341f41be21f16ed4078948130
2 / The unsuccessful fix
Exit 1"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(parent_type, header):
default = 'message/rfc822' if parent_type == 'multipart/digest' else 'text/plain'
mtype = default
charset = None
if header is not None:
head, _, rest = header.partition(';')
cand = head.strip().lower()
major, slash, minor = cand.partition('/')
if slash and major and minor and ' ' not in cand:
mtype = cand
for piece in rest.split(';'):
k, eq, v = piece.partition('=')
if eq and k.strip().lower() == 'charset':
charset = v.strip().lower()
if mtype.startswith('text/'):
charset = charset or 'us-ascii'
else:
charset = None
return [mtype, charset]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
_CASES = {1: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="UTF-8"'), ['text/html', 'utf-8']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-1'), ['text/plain', 'iso-8859-1']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 2: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="Windows-1252"'), ['text/html', 'windows-1252']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-2'), ['text/plain', 'iso-8859-2']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 3: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="KOI8-R"'), ['text/html', 'koi8-r']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-3'), ['text/plain', 'iso-8859-3']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 4: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="Shift_JIS"'), ['text/html', 'shift_jis']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-4'), ['text/plain', 'iso-8859-4']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 5: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="EUC-KR"'), ['text/html', 'euc-kr']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-5'), ['text/plain', 'iso-8859-5']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])]}
for _label, _args, _expected in _CASES[N]:
check(_label, solve(*_args), _expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| digest child without header | ['message/rfc822', None] | ['message/rfc822', None] | Passed |
| mixed child without header | ['text/plain', 'us-ascii'] | ['text/plain', 'us-ascii'] | Passed |
| truncated type in digest | ['message/rfc822', None] | ['message/rfc822', None] | Passed |
| missing major type | ['text/plain', 'us-ascii'] | ['text/plain', 'us-ascii'] | Passed |
| quoted upper-case charset | ['text/html', '"utf-8"'] | ['text/html', 'utf-8'] | Failed |
| charset on a binary type | ['image/png', None] | ['image/png', None] | Passed |
| padded header | ['text/plain', 'iso-8859-1'] | ['text/plain', 'iso-8859-1'] | Passed |
| nested message parent | ['text/plain', 'us-ascii'] | ['text/plain', 'us-ascii'] | Passed |
SHA-256 / 9e385cdafd9c6e673f1645b08e38ab85ed79b7ef4f827ff77f4af63fc310653b
3 / The verified repair
Exit 0"""Failure Map reference implementation. Python standard library only."""
import json
N = 1
observations = []
def solve(parent_type, header):
default = 'message/rfc822' if parent_type == 'multipart/digest' else 'text/plain'
mtype = default
charset = None
if header is not None:
head, _, rest = header.partition(';')
cand = head.strip().lower()
major, slash, minor = cand.partition('/')
if slash and major and minor and ' ' not in cand:
mtype = cand
for piece in rest.split(';'):
k, eq, v = piece.partition('=')
if eq and k.strip().lower() == 'charset':
charset = v.strip().strip('"').lower()
if mtype.startswith('text/'):
charset = charset or 'us-ascii'
else:
charset = None
return [mtype, charset]
def check(label, actual, expected):
observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
_CASES = {1: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="UTF-8"'), ['text/html', 'utf-8']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-1'), ['text/plain', 'iso-8859-1']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 2: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="Windows-1252"'), ['text/html', 'windows-1252']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-2'), ['text/plain', 'iso-8859-2']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 3: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="KOI8-R"'), ['text/html', 'koi8-r']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-3'), ['text/plain', 'iso-8859-3']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 4: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="Shift_JIS"'), ['text/html', 'shift_jis']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-4'), ['text/plain', 'iso-8859-4']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])], 5: [('digest child without header', ('multipart/digest', None), ['message/rfc822', None]), ('mixed child without header', ('multipart/mixed', None), ['text/plain', 'us-ascii']), ('truncated type in digest', ('multipart/digest', 'text/'), ['message/rfc822', None]), ('missing major type', ('multipart/mixed', '/html; charset=utf-8'), ['text/plain', 'us-ascii']), ('quoted upper-case charset', ('multipart/mixed', 'Text/HTML; charset="EUC-KR"'), ['text/html', 'euc-kr']), ('charset on a binary type', ('multipart/mixed', 'image/png; charset=binary'), ['image/png', None]), ('padded header', ('multipart/alternative', ' text/plain ; charset=ISO-8859-5'), ['text/plain', 'iso-8859-5']), ('nested message parent', ('message/rfc822', None), ['text/plain', 'us-ascii'])]}
for _label, _args, _expected in _CASES[N]:
check(_label, solve(*_args), _expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
| Boundary fixture | Actual | Expected | Outcome |
|---|---|---|---|
| digest child without header | ['message/rfc822', None] | ['message/rfc822', None] | Passed |
| mixed child without header | ['text/plain', 'us-ascii'] | ['text/plain', 'us-ascii'] | Passed |
| truncated type in digest | ['message/rfc822', None] | ['message/rfc822', None] | Passed |
| missing major type | ['text/plain', 'us-ascii'] | ['text/plain', 'us-ascii'] | Passed |
| quoted upper-case charset | ['text/html', 'utf-8'] | ['text/html', 'utf-8'] | Passed |
| charset on a binary type | ['image/png', None] | ['image/png', None] | Passed |
| padded header | ['text/plain', 'iso-8859-1'] | ['text/plain', 'iso-8859-1'] | Passed |
| nested message parent | ['text/plain', 'us-ascii'] | ['text/plain', 'us-ascii'] | Passed |
SHA-256 / 937ad6c7c2b1dfe1d377beba22461086b4ba8ea288085303e37e9d34693b027e
Verification & scope
Stipulated offline model over pre-parsed MIME dictionaries; not a conforming MIME parser, generator or mail client. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.
Observations recorded using Python 3.12.14 at 2026-09-29T14:49:15.964654+00:00.
Case digest / d9cebff2df8c079b4d2dc69ad327373c89784b56dacb1b01ca0b7e353dc05538