Skip to content

Commit 5b704fa

Browse files
author
Benny Bottema
committed
#491: handle body parts that have both Content-Disposition "attachment" and a ContentID for embedding it in HTML
1 parent 62d229b commit 5b704fa

File tree

5 files changed

+128314
-81
lines changed

5 files changed

+128314
-81
lines changed

modules/simple-java-mail/src/main/java/org/simplejavamail/converter/EmailConverter.java

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
package org.simplejavamail.converter;
22

3-
import jakarta.activation.DataSource;
43
import jakarta.mail.MessagingException;
54
import jakarta.mail.Session;
65
import jakarta.mail.internet.InternetAddress;
76
import jakarta.mail.internet.MimeMessage;
87
import lombok.val;
98
import org.jetbrains.annotations.NotNull;
109
import org.jetbrains.annotations.Nullable;
11-
import org.simplejavamail.api.email.CalendarMethod;
12-
import org.simplejavamail.api.email.ContentTransferEncoding;
13-
import org.simplejavamail.api.email.Email;
14-
import org.simplejavamail.api.email.EmailPopulatingBuilder;
15-
import org.simplejavamail.api.email.OriginalSmimeDetails;
10+
import org.simplejavamail.api.email.*;
1611
import org.simplejavamail.api.email.OriginalSmimeDetails.SmimeMode;
1712
import org.simplejavamail.api.internal.general.HeadersToIgnoreWhenParsingExternalEmails;
1813
import org.simplejavamail.api.internal.outlooksupport.model.EmailFromOutlookMessage;
@@ -32,25 +27,15 @@
3227
import org.simplejavamail.internal.moduleloader.ModuleLoader;
3328
import org.simplejavamail.internal.smimesupport.model.OriginalSmimeDetailsImpl;
3429

35-
import java.io.ByteArrayInputStream;
36-
import java.io.ByteArrayOutputStream;
37-
import java.io.File;
38-
import java.io.FileInputStream;
39-
import java.io.FileNotFoundException;
40-
import java.io.IOException;
41-
import java.io.InputStream;
42-
import java.io.OutputStream;
43-
import java.io.UnsupportedEncodingException;
30+
import java.io.*;
4431
import java.util.Map;
4532
import java.util.Properties;
4633

4734
import static java.lang.String.format;
4835
import static java.nio.charset.StandardCharsets.UTF_8;
4936
import static org.simplejavamail.api.email.OriginalSmimeDetails.SmimeMode.PLAIN;
5037
import static org.simplejavamail.internal.moduleloader.ModuleLoader.loadSmimeModule;
51-
import static org.simplejavamail.internal.util.MiscUtil.extractCID;
52-
import static org.simplejavamail.internal.util.MiscUtil.readInputStreamToString;
53-
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
38+
import static org.simplejavamail.internal.util.MiscUtil.*;
5439
import static org.simplejavamail.internal.util.Preconditions.checkNonEmptyArgument;
5540
import static org.simplejavamail.internal.util.Preconditions.verifyNonnullOrEmpty;
5641
import static org.simplejavamail.mailer.internal.EmailGovernanceImpl.NO_GOVERNANCE;
@@ -685,9 +670,9 @@ private static EmailPopulatingBuilder buildEmailFromMimeMessage(@NotNull final E
685670
builder.withCalendarText(CalendarMethod.valueOf(parsed.getCalendarMethod()), verifyNonnullOrEmpty(parsed.getCalendarContent()));
686671
}
687672

688-
for (final Map.Entry<String, DataSource> cid : parsed.getCidMap().entrySet()) {
673+
for (final Map.Entry<String, MimeDataSource> cid : parsed.getCidMap().entrySet()) {
689674
final String cidName = checkNonEmptyArgument(cid.getKey(), "cid.key");
690-
builder.withEmbeddedImage(extractCID(cidName), cid.getValue());
675+
builder.withEmbeddedImage(extractCID(cidName), cid.getValue().getDataSource());
691676
}
692677
for (final MimeDataSource attachment : parsed.getAttachmentList()) {
693678
final ContentTransferEncoding encoding = !valueNullOrEmpty(attachment.getContentTransferEncoding())
@@ -701,4 +686,4 @@ private static Session createDummySession() {
701686
return Session.getDefaultInstance(new Properties());
702687
}
703688

704-
}
689+
}

modules/simple-java-mail/src/main/java/org/simplejavamail/converter/internal/mimemessage/MimeMessageParser.java

Lines changed: 34 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,27 @@
11
package org.simplejavamail.converter.internal.mimemessage;
22

3-
import jakarta.activation.CommandMap;
4-
import jakarta.activation.MailcapCommandMap;
5-
import lombok.Getter;
6-
import org.eclipse.angus.mail.handlers.text_plain;
7-
import jakarta.activation.ActivationDataFlavor;
8-
import jakarta.activation.DataHandler;
9-
import jakarta.activation.DataSource;
3+
import jakarta.activation.*;
104
import jakarta.mail.Address;
115
import jakarta.mail.Message.RecipientType;
126
import jakarta.mail.MessagingException;
137
import jakarta.mail.Multipart;
148
import jakarta.mail.Part;
15-
import jakarta.mail.internet.AddressException;
16-
import jakarta.mail.internet.ContentType;
17-
import jakarta.mail.internet.InternetAddress;
18-
import jakarta.mail.internet.MimeBodyPart;
19-
import jakarta.mail.internet.MimeMessage;
20-
import jakarta.mail.internet.MimePart;
21-
import jakarta.mail.internet.MimeUtility;
22-
import jakarta.mail.internet.ParseException;
9+
import jakarta.mail.internet.*;
2310
import jakarta.mail.util.ByteArrayDataSource;
11+
import lombok.Getter;
2412
import lombok.val;
13+
import org.eclipse.angus.mail.handlers.text_plain;
2514
import org.jetbrains.annotations.NotNull;
2615
import org.jetbrains.annotations.Nullable;
27-
import org.simplejavamail.api.internal.general.HeadersToIgnoreWhenParsingExternalEmails;
2816
import org.simplejavamail.internal.util.MiscUtil;
2917
import org.simplejavamail.internal.util.NamedDataSource;
3018
import org.simplejavamail.internal.util.Preconditions;
19+
import org.slf4j.Logger;
3120

3221
import java.io.IOException;
3322
import java.io.InputStream;
3423
import java.io.UnsupportedEncodingException;
35-
import java.util.ArrayList;
36-
import java.util.Arrays;
37-
import java.util.Collection;
38-
import java.util.Collections;
39-
import java.util.Date;
40-
import java.util.HashMap;
41-
import java.util.Iterator;
42-
import java.util.List;
43-
import java.util.Map;
44-
import java.util.Set;
45-
import java.util.TreeMap;
46-
import java.util.TreeSet;
24+
import java.util.*;
4725
import java.util.regex.Matcher;
4826
import java.util.regex.Pattern;
4927

@@ -54,6 +32,7 @@
5432
import static java.util.stream.Collectors.toList;
5533
import static org.simplejavamail.internal.util.MiscUtil.extractCID;
5634
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
35+
import static org.slf4j.LoggerFactory.getLogger;
5736

5837
/**
5938
* Parses a MimeMessage and stores the individual parts such a plain text, HTML text and attachments.
@@ -62,6 +41,8 @@
6241
*/
6342
public final class MimeMessageParser {
6443

44+
private static final Logger LOGGER = getLogger(MimeMessageParser.class);
45+
6546
static {
6647
MailcapCommandMap mc = (MailcapCommandMap) CommandMap.getDefaultCommandMap();
6748
mc.addMailcap("text/calendar;; x-java-content-handler=" + text_calendar.class.getName());
@@ -116,20 +97,26 @@ private static void parseMimePartTree(@NotNull final MimePart currentPart, @NotN
11697
parseMimePartTree(getBodyPartAtIndex(mp, i), parsedComponents, fetchAttachmentData);
11798
}
11899
} else {
119-
final DataSource ds = createDataSource(currentPart, fetchAttachmentData);
120-
// if the diposition is not provided, for now the part should be treated as inline (later non-embedded inline attachments are moved)
121-
if (Part.ATTACHMENT.equalsIgnoreCase(disposition)) {
122-
parsedComponents.attachmentList.add(parseAttachment(parseContentID(currentPart), currentPart, ds));
123-
} else if (disposition == null || Part.INLINE.equalsIgnoreCase(disposition)) {
124-
if (parseContentID(currentPart) != null) {
125-
parsedComponents.cidMap.put(parseContentID(currentPart), ds);
126-
} else {
127-
// contentID missing -> treat as standard attachment
128-
parsedComponents.attachmentList.add(parseAttachment(null, currentPart, ds));
129-
}
130-
} else {
131-
throw new IllegalStateException("invalid attachment type");
132-
}
100+
parseDataSource(currentPart, parsedComponents, fetchAttachmentData, disposition);
101+
}
102+
}
103+
104+
private static void parseDataSource(@NotNull MimePart currentPart, @NotNull ParsedMimeMessageComponents parsedComponents, boolean fetchAttachmentData, String disposition) {
105+
final String contentID = parseContentID(currentPart);
106+
final MimeDataSource mimeDataSource = parseAttachment(contentID, currentPart, createDataSource(currentPart, fetchAttachmentData));
107+
final boolean isAttachment = Part.ATTACHMENT.equalsIgnoreCase(disposition);
108+
final boolean isInline = Part.INLINE.equalsIgnoreCase(disposition);
109+
110+
if (disposition != null && !isAttachment && !isInline) {
111+
LOGGER.warn("Content-Disposition '{}' for data source not recognized (it should be either 'attachment' or 'inline'). Skipping body part", disposition);
112+
}
113+
114+
if (!isInline || contentID == null) {
115+
parsedComponents.attachmentList.add(mimeDataSource);
116+
}
117+
if (contentID != null) {
118+
parsedComponents.cidMap.put(contentID, mimeDataSource);
119+
// when parsing is done, we'll move any sources from cidMap that are not referenced in HTML, to attachments (or remove if already there)
133120
}
134121
}
135122

@@ -545,11 +532,11 @@ public static Date parseSentDate(@NotNull final MimeMessage mimeMessage) {
545532

546533
static void moveInvalidEmbeddedResourcesToAttachments(ParsedMimeMessageComponents parsedComponents) {
547534
final String htmlContent = parsedComponents.htmlContent.toString();
548-
for (Iterator<Map.Entry<String, DataSource>> it = parsedComponents.cidMap.entrySet().iterator(); it.hasNext(); ) {
549-
Map.Entry<String, DataSource> cidEntry = it.next();
535+
for (Iterator<Map.Entry<String, MimeDataSource>> it = parsedComponents.cidMap.entrySet().iterator(); it.hasNext(); ) {
536+
Map.Entry<String, MimeDataSource> cidEntry = it.next();
550537
String cid = extractCID(cidEntry.getKey());
551538
if (!htmlContent.contains("cid:" + cid)) {
552-
parsedComponents.attachmentList.add(new MimeDataSource(cid, cidEntry.getValue(), null, null));
539+
parsedComponents.attachmentList.add(cidEntry.getValue());
553540
it.remove();
554541
}
555542
}
@@ -558,7 +545,7 @@ static void moveInvalidEmbeddedResourcesToAttachments(ParsedMimeMessageComponent
558545
@Getter
559546
public static class ParsedMimeMessageComponents {
560547
final Set<MimeDataSource> attachmentList = new TreeSet<>();
561-
final Map<String, DataSource> cidMap = new TreeMap<>();
548+
final Map<String, MimeDataSource> cidMap = new TreeMap<>();
562549
private final Map<String, Collection<Object>> headers = new HashMap<>();
563550
private final List<InternetAddress> toAddresses = new ArrayList<>();
564551
private final List<InternetAddress> ccAddresses = new ArrayList<>();
@@ -608,4 +595,4 @@ protected ActivationDataFlavor[] getDataFlavors() {
608595
return myDF;
609596
}
610597
}
611-
}
598+
}

modules/simple-java-mail/src/test/java/org/simplejavamail/converter/EmailConverterTest.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,8 @@ public void testGithub486_InvalidSignedOutlookMessage() {
342342
Email emailMime = EmailConverter.emlToEmail(new File(RESOURCE_TEST_MESSAGES + "/#486 TestValidSignedMimeMessage.eml"));
343343
Email emailOutlook = EmailConverter.outlookMsgToEmail(new File(RESOURCE_TEST_MESSAGES + "/#486 TestInvalidSignedOutlookMessage.msg"));
344344

345-
assertThat(emailMime.getEmbeddedImages()).areExactly(1, new Condition<>(at -> at.getName().contains(".jpg"), null));
346-
assertThat(emailMime.getAttachments()).areExactly(1, new Condition<>(at -> at.getName().contains(".jpg"), null));
345+
assertThat(emailMime.getEmbeddedImages()).areExactly(1, new Condition<>(at -> at.getName().contains(".jpg"), null));
346+
assertThat(emailMime.getAttachments()).areExactly(2, new Condition<>(at -> at.getName().contains(".jpg"), null));
347347

348348
assertThat(emailMime.getOriginalSmimeDetails().getSmimeMode()).isEqualTo(OriginalSmimeDetails.SmimeMode.SIGNED);
349349
assertThat(emailOutlook.getOriginalSmimeDetails().getSmimeMode()).isEqualTo(OriginalSmimeDetails.SmimeMode.SIGNED);
@@ -367,10 +367,25 @@ public void testGithub486_InvalidSignedOutlookMessage() {
367367
assertThat(emailMime.getReturnReceiptTo()).isEqualTo(emailOutlook.getReturnReceiptTo());
368368
}
369369

370+
@Test
371+
public void testGithub491_EmailWithMultiPurposeAttachments() {
372+
Email emailMime = EmailConverter.emlToEmail(new File(RESOURCE_TEST_MESSAGES + "/#491 Email with dual purpose datasources.eml"));
373+
374+
assertThat(emailMime.getEmbeddedImages()).satisfiesExactly(
375+
at -> {
376+
at.getName().equals("ii_lrkua30a0");
377+
at.getDataSource().getName().equals("doclife.jpg");
378+
});
379+
assertThat(emailMime.getAttachments()).satisfiesExactlyInAnyOrder(
380+
at -> at.getName().equals("Il Viaggio delle Ombre.pdf"),
381+
at -> at.getName().equals("Nyan Cat! [Official]-(480p).mp4"),
382+
at -> at.getName().equals("doclife.jpg"));
383+
}
384+
370385
@NotNull
371386
private List<AttachmentResource> asList(AttachmentResource attachment) {
372387
List<AttachmentResource> collectionAttachment = new ArrayList<>();
373388
collectionAttachment.add(attachment);
374389
return collectionAttachment;
375390
}
376-
}
391+
}

modules/simple-java-mail/src/test/java/org/simplejavamail/converter/internal/mimemessage/MimeMessageParserTest.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,36 +120,36 @@ private static BodyPart getBodyPartFromDatasource(final AttachmentResource attac
120120
@Test
121121
public void testMoveInvalidEmbeddedResourcesToAttachments_NoHtmlNoInvalid() throws IOException {
122122
ParsedMimeMessageComponents parsedComponents = new ParsedMimeMessageComponents();
123-
parsedComponents.cidMap.put("moo1", new ByteArrayDataSource("moomoo", "text/plain"));
124-
parsedComponents.cidMap.put("moo2", new ByteArrayDataSource("moomoo", "text/plain"));
123+
parsedComponents.cidMap.put("moo1", new MimeDataSource("moomoo1", new ByteArrayDataSource("moomoo", "text/plain"), null, null));
124+
parsedComponents.cidMap.put("moo2", new MimeDataSource("moomoo2", new ByteArrayDataSource("moomoo", "text/plain"), null, null));
125125
moveInvalidEmbeddedResourcesToAttachments(parsedComponents);
126126

127127
assertThat(parsedComponents.cidMap).isEmpty();
128-
assertThat(parsedComponents.attachmentList).extracting("name").containsOnly("moo1", "moo2");
128+
assertThat(parsedComponents.attachmentList).extracting("name").containsOnly("moomoo1", "moomoo2");
129129
}
130130

131131
@Test
132132
public void testMoveInvalidEmbeddedResourcesToAttachments_HtmlButNoInvalid() throws IOException {
133133
ParsedMimeMessageComponents parsedComponents = new ParsedMimeMessageComponents();
134134
parsedComponents.htmlContent.append("blah moo1 blah html");
135-
parsedComponents.cidMap.put("moo1", new ByteArrayDataSource("moomoo", "text/plain"));
136-
parsedComponents.cidMap.put("moo2", new ByteArrayDataSource("moomoo", "text/plain"));
135+
parsedComponents.cidMap.put("moo1", new MimeDataSource("moomoo1", new ByteArrayDataSource("moomoo", "text/plain"), null, null));
136+
parsedComponents.cidMap.put("moo2", new MimeDataSource("moomoo2", new ByteArrayDataSource("moomoo", "text/plain"), null, null));
137137
moveInvalidEmbeddedResourcesToAttachments(parsedComponents);
138138

139139
assertThat(parsedComponents.cidMap).isEmpty();
140-
assertThat(parsedComponents.attachmentList).extracting(MimeDataSource::getName).containsOnly("moo1", "moo2");
140+
assertThat(parsedComponents.attachmentList).extracting(MimeDataSource::getName).containsOnly("moomoo1", "moomoo2");
141141
}
142142

143143
@Test
144144
public void testMoveInvalidEmbeddedResourcesToAttachments_Invalid() throws IOException {
145145
ParsedMimeMessageComponents parsedComponents = new ParsedMimeMessageComponents();
146146
parsedComponents.htmlContent.append("blah cid:moo1 blah html");
147-
parsedComponents.cidMap.put("moo1", new ByteArrayDataSource("moomoo", "text/plain"));
148-
parsedComponents.cidMap.put("moo2", new ByteArrayDataSource("moomoo", "text/plain"));
147+
parsedComponents.cidMap.put("moo1", new MimeDataSource("moomoo1", new ByteArrayDataSource("moomoo", "text/plain"), null, null));
148+
parsedComponents.cidMap.put("moo2", new MimeDataSource("moomoo2", new ByteArrayDataSource("moomoo", "text/plain"), null, null));
149149
moveInvalidEmbeddedResourcesToAttachments(parsedComponents);
150150

151151
assertThat(parsedComponents.cidMap).containsOnlyKeys("moo1");
152-
assertThat(parsedComponents.attachmentList).extracting(MimeDataSource::getName).containsOnly("moo2");
152+
assertThat(parsedComponents.attachmentList).extracting(MimeDataSource::getName).containsOnly("moomoo2");
153153
}
154154

155155
@Test
@@ -199,4 +199,4 @@ public void testSemiColonSeparatedToAddresses() {
199199

200200
EmailAssert.assertThat(fixedEmail).hasOnlyRecipients(new Recipient("C.Cane", "[email protected]", TO));
201201
}
202-
}
202+
}

0 commit comments

Comments
 (0)