1
2
3
4
5
6
7
8 package com.atlassian.mail;
9
10 import com.atlassian.core.user.UserUtils;
11 import com.opensymphony.user.EntityNotFoundException;
12 import com.opensymphony.user.User;
13 import org.apache.commons.io.IOUtils;
14 import org.apache.commons.lang.StringUtils;
15 import org.apache.commons.lang.Validate;
16 import org.apache.log4j.Logger;
17
18 import java.io.BufferedReader;
19 import java.io.ByteArrayOutputStream;
20 import java.io.File;
21 import java.io.FileInputStream;
22 import java.io.FileNotFoundException;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.InputStreamReader;
27 import java.io.Reader;
28 import java.io.StringWriter;
29 import java.io.UnsupportedEncodingException;
30 import java.util.ArrayList;
31 import java.util.List;
32 import java.util.Locale;
33 import java.util.StringTokenizer;
34 import java.util.zip.ZipEntry;
35 import java.util.zip.ZipOutputStream;
36 import javax.activation.DataHandler;
37 import javax.activation.DataSource;
38 import javax.activation.FileDataSource;
39 import javax.mail.Address;
40 import javax.mail.BodyPart;
41 import javax.mail.Message;
42 import javax.mail.MessagingException;
43 import javax.mail.Multipart;
44 import javax.mail.Part;
45 import javax.mail.internet.AddressException;
46 import javax.mail.internet.InternetAddress;
47 import javax.mail.internet.MimeBodyPart;
48 import javax.mail.internet.MimeUtility;
49
50
51
52
53
54
55 public class MailUtils
56 {
57 private static final String DEFAULT_ENCODING = "ISO-8859-1";
58
59 static final int BUFFER_SIZE = 64 * 1024;
60 static final String MULTIPART_ALTERNATE_CONTENT_TYPE = "multipart/alternative";
61 static final String MULTIPART_RELATED_CONTENT_TYPE = "multipart/related";
62 static final String TEXT_CONTENT_TYPE = "text/plain";
63 static final String MESSAGE_CONTENT_TYPE = "message/rfc822";
64 static final String HTML_CONTENT_TYPE = "text/html";
65 static final String CONTENT_TYPE_X_PKCS7 = "application/x-pkcs7-signature";
66 static final String CONTENT_TYPE_PKCS7 = "application/pkcs7-signature";
67
68 private static final HtmlToTextConverter htmlConverter = new HtmlToTextConverter();
69 private static final Logger log = Logger.getLogger(MailUtils.class);
70
71
72
73
74 private static final String CONTENT_TRANSFER_ENCODING_HEADER = "Content-Transfer-Encoding";
75
76
77
78
79 private static final String CONTENT_ID_HEADER = "Content-ID";
80
81
82
83
84
85 public static class Attachment {
86 private final String contentType;
87 private final String fileName;
88 private final byte[] contents;
89
90 public Attachment(String contentType, String fileName, byte[] contents)
91 {
92 this.contentType = contentType;
93 this.fileName = fileName;
94 this.contents = contents;
95 }
96
97 public String getContentType()
98 {
99 return contentType;
100 }
101
102 public byte[] getContents()
103 {
104 return contents;
105 }
106
107 public String getFilename()
108 {
109 return fileName;
110 }
111 }
112
113
114
115
116 public static InternetAddress[] parseAddresses(String addresses) throws AddressException
117 {
118 List list = new ArrayList();
119 list.clear();
120 StringTokenizer st = new StringTokenizer(addresses, ", ");
121 while (st.hasMoreTokens())
122 {
123 list.add(new InternetAddress(st.nextToken()));
124 }
125 return (InternetAddress[]) list.toArray(new InternetAddress[list.size()]);
126 }
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149 public static String getBody(Message message) throws MessagingException
150 {
151 try
152 {
153 String content = extractTextFromPart(message);
154
155 if (content == null)
156 {
157 if (message.getContent() instanceof Multipart)
158 {
159 content = getBodyFromMultipart((Multipart) message.getContent());
160 }
161 }
162
163 if (content == null)
164 {
165
166 log.info("Could not find any body to extract from the message");
167 }
168
169 return content;
170 }
171 catch (ClassCastException cce)
172 {
173 log.info("Exception getting the content type of message - probably not of type 'String': " + cce.getMessage());
174 return null;
175 }
176 catch (IOException e)
177 {
178 log.info("IOException whilst getting message content " + e.getMessage());
179 return null;
180 }
181 }
182
183
184
185
186
187
188
189 public static Attachment[] getAttachments(Message message) throws MessagingException, IOException
190 {
191 List attachments = new ArrayList();
192
193 if (message.getContent() instanceof Multipart)
194 {
195 addAttachments(attachments, (Multipart)message.getContent());
196 }
197
198 return (Attachment[]) attachments.toArray(new Attachment[attachments.size()]);
199 }
200
201 private static void addAttachments(List attachments, Multipart parts) throws MessagingException, IOException
202 {
203 for (int i = 0, n = parts.getCount(); i < n; i++)
204 {
205 BodyPart part = parts.getBodyPart(i);
206
207 if (isAttachment(part))
208 {
209 InputStream content = part.getInputStream();
210 String contentType = part.getContentType();
211
212 attachments.add(new Attachment(contentType, part.getFileName(), toByteArray(content)));
213 }
214 else
215 {
216 try
217 {
218 if (part.getContent() instanceof Multipart)
219 {
220 addAttachments(attachments, (Multipart) part.getContent());
221 }
222 }
223 catch (UnsupportedEncodingException e)
224 {
225
226
227 log.warn("Unsupported encoding found for part while trying to discover attachments. "
228 + "Attachment will be ignored.", e);
229 }
230 }
231 }
232 }
233
234 private static boolean isAttachment(BodyPart part)
235 throws MessagingException
236 {
237 return Part.ATTACHMENT.equals(part.getDisposition()) || Part.INLINE.equals(part.getDisposition())
238 || (part.getDisposition() == null && part.getFileName() != null);
239 }
240
241
242
243
244
245
246
247 private static byte[] toByteArray(InputStream in) throws IOException
248 {
249 ByteArrayOutputStream out = new ByteArrayOutputStream();
250 byte[] buf = new byte[512];
251 int count;
252 while ((count = in.read(buf)) != -1)
253 {
254 out.write(buf, 0, count);
255 }
256
257 out.close();
258 return out.toByteArray();
259 }
260
261
262
263
264
265
266
267
268
269
270
271 public static User getAuthorFromSender(Message message) throws MessagingException
272 {
273 return getFirstValidUser(message.getFrom());
274 }
275
276
277
278
279
280
281
282
283
284 public static User getFirstValidUser(Address[] addresses)
285 {
286 if (addresses == null || addresses.length == 0)
287 return null;
288
289 for (int i = 0; i < addresses.length; i++)
290 {
291 if (addresses[i] instanceof InternetAddress)
292 {
293 InternetAddress email = (InternetAddress) addresses[i];
294
295 try
296 {
297 User validUser = UserUtils.getUserByEmail(email.getAddress());
298 return validUser;
299 }
300 catch (EntityNotFoundException e)
301 {
302
303 }
304 }
305 }
306
307 return null;
308 }
309
310
311
312
313 public static boolean hasRecipient(String matchEmail, Message message) throws MessagingException
314 {
315 Address[] addresses = message.getAllRecipients();
316
317 if (addresses == null || addresses.length == 0)
318 return false;
319
320 for (int i = 0; i < addresses.length; i++)
321 {
322 InternetAddress email = (InternetAddress) addresses[i];
323
324 if (matchEmail.compareToIgnoreCase(email.getAddress()) == 0)
325 return true;
326 }
327
328 return false;
329 }
330
331
332
333
334
335
336
337
338
339 public static List
340 {
341
342 ArrayList senders = new ArrayList();
343 Address[] addresses = message.getFrom();
344 if (addresses != null)
345 {
346 for (int i = 0; i < addresses.length; i++)
347 {
348 if (addresses[i] instanceof InternetAddress)
349 {
350 InternetAddress addr = (InternetAddress) addresses[i];
351
352 String emailAddress = StringUtils.trimToNull(addr.getAddress());
353 if (emailAddress != null)
354 {
355 senders.add(emailAddress);
356 }
357 }
358 }
359 }
360 return senders;
361 }
362
363
364
365
366
367
368
369
370
371 public static MimeBodyPart createAttachmentMimeBodyPart(String path) throws MessagingException
372 {
373 MimeBodyPart attachmentPart = new MimeBodyPart();
374 DataSource source = new FileDataSource(path);
375 attachmentPart.setDataHandler(new DataHandler(source));
376
377 String fileName = extractFilenameFromPath(path);
378
379 attachmentPart.setFileName(fileName);
380 return attachmentPart;
381 }
382
383 private static String extractFilenameFromPath(String path) {
384 if (path == null) return null;
385 StringTokenizer st = new StringTokenizer(path, "\\/");
386
387 String fileName;
388 do
389 {
390 fileName = st.nextToken();
391 }
392 while (st.hasMoreTokens());
393 return fileName;
394 }
395
396 public static MimeBodyPart createZippedAttachmentMimeBodyPart(String path) throws MessagingException
397 {
398 File tmpFile = null;
399 String fileName = extractFilenameFromPath(path);
400
401 try {
402 tmpFile = File.createTempFile("atlassian", null);
403 FileOutputStream fout = new FileOutputStream(tmpFile);
404 ZipOutputStream zout = new ZipOutputStream(fout);
405 zout.putNextEntry(new ZipEntry(fileName));
406
407 InputStream in = new FileInputStream(path);
408 final byte[] buffer = new byte[ BUFFER_SIZE ];
409 int n = 0;
410 while ( -1 != (n = in.read(buffer)) ) {
411 zout.write(buffer, 0, n);
412 }
413 zout.close();
414 in.close();
415 log.debug("Wrote temporary zip of attachment to " + tmpFile);
416 } catch (FileNotFoundException e) {
417 String err = "Couldn't find file '"+path+"' on server: "+e;
418 log.error(err, e);
419 MimeBodyPart mimeBodyPart = new MimeBodyPart();
420 mimeBodyPart.setText(err);
421 return mimeBodyPart;
422 } catch (IOException e) {
423 String err = "Error zipping log file '"+path+"' on server: "+e;
424 log.error(err, e);
425 MimeBodyPart mimeBodyPart = new MimeBodyPart();
426 mimeBodyPart.setText(err);
427 return mimeBodyPart;
428 }
429 MimeBodyPart attachmentPart = new MimeBodyPart();
430 DataSource source = new FileDataSource(tmpFile);
431 attachmentPart.setDataHandler(new DataHandler(source));
432 attachmentPart.setFileName(fileName+".zip");
433 attachmentPart.setHeader("Content-Type", "application/zip");
434 return attachmentPart;
435 }
436
437 private static String getBodyFromMultipart(Multipart multipart) throws MessagingException, IOException
438 {
439 StringBuffer sb = new StringBuffer();
440 getBodyFromMultipart(multipart, sb);
441 return sb.toString();
442 }
443
444 private static void getBodyFromMultipart(Multipart multipart, StringBuffer sb) throws MessagingException, IOException
445 {
446 String multipartType = multipart.getContentType();
447
448
449 if(multipartType != null && compareContentType(multipartType, MULTIPART_ALTERNATE_CONTENT_TYPE))
450 {
451 BodyPart part = getFirstInlinePartWithMimeType(multipart, TEXT_CONTENT_TYPE);
452 if(part != null)
453 {
454 appendMultipartText(extractTextFromPart(part), sb);
455 }
456 else
457 {
458 part = getFirstInlinePartWithMimeType(multipart, HTML_CONTENT_TYPE);
459 appendMultipartText(extractTextFromPart(part), sb);
460 }
461 return;
462 }
463
464
465 for (int i = 0, n = multipart.getCount(); i < n; i++)
466 {
467 BodyPart part = multipart.getBodyPart(i);
468 String contentType = part.getContentType();
469
470 if (!Part.ATTACHMENT.equals(part.getDisposition()) && contentType != null)
471 {
472 try
473 {
474 String content = extractTextFromPart(part);
475 if (content != null)
476 {
477 appendMultipartText(content, sb);
478 }
479 else if(part.getContent() instanceof Multipart)
480 {
481 getBodyFromMultipart((Multipart) part.getContent(), sb);
482 }
483 }
484 catch (IOException exception)
485 {
486
487
488 log.warn("Error retrieving content from part '" + exception.getMessage() + "'", exception);
489 }
490 }
491 }
492 }
493
494 private static void appendMultipartText(String content, StringBuffer sb) throws IOException, MessagingException
495 {
496 if (content != null)
497 {
498 if(sb.length() > 0) sb.append("\n");
499 sb.append(content);
500 }
501 }
502
503 private static String extractTextFromPart(Part part) throws IOException, MessagingException,
504 UnsupportedEncodingException
505 {
506 if (part == null)
507 return null;
508
509 String content = null;
510
511 if (isPartPlainText(part))
512 {
513 try
514 {
515 content = (String) part.getContent();
516 }
517 catch (UnsupportedEncodingException e)
518 {
519
520 log.warn("Found unsupported encoding '" + e.getMessage() + "'. Reading content with "
521 + DEFAULT_ENCODING + " encoding.");
522 content = getBody(part, DEFAULT_ENCODING);
523 }
524 }
525 else if (isPartHtml(part))
526 {
527 content = htmlConverter.convert((String) part.getContent());
528 }
529
530 if (content == null)
531 {
532 log.warn("Unable to extract text from MIME part with Content-Type '" + part.getContentType());
533 }
534
535 return content;
536 }
537
538 private static String getBody(Part part, String charsetName) throws UnsupportedEncodingException,
539 IOException, MessagingException
540 {
541 Reader input = null;
542 StringWriter output = null;
543 try
544 {
545 input = new BufferedReader(new InputStreamReader(part.getInputStream(), charsetName));
546 output = new StringWriter();
547 IOUtils.copy(input, output);
548 return output.getBuffer().toString();
549 }
550 finally
551 {
552 IOUtils.closeQuietly(input);
553 IOUtils.closeQuietly(output);
554 }
555 }
556
557 private static BodyPart getFirstInlinePartWithMimeType(Multipart multipart, String mimeType) throws MessagingException
558 {
559 for (int i = 0, n = multipart.getCount(); i < n; i++)
560 {
561 BodyPart part = multipart.getBodyPart(i);
562 String contentType = part.getContentType();
563 if (!Part.ATTACHMENT.equals(part.getDisposition()) && contentType != null && compareContentType(contentType, mimeType))
564 {
565 return part;
566 }
567 }
568 return null;
569 }
570
571 private static boolean compareContentType(String contentType, String mimeType)
572 {
573 return contentType.toLowerCase().startsWith(mimeType);
574 }
575
576
577
578
579
580
581
582
583
584 static public boolean isPartHtml(final Part part) throws MessagingException
585 {
586 final String contentType = MailUtils.getContentType(part);
587 return HTML_CONTENT_TYPE.equalsIgnoreCase(contentType);
588 }
589
590
591
592
593
594
595
596
597 static public boolean isPartPlainText(final Part part) throws MessagingException
598 {
599 final String contentType = MailUtils.getContentType(part);
600 return TEXT_CONTENT_TYPE.equalsIgnoreCase(contentType);
601 }
602
603
604
605
606
607
608
609
610 static public boolean isPartMessageType(final Part part) throws MessagingException
611 {
612
613 final String contentType = MailUtils.getContentType(part);
614 return MESSAGE_CONTENT_TYPE.equalsIgnoreCase(contentType);
615 }
616
617
618
619
620
621
622
623
624 static public boolean isPartRelated(final Part part) throws MessagingException
625 {
626 final String contentType = getContentType(part);
627 return MULTIPART_RELATED_CONTENT_TYPE.equalsIgnoreCase(contentType);
628 }
629
630
631
632
633
634
635
636
637
638 static public String getContentType(final Part part) throws MessagingException
639 {
640 checkPartNotNull(part);
641
642 final String contentType = part.getContentType();
643 return getContentType(contentType);
644 }
645
646
647
648
649
650
651
652 static public String getContentType(final String headerValue)
653 {
654 checkHeaderValue(headerValue);
655
656 String out = headerValue;
657
658 final int semiColon = headerValue.indexOf(';');
659 if (-1 != semiColon)
660 {
661 out = headerValue.substring(0, semiColon);
662 }
663
664 return out.trim();
665 }
666
667 static private void checkHeaderValue(final String headerValue)
668 {
669 Validate.notEmpty(headerValue);
670 }
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688 static public boolean isContentEmpty(final Part part) throws MessagingException, IOException
689 {
690 checkPartNotNull(part);
691
692 boolean definitelyEmpty = false;
693 final Object content = part.getContent();
694 if (null == content)
695 {
696 definitelyEmpty = true;
697 }
698 else
699 {
700 if (content instanceof String)
701 {
702 final String stringContent = (String) content;
703 definitelyEmpty = StringUtils.isBlank(stringContent);
704 }
705
706 if (content instanceof InputStream)
707 {
708 final InputStream inputStream = (InputStream) content;
709 try
710 {
711
712
713 final int firstByte = inputStream.read();
714 definitelyEmpty = -1 == firstByte;
715
716 }
717 finally
718 {
719 IOUtils.closeQuietly(inputStream);
720 }
721 }
722 }
723
724 return definitelyEmpty;
725 }
726
727
728
729
730
731
732 static private void checkPartNotNull(final Part part)
733 {
734 Validate.notNull(part, "part should not be null.");
735 }
736
737
738
739
740
741
742
743
744
745
746
747
748
749 static public boolean isPartInline(final Part part) throws MessagingException
750 {
751 checkPartNotNull(part);
752
753 boolean inline = false;
754
755
756 final String disposition = part.getDisposition();
757 if (Part.INLINE.equalsIgnoreCase(disposition))
758 {
759 final String file = part.getFileName();
760 if(!StringUtils.isBlank(file))
761 {
762 inline = true;
763 }
764 return inline;
765 }
766
767 final boolean gotContentId = MailUtils.hasContentId(part);
768 if (!gotContentId)
769 {
770 return false;
771 }
772 final boolean gotBase64 = MailUtils.isContentBase64Encoded(part);
773 if (!gotBase64)
774 {
775 return false;
776 }
777
778 return true;
779 }
780
781 static private boolean hasContentId(final Part part) throws MessagingException
782 {
783 boolean gotContentId = false;
784 final String[] contentIds = part.getHeader(MailUtils.CONTENT_ID_HEADER);
785 if (null != contentIds)
786 {
787 for (int i = 0; i < contentIds.length; i++)
788 {
789 final String contentId = contentIds[i];
790 if (contentId != null && contentId.length() > 0)
791 {
792 gotContentId = true;
793 break;
794 }
795 }
796 }
797 return gotContentId;
798 }
799
800
801
802
803
804
805
806
807 static private boolean isContentBase64Encoded(final Part part) throws MessagingException
808 {
809 boolean gotBase64 = false;
810 final String[] contentTransferEncodings = part.getHeader(CONTENT_TRANSFER_ENCODING_HEADER);
811 if (null != contentTransferEncodings)
812 {
813 for (int i = 0; i < contentTransferEncodings.length; i++)
814 {
815 final String contentTransferEncoding = contentTransferEncodings[i];
816 if ("base64".equals(contentTransferEncoding))
817 {
818 gotBase64 = true;
819 break;
820 }
821 }
822 }
823
824 return gotBase64;
825 }
826
827
828
829
830
831
832
833
834
835 static public boolean isPartAttachment(final Part part) throws MessagingException
836 {
837 checkPartNotNull(part);
838 return Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition());
839 }
840
841
842
843
844
845
846
847
848
849
850
851 static public String fixMimeEncodedFilename(final String filename) throws IOException
852 {
853 String newFilename = filename;
854 if (filename.startsWith("=?") || filename.endsWith("?="))
855 {
856 newFilename = MimeUtility.decodeText(filename);
857 }
858 return newFilename;
859 }
860
861
862
863
864
865
866
867
868
869 static public boolean isPartSignaturePKCS7(final Part part) throws MessagingException
870 {
871 MailUtils.checkPartNotNull(part);
872 final String contentType = MailUtils.getContentType(part).toLowerCase(Locale.getDefault());
873 return contentType.startsWith(CONTENT_TYPE_PKCS7) || contentType.startsWith(CONTENT_TYPE_X_PKCS7);
874 }
875 }