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 }
239
240
241
242
243
244
245
246 private static byte[] toByteArray(InputStream in) throws IOException
247 {
248 ByteArrayOutputStream out = new ByteArrayOutputStream();
249 byte[] buf = new byte[512];
250 int count;
251 while ((count = in.read(buf)) != -1)
252 {
253 out.write(buf, 0, count);
254 }
255
256 out.close();
257 return out.toByteArray();
258 }
259
260
261
262
263
264
265
266
267
268
269
270 public static User getAuthorFromSender(Message message) throws MessagingException
271 {
272 return getFirstValidUser(message.getFrom());
273 }
274
275
276
277
278
279
280
281
282
283 public static User getFirstValidUser(Address[] addresses)
284 {
285 if (addresses == null || addresses.length == 0)
286 return null;
287
288 for (int i = 0; i < addresses.length; i++)
289 {
290 if (addresses[i] instanceof InternetAddress)
291 {
292 InternetAddress email = (InternetAddress) addresses[i];
293
294 try
295 {
296 User validUser = UserUtils.getUserByEmail(email.getAddress());
297 return validUser;
298 }
299 catch (EntityNotFoundException e)
300 {
301
302 }
303 }
304 }
305
306 return null;
307 }
308
309
310
311
312 public static boolean hasRecipient(String matchEmail, Message message) throws MessagingException
313 {
314 Address[] addresses = message.getAllRecipients();
315
316 if (addresses == null || addresses.length == 0)
317 return false;
318
319 for (int i = 0; i < addresses.length; i++)
320 {
321 InternetAddress email = (InternetAddress) addresses[i];
322
323 if (matchEmail.compareToIgnoreCase(email.getAddress()) == 0)
324 return true;
325 }
326
327 return false;
328 }
329
330
331
332
333
334
335
336
337
338 public static List
339 {
340
341 ArrayList senders = new ArrayList();
342 Address[] addresses = message.getFrom();
343 if (addresses != null)
344 {
345 for (int i = 0; i < addresses.length; i++)
346 {
347 if (addresses[i] instanceof InternetAddress)
348 {
349 InternetAddress addr = (InternetAddress) addresses[i];
350
351 String emailAddress = StringUtils.trimToNull(addr.getAddress());
352 if (emailAddress != null)
353 {
354 senders.add(emailAddress);
355 }
356 }
357 }
358 }
359 return senders;
360 }
361
362
363
364
365
366
367
368
369
370 public static MimeBodyPart createAttachmentMimeBodyPart(String path) throws MessagingException
371 {
372 MimeBodyPart attachmentPart = new MimeBodyPart();
373 DataSource source = new FileDataSource(path);
374 attachmentPart.setDataHandler(new DataHandler(source));
375
376 String fileName = extractFilenameFromPath(path);
377
378 attachmentPart.setFileName(fileName);
379 return attachmentPart;
380 }
381
382 private static String extractFilenameFromPath(String path) {
383 if (path == null) return null;
384 StringTokenizer st = new StringTokenizer(path, "\\/");
385
386 String fileName;
387 do
388 {
389 fileName = st.nextToken();
390 }
391 while (st.hasMoreTokens());
392 return fileName;
393 }
394
395 public static MimeBodyPart createZippedAttachmentMimeBodyPart(String path) throws MessagingException
396 {
397 File tmpFile = null;
398 String fileName = extractFilenameFromPath(path);
399
400 try {
401 tmpFile = File.createTempFile("atlassian", null);
402 FileOutputStream fout = new FileOutputStream(tmpFile);
403 ZipOutputStream zout = new ZipOutputStream(fout);
404 zout.putNextEntry(new ZipEntry(fileName));
405
406 InputStream in = new FileInputStream(path);
407 final byte[] buffer = new byte[ BUFFER_SIZE ];
408 int n = 0;
409 while ( -1 != (n = in.read(buffer)) ) {
410 zout.write(buffer, 0, n);
411 }
412 zout.close();
413 in.close();
414 log.debug("Wrote temporary zip of attachment to " + tmpFile);
415 } catch (FileNotFoundException e) {
416 String err = "Couldn't find file '"+path+"' on server: "+e;
417 log.error(err, e);
418 MimeBodyPart mimeBodyPart = new MimeBodyPart();
419 mimeBodyPart.setText(err);
420 return mimeBodyPart;
421 } catch (IOException e) {
422 String err = "Error zipping log file '"+path+"' on server: "+e;
423 log.error(err, e);
424 MimeBodyPart mimeBodyPart = new MimeBodyPart();
425 mimeBodyPart.setText(err);
426 return mimeBodyPart;
427 }
428 MimeBodyPart attachmentPart = new MimeBodyPart();
429 DataSource source = new FileDataSource(tmpFile);
430 attachmentPart.setDataHandler(new DataHandler(source));
431 attachmentPart.setFileName(fileName+".zip");
432 attachmentPart.setHeader("Content-Type", "application/zip");
433 return attachmentPart;
434 }
435
436 private static String getBodyFromMultipart(Multipart multipart) throws MessagingException, IOException
437 {
438 StringBuffer sb = new StringBuffer();
439 getBodyFromMultipart(multipart, sb);
440 return sb.toString();
441 }
442
443 private static void getBodyFromMultipart(Multipart multipart, StringBuffer sb) throws MessagingException, IOException
444 {
445 String multipartType = multipart.getContentType();
446
447
448 if(multipartType != null && compareContentType(multipartType, MULTIPART_ALTERNATE_CONTENT_TYPE))
449 {
450 BodyPart part = getFirstInlinePartWithMimeType(multipart, TEXT_CONTENT_TYPE);
451 if(part != null)
452 {
453 appendMultipartText(extractTextFromPart(part), sb);
454 }
455 else
456 {
457 part = getFirstInlinePartWithMimeType(multipart, HTML_CONTENT_TYPE);
458 appendMultipartText(extractTextFromPart(part), sb);
459 }
460 return;
461 }
462
463
464 for (int i = 0, n = multipart.getCount(); i < n; i++)
465 {
466 BodyPart part = multipart.getBodyPart(i);
467 String contentType = part.getContentType();
468
469 if (!Part.ATTACHMENT.equals(part.getDisposition()) && contentType != null)
470 {
471 try
472 {
473 String content = extractTextFromPart(part);
474 if (content != null)
475 {
476 appendMultipartText(content, sb);
477 }
478 else if(part.getContent() instanceof Multipart)
479 {
480 getBodyFromMultipart((Multipart) part.getContent(), sb);
481 }
482 }
483 catch (IOException exception)
484 {
485
486
487 log.warn("Error retrieving content from part '" + exception.getMessage() + "'", exception);
488 }
489 }
490 }
491 }
492
493 private static void appendMultipartText(String content, StringBuffer sb) throws IOException, MessagingException
494 {
495 if (content != null)
496 {
497 if(sb.length() > 0) sb.append("\n");
498 sb.append(content);
499 }
500 }
501
502 private static String extractTextFromPart(Part part) throws IOException, MessagingException,
503 UnsupportedEncodingException
504 {
505 if (part == null)
506 return null;
507
508 String content = null;
509
510 if (isPartPlainText(part))
511 {
512 try
513 {
514 content = (String) part.getContent();
515 }
516 catch (UnsupportedEncodingException e)
517 {
518
519 log.warn("Found unsupported encoding '" + e.getMessage() + "'. Reading content with "
520 + DEFAULT_ENCODING + " encoding.");
521 content = getBody(part, DEFAULT_ENCODING);
522 }
523 }
524 else if (isPartHtml(part))
525 {
526 content = htmlConverter.convert((String) part.getContent());
527 }
528
529 if (content == null)
530 {
531 log.warn("Unable to extract text from MIME part with Content-Type '" + part.getContentType());
532 }
533
534 return content;
535 }
536
537 private static String getBody(Part part, String charsetName) throws UnsupportedEncodingException,
538 IOException, MessagingException
539 {
540 Reader input = null;
541 StringWriter output = null;
542 try
543 {
544 input = new BufferedReader(new InputStreamReader(part.getInputStream(), charsetName));
545 output = new StringWriter();
546 IOUtils.copy(input, output);
547 return output.getBuffer().toString();
548 }
549 finally
550 {
551 IOUtils.closeQuietly(input);
552 IOUtils.closeQuietly(output);
553 }
554 }
555
556 private static BodyPart getFirstInlinePartWithMimeType(Multipart multipart, String mimeType) throws MessagingException
557 {
558 for (int i = 0, n = multipart.getCount(); i < n; i++)
559 {
560 BodyPart part = multipart.getBodyPart(i);
561 String contentType = part.getContentType();
562 if (!Part.ATTACHMENT.equals(part.getDisposition()) && contentType != null && compareContentType(contentType, mimeType))
563 {
564 return part;
565 }
566 }
567 return null;
568 }
569
570 private static boolean compareContentType(String contentType, String mimeType)
571 {
572 return contentType.toLowerCase().startsWith(mimeType);
573 }
574
575
576
577
578
579
580
581
582
583 static public boolean isPartHtml(final Part part) throws MessagingException
584 {
585 return HTML_CONTENT_TYPE.equals(MailUtils.getContentType(part));
586 }
587
588
589
590
591
592
593
594
595 static public boolean isPartPlainText(final Part part) throws MessagingException
596 {
597 return TEXT_CONTENT_TYPE.equals(MailUtils.getContentType(part));
598 }
599
600
601
602
603
604
605
606
607 static public boolean isPartMessageType(final Part part) throws MessagingException
608 {
609
610 return MESSAGE_CONTENT_TYPE.equals(getContentType(part));
611 }
612
613
614
615
616
617
618
619
620 static public boolean isPartRelated(final Part part) throws MessagingException
621 {
622 return MULTIPART_RELATED_CONTENT_TYPE.equals(getContentType(part));
623 }
624
625
626
627
628
629
630
631
632
633 static public String getContentType(final Part part) throws MessagingException
634 {
635 checkPartNotNull(part);
636
637 final String contentType = part.getContentType();
638 return getContentType(contentType);
639 }
640
641
642
643
644
645
646
647 static public String getContentType(final String headerValue)
648 {
649 checkHeaderValue(headerValue);
650
651 String out = headerValue;
652
653 final int semiColon = headerValue.indexOf(';');
654 if (-1 != semiColon)
655 {
656 out = headerValue.substring(0, semiColon);
657 }
658
659 return out.trim();
660 }
661
662 static private void checkHeaderValue(final String headerValue)
663 {
664 Validate.notEmpty(headerValue);
665 }
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683 static public boolean isContentEmpty(final Part part) throws MessagingException, IOException
684 {
685 checkPartNotNull(part);
686
687 boolean definitelyEmpty = false;
688 final Object content = part.getContent();
689 if (null == content)
690 {
691 definitelyEmpty = true;
692 }
693 else
694 {
695 if (content instanceof String)
696 {
697 final String stringContent = (String) content;
698 definitelyEmpty = StringUtils.isBlank(stringContent);
699 }
700
701 if (content instanceof InputStream)
702 {
703 final InputStream inputStream = (InputStream) content;
704 try
705 {
706
707
708 final int firstByte = inputStream.read();
709 definitelyEmpty = -1 == firstByte;
710
711 }
712 finally
713 {
714 IOUtils.closeQuietly(inputStream);
715 }
716 }
717 }
718
719 return definitelyEmpty;
720 }
721
722
723
724
725
726
727 static private void checkPartNotNull(final Part part)
728 {
729 Validate.notNull(part, "part should not be null.");
730 }
731
732
733
734
735
736
737
738
739
740
741
742
743
744 static public boolean isPartInline(final Part part) throws MessagingException
745 {
746 checkPartNotNull(part);
747
748 boolean inline = false;
749
750
751 final String disposition = part.getDisposition();
752 if (Part.INLINE.equalsIgnoreCase(disposition))
753 {
754 final String file = part.getFileName();
755 if(!StringUtils.isBlank(file))
756 {
757 inline = true;
758 }
759 return inline;
760 }
761
762 final boolean gotContentId = MailUtils.hasContentId(part);
763 if (!gotContentId)
764 {
765 return false;
766 }
767 final boolean gotBase64 = MailUtils.isContentBase64Encoded(part);
768 if (!gotBase64)
769 {
770 return false;
771 }
772
773 return true;
774 }
775
776 static private boolean hasContentId(final Part part) throws MessagingException
777 {
778 boolean gotContentId = false;
779 final String[] contentIds = part.getHeader(MailUtils.CONTENT_ID_HEADER);
780 if (null != contentIds)
781 {
782 for (int i = 0; i < contentIds.length; i++)
783 {
784 final String contentId = contentIds[i];
785 if (contentId != null && contentId.length() > 0)
786 {
787 gotContentId = true;
788 break;
789 }
790 }
791 }
792 return gotContentId;
793 }
794
795
796
797
798
799
800
801
802 static private boolean isContentBase64Encoded(final Part part) throws MessagingException
803 {
804 boolean gotBase64 = false;
805 final String[] contentTransferEncodings = part.getHeader(CONTENT_TRANSFER_ENCODING_HEADER);
806 if (null != contentTransferEncodings)
807 {
808 for (int i = 0; i < contentTransferEncodings.length; i++)
809 {
810 final String contentTransferEncoding = contentTransferEncodings[i];
811 if ("base64".equals(contentTransferEncoding))
812 {
813 gotBase64 = true;
814 break;
815 }
816 }
817 }
818
819 return gotBase64;
820 }
821
822
823
824
825
826
827
828
829
830 static public boolean isPartAttachment(final Part part) throws MessagingException
831 {
832 checkPartNotNull(part);
833 return Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition());
834 }
835
836
837
838
839
840
841
842
843
844
845
846 static public String fixMimeEncodedFilename(final String filename) throws IOException
847 {
848 String newFilename = filename;
849 if (filename.startsWith("=?") || filename.endsWith("?="))
850 {
851 newFilename = MimeUtility.decodeText(filename);
852 }
853 return newFilename;
854 }
855
856
857
858
859
860
861
862
863
864 static public boolean isPartSignaturePKCS7(final Part part) throws MessagingException
865 {
866 MailUtils.checkPartNotNull(part);
867 final String contentType = MailUtils.getContentType(part).toLowerCase(Locale.getDefault());
868 return contentType.startsWith(CONTENT_TYPE_PKCS7) || contentType.startsWith(CONTENT_TYPE_X_PKCS7);
869 }
870 }