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:
parent
b4e6014a14
commit
217117ced0
|
|
@ -15,7 +15,6 @@ dependencies {
|
||||||
optional("jakarta.el:jakarta.el-api")
|
optional("jakarta.el:jakarta.el-api")
|
||||||
optional("jakarta.faces:jakarta.faces-api")
|
optional("jakarta.faces:jakarta.faces-api")
|
||||||
optional("jakarta.json.bind:jakarta.json.bind-api")
|
optional("jakarta.json.bind:jakarta.json.bind-api")
|
||||||
optional("jakarta.mail:jakarta.mail-api")
|
|
||||||
optional("jakarta.validation:jakarta.validation-api")
|
optional("jakarta.validation:jakarta.validation-api")
|
||||||
optional("jakarta.xml.bind:jakarta.xml.bind-api")
|
optional("jakarta.xml.bind:jakarta.xml.bind-api")
|
||||||
optional("io.reactivex.rxjava3:rxjava")
|
optional("io.reactivex.rxjava3:rxjava")
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.ISO_8859_1;
|
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.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME;
|
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 =
|
private final static Pattern BASE64_ENCODED_PATTERN =
|
||||||
Pattern.compile("=\\?([0-9a-zA-Z-_]+)\\?B\\?([+/0-9a-zA-Z]+=*)\\?=");
|
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 =
|
private static final String INVALID_HEADER_FIELD_PARAMETER_FORMAT =
|
||||||
"Invalid header field parameter format (as defined in RFC 5987)";
|
"Invalid header field parameter format (as defined in RFC 5987)";
|
||||||
|
|
||||||
|
|
@ -371,12 +375,20 @@ public final class ContentDisposition {
|
||||||
if (value.startsWith("=?") ) {
|
if (value.startsWith("=?") ) {
|
||||||
Matcher matcher = BASE64_ENCODED_PATTERN.matcher(value);
|
Matcher matcher = BASE64_ENCODED_PATTERN.matcher(value);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
String match1 = matcher.group(1);
|
charset = Charset.forName(matcher.group(1));
|
||||||
String match2 = matcher.group(2);
|
String encodedValue = matcher.group(2);
|
||||||
filename = new String(Base64.getDecoder().decode(match2), Charset.forName(match1));
|
filename = new String(Base64.getDecoder().decode(encodedValue), charset);
|
||||||
}
|
}
|
||||||
else {
|
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 {
|
else {
|
||||||
|
|
@ -498,6 +510,43 @@ public final class ContentDisposition {
|
||||||
c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~';
|
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) {
|
private static String escapeQuotationsInFilename(String filename) {
|
||||||
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
|
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
|
||||||
return filename;
|
return filename;
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.net.URLDecoder;
|
import java.net.URLDecoder;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
|
|
@ -29,9 +28,8 @@ import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import jakarta.mail.internet.MimeUtility;
|
|
||||||
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.ContentDisposition;
|
||||||
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpEntity;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpInputMessage;
|
import org.springframework.http.HttpInputMessage;
|
||||||
|
|
@ -526,7 +524,13 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
||||||
if (messageConverter.canWrite(partType, partContentType)) {
|
if (messageConverter.canWrite(partType, partContentType)) {
|
||||||
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
|
Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
|
||||||
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, 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()) {
|
if (!partHeaders.isEmpty()) {
|
||||||
multipartMessage.getHeaders().putAll(partHeaders);
|
multipartMessage.getHeaders().putAll(partHeaders);
|
||||||
}
|
}
|
||||||
|
|
@ -568,11 +572,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter<MultiValue
|
||||||
@Nullable
|
@Nullable
|
||||||
protected String getFilename(Object part) {
|
protected String getFilename(Object part) {
|
||||||
if (part instanceof Resource resource) {
|
if (part instanceof Resource resource) {
|
||||||
String filename = resource.getFilename();
|
return resource.getFilename();
|
||||||
if (filename != null && this.multipartCharset != null) {
|
|
||||||
filename = MimeDelegate.encode(filename, this.multipartCharset.name());
|
|
||||||
}
|
|
||||||
return filename;
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return null;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
@ -32,7 +31,6 @@ import java.util.LinkedHashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import jakarta.mail.internet.MimeUtility;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.Part;
|
import jakarta.servlet.http.Part;
|
||||||
|
|
||||||
|
|
@ -100,9 +98,6 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
|
||||||
ContentDisposition disposition = ContentDisposition.parse(headerValue);
|
ContentDisposition disposition = ContentDisposition.parse(headerValue);
|
||||||
String filename = disposition.getFilename();
|
String filename = disposition.getFilename();
|
||||||
if (filename != null) {
|
if (filename != null) {
|
||||||
if (filename.startsWith("=?") && filename.endsWith("?=")) {
|
|
||||||
filename = MimeDelegate.decode(filename);
|
|
||||||
}
|
|
||||||
files.add(part.getName(), new StandardMultipartFile(part, filename));
|
files.add(part.getName(), new StandardMultipartFile(part, filename));
|
||||||
}
|
}
|
||||||
else {
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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");
|
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
|
@Test
|
||||||
void parseEncodedFilenameWithoutCharset() {
|
void parseEncodedFilenameWithoutCharset() {
|
||||||
assertThat(parse("form-data; name=\"name\"; filename*=test.txt"))
|
assertThat(parse("form-data; name=\"name\"; filename*=test.txt"))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue