Python Library to parse forwarded email messages
Project description
email-forward-parser-py
email-forward-parser extracts the original message details from forwarded email
content. It can work with plain forwarded bodies or full .eml messages, including
common MIME encodings and attached .eml files.
The package is useful when you receive emails forwarded by users or mailboxes and need to recover the original sender, recipients, subject, sent date, and body.
Features
- Detects forwarded messages from subject prefixes such as
Fwd:andFW:. - Parses forwarded bodies from Gmail, Apple Mail, Outlook/Office 365, Thunderbird, Yahoo Mail, HubSpot, Missive, IONOS, and several localized clients.
- Extracts original metadata:
From,To,Cc,Subject,Date, and body. - Parses full
.emlmessages with MIME decoding. - Handles quoted-printable/base64 text bodies and attached
message/rfc822or.emlattachments. - Rebuilds an
.emlstring for the original message when a forwarded email is detected. - Ships with a 100% statement coverage gate.
Installation
python -m pip install email-forward-parser
Python 3.10 or newer is required.
The distribution name is email-forward-parser, but the import package is
emailforwardparser:
from emailforwardparser.forward_parser import get_forwarded_metadata
from emailforwardparser.client import EmailParserClient
Quick Start
Use get_forwarded_metadata() when you already have the email body text.
from emailforwardparser.forward_parser import get_forwarded_metadata
body = """Hi team
---------- Forwarded message ---------
From: Jane Doe <jane@example.com>
Date: Mon, 1 Jan 2024 at 12:00 PM
Subject: Original subject
To: Bob <bob@example.com>
Cc: Copy <copy@example.com>
Original body.
"""
result = get_forwarded_metadata(body, "Fwd: Original subject")
print(result.forwarded) # True
print(result.message) # Hi team
print(result.email.subject) # Original subject
print(result.email.from_.name) # Jane Doe
print(result.email.from_.address) # jane@example.com
print(result.email.to[0].address) # bob@example.com
print(result.email.cc[0].address) # copy@example.com
print(result.email.body) # Original body.
Use EmailParserClient when you have a full .eml message.
from emailforwardparser.client import EmailParserClient
client = EmailParserClient()
with open("forwarded.eml", encoding="utf8") as file:
raw_eml = file.read()
metadata = client.get_original_metadata(raw_eml)
if metadata.forwarded:
print(metadata.email.from_.address)
print(metadata.email.subject)
print(metadata.email.body)
API
get_forwarded_metadata(body, subject=None)
Parses a forwarded body string and returns a ForwardMetadata dataclass.
from emailforwardparser.forward_parser import get_forwarded_metadata
result = get_forwarded_metadata(body, subject)
subject is optional. Passing it helps the parser detect forwards that have a
forwarded subject prefix but no separator line in the body.
ForwardMetadata
@dataclass
class ForwardMetadata:
forwarded: bool
message: str
email: OriginalMetadata
forwarded:Truewhen forwarded content was detected.message: the note written by the person who forwarded the email, before the forwarded block.email: metadata extracted from the original email.
OriginalMetadata
@dataclass
class OriginalMetadata:
date: str
subject: str
body: str
from_: MailboxResult
to: list[MailboxResult]
cc: list[MailboxResult]
The sender field is named from_ because from is a Python keyword.
MailboxResult
@dataclass
class MailboxResult:
name: str
address: str
When a mailbox line has no valid email address, address is empty and the raw
value is kept in name.
EmailParserClient
EmailParserClient is the high-level wrapper for full .eml messages.
from emailforwardparser.client import EmailParserClient
client = EmailParserClient()
Methods:
get_original_metadata(email: str) -> ForwardMetadataParses a raw.emlstring and returns metadata for the original email. If the message is not forwarded, it returns metadata for the message itself.get_original_metadata_from_file(file_path: str) -> ForwardMetadataReads a.emlfile and returns the same metadata object.get_original_eml(email: str) -> dictReturns a dictionary with:forward: whether the input was detected as forwarded.Send-To: the first address from the wrapper message'sFromheader.eml: a raw.emlstring for the original message when forwarded, or the input/attached message when not forwarded.
get_original_eml_from_file(file_path: str) -> dictReads a.emlfile and returns the same dictionary.
Examples
Parse an Apple Mail Forward Without a Subject
from emailforwardparser.forward_parser import get_forwarded_metadata
body = """Personal note
Begin forwarded message:
From: Jane Doe <jane@example.com>
Date: Mon, 1 Jan 2024 at 12:00 PM
Subject: Original subject
To: Bob <bob@example.com>
Original body.
"""
result = get_forwarded_metadata(body)
assert result.forwarded is True
assert result.message == "Personal note"
assert result.email.subject == "Original subject"
assert result.email.from_.address == "jane@example.com"
Parse a Quoted-Printable .eml
from emailforwardparser.client import EmailParserClient
raw_eml = """From: Forwarder <forwarder@example.com>
To: parser@example.com
Subject: Fwd: Original subject
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Hi=0A=0A---------- Forwarded message ---------=0AFrom: Jane Doe <jane@example.com>=
=0ADate: Mon, 1 Jan 2024 at 12:00 PM=0ASubject: Original subject=0ATo: Bob =
<bob@example.com>=0A=0AOriginal caf=C3=A9 body.=0A
"""
metadata = EmailParserClient().get_original_metadata(raw_eml)
assert metadata.forwarded is True
assert metadata.email.body == "Original café body."
Rebuild the Original .eml
from email.message import EmailMessage
from emailforwardparser.client import EmailParserClient
message = EmailMessage()
message["From"] = "Forwarder <forwarder@example.com>"
message["To"] = "parser@example.com"
message["Subject"] = "Fwd: Original subject"
message.set_content("""Hi
---------- Forwarded message ---------
From: Jane Doe <jane@example.com>
Date: Mon, 1 Jan 2024 at 12:00 PM
Subject: Original subject
To: Bob <bob@example.com>
Original body.
""")
data = EmailParserClient().get_original_eml(message.as_string())
assert data["forward"] is True
print(data["eml"])
Read From a File
from emailforwardparser.client import EmailParserClient
client = EmailParserClient()
metadata = client.get_original_metadata_from_file("forwarded.eml")
data = client.get_original_eml_from_file("forwarded.eml")
print(metadata.email.subject)
print(data["eml"])
Non-Forwarded Messages
If a message is not forwarded, forwarded is False. With the client wrapper,
the returned email metadata describes the message itself.
from emailforwardparser.client import EmailParserClient
metadata = EmailParserClient().get_original_metadata(raw_eml)
if not metadata.forwarded:
print("This was not a forwarded message")
print(metadata.email.subject)
Behavior Notes
- Parsing is regex-based. It is designed for common forwarded-message formats, not every possible custom email template.
- Missing fields are returned as empty strings or empty lists.
- Multiple
ToandCcrecipients are returned in order. - The parser normalizes common email encodings and non-breaking spaces.
- The client prefers the first non-attachment
text/plainbody part when reading multipart.emlmessages. - Attached
.emlmessages are returned directly when present.
Development
Install the development and test tools:
python -m pip install -e ".[dev,test]"
Run the full local quality gate:
python -m isort --check-only emailforwardparser tests
python -m black --check emailforwardparser tests
python -m ruff check emailforwardparser tests
python -m mypy
python -m pytest
pytest is configured to require 100% statement coverage.
Release
Build and verify the distribution:
python -m pip install --upgrade build twine
python -m pytest
rm -rf dist build
python -m build
python -m twine check dist/*
Upload to PyPI:
python -m twine upload dist/*
License and Credits
This project is distributed under the Apache License, Version 2.0. See
LICENSE for the full license text.
The project was originally created by Garrett Marking. This fork/package includes additional maintenance, packaging, typing, test coverage, and parser/client fixes by Felipe Hertzer.
Apache 2.0 allows redistribution and modification, including publishing to PyPI, as long as the license conditions are followed. In practice for this package:
- Keep the Apache 2.0 license text with redistributions.
- Keep existing attribution notices from the source distribution.
- Keep the
NOTICEfile with source and binary distributions. - Make it clear when files have been modified.
For the canonical license terms, refer to the Apache Software Foundation's Apache License 2.0 page.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file email_forward_parser-0.1.3.tar.gz.
File metadata
- Download URL: email_forward_parser-0.1.3.tar.gz
- Upload date:
- Size: 24.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f1358292887db29714182b837d8f4a571cca9994c71f5ec23e89bc0deec4edd
|
|
| MD5 |
74a68645451e4536cd8873c6f5714235
|
|
| BLAKE2b-256 |
93345bd712f0419937a1f2cff826fa742dd095ac3049a6b6ab86739addeb1703
|
Provenance
The following attestation bundles were made for email_forward_parser-0.1.3.tar.gz:
Publisher:
workflow.yml on felipehertzer/email-forward-parser-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
email_forward_parser-0.1.3.tar.gz -
Subject digest:
4f1358292887db29714182b837d8f4a571cca9994c71f5ec23e89bc0deec4edd - Sigstore transparency entry: 1762916770
- Sigstore integration time:
-
Permalink:
felipehertzer/email-forward-parser-py@e06155da8c2d424b2c63616b8bd610770162e93f -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/felipehertzer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@e06155da8c2d424b2c63616b8bd610770162e93f -
Trigger Event:
push
-
Statement type:
File details
Details for the file email_forward_parser-0.1.3-py3-none-any.whl.
File metadata
- Download URL: email_forward_parser-0.1.3-py3-none-any.whl
- Upload date:
- Size: 25.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c23a5e60ab9c4eb63bce99198396145dbfd090237f0bbb704b113b1a6be9d385
|
|
| MD5 |
8275f6e841cafd72114e4dcac6850403
|
|
| BLAKE2b-256 |
510dc71adb1fe8ab37e6e408658633b3352fb99f096b8ceedc55f8e44e84be35
|
Provenance
The following attestation bundles were made for email_forward_parser-0.1.3-py3-none-any.whl:
Publisher:
workflow.yml on felipehertzer/email-forward-parser-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
email_forward_parser-0.1.3-py3-none-any.whl -
Subject digest:
c23a5e60ab9c4eb63bce99198396145dbfd090237f0bbb704b113b1a6be9d385 - Sigstore transparency entry: 1762916919
- Sigstore integration time:
-
Permalink:
felipehertzer/email-forward-parser-py@e06155da8c2d424b2c63616b8bd610770162e93f -
Branch / Tag:
refs/tags/v0.1.3 - Owner: https://github.com/felipehertzer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@e06155da8c2d424b2c63616b8bd610770162e93f -
Trigger Event:
push
-
Statement type: