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 <btell...@linagora.com> 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: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org