This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 57bc76af0c [FIX] EmailSubmission/set fails when underlying mail has LF
only headers (#2772)
57bc76af0c is described below
commit 57bc76af0cf3bcbe053b3ff7c762aedf090af630
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Jul 28 05:24:26 2025 +0200
[FIX] EmailSubmission/set fails when underlying mail has LF only headers
(#2772)
---
.../james/DistributedPostgresJamesServerTest.java | 2 +-
.../main/java/org/apache/james/blob/api/Store.java | 2 +
.../james/blob/memory/MemoryBlobStoreDAO.java | 13 ++
.../apache/james/blob/mail/MimeMessageStore.java | 12 +-
.../james/blob/mail/MimeMessageStoreTest.java | 132 +++++++++++++++++++++
.../james/server/core/MimeMessageWrapper.java | 22 +++-
6 files changed, 175 insertions(+), 8 deletions(-)
diff --git
a/server/apps/postgres-app/src/test/java/org/apache/james/DistributedPostgresJamesServerTest.java
b/server/apps/postgres-app/src/test/java/org/apache/james/DistributedPostgresJamesServerTest.java
index e34aaed6e2..46f466b69a 100644
---
a/server/apps/postgres-app/src/test/java/org/apache/james/DistributedPostgresJamesServerTest.java
+++
b/server/apps/postgres-app/src/test/java/org/apache/james/DistributedPostgresJamesServerTest.java
@@ -110,7 +110,7 @@ class DistributedPostgresJamesServerTest implements
JamesServerConcreteContract
int imapPort =
jamesServer.getProbe(ImapGuiceProbe.class).getImapPort();
smtpMessageSender.connect(JAMES_SERVER_HOST,
jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
.authenticate(USER, PASSWORD)
- .sendMessageWithHeaders(USER, USER, "header: toto\\r\\n\\r\\n" +
Strings.repeat("0123456789\n", 1024));
+ .sendMessageWithHeaders(USER, USER, "header: toto\r\n\r\n" +
Strings.repeat("0123456789\r\n", 1024));
AWAIT.until(() -> testIMAPClient.connect(JAMES_SERVER_HOST, imapPort)
.login(USER, PASSWORD)
.select(TestIMAPClient.INBOX)
diff --git
a/server/blob/blob-common/src/main/java/org/apache/james/blob/api/Store.java
b/server/blob/blob-common/src/main/java/org/apache/james/blob/api/Store.java
index 6f64563779..229d0925cc 100644
--- a/server/blob/blob-common/src/main/java/org/apache/james/blob/api/Store.java
+++ b/server/blob/blob-common/src/main/java/org/apache/james/blob/api/Store.java
@@ -34,6 +34,7 @@ import org.reactivestreams.Publisher;
import com.github.fge.lambdas.Throwing;
import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
import com.google.common.hash.HashCode;
import com.google.common.hash.HashFunction;
import com.google.common.io.ByteProcessor;
@@ -90,6 +91,7 @@ public interface Store<T, I> {
@Override
public Mono<I> save(T t) {
+ Preconditions.checkNotNull(t);
return Flux.fromStream(encoder.encode(t))
.flatMapSequential(this::saveEntry)
.collectMap(Tuple2::getT1, Tuple2::getT2)
diff --git
a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
index dda38b65e1..22e586191d 100644
---
a/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
+++
b/server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java
@@ -99,9 +99,22 @@ public class MemoryBlobStoreDAO implements BlobStoreDAO {
throw new ObjectStoreIOException("IOException occured", e);
}
})
+ .map(bytes -> checkContentSize(content, bytes))
.flatMap(bytes -> save(bucketName, blobId, bytes));
}
+ private static byte[] checkContentSize(ByteSource content, byte[] bytes) {
+ try {
+ long preComputedSize = content.size();
+ long realSize = bytes.length;
+ Preconditions.checkArgument(content.size() == realSize,
+ "Difference in size between the pre-computed content can cause
other blob stores to fail thus we need to test for alignment. Expecting " +
realSize + " but pre-computed size was " + preComputedSize);
+ return bytes;
+ } catch (IOException e) {
+ throw new ObjectStoreIOException("IOException occured", e);
+ }
+ }
+
@Override
public Mono<Void> delete(BucketName bucketName, BlobId blobId) {
Preconditions.checkNotNull(bucketName);
diff --git
a/server/blob/mail-store/src/main/java/org/apache/james/blob/mail/MimeMessageStore.java
b/server/blob/mail-store/src/main/java/org/apache/james/blob/mail/MimeMessageStore.java
index 8324b80184..818cb08be1 100644
---
a/server/blob/mail-store/src/main/java/org/apache/james/blob/mail/MimeMessageStore.java
+++
b/server/blob/mail-store/src/main/java/org/apache/james/blob/mail/MimeMessageStore.java
@@ -26,6 +26,7 @@ import static
org.apache.james.blob.mail.MimeMessagePartsId.HEADER_BLOB_TYPE;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.util.Map;
import java.util.UUID;
@@ -35,6 +36,7 @@ import jakarta.inject.Inject;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
+import org.apache.commons.io.output.CountingOutputStream;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.james.blob.api.BlobStore;
import org.apache.james.blob.api.BlobType;
@@ -81,6 +83,7 @@ public class MimeMessageStore {
@Override
public Stream<Pair<BlobType, Store.Impl.ValueToSave>>
encode(MimeMessage message) {
Preconditions.checkNotNull(message);
+
return Stream.of(
Pair.of(HEADER_BLOB_TYPE, (bucketName, blobStore) -> {
try {
@@ -107,7 +110,14 @@ public class MimeMessageStore {
@Override
public long size() throws IOException {
try {
- return message.getSize();
+ int size = message.getSize();
+ if (size < 0) {
+ // Size is unknown: we need to compute it
+ CountingOutputStream countingOutputStream
= new CountingOutputStream(OutputStream.nullOutputStream());
+
openStream().transferTo(countingOutputStream);
+ return countingOutputStream.getCount();
+ }
+ return size;
} catch (MessagingException e) {
throw new IOException("Failed accessing body
size", e);
}
diff --git
a/server/blob/mail-store/src/test/java/org/apache/james/blob/mail/MimeMessageStoreTest.java
b/server/blob/mail-store/src/test/java/org/apache/james/blob/mail/MimeMessageStoreTest.java
index 54351e44b7..2ecad6ba33 100644
---
a/server/blob/mail-store/src/test/java/org/apache/james/blob/mail/MimeMessageStoreTest.java
+++
b/server/blob/mail-store/src/test/java/org/apache/james/blob/mail/MimeMessageStoreTest.java
@@ -23,6 +23,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import jakarta.mail.internet.MimeMessage;
@@ -34,6 +36,8 @@ import org.apache.james.blob.api.PlainBlobId;
import org.apache.james.blob.api.Store;
import org.apache.james.blob.memory.MemoryBlobStoreFactory;
import org.apache.james.core.builder.MimeMessageBuilder;
+import org.apache.james.server.core.MimeMessageSource;
+import org.apache.james.server.core.MimeMessageWrapper;
import org.apache.james.util.MimeMessageUtil;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.BeforeEach;
@@ -102,6 +106,134 @@ class MimeMessageStoreTest {
.isInstanceOf(ObjectNotFoundException.class);
}
+ @Test
+ void shouldSupportStoringMimeMessageWrapperWithLFInHeaders() {
+ MimeMessageSource mimeMessageSource = new MimeMessageSource() {
+ private byte[] bytes = "h1: v1\nh2:
v2\r\n\r\nkrd2\r\nuhwevre\r\n".getBytes(StandardCharsets.UTF_8);
+
+ @Override
+ public String getSourceId() {
+ return "ABC";
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public long getMessageSize() {
+ return bytes.length;
+ }
+ };
+ MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
+
+ assertThatCode(() ->
testee.save(message).block()).doesNotThrowAnyException();
+ }
+
+ @Test
+ void shouldSupportStoringMimeMessageWrapperWithOnlyOneLine() {
+ MimeMessageSource mimeMessageSource = new MimeMessageSource() {
+ private byte[] bytes = "header:
toto\\r\\n\\r\\n0123456789".getBytes(StandardCharsets.UTF_8);
+
+ @Override
+ public String getSourceId() {
+ return "ABC";
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public long getMessageSize() {
+ return bytes.length;
+ }
+ };
+ MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
+
+ assertThatCode(() ->
testee.save(message).block()).doesNotThrowAnyException();
+ }
+
+ @Test
+ void shouldSupportStoringMimeMessageWrapperAfterHeaderModification()
throws Exception {
+ MimeMessageSource mimeMessageSource = new MimeMessageSource() {
+ private byte[] bytes = "h1: v1\r\nh2:
v2\r\n\r\nkrd2\r\nuhwevre\r\n".getBytes(StandardCharsets.UTF_8);
+
+ @Override
+ public String getSourceId() {
+ return "ABC";
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public long getMessageSize() {
+ return bytes.length;
+ }
+ };
+ MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
+ message.addHeader("toto", "tata");
+
+ assertThatCode(() ->
testee.save(message).block()).doesNotThrowAnyException();
+ }
+
+ @Test
+ void shouldSupportStoringMimeMessageWrapperAfterHeaderModificationAndLF()
throws Exception {
+ MimeMessageSource mimeMessageSource = new MimeMessageSource() {
+ private byte[] bytes = "h1: v1\nh2:
v2\r\n\r\nkrd2\r\nuhwevre\r\n".getBytes(StandardCharsets.UTF_8);
+
+ @Override
+ public String getSourceId() {
+ return "ABC";
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public long getMessageSize() {
+ return bytes.length;
+ }
+ };
+ MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
+ message.addHeader("toto", "tata");
+
+ assertThatCode(() ->
testee.save(message).block()).doesNotThrowAnyException();
+ }
+
+ @Test
+ void
shouldSupportStoringMimeMessageWrapperWithOnlyOneLineAbdAdditionalHeader()
throws Exception {
+ MimeMessageSource mimeMessageSource = new MimeMessageSource() {
+ private byte[] bytes = "header:
toto\\r\\n\\r\\n0123456789".getBytes(StandardCharsets.UTF_8);
+
+ @Override
+ public String getSourceId() {
+ return "ABC";
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public long getMessageSize() {
+ return bytes.length;
+ }
+ };
+ MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
+ message.addHeader("toto", "tata");
+
+ assertThatCode(() ->
testee.save(message).block()).doesNotThrowAnyException();
+ }
+
@Test
void deleteShouldNotThrowWhenCalledOnNonExistingData() throws Exception {
MimeMessagePartsId parts = MimeMessagePartsId.builder()
diff --git
a/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageWrapper.java
b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageWrapper.java
index e9d922d6f9..091959ebce 100644
---
a/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageWrapper.java
+++
b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageWrapper.java
@@ -215,6 +215,20 @@ public class MimeMessageWrapper extends MimeMessage
implements Disposable {
}
}
+ protected long loadHeadersCounting() throws MessagingException {
+ if (source != null) {
+ try (InputStream in = source.getInputStream();
+ CountingInputStream countingInputStream = new
CountingInputStream(in)) {
+ headers = createInternetHeaders(countingInputStream);
+ return countingInputStream.getCount();
+ } catch (IOException ioe) {
+ throw new MessagingException("Unable to parse headers from
stream: " + ioe.getMessage(), ioe);
+ }
+ } else {
+ throw new MessagingException("loadHeaders called for a message
with no source, contentStream or stream");
+ }
+ }
+
/**
* Load the complete MimeMessage from the internal source.
*
@@ -361,12 +375,8 @@ public class MimeMessageWrapper extends MimeMessage
implements Disposable {
if (source != null && !bodyModified) {
try {
long fullSize = source.getMessageSize();
- if (headers == null) {
- loadHeaders();
- }
- // 2 == CRLF
- return Math.max(0, (int) (fullSize - initialHeaderSize -
HEADER_BODY_SEPARATOR_SIZE));
-
+ long l = loadHeadersCounting();
+ return Math.max(0, (int) (fullSize - l));
} catch (IOException e) {
throw new MessagingException("Unable to calculate message
size");
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]