Index: xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-send/xwiki-platform-mail-send-default/src/test/java/org/xwiki/mail/integration/JavaIntegrationTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-send/xwiki-platform-mail-send-default/src/test/java/org/xwiki/mail/integration/JavaIntegrationTest.java (revision e5e32adc762c059d9eee53c29618553e598e2f8c) +++ xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-send/xwiki-platform-mail-send-default/src/test/java/org/xwiki/mail/integration/JavaIntegrationTest.java (date 1545952835000) @@ -19,6 +19,8 @@ */ package org.xwiki.mail.integration; +import java.io.ByteArrayInputStream; +import java.io.File; import java.io.InputStream; import java.util.Arrays; import java.util.Collections; @@ -36,6 +38,7 @@ import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.junit.After; import org.junit.Before; @@ -47,6 +50,7 @@ import org.xwiki.context.Execution; import org.xwiki.context.ExecutionContext; import org.xwiki.context.ExecutionContextManager; +import org.xwiki.environment.Environment; import org.xwiki.environment.internal.EnvironmentConfiguration; import org.xwiki.environment.internal.StandardEnvironment; import org.xwiki.mail.MailListener; @@ -75,9 +79,12 @@ import com.icegreen.greenmail.junit.GreenMailRule; import com.icegreen.greenmail.util.ServerSetupTest; import com.xpn.xwiki.XWikiContext; +import com.xpn.xwiki.api.Attachment; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Integration tests to prove that mail sending is working fully end to end with the Java API. @@ -103,6 +110,10 @@ { private static final String PERMDIR = "target/" + JavaIntegrationTest.class.getSimpleName(); + private static final String TMPDIR = String.format("%s/tmp", PERMDIR); + + private static final String MAILTMPDIR = String.format("%s/mail", TMPDIR); + @Rule public GreenMailRule mail = new GreenMailRule(getCustomServerSetup(ServerSetupTest.SMTP)); @@ -111,9 +122,11 @@ private TestMailSenderConfiguration configuration; - private MimeBodyPartFactory defaultBodyPartFactory; + private TextMimeBodyPartFactory defaultBodyPartFactory; - private MimeBodyPartFactory htmlBodyPartFactory; + private HTMLMimeBodyPartFactory htmlBodyPartFactory; + + private AttachmentMimeBodyPartFactory attachmentBodyPartFactory; private MailSender sender; @@ -149,10 +162,16 @@ @Before public void initialize() throws Exception { + // Make sure files for temporary attachments are saved in the target directory + StandardEnvironment environment = this.componentManager.getInstance(Environment.class); + environment.setTemporaryDirectory(new File(TMPDIR)); + this.defaultBodyPartFactory = this.componentManager.getInstance( new DefaultParameterizedType(null, MimeBodyPartFactory.class, String.class)); this.htmlBodyPartFactory = this.componentManager.getInstance( new DefaultParameterizedType(null, MimeBodyPartFactory.class, String.class), "text/html"); + this.attachmentBodyPartFactory = this.componentManager.getInstance( + new DefaultParameterizedType(null, MimeBodyPartFactory.class, Attachment.class), "xwiki/attachment"); this.sender = this.componentManager.getInstance(MailSender.class); // Set the EC @@ -199,7 +218,7 @@ Multipart multipart = new MimeMultipart("mixed"); // Add text in the body multipart.addBodyPart(this.defaultBodyPartFactory.create("some text here", - Collections.singletonMap("mimetype", "text/plain"))); + Collections.singletonMap("mimetype", "text/plain"))); message.setContent(multipart); // We also test using some default BCC addresses from configuration in this test @@ -253,7 +272,7 @@ Multipart multipart = new MimeMultipart("alternative"); // Add an HTML body part multipart.addBodyPart(this.htmlBodyPartFactory.create( - "simple meeting invitation", Collections.emptyMap())); + "simple meeting invitation", Collections.emptyMap())); // Add the Calendar invitation body part String calendarContent = "BEGIN:VCALENDAR\r\n" + "METHOD:REQUEST\r\n" @@ -310,11 +329,62 @@ assertEquals(calendarContent, IOUtils.toString(is)); } + @Test + public void sendMailWithAttachment() throws Exception + { + // Remove any tmp file to start with a clean slate + FileUtils.cleanDirectory(new File(MAILTMPDIR)); + + // Step 1: Create a JavaMail Session + Session session = Session.getInstance(this.configuration.getAllProperties()); + + // Step 2: Create the Message to send + MimeMessage message = new MimeMessage(session); + message.setSubject("subject"); + message.setRecipient(RecipientType.TO, new InternetAddress("john@doe.com")); + + // Step 3: Add the attachment + Multipart multipart = new MimeMultipart("mixed"); + Attachment attachment = mock(Attachment.class); + when(attachment.getContentInputStream()).thenReturn(new ByteArrayInputStream("attachment content".getBytes())); + when(attachment.getFilename()).thenReturn("attachment.txt"); + when(attachment.getMimeType()).thenReturn("text/plain"); + multipart.addBodyPart(this.attachmentBodyPartFactory.create(attachment, Collections.emptyMap())); + message.setContent(multipart); + + // Verify that a temporary file was saved in the TMP dir + assertEquals(1, new File(MAILTMPDIR).listFiles().length); + + // Step 4: Send the mail and wait for it to be sent + this.sender.sendAsynchronously(Arrays.asList(message), session, null); + + // Verify that the mail has been received (wait maximum 30 seconds). + this.mail.waitForIncomingEmail(30000L, 1); + MimeMessage[] messages = this.mail.getReceivedMessages(); + + assertEquals("subject", messages[0].getHeader("Subject", null)); + assertEquals("john@doe.com", messages[0].getHeader("To", null)); + + assertEquals(1, ((MimeMultipart) messages[0].getContent()).getCount()); + + BodyPart attachmentPart = ((MimeMultipart) messages[0].getContent()).getBodyPart(0); + String content = IOUtils.toString((InputStream) attachmentPart.getContent(), "UTF-8"); + + // Make sure that our special tmp file location header is not sent in the mail + assertNull(attachmentPart.getHeader(AttachmentMimeBodyPartFactory.TEMPORARY_FILE_HEADER)); + + assertEquals("attachment content", content); + + // Verify that the mail sent removed the temporary file + assertEquals(0, new File(MAILTMPDIR).listFiles().length); + } + @Test public void sendMailWithCustomMessageId() throws Exception { Session session = Session.getInstance(this.configuration.getAllProperties()); - MimeMessage message = new MimeMessage(session) { + MimeMessage message = new MimeMessage(session) + { @Override protected void updateMessageID() throws MessagingException { Index: xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-send/xwiki-platform-mail-send-default/src/main/java/org/xwiki/mail/internal/factory/attachment/AttachmentMimeBodyPartFactory.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-send/xwiki-platform-mail-send-default/src/main/java/org/xwiki/mail/internal/factory/attachment/AttachmentMimeBodyPartFactory.java (revision e5e32adc762c059d9eee53c29618553e598e2f8c) +++ xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-send/xwiki-platform-mail-send-default/src/main/java/org/xwiki/mail/internal/factory/attachment/AttachmentMimeBodyPartFactory.java (date 1545947890000) @@ -42,7 +42,6 @@ import org.xwiki.mail.internal.factory.AbstractMimeBodyPartFactory; import com.xpn.xwiki.api.Attachment; -import com.xpn.xwiki.internal.file.TemporaryFile; /** * Creates an attachment Body Part from an {@link Attachment} object. This will be added to a Multi Part message. @@ -55,6 +54,8 @@ @Singleton public class AttachmentMimeBodyPartFactory extends AbstractMimeBodyPartFactory implements Initializable { + public static final String TEMPORARY_FILE_HEADER = "X-TmpFile"; + @Inject private Environment environment; @@ -80,7 +81,9 @@ MimeBodyPart attachmentPart = new MimeBodyPart(); // Save the attachment to a temporary file on the file system and wrap it in a Java Mail Data Source. - DataSource source = createTemporaryAttachmentDataSource(attachment); + // Note that we copy the attachment to a file instead of using directly the attachment data because the + // attachment could be removed before the mail is sent and the mail would point to some non-existing data. + DataSource source = createTemporaryAttachmentDataSource(attachment, attachmentPart); attachmentPart.setDataHandler(new DataHandler(source)); attachmentPart.setHeader("Content-Type", attachment.getMimeType()); @@ -98,15 +101,19 @@ return attachmentPart; } - private DataSource createTemporaryAttachmentDataSource(Attachment attachment) throws MessagingException + private DataSource createTemporaryAttachmentDataSource(Attachment attachment, MimeBodyPart attachmentPart) + throws MessagingException { File temporaryAttachmentFile; FileOutputStream fos = null; try { - temporaryAttachmentFile = - new TemporaryFile(File.createTempFile("attachment", ".tmp", this.temporaryDirectory)); + temporaryAttachmentFile = File.createTempFile("attachment", ".tmp", this.temporaryDirectory); fos = new FileOutputStream(temporaryAttachmentFile); IOUtils.copyLarge(attachment.getContentInputStream(), fos); + + // Add a header with the location of the temporary file so that it can be removed when no longer needed so that + // it doesn't stay lying around. This is done in the Mail Preparation Thread. + attachmentPart.setHeader(TEMPORARY_FILE_HEADER, temporaryAttachmentFile.getAbsolutePath()); } catch (Exception e) { throw new MessagingException( String.format("Failed to save attachment [%s] to the file system", attachment.getFilename()), e); Index: xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-send/xwiki-platform-mail-send-default/src/main/java/org/xwiki/mail/internal/FileSystemMailContentStore.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-send/xwiki-platform-mail-send-default/src/main/java/org/xwiki/mail/internal/FileSystemMailContentStore.java (revision e5e32adc762c059d9eee53c29618553e598e2f8c) +++ xwiki-platform-core/xwiki-platform-mail/xwiki-platform-mail-send/xwiki-platform-mail-send-default/src/main/java/org/xwiki/mail/internal/FileSystemMailContentStore.java (date 1545952687000) @@ -19,18 +19,25 @@ */ package org.xwiki.mail.internal; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; +import javax.mail.Multipart; +import javax.mail.Part; import javax.mail.Session; +import javax.mail.internet.MimeMessage; +import org.apache.commons.io.FileUtils; import org.xwiki.component.annotation.Component; import org.xwiki.component.phase.Initializable; import org.xwiki.component.phase.InitializationException; @@ -38,6 +45,7 @@ import org.xwiki.mail.ExtendedMimeMessage; import org.xwiki.mail.MailContentStore; import org.xwiki.mail.MailStoreException; +import org.xwiki.mail.internal.factory.attachment.AttachmentMimeBodyPartFactory; /** * Stores mail content on the file system. @@ -79,7 +87,8 @@ uniqueMessageId = message.getUniqueMessageId(); messageFile = getMessageFile(batchId, uniqueMessageId); } - message.writeTo(new FileOutputStream(messageFile)); + + writeTo(message, new FileOutputStream(messageFile)); } catch (Exception e) { throw new MailStoreException(String.format( "Failed to save message (id [%s], batch id [%s]) into file [%s]", @@ -118,6 +127,34 @@ } } + private void writeTo(ExtendedMimeMessage message, FileOutputStream output) throws Exception + { + // Delete any temporary file used to hold attachments since their content will have been serialized + // by the call to writeTo() above. This avoids keeping temporary files on the filesystem. + + List temporaryFiles = new ArrayList<>(); + Object content = message.getContent(); + if (content instanceof Multipart) { + Multipart multipart = (Multipart) content; + for (int i = 0; i < multipart.getCount(); i++) { + Part part = multipart.getBodyPart(i); + String[] temporaryFileLocations = part.getHeader(AttachmentMimeBodyPartFactory.TEMPORARY_FILE_HEADER); + if (temporaryFileLocations != null && temporaryFileLocations.length > 0) { + temporaryFiles.add(new File(temporaryFileLocations[0])); + // Remove the special marker header so that it doesn't get sent. + part.removeHeader(AttachmentMimeBodyPartFactory.TEMPORARY_FILE_HEADER); + message.saveChanges(); + } + } + } + + message.writeTo(output); + + for (File temporaryFile : temporaryFiles) { + FileUtils.forceDelete(temporaryFile); + } + } + private File getBatchDirectory(String batchId) { File batchDirectory = new File(rootDirectory, getURLEncoded(batchId)); @@ -125,7 +162,8 @@ return batchDirectory; } - private File getMessageFile(String batchId, String uniqueMessageId) { + private File getMessageFile(String batchId, String uniqueMessageId) + { return new File(getBatchDirectory(batchId), getURLEncoded(uniqueMessageId)); } @@ -137,4 +175,4 @@ throw new RuntimeException("UTF-8 not available, this Java VM is not standards compliant!"); } } -} +} \ No newline at end of file