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