Skip to content

Commit 5368d30

Browse files
committed
#293: decode all values extracted from MimeMessage, but also encode attachment description properly when sending
1 parent 3d41a46 commit 5368d30

File tree

7 files changed

+5021
-33
lines changed

7 files changed

+5021
-33
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import jakarta.mail.Session;
66
import jakarta.mail.internet.InternetAddress;
77
import jakarta.mail.internet.MimeMessage;
8+
import lombok.val;
89
import org.jetbrains.annotations.NotNull;
910
import org.jetbrains.annotations.Nullable;
1011
import org.simplejavamail.api.email.CalendarMethod;
@@ -117,9 +118,10 @@ public static EmailPopulatingBuilder mimeMessageToEmailBuilder(@NotNull final Mi
117118
@NotNull
118119
public static EmailPopulatingBuilder mimeMessageToEmailBuilder(@NotNull final MimeMessage mimeMessage, @Nullable final Pkcs12Config pkcs12Config, final boolean fetchAttachmentData) {
119120
checkNonEmptyArgument(mimeMessage, "mimeMessage");
120-
final EmailPopulatingBuilder builder = EmailBuilder.ignoringDefaults().startingBlank();
121-
final ParsedMimeMessageComponents parsed = MimeMessageParser.parseMimeMessage(mimeMessage, fetchAttachmentData);
122-
return decryptAttachments(buildEmailFromMimeMessage(builder, parsed), mimeMessage, pkcs12Config);
121+
val builder = EmailBuilder.ignoringDefaults().startingBlank();
122+
val parsed = MimeMessageParser.parseMimeMessage(mimeMessage, fetchAttachmentData);
123+
val emailBuilder = buildEmailFromMimeMessage(builder, parsed);
124+
return decryptAttachments(emailBuilder, mimeMessage, pkcs12Config);
123125
}
124126

125127
/**
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.simplejavamail.converter.internal.mimemessage;
2+
3+
import jakarta.mail.Header;
4+
import lombok.Value;
5+
import lombok.val;
6+
7+
@Value
8+
class DecodedHeader {
9+
10+
String name;
11+
String value;
12+
13+
public static DecodedHeader of(Header h) {
14+
val decodedName = MimeMessageParser.decodeText(h.getName());
15+
val decodedValue = MimeMessageParser.decodeText(h.getValue());
16+
return new DecodedHeader(decodedName, decodedValue);
17+
}
18+
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import jakarta.mail.internet.ParameterList;
1717
import lombok.val;
1818
import org.jetbrains.annotations.NotNull;
19+
import org.jetbrains.annotations.Nullable;
1920
import org.simplejavamail.api.email.AttachmentResource;
2021
import org.simplejavamail.api.email.Email;
2122
import org.simplejavamail.api.email.Recipient;
@@ -33,6 +34,7 @@
3334

3435
import static java.lang.Boolean.TRUE;
3536
import static java.lang.String.format;
37+
import static java.util.Optional.ofNullable;
3638
import static org.simplejavamail.internal.util.MiscUtil.orOther;
3739
import static org.simplejavamail.internal.util.MiscUtil.orOtherList;
3840
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
@@ -296,7 +298,8 @@ private static BodyPart getBodyPartFromDatasource(final AttachmentResource attac
296298
pl.set("name", fileName);
297299
attachmentPart.setHeader("Content-Type", contentType + pl);
298300
attachmentPart.setHeader("Content-ID", format("<%s>", resourceName));
299-
attachmentPart.setHeader("Content-Description", attachmentResource.getDescription());
301+
302+
attachmentPart.setHeader("Content-Description", determineAttachmentDescription(attachmentResource));
300303
if (!valueNullOrEmpty(attachmentResource.getContentTransferEncoding())) {
301304
attachmentPart.setHeader("Content-Transfer-Encoding", attachmentResource.getContentTransferEncoding().getEncoder());
302305
}
@@ -337,4 +340,9 @@ private static String possiblyAddExtension(final String datasourceName, String r
337340
}
338341
return resourceName;
339342
}
343+
344+
@Nullable
345+
private static String determineAttachmentDescription(AttachmentResource attachmentResource) {
346+
return ofNullable(attachmentResource.getDescription()).map(MiscUtil::encodeText).orElse(null);
347+
}
340348
}

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

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import jakarta.activation.DataSource;
88
import jakarta.activation.MailcapCommandMap;
99
import jakarta.mail.Address;
10-
import jakarta.mail.Header;
1110
import jakarta.mail.Message.RecipientType;
1211
import jakarta.mail.MessagingException;
1312
import jakarta.mail.Multipart;
@@ -21,6 +20,7 @@
2120
import jakarta.mail.internet.MimeUtility;
2221
import jakarta.mail.internet.ParseException;
2322
import jakarta.mail.util.ByteArrayDataSource;
23+
import lombok.val;
2424
import org.jetbrains.annotations.NotNull;
2525
import org.jetbrains.annotations.Nullable;
2626
import org.simplejavamail.internal.util.MiscUtil;
@@ -48,6 +48,7 @@
4848
import static java.lang.String.format;
4949
import static java.nio.charset.StandardCharsets.UTF_8;
5050
import static java.util.Optional.ofNullable;
51+
import static java.util.stream.Collectors.toList;
5152
import static org.simplejavamail.internal.util.MiscUtil.extractCID;
5253
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
5354

@@ -139,7 +140,7 @@ public static ParsedMimeMessageComponents parseMimeMessage(@NotNull final MimeMe
139140
}
140141

141142
private static void parseMimePartTree(@NotNull final MimePart currentPart, @NotNull final ParsedMimeMessageComponents parsedComponents, final boolean fetchAttachmentData) {
142-
for (final Header header : retrieveAllHeaders(currentPart)) {
143+
for (final DecodedHeader header : retrieveAllHeaders(currentPart)) {
143144
parseHeader(header, parsedComponents);
144145
}
145146

@@ -180,7 +181,7 @@ private static void parseMimePartTree(@NotNull final MimePart currentPart, @NotN
180181

181182
private static void checkContentTransferEncoding(final MimePart currentPart, @NotNull final ParsedMimeMessageComponents parsedComponents) {
182183
if (parsedComponents.contentTransferEncoding == null) {
183-
for (final Header header : retrieveAllHeaders(currentPart)) {
184+
for (final DecodedHeader header : retrieveAllHeaders(currentPart)) {
184185
if (isEmailHeader(header, "Content-Transfer-Encoding")) {
185186
parsedComponents.contentTransferEncoding = header.getValue();
186187
}
@@ -198,24 +199,27 @@ private static MimeDataSource parseAttachment(@Nullable final String contentId,
198199
}
199200

200201
@SuppressWarnings("StatementWithEmptyBody")
201-
private static void parseHeader(final Header header, @NotNull final ParsedMimeMessageComponents parsedComponents) {
202+
private static void parseHeader(final DecodedHeader header, @NotNull final ParsedMimeMessageComponents parsedComponents) {
203+
val headerValue = decodeText(header.getValue());
204+
val headerName = decodeText(header.getName());
205+
202206
if (isEmailHeader(header, "Disposition-Notification-To")) {
203-
parsedComponents.dispositionNotificationTo = createAddress(header.getValue(), "Disposition-Notification-To");
207+
parsedComponents.dispositionNotificationTo = createAddress(headerValue, "Disposition-Notification-To");
204208
} else if (isEmailHeader(header, "Return-Receipt-To")) {
205-
parsedComponents.returnReceiptTo = createAddress(header.getValue(), "Return-Receipt-To");
209+
parsedComponents.returnReceiptTo = createAddress(headerValue, "Return-Receipt-To");
206210
} else if (isEmailHeader(header, "Return-Path")) {
207-
parsedComponents.bounceToAddress = createAddress(header.getValue(), "Return-Path");
208-
} else if (!HEADERS_TO_IGNORE.contains(header.getName())) {
209-
if (!parsedComponents.headers.containsKey(header.getName())) {
210-
parsedComponents.headers.put(header.getName(), new ArrayList<>());
211+
parsedComponents.bounceToAddress = createAddress(headerValue, "Return-Path");
212+
} else if (!HEADERS_TO_IGNORE.contains(headerName)) {
213+
if (!parsedComponents.headers.containsKey(headerName)) {
214+
parsedComponents.headers.put(headerName, new ArrayList<>());
211215
}
212-
parsedComponents.headers.get(header.getName()).add(MimeUtility.unfold(header.getValue()));
216+
parsedComponents.headers.get(headerName).add(MimeUtility.unfold(headerValue));
213217
} else {
214218
// header recognized, but not relevant (see #HEADERS_TO_IGNORE)
215219
}
216220
}
217221

218-
private static boolean isEmailHeader(Header header, String emailHeaderName) {
222+
private static boolean isEmailHeader(DecodedHeader header, String emailHeaderName) {
219223
return header.getName().equals(emailHeaderName) &&
220224
!valueNullOrEmpty(header.getValue()) &&
221225
!valueNullOrEmpty(header.getValue().trim()) &&
@@ -226,7 +230,7 @@ private static boolean isEmailHeader(Header header, String emailHeaderName) {
226230
public static String parseFileName(@NotNull final Part currentPart) {
227231
try {
228232
if (currentPart.getFileName() != null) {
229-
return currentPart.getFileName();
233+
return decodeText(currentPart.getFileName());
230234
} else {
231235
// replicate behavior from Thunderbird
232236
if (Arrays.asList(currentPart.getHeader("Content-Type")).contains("message/rfc822")) {
@@ -276,7 +280,9 @@ public static String parseCalendarMethod(@NotNull MimePart currentPart) {
276280
@Nullable
277281
public static String parseContentID(@NotNull final MimePart currentPart) {
278282
try {
279-
return currentPart.getContentID();
283+
return ofNullable(currentPart.getContentID())
284+
.map(MimeMessageParser::decodeText)
285+
.orElse(null);
280286
} catch (final MessagingException e) {
281287
throw new MimeMessageParseException(MimeMessageParseException.ERROR_GETTING_CONTENT_ID, e);
282288
}
@@ -336,9 +342,11 @@ private static String parseResourceName(@Nullable String possibleWrappedContentI
336342

337343
@SuppressWarnings("WeakerAccess")
338344
@NotNull
339-
public static List<Header> retrieveAllHeaders(@NotNull final MimePart part) {
345+
public static List<DecodedHeader> retrieveAllHeaders(@NotNull final MimePart part) {
340346
try {
341-
return Collections.list(part.getAllHeaders());
347+
return Collections.list(part.getAllHeaders()).stream()
348+
.map(DecodedHeader::of)
349+
.collect(toList());
342350
} catch (final MessagingException e) {
343351
throw new MimeMessageParseException(MimeMessageParseException.ERROR_GETTING_ALL_HEADERS, e);
344352
}
@@ -430,15 +438,6 @@ private static String parseDataSourceName(@NotNull final Part part, @NotNull fin
430438
return !valueNullOrEmpty(result) ? decodeText(result) : null;
431439
}
432440

433-
@NotNull
434-
private static String decodeText(@NotNull final String result) {
435-
try {
436-
return MimeUtility.decodeText(result);
437-
} catch (final UnsupportedEncodingException e) {
438-
throw new MimeMessageParseException(MimeMessageParseException.ERROR_DECODING_TEXT, e);
439-
}
440-
}
441-
442441
@NotNull
443442
private static byte[] readContent(@NotNull final InputStream is) {
444443
try {
@@ -472,7 +471,9 @@ public static Address[] retrieveRecipients(@NotNull final MimeMessage mimeMessag
472471
try {
473472
// return mimeMessage.getRecipients(recipientType); // can fail in strict mode, see https://github.com/bbottema/simple-java-mail/issues/227
474473
// workaround following (copied and modified from JavaMail internal code):
475-
String s = mimeMessage.getHeader(getHeaderName(recipientType), ",");
474+
val s = ofNullable(mimeMessage.getHeader(getHeaderName(recipientType), ","))
475+
.map(MimeMessageParser::decodeText)
476+
.orElse(null);
476477
return (s == null) ? null : InternetAddress.parseHeader(s, false);
477478
} catch (final MessagingException e) {
478479
throw new MimeMessageParseException(format(MimeMessageParseException.ERROR_GETTING_RECIPIENTS, recipientType), e);
@@ -492,20 +493,33 @@ private static String getHeaderName(RecipientType recipientType) {
492493
@Nullable
493494
public static String parseContentDescription(@NotNull final MimePart mimePart) {
494495
try {
495-
return mimePart.getHeader("Content-Description", ",");
496+
return ofNullable(mimePart.getHeader("Content-Description", ","))
497+
.map(MimeMessageParser::decodeText)
498+
.orElse(null);
496499
} catch (final MessagingException e) {
497500
throw new MimeMessageParseException(MimeMessageParseException.ERROR_GETTING_CONTENT_DESCRIPTION, e);
498501
}
499502
}
500503
@Nullable
501504
public static String parseContentTransferEncoding(@NotNull final MimePart mimePart) {
502505
try {
503-
return mimePart.getHeader("Content-Transfer-Encoding", ",");
506+
return ofNullable(mimePart.getHeader("Content-Transfer-Encoding", ","))
507+
.map(MimeMessageParser::decodeText)
508+
.orElse(null);
504509
} catch (final MessagingException e) {
505510
throw new MimeMessageParseException(MimeMessageParseException.ERROR_GETTING_CONTENT_TRANSFER_ENCODING, e);
506511
}
507512
}
508513

514+
@NotNull
515+
static String decodeText(@NotNull final String result) {
516+
try {
517+
return MimeUtility.decodeText(result);
518+
} catch (final UnsupportedEncodingException e) {
519+
throw new MimeMessageParseException(MimeMessageParseException.ERROR_DECODING_TEXT, e);
520+
}
521+
}
522+
509523
@NotNull
510524
private static List<InternetAddress> parseInternetAddresses(@Nullable final Address[] recipients) {
511525
final List<Address> addresses = (recipients != null) ? Arrays.asList(recipients) : new ArrayList<>();

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import org.simplejavamail.api.email.Email;
1111
import org.simplejavamail.api.email.EmailAssert;
1212
import org.simplejavamail.api.email.Recipient;
13+
import org.simplejavamail.api.mailer.Mailer;
14+
import org.simplejavamail.api.mailer.config.TransportStrategy;
15+
import org.simplejavamail.mailer.MailerBuilder;
1316
import testutil.ConfigLoaderTestHelper;
1417
import testutil.EmailHelper;
1518
import testutil.SecureTestDataHelper;

modules/simple-java-mail/src/test/java/org/simplejavamail/mailer/MailerLiveTest.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.junit.Rule;
1010
import org.junit.Test;
1111
import org.simplejavamail.api.email.AttachmentResource;
12+
import org.simplejavamail.api.email.ContentTransferEncoding;
1213
import org.simplejavamail.api.email.Email;
1314
import org.simplejavamail.api.email.EmailAssert;
1415
import org.simplejavamail.api.email.EmailPopulatingBuilder;
@@ -63,7 +64,11 @@
6364
@SuppressWarnings("unused")
6465
public class MailerLiveTest {
6566

66-
private static final String RESOURCES_PKCS = determineResourceFolder("simple-java-mail") + "/test/resources/pkcs12";
67+
private static final String RESOURCES = determineResourceFolder("simple-java-mail") + "/test/resources";
68+
69+
private static final String RESOURCE_TEST_MESSAGES = RESOURCES + "/test-messages";
70+
71+
private static final String RESOURCES_PKCS = RESOURCES + "/pkcs12";
6772

6873
private static final Integer SERVER_PORT = 251;
6974

@@ -601,4 +606,21 @@ private static void sendAndVerifyEmailTooBigException(Mailer mailer) {
601606
.isInstanceOf(EmailTooBigException.class)
602607
.hasMessageContaining("bytes exceeds maximum allowed size of 4 bytes");
603608
}
609+
610+
@Test
611+
public void testNonASCIIAttachementNames() throws MessagingException {
612+
val email = EmailConverter.emlToEmail(new File(RESOURCE_TEST_MESSAGES + "/#293 Email with vers quoted printable.eml"));
613+
614+
mailer.sendMail(email);
615+
616+
val receivedEmail = mimeMessageToEmail(smtpServerRule.getOnlyMessage().getMimeMessage());
617+
618+
assertThat(receivedEmail.getAttachments()).hasSize(1);
619+
620+
val attachment = receivedEmail.getAttachments().get(0);
621+
622+
assertThat(attachment.getName()).isEqualTo("Configure_SSO_for_Admin_Console_Access_\u2013_Silverfort.pdf.html");
623+
assertThat(attachment.getDescription()).isEqualTo("Configure_SSO_for_Admin_Console_Access_\u2013_Silverfort.pdf.html");
624+
assertThat(attachment.getContentTransferEncoding()).isEqualTo(ContentTransferEncoding.BASE_64);
625+
}
604626
}

0 commit comments

Comments
 (0)