package com.fsck.k9.message; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; import android.app.Application; import com.fsck.k9.Account.QuoteStyle; import com.fsck.k9.Identity; import com.fsck.k9.K9RobolectricTestRunner; import com.fsck.k9.activity.misc.Attachment; import com.fsck.k9.mail.Address; import com.fsck.k9.mail.BodyPart; import com.fsck.k9.mail.BoundaryGenerator; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Message.RecipientType; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.internet.MessageIdGenerator; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.message.MessageBuilder.Callback; import com.fsck.k9.message.quote.InsertableHtmlContent; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @RunWith(K9RobolectricTestRunner.class) public class MessageBuilderTest { public static final String TEST_MESSAGE_TEXT = "soviet message\r\ntext ☭"; public static final String TEST_ATTACHMENT_TEXT = "text data in attachment"; public static final String TEST_SUBJECT = "test_subject"; public static final Address TEST_IDENTITY_ADDRESS = new Address("test@example.org", "tester"); public static final Address[] TEST_TO = new Address[] { new Address("to1@example.org", "recip 1"), new Address("to2@example.org", "recip 2") }; public static final Address[] TEST_CC = new Address[] { new Address("cc@example.org", "cc recip") }; public static final Address[] TEST_BCC = new Address[] { new Address("bcc@example.org", "bcc recip") }; public static final String TEST_MESSAGE_ID = "<00000000-0000-007B-0000-0000000000EA@example.org>"; public static final Date SENT_DATE = new Date(10000000000L); public static final String BOUNDARY_1 = "----boundary1"; public static final String BOUNDARY_2 = "----boundary2"; public static final String BOUNDARY_3 = "----boundary3"; public static final String MESSAGE_HEADERS = "" + "Date: Sun, 26 Apr 1970 17:46:40 +0000\r\n" + "From: tester <test@example.org>\r\n" + "To: recip 1 <to1@example.org>,recip 2 <to2@example.org>\r\n" + "CC: cc recip <cc@example.org>\r\n" + "BCC: bcc recip <bcc@example.org>\r\n" + "Subject: test_subject\r\n" + "User-Agent: K-9 Mail for Android\r\n" + "In-Reply-To: inreplyto\r\n" + "References: references\r\n" + "Message-ID: " + TEST_MESSAGE_ID + "\r\n" + "MIME-Version: 1.0\r\n"; public static final String MESSAGE_CONTENT = "" + "Content-Type: text/plain;\r\n" + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "soviet message\r\n" + "text =E2=98=AD"; public static final String MESSAGE_CONTENT_WITH_ATTACH = "" + "Content-Type: multipart/mixed; boundary=\"" + BOUNDARY_1 + "\"\r\n" + "Content-Transfer-Encoding: 7bit\r\n" + "\r\n" + "--" + BOUNDARY_1 + "\r\n" + "Content-Type: text/plain;\r\n" + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "soviet message\r\n" + "text =E2=98=AD\r\n" + "--" + BOUNDARY_1 + "\r\n" + "Content-Type: text/plain;\r\n" + " name=\"attach.txt\"\r\n" + "Content-Transfer-Encoding: base64\r\n" + "Content-Disposition: attachment;\r\n" + " filename=\"attach.txt\";\r\n" + " size=23\r\n" + "\r\n" + "dGV4dCBkYXRhIGluIGF0dGFjaG1lbnQ=\r\n" + "\r\n" + "--" + BOUNDARY_1 + "--\r\n"; public static final String MESSAGE_CONTENT_WITH_MESSAGE_ATTACH = "" + "Content-Type: multipart/mixed; boundary=\"" + BOUNDARY_1 + "\"\r\n" + "Content-Transfer-Encoding: 7bit\r\n" + "\r\n" + "--" + BOUNDARY_1 + "\r\n" + "Content-Type: text/plain;\r\n" + " charset=utf-8\r\n" + "Content-Transfer-Encoding: quoted-printable\r\n" + "\r\n" + "soviet message\r\n" + "text =E2=98=AD\r\n" + "--" + BOUNDARY_1 + "\r\n" + "Content-Type: application/octet-stream;\r\n" + " name=\"attach.txt\"\r\n" + "Content-Transfer-Encoding: base64\r\n" + "Content-Disposition: attachment;\r\n" + " filename=\"attach.txt\";\r\n" + " size=23\r\n" + "\r\n" + "dGV4dCBkYXRhIGluIGF0dGFjaG1lbnQ=\r\n" + "\r\n" + "--" + BOUNDARY_1 + "--\r\n"; private Application context; private MessageIdGenerator messageIdGenerator; private BoundaryGenerator boundaryGenerator; private Callback callback; @Before public void setUp() throws Exception { messageIdGenerator = mock(MessageIdGenerator.class); when(messageIdGenerator.generateMessageId(any(Message.class))).thenReturn(TEST_MESSAGE_ID); boundaryGenerator = mock(BoundaryGenerator.class); when(boundaryGenerator.generateBoundary()).thenReturn(BOUNDARY_1, BOUNDARY_2, BOUNDARY_3); callback = mock(Callback.class); context = RuntimeEnvironment.application; } @Test public void build_shouldSucceed() throws Exception { MessageBuilder messageBuilder = createSimpleMessageBuilder(); messageBuilder.buildAsync(callback); MimeMessage message = getMessageFromCallback(); assertEquals("text/plain", message.getMimeType()); assertEquals(TEST_SUBJECT, message.getSubject()); assertEquals(TEST_IDENTITY_ADDRESS, message.getFrom()[0]); assertArrayEquals(TEST_TO, message.getRecipients(RecipientType.TO)); assertArrayEquals(TEST_CC, message.getRecipients(RecipientType.CC)); assertArrayEquals(TEST_BCC, message.getRecipients(RecipientType.BCC)); assertEquals(MESSAGE_HEADERS + MESSAGE_CONTENT, getMessageContents(message)); } @Test public void build_withAttachment_shouldSucceed() throws Exception { MessageBuilder messageBuilder = createSimpleMessageBuilder(); Attachment attachment = createAttachmentWithContent("text/plain", "attach.txt", TEST_ATTACHMENT_TEXT); messageBuilder.setAttachments(Collections.singletonList(attachment)); messageBuilder.buildAsync(callback); MimeMessage message = getMessageFromCallback(); assertEquals(MESSAGE_HEADERS + MESSAGE_CONTENT_WITH_ATTACH, getMessageContents(message)); } @Test public void build_usingHtmlFormat_shouldUseMultipartAlternativeInCorrectOrder() { MessageBuilder messageBuilder = createHtmlMessageBuilder(); messageBuilder.buildAsync(callback); MimeMessage message = getMessageFromCallback(); assertEquals(MimeMultipart.class, message.getBody().getClass()); assertEquals("multipart/alternative", ((MimeMultipart) message.getBody()).getMimeType()); List<BodyPart> parts = ((MimeMultipart) message.getBody()).getBodyParts(); //RFC 2046 - 5.1.4. - Best type is last displayable assertEquals("text/plain", parts.get(0).getMimeType()); assertEquals("text/html", parts.get(1).getMimeType()); } @Test public void build_withMessageAttachment_shouldAttachAsApplicationOctetStream() throws Exception { MessageBuilder messageBuilder = createSimpleMessageBuilder(); Attachment attachment = createAttachmentWithContent("message/rfc822", "attach.txt", TEST_ATTACHMENT_TEXT); messageBuilder.setAttachments(Collections.singletonList(attachment)); messageBuilder.buildAsync(callback); MimeMessage message = getMessageFromCallback(); assertEquals(MESSAGE_HEADERS + MESSAGE_CONTENT_WITH_MESSAGE_ATTACH, getMessageContents(message)); } @Test public void build_detachAndReattach_shouldSucceed() throws MessagingException { MessageBuilder messageBuilder = createSimpleMessageBuilder(); Callback anotherCallback = mock(Callback.class); Robolectric.getBackgroundThreadScheduler().pause(); messageBuilder.buildAsync(callback); messageBuilder.detachCallback(); Robolectric.getBackgroundThreadScheduler().unPause(); messageBuilder.reattachCallback(anotherCallback); verifyNoMoreInteractions(callback); verify(anotherCallback).onMessageBuildSuccess(any(MimeMessage.class), eq(false)); verifyNoMoreInteractions(anotherCallback); } @Test public void buildWithException_shouldThrow() throws MessagingException { MessageBuilder messageBuilder = new SimpleMessageBuilder(context, messageIdGenerator, boundaryGenerator) { @Override protected void buildMessageInternal() { queueMessageBuildException(new MessagingException("expected error")); } }; messageBuilder.buildAsync(callback); verify(callback).onMessageBuildException(any(MessagingException.class)); verifyNoMoreInteractions(callback); } @Test public void buildWithException_detachAndReattach_shouldThrow() throws MessagingException { Callback anotherCallback = mock(Callback.class); MessageBuilder messageBuilder = new SimpleMessageBuilder(context, messageIdGenerator, boundaryGenerator) { @Override protected void buildMessageInternal() { queueMessageBuildException(new MessagingException("expected error")); } }; Robolectric.getBackgroundThreadScheduler().pause(); messageBuilder.buildAsync(callback); messageBuilder.detachCallback(); Robolectric.getBackgroundThreadScheduler().unPause(); messageBuilder.reattachCallback(anotherCallback); verifyNoMoreInteractions(callback); verify(anotherCallback).onMessageBuildException(any(MessagingException.class)); verifyNoMoreInteractions(anotherCallback); } private MimeMessage getMessageFromCallback() { ArgumentCaptor<MimeMessage> mimeMessageCaptor = ArgumentCaptor.forClass(MimeMessage.class); verify(callback).onMessageBuildSuccess(mimeMessageCaptor.capture(), eq(false)); verifyNoMoreInteractions(callback); return mimeMessageCaptor.getValue(); } private String getMessageContents(MimeMessage message) throws IOException, MessagingException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); message.writeTo(outputStream); return outputStream.toString(); } private Attachment createAttachmentWithContent(String mimeType, String filename, String content) throws Exception { byte[] bytes = content.getBytes(); File tempFile = File.createTempFile("pre", ".tmp"); tempFile.deleteOnExit(); FileOutputStream fileOutputStream = new FileOutputStream(tempFile); fileOutputStream.write(bytes); fileOutputStream.close(); return Attachment.createAttachment(null, 0, mimeType) .deriveWithMetadataLoaded(mimeType, filename, bytes.length) .deriveWithLoadComplete(tempFile.getAbsolutePath()); } private MessageBuilder createSimpleMessageBuilder() { Identity identity = createIdentity(); return new SimpleMessageBuilder(context, messageIdGenerator, boundaryGenerator) .setSubject(TEST_SUBJECT) .setSentDate(SENT_DATE) .setHideTimeZone(true) .setTo(Arrays.asList(TEST_TO)) .setCc(Arrays.asList(TEST_CC)) .setBcc(Arrays.asList(TEST_BCC)) .setInReplyTo("inreplyto") .setReferences("references") .setRequestReadReceipt(false) .setIdentity(identity) .setMessageFormat(SimpleMessageFormat.TEXT) .setText(TEST_MESSAGE_TEXT) .setAttachments(new ArrayList<Attachment>()) .setSignature("signature") .setQuoteStyle(QuoteStyle.PREFIX) .setQuotedTextMode(QuotedTextMode.NONE) .setQuotedText("quoted text") .setQuotedHtmlContent(new InsertableHtmlContent()) .setReplyAfterQuote(false) .setSignatureBeforeQuotedText(false) .setIdentityChanged(false) .setSignatureChanged(false) .setCursorPosition(0) .setMessageReference(null) .setDraft(false); } private MessageBuilder createHtmlMessageBuilder() { return createSimpleMessageBuilder().setMessageFormat(SimpleMessageFormat.HTML); } private Identity createIdentity() { Identity identity = new Identity(); identity.setName(TEST_IDENTITY_ADDRESS.getPersonal()); identity.setEmail(TEST_IDENTITY_ADDRESS.getAddress()); identity.setDescription("test identity"); identity.setSignatureUse(false); return identity; } }