1   package test.atlassian.mail;
2   
3   import com.atlassian.mail.MailUtils;
4   import junit.framework.TestCase;
5   import org.apache.commons.io.IOUtils;
6   import test.mock.mail.MockMessage;
7   
8   import java.io.ByteArrayInputStream;
9   import java.io.FileNotFoundException;
10  import java.io.IOException;
11  import java.io.InputStream;
12  import java.util.ArrayList;
13  import java.util.Collection;
14  import java.util.List;
15  import java.util.Properties;
16  import javax.activation.DataHandler;
17  import javax.mail.Address;
18  import javax.mail.BodyPart;
19  import javax.mail.Message;
20  import javax.mail.MessagingException;
21  import javax.mail.Multipart;
22  import javax.mail.Part;
23  import javax.mail.Session;
24  import javax.mail.internet.AddressException;
25  import javax.mail.internet.InternetAddress;
26  import javax.mail.internet.MimeBodyPart;
27  import javax.mail.internet.MimeMessage;
28  import javax.mail.internet.MimeMultipart;
29  import javax.mail.internet.NewsAddress;
30  
31  public class TestMailUtils extends TestCase
32  {
33      public TestMailUtils(String s)
34      {
35          super(s);
36      }
37  
38      protected void setUp() throws Exception
39      {
40          super.setUp();
41      }
42  
43  	//MAIL-49
44  	public void testGetAttachmentWithMissingDisposition() throws MessagingException, IOException
45  	{
46  		MimeMessage msg = new MimeMessage(Session.getDefaultInstance(new Properties()));
47  		MimeMultipart content = new MimeMultipart();
48  
49  		BodyPart text = new MimeBodyPart();
50  		text.setText("this is a text");
51  		content.addBodyPart(text);
52  
53  		MimeBodyPart attachmentPart = new MimeBodyPart();
54          attachmentPart.setDataHandler(new DataHandler("testing", "text/plain"));
55  		attachmentPart.addHeader("Content-Type", "text/plain;\n" +
56  				"\tname=\"text.txt\"");
57  		content.addBodyPart(attachmentPart);
58  		msg.setContent(content);
59  
60  		MailUtils.Attachment[] attachments = MailUtils.getAttachments(msg);
61  		assertEquals(1, attachments.length);
62  		assertEquals("text.txt", attachments[0].getFilename());		
63  	}
64  
65      public void testEmptyBodyPart() throws MessagingException
66      {
67          MimeMessage msg = new MimeMessage(Session.getDefaultInstance(new Properties()));
68  
69          MimeMultipart multi = new MimeMultipart();
70          BodyPart plainText = new MimeBodyPart();
71          BodyPart deliveryStatus = new MimeBodyPart();
72  
73          plainText.setText("test");
74          deliveryStatus.setHeader("Content-Type","text/plain" );
75  
76          multi.addBodyPart(plainText);
77          multi.addBodyPart(deliveryStatus);
78  
79          msg.setContent(multi);
80          msg.saveChanges();
81  
82          String body = MailUtils.getBody(msg);
83          assertEquals("test", body);
84      }
85  
86      public void testParseAddresses() throws AddressException
87      {
88          InternetAddress[] addresses = MailUtils.parseAddresses("edwin@atlassian.com, mike@atlassian.com, owen@atlassian.com");
89  
90          assertEquals(new InternetAddress("edwin@atlassian.com"), addresses[0]);
91          assertEquals(new InternetAddress("mike@atlassian.com"), addresses[1]);
92          assertEquals(new InternetAddress("owen@atlassian.com"), addresses[2]);
93      }
94  
95      public void testGetBody() throws MessagingException, IOException
96      {
97          testGetBodyWithContentType("text/plain");
98      }
99      
100     public void testGetBodyWithDifferentContentTypes() throws MessagingException, IOException
101     {
102         testGetBodyWithContentType("TEXT/PLAIN");
103         testGetBodyWithContentType("tExt/plAIN");
104 
105         testGetBodyWithContentType("text/html");
106         testGetBodyWithContentType("text/HTML");
107         testGetBodyWithContentType("TEXT/HTML");
108     }
109 
110     private void testGetBodyWithContentType(final String contentType) throws MessagingException
111     {
112         Message msg = new MimeMessage(null, new ByteArrayInputStream("test".getBytes()));
113         msg.setContent("test message", "");
114         msg.setHeader("Content-Type", contentType);
115         assertEquals("test message", MailUtils.getBody(msg));
116 
117         msg.setContent(new Integer(1), contentType);
118         assertNull(MailUtils.getBody(msg));
119     }
120 
121     /**
122      * Creating edge cases for this test is difficult because the mail api
123      * tries to stop you doing funky things with nulls, blanks and padded
124      * values for addresses. This looks like the 80% coverage with 20% effort.
125      *
126      * @throws Exception
127      */
128     public void testGetSenders() throws Exception
129     {
130         Address[] blankaddresslist = { };
131 
132         Message msg = createMockMessageWithSenders(blankaddresslist);
133         assertEquals(0, MailUtils.getSenders(msg).size());
134 
135         // make a mock message that will return a null in the from address list
136         Address[] allNulls = { null, null };
137         msg = createMockMessageWithSenders(allNulls);
138         assertEquals(0, MailUtils.getSenders(msg).size());
139 
140         // check mixed bad with one good address list
141         String goodAddress = "good@address.com";
142         Address[] oneGood = { null, new InternetAddress(goodAddress) };
143         msg = createMockMessageWithSenders(oneGood);
144         List senders = MailUtils.getSenders(msg);
145         assertEquals(1, senders.size());
146         assertEquals(goodAddress, senders.get(0));
147 
148         // check mixed bag of types, nulls and a padded duplicate
149         Address[] mixedBag = {
150                 new InternetAddress(goodAddress),
151                 new NewsAddress(),
152                 null,
153                 new InternetAddress("  " + goodAddress) };
154         msg = createMockMessageWithSenders(mixedBag);
155         senders = MailUtils.getSenders(msg);
156         assertEquals(2, senders.size());
157         // should have two the same (trimmed)
158         assertEquals(goodAddress, senders.get(0));
159         assertEquals(goodAddress, senders.get(1));
160     }
161 
162     private Message createMockMessageWithSenders(final Address[] addresses) throws MessagingException
163     {
164         Message msg = new MockMessage()
165         {
166 
167             public Address[] getFrom() throws MessagingException
168             {
169                 return addresses;
170             }
171         };
172         return msg;
173     }
174 
175 
176     public void testHasRecipient() throws MessagingException
177     {
178         Message msg = new MimeMessage(null, new ByteArrayInputStream("test".getBytes()));
179         assertTrue(!MailUtils.hasRecipient("edwin@atlassian.com", msg));
180         msg.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress("edwin@atlassian.com"));
181         assertTrue(MailUtils.hasRecipient("edwin@atlassian.com", msg));
182     }
183 
184     public void testCreateAttachmentMimeBodyPartFileNames() throws MessagingException
185     {
186         MimeBodyPart mbp = MailUtils.createAttachmentMimeBodyPart("C:/test/foo/bar/export.zip");
187         assertEquals("export.zip", mbp.getFileName());
188     }
189 
190     public void testCreateAttachmentMimeBodyPartFileNames2() throws MessagingException
191     {
192         MimeBodyPart mbp = MailUtils.createAttachmentMimeBodyPart("C:\\test\\foo\\bar\\export.zip");
193         assertEquals("export.zip", mbp.getFileName());
194     }
195 
196     public void testGetBodyText() throws MessagingException
197     {
198         assertEquals("This is a simple test mail, which isn't in HTML and has no attachments.",
199                 MailUtils.getBody(makeMessageFromResourceFile("/testmails/simplebody.txt")));
200     }
201 
202     public void testGetBodyTextIfContentEncodingUnsupported() throws MessagingException
203     {
204         assertEquals("This is a simple test mail, which isn't in HTML and has no attachments.",
205                      MailUtils.getBody(makeMessageFromResourceFile("/testmails/simplebodyunsupportedencoding.txt")));
206     }
207 
208     public void testGetBodyHtml() throws MessagingException
209     {
210         assertEquals("This is an HTML test mail\nwith a linebreak.",
211                 MailUtils.getBody(makeMessageFromResourceFile("/testmails/simplehtml.txt")));
212     }
213 
214     public void testGetBodyMultipartAlternativeGetsTextMatch() throws MessagingException
215     {
216         assertEquals("This is the text part of the mail",
217                 MailUtils.getBody(makeMessageFromResourceFile("/testmails/multipart.txt")));
218     }
219 
220     public void testGetBodyMultipartGetsFirstTextMatch() throws MessagingException
221     {
222         assertEquals("This is the first part",
223                 MailUtils.getBody(makeMessageFromResourceFile("/testmails/multipartbothtext.txt")));
224     }
225 
226     public void testGetBodyMultipartGetsHtmlIfNoText() throws MessagingException
227     {
228         assertEquals("This is\nthe HTML part of the mail",
229                 MailUtils.getBody(makeMessageFromResourceFile("/testmails/multipartnotext.txt")));
230     }
231 
232     public void testGetBodyMultipartNoMatch() throws MessagingException
233     {
234         assertEquals("", MailUtils.getBody(makeMessageFromResourceFile("/testmails/multipartnobody.txt")));
235     }
236 
237     public void testGetBodyMultipartMixed() throws MessagingException
238     {
239         assertEquals("Text alternative", MailUtils.getBody(makeMessageFromResourceFile("/testmails/multipartmixedotherorder.txt")));
240     }
241 
242     public void testGetBodyMultipartMixedWithLotsOfTextAndHtml() throws MessagingException
243     {
244         assertEquals("Text alternative\nThis is the first part after the alternative\nThis is the second part after the alternative",
245                 MailUtils.getBody(makeMessageFromResourceFile("/testmails/multipartmixedtextandhtml.txt")));
246     }
247 
248     public void testGetAttachmentsNoAttachments() throws MessagingException, IOException
249     {
250         assertEquals("no attachments", 0,
251                 MailUtils.getAttachments(makeMessageFromResourceFile("/testmails/multipart.txt")).length);
252     }
253 
254     public void testGetAttachmentsIfContentEncodingIsNotSupported() throws MessagingException, IOException
255     {
256         Message message = makeMessageFromResourceFile("/testmails/multipartunsupportedencoding.txt");
257         assertEquals("This is the text part of the mail", MailUtils.getBody(message));
258         assertEquals("no attachments", 0,
259                      MailUtils.getAttachments(message).length);
260     }
261 
262     public void testGetAttachmentsHasAttachments() throws MessagingException, IOException
263     {
264         MailUtils.Attachment[] attachments = MailUtils.getAttachments(makeMessageFromResourceFile("/testmails/multipartmixed.txt"));
265         assertEquals("has attachments", 2, attachments.length);
266         assertEquals("Attachment 1", new String(attachments[0].getContents()));
267         assertEquals("image/jpeg", attachments[0].getContentType().substring(0, 10));
268         assertEquals("bugger.jpg", attachments[0].getFilename());
269         assertEquals("html<br>attachment", new String(attachments[1].getContents()));
270         assertEquals("text/html", attachments[1].getContentType().substring(0, 9));
271         assertEquals("foo.html", attachments[1].getFilename());
272     }
273 
274     private Message makeMessageFromResourceFile(String resourceName) throws MessagingException
275     {
276         return new MimeMessage(null, this.getClass().getResourceAsStream(resourceName));
277     }
278 
279 
280     public void testGetContentTypeFromHeaderValue()
281     {
282         final String input = "text/plain";
283         final String actual = MailUtils.getContentType(input);
284         final String expected = input;
285 
286         assertEquals(expected, actual);
287     }
288 
289     public void testGetContentTypeFromHeaderValueWithParameter()
290     {
291         final String input = "text/plain; something=somevalue";
292         final String actual = MailUtils.getContentType(input);
293         final String expected = "text/plain";
294 
295         assertEquals(expected, actual);
296     }
297 
298     /**
299      * A simplistic attempt at a enum safe pattern. Use these constnats to make the checkParts invocations
300      * more englishlike and readable.
301      */
302     final static boolean PLAIN_TEXT_YES = true;
303     final static boolean PLAIN_TEXT_NO = false;
304 
305     final static boolean HTML_YES = true;
306     final static boolean HTML_NO = false;
307 
308     final static boolean INLINE_YES = true;
309     final static boolean INLINE_NO = false;
310 
311     final static boolean ATTACHMENT_YES = true;
312     final static boolean ATTACHMENT_NO = false;
313 
314     public void testIsPlainTextPart() throws Exception
315     {
316         this.checkParts("OutlookPlainText.msg", PLAIN_TEXT_YES, HTML_YES, INLINE_NO, ATTACHMENT_NO);
317     }
318 
319     public void testIsHtmlPart() throws Exception
320     {
321         this.checkParts("OutlookHtml.msg", PLAIN_TEXT_YES, HTML_YES, INLINE_NO, ATTACHMENT_NO);
322     }
323 
324     public void testIsImageAttachedPart() throws Exception
325     {
326         this.checkParts("OutlookHtmlImageAttached.msg", PLAIN_TEXT_YES, HTML_YES, INLINE_NO, ATTACHMENT_YES);
327     }
328 
329     public void testIsInlineImagePart() throws Exception
330     {
331         this.checkParts("OutlookHtmlInlineImage.msg", PLAIN_TEXT_YES, HTML_YES, INLINE_YES, ATTACHMENT_NO);
332     }
333 
334     public void testIsInlineImagePartWherePartHasDisposition() throws Exception
335     {
336         this.checkParts("ThunderbirdHtmlAndPlainTextInlineImage.msg", PLAIN_TEXT_YES, HTML_YES, INLINE_YES, ATTACHMENT_NO);
337     }
338 
339     public void testIsAttachmentPart() throws Exception
340     {
341         this.checkParts("OutlookHtmlBinaryAttachment.msg", PLAIN_TEXT_YES, HTML_YES, INLINE_NO, ATTACHMENT_YES);
342     }
343 
344     public void testIsRelatedPart() throws Exception
345     {
346         assertFalse(MailUtils.isPartRelated(makeMessageFromResourceFile("/testmails/multipartmixedtextandhtml.txt")));
347         assertTrue(MailUtils.isPartRelated(makeMessageFromResourceFile("/testmails/multipartrelated.txt")));
348     }
349 
350     /**
351      * The mail method which firstly creates a message from a message file.
352      * After that all the parts present are tested against the predicates passed in as the boolean parameters. Eg if plainTextPresent is set to false
353      * and a plain text part is found by MailUtils.isPartPlainText() then it will complain. Its got other smarts to make sure the XXXPresent parameters
354      * match what was found.
355      *
356      * @param filename
357      * @param plainTextExpected
358      * @param htmlExpected
359      * @param inlineExpected
360      * @param attachmentExpected
361      * @throws Exception
362      */
363     void checkParts(final String filename, final boolean plainTextExpected, final boolean htmlExpected, final boolean inlineExpected, final boolean attachmentExpected)
364             throws Exception
365     {
366         assertNotNull(filename);
367 
368         final Part[] parts = this.createPartsFromMessage(filename);
369         assertNotNull("parts", parts);
370         assertTrue("Expected atleast 1 part but got " + parts.length + " part(s)", parts.length > 0);
371 
372         // testing time...
373         boolean plainTextFound = false;
374         boolean htmlFound = false;
375         boolean inlineFound = false;
376         boolean attachmentFound = false;
377 
378         for (int i = 0; i < parts.length; i++)
379         {
380             final Part part = parts[i];
381 
382             if (MailUtils.isPartPlainText(part))
383             {
384                 assertTrue("PlainText part found when none was expected", plainTextExpected);
385                 plainTextFound = true;
386                 continue;
387             }
388 
389             if (MailUtils.isPartHtml(part))
390             {
391                 assertTrue("Html part found when none was expected", htmlExpected);
392                 htmlFound = true;
393             }
394 
395             if (MailUtils.isPartInline(part))
396             {
397                 assertTrue("Inline part found when none was expected", inlineExpected);
398                 inlineFound = true;
399 
400                 final boolean reportedEmpty = MailUtils.isContentEmpty(part);
401                 assertFalse("All inline parts in the prepared msg files are never empty...", reportedEmpty);
402             }
403 
404             if (MailUtils.isPartAttachment(part))
405             {
406                 assertTrue("Attachment part found when none was expected", attachmentExpected);
407                 attachmentFound = true;
408 
409                 final boolean reportedEmpty = MailUtils.isContentEmpty(part);
410                 assertFalse("All attachments parts in the prepared msg files are never empty...", reportedEmpty);
411             }
412         }
413 
414         assertEquals("Expected to find a plain text part but one was not found", plainTextExpected, plainTextFound);
415         assertEquals("Expected to find a html part but one was not found", htmlExpected, htmlFound);
416         assertEquals("Expected to find a inline part but one was not found", inlineExpected, inlineFound);
417         assertEquals("Expected to find a attachment part but one was not found", attachmentExpected, attachmentFound);
418     }
419 
420     /**
421      * Nice little method that creates an array of parts after creating a new message from a *.msg file.
422      * Any multiparts are traversed until to grab their parts.
423      *
424      * @param filename The test msg to load.
425      * @return An array of parts.
426      * @throws Exception
427      */
428     Part[] createPartsFromMessage(final String filename) throws Exception
429     {
430         assertNotNull(filename);
431 
432         InputStream fis = null;
433 
434         try
435         {
436             fis = getTestEmailFile(filename);
437 
438             // build a message...
439             final Message message = new MimeMessage(Session.getDefaultInstance(new Properties()), fis);
440             final Part[] parts = this.getAllParts(message);
441             assertNotNull("parts", parts);
442             assertTrue("Expected atleast 1 part but got " + parts.length + " part(s)", parts.length > 0);
443 
444             return parts;
445         }
446         finally
447         {
448             IOUtils.closeQuietly(fis);
449         }
450     }
451 
452     private InputStream getTestEmailFile(String filename) throws FileNotFoundException
453     {
454         // try it as a class resource
455         return getClass().getResourceAsStream("/testmails/" + filename);
456     }
457 
458     /**
459      * This method along with its helpers returns all parts that contain content other than Multiparts( they are continuously
460      * expanded until the raw parts are located). All these parts are placed into an array and returned.
461      *
462      * @param message The message
463      * @return An array of parts nb, no MultiParts will be in here...
464      * @throws Exception only because javamail does...
465      */
466     Part[] getAllParts(final Message message) throws Exception
467     {
468         final List partsList = new ArrayList();
469         this.processMessageParts(message, partsList);
470         final Part[] parts = (Part[]) partsList.toArray(new Part[0]);
471         return parts;
472     }
473 
474     void processMessageParts(final Message message, final Collection parts) throws Exception
475     {
476         final Object content = message.getContent();
477         this.handlePartOrContent(content, parts);
478     }
479 
480     void handlePartOrContent(final Object partOrContent, final Collection parts) throws Exception
481     {
482         if (partOrContent instanceof Multipart)
483         {
484             this.processMultipartParts((Multipart) partOrContent, parts);
485             return;
486         }
487 
488         if (partOrContent instanceof BodyPart)
489         {
490             final BodyPart bodyPart = (BodyPart) partOrContent;
491             final Object bodyPartContent = bodyPart.getContent();
492 
493             if (bodyPartContent instanceof Multipart)
494             {
495                 this.handlePartOrContent(bodyPartContent, parts);
496             }
497             else
498             {
499                 parts.add(bodyPart);
500             }
501             return;
502         }
503         parts.add((Part) partOrContent);
504     }
505 
506     void processMultipartParts(final Multipart multipart, final Collection parts) throws Exception
507     {
508         final int partsCount = multipart.getCount();
509         for (int i = 0; i < partsCount; i++)
510         {
511             final BodyPart bodyPart = multipart.getBodyPart(i);
512             this.handlePartOrContent(bodyPart, parts);
513         }
514     }
515 
516 
517     static final String X_PKCS7_SIGNATURE = "application/x-pkcs7-signature";
518 
519     public void testPositivePartIsSignature() throws Exception
520     {
521         final Message message = createMessageWithAttachment(X_PKCS7_SIGNATURE, "attachment", "filename", "xxxx");
522         final boolean actual = MailUtils.isPartSignaturePKCS7(message);
523         assertTrue("The message being tested is of \"" + X_PKCS7_SIGNATURE + "\" but MailUtils returned false instead of true", actual);
524     }
525 
526     public void testNegativePartIsSignature() throws Exception
527     {
528         final Message message = createMessageWithAttachment("text/plain", "", null, "xxxx");
529         final boolean actual = MailUtils.isPartSignaturePKCS7(message);
530         assertFalse("The message being tested is of \"" + X_PKCS7_SIGNATURE + "\" but MailUtils returned true for a text/plain when it should have returned false.", actual);
531     }
532 
533     public void testTextPartContentIsEmpty() throws Exception
534     {
535         final Message message = createMessageWithAttachment("text/plain", "", null, "");
536         final boolean actual = MailUtils.isContentEmpty(message);
537         assertTrue("The plaintext message contains a part that should be empty.", actual);
538     }
539 
540 
541     public void testTextPartContentIsEmptyOnNullContent() throws Exception
542     {
543         final Message message = createMessageWithAttachment("text/plain", "", null, (String) null);
544         final boolean actual = MailUtils.isContentEmpty(message);
545         assertTrue("The plaintext message contains a part that should be empty.", actual);
546     }
547 
548     public void testTextPartContentIsEmptyBecauseContainsOnlyWhitespace() throws Exception
549     {
550         final Message message = createMessageWithAttachment("text/plain", "", null, "   ");
551         final boolean actual = MailUtils.isContentEmpty(message);
552         assertTrue("The plaintext message contains a part that should be empty.", actual);
553     }
554 
555     public void testBinaryAttachmentPartContentIsEmpty() throws Exception
556     {
557         final Message message = createMessageWithAttachment("binary/octet-stream", "file.bin", Part.ATTACHMENT, new byte[0]);
558         final boolean actual = MailUtils.isContentEmpty(message);
559         assertTrue("The attachment part should be empty.", actual);
560     }
561 
562     public void testBinaryAttachmentPartContentIsNotEmpty() throws Exception
563     {
564         final Message message = createMessageWithAttachment("binary/octet-stream", "file.bin", Part.ATTACHMENT, " NOT EMPTY!!!  ".getBytes());
565         final boolean actual = MailUtils.isContentEmpty(message);
566         assertFalse("The attachment part should be empty.", actual);
567     }
568 
569     public void testInlineAttachment() throws Exception
570     {
571         final Message message = createMessageWithAttachment("image/jpeg", "image.jpeg", Part.INLINE, " NOT EMPTY!!!  ".getBytes());
572         final boolean actual = MailUtils.isPartInline(message);
573         assertFalse("The attachment part is an inline part.", actual);
574     }
575 
576     /**
577      * This test tests the oddf case where a part has a content-id but is not base64 encoded(aka binary) and therefore
578      * fails to be recognised as a probable inline part.
579      *
580      * @throws Exception
581      */
582     public void testInlineAttachmentThatIsntBase64Encoded() throws Exception
583     {
584         final Message message = createMessageWithAttachment("image/jpeg", "image.jpeg", null, " NOT EMPTY!!!  ".getBytes());
585         message.setHeader("Content-ID", "1234567890");
586         final boolean actual = MailUtils.isPartInline(message);
587         assertFalse("The attachment part isnt not base64 encoded.", actual);
588     }
589 
590     /**
591      * Helper which creates a message with a part with the given disposition, filename and content etc.
592      *
593      * @param mimeType
594      * @param disposition
595      * @param filename
596      * @param content
597      * @return
598      * @throws Exception
599      */
600     Message createMessageWithAttachment(final String mimeType, final String disposition, final String filename, final String content)
601             throws Exception
602     {
603         assertNotNull("mimeType", mimeType);
604         assertTrue("mimeType must not be empty", mimeType.length() > 0);
605 
606         final Message message = new MimeMessage(Session.getDefaultInstance(new Properties()));
607         message.setContent(content, mimeType);
608         if (null != filename && filename.length() > 0)
609         {
610             message.setFileName(filename);
611         }
612         message.setHeader("Content-Type", mimeType);
613         message.setDisposition(disposition);
614         return message;
615     }
616 
617     Message createMessageWithAttachment(final String mimeType, final String disposition, final String filename, final byte[] content)
618             throws Exception
619     {
620         assertNotNull("mimeType", mimeType);
621         assertTrue("mimeType must not be empty", mimeType.length() > 0);
622 
623         final Message message = new MimeMessage(Session.getDefaultInstance(new Properties()));
624         message.setContent(new ByteArrayInputStream(content), mimeType);
625         if (null != filename && filename.length() > 0)
626         {
627             message.setFileName(filename);
628         }
629         message.setHeader("Content-Type", mimeType);
630         message.setDisposition(disposition);
631         return message;
632     }
633 }