Skip to content

Commit 57bc76a

Browse files
authored
[FIX] EmailSubmission/set fails when underlying mail has LF only headers (#2772)
1 parent b174983 commit 57bc76a

File tree

6 files changed

+175
-8
lines changed

6 files changed

+175
-8
lines changed

server/apps/postgres-app/src/test/java/org/apache/james/DistributedPostgresJamesServerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ void guiceServerShouldUpdateQuota(GuiceJamesServer jamesServer) throws Exception
110110
int imapPort = jamesServer.getProbe(ImapGuiceProbe.class).getImapPort();
111111
smtpMessageSender.connect(JAMES_SERVER_HOST, jamesServer.getProbe(SmtpGuiceProbe.class).getSmtpPort())
112112
.authenticate(USER, PASSWORD)
113-
.sendMessageWithHeaders(USER, USER, "header: toto\\r\\n\\r\\n" + Strings.repeat("0123456789\n", 1024));
113+
.sendMessageWithHeaders(USER, USER, "header: toto\r\n\r\n" + Strings.repeat("0123456789\r\n", 1024));
114114
AWAIT.until(() -> testIMAPClient.connect(JAMES_SERVER_HOST, imapPort)
115115
.login(USER, PASSWORD)
116116
.select(TestIMAPClient.INBOX)

server/blob/blob-common/src/main/java/org/apache/james/blob/api/Store.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import com.github.fge.lambdas.Throwing;
3636
import com.google.common.base.Optional;
37+
import com.google.common.base.Preconditions;
3738
import com.google.common.hash.HashCode;
3839
import com.google.common.hash.HashFunction;
3940
import com.google.common.io.ByteProcessor;
@@ -90,6 +91,7 @@ public Impl(BlobPartsId.Factory<I> idFactory, Encoder<T> encoder, Decoder<T> dec
9091

9192
@Override
9293
public Mono<I> save(T t) {
94+
Preconditions.checkNotNull(t);
9395
return Flux.fromStream(encoder.encode(t))
9496
.flatMapSequential(this::saveEntry)
9597
.collectMap(Tuple2::getT1, Tuple2::getT2)

server/blob/blob-memory/src/main/java/org/apache/james/blob/memory/MemoryBlobStoreDAO.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,22 @@ public Mono<Void> save(BucketName bucketName, BlobId blobId, ByteSource content)
9999
throw new ObjectStoreIOException("IOException occured", e);
100100
}
101101
})
102+
.map(bytes -> checkContentSize(content, bytes))
102103
.flatMap(bytes -> save(bucketName, blobId, bytes));
103104
}
104105

106+
private static byte[] checkContentSize(ByteSource content, byte[] bytes) {
107+
try {
108+
long preComputedSize = content.size();
109+
long realSize = bytes.length;
110+
Preconditions.checkArgument(content.size() == realSize,
111+
"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);
112+
return bytes;
113+
} catch (IOException e) {
114+
throw new ObjectStoreIOException("IOException occured", e);
115+
}
116+
}
117+
105118
@Override
106119
public Mono<Void> delete(BucketName bucketName, BlobId blobId) {
107120
Preconditions.checkNotNull(bucketName);

server/blob/mail-store/src/main/java/org/apache/james/blob/mail/MimeMessageStore.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import java.io.IOException;
2828
import java.io.InputStream;
29+
import java.io.OutputStream;
2930
import java.io.SequenceInputStream;
3031
import java.util.Map;
3132
import java.util.UUID;
@@ -35,6 +36,7 @@
3536
import jakarta.mail.MessagingException;
3637
import jakarta.mail.internet.MimeMessage;
3738

39+
import org.apache.commons.io.output.CountingOutputStream;
3840
import org.apache.commons.lang3.tuple.Pair;
3941
import org.apache.james.blob.api.BlobStore;
4042
import org.apache.james.blob.api.BlobType;
@@ -81,6 +83,7 @@ static class MimeMessageEncoder implements Store.Impl.Encoder<MimeMessage> {
8183
@Override
8284
public Stream<Pair<BlobType, Store.Impl.ValueToSave>> encode(MimeMessage message) {
8385
Preconditions.checkNotNull(message);
86+
8487
return Stream.of(
8588
Pair.of(HEADER_BLOB_TYPE, (bucketName, blobStore) -> {
8689
try {
@@ -107,7 +110,14 @@ public InputStream openStream() throws IOException {
107110
@Override
108111
public long size() throws IOException {
109112
try {
110-
return message.getSize();
113+
int size = message.getSize();
114+
if (size < 0) {
115+
// Size is unknown: we need to compute it
116+
CountingOutputStream countingOutputStream = new CountingOutputStream(OutputStream.nullOutputStream());
117+
openStream().transferTo(countingOutputStream);
118+
return countingOutputStream.getCount();
119+
}
120+
return size;
111121
} catch (MessagingException e) {
112122
throw new IOException("Failed accessing body size", e);
113123
}

server/blob/mail-store/src/test/java/org/apache/james/blob/mail/MimeMessageStoreTest.java

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import static org.assertj.core.api.Assertions.assertThatCode;
2424
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2525

26+
import java.io.ByteArrayInputStream;
27+
import java.io.InputStream;
2628
import java.nio.charset.StandardCharsets;
2729

2830
import jakarta.mail.internet.MimeMessage;
@@ -34,6 +36,8 @@
3436
import org.apache.james.blob.api.Store;
3537
import org.apache.james.blob.memory.MemoryBlobStoreFactory;
3638
import org.apache.james.core.builder.MimeMessageBuilder;
39+
import org.apache.james.server.core.MimeMessageSource;
40+
import org.apache.james.server.core.MimeMessageWrapper;
3741
import org.apache.james.util.MimeMessageUtil;
3842
import org.assertj.core.api.SoftAssertions;
3943
import org.junit.jupiter.api.BeforeEach;
@@ -102,6 +106,134 @@ void readShouldNotReturnDeletedMessage() throws Exception {
102106
.isInstanceOf(ObjectNotFoundException.class);
103107
}
104108

109+
@Test
110+
void shouldSupportStoringMimeMessageWrapperWithLFInHeaders() {
111+
MimeMessageSource mimeMessageSource = new MimeMessageSource() {
112+
private byte[] bytes = "h1: v1\nh2: v2\r\n\r\nkrd2\r\nuhwevre\r\n".getBytes(StandardCharsets.UTF_8);
113+
114+
@Override
115+
public String getSourceId() {
116+
return "ABC";
117+
}
118+
119+
@Override
120+
public InputStream getInputStream() {
121+
return new ByteArrayInputStream(bytes);
122+
}
123+
124+
@Override
125+
public long getMessageSize() {
126+
return bytes.length;
127+
}
128+
};
129+
MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
130+
131+
assertThatCode(() -> testee.save(message).block()).doesNotThrowAnyException();
132+
}
133+
134+
@Test
135+
void shouldSupportStoringMimeMessageWrapperWithOnlyOneLine() {
136+
MimeMessageSource mimeMessageSource = new MimeMessageSource() {
137+
private byte[] bytes = "header: toto\\r\\n\\r\\n0123456789".getBytes(StandardCharsets.UTF_8);
138+
139+
@Override
140+
public String getSourceId() {
141+
return "ABC";
142+
}
143+
144+
@Override
145+
public InputStream getInputStream() {
146+
return new ByteArrayInputStream(bytes);
147+
}
148+
149+
@Override
150+
public long getMessageSize() {
151+
return bytes.length;
152+
}
153+
};
154+
MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
155+
156+
assertThatCode(() -> testee.save(message).block()).doesNotThrowAnyException();
157+
}
158+
159+
@Test
160+
void shouldSupportStoringMimeMessageWrapperAfterHeaderModification() throws Exception {
161+
MimeMessageSource mimeMessageSource = new MimeMessageSource() {
162+
private byte[] bytes = "h1: v1\r\nh2: v2\r\n\r\nkrd2\r\nuhwevre\r\n".getBytes(StandardCharsets.UTF_8);
163+
164+
@Override
165+
public String getSourceId() {
166+
return "ABC";
167+
}
168+
169+
@Override
170+
public InputStream getInputStream() {
171+
return new ByteArrayInputStream(bytes);
172+
}
173+
174+
@Override
175+
public long getMessageSize() {
176+
return bytes.length;
177+
}
178+
};
179+
MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
180+
message.addHeader("toto", "tata");
181+
182+
assertThatCode(() -> testee.save(message).block()).doesNotThrowAnyException();
183+
}
184+
185+
@Test
186+
void shouldSupportStoringMimeMessageWrapperAfterHeaderModificationAndLF() throws Exception {
187+
MimeMessageSource mimeMessageSource = new MimeMessageSource() {
188+
private byte[] bytes = "h1: v1\nh2: v2\r\n\r\nkrd2\r\nuhwevre\r\n".getBytes(StandardCharsets.UTF_8);
189+
190+
@Override
191+
public String getSourceId() {
192+
return "ABC";
193+
}
194+
195+
@Override
196+
public InputStream getInputStream() {
197+
return new ByteArrayInputStream(bytes);
198+
}
199+
200+
@Override
201+
public long getMessageSize() {
202+
return bytes.length;
203+
}
204+
};
205+
MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
206+
message.addHeader("toto", "tata");
207+
208+
assertThatCode(() -> testee.save(message).block()).doesNotThrowAnyException();
209+
}
210+
211+
@Test
212+
void shouldSupportStoringMimeMessageWrapperWithOnlyOneLineAbdAdditionalHeader() throws Exception {
213+
MimeMessageSource mimeMessageSource = new MimeMessageSource() {
214+
private byte[] bytes = "header: toto\\r\\n\\r\\n0123456789".getBytes(StandardCharsets.UTF_8);
215+
216+
@Override
217+
public String getSourceId() {
218+
return "ABC";
219+
}
220+
221+
@Override
222+
public InputStream getInputStream() {
223+
return new ByteArrayInputStream(bytes);
224+
}
225+
226+
@Override
227+
public long getMessageSize() {
228+
return bytes.length;
229+
}
230+
};
231+
MimeMessage message = new MimeMessageWrapper(mimeMessageSource);
232+
message.addHeader("toto", "tata");
233+
234+
assertThatCode(() -> testee.save(message).block()).doesNotThrowAnyException();
235+
}
236+
105237
@Test
106238
void deleteShouldNotThrowWhenCalledOnNonExistingData() throws Exception {
107239
MimeMessagePartsId parts = MimeMessagePartsId.builder()

server/container/core/src/main/java/org/apache/james/server/core/MimeMessageWrapper.java

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,20 @@ protected void loadHeaders() throws MessagingException {
215215
}
216216
}
217217

218+
protected long loadHeadersCounting() throws MessagingException {
219+
if (source != null) {
220+
try (InputStream in = source.getInputStream();
221+
CountingInputStream countingInputStream = new CountingInputStream(in)) {
222+
headers = createInternetHeaders(countingInputStream);
223+
return countingInputStream.getCount();
224+
} catch (IOException ioe) {
225+
throw new MessagingException("Unable to parse headers from stream: " + ioe.getMessage(), ioe);
226+
}
227+
} else {
228+
throw new MessagingException("loadHeaders called for a message with no source, contentStream or stream");
229+
}
230+
}
231+
218232
/**
219233
* Load the complete MimeMessage from the internal source.
220234
*
@@ -361,12 +375,8 @@ public int getSize() throws MessagingException {
361375
if (source != null && !bodyModified) {
362376
try {
363377
long fullSize = source.getMessageSize();
364-
if (headers == null) {
365-
loadHeaders();
366-
}
367-
// 2 == CRLF
368-
return Math.max(0, (int) (fullSize - initialHeaderSize - HEADER_BODY_SEPARATOR_SIZE));
369-
378+
long l = loadHeadersCounting();
379+
return Math.max(0, (int) (fullSize - l));
370380
} catch (IOException e) {
371381
throw new MessagingException("Unable to calculate message size");
372382
}

0 commit comments

Comments
 (0)