1 /*
   2  * Copyright (c) 1997, 2010, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 /*
  27  * @(#)MimeUtility.java       1.45 03/03/10
  28  */
  29 
  30 
  31 
  32 package com.sun.xml.internal.messaging.saaj.packaging.mime.internet;
  33 
  34 import java.io.*;
  35 import java.util.*;
  36 
  37 import javax.activation.DataHandler;
  38 import javax.activation.DataSource;
  39 
  40 import com.sun.xml.internal.messaging.saaj.packaging.mime.MessagingException;
  41 import com.sun.xml.internal.messaging.saaj.packaging.mime.util.*;
  42 import com.sun.xml.internal.messaging.saaj.util.SAAJUtil;
  43 
  44 /**
  45  * This is a utility class that provides various MIME related
  46  * functionality. <p>
  47  *
  48  * There are a set of methods to encode and decode MIME headers as
  49  * per RFC 2047. A brief description on handling such headers is
  50  * given below: <p>
  51  *
  52  * RFC 822 mail headers <strong>must</strong> contain only US-ASCII
  53  * characters. Headers that contain non US-ASCII characters must be
  54  * encoded so that they contain only US-ASCII characters. Basically,
  55  * this process involves using either BASE64 or QP to encode certain
  56  * characters. RFC 2047 describes this in detail. <p>
  57  *
  58  * In Java, Strings contain (16 bit) Unicode characters. ASCII is a
  59  * subset of Unicode (and occupies the range 0 - 127). A String
  60  * that contains only ASCII characters is already mail-safe. If the
  61  * String contains non US-ASCII characters, it must be encoded. An
  62  * additional complexity in this step is that since Unicode is not
  63  * yet a widely used charset, one might want to first charset-encode
  64  * the String into another charset and then do the transfer-encoding.
  65  * <p>
  66  * Note that to get the actual bytes of a mail-safe String (say,
  67  * for sending over SMTP), one must do
  68  * <p><blockquote><pre>
  69  *
  70  *      byte[] bytes = string.getBytes("iso-8859-1");
  71  *
  72  * </pre></blockquote><p>
  73  *
  74  * The <code>setHeader</code> and <code>addHeader</code> methods
  75  * on MimeMessage and MimeBodyPart assume that the given header values
  76  * are Unicode strings that contain only US-ASCII characters. Hence
  77  * the callers of those methods must insure that the values they pass
  78  * do not contain non US-ASCII characters. The methods in this class
  79  * help do this. <p>
  80  *
  81  * The <code>getHeader</code> family of methods on MimeMessage and
  82  * MimeBodyPart return the raw header value. These might be encoded
  83  * as per RFC 2047, and if so, must be decoded into Unicode Strings.
  84  * The methods in this class help to do this. <p>
  85  *
  86  * Several System properties control strict conformance to the MIME
  87  * spec.  Note that these are not session properties but must be set
  88  * globally as System properties. <p>
  89  *
  90  * The <code>mail.mime.decodetext.strict</code> property controls
  91  * decoding of MIME encoded words.  The MIME spec requires that encoded
  92  * words start at the beginning of a whitespace separated word.  Some
  93  * mailers incorrectly include encoded words in the middle of a word.
  94  * If the <code>mail.mime.decodetext.strict</code> System property is
  95  * set to <code>"false"</code>, an attempt will be made to decode these
  96  * illegal encoded words. The default is true. <p>
  97  *
  98  * The <code>mail.mime.encodeeol.strict</code> property controls the
  99  * choice of Content-Transfer-Encoding for MIME parts that are not of
 100  * type "text".  Often such parts will contain textual data for which
 101  * an encoding that allows normal end of line conventions is appropriate.
 102  * In rare cases, such a part will appear to contain entirely textual
 103  * data, but will require an encoding that preserves CR and LF characters
 104  * without change.  If the <code>mail.mime.decodetext.strict</code>
 105  * System property is set to <code>"true"</code>, such an encoding will
 106  * be used when necessary.  The default is false. <p>
 107  *
 108  * In addition, the <code>mail.mime.charset</code> System property can
 109  * be used to specify the default MIME charset to use for encoded words
 110  * and text parts that don't otherwise specify a charset.  Normally, the
 111  * default MIME charset is derived from the default Java charset, as
 112  * specified in the <code>file.encoding</code> System property.  Most
 113  * applications will have no need to explicitly set the default MIME
 114  * charset.  In cases where the default MIME charset to be used for
 115  * mail messages is different than the charset used for files stored on
 116  * the system, this property should be set.
 117  *
 118  * @version 1.45, 03/03/10
 119  * @author  John Mani
 120  * @author  Bill Shannon
 121  */
 122 
 123 public class MimeUtility {
 124 
 125     // This class cannot be instantiated
 126     private MimeUtility() { }
 127 
 128     public static final int ALL = -1;
 129 
 130     private static final int BUFFER_SIZE = 1024;
 131     private static boolean decodeStrict = true;
 132     private static boolean encodeEolStrict = false;
 133     private static boolean foldEncodedWords = false;
 134     private static boolean foldText = true;
 135 
 136     static {
 137         try {
 138             String s = SAAJUtil.getSystemProperty("mail.mime.decodetext.strict");
 139             // default to true
 140             decodeStrict = s == null || !s.equalsIgnoreCase("false");
 141             s = SAAJUtil.getSystemProperty("mail.mime.encodeeol.strict");
 142             // default to false
 143             encodeEolStrict = s != null && s.equalsIgnoreCase("true");
 144             s = SAAJUtil.getSystemProperty("mail.mime.foldencodedwords");
 145             // default to false
 146             foldEncodedWords = s != null && s.equalsIgnoreCase("true");
 147             s = SAAJUtil.getSystemProperty("mail.mime.foldtext");
 148             // default to true
 149             foldText = s == null || !s.equalsIgnoreCase("false");
 150         } catch (SecurityException sex) {
 151             // ignore it
 152         }
 153     }
 154 
 155 
 156     /**
 157      * Get the content-transfer-encoding that should be applied
 158      * to the input stream of this datasource, to make it mailsafe. <p>
 159      *
 160      * The algorithm used here is: <br>
 161      * <ul>
 162      * <li>
 163      * If the primary type of this datasource is "text" and if all
 164      * the bytes in its input stream are US-ASCII, then the encoding
 165      * is "7bit". If more than half of the bytes are non-US-ASCII, then
 166      * the encoding is "base64". If less than half of the bytes are
 167      * non-US-ASCII, then the encoding is "quoted-printable".
 168      * <li>
 169      * If the primary type of this datasource is not "text", then if
 170      * all the bytes of its input stream are US-ASCII, the encoding
 171      * is "7bit". If there is even one non-US-ASCII character, the
 172      * encoding is "base64".
 173      * </ul>
 174      *
 175      * @param   ds      DataSource
 176      * @return          the encoding. This is either "7bit",
 177      *                  "quoted-printable" or "base64"
 178      */
 179     public static String getEncoding(DataSource ds) {
 180         ContentType cType = null;
 181         InputStream is = null;
 182         String encoding = null;
 183 
 184         try {
 185             cType = new ContentType(ds.getContentType());
 186             is = ds.getInputStream();
 187         } catch (Exception ex) {
 188             return "base64"; // what else ?!
 189         }
 190 
 191         boolean isText = cType.match("text/*");
 192         // if not text, stop processing when we see non-ASCII
 193         int i = checkAscii(is, ALL, !isText);
 194         switch (i) {
 195         case ALL_ASCII:
 196             encoding = "7bit"; // all ascii
 197             break;
 198         case MOSTLY_ASCII:
 199             encoding = "quoted-printable"; // mostly ascii
 200             break;
 201         default:
 202             encoding = "base64"; // mostly binary
 203             break;
 204         }
 205 
 206         // Close the input stream
 207         try {
 208             is.close();
 209         } catch (IOException ioex) { }
 210 
 211         return encoding;
 212     }
 213 
 214     /**
 215      * Same as <code>getEncoding(DataSource)</code> except that instead
 216      * of reading the data from an <code>InputStream</code> it uses the
 217      * <code>writeTo</code> method to examine the data.  This is more
 218      * efficient in the common case of a <code>DataHandler</code>
 219      * created with an object and a MIME type (for example, a
 220      * "text/plain" String) because all the I/O is done in this
 221      * thread.  In the case requiring an <code>InputStream</code> the
 222      * <code>DataHandler</code> uses a thread, a pair of pipe streams,
 223      * and the <code>writeTo</code> method to produce the data. <p>
 224      *
 225      * @since   JavaMail 1.2
 226      */
 227     public static String getEncoding(DataHandler dh) {
 228         ContentType cType = null;
 229         String encoding = null;
 230 
 231         /*
 232          * Try to pick the most efficient means of determining the
 233          * encoding.  If this DataHandler was created using a DataSource,
 234          * the getEncoding(DataSource) method is typically faster.  If
 235          * the DataHandler was created with an object, this method is
 236          * much faster.  To distinguish the two cases, we use a heuristic.
 237          * A DataHandler created with an object will always have a null name.
 238          * A DataHandler created with a DataSource will usually have a
 239          * non-null name.
 240          *
 241          * XXX - This is actually quite a disgusting hack, but it makes
 242          *       a common case run over twice as fast.
 243          */
 244         if (dh.getName() != null)
 245             return getEncoding(dh.getDataSource());
 246 
 247         try {
 248             cType = new ContentType(dh.getContentType());
 249         } catch (Exception ex) {
 250             return "base64"; // what else ?!
 251         }
 252 
 253         if (cType.match("text/*")) {
 254             // Check all of the available bytes
 255             AsciiOutputStream aos = new AsciiOutputStream(false, false);
 256             try {
 257                 dh.writeTo(aos);
 258             } catch (IOException ex) { }        // ignore it
 259             switch (aos.getAscii()) {
 260             case ALL_ASCII:
 261                 encoding = "7bit"; // all ascii
 262                 break;
 263             case MOSTLY_ASCII:
 264                 encoding = "quoted-printable"; // mostly ascii
 265                 break;
 266             default:
 267                 encoding = "base64"; // mostly binary
 268                 break;
 269             }
 270         } else { // not "text"
 271             // Check all of available bytes, break out if we find
 272             // at least one non-US-ASCII character
 273             AsciiOutputStream aos =
 274                         new AsciiOutputStream(true, encodeEolStrict);
 275             try {
 276                 dh.writeTo(aos);
 277             } catch (IOException ex) { }        // ignore it
 278             if (aos.getAscii() == ALL_ASCII) // all ascii
 279                 encoding = "7bit";
 280             else // found atleast one non-ascii character, use b64
 281                 encoding = "base64";
 282         }
 283 
 284         return encoding;
 285     }
 286 
 287     /**
 288      * Decode the given input stream. The Input stream returned is
 289      * the decoded input stream. All the encodings defined in RFC 2045
 290      * are supported here. They include "base64", "quoted-printable",
 291      * "7bit", "8bit", and "binary". In addition, "uuencode" is also
 292      * supported.
 293      *
 294      * @param   is              input stream
 295      * @param   encoding        the encoding of the stream.
 296      * @return                  decoded input stream.
 297      */
 298     public static InputStream decode(InputStream is, String encoding)
 299                 throws MessagingException {
 300         if (encoding.equalsIgnoreCase("base64"))
 301             return new BASE64DecoderStream(is);
 302         else if (encoding.equalsIgnoreCase("quoted-printable"))
 303             return new QPDecoderStream(is);
 304         else if (encoding.equalsIgnoreCase("uuencode") ||
 305                  encoding.equalsIgnoreCase("x-uuencode") ||
 306                  encoding.equalsIgnoreCase("x-uue"))
 307             return new UUDecoderStream(is);
 308         else if (encoding.equalsIgnoreCase("binary") ||
 309                  encoding.equalsIgnoreCase("7bit") ||
 310                  encoding.equalsIgnoreCase("8bit"))
 311             return is;
 312         else
 313             throw new MessagingException("Unknown encoding: " + encoding);
 314     }
 315 
 316     /**
 317      * Wrap an encoder around the given output stream.
 318      * All the encodings defined in RFC 2045 are supported here.
 319      * They include "base64", "quoted-printable", "7bit", "8bit" and
 320      * "binary". In addition, "uuencode" is also supported.
 321      *
 322      * @param   os              output stream
 323      * @param   encoding        the encoding of the stream.
 324      * @return                  output stream that applies the
 325      *                          specified encoding.
 326      */
 327     public static OutputStream encode(OutputStream os, String encoding)
 328                 throws MessagingException {
 329         if (encoding == null)
 330             return os;
 331         else if (encoding.equalsIgnoreCase("base64"))
 332             return new BASE64EncoderStream(os);
 333         else if (encoding.equalsIgnoreCase("quoted-printable"))
 334             return new QPEncoderStream(os);
 335         else if (encoding.equalsIgnoreCase("uuencode") ||
 336                  encoding.equalsIgnoreCase("x-uuencode") ||
 337                  encoding.equalsIgnoreCase("x-uue"))
 338             return new UUEncoderStream(os);
 339         else if (encoding.equalsIgnoreCase("binary") ||
 340                  encoding.equalsIgnoreCase("7bit") ||
 341                  encoding.equalsIgnoreCase("8bit"))
 342             return os;
 343         else
 344             throw new MessagingException("Unknown encoding: " +encoding);
 345     }
 346 
 347     /**
 348      * Wrap an encoder around the given output stream.
 349      * All the encodings defined in RFC 2045 are supported here.
 350      * They include "base64", "quoted-printable", "7bit", "8bit" and
 351      * "binary". In addition, "uuencode" is also supported.
 352      * The <code>filename</code> parameter is used with the "uuencode"
 353      * encoding and is included in the encoded output.
 354      *
 355      * @param   os              output stream
 356      * @param   encoding        the encoding of the stream.
 357      * @param   filename        name for the file being encoded (only used
 358      *                          with uuencode)
 359      * @return                  output stream that applies the
 360      *                          specified encoding.
 361      * @since                   JavaMail 1.2
 362      */
 363     public static OutputStream encode(OutputStream os, String encoding,
 364                                       String filename)
 365                 throws MessagingException {
 366         if (encoding == null)
 367             return os;
 368         else if (encoding.equalsIgnoreCase("base64"))
 369             return new BASE64EncoderStream(os);
 370         else if (encoding.equalsIgnoreCase("quoted-printable"))
 371             return new QPEncoderStream(os);
 372         else if (encoding.equalsIgnoreCase("uuencode") ||
 373                  encoding.equalsIgnoreCase("x-uuencode") ||
 374                  encoding.equalsIgnoreCase("x-uue"))
 375             return new UUEncoderStream(os, filename);
 376         else if (encoding.equalsIgnoreCase("binary") ||
 377                  encoding.equalsIgnoreCase("7bit") ||
 378                  encoding.equalsIgnoreCase("8bit"))
 379             return os;
 380         else
 381             throw new MessagingException("Unknown encoding: " +encoding);
 382     }
 383 
 384     /**
 385      * Encode a RFC 822 "text" token into mail-safe form as per
 386      * RFC 2047. <p>
 387      *
 388      * The given Unicode string is examined for non US-ASCII
 389      * characters. If the string contains only US-ASCII characters,
 390      * it is returned as-is.  If the string contains non US-ASCII
 391      * characters, it is first character-encoded using the platform's
 392      * default charset, then transfer-encoded using either the B or
 393      * Q encoding. The resulting bytes are then returned as a Unicode
 394      * string containing only ASCII  characters. <p>
 395      *
 396      * Note that this method should be used to encode only
 397      * "unstructured" RFC 822 headers. <p>
 398      *
 399      * Example of usage:
 400      * <p><blockquote><pre>
 401      *
 402      *  MimeBodyPart part = ...
 403      *  String rawvalue = "FooBar Mailer, Japanese version 1.1"
 404      *  try {
 405      *    // If we know for sure that rawvalue contains only US-ASCII
 406      *    // characters, we can skip the encoding part
 407      *    part.setHeader("X-mailer", MimeUtility.encodeText(rawvalue));
 408      *  } catch (UnsupportedEncodingException e) {
 409      *    // encoding failure
 410      *  } catch (MessagingException me) {
 411      *   // setHeader() failure
 412      *  }
 413      *
 414      * </pre></blockquote><p>
 415      *
 416      * @param   text    unicode string
 417      * @return  Unicode string containing only US-ASCII characters
 418      * @exception UnsupportedEncodingException if the encoding fails
 419      */
 420     public static String encodeText(String text)
 421                         throws UnsupportedEncodingException {
 422         return encodeText(text, null, null);
 423     }
 424 
 425     /**
 426      * Encode a RFC 822 "text" token into mail-safe form as per
 427      * RFC 2047. <p>
 428      *
 429      * The given Unicode string is examined for non US-ASCII
 430      * characters. If the string contains only US-ASCII characters,
 431      * it is returned as-is.  If the string contains non US-ASCII
 432      * characters, it is first character-encoded using the specified
 433      * charset, then transfer-encoded using either the B or Q encoding.
 434      * The resulting bytes are then returned as a Unicode string
 435      * containing only ASCII characters. <p>
 436      *
 437      * Note that this method should be used to encode only
 438      * "unstructured" RFC 822 headers.
 439      *
 440      * @param   text    the header value
 441      * @param   charset the charset. If this parameter is null, the
 442      *          platform's default chatset is used.
 443      * @param   encoding the encoding to be used. Currently supported
 444      *          values are "B" and "Q". If this parameter is null, then
 445      *          the "Q" encoding is used if most of characters to be
 446      *          encoded are in the ASCII charset, otherwise "B" encoding
 447      *          is used.
 448      * @return  Unicode string containing only US-ASCII characters
 449      */
 450     public static String encodeText(String text, String charset,
 451                                     String encoding)
 452                         throws UnsupportedEncodingException {
 453         return encodeWord(text, charset, encoding, false);
 454     }
 455 
 456     /**
 457      * Decode "unstructured" headers, that is, headers that are defined
 458      * as '*text' as per RFC 822. <p>
 459      *
 460      * The string is decoded using the algorithm specified in
 461      * RFC 2047, Section 6.1.1. If the charset-conversion fails
 462      * for any sequence, an UnsupportedEncodingException is thrown.
 463      * If the String is not an RFC 2047 style encoded header, it is
 464      * returned as-is <p>
 465      *
 466      * Example of usage:
 467      * <p><blockquote><pre>
 468      *
 469      *  MimeBodyPart part = ...
 470      *  String rawvalue = null;
 471      *  String  value = null;
 472      *  try {
 473      *    if ((rawvalue = part.getHeader("X-mailer")[0]) != null)
 474      *      value = MimeUtility.decodeText(rawvalue);
 475      *  } catch (UnsupportedEncodingException e) {
 476      *      // Don't care
 477      *      value = rawvalue;
 478      *  } catch (MessagingException me) { }
 479      *
 480      *  return value;
 481      *
 482      * </pre></blockquote><p>
 483      *
 484      * @param   etext   the possibly encoded value
 485      * @exception       UnsupportedEncodingException if the charset
 486      *                  conversion failed.
 487      */
 488     public static String decodeText(String etext)
 489                 throws UnsupportedEncodingException {
 490         /*
 491          * We look for sequences separated by "linear-white-space".
 492          * (as per RFC 2047, Section 6.1.1)
 493          * RFC 822 defines "linear-white-space" as SPACE | HT | CR | NL.
 494          */
 495         String lwsp = " \t\n\r";
 496         StringTokenizer st;
 497 
 498         /*
 499          * First, lets do a quick run thru the string and check
 500          * whether the sequence "=?"  exists at all. If none exists,
 501          * we know there are no encoded-words in here and we can just
 502          * return the string as-is, without suffering thru the later
 503          * decoding logic.
 504          * This handles the most common case of unencoded headers
 505          * efficiently.
 506          */
 507         if (etext.indexOf("=?") == -1)
 508             return etext;
 509 
 510         // Encoded words found. Start decoding ...
 511 
 512         st = new StringTokenizer(etext, lwsp, true);
 513         StringBuffer sb = new StringBuffer();  // decode buffer
 514         StringBuffer wsb = new StringBuffer(); // white space buffer
 515         boolean prevWasEncoded = false;
 516 
 517         while (st.hasMoreTokens()) {
 518             char c;
 519             String s = st.nextToken();
 520             // If whitespace, append it to the whitespace buffer
 521             if (((c = s.charAt(0)) == ' ') || (c == '\t') ||
 522                 (c == '\r') || (c == '\n'))
 523                 wsb.append(c);
 524             else {
 525                 // Check if token is an 'encoded-word' ..
 526                 String word;
 527                 try {
 528                     word = decodeWord(s);
 529                     // Yes, this IS an 'encoded-word'.
 530                     if (!prevWasEncoded && wsb.length() > 0) {
 531                         // if the previous word was also encoded, we
 532                         // should ignore the collected whitespace. Else
 533                         // we include the whitespace as well.
 534                         sb.append(wsb);
 535                     }
 536                     prevWasEncoded = true;
 537                 } catch (ParseException pex) {
 538                     // This is NOT an 'encoded-word'.
 539                     word = s;
 540                     // possibly decode inner encoded words
 541                     if (!decodeStrict)
 542                         word = decodeInnerWords(word);
 543                     // include colleced whitespace ..
 544                     if (wsb.length() > 0)
 545                         sb.append(wsb);
 546                     prevWasEncoded = false;
 547                 }
 548                 sb.append(word); // append the actual word
 549                 wsb.setLength(0); // reset wsb for reuse
 550             }
 551         }
 552         return sb.toString();
 553     }
 554 
 555     /**
 556      * Encode a RFC 822 "word" token into mail-safe form as per
 557      * RFC 2047. <p>
 558      *
 559      * The given Unicode string is examined for non US-ASCII
 560      * characters. If the string contains only US-ASCII characters,
 561      * it is returned as-is.  If the string contains non US-ASCII
 562      * characters, it is first character-encoded using the platform's
 563      * default charset, then transfer-encoded using either the B or
 564      * Q encoding. The resulting bytes are then returned as a Unicode
 565      * string containing only ASCII  characters. <p>
 566      *
 567      * This method is meant to be used when creating RFC 822 "phrases".
 568      * The InternetAddress class, for example, uses this to encode
 569      * it's 'phrase' component.
 570      *
 571      * @param   text    unicode string
 572      * @return  Array of Unicode strings containing only US-ASCII
 573      *          characters.
 574      * @exception UnsupportedEncodingException if the encoding fails
 575      */
 576     public static String encodeWord(String word)
 577                         throws UnsupportedEncodingException {
 578         return encodeWord(word, null, null);
 579     }
 580 
 581     /**
 582      * Encode a RFC 822 "word" token into mail-safe form as per
 583      * RFC 2047. <p>
 584      *
 585      * The given Unicode string is examined for non US-ASCII
 586      * characters. If the string contains only US-ASCII characters,
 587      * it is returned as-is.  If the string contains non US-ASCII
 588      * characters, it is first character-encoded using the specified
 589      * charset, then transfer-encoded using either the B or Q encoding.
 590      * The resulting bytes are then returned as a Unicode string
 591      * containing only ASCII characters. <p>
 592      *
 593      * @param   text    unicode string
 594      * @param   charset the MIME charset
 595      * @param   encoding the encoding to be used. Currently supported
 596      *          values are "B" and "Q". If this parameter is null, then
 597      *          the "Q" encoding is used if most of characters to be
 598      *          encoded are in the ASCII charset, otherwise "B" encoding
 599      *          is used.
 600      * @return  Unicode string containing only US-ASCII characters
 601      * @exception UnsupportedEncodingException if the encoding fails
 602      */
 603     public static String encodeWord(String word, String charset,
 604                                     String encoding)
 605                         throws UnsupportedEncodingException {
 606         return encodeWord(word, charset, encoding, true);
 607     }
 608 
 609     /*
 610      * Encode the given string. The parameter 'encodingWord' should
 611      * be true if a RFC 822 "word" token is being encoded and false if a
 612      * RFC 822 "text" token is being encoded. This is because the
 613      * "Q" encoding defined in RFC 2047 has more restrictions when
 614      * encoding "word" tokens. (Sigh)
 615      */
 616     private static String encodeWord(String string, String charset,
 617                                      String encoding, boolean encodingWord)
 618                         throws UnsupportedEncodingException {
 619 
 620         // If 'string' contains only US-ASCII characters, just
 621         // return it.
 622         int ascii = checkAscii(string);
 623         if (ascii == ALL_ASCII)
 624             return string;
 625 
 626         // Else, apply the specified charset conversion.
 627         String jcharset;
 628         if (charset == null) { // use default charset
 629             jcharset = getDefaultJavaCharset(); // the java charset
 630             charset = getDefaultMIMECharset(); // the MIME equivalent
 631         } else // MIME charset -> java charset
 632             jcharset = javaCharset(charset);
 633 
 634         // If no transfer-encoding is specified, figure one out.
 635         if (encoding == null) {
 636             if (ascii != MOSTLY_NONASCII)
 637                 encoding = "Q";
 638             else
 639                 encoding = "B";
 640         }
 641 
 642         boolean b64;
 643         if (encoding.equalsIgnoreCase("B"))
 644             b64 = true;
 645         else if (encoding.equalsIgnoreCase("Q"))
 646             b64 = false;
 647         else
 648             throw new UnsupportedEncodingException(
 649                         "Unknown transfer encoding: " + encoding);
 650 
 651         StringBuffer outb = new StringBuffer(); // the output buffer
 652         doEncode(string, b64, jcharset,
 653                  // As per RFC 2047, size of an encoded string should not
 654                  // exceed 75 bytes.
 655                  // 7 = size of "=?", '?', 'B'/'Q', '?', "?="
 656                  75 - 7 - charset.length(), // the available space
 657                  "=?" + charset + "?" + encoding + "?", // prefix
 658                  true, encodingWord, outb);
 659 
 660         return outb.toString();
 661     }
 662 
 663     private static void doEncode(String string, boolean b64,
 664                 String jcharset, int avail, String prefix,
 665                 boolean first, boolean encodingWord, StringBuffer buf)
 666                         throws UnsupportedEncodingException {
 667 
 668         // First find out what the length of the encoded version of
 669         // 'string' would be.
 670         byte[] bytes = string.getBytes(jcharset);
 671         int len;
 672         if (b64) // "B" encoding
 673             len = BEncoderStream.encodedLength(bytes);
 674         else // "Q"
 675             len = QEncoderStream.encodedLength(bytes, encodingWord);
 676 
 677         int size;
 678         if ((len > avail) && ((size = string.length()) > 1)) {
 679             // If the length is greater than 'avail', split 'string'
 680             // into two and recurse.
 681             doEncode(string.substring(0, size/2), b64, jcharset,
 682                      avail, prefix, first, encodingWord, buf);
 683             doEncode(string.substring(size/2, size), b64, jcharset,
 684                      avail, prefix, false, encodingWord, buf);
 685         } else {
 686             // length <= than 'avail'. Encode the given string
 687             ByteArrayOutputStream os = new ByteArrayOutputStream(BUFFER_SIZE);
 688             OutputStream eos; // the encoder
 689             if (b64) // "B" encoding
 690                 eos = new BEncoderStream(os);
 691             else // "Q" encoding
 692                 eos = new QEncoderStream(os, encodingWord);
 693 
 694             try { // do the encoding
 695                 eos.write(bytes);
 696                 eos.close();
 697             } catch (IOException ioex) { }
 698 
 699             byte[] encodedBytes = os.toByteArray(); // the encoded stuff
 700             // Now write out the encoded (all ASCII) bytes into our
 701             // StringBuffer
 702             if (!first) // not the first line of this sequence
 703                 if (foldEncodedWords)
 704                     buf.append("\r\n "); // start a continuation line
 705                 else
 706                     buf.append(" "); // line will be folded later
 707 
 708             buf.append(prefix);
 709             for (int i = 0; i < encodedBytes.length; i++)
 710                 buf.append((char)encodedBytes[i]);
 711             buf.append("?="); // terminate the current sequence
 712         }
 713     }
 714 
 715     /**
 716      * The string is parsed using the rules in RFC 2047 for parsing
 717      * an "encoded-word". If the parse fails, a ParseException is
 718      * thrown. Otherwise, it is transfer-decoded, and then
 719      * charset-converted into Unicode. If the charset-conversion
 720      * fails, an UnsupportedEncodingException is thrown.<p>
 721      *
 722      * @param   eword   the possibly encoded value
 723      * @exception       ParseException if the string is not an
 724      *                  encoded-word as per RFC 2047.
 725      * @exception       UnsupportedEncodingException if the charset
 726      *                  conversion failed.
 727      */
 728     public static String decodeWord(String eword)
 729                 throws ParseException, UnsupportedEncodingException {
 730 
 731         if (!eword.startsWith("=?")) // not an encoded word
 732             throw new ParseException();
 733 
 734         // get charset
 735         int start = 2; int pos;
 736         if ((pos = eword.indexOf('?', start)) == -1)
 737             throw new ParseException();
 738         String charset = javaCharset(eword.substring(start, pos));
 739 
 740         // get encoding
 741         start = pos+1;
 742         if ((pos = eword.indexOf('?', start)) == -1)
 743             throw new ParseException();
 744         String encoding = eword.substring(start, pos);
 745 
 746         // get encoded-sequence
 747         start = pos+1;
 748         if ((pos = eword.indexOf("?=", start)) == -1)
 749             throw new ParseException();
 750         String word = eword.substring(start, pos);
 751 
 752         try {
 753             // Extract the bytes from word
 754             ByteArrayInputStream bis =
 755                 new ByteArrayInputStream(ASCIIUtility.getBytes(word));
 756 
 757             // Get the appropriate decoder
 758             InputStream is;
 759             if (encoding.equalsIgnoreCase("B"))
 760                 is = new BASE64DecoderStream(bis);
 761             else if (encoding.equalsIgnoreCase("Q"))
 762                 is = new QDecoderStream(bis);
 763             else
 764                 throw new UnsupportedEncodingException(
 765                                 "unknown encoding: " + encoding);
 766 
 767             // For b64 & q, size of decoded word <= size of word. So
 768             // the decoded bytes must fit into the 'bytes' array. This
 769             // is certainly more efficient than writing bytes into a
 770             // ByteArrayOutputStream and then pulling out the byte[]
 771             // from it.
 772             int count = bis.available();
 773             byte[] bytes = new byte[count];
 774             // count is set to the actual number of decoded bytes
 775             count = is.read(bytes, 0, count);
 776 
 777             // Finally, convert the decoded bytes into a String using
 778             // the specified charset
 779             String s = new String(bytes, 0, count, charset);
 780             if (pos + 2 < eword.length()) {
 781                 // there's still more text in the string
 782                 String rest = eword.substring(pos + 2);
 783                 if (!decodeStrict)
 784                     rest = decodeInnerWords(rest);
 785                 s += rest;
 786             }
 787             return s;
 788         } catch (UnsupportedEncodingException uex) {
 789             // explicitly catch and rethrow this exception, otherwise
 790             // the below IOException catch will swallow this up!
 791             throw uex;
 792         } catch (IOException ioex) {
 793             // Shouldn't happen.
 794             throw new ParseException();
 795         } catch (IllegalArgumentException iex) {
 796             /* An unknown charset of the form ISO-XXX-XXX, will cause
 797              * the JDK to throw an IllegalArgumentException ... Since the
 798              * JDK will attempt to create a classname using this string,
 799              * but valid classnames must not contain the character '-',
 800              * and this results in an IllegalArgumentException, rather than
 801              * the expected UnsupportedEncodingException. Yikes
 802              */
 803             throw new UnsupportedEncodingException();
 804         }
 805     }
 806 
 807     /**
 808      * Look for encoded words within a word.  The MIME spec doesn't
 809      * allow this, but many broken mailers, especially Japanese mailers,
 810      * produce such incorrect encodings.
 811      */
 812     private static String decodeInnerWords(String word)
 813                                 throws UnsupportedEncodingException {
 814         int start = 0, i;
 815         StringBuffer buf = new StringBuffer();
 816         while ((i = word.indexOf("=?", start)) >= 0) {
 817             buf.append(word.substring(start, i));
 818             int end = word.indexOf("?=", i);
 819             if (end < 0)
 820                 break;
 821             String s = word.substring(i, end + 2);
 822             try {
 823                 s = decodeWord(s);
 824             } catch (ParseException pex) {
 825                 // ignore it, just use the original string
 826             }
 827             buf.append(s);
 828             start = end + 2;
 829         }
 830         if (start == 0)
 831             return word;
 832         if (start < word.length())
 833             buf.append(word.substring(start));
 834         return buf.toString();
 835     }
 836 
 837     /**
 838      * A utility method to quote a word, if the word contains any
 839      * characters from the specified 'specials' list.<p>
 840      *
 841      * The <code>HeaderTokenizer</code> class defines two special
 842      * sets of delimiters - MIME and RFC 822. <p>
 843      *
 844      * This method is typically used during the generation of
 845      * RFC 822 and MIME header fields.
 846      *
 847      * @param   word    word to be quoted
 848      * @param   specials the set of special characters
 849      * @return          the possibly quoted word
 850      * @see     javax.mail.internet.HeaderTokenizer#MIME
 851      * @see     javax.mail.internet.HeaderTokenizer#RFC822
 852      */
 853     public static String quote(String word, String specials) {
 854         int len = word.length();
 855 
 856         /*
 857          * Look for any "bad" characters, Escape and
 858          *  quote the entire string if necessary.
 859          */
 860         boolean needQuoting = false;
 861         for (int i = 0; i < len; i++) {
 862             char c = word.charAt(i);
 863             if (c == '"' || c == '\\' || c == '\r' || c == '\n') {
 864                 // need to escape them and then quote the whole string
 865                 StringBuffer sb = new StringBuffer(len + 3);
 866                 sb.append('"');
 867                 sb.append(word.substring(0, i));
 868                 int lastc = 0;
 869                 for (int j = i; j < len; j++) {
 870                     char cc = word.charAt(j);
 871                     if ((cc == '"') || (cc == '\\') ||
 872                         (cc == '\r') || (cc == '\n'))
 873                         if (cc == '\n' && lastc == '\r')
 874                             ;   // do nothing, CR was already escaped
 875                         else
 876                             sb.append('\\');    // Escape the character
 877                     sb.append(cc);
 878                     lastc = cc;
 879                 }
 880                 sb.append('"');
 881                 return sb.toString();
 882             } else if (c < 040 || c >= 0177 || specials.indexOf(c) >= 0)
 883                 // These characters cause the string to be quoted
 884                 needQuoting = true;
 885         }
 886 
 887         if (needQuoting) {
 888             StringBuffer sb = new StringBuffer(len + 2);
 889             sb.append('"').append(word).append('"');
 890             return sb.toString();
 891         } else
 892             return word;
 893     }
 894 
 895     /**
 896      * Fold a string at linear whitespace so that each line is no longer
 897      * than 76 characters, if possible.  If there are more than 76
 898      * non-whitespace characters consecutively, the string is folded at
 899      * the first whitespace after that sequence.  The parameter
 900      * <code>used</code> indicates how many characters have been used in
 901      * the current line; it is usually the length of the header name. <p>
 902      *
 903      * Note that line breaks in the string aren't escaped; they probably
 904      * should be.
 905      *
 906      * @param   used    characters used in line so far
 907      * @param   s       the string to fold
 908      * @return          the folded string
 909      */
 910     /*public*/ static String fold(int used, String s) {
 911         if (!foldText)
 912             return s;
 913 
 914         int end;
 915         char c;
 916         // Strip trailing spaces
 917         for (end = s.length() - 1; end >= 0; end--) {
 918             c = s.charAt(end);
 919             if (c != ' ' && c != '\t')
 920                 break;
 921         }
 922         if (end != s.length() - 1)
 923             s = s.substring(0, end + 1);
 924 
 925         // if the string fits now, just return it
 926         if (used + s.length() <= 76)
 927             return s;
 928 
 929         // have to actually fold the string
 930         StringBuffer sb = new StringBuffer(s.length() + 4);
 931         char lastc = 0;
 932         while (used + s.length() > 76) {
 933             int lastspace = -1;
 934             for (int i = 0; i < s.length(); i++) {
 935                 if (lastspace != -1 && used + i > 76)
 936                     break;
 937                 c = s.charAt(i);
 938                 if (c == ' ' || c == '\t')
 939                     if (!(lastc == ' ' || lastc == '\t'))
 940                         lastspace = i;
 941                 lastc = c;
 942             }
 943             if (lastspace == -1) {
 944                 // no space, use the whole thing
 945                 sb.append(s);
 946                 s = "";
 947                 used = 0;
 948                 break;
 949             }
 950             sb.append(s.substring(0, lastspace));
 951             sb.append("\r\n");
 952             lastc = s.charAt(lastspace);
 953             sb.append(lastc);
 954             s = s.substring(lastspace + 1);
 955             used = 1;
 956         }
 957         sb.append(s);
 958         return sb.toString();
 959     }
 960 
 961     /**
 962      * Unfold a folded header.  Any line breaks that aren't escaped and
 963      * are followed by whitespace are removed.
 964      *
 965      * @param   s       the string to unfold
 966      * @return          the unfolded string
 967      */
 968     /*public*/ static String unfold(String s) {
 969         if (!foldText)
 970             return s;
 971 
 972         StringBuffer sb = null;
 973         int i;
 974         while ((i = indexOfAny(s, "\r\n")) >= 0) {
 975             int start = i;
 976             int l = s.length();
 977             i++;                // skip CR or NL
 978             if (i < l && s.charAt(i - 1) == '\r' && s.charAt(i) == '\n')
 979                 i++;    // skip LF
 980             if (start == 0 || s.charAt(start - 1) != '\\') {
 981                 char c;
 982                 // if next line starts with whitespace, skip all of it
 983                 // XXX - always has to be true?
 984                 if (i < l && ((c = s.charAt(i)) == ' ' || c == '\t')) {
 985                     i++;        // skip whitespace
 986                     while (i < l && ((c = s.charAt(i)) == ' ' || c == '\t'))
 987                         i++;
 988                     if (sb == null)
 989                         sb = new StringBuffer(s.length());
 990                     if (start != 0) {
 991                         sb.append(s.substring(0, start));
 992                         sb.append(' ');
 993                     }
 994                     s = s.substring(i);
 995                     continue;
 996                 }
 997                 // it's not a continuation line, just leave it in
 998                 if (sb == null)
 999                     sb = new StringBuffer(s.length());
1000                 sb.append(s.substring(0, i));
1001                 s = s.substring(i);
1002             } else {
1003                 // there's a backslash at "start - 1"
1004                 // strip it out, but leave in the line break
1005                 if (sb == null)
1006                     sb = new StringBuffer(s.length());
1007                 sb.append(s.substring(0, start - 1));
1008                 sb.append(s.substring(start, i));
1009                 s = s.substring(i);
1010             }
1011         }
1012         if (sb != null) {
1013             sb.append(s);
1014             return sb.toString();
1015         } else
1016             return s;
1017     }
1018 
1019     /**
1020      * Return the first index of any of the characters in "any" in "s",
1021      * or -1 if none are found.
1022      *
1023      * This should be a method on String.
1024      */
1025     private static int indexOfAny(String s, String any) {
1026         return indexOfAny(s, any, 0);
1027     }
1028 
1029     private static int indexOfAny(String s, String any, int start) {
1030         try {
1031             int len = s.length();
1032             for (int i = start; i < len; i++) {
1033                 if (any.indexOf(s.charAt(i)) >= 0)
1034                     return i;
1035             }
1036             return -1;
1037         } catch (StringIndexOutOfBoundsException e) {
1038             return -1;
1039         }
1040     }
1041 
1042     /**
1043      * Convert a MIME charset name into a valid Java charset name. <p>
1044      *
1045      * @param charset   the MIME charset name
1046      * @return  the Java charset equivalent. If a suitable mapping is
1047      *          not available, the passed in charset is itself returned.
1048      */
1049     public static String javaCharset(String charset) {
1050         if (mime2java == null || charset == null)
1051             // no mapping table, or charset parameter is null
1052             return charset;
1053 
1054         String alias = (String)mime2java.get(charset.toLowerCase());
1055         return alias == null ? charset : alias;
1056     }
1057 
1058     /**
1059      * Convert a java charset into its MIME charset name. <p>
1060      *
1061      * Note that a future version of JDK (post 1.2) might provide
1062      * this functionality, in which case, we may deprecate this
1063      * method then.
1064      *
1065      * @param   charset    the JDK charset
1066      * @return          the MIME/IANA equivalent. If a mapping
1067      *                  is not possible, the passed in charset itself
1068      *                  is returned.
1069      * @since           JavaMail 1.1
1070      */
1071     public static String mimeCharset(String charset) {
1072         if (java2mime == null || charset == null)
1073             // no mapping table or charset param is null
1074             return charset;
1075 
1076         String alias = (String)java2mime.get(charset.toLowerCase());
1077         return alias == null ? charset : alias;
1078     }
1079 
1080     private static String defaultJavaCharset;
1081     private static String defaultMIMECharset;
1082 
1083     /**
1084      * Get the default charset corresponding to the system's current
1085      * default locale.  If the System property <code>mail.mime.charset</code>
1086      * is set, a system charset corresponding to this MIME charset will be
1087      * returned. <p>
1088      *
1089      * @return  the default charset of the system's default locale,
1090      *          as a Java charset. (NOT a MIME charset)
1091      * @since   JavaMail 1.1
1092      */
1093     public static String getDefaultJavaCharset() {
1094         if (defaultJavaCharset == null) {
1095             /*
1096              * If mail.mime.charset is set, it controls the default
1097              * Java charset as well.
1098              */
1099             String mimecs = null;
1100 
1101             mimecs = SAAJUtil.getSystemProperty("mail.mime.charset");
1102 
1103             if (mimecs != null && mimecs.length() > 0) {
1104                 defaultJavaCharset = javaCharset(mimecs);
1105                 return defaultJavaCharset;
1106             }
1107 
1108             try {
1109                 defaultJavaCharset = System.getProperty("file.encoding",
1110                                                         "8859_1");
1111             } catch (SecurityException sex) {
1112 
1113                 class NullInputStream extends InputStream {
1114                     public int read() {
1115                         return 0;
1116                     }
1117                 }
1118                 InputStreamReader reader =
1119                         new InputStreamReader(new NullInputStream());
1120                 defaultJavaCharset = reader.getEncoding();
1121                 if (defaultJavaCharset == null)
1122                     defaultJavaCharset = "8859_1";
1123             }
1124         }
1125 
1126         return defaultJavaCharset;
1127     }
1128 
1129     /*
1130      * Get the default MIME charset for this locale.
1131      */
1132     static String getDefaultMIMECharset() {
1133         if (defaultMIMECharset == null) {
1134                 defaultMIMECharset = SAAJUtil.getSystemProperty("mail.mime.charset");
1135         }
1136         if (defaultMIMECharset == null)
1137             defaultMIMECharset = mimeCharset(getDefaultJavaCharset());
1138         return defaultMIMECharset;
1139     }
1140 
1141     // Tables to map MIME charset names to Java names and vice versa.
1142     // XXX - Should eventually use J2SE 1.4 java.nio.charset.Charset
1143     private static Hashtable mime2java;
1144     private static Hashtable java2mime;
1145 
1146     static {
1147         java2mime = new Hashtable(40);
1148         mime2java = new Hashtable(10);
1149 
1150         try {
1151             // Use this class's classloader to load the mapping file
1152             // XXX - we should use SecuritySupport, but it's in another package
1153             InputStream is =
1154                     com.sun.xml.internal.messaging.saaj.packaging.mime.internet.MimeUtility.class.getResourceAsStream(
1155                     "/META-INF/javamail.charset.map");
1156 
1157             if (is != null) {
1158                 is = new LineInputStream(is);
1159 
1160                 // Load the JDK-to-MIME charset mapping table
1161                 loadMappings((LineInputStream)is, java2mime);
1162 
1163                 // Load the MIME-to-JDK charset mapping table
1164                 loadMappings((LineInputStream)is, mime2java);
1165             }
1166         } catch (Exception ex) { }
1167 
1168         // If we didn't load the tables, e.g., because we didn't have
1169         // permission, load them manually.  The entries here should be
1170         // the same as the default javamail.charset.map.
1171         if (java2mime.isEmpty()) {
1172             java2mime.put("8859_1", "ISO-8859-1");
1173             java2mime.put("iso8859_1", "ISO-8859-1");
1174             java2mime.put("ISO8859-1", "ISO-8859-1");
1175 
1176             java2mime.put("8859_2", "ISO-8859-2");
1177             java2mime.put("iso8859_2", "ISO-8859-2");
1178             java2mime.put("ISO8859-2", "ISO-8859-2");
1179 
1180             java2mime.put("8859_3", "ISO-8859-3");
1181             java2mime.put("iso8859_3", "ISO-8859-3");
1182             java2mime.put("ISO8859-3", "ISO-8859-3");
1183 
1184             java2mime.put("8859_4", "ISO-8859-4");
1185             java2mime.put("iso8859_4", "ISO-8859-4");
1186             java2mime.put("ISO8859-4", "ISO-8859-4");
1187 
1188             java2mime.put("8859_5", "ISO-8859-5");
1189             java2mime.put("iso8859_5", "ISO-8859-5");
1190             java2mime.put("ISO8859-5", "ISO-8859-5");
1191 
1192             java2mime.put("8859_6", "ISO-8859-6");
1193             java2mime.put("iso8859_6", "ISO-8859-6");
1194             java2mime.put("ISO8859-6", "ISO-8859-6");
1195 
1196             java2mime.put("8859_7", "ISO-8859-7");
1197             java2mime.put("iso8859_7", "ISO-8859-7");
1198             java2mime.put("ISO8859-7", "ISO-8859-7");
1199 
1200             java2mime.put("8859_8", "ISO-8859-8");
1201             java2mime.put("iso8859_8", "ISO-8859-8");
1202             java2mime.put("ISO8859-8", "ISO-8859-8");
1203 
1204             java2mime.put("8859_9", "ISO-8859-9");
1205             java2mime.put("iso8859_9", "ISO-8859-9");
1206             java2mime.put("ISO8859-9", "ISO-8859-9");
1207 
1208             java2mime.put("SJIS", "Shift_JIS");
1209             java2mime.put("MS932", "Shift_JIS");
1210             java2mime.put("JIS", "ISO-2022-JP");
1211             java2mime.put("ISO2022JP", "ISO-2022-JP");
1212             java2mime.put("EUC_JP", "euc-jp");
1213             java2mime.put("KOI8_R", "koi8-r");
1214             java2mime.put("EUC_CN", "euc-cn");
1215             java2mime.put("EUC_TW", "euc-tw");
1216             java2mime.put("EUC_KR", "euc-kr");
1217         }
1218         if (mime2java.isEmpty()) {
1219             mime2java.put("iso-2022-cn", "ISO2022CN");
1220             mime2java.put("iso-2022-kr", "ISO2022KR");
1221             mime2java.put("utf-8", "UTF8");
1222             mime2java.put("utf8", "UTF8");
1223             mime2java.put("ja_jp.iso2022-7", "ISO2022JP");
1224             mime2java.put("ja_jp.eucjp", "EUCJIS");
1225             mime2java.put("euc-kr", "KSC5601");
1226             mime2java.put("euckr", "KSC5601");
1227             mime2java.put("us-ascii", "ISO-8859-1");
1228             mime2java.put("x-us-ascii", "ISO-8859-1");
1229         }
1230     }
1231 
1232     private static void loadMappings(LineInputStream is, Hashtable table) {
1233         String currLine;
1234 
1235         while (true) {
1236             try {
1237                 currLine = is.readLine();
1238             } catch (IOException ioex) {
1239                 break; // error in reading, stop
1240             }
1241 
1242             if (currLine == null) // end of file, stop
1243                 break;
1244             if (currLine.startsWith("--") && currLine.endsWith("--"))
1245                 // end of this table
1246                 break;
1247 
1248             // ignore empty lines and comments
1249             if (currLine.trim().length() == 0 || currLine.startsWith("#"))
1250                 continue;
1251 
1252             // A valid entry is of the form <key><separator><value>
1253             // where, <separator> := SPACE | HT. Parse this
1254             StringTokenizer tk = new StringTokenizer(currLine, " \t");
1255             try {
1256                 String key = tk.nextToken();
1257                 String value = tk.nextToken();
1258                 table.put(key.toLowerCase(), value);
1259             } catch (NoSuchElementException nex) { }
1260         }
1261     }
1262 
1263     static final int ALL_ASCII          = 1;
1264     static final int MOSTLY_ASCII       = 2;
1265     static final int MOSTLY_NONASCII    = 3;
1266 
1267     /**
1268      * Check if the given string contains non US-ASCII characters.
1269      * @param   s       string
1270      * @return          ALL_ASCII if all characters in the string
1271      *                  belong to the US-ASCII charset. MOSTLY_ASCII
1272      *                  if more than half of the available characters
1273      *                  are US-ASCII characters. Else MOSTLY_NONASCII.
1274      */
1275     static int checkAscii(String s) {
1276         int ascii = 0, non_ascii = 0;
1277         int l = s.length();
1278 
1279         for (int i = 0; i < l; i++) {
1280             if (nonascii((int)s.charAt(i))) // non-ascii
1281                 non_ascii++;
1282             else
1283                 ascii++;
1284         }
1285 
1286         if (non_ascii == 0)
1287             return ALL_ASCII;
1288         if (ascii > non_ascii)
1289             return MOSTLY_ASCII;
1290 
1291         return MOSTLY_NONASCII;
1292     }
1293 
1294     /**
1295      * Check if the given byte array contains non US-ASCII characters.
1296      * @param   b       byte array
1297      * @return          ALL_ASCII if all characters in the string
1298      *                  belong to the US-ASCII charset. MOSTLY_ASCII
1299      *                  if more than half of the available characters
1300      *                  are US-ASCII characters. Else MOSTLY_NONASCII.
1301      *
1302      * XXX - this method is no longer used
1303      */
1304     static int checkAscii(byte[] b) {
1305         int ascii = 0, non_ascii = 0;
1306 
1307         for (int i=0; i < b.length; i++) {
1308             // The '&' operator automatically causes b[i] to be promoted
1309             // to an int, and we mask out the higher bytes in the int
1310             // so that the resulting value is not a negative integer.
1311             if (nonascii(b[i] & 0xff)) // non-ascii
1312                 non_ascii++;
1313             else
1314                 ascii++;
1315         }
1316 
1317         if (non_ascii == 0)
1318             return ALL_ASCII;
1319         if (ascii > non_ascii)
1320             return MOSTLY_ASCII;
1321 
1322         return MOSTLY_NONASCII;
1323     }
1324 
1325     /**
1326      * Check if the given input stream contains non US-ASCII characters.
1327      * Upto <code>max</code> bytes are checked. If <code>max</code> is
1328      * set to <code>ALL</code>, then all the bytes available in this
1329      * input stream are checked. If <code>breakOnNonAscii</code> is true
1330      * the check terminates when the first non-US-ASCII character is
1331      * found and MOSTLY_NONASCII is returned. Else, the check continues
1332      * till <code>max</code> bytes or till the end of stream.
1333      *
1334      * @param   is      the input stream
1335      * @param   max     maximum bytes to check for. The special value
1336      *                  ALL indicates that all the bytes in this input
1337      *                  stream must be checked.
1338      * @param   breakOnNonAscii if <code>true</code>, then terminate the
1339      *                  the check when the first non-US-ASCII character
1340      *                  is found.
1341      * @return          ALL_ASCII if all characters in the string
1342      *                  belong to the US-ASCII charset. MOSTLY_ASCII
1343      *                  if more than half of the available characters
1344      *                  are US-ASCII characters. Else MOSTLY_NONASCII.
1345      */
1346     static int checkAscii(InputStream is, int max, boolean breakOnNonAscii) {
1347         int ascii = 0, non_ascii = 0;
1348         int len;
1349         int block = 4096;
1350         int linelen = 0;
1351         boolean longLine = false, badEOL = false;
1352         boolean checkEOL = encodeEolStrict && breakOnNonAscii;
1353         byte buf[] = null;
1354         if (max != 0) {
1355             block = (max == ALL) ? 4096 : Math.min(max, 4096);
1356             buf = new byte[block];
1357         }
1358         while (max != 0) {
1359             try {
1360                 if ((len = is.read(buf, 0, block)) == -1)
1361                     break;
1362                 int lastb = 0;
1363                 for (int i = 0; i < len; i++) {
1364                     // The '&' operator automatically causes b[i] to
1365                     // be promoted to an int, and we mask out the higher
1366                     // bytes in the int so that the resulting value is
1367                     // not a negative integer.
1368                     int b = buf[i] & 0xff;
1369                     if (checkEOL &&
1370                             ((lastb == '\r' && b != '\n') ||
1371                             (lastb != '\r' && b == '\n')))
1372                         badEOL = true;
1373                     if (b == '\r' || b == '\n')
1374                         linelen = 0;
1375                     else {
1376                         linelen++;
1377                         if (linelen > 998)      // 1000 - CRLF
1378                             longLine = true;
1379                     }
1380                     if (nonascii(b)) {  // non-ascii
1381                         if (breakOnNonAscii) // we are done
1382                             return MOSTLY_NONASCII;
1383                         else
1384                             non_ascii++;
1385                     } else
1386                         ascii++;
1387                     lastb = b;
1388                 }
1389             } catch (IOException ioex) {
1390                 break;
1391             }
1392             if (max != ALL)
1393                 max -= len;
1394         }
1395 
1396         if (max == 0 && breakOnNonAscii)
1397             // We have been told to break on the first non-ascii character.
1398             // We haven't got any non-ascii character yet, but then we
1399             // have not checked all of the available bytes either. So we
1400             // cannot say for sure that this input stream is ALL_ASCII,
1401             // and hence we must play safe and return MOSTLY_NONASCII
1402 
1403             return MOSTLY_NONASCII;
1404 
1405         if (non_ascii == 0) { // no non-us-ascii characters so far
1406             // If we're looking at non-text data, and we saw CR without LF
1407             // or vice versa, consider this mostly non-ASCII so that it
1408             // will be base64 encoded (since the quoted-printable encoder
1409             // doesn't encode this case properly).
1410             if (badEOL)
1411                 return MOSTLY_NONASCII;
1412             // if we've seen a long line, we degrade to mostly ascii
1413             else if (longLine)
1414                 return MOSTLY_ASCII;
1415             else
1416                 return ALL_ASCII;
1417         }
1418         if (ascii > non_ascii) // mostly ascii
1419             return MOSTLY_ASCII;
1420         return MOSTLY_NONASCII;
1421     }
1422 
1423     static final boolean nonascii(int b) {
1424         return b >= 0177 || (b < 040 && b != '\r' && b != '\n' && b != '\t');
1425     }
1426 }
1427 
1428 /**
1429  * An OutputStream that determines whether the data written to
1430  * it is all ASCII, mostly ASCII, or mostly non-ASCII.
1431  */
1432 class AsciiOutputStream extends OutputStream {
1433     private boolean breakOnNonAscii;
1434     private int ascii = 0, non_ascii = 0;
1435     private int linelen = 0;
1436     private boolean longLine = false;
1437     private boolean badEOL = false;
1438     private boolean checkEOL = false;
1439     private int lastb = 0;
1440     private int ret = 0;
1441 
1442     public AsciiOutputStream(boolean breakOnNonAscii, boolean encodeEolStrict) {
1443         this.breakOnNonAscii = breakOnNonAscii;
1444         checkEOL = encodeEolStrict && breakOnNonAscii;
1445     }
1446 
1447     public void write(int b) throws IOException {
1448         check(b);
1449     }
1450 
1451     public void write(byte b[]) throws IOException {
1452         write(b, 0, b.length);
1453     }
1454 
1455     public void write(byte b[], int off, int len) throws IOException {
1456         len += off;
1457         for (int i = off; i < len ; i++)
1458             check(b[i]);
1459     }
1460 
1461     private final void check(int b) throws IOException {
1462         b &= 0xff;
1463         if (checkEOL &&
1464                 ((lastb == '\r' && b != '\n') || (lastb != '\r' && b == '\n')))
1465             badEOL = true;
1466         if (b == '\r' || b == '\n')
1467             linelen = 0;
1468         else {
1469             linelen++;
1470             if (linelen > 998)  // 1000 - CRLF
1471                 longLine = true;
1472         }
1473         if (MimeUtility.nonascii(b)) { // non-ascii
1474             non_ascii++;
1475             if (breakOnNonAscii) {      // we are done
1476                 ret = MimeUtility.MOSTLY_NONASCII;
1477                 throw new EOFException();
1478             }
1479         } else
1480             ascii++;
1481         lastb = b;
1482     }
1483 
1484     /**
1485      * Return ASCII-ness of data stream.
1486      */
1487     public int getAscii() {
1488         if (ret != 0)
1489             return ret;
1490         // If we're looking at non-text data, and we saw CR without LF
1491         // or vice versa, consider this mostly non-ASCII so that it
1492         // will be base64 encoded (since the quoted-printable encoder
1493         // doesn't encode this case properly).
1494         if (badEOL)
1495             return MimeUtility.MOSTLY_NONASCII;
1496         else if (non_ascii == 0) { // no non-us-ascii characters so far
1497             // if we've seen a long line, we degrade to mostly ascii
1498             if (longLine)
1499                 return MimeUtility.MOSTLY_ASCII;
1500             else
1501                 return MimeUtility.ALL_ASCII;
1502         }
1503         if (ascii > non_ascii) // mostly ascii
1504             return MimeUtility.MOSTLY_ASCII;
1505         return MimeUtility.MOSTLY_NONASCII;
1506     }
1507 }