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