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