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