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