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.util.ArrayList;
21  import java.util.List;
22  import java.util.Locale;
23  import java.util.StringTokenizer;
24  import java.util.zip.ZipEntry;
25  import java.util.zip.ZipOutputStream;
26  import javax.activation.DataHandler;
27  import javax.activation.DataSource;
28  import javax.activation.FileDataSource;
29  import javax.mail.Address;
30  import javax.mail.BodyPart;
31  import javax.mail.Message;
32  import javax.mail.MessagingException;
33  import javax.mail.Multipart;
34  import javax.mail.Part;
35  import javax.mail.internet.AddressException;
36  import javax.mail.internet.InternetAddress;
37  import javax.mail.internet.MimeBodyPart;
38  import javax.mail.internet.MimeUtility;
39  
40  // TODO: Doesn't handle charsets/encoding very well. Or, indeed, at all.
41  /**
42   * This class contains a bunch of static helper methods that make life a bit easier particularly with the processing of
43   * Parts.
44   */
45  public class MailUtils
46  {
47      private static final String DEFAULT_ENCODING = "ISO-8859-1";
48      
49      static final int BUFFER_SIZE = 64 * 1024;
50      static final String MULTIPART_ALTERNATE_CONTENT_TYPE = "multipart/alternative";
51      static final String MULTIPART_RELATED_CONTENT_TYPE = "multipart/related";
52      static final String TEXT_CONTENT_TYPE = "text/plain";
53      static final String MESSAGE_CONTENT_TYPE = "message/rfc822";
54      static final String HTML_CONTENT_TYPE = "text/html";
55      static final String CONTENT_TYPE_X_PKCS7 = "application/x-pkcs7-signature";
56      static final String CONTENT_TYPE_PKCS7 = "application/pkcs7-signature";
57  
58      private static final HtmlToTextConverter htmlConverter = new HtmlToTextConverter();
59      private static final Logger log = Logger.getLogger(MailUtils.class);
60  
61      /**
62       * The content transfer encoding header, which is used to identify whether a part is base64 encoded.
63       */
64      private static final String CONTENT_TRANSFER_ENCODING_HEADER = "Content-Transfer-Encoding";
65  
66      /**
67       * Content header id
68       */
69      private static final String CONTENT_ID_HEADER = "Content-ID";
70  
71      /**
72       * Very simple representation of a mail attachment after it has been
73       * extracted from a message.
74       */
75      public static class Attachment {
76          private final String contentType;
77          private final String fileName;
78          private final byte[] contents;
79  
80          public Attachment(String contentType, String fileName, byte[] contents)
81          {
82              this.contentType = contentType;
83              this.fileName = fileName;
84              this.contents = contents;
85          }
86  
87          public String getContentType()
88          {
89              return contentType;
90          }
91  
92          public byte[] getContents()
93          {
94              return contents;
95          }
96  
97          public String getFilename()
98          {
99              return fileName;
100         }
101     }
102 
103     /**
104      * Parse addresses from a comma (and space) separated string into the proper array
105      */
106     public static InternetAddress[] parseAddresses(String addresses) throws AddressException
107     {
108         List<InternetAddress> list = new ArrayList<InternetAddress>();
109         list.clear();
110         StringTokenizer st = new StringTokenizer(addresses, ", ");
111         while (st.hasMoreTokens())
112         {
113             list.add(new InternetAddress(st.nextToken()));
114         }
115         return list.toArray(new InternetAddress[list.size()]);
116     }
117 
118     /**
119      * Get the body of the message as a String. The algorithm for finding the body is as follows:
120      *
121      * <ol><li>If the message is a single part, and that part is text/plain, return it.
122      *     <li>If the message is a single part, and that part is text/html, convert it to
123      *         text (stripping out the HTML) and return it.
124      *     <li>If the message is multi-part, return the first text/plain part that isn't marked
125      *         explicitly as an attachment.
126      *     <li>If the message is multi-part, but does not contain any text/plain parts, return
127      *         the first text/html part that isn't marked explicitly as an attachment, converting
128      *         it to text and stripping the HTML.
129      *     <li>If nothing is found in any of the steps above, return null.
130      * </ol>
131      *
132      * <p>Note: If the message contains nested multipart parts, an HTML part nested at a higher level will
133      * take precedence over a text part nested deeper.
134      *
135      * @param message The message to retrieve the body from
136      * @return The message body, or null if the message could not be parsed
137      * @throws javax.mail.MessagingException If there was an error getting the content from the message
138      */
139     public static String getBody(Message message) throws MessagingException
140     {
141         try
142         {
143             String content = extractTextFromPart(message);
144                 
145             if (content == null)
146             {
147                 if (message.getContent() instanceof Multipart)
148                 {
149                     content = getBodyFromMultipart((Multipart) message.getContent());
150                 }
151             }
152 
153             if (content == null)
154             {
155                 //didn't match anything above
156                 log.info("Could not find any body to extract from the message");
157             }
158             
159             return content;
160         }
161         catch (ClassCastException cce)
162         {
163             log.info("Exception getting the content type of message - probably not of type 'String': " + cce.getMessage());
164             return null;
165         }
166         catch (IOException e)
167         {
168             log.info("IOException whilst getting message content " + e.getMessage());
169             return null;
170         }
171     }
172 
173     /**
174      * Gets all parts of a message that are attachments rather than alternative inline bits.
175      *
176      * @param message the message from which to extract the attachments
177      * @return an array of the extracted attachments
178      */
179     public static Attachment[] getAttachments(Message message) throws MessagingException, IOException
180     {
181         List<Attachment> attachments = new ArrayList<Attachment>();
182 
183         if (message.getContent() instanceof Multipart)
184         {
185             addAttachments(attachments, (Multipart)message.getContent());
186         }
187 
188         return attachments.toArray(new Attachment[attachments.size()]);
189     }
190 
191     private static void addAttachments(List<Attachment> attachments, Multipart parts) throws MessagingException, IOException
192     {
193         for (int i = 0, n = parts.getCount(); i < n; i++)
194         {
195             BodyPart part = parts.getBodyPart(i);
196 
197             if (isAttachment(part))
198             {
199                 InputStream content = part.getInputStream();
200                 String contentType = part.getContentType();
201 
202                 attachments.add(new Attachment(contentType, part.getFileName(), toByteArray(content)));
203             }
204             else
205             {
206                 try
207                 {
208                     if (part.getContent() instanceof Multipart)
209                     {
210                         addAttachments(attachments, (Multipart) part.getContent());
211                     }
212                 }
213                 catch (UnsupportedEncodingException e)
214                 {
215                     // ignore because it's probably not a multipart part anyway
216                     // if the encoding is unsupported
217                     log.warn("Unsupported encoding found for part while trying to discover attachments. "
218                             + "Attachment will be ignored.", e);
219                 }
220             }
221         }
222     }
223 
224     private static boolean isAttachment(BodyPart part)
225             throws MessagingException
226     {
227         return Part.ATTACHMENT.equals(part.getDisposition()) || Part.INLINE.equals(part.getDisposition())
228 				|| (part.getDisposition() == null && part.getFileName() != null);
229     }
230 
231     /**
232      * Convert the contents of an input stream into a byte array.
233      *
234      * @param in
235      * @return the contents of that stream as a byte array.
236      */
237     private static byte[] toByteArray(InputStream in) throws IOException
238     {
239         ByteArrayOutputStream out = new ByteArrayOutputStream();
240         byte[] buf = new byte[512];
241         int count;
242         while ((count = in.read(buf)) != -1)
243         {
244             out.write(buf, 0, count);
245         }
246 
247         out.close();
248         return out.toByteArray();
249     }
250 
251     /**
252      * @return true if at least one of the recipients matches the email address given.
253      */
254     public static boolean hasRecipient(String matchEmail, Message message) throws MessagingException
255     {
256         Address[] addresses = message.getAllRecipients();
257 
258         if (addresses == null || addresses.length == 0)
259             return false;
260 
261         for (int i = 0; i < addresses.length; i++)
262         {
263             InternetAddress email = (InternetAddress) addresses[i];
264 
265             if (matchEmail.compareToIgnoreCase(email.getAddress()) == 0)
266                 return true;
267         }
268 
269         return false;
270     }
271 
272     /**
273      * Returns a List<String> of trimmed non-null email addresses from the
274      * given potentially dirty pile of addresses listed as senders on the
275      * given message.
276      * @param message the message from which to get senders.
277      * @return a nice List<String> of email addresses.
278      * @throws MessagingException if the senders can't be retrieved from message.
279      */
280     public static List<String> getSenders(Message message) throws MessagingException
281     {
282 
283         ArrayList<String> senders = new ArrayList<String>();
284         Address[] addresses = message.getFrom();
285         if (addresses != null)
286         {
287             for (int i = 0; i < addresses.length; i++)
288             {
289                 if (addresses[i] instanceof InternetAddress)
290                 {
291                     InternetAddress addr = (InternetAddress) addresses[i];
292                     // Trim down the email address to remove any whitespace etc.
293                     String emailAddress = StringUtils.trimToNull(addr.getAddress());
294                     if (emailAddress != null)
295                     {
296                         senders.add(emailAddress);
297                     }
298                 }
299             }
300         }
301         return senders;
302     }
303 
304     /**
305      * Produces a mimebodypart object from an attachment file path. An attachment needs to be in this form to be attached
306      * to an email for sending
307      *
308      * @param path
309      * @return
310      * @throws MessagingException
311      */
312     public static MimeBodyPart createAttachmentMimeBodyPart(String path) throws MessagingException
313     {
314         MimeBodyPart attachmentPart = new MimeBodyPart();
315         DataSource source = new FileDataSource(path);
316         attachmentPart.setDataHandler(new DataHandler(source));
317 
318         String fileName = extractFilenameFromPath(path);
319 
320         attachmentPart.setFileName(fileName);
321         return attachmentPart;
322     }
323 
324     private static String extractFilenameFromPath(String path) {
325         if (path == null) return null;
326         StringTokenizer st = new StringTokenizer(path, "\\/");
327 
328         String fileName;
329         do
330         {
331             fileName = st.nextToken();
332         }
333         while (st.hasMoreTokens());
334         return fileName;
335     }
336 
337     public static MimeBodyPart createZippedAttachmentMimeBodyPart(String path) throws MessagingException
338     {
339         File tmpFile = null;
340         String fileName = extractFilenameFromPath(path);
341 
342         try {
343             tmpFile = File.createTempFile("atlassian", null);
344             FileOutputStream fout = new FileOutputStream(tmpFile);
345             ZipOutputStream zout = new ZipOutputStream(fout);
346             zout.putNextEntry(new ZipEntry(fileName));
347 
348             InputStream in = new FileInputStream(path);
349             final byte[] buffer = new byte[ BUFFER_SIZE ];
350             int n = 0;
351             while ( -1 != (n = in.read(buffer)) ) {
352                 zout.write(buffer, 0, n);
353             }
354             zout.close();
355             in.close();
356             log.debug("Wrote temporary zip of attachment to " + tmpFile);
357         } catch (FileNotFoundException e) {
358             String err = "Couldn't find file '"+path+"' on server: "+e;
359             log.error(err, e);
360             MimeBodyPart mimeBodyPart = new MimeBodyPart();
361             mimeBodyPart.setText(err);
362             return mimeBodyPart;
363         } catch (IOException e) {
364             String err = "Error zipping log file '"+path+"' on server: "+e;
365             log.error(err, e);
366             MimeBodyPart mimeBodyPart = new MimeBodyPart();
367             mimeBodyPart.setText(err);
368             return mimeBodyPart;
369         }
370         MimeBodyPart attachmentPart = new MimeBodyPart();
371         DataSource source = new FileDataSource(tmpFile);
372         attachmentPart.setDataHandler(new DataHandler(source));
373         attachmentPart.setFileName(fileName+".zip");
374         attachmentPart.setHeader("Content-Type", "application/zip");
375         return attachmentPart;
376     }
377 
378     private static String getBodyFromMultipart(Multipart multipart) throws MessagingException, IOException
379     {
380         StringBuffer sb = new StringBuffer();
381         getBodyFromMultipart(multipart, sb);
382         return sb.toString();
383     }
384 
385     private static void getBodyFromMultipart(Multipart multipart, StringBuffer sb) throws MessagingException, IOException
386     {
387         String multipartType = multipart.getContentType();
388 
389         // if an multipart/alternative type we just get the first text or html content found
390         if(multipartType != null && compareContentType(multipartType, MULTIPART_ALTERNATE_CONTENT_TYPE))
391         {
392             BodyPart part = getFirstInlinePartWithMimeType(multipart, TEXT_CONTENT_TYPE);
393             if(part != null)
394             {
395                 appendMultipartText(extractTextFromPart(part), sb);
396             }
397             else
398             {
399                 part = getFirstInlinePartWithMimeType(multipart, HTML_CONTENT_TYPE);
400                 appendMultipartText(extractTextFromPart(part), sb);
401             }
402             return;
403         }
404 
405         // otherwise assume multipart/mixed type and construct the contents by retrieving all text and html
406         for (int i = 0, n = multipart.getCount(); i < n; i++)
407         {
408             BodyPart part = multipart.getBodyPart(i);
409             String contentType = part.getContentType();
410 
411             if (!Part.ATTACHMENT.equals(part.getDisposition()) && contentType != null)
412             {
413                 try
414                 {
415                     String content = extractTextFromPart(part);
416                     if (content != null)
417                     {
418                         appendMultipartText(content, sb);
419                     }
420                     else if(part.getContent() instanceof Multipart)
421                     {
422                         getBodyFromMultipart((Multipart) part.getContent(), sb);
423                     }
424                 }
425                 catch (IOException exception)
426                 {
427                     // We swallow the exception because we want to allow processing to continue
428                     // even if there is a bad part in one part of the message
429                     log.warn("Error retrieving content from part '" + exception.getMessage() + "'", exception);
430                 }
431             }
432         }
433     }
434 
435     private static void appendMultipartText(String content, StringBuffer sb) throws IOException, MessagingException
436     {
437         if (content != null)
438         {
439             if(sb.length() > 0) sb.append("\n");
440             sb.append(content);
441         }
442     }
443 
444     private static String extractTextFromPart(Part part) throws IOException, MessagingException,
445             UnsupportedEncodingException
446     {
447         if (part == null)
448             return null;
449 
450         String content = null;
451 
452         if (isPartPlainText(part))
453         {
454             try
455             {
456                 content = (String) part.getContent();
457             }
458             catch (UnsupportedEncodingException e)
459             {
460                 // If the encoding is unsupported read the content with default encoding
461                 log.warn("Found unsupported encoding '" + e.getMessage() + "'. Reading content with "
462                         + DEFAULT_ENCODING + " encoding.");
463                 content = getBody(part, DEFAULT_ENCODING);
464             }
465         }
466         else if (isPartHtml(part))
467         {
468             content = htmlConverter.convert((String) part.getContent());
469         }
470 
471         if (content == null)
472         {
473             log.warn("Unable to extract text from MIME part with Content-Type '" + part.getContentType());
474         }
475 
476         return content;
477     }
478 
479     private static String getBody(Part part, String charsetName) throws UnsupportedEncodingException,
480             IOException, MessagingException
481     {
482         Reader input = null;
483         StringWriter output = null;
484         try
485         {
486             input = new BufferedReader(new InputStreamReader(part.getInputStream(), charsetName));
487             output = new StringWriter();
488             IOUtils.copy(input, output);
489             return output.getBuffer().toString();
490         }
491         finally
492         {
493             IOUtils.closeQuietly(input);
494             IOUtils.closeQuietly(output);
495         }
496     }
497 
498     private static BodyPart getFirstInlinePartWithMimeType(Multipart multipart, String mimeType) throws MessagingException
499     {
500         for (int i = 0, n = multipart.getCount(); i < n; i++)
501         {
502             BodyPart part = multipart.getBodyPart(i);
503             String contentType = part.getContentType();
504             if (!Part.ATTACHMENT.equals(part.getDisposition()) && contentType != null && compareContentType(contentType, mimeType))
505             {
506                 return part;
507             }
508         }
509         return null;
510     }
511 
512     private static boolean compareContentType(String contentType, String mimeType)
513     {
514         return contentType.toLowerCase().startsWith(mimeType);
515     }
516 
517 
518     /**
519      * Tests if a particular part content type is text/html.
520      *
521      * @param part The part being tested.
522      * @return true if the part content type is text/html
523      * @throws MessagingException if javamail complains
524      */
525     static public boolean isPartHtml(final Part part) throws MessagingException
526     {
527         final String contentType = MailUtils.getContentType(part);
528         return HTML_CONTENT_TYPE.equalsIgnoreCase(contentType);
529     }
530 
531     /**
532      * Tests if the provided part content type is text/plain.
533      *
534      * @param part The part being tested.
535      * @return true if the part content type is text/plain
536      * @throws MessagingException if javamail complains
537      */
538     static public boolean isPartPlainText(final Part part) throws MessagingException
539     {
540         final String contentType = MailUtils.getContentType(part);
541         return TEXT_CONTENT_TYPE.equalsIgnoreCase(contentType);
542     }
543 
544     /**
545      * Tests if the provided part's content type is message/rfc822
546      *
547      * @param part The part being tested.
548      * @return true if the part content type is message/rfc822
549      * @throws MessagingException if javamail complains
550      */
551     static public boolean isPartMessageType(final Part part) throws MessagingException
552     {
553         // currently, only "message/rfc822" content type is supported
554         final String contentType = MailUtils.getContentType(part);
555         return MESSAGE_CONTENT_TYPE.equalsIgnoreCase(contentType);
556     }
557 
558     /**
559      * Tests if the provided part's content type is multipart/related
560      *
561      * @param part The part being tested.
562      * @return true if the part content type is multipart/related
563      * @throws MessagingException if javamail complains
564      */
565     static public boolean isPartRelated(final Part part) throws MessagingException
566     {
567         final String contentType = getContentType(part);
568         return MULTIPART_RELATED_CONTENT_TYPE.equalsIgnoreCase(contentType);
569     }
570 
571     /**
572      * Helper which returns the pure mime/subMime content type less any other extra parameters which may
573      * accompany the header value.
574      *
575      * @param part the mail part to extract the content-type from.
576      * @return the pure mime/subMime type
577      * @throws MessagingException if retrieving the part's Content-Type header fails
578      */
579     static public String getContentType(final Part part) throws MessagingException
580     {
581         checkPartNotNull(part);
582 
583         final String contentType = part.getContentType();
584         return getContentType(contentType);
585     }
586 
587     /**
588      * Helper which extracts the content type from a header value removing parameters and so on.
589      *
590      * @param headerValue The header value.
591      * @return The actual content type
592      */
593     static public String getContentType(final String headerValue)
594     {
595         checkHeaderValue(headerValue);
596 
597         String out = headerValue;
598 
599         final int semiColon = headerValue.indexOf(';');
600         if (-1 != semiColon)
601         {
602             out = headerValue.substring(0, semiColon);
603         }
604 
605         return out.trim();
606     }
607 
608     static private void checkHeaderValue(final String headerValue)
609     {
610         Validate.notEmpty(headerValue);
611     }
612 
613     /**
614      * Tests if the content of the part content is empty.  The definition of empty depends on whether the content is text
615      * or binary.
616      * <p/>
617      * Text content for content types like plain/text and html/text is defined as being empty if it contains an empty string
618      * after doing a trim(). If the string contains 50 spaces it is still empty whilst a string with a solitary "a"
619      * isnt.
620      * <p/>
621      * For binary content (like images) if the content contains 0 bytes it is empty whilst anything with 1 or more bytes
622      * is NOT considered empty.
623      *
624      * @param part a mail part - may or may not have content.
625      * @return true/false if the content is deemed empty as per above rules.
626      * @throws MessagingException if retrieving content fails.
627      * @throws IOException        if retrieving content fails or reading content input stream fails.
628      */
629     static public boolean isContentEmpty(final Part part) throws MessagingException, IOException
630     {
631         checkPartNotNull(part);
632 
633         boolean definitelyEmpty = false;
634         final Object content = part.getContent();
635         if (null == content)
636         {
637             definitelyEmpty = true;
638         }
639         else
640         {
641             if (content instanceof String)
642             {
643                 final String stringContent = (String) content;
644                 definitelyEmpty = StringUtils.isBlank(stringContent);
645             }
646 
647             if (content instanceof InputStream)
648             {
649                 final InputStream inputStream = (InputStream) content;
650                 try
651                 {
652 
653                     // try and read a byte.. it we get one its not empty, if we dont its empty.
654                     final int firstByte = inputStream.read();
655                     definitelyEmpty = -1 == firstByte;
656 
657                 }
658                 finally
659                 {
660                     IOUtils.closeQuietly(inputStream);
661                 }
662             }
663         }
664 
665         return definitelyEmpty;
666     }
667 
668     /**
669      * Asserts that the part parameter is not null, throwing a NullPointerException if the part parameter is null.
670      *
671      * @param part The parameter part
672      */
673     static private void checkPartNotNull(final Part part)
674     {
675         Validate.notNull(part, "part should not be null.");
676     }
677 
678     /**
679      * This method uses a number of checks to determine if the given part actually represents an inline (typically image) part.
680      * Some email clients (aka lotus notes) dont appear to correctly set the disposition to inline so a number of
681      * additional checks are required, hence the multi staged approached.
682      * <p/>
683      * eg. inline images from notes wont have a inline disposition but will have a content id and will also have their
684      * content base64 encoded. This approach helps us correctly identify inline images or other binary parts.
685      *
686      * @param part The part being tested.
687      * @return True if the part is inline false in all other cases.
688      * @throws MessagingException as thrown by java mail
689      */
690     static public boolean isPartInline(final Part part) throws MessagingException
691     {
692         checkPartNotNull(part);
693 
694         boolean inline = false;
695 
696         // an inline part is only considered inline if its also got a filename...
697         final String disposition = part.getDisposition();
698         if (Part.INLINE.equalsIgnoreCase(disposition))
699         {
700             final String file = part.getFileName();
701             if(!StringUtils.isBlank(file))
702             {
703                 inline = true;
704             }
705             return inline;
706         }
707 
708         final boolean gotContentId = MailUtils.hasContentId(part);
709         if (!gotContentId)
710         {
711             return false;
712         }
713         final boolean gotBase64 = MailUtils.isContentBase64Encoded(part);
714         if (!gotBase64)
715         {
716             return false;
717         }
718 
719         return true;
720     }
721 
722     static private boolean hasContentId(final Part part) throws MessagingException
723     {
724         boolean gotContentId = false;
725         final String[] contentIds = part.getHeader(MailUtils.CONTENT_ID_HEADER);
726         if (null != contentIds)
727         {
728             for (int i = 0; i < contentIds.length; i++)
729             {
730                 final String contentId = contentIds[i];
731                 if (contentId != null && contentId.length() > 0)
732                 {
733                     gotContentId = true;
734                     break;
735                 }
736             } // for
737         }
738         return gotContentId;
739     }
740 
741     /**
742      * Checks if a part's content is base64 encoded by scanning for a content transfer encoding header value.
743      *
744      * @param part THe part being tested.
745      * @return True if the content is base 64 encoded, false in all other cases.
746      * @throws MessagingException if javamail complains
747      */
748     static private boolean isContentBase64Encoded(final Part part) throws MessagingException
749     {
750         boolean gotBase64 = false;
751         final String[] contentTransferEncodings = part.getHeader(CONTENT_TRANSFER_ENCODING_HEADER);
752         if (null != contentTransferEncodings)
753         {
754             for (int i = 0; i < contentTransferEncodings.length; i++)
755             {
756                 final String contentTransferEncoding = contentTransferEncodings[i];
757                 if ("base64".equals(contentTransferEncoding))
758                 {
759                     gotBase64 = true;
760                     break;
761                 }
762             }
763         }
764 
765         return gotBase64;
766     }
767 
768     /**
769      * Tests if the provided part is an attachment. Note this method does not test if the content is empty etc it merely
770      * tests whether or not the part is an attachment of some sort.
771      *
772      * @param part The part being tested.
773      * @throws MessagingException if javamail complains
774      * @returns True if the part is an attachment otherwise returns false
775      */
776     static public boolean isPartAttachment(final Part part) throws MessagingException
777     {
778         checkPartNotNull(part);
779         return Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition());
780     }
781 
782     /**
783      * This method may be used to fix any mime encoded filenames that have been returned by javamail.
784      * No harm can occur from calling this method unnecessarily except for wasting a few cpu cycles...
785      * <p/>
786      * Very probably a MIME-encoded filename - see http://java.sun.com/products/javamail/FAQ.html#encodefilename
787      *
788      * @param filename
789      * @return The fixed filename.
790      * @throws IOException {@see MimeUtility#decodeText}
791      */
792     static public String fixMimeEncodedFilename(final String filename) throws IOException
793     {
794         String newFilename = filename;
795         if (filename.startsWith("=?") || filename.endsWith("?="))
796         {
797             newFilename = MimeUtility.decodeText(filename);
798         }
799         return newFilename;
800     }
801 
802 
803     /**
804      * Tests if a part is actually a signature. This is required to fix JRA-9933.
805      *
806      * @param part a mail part. The part is assumed to have a content-type header.
807      * @return true if the content-type header matches the standard PKCS7 mime types
808      * @throws MessagingException if retrieving the Content-Type from the part fails.
809      */
810     static public boolean isPartSignaturePKCS7(final Part part) throws MessagingException
811     {
812         MailUtils.checkPartNotNull(part);
813         final String contentType = MailUtils.getContentType(part).toLowerCase(Locale.getDefault());
814         return contentType.startsWith(CONTENT_TYPE_PKCS7) || contentType.startsWith(CONTENT_TYPE_X_PKCS7);
815     }
816 }