You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2024/02/26 04:03:52 UTC
(james-project) branch master updated: JAMES-3995 Optimize Email/get
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 aee6550277 JAMES-3995 Optimize Email/get
aee6550277 is described below
commit aee6550277db4e31099867e1d29c7598f9a4dad9
Author: Benoit TELLIER <bt...@linagora.com>
AuthorDate: Thu Feb 22 22:47:26 2024 +0100
JAMES-3995 Optimize Email/get
---
.../org/apache/james/jmap/api/model/Preview.java | 2 +
.../AvoidBinaryBodyBufferingBodyFactory.java | 366 +++++++++++++++++++++
.../apache/james/jmap/mime4j/FakeBinaryBody.java | 64 ++++
.../org/apache/james/jmap/mime4j/SizeUtils.java | 152 +++++++++
.../ComputeMessageFastViewProjectionListener.java | 4 +-
.../src/main/resources/eml/nested2.eml | 77 +++++
.../rfc8621/contract/EmailGetMethodContract.scala | 63 +++-
.../contract/EmailParseMethodContract.scala | 2 +-
.../scala/org/apache/james/jmap/mail/Email.scala | 6 +-
.../org/apache/james/jmap/mail/EmailBodyPart.scala | 17 +-
10 files changed, 734 insertions(+), 19 deletions(-)
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Preview.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Preview.java
index 5474b6211c..2ef3ecc876 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Preview.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/model/Preview.java
@@ -29,6 +29,7 @@ import java.util.Objects;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
+import org.apache.james.jmap.mime4j.AvoidBinaryBodyBufferingBodyFactory;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.model.MessageResult;
import org.apache.james.mime4j.dom.Message;
@@ -81,6 +82,7 @@ public class Preview {
private Message parse(InputStream inputStream) throws IOException {
DefaultMessageBuilder defaultMessageBuilder = new DefaultMessageBuilder();
defaultMessageBuilder.setMimeEntityConfig(MimeConfig.PERMISSIVE);
+ defaultMessageBuilder.setBodyFactory(new AvoidBinaryBodyBufferingBodyFactory());
return defaultMessageBuilder.parseMessage(inputStream);
}
}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/mime4j/AvoidBinaryBodyBufferingBodyFactory.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/mime4j/AvoidBinaryBodyBufferingBodyFactory.java
new file mode 100644
index 0000000000..42abd42f2d
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/mime4j/AvoidBinaryBodyBufferingBodyFactory.java
@@ -0,0 +1,366 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.mime4j;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+
+import org.apache.james.mime4j.Charsets;
+import org.apache.james.mime4j.dom.BinaryBody;
+import org.apache.james.mime4j.dom.SingleBody;
+import org.apache.james.mime4j.dom.TextBody;
+import org.apache.james.mime4j.io.InputStreams;
+import org.apache.james.mime4j.message.BasicBodyFactory;
+import org.apache.james.mime4j.message.BodyFactory;
+import org.apache.james.mime4j.util.ByteArrayOutputStreamRecycler;
+import org.apache.james.mime4j.util.ContentUtil;
+
+import com.google.common.io.CountingOutputStream;
+
+/**
+ * Factory for creating message bodies.
+ */
+public class AvoidBinaryBodyBufferingBodyFactory implements BodyFactory {
+
+ public static final BasicBodyFactory INSTANCE = new BasicBodyFactory();
+
+ private final Charset defaultCharset;
+
+ public AvoidBinaryBodyBufferingBodyFactory() {
+ this(true);
+ }
+
+ public AvoidBinaryBodyBufferingBodyFactory(final Charset defaultCharset) {
+ this.defaultCharset = defaultCharset;
+ }
+
+ public AvoidBinaryBodyBufferingBodyFactory(final boolean lenient) {
+ this(lenient ? Charset.defaultCharset() : null);
+ }
+
+ /**
+ * @return the defaultCharset
+ */
+ public Charset getDefaultCharset() {
+ return defaultCharset;
+ }
+
+ /**
+ * <p>
+ * Select the Charset for the given <code>mimeCharset</code> string.
+ * </p>
+ * <p>
+ * If you need support for non standard or invalid <code>mimeCharset</code> specifications you might want to
+ * create your own derived {@link BodyFactory} extending {@link BasicBodyFactory} and overriding this method as
+ * suggested by <a href="https://issues.apache.org/jira/browse/MIME4J-218">MIME4J-218</a>
+ * </p>
+ * <p>
+ * The default behavior is lenient, invalid <code>mimeCharset</code> specifications will return the
+ * <code>defaultCharset</code>.
+ * </p>
+ *
+ * @param mimeCharset - the string specification for a Charset e.g. "UTF-8"
+ * @throws UnsupportedEncodingException if the mimeCharset is invalid
+ */
+ protected Charset resolveCharset(final String mimeCharset) throws UnsupportedEncodingException {
+ if (mimeCharset != null) {
+ try {
+ return Charset.forName(mimeCharset);
+ } catch (UnsupportedCharsetException ex) {
+ if (defaultCharset == null) {
+ throw new UnsupportedEncodingException(mimeCharset);
+ }
+ } catch (IllegalCharsetNameException ex) {
+ if (defaultCharset == null) {
+ throw new UnsupportedEncodingException(mimeCharset);
+ }
+ }
+ }
+ return defaultCharset;
+ }
+
+ public TextBody textBody(final String text, final String mimeCharset) throws UnsupportedEncodingException {
+ if (text == null) {
+ throw new IllegalArgumentException("Text may not be null");
+ }
+ return new StringBody1(text, resolveCharset(mimeCharset));
+ }
+
+ public TextBody textBody(final byte[] content, final Charset charset) {
+ if (content == null) {
+ throw new IllegalArgumentException("Content may not be null");
+ }
+ return new StringBody2(content, charset);
+ }
+
+ public TextBody textBody(final InputStream content, final String mimeCharset) throws IOException {
+ if (content == null) {
+ throw new IllegalArgumentException("Input stream may not be null");
+ }
+ return new StringBody3(ContentUtil.bufferEfficient(content), resolveCharset(mimeCharset));
+ }
+
+ public TextBody textBody(final String text, final Charset charset) {
+ if (text == null) {
+ throw new IllegalArgumentException("Text may not be null");
+ }
+ return new StringBody1(text, charset);
+ }
+
+ public TextBody textBody(final String text) {
+ return textBody(text, Charsets.DEFAULT_CHARSET);
+ }
+
+ public BinaryBody binaryBody(final String content, final Charset charset) {
+ if (content == null) {
+ throw new IllegalArgumentException("Content may not be null");
+ }
+ return new BinaryBody2(content, charset);
+ }
+
+ public BinaryBody binaryBody(final InputStream is) throws IOException {
+ CountingOutputStream out = new CountingOutputStream(OutputStream.nullOutputStream());
+ is.transferTo(out);
+ return new FakeBinaryBody(out.getCount());
+ }
+
+ public BinaryBody binaryBody(final byte[] buf) {
+ return new BinaryBody1(buf);
+ }
+
+ static class StringBody1 extends TextBody {
+
+ private final String content;
+ private final Charset charset;
+
+ StringBody1(final String content, final Charset charset) {
+ super();
+ this.content = content;
+ this.charset = charset;
+ }
+
+ @Override
+ public String getMimeCharset() {
+ return this.charset != null ? this.charset.name() : null;
+ }
+
+ @Override
+ public Charset getCharset() {
+ return charset;
+ }
+
+ @Override
+ public Reader getReader() throws IOException {
+ return new StringReader(this.content);
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return InputStreams.create(this.content,
+ this.charset != null ? this.charset : Charsets.DEFAULT_CHARSET);
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public SingleBody copy() {
+ return new StringBody1(this.content, this.charset);
+ }
+
+ }
+
+ static class StringBody2 extends TextBody {
+
+ private final byte[] content;
+ private final Charset charset;
+
+ StringBody2(final byte[] content, final Charset charset) {
+ super();
+ this.content = content;
+ this.charset = charset;
+ }
+
+ @Override
+ public String getMimeCharset() {
+ return this.charset != null ? this.charset.name() : null;
+ }
+
+ @Override
+ public Charset getCharset() {
+ return charset;
+ }
+
+ @Override
+ public Reader getReader() throws IOException {
+ return new InputStreamReader(InputStreams.create(this.content), this.charset);
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return InputStreams.create(this.content);
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ out.write(content);
+ }
+
+ @Override
+ public long size() {
+ return content.length;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public SingleBody copy() {
+ return new StringBody2(this.content, this.charset);
+ }
+
+ }
+
+ static class StringBody3 extends TextBody {
+
+ private final ByteArrayOutputStreamRecycler.Wrapper content;
+ private final Charset charset;
+
+ StringBody3(final ByteArrayOutputStreamRecycler.Wrapper content, final Charset charset) {
+ super();
+ this.content = content;
+ this.charset = charset;
+ }
+
+ @Override
+ public String getMimeCharset() {
+ return this.charset != null ? this.charset.name() : null;
+ }
+
+ @Override
+ public Charset getCharset() {
+ return charset;
+ }
+
+ @Override
+ public Reader getReader() throws IOException {
+ return new InputStreamReader(this.content.getValue().toInputStream(), this.charset);
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return this.content.getValue().toInputStream();
+ }
+
+ @Override
+ public long size() {
+ return content.getValue().size();
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ content.getValue().writeTo(out);
+ }
+
+ @Override
+ public void dispose() {
+ this.content.release();
+ }
+
+ @Override
+ public SingleBody copy() {
+ return new StringBody3(this.content, this.charset);
+ }
+
+ }
+
+ static class BinaryBody2 extends BinaryBody {
+
+ private final String content;
+ private final Charset charset;
+
+ BinaryBody2(final String content, final Charset charset) {
+ super();
+ this.content = content;
+ this.charset = charset;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return InputStreams.create(this.content,
+ this.charset != null ? this.charset : Charsets.DEFAULT_CHARSET);
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public SingleBody copy() {
+ return new BinaryBody2(this.content, this.charset);
+ }
+
+ }
+
+ static class BinaryBody1 extends BinaryBody {
+
+ private final byte[] content;
+
+ BinaryBody1(final byte[] content) {
+ super();
+ this.content = content;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return InputStreams.create(this.content);
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ out.write(content);
+ }
+
+ @Override
+ public long size() {
+ return content.length;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public SingleBody copy() {
+ return new BinaryBody1(this.content);
+ }
+
+ }
+}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/mime4j/FakeBinaryBody.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/mime4j/FakeBinaryBody.java
new file mode 100644
index 0000000000..687567448e
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/mime4j/FakeBinaryBody.java
@@ -0,0 +1,64 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.mime4j;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.commons.lang3.NotImplementedException;
+import org.apache.james.mime4j.dom.BinaryBody;
+import org.apache.james.mime4j.dom.SingleBody;
+
+public class FakeBinaryBody extends BinaryBody {
+ private final long size;
+
+ FakeBinaryBody(long size) {
+ this.size = size;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public long size() {
+ return size;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+ @Override
+ public SingleBody copy() {
+ return new FakeBinaryBody(size);
+ }
+}
diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/mime4j/SizeUtils.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/mime4j/SizeUtils.java
new file mode 100644
index 0000000000..0a85a3357b
--- /dev/null
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/mime4j/SizeUtils.java
@@ -0,0 +1,152 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.mime4j;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.james.mime4j.dom.Body;
+import org.apache.james.mime4j.dom.Entity;
+import org.apache.james.mime4j.dom.Header;
+import org.apache.james.mime4j.dom.Message;
+import org.apache.james.mime4j.dom.Multipart;
+import org.apache.james.mime4j.dom.SingleBody;
+import org.apache.james.mime4j.dom.field.ContentTypeField;
+import org.apache.james.mime4j.dom.field.FieldName;
+import org.apache.james.mime4j.message.BodyPart;
+import org.apache.james.mime4j.message.DefaultMessageWriter;
+import org.apache.james.mime4j.message.MessageImpl;
+import org.apache.james.mime4j.message.MultipartImpl;
+import org.apache.james.mime4j.util.ByteSequence;
+import org.apache.james.mime4j.util.ContentUtil;
+
+import com.google.common.io.CountingOutputStream;
+
+public class SizeUtils {
+ public static long sizeOf(Entity entity) throws IOException {
+ if (entity instanceof BodyPart) {
+ BodyPart bodyPart = (BodyPart) entity;
+
+ return sizeOf(bodyPart.getBody());
+ }
+ if (entity instanceof MessageImpl) {
+ MessageImpl bodyPart = (MessageImpl) entity;
+
+ return sizeOf(bodyPart.getBody());
+ }
+ CountingOutputStream countingOutputStream = new CountingOutputStream(OutputStream.nullOutputStream());
+ DefaultMessageWriter defaultMessageWriter = new DefaultMessageWriter();
+ defaultMessageWriter.writeEntity(entity, countingOutputStream);
+ return countingOutputStream.getCount();
+ }
+
+ public static long sizeOf(Header header) throws IOException {
+ CountingOutputStream countingOutputStream = new CountingOutputStream(OutputStream.nullOutputStream());
+ DefaultMessageWriter defaultMessageWriter = new DefaultMessageWriter();
+ defaultMessageWriter.writeHeader(header, countingOutputStream);
+ return countingOutputStream.getCount();
+ }
+
+ public static long sizeOf(Body body) throws IOException {
+ if (body instanceof FakeBinaryBody) {
+ return ((FakeBinaryBody) body).getSize();
+ }
+ if (body instanceof SingleBody) {
+ return ((SingleBody) body).size();
+ }
+ if (body instanceof Multipart) {
+ return sizeOfMultipart((Multipart) body);
+ }
+ if (body instanceof Message) {
+ Message message = (Message) body;
+ return sizeOf(message.getHeader()) + sizeOf(message.getBody());
+ }
+ CountingOutputStream countingOutputStream = new CountingOutputStream(OutputStream.nullOutputStream());
+ DefaultMessageWriter defaultMessageWriter = new DefaultMessageWriter();
+ defaultMessageWriter.writeBody(body, countingOutputStream);
+ return countingOutputStream.getCount();
+ }
+
+ // Inspired from DefaultMessageWriter
+ public static long sizeOfMultipart(Multipart multipart)
+ throws IOException {
+ long result = 0;
+ ContentTypeField contentType = getContentType(multipart);
+
+ ByteSequence boundary = getBoundary(contentType);
+
+ ByteSequence preamble;
+ ByteSequence epilogue;
+ if (multipart instanceof MultipartImpl) {
+ preamble = ((MultipartImpl) multipart).getPreambleRaw();
+ epilogue = ((MultipartImpl) multipart).getEpilogueRaw();
+ } else {
+ preamble = multipart.getPreamble() != null ? ContentUtil.encode(multipart.getPreamble()) : null;
+ epilogue = multipart.getEpilogue() != null ? ContentUtil.encode(multipart.getEpilogue()) : null;
+ }
+ if (preamble != null) {
+ result += preamble.length() + 2;
+ }
+
+ for (Entity bodyPart : multipart.getBodyParts()) {
+ result += 2 + boundary.length() + 2; // -- boudary CRLF
+ result += sizeOf(bodyPart.getHeader());
+ result += sizeOf(bodyPart);
+ result += 2; // CRLF
+ }
+
+ result += 2 + boundary.length() + 2 + 2; // -- boudary -- CRLF
+ if (epilogue != null) {
+ result += epilogue.length();
+ }
+ return result;
+ }
+
+
+ // Taken from DefaultMessageWriter
+ private static ContentTypeField getContentType(Multipart multipart) {
+ Entity parent = multipart.getParent();
+ if (parent == null) {
+ throw new IllegalArgumentException("Missing parent entity in multipart");
+ }
+
+ Header header = parent.getHeader();
+ if (header == null) {
+ throw new IllegalArgumentException("Missing header in parent entity");
+ }
+
+ ContentTypeField contentType = (ContentTypeField) header
+ .getField(FieldName.CONTENT_TYPE_LOWERCASE);
+ if (contentType == null) {
+ throw new IllegalArgumentException("Content-Type field not specified");
+ }
+
+ return contentType;
+ }
+
+ // Taken from DefaultMessageWriter
+ private static ByteSequence getBoundary(ContentTypeField contentType) {
+ String boundary = contentType.getBoundary();
+ if (boundary == null) {
+ throw new IllegalArgumentException("Multipart boundary not specified. Mime-Type: " + contentType.getMimeType() + ", Raw: " + contentType.toString());
+ }
+ return ContentUtil.encode(boundary);
+ }
+}
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListener.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListener.java
index c75f47120b..3a0c7941df 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListener.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListener.java
@@ -48,6 +48,7 @@ import com.google.common.collect.ImmutableSet;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
public class ComputeMessageFastViewProjectionListener implements EventListener.ReactiveGroupEventListener {
public static class ComputeMessageFastViewProjectionListenerGroup extends Group {
@@ -103,7 +104,8 @@ public class ComputeMessageFastViewProjectionListener implements EventListener.R
return Flux.from(messageIdManager.getMessagesReactive(addedEvent.getMessageIds(), FetchGroup.FULL_CONTENT, session))
.flatMap(Throwing.function(messageResult -> Mono.fromCallable(
() -> Pair.of(messageResult.getMessageId(),
- computeFastViewPrecomputedProperties(messageResult)))), DEFAULT_CONCURRENCY)
+ computeFastViewPrecomputedProperties(messageResult)))
+ .subscribeOn(Schedulers.parallel())), DEFAULT_CONCURRENCY)
.flatMap(message -> messageFastViewProjection.store(message.getKey(), message.getValue()), DEFAULT_CONCURRENCY)
.then();
}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/nested2.eml b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/nested2.eml
new file mode 100644
index 0000000000..7dc18bee6e
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/nested2.eml
@@ -0,0 +1,77 @@
+Content-Type: multipart/mixed; boundary="------------6a0Tq7Y8z9M0dIG10PnZ7l7E"
+Message-ID: <b3...@linagora.com>
+Date: Wed, 21 Feb 2024 22:58:55 +0100
+MIME-Version: 1.0
+References: <Mi...@linagora.com>
+Subject: Fwd: Will be nested
+To: btellier@linagora.com
+From: "btellier@linagora.com" <bt...@linagora.com>
+In-Reply-To: <Mi...@linagora.com>
+X-Forwarded-Message-Id: <Mi...@linagora.com>
+
+This is a multi-part message in MIME format.
+--------------6a0Tq7Y8z9M0dIG10PnZ7l7E
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+
+--------------6a0Tq7Y8z9M0dIG10PnZ7l7E
+Content-Type: message/rfc822; name="Will be nested.eml"
+Content-Disposition: attachment; filename="Will be nested.eml"
+Content-Transfer-Encoding: 7bit
+
+MIME-Version: 1.0
+Subject: Will be nested
+From: Benoit TELLIER <bx...@linagora.com>
+To: =?ISO-8859-1?Q?Beno=EEt_TELLIER?= <bx...@linagora.com>
+Reply-To: btellier@linagora.com
+Date: Wed, 21 Feb 2024 21:58:17 +0000
+Message-ID: <Mi...@linagora.com>
+Content-Type: multipart/mixed;
+ boundary="-=Part.8f5.4ee8b8343a133e4f.18dcdad38b0.f1b9fcef27eb8231=-"
+
+---=Part.8f5.4ee8b8343a133e4f.18dcdad38b0.f1b9fcef27eb8231=-
+Content-Type: multipart/alternative;
+ boundary="-=Part.8f4.9ab3439f1c0c1361.18dcdad38b0.b6d054e4dd1744c7=-"
+
+---=Part.8f4.9ab3439f1c0c1361.18dcdad38b0.b6d054e4dd1744c7=-
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+Nested body
+
+--=C2=A0
+
+Best regards,
+
+Benoit TELLIER
+
+---=Part.8f4.9ab3439f1c0c1361.18dcdad38b0.b6d054e4dd1744c7=-
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+<div>Nested body<br><br></div><div class=3D"tmail-signature" style=3D"displ=
+ay: block;"><span class=3D"tmail_signature_prefix">-- </span><br><br>B=
+est regards,<br><br><b>Benoit TELLIER</b><br><br>General manager of <a href=
+=3D"https://linagora=2Evn">Linagora VIETNAM</a>=2E<br>Product owner for <a =
+href=3D"https://github=2Ecom/linagora/tmail-flutter">Team-Mail</a> product=
+=2E<br>Chairman of the <a href=3D"https://james=2Eapache=2Eorg/">Apache Jam=
+es project</a>=2E<br><br>Mail: <a href=3D"mailto:btellier@linagora=2Ecom">b=
+tellier@linagora=2Ecom</a><br>Tel: (0033) 6 77 26 04 58 (WhatsApp, Signal)<=
+br><br><br></div>
+---=Part.8f4.9ab3439f1c0c1361.18dcdad38b0.b6d054e4dd1744c7=---
+
+---=Part.8f5.4ee8b8343a133e4f.18dcdad38b0.f1b9fcef27eb8231=-
+Content-Type: application/octet-stream;
+ name="=?US-ASCII?Q?signal-desktop-keyring.gpg?="
+Content-Disposition: attachment
+Content-Transfer-Encoding: base64
+
+mQINBFjlSicBEACgho//0EzxuvuCn01LwFqGAgwPKcSSl4L+AWws5/YbsZZvmTBkggIiVOCIMh+d
+3cmGu5W3ydaeUbWbFGNsxO44EB5YBZcuLa5EzRKbNPVaOXKXmhp+w0mEbkoKbF+3mz3lifwBnzcB
+pukyJDgcJSq8cXfq5JsDPR1KAL6ph/kwKeiDNg+8oFgqfboukK56yPTYc9iM8hkTFdx9L6JCJaZG
+XME/ru6EZofUFxeVdev5+9ztYJBBZCGMug5Xp3Gxh/9JUWi6F1+9qAyzN+O606NOXLwcmq5KZL0g
+
+---=Part.8f5.4ee8b8343a133e4f.18dcdad38b0.f1b9fcef27eb8231=---
+
+--------------6a0Tq7Y8z9M0dIG10PnZ7l7E--
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
index 1a90ecf267..aa1c7f45e9 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
@@ -3254,6 +3254,67 @@ trait EmailGetMethodContract {
|}""".stripMargin)
}
+ @Test
+ def shouldSupportAttachedMessageWithComplexMultipart(server: GuiceJamesServer): Unit = {
+ val path = MailboxPath.inbox(BOB)
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+ val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, path, AppendCommand.from(
+ ClassLoaderUtils.getSystemResourceAsSharedStream("eml/nested2.eml")))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Email/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${messageId.serialize}"],
+ | "properties":["bodyStructure"],
+ | "bodyProperties":["partId", "blobId", "subParts","size", "type"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].state")
+ .inPath("methodResponses[0][1].list[0].bodyStructure")
+ .isEqualTo(
+ s"""{
+ | "subParts": [
+ | {
+ | "size": 0,
+ | "partId": "2",
+ | "blobId": "${messageId.serialize()}_2",
+ | "type": "text/plain"
+ | },
+ | {
+ | "size": 2093,
+ | "partId": "3",
+ | "blobId": "${messageId.serialize()}_3",
+ | "type": "message/rfc822"
+ | }
+ | ],
+ | "size": 2513,
+ | "partId": "1",
+ | "type": "multipart/mixed"
+ | }""".stripMargin)
+ }
+
@Test
def mailboxIdsPropertiesShouldBeReturned(server: GuiceJamesServer): Unit = {
val path = MailboxPath.inbox(BOB)
@@ -3866,7 +3927,7 @@ trait EmailGetMethodContract {
| "value": "$contentType"
| }
| ],
- | "size": 2287,
+ | "size": 1880,
| "type": "multipart/mixed",
| "charset": "us-ascii",
| "subParts": [
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailParseMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailParseMethodContract.scala
index be5fa494aa..59d934eb92 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailParseMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailParseMethodContract.scala
@@ -327,7 +327,7 @@ trait EmailParseMethodContract {
| }
| ],
| "subject": "test subject",
- | "size": 807,
+ | "size": 797,
| "blobId": "${messageId.serialize()}_3",
| "preview": "test body",
| "messageId": [
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index 334f3bb82a..82c51a7633 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -44,7 +44,7 @@ import org.apache.james.jmap.mail.EmailHeaderName.{ADDRESSES_NAMES, DATE, MESSAG
import org.apache.james.jmap.mail.FastViewWithAttachmentsMetadataReadLevel.supportedByFastViewWithAttachments
import org.apache.james.jmap.mail.KeywordsFactory.LENIENT_KEYWORDS_FACTORY
import org.apache.james.jmap.method.ZoneIdProvider
-import org.apache.james.jmap.mime4j.JamesBodyDescriptorBuilder
+import org.apache.james.jmap.mime4j.{JamesBodyDescriptorBuilder, AvoidBinaryBodyBufferingBodyFactory}
import org.apache.james.mailbox.model.FetchGroup.{FULL_CONTENT, HEADERS, HEADERS_WITH_ATTACHMENTS_METADATA, MINIMAL}
import org.apache.james.mailbox.model.{FetchGroup, MailboxId, MessageId, MessageResult, ThreadId => JavaThreadId}
import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
@@ -52,7 +52,7 @@ import org.apache.james.mime4j.codec.DecodeMonitor
import org.apache.james.mime4j.dom.field.{AddressListField, DateTimeField, MailboxField, MailboxListField}
import org.apache.james.mime4j.dom.{Header, Message}
import org.apache.james.mime4j.field.{AddressListFieldLenientImpl, LenientFieldParser}
-import org.apache.james.mime4j.message.{BasicBodyFactory, DefaultMessageBuilder}
+import org.apache.james.mime4j.message.DefaultMessageBuilder
import org.apache.james.mime4j.stream.{Field, MimeConfig, RawFieldParser}
import org.apache.james.mime4j.util.MimeUtil
import org.apache.james.util.AuditTrail
@@ -138,7 +138,7 @@ object Email {
defaultMessageBuilder.setMimeEntityConfig(MimeConfig.PERMISSIVE)
defaultMessageBuilder.setDecodeMonitor(DecodeMonitor.SILENT)
defaultMessageBuilder.setBodyDescriptorBuilder(new JamesBodyDescriptorBuilder(null, LenientFieldParser.getParser, DecodeMonitor.SILENT))
- defaultMessageBuilder.setBodyFactory(new BasicBodyFactory(defaultCharset))
+ defaultMessageBuilder.setBodyFactory(new AvoidBinaryBodyBufferingBodyFactory(defaultCharset))
val resultMessage = Try(defaultMessageBuilder.parseMessage(inputStream))
resultMessage.fold(e => {
Try(inputStream.close())
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
index cda256d791..28ecc58c62 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
@@ -23,7 +23,6 @@ import java.io.OutputStream
import java.time.ZoneId
import cats.implicits._
-import com.google.common.io.CountingOutputStream
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric.NonNegative
@@ -34,14 +33,14 @@ import org.apache.james.jmap.api.model.Size.Size
import org.apache.james.jmap.core.Properties
import org.apache.james.jmap.mail.EmailBodyPart.{FILENAME_PREFIX, MDN_TYPE, MULTIPART_ALTERNATIVE, TEXT_HTML, TEXT_PLAIN, of}
import org.apache.james.jmap.mail.PartId.PartIdValue
-import org.apache.james.jmap.mime4j.JamesBodyDescriptorBuilder
+import org.apache.james.jmap.mime4j.{ JamesBodyDescriptorBuilder, SizeUtils}
import org.apache.james.mailbox.model.{Cid, MessageAttachmentMetadata, MessageResult}
import org.apache.james.mime4j.Charsets.ISO_8859_1
import org.apache.james.mime4j.codec.{DecodeMonitor, DecoderUtil}
import org.apache.james.mime4j.dom.field.{ContentDispositionField, ContentLanguageField, ContentTypeField, FieldName}
-import org.apache.james.mime4j.dom.{Entity, Message, Multipart, SingleBody, TextBody => Mime4JTextBody}
+import org.apache.james.mime4j.dom.{Entity, Message, Multipart, TextBody => Mime4JTextBody}
import org.apache.james.mime4j.field.LenientFieldParser
-import org.apache.james.mime4j.message.{BasicBodyFactory, DefaultMessageBuilder, DefaultMessageWriter}
+import org.apache.james.mime4j.message.{BasicBodyFactory, DefaultMessageBuilder}
import org.apache.james.mime4j.stream.{Field, MimeConfig, RawField}
import org.apache.james.util.html.HtmlTextExtractor
@@ -181,15 +180,7 @@ object EmailBodyPart {
.headOption
.map(_.getBody)
- private def size(entity: Entity): Try[Size] =
- entity.getBody match {
- case body: SingleBody => refineSize(body.size())
- case body =>
- val countingOutputStream: CountingOutputStream = new CountingOutputStream(OutputStream.nullOutputStream())
- val writer = new DefaultMessageWriter
- writer.writeBody(body, countingOutputStream)
- refineSize(countingOutputStream.getCount)
- }
+ private def size(entity: Entity): Try[Size] = refineSize(SizeUtils.sizeOf(entity))
private def refineSize(l: Long): Try[Size] = refineV[NonNegative](l) match {
case scala.Right(size) => Success(size)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org