Remove Jakarta Mail dependency from spring-web

This commit removes the dependency that the spring-web module has on
Jakarta Mail.

In FormHttpMessageConverter, a dependency on
jakarta.mail.internet.MimeUtility was replaced by existing encoding
logic in ContentDisposition.

In StandardMultipartHttpServletRequest, a dependency on the same
MimeUtility was replaced by new quoted-printable decoding logic in
ContentDisposition.

Closes gh-28392
This commit is contained in:
Arjen Poutsma 2022-04-28 15:21:58 +02:00
parent b4e6014a14
commit 217117ced0
5 changed files with 78 additions and 55 deletions

View File

@ -15,7 +15,6 @@ dependencies {
optional("jakarta.el:jakarta.el-api")
optional("jakarta.faces:jakarta.faces-api")
optional("jakarta.json.bind:jakarta.json.bind-api")
optional("jakarta.mail:jakarta.mail-api")
optional("jakarta.validation:jakarta.validation-api")
optional("jakarta.xml.bind:jakarta.xml.bind-api")
optional("io.reactivex.rxjava3:rxjava")

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -33,6 +33,7 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.StreamUtils;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
@ -51,6 +52,9 @@ public final class ContentDisposition {
private final static Pattern BASE64_ENCODED_PATTERN =
Pattern.compile("=\\?([0-9a-zA-Z-_]+)\\?B\\?([+/0-9a-zA-Z]+=*)\\?=");
private final static Pattern QUOTED_PRINTABLE_ENCODED_PATTERN =
Pattern.compile("=\\?([0-9a-zA-Z-_]+)\\?Q\\?(\\p{Print}+)\\?=");
private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
"Invalid header field parameter format (as defined in RFC 5987)";
@ -371,12 +375,20 @@ public final class ContentDisposition {
if (value.startsWith("=?") ) {
Matcher matcher = BASE64_ENCODED_PATTERN.matcher(value);
if (matcher.find()) {
String match1 = matcher.group(1);
String match2 = matcher.group(2);
filename = new String(Base64.getDecoder().decode(match2), Charset.forName(match1));
charset = Charset.forName(matcher.group(1));
String encodedValue = matcher.group(2);
filename = new String(Base64.getDecoder().decode(encodedValue), charset);
}
else {
filename = value;
matcher = QUOTED_PRINTABLE_ENCODED_PATTERN.matcher(value);
if (matcher.find()) {
charset = Charset.forName(matcher.group(1));
String encodedValue = matcher.group(2);
filename = decodeQuotedPrintableFilename(encodedValue, charset);
}
else {
filename = value;
}
}
}
else {
@ -498,6 +510,43 @@ public final class ContentDisposition {
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
}
/**
* Decode the given header field param as described in RFC 2047.
* @param filename the filename
* @param charset the charset for the filename
* @return the encoded header field param
* @see <a href="https://tools.ietf.org/html/rfc2047">RFC 2047</a>
*/
private static String decodeQuotedPrintableFilename(String filename, Charset charset) {
Assert.notNull(filename, "'input' String` should not be null");
Assert.notNull(charset, "'charset' should not be null");
byte[] value = filename.getBytes(US_ASCII);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int index = 0;
while (index < value.length) {
byte b = value[index];
if (b == '_') {
baos.write(' ');
index++;
}
else if (b == '=' && index < value.length - 2) {
int i1 = Character.digit((char) value[index + 1], 16);
int i2 = Character.digit((char) value[index + 2], 16);
if (i1 == -1 || i2 == -1) {
throw new IllegalArgumentException("Not a valid hex sequence: " + filename.substring(index));
}
baos.write((i1 << 4) | i2);
index += 3;
}
else {
baos.write(b);
index++;
}
}
return StreamUtils.copyToString(baos, charset);
}
private static String escapeQuotationsInFilename(String filename) {
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
return filename;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,7 +18,6 @@ package org.springframework.http.converter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
@ -29,9 +28,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import jakarta.mail.internet.MimeUtility;
import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
@ -526,7 +524,13 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
if (messageConverter.canWrite(partType, partContentType)) {
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
String filename = getFilename(partBody);
ContentDisposition.Builder cd = ContentDisposition.formData()
.name(name);
if (filename != null) {
cd.filename(filename, this.multipartCharset);
}
multipartMessage.getHeaders().setContentDisposition(cd.build());
if (!partHeaders.isEmpty()) {
multipartMessage.getHeaders().putAll(partHeaders);
}
@ -568,11 +572,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
@Nullable
protected String getFilename(Object part) {
if (part instanceof Resource resource) {
String filename = resource.getFilename();
if (filename != null && this.multipartCharset != null) {
filename = MimeDelegate.encode(filename, this.multipartCharset.name());
}
return filename;
return resource.getFilename();
}
else {
return null;
@ -655,20 +655,4 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
}
}
/**
* Inner class to avoid a hard dependency on the JavaMail API.
*/
private static class MimeDelegate {
public static String encode(String value, String charset) {
try {
return MimeUtility.encodeText(value, charset, null);
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,7 +20,6 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
@ -32,7 +31,6 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import jakarta.mail.internet.MimeUtility;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.Part;
@ -100,9 +98,6 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
ContentDisposition disposition = ContentDisposition.parse(headerValue);
String filename = disposition.getFilename();
if (filename != null) {
if (filename.startsWith("=?") && filename.endsWith("?=")) {
filename = MimeDelegate.decode(filename);
}
files.add(part.getName(), new StandardMultipartFile(part, filename));
}
else {
@ -271,20 +266,4 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
}
}
/**
* Inner class to avoid a hard dependency on the JavaMail API.
*/
private static class MimeDelegate {
public static String decode(String value) {
try {
return MimeUtility.decodeText(value);
}
catch (UnsupportedEncodingException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -92,6 +92,18 @@ class ContentDispositionTests {
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
}
@Test
void parseQuotedPrintableFilename() {
String input = "attachment; filename=\"=?UTF-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E.csv?=\"";
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
}
@Test
void parseQuotedPrintableShiftJISFilename() {
String input = "attachment; filename=\"=?SHIFT_JIS?Q?=93=FA=96{=8C=EA.csv?=\"";
assertThat(parse(input).getFilename()).isEqualTo("日本語.csv");
}
@Test
void parseEncodedFilenameWithoutCharset() {
assertThat(parse("form-data; name=\"name\"; filename*=test.txt"))