View Javadoc

1   /**
2    * Created by IntelliJ IDEA.
3    * User: Edwin
4    * Date: Dec 5, 2002
5    * Time: 2:56:58 PM
6    * To change this template use Options | File Templates.
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  // TODO: Doesn't handle charsets/encoding very well. Or, indeed, at all.
51  /**
52   * This class contains a bunch of static helper methods that make life a bit easier particularly with the processing of
53   * Parts.
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       * The content transfer encoding header, which is used to identify whether a part is base64 encoded.
73       */
74      private static final String CONTENT_TRANSFER_ENCODING_HEADER = "Content-Transfer-Encoding";
75  
76      /**
77       * Content header id
78       */
79      private static final String CONTENT_ID_HEADER = "Content-ID";
80  
81      /**
82       * Very simple representation of a mail attachment after it has been
83       * extracted from a message.
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      * Parse addresses from a comma (and space) separated string into the proper array
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      * Get the body of the message as a String. The algorithm for finding the body is as follows:
130      *
131      * <ol><li>If the message is a single part, and that part is text/plain, return it.
132      *     <li>If the message is a single part, and that part is text/html, convert it to
133      *         text (stripping out the HTML) and return it.
134      *     <li>If the message is multi-part, return the first text/plain part that isn't marked
135      *         explicitly as an attachment.
136      *     <li>If the message is multi-part, but does not contain any text/plain parts, return
137      *         the first text/html part that isn't marked explicitly as an attachment, converting
138      *         it to text and stripping the HTML.
139      *     <li>If nothing is found in any of the steps above, return null.
140      * </ol>
141      *
142      * <p>Note: If the message contains nested multipart parts, an HTML part nested at a higher level will
143      * take precedence over a text part nested deeper.
144      *
145      * @param message The message to retrieve the body from
146      * @return The message body, or null if the message could not be parsed
147      * @throws javax.mail.MessagingException If there was an error getting the content from the message
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                 //didn't match anything above
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      * Gets all parts of a message that are attachments rather than alternative inline bits.
185      *
186      * @param message the message from which to extract the attachments
187      * @return an array of the extracted attachments
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                     // ignore because it's probably not a multipart part anyway
226                     // if the encoding is unsupported
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      * Convert the contents of an input stream into a byte array.
242      *
243      * @param in
244      * @return the contents of that stream as a byte array.
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      * Get the user that has the same email address as the author of the message.  If multiple
262      * authors, take the first one.
263      *
264      * @param message The message to get the author from.
265      * @return The user who has the same email address as the author of the message
266      * @throws javax.mail.MessagingException If an error occurred getting the message author
267      * @deprecated Now incorporated into JIRA due to app-specific logic, will
268      * be removed in a future release. (complain to chris@atlassian.com)
269      */
270     public static User getAuthorFromSender(Message message) throws MessagingException
271     {
272         return getFirstValidUser(message.getFrom());
273     }
274 
275     /**
276      * Given an array of addresses, this method returns the first valid address.
277      *
278      * @param addresses addresses to be used to search for a User.
279      * @return a User for the email address or null.
280      * @deprecated Now incorporated into JIRA due to app-specific logic, will
281      * be removed in a future release. (complain to chris@atlassian.com)
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                     // keep cycling
302                 }
303             }
304         }
305 
306         return null;
307     }
308 
309     /**
310      * @return true if at least one of the recipients matches the email address given.
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      * Returns a List<String> of trimmed non-null email addresses from the
332      * given potentially dirty pile of addresses listed as senders on the
333      * given message.
334      * @param message the message from which to get senders.
335      * @return a nice List<String> of email addresses.
336      * @throws MessagingException if the senders can't be retrieved from message.
337      */
338     public static List /*<String>*/ getSenders(Message message) throws MessagingException
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                     // Trim down the email address to remove any whitespace etc.
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      * Produces a mimebodypart object from an attachment file path. An attachment needs to be in this form to be attached
364      * to an email for sending
365      *
366      * @param path
367      * @return
368      * @throws MessagingException
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         // if an multipart/alternative type we just get the first text or html content found
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         // otherwise assume multipart/mixed type and construct the contents by retrieving all text and html
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                     // We swallow the exception because we want to allow processing to continue
486                     // even if there is a bad part in one part of the message
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                 // If the encoding is unsupported read the content with default encoding
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      * Tests if a particular part content type is text/html.
578      *
579      * @param part The part being tested.
580      * @return true if the part content type is text/html
581      * @throws MessagingException if javamail complains
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      * Tests if the provided part content type is text/plain.
590      *
591      * @param part The part being tested.
592      * @return true if the part content type is text/plain
593      * @throws MessagingException if javamail complains
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      * Tests if the provided part's content type is message/rfc822
602      *
603      * @param part The part being tested.
604      * @return true if the part content type is message/rfc822
605      * @throws MessagingException if javamail complains
606      */
607     static public boolean isPartMessageType(final Part part) throws MessagingException
608     {
609         // currently, only "message/rfc822" content type is supported
610         return MESSAGE_CONTENT_TYPE.equals(getContentType(part));
611     }
612 
613     /**
614      * Tests if the provided part's content type is multipart/related
615      *
616      * @param part The part being tested.
617      * @return true if the part content type is multipart/related
618      * @throws MessagingException if javamail complains
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      * Helper which returns the pure mime/subMime content type less any other extra parameters which may
627      * accompany the header value.
628      *
629      * @param part the mail part to extract the content-type from.
630      * @return the pure mime/subMime type
631      * @throws MessagingException if retrieving the part's Content-Type header fails
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      * Helper which extracts the content type from a header value removing parameters and so on.
643      *
644      * @param headerValue The header value.
645      * @return The actual content type
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      * Tests if the content of the part content is empty.  The definition of empty depends on whether the content is text
669      * or binary.
670      * <p/>
671      * Text content for content types like plain/text and html/text is defined as being empty if it contains an empty string
672      * after doing a trim(). If the string contains 50 spaces it is still empty whilst a string with a solitary "a"
673      * isnt.
674      * <p/>
675      * For binary content (like images) if the content contains 0 bytes it is empty whilst anything with 1 or more bytes
676      * is NOT considered empty.
677      *
678      * @param part a mail part - may or may not have content.
679      * @return true/false if the content is deemed empty as per above rules.
680      * @throws MessagingException if retrieving content fails.
681      * @throws IOException        if retrieving content fails or reading content input stream fails.
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                     // try and read a byte.. it we get one its not empty, if we dont its empty.
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      * Asserts that the part parameter is not null, throwing a NullPointerException if the part parameter is null.
724      *
725      * @param part The parameter part
726      */
727     static private void checkPartNotNull(final Part part)
728     {
729         Validate.notNull(part, "part should not be null.");
730     }
731 
732     /**
733      * This method uses a number of checks to determine if the given part actually represents an inline (typically image) part.
734      * Some email clients (aka lotus notes) dont appear to correctly set the disposition to inline so a number of
735      * additional checks are required, hence the multi staged approached.
736      * <p/>
737      * eg. inline images from notes wont have a inline disposition but will have a content id and will also have their
738      * content base64 encoded. This approach helps us correctly identify inline images or other binary parts.
739      *
740      * @param part The part being tested.
741      * @return True if the part is inline false in all other cases.
742      * @throws MessagingException as thrown by java mail
743      */
744     static public boolean isPartInline(final Part part) throws MessagingException
745     {
746         checkPartNotNull(part);
747 
748         boolean inline = false;
749 
750         // an inline part is only considered inline if its also got a filename...
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             } // for
791         }
792         return gotContentId;
793     }
794 
795     /**
796      * Checks if a part's content is base64 encoded by scanning for a content transfer encoding header value.
797      *
798      * @param part THe part being tested.
799      * @return True if the content is base 64 encoded, false in all other cases.
800      * @throws MessagingException if javamail complains
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      * Tests if the provided part is an attachment. Note this method does not test if the content is empty etc it merely
824      * tests whether or not the part is an attachment of some sort.
825      *
826      * @param part The part being tested.
827      * @throws MessagingException if javamail complains
828      * @returns True if the part is an attachment otherwise returns false
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      * This method may be used to fix any mime encoded filenames that have been returned by javamail.
838      * No harm can occur from calling this method unnecessarily except for wasting a few cpu cycles...
839      * <p/>
840      * Very probably a MIME-encoded filename - see http://java.sun.com/products/javamail/FAQ.html#encodefilename
841      *
842      * @param filename
843      * @return The fixed filename.
844      * @throws IOException {@see MimeUtility#decodeText}
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      * Tests if a part is actually a signature. This is required to fix JRA-9933.
859      *
860      * @param part a mail part. The part is assumed to have a content-type header.
861      * @return true if the content-type header matches the standard PKCS7 mime types
862      * @throws MessagingException if retrieving the Content-Type from the part fails.
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 }