This script converts .emlx
and .partial.emlx
files written by Apple’s Mail.app into fully self-contained, “stand alone” .eml
files which can be imported and opened by a great variety of email applications (Mail.app, Thunderbird, …).
Apple uses these formats for internal storage (see ~/Library/Mail/Vx
), and under normal circumstances you will not come in contact with those files. Unfortunately, one of my IMAP mailboxes went out of service and I was not able to copy all the messages to a different account with Mail.app, even though all mails and attachments were there (see here for the story).
That’s why I created this script.
This is the easiest way if you’re not a Node.js developer. Install the script and all dependencies with Homebrew:
$ brew install qqilihq/partial-emlx-converter/partial-emlx-converter
I would like to make this script available in the Homebrew core repository as well, but for this the project needs more ⭐️ and 🍴 — please help!
Use a current version of Node.js (currently built and tested with v10.15.3 LTS) and run the following command to install the script globally with npm:
$ npm install --global partial-emlx-converter
partial-emlx-converter
supports to operation modes: convert
to convert .emlx
files to .eml
files, and imapImport
for importing .emlx
files directly to an IMAP server.
Run the script with at least two arguments: (1) Path to the directory which contains the .emlx
and .partial.emlx
files, (2) path to the existing directory where the results should be written to.
$ partial-emlx-converter convert
Usage: partial-emlx-converter convert [options] <input_directory> <output_directory>
convert .emlx-files from input folder to .eml files in output folder
Arguments:
input_directory input folder to read .emlx-files from
output_directory output folder for .eml-files
Options:
--ignoreErrors Don't abort the conversion on error (see the log output for details in this case)
--skipDeleted Skip messages marked as deleted
-h, --help display help for command
# sample
$ partial-emlx-converter /path/to/input /path/to/result
Optionally, you can specify --ignoreErrors
as third argument. This way, the conversion will not be aborted in case there’s an error for a file (see the log output for details in this case).
You can also use --skipDeleted
to skip messages marked as deleted in the emlx flags.
With the IMAP import, additional context information (IMAP Flags / received timestamps) from the .emlx attached plist object is used.
$ ./bin/partial-emlx-converter imapImport --help
Usage: partial-emlx-converter imapImport [options] <input_directory>
Import mails from emlx to IMAP server
Arguments:
input_directory input folder to read .emlx-files from
Options:
-p,--port <port_number> IMAP port (default: 993)
-u,--user <username> User for IMAP authentication
--pass <password> Password for IMAP authentication (env: IMAP_PASS)
-h,--host <hostname> IMAP server hostname
-m,--mailbox <mailbox> IMAP mailbox to import mails into (default: "import")
--tls <mode> Use `no` to disable TLS (choices: "yes", "no", default: tls enabled)
--skipDeleted Skip messages marked as deleted
--ignoreErrors Don't abort conversion on error (see the log output for details in this case)
--help display help for command
Use a current version of Node.js (currently built and tested with v10.15.3 LTS). Install the dependencies, run the tests, and compile the TypeScript code with yarn or npm:
$ yarn
$ yarn test
$ yarn build
Commit all changes and run the following:
$ npm login
$ npm version <update_type>
$ npm publish
… where <update_type>
is one of patch
, minor
, or major
. This will update the package.json
, and create a tagged Git commit with the version number.
After releasing a new version, remember to update the Homebrew formula here.
Disclaimer: I figured out the following by reverse engineering. I cannot give any guarantee about the correctness. If you feel, that something should be corrected, please let me know.
.emlx
and .partial.emlx
are similar to .eml
, with the following peculiarities:
These files start with a line which contains the length of the actual .eml
payload:
2945
Return-Path: <john@example.com>
X-Original-To: john@example.com
…
The number 2945
denotes, that the actual .eml
payload is 2945 characters long, starting from the second line.
At the end, these files contain an XML property list epilogue, which holds some Mail.app-specific meta data. Using the given character length at the file’s beginning, this epilogue can be stripped away easily and an .eml
file can be created.
Edit: Later, I found those additional sources, which basically confirm my findings:
- Patching .emlx files
- emlx.py
- emlx flags?
- Documentation on Apple Mail's .emlx data structure(s) (for conversion purposes)?
Mail.app uses this format to save emails which contain attachments. Attachments are saved as separate, regular files relative to the .partial.emlx
file. Afaik, Apple does this due to Spotlight indexing.
Mail.app’s internal file structure looks as follows (nested into two further hierarchies of directories named with number 0 to 9):
Attachments/
1234/
1.2/
image001.jpg
2/
file.zip
…
Messages/
1234.partial.emlx
…
1234
is obviously the email’s ID. The Attachments
directory contains the raw attachment files, whereas Messages
contains the messages stripped of their attachments (and .emlx
files, for messages which did not contain any attached files in first place).
The subdirectories 1.2
and 2
in above’s example are numbered according to their positions within the corresponding email’s Multipart hierarchy.
To convert a .partial.emlx
file into an .eml
file, the separated attachments need to be re-integrated into the file.
Without the following modules I would probably be still working on this script (or have given up on the way). Thank you for saving me so much time!
Beside that, here are some resources which I found very helpful during development:
- Test Cases for HTTP Content-Disposition header field (RFC 6266) and the Encodings defined in RFCs 2047, 2231 and 5987
- The Content-Transfer-Encoding Header Field
Pull requests are very welcome and I highly appreciate any contributions. Feel free to discuss bugs or new features by opening a new issue. In case you submit any bug fixes, please provide corresponding test cases and make sure that existing tests do not break.
Copyright (c) 2018 – 2025 Philipp Katz