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 <btell...@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: <b3493b24-116a-4d6b-a61c-1a3b09e44...@linagora.com> +Date: Wed, 21 Feb 2024 22:58:55 +0100 +MIME-Version: 1.0 +References: <mime4j.8f3.69abfdd624c15c44.18dcdad3...@linagora.com> +Subject: Fwd: Will be nested +To: btell...@linagora.com +From: "btell...@linagora.com" <btell...@linagora.com> +In-Reply-To: <mime4j.8f3.69abfdd624c15c44.18dcdad3...@linagora.com> +X-Forwarded-Message-Id: <mime4j.8f3.69abfdd624c15c44.18dcdad3...@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: btell...@linagora.com +Date: Wed, 21 Feb 2024 21:58:17 +0000 +Message-ID: <mime4j.8f3.69abfdd624c15c44.18dcdad3...@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-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org