This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 4e781e23d0 TIKA-4890: detect and parse Apple Mail emlx files (#3207)
4e781e23d0 is described below
commit 4e781e23d0991004d4cbb9e50e1450f284fd4e40
Author: Tim Allison <[email protected]>
AuthorDate: Sat Sep 19 07:29:43 2026 -0400
TIKA-4890: detect and parse Apple Mail emlx files (#3207)
---
CHANGES.txt | 5 +
docs/modules/ROOT/partials/supported-formats.adoc | 1 +
.../org/apache/tika/mime/tika-mimetypes.xml | 19 ++--
.../org/apache/tika/mime/EMLXDetectionTest.java | 88 ++++++++++++++++++
.../java/org/apache/tika/mime/TestMimeTypes.java | 2 +
.../org/apache/tika/parser/mail/RFC822Parser.java | 49 +++++++++-
.../java/org/apache/tika/parser/mail/EMLXTest.java | 103 +++++++++++++++++++++
.../test-documents/testEMLX_multipart_html.emlx | 86 +++++++++++++++++
.../test-documents/testEMLX_single_part_html.emlx | 24 +++++
9 files changed, 364 insertions(+), 13 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 014218bc0c..077021aee5 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,10 @@
Release 4.1.0 - unreleased
+ * Apple Mail emlx files are detected as message/x-emlx (real files pad the
+ byte-count line to 10 columns and were falling through to text/html or
+ message/rfc822), and RFC822Parser now parses them, dropping the byte-count
+ line and the trailing plist (TIKA-4890).
+
* The tika-server UAT gains an inference pass (TIKA-4913).
* Calls to a hosted inference engine retry a 429, 502, 503 or 504 answer
diff --git a/docs/modules/ROOT/partials/supported-formats.adoc
b/docs/modules/ROOT/partials/supported-formats.adoc
index c261b37641..e6b296902e 100644
--- a/docs/modules/ROOT/partials/supported-formats.adoc
+++ b/docs/modules/ROOT/partials/supported-formats.adoc
@@ -161,6 +161,7 @@
** `application/vnd.apple.pages.18`
* `org.apache.tika.parser.mail.`
link:{tika-javadoc-url}/org/apache/tika/parser/mail/RFC822Parser.html[RFC822Parser]
** `message/rfc822`
+** `message/x-emlx`
* `org.apache.tika.parser.markdown.`
link:{tika-javadoc-url}/org/apache/tika/parser/markdown/MarkdownParser.html[MarkdownParser]
** `text/markdown`
* `org.apache.tika.parser.mat.`
link:{tika-javadoc-url}/org/apache/tika/parser/mat/MatParser.html[MatParser]
diff --git
a/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
b/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
index bd98b2ae40..e88e55adf0 100644
--- a/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
+++ b/tika-core/src/main/resources/org/apache/tika/mime/tika-mimetypes.xml
@@ -3970,17 +3970,16 @@
</mime-type>
<mime-type type="message/x-emlx">
+ <_comment>Apple Mail message</_comment>
+ <!-- line 1 is the message's byte count, which Apple Mail pads with spaces
to 10
+ columns; line 2 is the first RFC822 header. An XML plist follows the
message,
+ but it is beyond the magic window -->
<magic priority="70">
- <match value="\nRelay-Version:" type="string" offset="2:9"/>
- <match value="\n#!\ rnews" type="string" offset="2:9"/>
- <match value="\nN#!\ rnews" type="string" offset="2:9"/>
- <match value="\nForward\ to" type="string" offset="2:9"/>
- <match value="\nPipe\ to" type="string" offset="2:9"/>
- <match value="\nReturn-Path:" type="string" offset="2:9"/>
- <match value="\nFrom:" type="string" offset="2:9"/>
- <match value="\nReceived:" type="string" offset="2:9"/>
- <match value="\nMessage-ID:" type="string" offset="2:9"/>
- <match value="\nDate:" type="string" offset="2:9"/>
+ <!-- gate: the count line ends within the first 11 bytes; keeps the
regex (an 8K
+ decode per evaluation) off files that cannot be emlx -->
+ <match value="\n" type="string" offset="1:10">
+ <match value="(?i)[0-9]{1,10}
{0,9}\n((Authentication-Results|Content-Type|Date|Delivered-To|From|Message-ID|MIME-Version|Received|Received-SPF|Return-Path|Subject|To):|(X|DKIM|ARC)-)"
type="regex" offset="0"/>
+ </match>
</magic>
<glob pattern="*.emlx"/>
<sub-class-of type="text/x-tika-text-based-message"/>
diff --git
a/tika-core/src/test/java/org/apache/tika/mime/EMLXDetectionTest.java
b/tika-core/src/test/java/org/apache/tika/mime/EMLXDetectionTest.java
new file mode 100644
index 0000000000..caa909558e
--- /dev/null
+++ b/tika-core/src/test/java/org/apache/tika/mime/EMLXDetectionTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.mime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+
+public class EMLXDetectionTest {
+
+ private static final MimeTypes MIME_TYPES =
MimeTypes.getDefaultMimeTypes();
+
+ private static final String HEADERS = "From: [email protected]\nTo:
[email protected]\n" +
+ "Subject: hi\nDate: Sat, 26 Sep 2015 09:30:20 -0700\n\nbody\n";
+
+ @Test
+ public void testByteCountLine() throws Exception {
+ // Apple Mail pads the count to 10 columns; an unpadded count is
accepted too
+ assertMime("message/x-emlx", "1795 \n" + HEADERS);
+ assertMime("message/x-emlx", "1795\n" + HEADERS);
+ assertMime("message/x-emlx", "1\n" + HEADERS);
+ assertMime("message/x-emlx", "1234567890\n" + HEADERS);
+
+ // not a byte count
+ assertMime("message/rfc822", HEADERS);
+ assertMime("text/plain", "Hello\n" + HEADERS);
+ assertMime("text/plain", "17 95\n" + HEADERS);
+ assertMime("text/plain", "1795 \n\n" + HEADERS);
+ assertMime("text/plain", "12345678901\n" + HEADERS);
+ }
+
+ @Test
+ public void testFirstHeader() throws Exception {
+ for (String header : new String[]{"Delivered-To: [email protected]",
"Return-Path: <[email protected]>",
+ "Received: from mx.example.com", "X-Original-To:
[email protected]",
+ "DKIM-Signature: v=1; a=rsa-sha256", "ARC-Seal: i=1;
a=rsa-sha256",
+ "Authentication-Results: mx.example.com; spf=pass",
+ "Received-SPF: pass (mx.example.com: ...)", "Subject: hi",
+ "Mime-Version: 1.0 (Mac OS X Mail 13.4 \\(3608.120.23.2.7\\))",
+ "Content-Type: text/html;", "Message-Id: <[email protected]>"})
{
+ assertMime("message/x-emlx", "701 \n" + header + "\n" +
HEADERS);
+ }
+ assertMime("text/plain", "701 \nNot a header\n" + HEADERS);
+ assertMime("text/plain", "701 \n\n" + HEADERS);
+ }
+
+ @Test
+ public void testHtmlPartDoesNotWin() throws Exception {
+ // a long header block pushes Message-ID past the rfc822 window while
+ // an html part sits inside the html window
+ StringBuilder sb = new StringBuilder("4569 \nDelivered-To:
[email protected]\n");
+ for (int i = 0; i < 12; i++) {
+ sb.append("Received: from relay").append(i)
+ .append(".example.com by mx.example.com with ESMTP id
abc").append(i)
+ .append("; Sat, 26 Sep 2015 13:02:49 -0700 (PDT)\n");
+ }
+ sb.append(HEADERS.replace("\nbody\n",
"\n<html><body>body</body></html>\n"));
+ assertMime("message/x-emlx", sb.toString());
+ }
+
+ private void assertMime(String expected, String txt) throws IOException {
+ try (TikaInputStream tis =
TikaInputStream.get(txt.getBytes(StandardCharsets.UTF_8))) {
+ MediaType mediaType = MIME_TYPES.detect(tis, new Metadata(), new
ParseContext());
+ assertEquals(expected, mediaType.toString(), txt);
+ }
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/mime/TestMimeTypes.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/mime/TestMimeTypes.java
index 74c2035772..8ec7ae208d 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/mime/TestMimeTypes.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-integration-tests/src/test/java/org/apache/tika/mime/TestMimeTypes.java
@@ -1041,6 +1041,8 @@ public class TestMimeTypes {
public void testEmail() throws IOException {
// EMLX
assertTypeDetection("testEMLX.emlx", "message/x-emlx");
+ assertTypeDetection("testEMLX_multipart_html.emlx", "message/x-emlx");
+ assertTypeDetection("testEMLX_single_part_html.emlx",
"message/x-emlx");
// Groupwise
assertTypeDetection("testGroupWiseEml.eml", "message/rfc822");
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/main/java/org/apache/tika/parser/mail/RFC822Parser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/main/java/org/apache/tika/parser/mail/RFC822Parser.java
index 0fe3ce1e4b..cee6a203b1 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/main/java/org/apache/tika/parser/mail/RFC822Parser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/main/java/org/apache/tika/parser/mail/RFC822Parser.java
@@ -17,7 +17,10 @@
package org.apache.tika.parser.mail;
import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashSet;
import java.util.Set;
import org.apache.james.mime4j.MimeException;
@@ -34,6 +37,7 @@ import org.apache.tika.detect.Detector;
import org.apache.tika.exception.TikaException;
import org.apache.tika.exception.ZeroByteFileException;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
+import org.apache.tika.io.BoundedInputStream;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.mime.MediaType;
@@ -45,6 +49,9 @@ import org.apache.tika.sax.XHTMLContentHandler;
* Uses apache-mime4j to parse emails. Each part is treated with the
* corresponding parser and displayed within elements.
* <p/>
+ * Also handles Apple Mail's emlx framing: a first line carrying the message's
+ * byte count, and an XML plist after the message. Both are dropped.
+ * <p/>
* A {@link MimeConfig} object can be passed in the parsing context
* to better control the parsing process.
*
@@ -72,8 +79,12 @@ public class RFC822Parser implements Parser {
}
}
- private static final Set<MediaType> SUPPORTED_TYPES =
- Collections.singleton(MediaType.parse("message/rfc822"));
+ private static final Set<MediaType> SUPPORTED_TYPES =
Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(MediaType.parse("message/rfc822"),
+ MediaType.parse("message/x-emlx"))));
+
+ // digits, then Apple Mail's space padding to 10 columns, then the newline
+ private static final int EMLX_COUNT_LINE_MAX = 11;
//rely on the detector to be thread-safe
//built lazily and then reused
@@ -134,7 +145,12 @@ public class RFC822Parser implements Parser {
xhtml.startDocument();
checkForZeroByte(tis);//avoid stackoverflow
try {
- parser.parse(tis);
+ InputStream message = tis;
+ long emlxLength = readEmlxByteCount(tis);
+ if (emlxLength >= 0) {
+ message = new BoundedInputStream(emlxLength, tis);
+ }
+ parser.parse(message);
} catch (IOException e) {
tis.throwIfCauseOf(e);
throw new TikaException("Failed to parse an email message", e);
@@ -152,6 +168,33 @@ public class RFC822Parser implements Parser {
xhtml.endDocument();
}
+ /**
+ * Consumes the emlx byte-count line if the stream starts with one.
+ *
+ * @return the message's length in bytes, or -1 if the stream is not emlx
framed
+ */
+ private static long readEmlxByteCount(TikaInputStream tis) throws
IOException {
+ tis.mark(EMLX_COUNT_LINE_MAX);
+ long count = 0;
+ int digits = 0;
+ boolean padding = false;
+ for (int i = 0; i < EMLX_COUNT_LINE_MAX; i++) {
+ int c = tis.read();
+ if (c >= '0' && c <= '9' && !padding) {
+ count = count * 10 + (c - '0');
+ digits++;
+ } else if (c == ' ' && digits > 0) {
+ padding = true;
+ } else if (c == '\n' && digits > 0) {
+ return count;
+ } else {
+ break;
+ }
+ }
+ tis.reset();
+ return -1;
+ }
+
private void checkForZeroByte(TikaInputStream tstream) throws IOException,
ZeroByteFileException {
tstream.mark(1);
try {
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/java/org/apache/tika/parser/mail/EMLXTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/java/org/apache/tika/parser/mail/EMLXTest.java
new file mode 100644
index 0000000000..33a8fd4a25
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/java/org/apache/tika/parser/mail/EMLXTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.mail;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Message;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
+import org.apache.tika.parser.AutoDetectParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+
+/**
+ * Apple Mail emlx: a byte-count line, the RFC822 message, then an XML plist.
+ */
+public class EMLXTest extends TikaTest {
+
+ private static final String PLIST = "<?xml version=\"1.0\"
encoding=\"UTF-8\"?>\n" +
+ "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " +
+ "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" +
+ "<plist version=\"1.0\">\n<dict>\n\t<key>date-received</key>\n" +
+ "\t<integer>1698393810</integer>\n</dict>\n</plist>\n";
+
+ @Test
+ public void testLongHeaderBlockWithHtmlAlternative() throws Exception {
+ XMLResult r = getXML("testEMLX_multipart_html.emlx");
+ assertEquals("message/x-emlx",
r.metadata.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals("Set the Foundation Straight",
r.metadata.get(TikaCoreProperties.TITLE));
+ assertEquals("[email protected]",
r.metadata.get(Message.MESSAGE_FROM_EMAIL));
+ assertEquals("2015-09-26T16:30:20Z",
r.metadata.get(TikaCoreProperties.CREATED));
+ assertContains("The plan for the Foundation is in this message’s HTML
part.", r.xml);
+ assertPlistAndCountDropped(r.xml, "2803");
+ }
+
+ @Test
+ public void testSinglePartHtml() throws Exception {
+ XMLResult r = getXML("testEMLX_single_part_html.emlx");
+ assertEquals("message/x-emlx",
r.metadata.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals("Trial balance",
r.metadata.get(TikaCoreProperties.TITLE));
+ assertContains("The trial balance is ready for review.", r.xml);
+ assertPlistAndCountDropped(r.xml, "446");
+ }
+
+ @Test
+ public void testUnpaddedByteCount() throws Exception {
+ String message = "Subject: Unpadded\nFrom: [email protected]\nTo:
[email protected]\n\n" +
+ "the body\n";
+ String emlx = message.length() + "\n" + message + PLIST;
+ try (TikaInputStream tis =
TikaInputStream.get(emlx.getBytes(StandardCharsets.US_ASCII))) {
+ XMLResult r = getXML(tis, new AutoDetectParser(), new Metadata());
+ assertEquals("message/x-emlx",
r.metadata.get(HttpHeaders.CONTENT_TYPE));
+ assertEquals("Unpadded", r.metadata.get(TikaCoreProperties.TITLE));
+ assertContains("the body", r.xml);
+ assertPlistAndCountDropped(r.xml,
Integer.toString(message.length()));
+ }
+ }
+
+ @Test
+ public void testForcedTypeStillStripsFraming() throws Exception {
+ // an emlx handed to the parser as message/rfc822 must not leak the
plist either
+ String message = "Subject: Forced\nFrom: [email protected]\n\nthe
body\n";
+ String emlx = (message.length() + " \n") + message + PLIST;
+ Metadata metadata = new Metadata();
+ metadata.set(HttpHeaders.CONTENT_TYPE, "message/rfc822");
+ ParseContext context = new ParseContext();
+ context.set(Parser.class, new AutoDetectParser());
+ try (TikaInputStream tis =
TikaInputStream.get(emlx.getBytes(StandardCharsets.US_ASCII))) {
+ XMLResult r = getXML(tis, new RFC822Parser(), metadata, context);
+ assertEquals("Forced", r.metadata.get(TikaCoreProperties.TITLE));
+ assertContains("the body", r.xml);
+ assertPlistAndCountDropped(r.xml,
Integer.toString(message.length()));
+ }
+ }
+
+ private static void assertPlistAndCountDropped(String xml, String
byteCount) {
+ String body = xml.substring(xml.indexOf("<body"));
+ assertNotContained(byteCount, body);
+ assertNotContained("date-received", body);
+ assertNotContained("plist", body);
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/resources/test-documents/testEMLX_multipart_html.emlx
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/resources/test-documents/testEMLX_multipart_html.emlx
new file mode 100644
index 0000000000..bc470cbaae
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/resources/test-documents/testEMLX_multipart_html.emlx
@@ -0,0 +1,86 @@
+2803
+Delivered-To: [email protected]
+Received: by 10.0.0.1 with SMTP id a1csp000001abc; Sat, 26 Sep 2015
+ 13:02:49 -0700 (PDT)
+X-Received: by 10.0.0.2 with SMTP id b2mr000002pbc.10.1443297769000;
+ Sat, 26 Sep 2015 13:02:49 -0700 (PDT)
+Return-Path: <[email protected]>
+Received: from mail.example.com (mail.example.com. [10.0.0.3]) by
+ mx.example.net with ESMTP id c3si000003abc.10.2015.09.26.13.02.48 for
+ <[email protected]>; Sat, 26 Sep 2015 13:02:49 -0700 (PDT)
+Received-SPF: pass (example.net: domain of [email protected]
+ designates 10.0.0.3 as permitted sender) client-ip=10.0.0.3;
+Authentication-Results: mx.example.net; spf=pass (example.net: domain of
+ [email protected] designates 10.0.0.3 as permitted sender)
+ [email protected]; dkim=pass [email protected]
+DKIM-Signature: v=1; a=rsa-sha256; c=simple; d=example.com; s=selector;
+ t=1443297733; x=1443902533; q=dns/txt; h=From:To:
+ Subject:Date:MIME-Version:Content-Type:Reply-To:Message-ID; bh=
+ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=; b=BBBBBBBBBBBBBBBBBB
+ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+ BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
+ BBBBBBBBBBBBBBBBBBBBBBBBBBBB=
+Received: from relay01 by mail.example.com (ExampleMTA v1.0) with
+ ESMTPA id d4msg000004.msg for <[email protected]>; Sat, 26 Sep 2015
+ 13:02:11 -0700
+X-Spam-Processed: mail.example.com, Sat, 26 Sep 2015 13:02:11 -0700 (not
+ processed: spam filter heuristic analysis disabled)
+X-MTA-Helo: relay01
+X-MTA-Arrival-Date: Sat, 26 Sep 2015 13:02:11 -0700
+X-Authenticated-Sender: [email protected]
+X-Return-Path: [email protected]
+X-Envelope-From: [email protected]
+X-MTA-Deliver-To: [email protected]
+From: Hari Seldon <[email protected]>
+To: "Venabili, Dors" <[email protected]>
+Subject: Set the Foundation Straight
+Date: Sat, 26 Sep 2015 09:30:20 -0700
+MIME-Version: 1.0
+X-Mailer: ExampleMailer 0.5.8b
+Content-Type: multipart/alternative;
+ boundary="_=ExampleMailer=_0123456789abcdef"
+Reply-To: Hari Seldon <[email protected]>
+Message-ID: <[email protected]>
+
+
+--_=ExampleMailer=_0123456789abcdef
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Content-Type: TEXT/PLAIN;
+ charset=utf-8
+
+Dear Dors Venabili,
+
+The plan for the Foundation is attached in this message=E2=80=99s HTML part.
+
+Best regards,
+Hari Seldon
+
+--_=ExampleMailer=_0123456789abcdef
+Content-Transfer-Encoding: QUOTED-PRINTABLE
+Content-Type: TEXT/HTML;
+ charset=utf-8
+
+<html><head><title>Set the Foundation Straight</title></head>
+<body><font face=3D"Arial" size=3D"2">
+Dear Dors Venabili,<br><br>
+The plan for the Foundation is in this message=E2=80=99s HTML part.<br><br>
+Best regards,<br>Hari Seldon<br>
+</font></body></html>
+
+--_=ExampleMailer=_0123456789abcdef--
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>conversation-id</key>
+ <integer>900001</integer>
+ <key>date-last-viewed</key>
+ <integer>0</integer>
+ <key>date-received</key>
+ <integer>1443297769</integer>
+ <key>flags</key>
+ <integer>25803619329</integer>
+ <key>remote-id</key>
+ <string>90002</string>
+</dict>
+</plist>
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/resources/test-documents/testEMLX_single_part_html.emlx
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/resources/test-documents/testEMLX_single_part_html.emlx
new file mode 100644
index 0000000000..2cd779cf58
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-mail-module/src/test/resources/test-documents/testEMLX_single_part_html.emlx
@@ -0,0 +1,24 @@
+446
+Subject: Trial balance
+Mime-Version: 1.0 (Mac OS X Mail 13.4 \(3608.120.23.2.7\))
+Content-Type: text/html;
+ charset=utf-8
+Message-Id: <[email protected]>
+Date: Fri, 27 Oct 2023 10:03:30 +0200
+To: [email protected]
+Content-Transfer-Encoding: 7bit
+From: Hari Seldon <[email protected]>
+
+<html><head></head><body dir="auto" style="word-wrap: break-word;">The trial
balance is ready for review.</body></html>
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>date-last-viewed</key>
+ <integer>1698393810</integer>
+ <key>date-received</key>
+ <integer>1698393810</integer>
+ <key>flags</key>
+ <integer>8623685697</integer>
+</dict>
+</plist>