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 				|| (part.getDisposition() == null && part.getFileName() != null);
239     }
240 
241     /**
242      * Convert the contents of an input stream into a byte array.
243      *
244      * @param in
245      * @return the contents of that stream as a byte array.
246      */
247     private static byte[] toByteArray(InputStream in) throws IOException
248     {
249         ByteArrayOutputStream out = new ByteArrayOutputStream();
250         byte[] buf = new byte[512];
251         int count;
252         while ((count = in.read(buf)) != -1)
253         {
254             out.write(buf, 0, count);
255         }
256 
257         out.close();
258         return out.toByteArray();
259     }
260 
261     /**
262      * Get the user that has the same email address as the author of the message.  If multiple
263      * authors, take the first one.
264      *
265      * @param message The message to get the author from.
266      * @return The user who has the same email address as the author of the message
267      * @throws javax.mail.MessagingException If an error occurred getting the message author
268      * @deprecated Now incorporated into JIRA due to app-specific logic, will
269      * be removed in a future release. (complain to chris@atlassian.com)
270      */
271     public static User getAuthorFromSender(Message message) throws MessagingException
272     {
273         return getFirstValidUser(message.getFrom());
274     }
275 
276     /**
277      * Given an array of addresses, this method returns the first valid address.
278      *
279      * @param addresses addresses to be used to search for a User.
280      * @return a User for the email address or null.
281      * @deprecated Now incorporated into JIRA due to app-specific logic, will
282      * be removed in a future release. (complain to chris@atlassian.com)
283      */
284     public static User getFirstValidUser(Address[] addresses)
285     {
286         if (addresses == null || addresses.length == 0)
287             return null;
288 
289         for (int i = 0; i < addresses.length; i++)
290         {
291             if (addresses[i] instanceof InternetAddress)
292             {
293                 InternetAddress email = (InternetAddress) addresses[i];
294 
295                 try
296                 {
297                     User validUser = UserUtils.getUserByEmail(email.getAddress());
298                     return validUser;
299                 }
300                 catch (EntityNotFoundException e)
301                 {
302                     // keep cycling
303                 }
304             }
305         }
306 
307         return null;
308     }
309 
310     /**
311      * @return true if at least one of the recipients matches the email address given.
312      */
313     public static boolean hasRecipient(String matchEmail, Message message) throws MessagingException
314     {
315         Address[] addresses = message.getAllRecipients();
316 
317         if (addresses == null || addresses.length == 0)
318             return false;
319 
320         for (int i = 0; i < addresses.length; i++)
321         {
322             InternetAddress email = (InternetAddress) addresses[i];
323 
324             if (matchEmail.compareToIgnoreCase(email.getAddress()) == 0)
325                 return true;
326         }
327 
328         return false;
329     }
330 
331         /**
332      * Returns a List<String> of trimmed non-null email addresses from the
333      * given potentially dirty pile of addresses listed as senders on the
334      * given message.
335      * @param message the message from which to get senders.
336      * @return a nice List<String> of email addresses.
337      * @throws MessagingException if the senders can't be retrieved from message.
338      */
339     public static List /*<String>*/ getSenders(Message message) throws MessagingException
340     {
341 
342         ArrayList senders = new ArrayList();
343         Address[] addresses = message.getFrom();
344         if (addresses != null)
345         {
346             for (int i = 0; i < addresses.length; i++)
347             {
348                 if (addresses[i] instanceof InternetAddress)
349                 {
350                     InternetAddress addr = (InternetAddress) addresses[i];
351                     // Trim down the email address to remove any whitespace etc.
352                     String emailAddress = StringUtils.trimToNull(addr.getAddress());
353                     if (emailAddress != null)
354                     {
355                         senders.add(emailAddress);
356                     }
357                 }
358             }
359         }
360         return senders;
361     }
362 
363     /**
364      * Produces a mimebodypart object from an attachment file path. An attachment needs to be in this form to be attached
365      * to an email for sending
366      *
367      * @param path
368      * @return
369      * @throws MessagingException
370      */
371     public static MimeBodyPart createAttachmentMimeBodyPart(String path) throws MessagingException
372     {
373         MimeBodyPart attachmentPart = new MimeBodyPart();
374         DataSource source = new FileDataSource(path);
375         attachmentPart.setDataHandler(new DataHandler(source));
376 
377         String fileName = extractFilenameFromPath(path);
378 
379         attachmentPart.setFileName(fileName);
380         return attachmentPart;
381     }
382 
383     private static String extractFilenameFromPath(String path) {
384         if (path == null) return null;
385         StringTokenizer st = new StringTokenizer(path, "\\/");
386 
387         String fileName;
388         do
389         {
390             fileName = st.nextToken();
391         }
392         while (st.hasMoreTokens());
393         return fileName;
394     }
395 
396     public static MimeBodyPart createZippedAttachmentMimeBodyPart(String path) throws MessagingException
397     {
398         File tmpFile = null;
399         String fileName = extractFilenameFromPath(path);
400 
401         try {
402             tmpFile = File.createTempFile("atlassian", null);
403             FileOutputStream fout = new FileOutputStream(tmpFile);
404             ZipOutputStream zout = new ZipOutputStream(fout);
405             zout.putNextEntry(new ZipEntry(fileName));
406 
407             InputStream in = new FileInputStream(path);
408             final byte[] buffer = new byte[ BUFFER_SIZE ];
409             int n = 0;
410             while ( -1 != (n = in.read(buffer)) ) {
411                 zout.write(buffer, 0, n);
412             }
413             zout.close();
414             in.close();
415             log.debug("Wrote temporary zip of attachment to " + tmpFile);
416         } catch (FileNotFoundException e) {
417             String err = "Couldn't find file '"+path+"' on server: "+e;
418             log.error(err, e);
419             MimeBodyPart mimeBodyPart = new MimeBodyPart();
420             mimeBodyPart.setText(err);
421             return mimeBodyPart;
422         } catch (IOException e) {
423             String err = "Error zipping log file '"+path+"' on server: "+e;
424             log.error(err, e);
425             MimeBodyPart mimeBodyPart = new MimeBodyPart();
426             mimeBodyPart.setText(err);
427             return mimeBodyPart;
428         }
429         MimeBodyPart attachmentPart = new MimeBodyPart();
430         DataSource source = new FileDataSource(tmpFile);
431         attachmentPart.setDataHandler(new DataHandler(source));
432         attachmentPart.setFileName(fileName+".zip");
433         attachmentPart.setHeader("Content-Type", "application/zip");
434         return attachmentPart;
435     }
436 
437     private static String getBodyFromMultipart(Multipart multipart) throws MessagingException, IOException
438     {
439         StringBuffer sb = new StringBuffer();
440         getBodyFromMultipart(multipart, sb);
441         return sb.toString();
442     }
443 
444     private static void getBodyFromMultipart(Multipart multipart, StringBuffer sb) throws MessagingException, IOException
445     {
446         String multipartType = multipart.getContentType();
447 
448         // if an multipart/alternative type we just get the first text or html content found
449         if(multipartType != null && compareContentType(multipartType, MULTIPART_ALTERNATE_CONTENT_TYPE))
450         {
451             BodyPart part = getFirstInlinePartWithMimeType(multipart, TEXT_CONTENT_TYPE);
452             if(part != null)
453             {
454                 appendMultipartText(extractTextFromPart(part), sb);
455             }
456             else
457             {
458                 part = getFirstInlinePartWithMimeType(multipart, HTML_CONTENT_TYPE);
459                 appendMultipartText(extractTextFromPart(part), sb);
460             }
461             return;
462         }
463 
464         // otherwise assume multipart/mixed type and construct the contents by retrieving all text and html
465         for (int i = 0, n = multipart.getCount(); i < n; i++)
466         {
467             BodyPart part = multipart.getBodyPart(i);
468             String contentType = part.getContentType();
469 
470             if (!Part.ATTACHMENT.equals(part.getDisposition()) && contentType != null)
471             {
472                 try
473                 {
474                     String content = extractTextFromPart(part);
475                     if (content != null)
476                     {
477                         appendMultipartText(content, sb);
478                     }
479                     else if(part.getContent() instanceof Multipart)
480                     {
481                         getBodyFromMultipart((Multipart) part.getContent(), sb);
482                     }
483                 }
484                 catch (IOException exception)
485                 {
486                     // We swallow the exception because we want to allow processing to continue
487                     // even if there is a bad part in one part of the message
488                     log.warn("Error retrieving content from part '" + exception.getMessage() + "'", exception);
489                 }
490             }
491         }
492     }
493 
494     private static void appendMultipartText(String content, StringBuffer sb) throws IOException, MessagingException
495     {
496         if (content != null)
497         {
498             if(sb.length() > 0) sb.append("\n");
499             sb.append(content);
500         }
501     }
502 
503     private static String extractTextFromPart(Part part) throws IOException, MessagingException,
504             UnsupportedEncodingException
505     {
506         if (part == null)
507             return null;
508 
509         String content = null;
510 
511         if (isPartPlainText(part))
512         {
513             try
514             {
515                 content = (String) part.getContent();
516             }
517             catch (UnsupportedEncodingException e)
518             {
519                 // If the encoding is unsupported read the content with default encoding
520                 log.warn("Found unsupported encoding '" + e.getMessage() + "'. Reading content with "
521                         + DEFAULT_ENCODING + " encoding.");
522                 content = getBody(part, DEFAULT_ENCODING);
523             }
524         }
525         else if (isPartHtml(part))
526         {
527             content = htmlConverter.convert((String) part.getContent());
528         }
529 
530         if (content == null)
531         {
532             log.warn("Unable to extract text from MIME part with Content-Type '" + part.getContentType());
533         }
534 
535         return content;
536     }
537 
538     private static String getBody(Part part, String charsetName) throws UnsupportedEncodingException,
539             IOException, MessagingException
540     {
541         Reader input = null;
542         StringWriter output = null;
543         try
544         {
545             input = new BufferedReader(new InputStreamReader(part.getInputStream(), charsetName));
546             output = new StringWriter();
547             IOUtils.copy(input, output);
548             return output.getBuffer().toString();
549         }
550         finally
551         {
552             IOUtils.closeQuietly(input);
553             IOUtils.closeQuietly(output);
554         }
555     }
556 
557     private static BodyPart getFirstInlinePartWithMimeType(Multipart multipart, String mimeType) throws MessagingException
558     {
559         for (int i = 0, n = multipart.getCount(); i < n; i++)
560         {
561             BodyPart part = multipart.getBodyPart(i);
562             String contentType = part.getContentType();
563             if (!Part.ATTACHMENT.equals(part.getDisposition()) && contentType != null && compareContentType(contentType, mimeType))
564             {
565                 return part;
566             }
567         }
568         return null;
569     }
570 
571     private static boolean compareContentType(String contentType, String mimeType)
572     {
573         return contentType.toLowerCase().startsWith(mimeType);
574     }
575 
576 
577     /**
578      * Tests if a particular part content type is text/html.
579      *
580      * @param part The part being tested.
581      * @return true if the part content type is text/html
582      * @throws MessagingException if javamail complains
583      */
584     static public boolean isPartHtml(final Part part) throws MessagingException
585     {
586         final String contentType = MailUtils.getContentType(part);
587         return HTML_CONTENT_TYPE.equalsIgnoreCase(contentType);
588     }
589 
590     /**
591      * Tests if the provided part content type is text/plain.
592      *
593      * @param part The part being tested.
594      * @return true if the part content type is text/plain
595      * @throws MessagingException if javamail complains
596      */
597     static public boolean isPartPlainText(final Part part) throws MessagingException
598     {
599         final String contentType = MailUtils.getContentType(part);
600         return TEXT_CONTENT_TYPE.equalsIgnoreCase(contentType);
601     }
602 
603     /**
604      * Tests if the provided part's content type is message/rfc822
605      *
606      * @param part The part being tested.
607      * @return true if the part content type is message/rfc822
608      * @throws MessagingException if javamail complains
609      */
610     static public boolean isPartMessageType(final Part part) throws MessagingException
611     {
612         // currently, only "message/rfc822" content type is supported
613         final String contentType = MailUtils.getContentType(part);
614         return MESSAGE_CONTENT_TYPE.equalsIgnoreCase(contentType);
615     }
616 
617     /**
618      * Tests if the provided part's content type is multipart/related
619      *
620      * @param part The part being tested.
621      * @return true if the part content type is multipart/related
622      * @throws MessagingException if javamail complains
623      */
624     static public boolean isPartRelated(final Part part) throws MessagingException
625     {
626         final String contentType = getContentType(part);
627         return MULTIPART_RELATED_CONTENT_TYPE.equalsIgnoreCase(contentType);
628     }
629 
630     /**
631      * Helper which returns the pure mime/subMime content type less any other extra parameters which may
632      * accompany the header value.
633      *
634      * @param part the mail part to extract the content-type from.
635      * @return the pure mime/subMime type
636      * @throws MessagingException if retrieving the part's Content-Type header fails
637      */
638     static public String getContentType(final Part part) throws MessagingException
639     {
640         checkPartNotNull(part);
641 
642         final String contentType = part.getContentType();
643         return getContentType(contentType);
644     }
645 
646     /**
647      * Helper which extracts the content type from a header value removing parameters and so on.
648      *
649      * @param headerValue The header value.
650      * @return The actual content type
651      */
652     static public String getContentType(final String headerValue)
653     {
654         checkHeaderValue(headerValue);
655 
656         String out = headerValue;
657 
658         final int semiColon = headerValue.indexOf(';');
659         if (-1 != semiColon)
660         {
661             out = headerValue.substring(0, semiColon);
662         }
663 
664         return out.trim();
665     }
666 
667     static private void checkHeaderValue(final String headerValue)
668     {
669         Validate.notEmpty(headerValue);
670     }
671 
672     /**
673      * Tests if the content of the part content is empty.  The definition of empty depends on whether the content is text
674      * or binary.
675      * <p/>
676      * Text content for content types like plain/text and html/text is defined as being empty if it contains an empty string
677      * after doing a trim(). If the string contains 50 spaces it is still empty whilst a string with a solitary "a"
678      * isnt.
679      * <p/>
680      * For binary content (like images) if the content contains 0 bytes it is empty whilst anything with 1 or more bytes
681      * is NOT considered empty.
682      *
683      * @param part a mail part - may or may not have content.
684      * @return true/false if the content is deemed empty as per above rules.
685      * @throws MessagingException if retrieving content fails.
686      * @throws IOException        if retrieving content fails or reading content input stream fails.
687      */
688     static public boolean isContentEmpty(final Part part) throws MessagingException, IOException
689     {
690         checkPartNotNull(part);
691 
692         boolean definitelyEmpty = false;
693         final Object content = part.getContent();
694         if (null == content)
695         {
696             definitelyEmpty = true;
697         }
698         else
699         {
700             if (content instanceof String)
701             {
702                 final String stringContent = (String) content;
703                 definitelyEmpty = StringUtils.isBlank(stringContent);
704             }
705 
706             if (content instanceof InputStream)
707             {
708                 final InputStream inputStream = (InputStream) content;
709                 try
710                 {
711 
712                     // try and read a byte.. it we get one its not empty, if we dont its empty.
713                     final int firstByte = inputStream.read();
714                     definitelyEmpty = -1 == firstByte;
715 
716                 }
717                 finally
718                 {
719                     IOUtils.closeQuietly(inputStream);
720                 }
721             }
722         }
723 
724         return definitelyEmpty;
725     }
726 
727     /**
728      * Asserts that the part parameter is not null, throwing a NullPointerException if the part parameter is null.
729      *
730      * @param part The parameter part
731      */
732     static private void checkPartNotNull(final Part part)
733     {
734         Validate.notNull(part, "part should not be null.");
735     }
736 
737     /**
738      * This method uses a number of checks to determine if the given part actually represents an inline (typically image) part.
739      * Some email clients (aka lotus notes) dont appear to correctly set the disposition to inline so a number of
740      * additional checks are required, hence the multi staged approached.
741      * <p/>
742      * eg. inline images from notes wont have a inline disposition but will have a content id and will also have their
743      * content base64 encoded. This approach helps us correctly identify inline images or other binary parts.
744      *
745      * @param part The part being tested.
746      * @return True if the part is inline false in all other cases.
747      * @throws MessagingException as thrown by java mail
748      */
749     static public boolean isPartInline(final Part part) throws MessagingException
750     {
751         checkPartNotNull(part);
752 
753         boolean inline = false;
754 
755         // an inline part is only considered inline if its also got a filename...
756         final String disposition = part.getDisposition();
757         if (Part.INLINE.equalsIgnoreCase(disposition))
758         {
759             final String file = part.getFileName();
760             if(!StringUtils.isBlank(file))
761             {
762                 inline = true;
763             }
764             return inline;
765         }
766 
767         final boolean gotContentId = MailUtils.hasContentId(part);
768         if (!gotContentId)
769         {
770             return false;
771         }
772         final boolean gotBase64 = MailUtils.isContentBase64Encoded(part);
773         if (!gotBase64)
774         {
775             return false;
776         }
777 
778         return true;
779     }
780 
781     static private boolean hasContentId(final Part part) throws MessagingException
782     {
783         boolean gotContentId = false;
784         final String[] contentIds = part.getHeader(MailUtils.CONTENT_ID_HEADER);
785         if (null != contentIds)
786         {
787             for (int i = 0; i < contentIds.length; i++)
788             {
789                 final String contentId = contentIds[i];
790                 if (contentId != null && contentId.length() > 0)
791                 {
792                     gotContentId = true;
793                     break;
794                 }
795             } // for
796         }
797         return gotContentId;
798     }
799 
800     /**
801      * Checks if a part's content is base64 encoded by scanning for a content transfer encoding header value.
802      *
803      * @param part THe part being tested.
804      * @return True if the content is base 64 encoded, false in all other cases.
805      * @throws MessagingException if javamail complains
806      */
807     static private boolean isContentBase64Encoded(final Part part) throws MessagingException
808     {
809         boolean gotBase64 = false;
810         final String[] contentTransferEncodings = part.getHeader(CONTENT_TRANSFER_ENCODING_HEADER);
811         if (null != contentTransferEncodings)
812         {
813             for (int i = 0; i < contentTransferEncodings.length; i++)
814             {
815                 final String contentTransferEncoding = contentTransferEncodings[i];
816                 if ("base64".equals(contentTransferEncoding))
817                 {
818                     gotBase64 = true;
819                     break;
820                 }
821             }
822         }
823 
824         return gotBase64;
825     }
826 
827     /**
828      * Tests if the provided part is an attachment. Note this method does not test if the content is empty etc it merely
829      * tests whether or not the part is an attachment of some sort.
830      *
831      * @param part The part being tested.
832      * @throws MessagingException if javamail complains
833      * @returns True if the part is an attachment otherwise returns false
834      */
835     static public boolean isPartAttachment(final Part part) throws MessagingException
836     {
837         checkPartNotNull(part);
838         return Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition());
839     }
840 
841     /**
842      * This method may be used to fix any mime encoded filenames that have been returned by javamail.
843      * No harm can occur from calling this method unnecessarily except for wasting a few cpu cycles...
844      * <p/>
845      * Very probably a MIME-encoded filename - see http://java.sun.com/products/javamail/FAQ.html#encodefilename
846      *
847      * @param filename
848      * @return The fixed filename.
849      * @throws IOException {@see MimeUtility#decodeText}
850      */
851     static public String fixMimeEncodedFilename(final String filename) throws IOException
852     {
853         String newFilename = filename;
854         if (filename.startsWith("=?") || filename.endsWith("?="))
855         {
856             newFilename = MimeUtility.decodeText(filename);
857         }
858         return newFilename;
859     }
860 
861 
862     /**
863      * Tests if a part is actually a signature. This is required to fix JRA-9933.
864      *
865      * @param part a mail part. The part is assumed to have a content-type header.
866      * @return true if the content-type header matches the standard PKCS7 mime types
867      * @throws MessagingException if retrieving the Content-Type from the part fails.
868      */
869     static public boolean isPartSignaturePKCS7(final Part part) throws MessagingException
870     {
871         MailUtils.checkPartNotNull(part);
872         final String contentType = MailUtils.getContentType(part).toLowerCase(Locale.getDefault());
873         return contentType.startsWith(CONTENT_TYPE_PKCS7) || contentType.startsWith(CONTENT_TYPE_X_PKCS7);
874     }
875 }