Build and Parse Email Messages Offline
Construct a plain-text EmailMessage and parse its bytes while keeping transport separate.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Build and Parse Email Messages Offline
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
An email message is more than a block of text with a few labels. It has headers, a blank boundary, body content, character encodings, and sometimes multiple parts or attachments. Python's `email` package can build and parse that structure without sending anything. Today we will make a message entirely in memory, serialize it to bytes, parse those bytes back, and check the fields we expect. First we will reproduce a naive parser that mistakes a body line containing a colon for a header. Then we will use `EmailMessage` and a modern policy to let the library handle the message format. This is an offline tutorial: no SMTP connection, no inbox access, and no real
- 00:43
recipient is involved. The sample addresses use `example.com`. A program that actually sends mail would need delivery configuration, consent, authentication, and handling for failures; none of that is implied by successfully constructing a message. We are learning how to represent the content correctly and verify a round trip. The code sees every line with a colon as a header, including the body line `Note: bring the report.`. In a real message, the blank line separates header fields from body content. This toy loop ignores that boundary. It also ignores folded headers, encoded text, MIME parts, and other details that
- 01:26
make handwritten email parsing brittle. We should use a parser designed for the format rather than adding more string conditions one by one. The same applies when building a message: concatenating header lines by hand can mishandle non-ASCII text or newlines in unexpected places. In this demonstration, the raw string is harmless and local; no mail is transmitted. The purpose is to show a wrong classification, not to test an email server. Our next step is to construct a message through the standard-library API and let that library serialize the boundaries and headers.
- 02:06
Then we will parse it again and inspect the resulting object rather than reading guessed fields from every colon in the text. `EmailMessage` stores headers separately from the body. The `Note:` text remains content, not a newly discovered header. `set_content` establishes a plain-text body and the content type reflects that choice. We can inspect fields through the object's API without manually searching for the first blank line. This small message has one body part. Email can also contain HTML alternatives and attachments; those introduce MIME structure that is another reason to use the library.
- 02:46
Be careful when displaying or forwarding HTML mail: parsing it does not make its contents safe to render. For this tutorial, plain text keeps the example focused. A header such as `Subject` is also not a place to concatenate untrusted values with newlines. Assign fields through the API and validate who is allowed to choose addresses and content in your application. Constructing a message is still not delivery. The object exists only in this Python process until we explicitly serialize or pass it to another service. We serialize the message to bytes using the SMTP policy, which uses the line
- 03:27
endings expected for transport-style email data. The subject contains an accented character, so the library handles the necessary header representation when producing bytes. `BytesParser` reconstructs a message object from those bytes. The parsed subject comes back as readable text, the line beginning `Note:` remains in the body, and there is no `Note` header. That directly repairs the wrong classification from the first run. We are not sending `wire` anywhere; it is simply a byte string in memory. A real application might store it, display it, or eventually pass it to a mail transport. The transport operation is separate and can fail for many reasons unrelated to
- 04:09
parsing. Round-trip tests are helpful because they inspect the structure that a downstream parser will actually see, not just the in- memory object before serialization. They are especially valuable when non- ASCII text or multipart content is involved. The assertions verify the fields a reader cares about after serialization and parsing. The sender, recipient, and subject survive the round trip. The `Note:` line stays in plain-text content and does not appear as a header. We also check the content type. The final byte count demonstrates that the message was serialized; it is not evidence
- 04:49
of delivery. No network call occurs in this code. Try adding a second paragraph to the body in the lesson console and confirm it remains body text. Then change the subject to include another non-ASCII character and check that the parsed subject is still readable. The general pattern is to let `EmailMessage` create structure, serialize it with an appropriate policy, and use a parser to verify the result. If you later build a mail-sending feature, treat transport, authentication, and user permission as separate concerns. For now, completing the practice task means building a correct offline message
- 05:29
and rejecting the temptation to parse headers by splitting every line on a colon.
Understand the concept
An email message is a structured document with headers and a body. Python's email.message.EmailMessage builds that document; it does not send anything. Sending requires a separate transport such as SMTP, which this browser exercise deliberately does not use. That separation makes the examples safe and repeatable: we can inspect the generated bytes without contacting a mail server or collecting any real address.
Set From, To, and Subject as headers, then call set_content for the plain-text body. The library handles message formatting and text encoding. Calling as_bytes serializes the result; BytesParser(policy=policy.default).parsebytes reads it back. A round trip checks the actual wire-like representation instead of merely looking at attributes on the object before serialization.
Headers should contain single-line values. A newline in a supplied subject or address is an invalid input for this small teaching function and should be rejected before assignment. Require nonblank strings for sender, recipient, subject, and body; use example.invalid addresses in demos to make clear that nobody receives a message. This validator is not a complete email address parser or anti-spam system.
The checked build_notice function returns an EmailMessage, which tests serialize and parse again. It rejects blank fields and newlines in header fields, and the test checks the text body through get_content. The distinction between a parsed message and a delivered message matters: successful parsing proves the message structure round-trips, not that any mailbox exists or that sending would succeed.
- EmailMessage builds a document without sending
- Headers and body are different parts
- BytesParser reads serialized messages
- Header values must be single-line
See it step by step
Read the code, predict the output, then compare it with the result.
01. Build a plain-text message locally
from email.message import EmailMessage
message=EmailMessage()
message['From']='a@example.invalid'
message['To']='b@example.invalid'
message['Subject']='Practice'
message.set_content('Hello')
print(message['Subject'])
print(message.get_content().strip())Practice
HelloEmailMessage assembles headers and text in memory; no transport runs.
02. Parse serialized bytes
from email.message import EmailMessage
from email.parser import BytesParser
from email import policy
message=EmailMessage();message['Subject']='Practice';message.set_content('Hello')
parsed=BytesParser(policy=policy.default).parsebytes(message.as_bytes())
print(parsed['Subject'], parsed.get_content().strip())Practice HelloA bytes round trip verifies the message representation remains readable.
03. Reject a multiline subject before building
subject='Hello\nBcc: hidden@example.invalid'
print('\n' in subject or '\r' in subject)
print(subject.splitlines()[0])True
HelloThe supplied subject is not one header line; reject it rather than silently truncating.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Construct a message without sending it
The first run creates EmailMessage, assigns headers, and sets a plain-text body. It prints the subject and body from the in-memory object. No mail transport is used and the example.invalid addresses do not designate real recipients. This is a safe way to learn the message format without contacting a server.
Treat construction and delivery as separate operations. A syntactically complete message can still be undeliverable, and this lesson makes no delivery claim. Validate sender, recipient, subject, and body as nonblank strings before building the object. When assigning text content, let the email library handle the document's encoding and formatting.
from email.message import EmailMessage
message=EmailMessage()
message['From']='a@example.invalid'
message['To']='b@example.invalid'
message['Subject']='Practice'
message.set_content('Hello')
print(message['Subject'])
print(message.get_content().strip())Practice
Hello- Create an EmailMessage in memory.
- Set headers and a text body.
- Inspect content without invoking a transport.
Round-trip the serialized bytes
The next example serializes a message with as_bytes, then parses those bytes with BytesParser and the default policy. The parsed subject and body still read Practice Hello. A round trip checks the actual serialized representation, which is more informative than only inspecting the object before serialization.
Use this test to validate the small build_notice function: build a message, serialize it, parse it, and compare the fields the application relies on. Do not treat the successful round trip as evidence of delivery, mailbox existence, or safety from spam filtering. Those are separate systems outside this browser exercise.
from email.message import EmailMessage
from email.parser import BytesParser
from email import policy
message=EmailMessage();message['Subject']='Practice';message.set_content('Hello')
parsed=BytesParser(policy=policy.default).parsebytes(message.as_bytes())
print(parsed['Subject'], parsed.get_content().strip())Practice Hello- Serialize the completed message to bytes.
- Parse it with an explicit policy.
- Compare the header and plain-text body after parsing.
Reject multiline header input
The final text includes a newline followed by another header-looking line. The example prints True for the newline check and shows the first line only to demonstrate the suspicious shape. A teaching function should reject such input rather than silently truncating it or passing it into a header field.
Require each supplied header value to be a nonblank single line before creating the message. The library also performs header validation, but the explicit application rule produces a predictable error close to the user input. This is a narrow validator, not a complete email-address parser or anti-spam policy. Keep tests focused on blank and multiline values plus a valid round trip.
subject='Hello\nBcc: hidden@example.invalid'
print('\n' in subject or '\r' in subject)
print(subject.splitlines()[0])True
Hello- Inspect a proposed subject for carriage returns or newlines.
- Reject invalid header shape before assignment.
- Round-trip only after inputs pass validation.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Validate each field before constructing EmailMessage. For headers, reject '\r' and '\n'; set From, To, Subject, then call set_content(body).
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Claiming a constructed message was sent
message = EmailMessage(); print('delivered')message = EmailMessage(); print('message built locally')What happens: No transport or mailbox interaction occurred.
Keep message construction separate from delivery.
Putting unvalidated multiline input in a header
message['Subject'] = user_subjectif '\n' in user_subject or '\r' in user_subject:
raise ValueError('single-line subject required')What happens: A supplied newline violates this function's header contract.
Validate header shape before assignment.