diff --git a/src/main/java/org/apache/commons/mail/LazyByteArrayDataSource.java b/src/main/java/org/apache/commons/mail/LazyByteArrayDataSource.java new file mode 100644 index 000000000..cd4586f83 --- /dev/null +++ b/src/main/java/org/apache/commons/mail/LazyByteArrayDataSource.java @@ -0,0 +1,106 @@ +/* + * 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.commons.mail; + +import javax.activation.DataSource; +import javax.mail.util.ByteArrayDataSource; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + *
This class is created to replace the usage of {@code org.apache.commons.mail.ByteArrayDataSource} and {@code javax.mail.util.ByteArrayDataSource}, + * as both implementations load attachment binary in eager manner. + * + *
In order to cater the scenario that user only access the metadata (Name, Type) but not interested in the actual attachment binary, + * in this scenario, the memory usage can be further reduced as attachment binary only loaded when .getInputStream() called. + * + * @since 1.5 + */ +public class LazyByteArrayDataSource implements DataSource { + + /** InputStream reference for the email attachment binary. */ + private final InputStream referenceInputStream; + + /** ByteArrayDateSource instance which contain email attachment binary in the form of byte array. */ + private ByteArrayDataSource ds; + + /** Name of the attachment. */ + private final String name; + + /** Type of the attachment. */ + private final String type; + + /** + * Constructs a new instance to read all necessary information for an email attachment. + * + * @param is the InputStream which represent the attachment binary. + * @param type the type of the attachment. + * @param name the name of the attachment. + */ + public LazyByteArrayDataSource(InputStream is, String type, String name) { + this.referenceInputStream = is; + this.type = type; + this.name = name; + } + + /** + * Gets an {@code ByteArrayDataSource} instance, to represent the email attachment. + * + * @return An ByteArrayDataSource instance which contain the email attachment. + * @throws IOException resolving the email attachment failed + */ + @Override + public synchronized InputStream getInputStream() throws IOException { + if (ds == null) { + //Only read attachment data to memory when getInputStream() is called. + ds = new ByteArrayDataSource(referenceInputStream, type); + ds.setName(name); + } + return ds.getInputStream(); + } + + /** + * Not supported. + * + * @return N/A + */ + @Override + public OutputStream getOutputStream() throws UnsupportedOperationException { + throw new UnsupportedOperationException("cannot do this"); + } + + /** + * Gets the content type. + * + * @return A String. + */ + @Override + public String getContentType() { + return type; + } + + /** + * Gets the name. + * + * @return A String. + */ + @Override + public String getName() { + return name; + } +} diff --git a/src/main/java/org/apache/commons/mail/util/MimeMessageParser.java b/src/main/java/org/apache/commons/mail/util/MimeMessageParser.java index e29269eeb..3ac50c455 100644 --- a/src/main/java/org/apache/commons/mail/util/MimeMessageParser.java +++ b/src/main/java/org/apache/commons/mail/util/MimeMessageParser.java @@ -16,11 +16,9 @@ */ package org.apache.commons.mail.util; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; +import org.apache.commons.mail.LazyByteArrayDataSource; + import java.io.IOException; -import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; @@ -43,7 +41,6 @@ import javax.mail.internet.MimePart; import javax.mail.internet.MimeUtility; import javax.mail.internet.ParseException; -import javax.mail.util.ByteArrayDataSource; /** * Parses a MimeMessage and stores the individual parts such a plain text, @@ -270,15 +267,8 @@ protected DataSource createDataSource(final Multipart parent, final MimePart par final DataHandler dataHandler = part.getDataHandler(); final DataSource dataSource = dataHandler.getDataSource(); final String contentType = getBaseMimeType(dataSource.getContentType()); - byte[] content; - try (InputStream inputStream = dataSource.getInputStream()) - { - content = this.getContent(inputStream); - } - final ByteArrayDataSource result = new ByteArrayDataSource(content, contentType); final String dataSourceName = getDataSourceName(part, dataSource); - result.setName(dataSourceName); - return result; + return new LazyByteArrayDataSource(dataSource.getInputStream(), contentType, dataSourceName); } /** @return Returns the mimeMessage. */ @@ -411,28 +401,6 @@ protected String getDataSourceName(final Part part, final DataSource dataSource) return result; } - /** - * Read the content of the input stream. - * - * @param is the input stream to process - * @return the content of the input stream - * @throws IOException reading the input stream failed - */ - private byte[] getContent(final InputStream is) - throws IOException - { - final ByteArrayOutputStream os = new ByteArrayOutputStream(); - final BufferedInputStream isReader = new BufferedInputStream(is); - try (BufferedOutputStream osWriter = new BufferedOutputStream(os)) { - int ch; - while ((ch = isReader.read()) != -1) - { - osWriter.write(ch); - } - osWriter.flush(); - return os.toByteArray(); - } - } /** * Parses the mimeType. diff --git a/src/test/java/org/apache/commons/mail/util/MimeMessageParserTest.java b/src/test/java/org/apache/commons/mail/util/MimeMessageParserTest.java index 5b507d439..7b47747da 100644 --- a/src/test/java/org/apache/commons/mail/util/MimeMessageParserTest.java +++ b/src/test/java/org/apache/commons/mail/util/MimeMessageParserTest.java @@ -16,6 +16,10 @@ */ package org.apache.commons.mail.util; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -23,12 +27,15 @@ import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.InputStream; import java.util.List; import java.util.Properties; +import javax.activation.DataHandler; import javax.activation.DataSource; import javax.mail.Session; import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimePart; import org.apache.commons.mail.HtmlEmail; import org.junit.Test; @@ -498,4 +505,60 @@ public void testParseInlineCID() throws Exception assertEquals(ds, mimeMessageParser.getAttachmentList().get(0)); } + @Test + public void testAttachmentNotLoaded() throws Exception + { + final MimeMessageParser mimeMessageParser = new MimeMessageParser(null); + final InputStream inputStream = createMock(InputStream.class); + final MimePart mimePart = getMockedMimePart(inputStream); + + // Create data source with mocked data. + final DataSource dataSource = mimeMessageParser.createDataSource(null,mimePart); + // Verify no inputStream.read() call is made at this point (Lazy initialization). + verify(inputStream); + } + + + @Test + public void testAttachmentLoaded() throws Exception + { + final MimeMessageParser mimeMessageParser = new MimeMessageParser(null); + final InputStream inputStream = createMock(InputStream.class); + // Despite .getInputStream() called for 3 times, but the desk IO for attachment read should only happen once. + expect(inputStream.read(new byte[8192])).andReturn(0).once(); + final MimePart mimePart = getMockedMimePart(inputStream); + + // Create data source with mocked data. + final DataSource dataSource = mimeMessageParser.createDataSource(null,mimePart); + + dataSource.getInputStream(); + dataSource.getInputStream(); + dataSource.getInputStream(); + // To make sure disk IO only happen when .getInputStream() invoked for first time but not during the object construction. + verify(inputStream); + + } + + /** + * Helper method to return a mocked MimePart class. + * @param inputStream Mocked input stream + * @return Mocked MimePart instance. + * @throws Exception When attachment read failed. + */ + private MimePart getMockedMimePart(InputStream inputStream) throws Exception + { + + final MimePart mimePart = createMock(MimePart.class); + final DataSource dataSource = createMock(DataSource.class); + final DataHandler dataHandler = new DataHandler(dataSource); + + expect(dataSource.getContentType()).andReturn("test_type"); + expect(dataSource.getName()).andReturn("test_name"); + expect(dataSource.getInputStream()).andReturn(inputStream).once(); + expect(mimePart.getDataHandler()).andReturn(dataHandler); + replay(mimePart,dataSource,inputStream); + + return mimePart; + } + }