/[classpathx]/mail/source/javax/mail/internet/MimeUtility.java
ViewVC logotype

Contents of /mail/source/javax/mail/internet/MimeUtility.java

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1.16 - (show annotations) (download)
Wed Jan 19 19:06:52 2005 UTC (19 years, 4 months ago) by dog
Branch: MAIN
Changes since 1.15: +5 -5 lines
2005-01-19  Chris Burdess  <dog@bluezoo.org>

        * acinclude.m4: Detect JSSE in Java runtime.
        * UUInputStream.java,UUOutputStream.java: New UU encoder/decoder
        supporting block reads and under FSF copyright.
        * UUDecoderStream.java,UUEncoderStream.java: Removed.

1 /*
2 * MimeUtility.java
3 * Copyright(C) 2002, 2004 The Free Software Foundation
4 *
5 * This file is part of GNU JavaMail, a library.
6 *
7 * GNU JavaMail is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 *(at your option) any later version.
11 *
12 * GNU JavaMail is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this library; if not, write to the Free Software
19 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 *
21 * As a special exception, if you link this library with other files to
22 * produce an executable, this library does not by itself cause the
23 * resulting executable to be covered by the GNU General Public License.
24 * This exception does not however invalidate any other reasons why the
25 * executable file might be covered by the GNU General Public License.
26 */
27
28 package javax.mail.internet;
29
30 import java.io.ByteArrayInputStream;
31 import java.io.ByteArrayOutputStream;
32 import java.io.EOFException;
33 import java.io.InputStream;
34 import java.io.InputStreamReader;
35 import java.io.IOException;
36 import java.io.OutputStream;
37 import java.io.UnsupportedEncodingException;
38 import java.util.HashMap;
39 import java.util.StringTokenizer;
40 import java.util.NoSuchElementException;
41 import javax.activation.DataHandler;
42 import javax.activation.DataSource;
43 import javax.mail.MessagingException;
44 import javax.mail.Session;
45
46 import gnu.inet.util.LineInputStream;
47 import gnu.mail.util.Base64InputStream;
48 import gnu.mail.util.Base64OutputStream;
49 import gnu.mail.util.BOutputStream;
50 import gnu.mail.util.QInputStream;
51 import gnu.mail.util.QOutputStream;
52 import gnu.mail.util.QPInputStream;
53 import gnu.mail.util.QPOutputStream;
54 import gnu.mail.util.UUInputStream;
55 import gnu.mail.util.UUOutputStream;
56
57 /**
58 * This is a utility class that provides various MIME related functionality.
59 * <p>
60 * There are a set of methods to encode and decode MIME headers as per
61 * RFC 2047. A brief description on handling such headers is given below:
62 * <p>
63 * RFC 822 mail headers must contain only US-ASCII characters. Headers that
64 * contain non US-ASCII characters must be encoded so that they contain only
65 * US-ASCII characters. Basically, this process involves using either BASE64
66 * or QP to encode certain characters. RFC 2047 describes this in detail.
67 * <p>
68 * In Java, Strings contain(16 bit) Unicode characters. ASCII is a subset of
69 * Unicode(and occupies the range 0 - 127). A String that contains only ASCII
70 * characters is already mail-safe. If the String contains non US-ASCII
71 * characters, it must be encoded. An additional complexity in this step is that
72 * since Unicode is not yet a widely used charset, one might want to first
73 * charset-encode the String into another charset and then do the
74 * transfer-encoding.
75 * <p>
76 * Note that to get the actual bytes of a mail-safe String(say, for sending
77 * over SMTP), one must do
78 * <pre>
79
80 byte[] bytes = string.getBytes("iso-8859-1");
81
82 <pre>
83 * The <code>setHeader()</code> and <code>addHeader()</code> methods on
84 * MimeMessage and MimeBodyPart assume that the given header values are
85 * Unicode strings that contain only US-ASCII characters. Hence the callers
86 * of those methods must insure that the values they pass do not contain non
87 * US-ASCII characters. The methods in this class help do this.
88 * <p>
89 * The <code>getHeader()</code> family of methods on MimeMessage and
90 * MimeBodyPart return the raw header value. These might be encoded as per
91 * RFC 2047, and if so, must be decoded into Unicode Strings.
92 * The methods in this class help to do this.
93 *
94 * @author <a href="mailto:dog@gnu.org">Chris Burdess</a>
95 * @version 1.3
96 */
97 public class MimeUtility
98 {
99
100 /*
101 * Uninstantiable.
102 */
103 private MimeUtility()
104 {
105 }
106
107 /**
108 * Get the content-transfer-encoding that should be applied to the input
109 * stream of this datasource, to make it mailsafe.
110 * <p>
111 * The algorithm used here is:
112 * <ul>
113 * <li>If the primary type of this datasource is "text" and if all the bytes
114 * in its input stream are US-ASCII, then the encoding is "7bit". If more
115 * than half of the bytes are non-US-ASCII, then the encoding is "base64".
116 * If less than half of the bytes are non-US-ASCII, then the encoding is
117 * "quoted-printable".
118 * <li>If the primary type of this datasource is not "text", then if all the
119 * bytes of its input stream are US-ASCII, the encoding is "7bit". If
120 * there is even one non-US-ASCII character, the encoding is "base64".
121 * @param ds DataSource
122 * @return the encoding.
123 * This is either "7bit", "quoted-printable" or "base64"
124 */
125 public static String getEncoding(DataSource ds)
126 {
127 String encoding = "base64";
128 InputStream is = null;
129 try
130 {
131 is = ds.getInputStream();
132 ContentType ct = new ContentType(ds.getContentType());
133 boolean text = ct.match("text/*");
134 switch (asciiStatus(is, ALL, text))
135 {
136 case ALL_ASCII:
137 encoding = "7bit";
138 break;
139 case MAJORITY_ASCII:
140 if (text)
141 {
142 encoding = "quoted-printable";
143 }
144 break;
145 }
146 }
147 catch (Exception e)
148 {
149 }
150 try
151 {
152 is.close();
153 }
154 catch (IOException e)
155 {
156 }
157 return encoding;
158 }
159
160 /**
161 * Same as getEncoding(DataSource) except that instead of reading the data
162 * from an InputStream it uses the writeTo method to examine the data.
163 * This is more efficient in the common case of a DataHandler created
164 * with an object and a MIME type(for example, a "text/plain" String)
165 * because all the I/O is done in this thread.
166 * In the case requiring an InputStream the DataHandler uses a thread,
167 * a pair of pipe streams, and the writeTo method to produce the data.
168 */
169 public static String getEncoding(DataHandler dh)
170 {
171 String encoding = "base64";
172 if (dh.getName() != null)
173 {
174 return getEncoding(dh.getDataSource());
175 }
176 try
177 {
178 ContentType ct = new ContentType(dh.getContentType());
179 boolean text = ct.match("text/*");
180
181 AsciiOutputStream aos =
182 new AsciiOutputStream(!text, encodeeolStrict() && !text);
183 try
184 {
185 dh.writeTo(aos);
186 }
187 catch (IOException e)
188 {
189 }
190 switch (aos.status())
191 {
192 case ALL_ASCII:
193 encoding = "7bit";
194 break;
195 case MAJORITY_ASCII:
196 if (text)
197 {
198 encoding = "quoted-printable";
199 }
200 break;
201 }
202 }
203 catch (Exception e)
204 {
205 }
206 return encoding;
207 }
208
209 /**
210 * Decode the given input stream.
211 * The Input stream returned is the decoded input stream.
212 * All the encodings defined in RFC 2045 are supported here.
213 * They include "base64", "quoted-printable", "7bit", "8bit", and
214 * "binary". In addition, "uuencode" is also supported.
215 * @param is input stream
216 * @param encoding the encoding of the stream.
217 * @return decoded input stream.
218 */
219 public static InputStream decode(InputStream is, String encoding)
220 throws MessagingException
221 {
222 if (encoding.equalsIgnoreCase("base64"))
223 {
224 return new Base64InputStream(is);
225 }
226 if (encoding.equalsIgnoreCase("quoted-printable"))
227 {
228 return new QPInputStream(is);
229 }
230 if (encoding.equalsIgnoreCase("uuencode") ||
231 encoding.equalsIgnoreCase("x-uuencode"))
232 {
233 return new UUInputStream(is);
234 }
235 if (encoding.equalsIgnoreCase("binary") ||
236 encoding.equalsIgnoreCase("7bit") ||
237 encoding.equalsIgnoreCase("8bit"))
238 {
239 return is;
240 }
241 throw new MessagingException("Unknown encoding: " + encoding);
242 }
243
244 /**
245 * Wrap an encoder around the given output stream.
246 * All the encodings defined in RFC 2045 are supported here.
247 * They include "base64", "quoted-printable", "7bit", "8bit" and "binary".
248 * In addition, "uuencode" is also supported.
249 * @param os output stream
250 * @param encoding the encoding of the stream.
251 * @return output stream that applies the specified encoding.
252 */
253 public static OutputStream encode(OutputStream os, String encoding)
254 throws MessagingException
255 {
256 if (encoding == null)
257 {
258 return os;
259 }
260 if (encoding.equalsIgnoreCase("base64"))
261 {
262 return new Base64OutputStream(os);
263 }
264 if (encoding.equalsIgnoreCase("quoted-printable"))
265 {
266 return new QPOutputStream(os);
267 }
268 if (encoding.equalsIgnoreCase("uuencode") ||
269 encoding.equalsIgnoreCase("x-uuencode"))
270 {
271 return new UUOutputStream(os);
272 }
273 if (encoding.equalsIgnoreCase("binary") ||
274 encoding.equalsIgnoreCase("7bit") ||
275 encoding.equalsIgnoreCase("8bit"))
276 {
277 return os;
278 }
279 throw new MessagingException("Unknown encoding: " + encoding);
280 }
281
282 /**
283 * Wrap an encoder around the given output stream.
284 * All the encodings defined in RFC 2045 are supported here.
285 * They include "base64", "quoted-printable", "7bit", "8bit" and "binary".
286 * In addition, "uuencode" is also supported. The <code>filename</code>
287 * parameter is used with the "uuencode" encoding and is included in the
288 * encoded output.
289 * @param os output stream
290 * @param encoding the encoding of the stream.
291 * @param filename name for the file being encoded(only used with uuencode)
292 * @return output stream that applies the specified encoding.
293 */
294 public static OutputStream encode(OutputStream os, String encoding,
295 String filename)
296 throws MessagingException
297 {
298 if (encoding == null)
299 {
300 return os;
301 }
302 if (encoding.equalsIgnoreCase("base64"))
303 {
304 return new Base64OutputStream(os);
305 }
306 if (encoding.equalsIgnoreCase("quoted-printable"))
307 {
308 return new QPOutputStream(os);
309 }
310 if (encoding.equalsIgnoreCase("uuencode") ||
311 encoding.equalsIgnoreCase("x-uuencode"))
312 {
313 return new UUOutputStream(os, filename);
314 }
315 if (encoding.equalsIgnoreCase("binary") ||
316 encoding.equalsIgnoreCase("7bit") ||
317 encoding.equalsIgnoreCase("8bit"))
318 {
319 return os;
320 }
321 throw new MessagingException("Unknown encoding: " + encoding);
322 }
323
324 /**
325 * Encode a RFC 822 "text" token into mail-safe form as per RFC 2047.
326 * <p>
327 * The given Unicode string is examined for non US-ASCII characters. If the
328 * string contains only US-ASCII characters, it is returned as-is. If the
329 * string contains non US-ASCII characters, it is first character-encoded
330 * using the platform's default charset, then transfer-encoded using either
331 * the B or Q encoding. The resulting bytes are then returned as a Unicode
332 * string containing only ASCII characters.
333 * <p>
334 * Note that this method should be used to encode only "unstructured"
335 * RFC 822 headers.
336 * <p>
337 * Example of usage:
338 * <pre>
339 MimePart part = ...
340 String rawvalue = "FooBar Mailer, Japanese version 1.1"
341 try {
342 // If we know for sure that rawvalue contains only US-ASCII
343 // characters, we can skip the encoding part
344 part.setHeader("X-mailer", MimeUtility.encodeText(rawvalue));
345 } catch (UnsupportedEncodingException e) {
346 // encoding failure
347 } catch (MessagingException me) {
348 // setHeader() failure
349 }
350 </pre>
351 * @param text unicode string
352 * @return Unicode string containing only US-ASCII characters
353 * @param UnsupportedEncodingException if the encoding fails
354 */
355 public static String encodeText(String text)
356 throws UnsupportedEncodingException
357 {
358 return encodeText(text, null, null);
359 }
360
361 /**
362 * Encode a RFC 822 "text" token into mail-safe form as per RFC 2047.
363 * <p>
364 * The given Unicode string is examined for non US-ASCII characters. If the
365 * string contains only US-ASCII characters, it is returned as-is. If the
366 * string contains non US-ASCII characters, it is first character-encoded
367 * using the platform's default charset, then transfer-encoded using either
368 * the B or Q encoding. The resulting bytes are then returned as a Unicode
369 * string containing only ASCII characters.
370 * <p>
371 * Note that this method should be used to encode only "unstructured"
372 * RFC 822 headers.
373 * <p>
374 * @param text the header value
375 * @param charset the charset. If this parameter is null, the platform's
376 * default chatset is used.
377 * @param encoding the encoding to be used.
378 * Currently supported values are "B" and "Q".
379 * If this parameter is null, then the "Q" encoding is used if most of the
380 * characters to be encoded are in the ASCII charset, otherwise "B"
381 * encoding is used.
382 * @return Unicode string containing only US-ASCII characters
383 */
384 public static String encodeText(String text, String charset, String encoding)
385 throws UnsupportedEncodingException
386 {
387 return encodeWord(text, charset, encoding, false);
388 }
389
390 /**
391 * Decode "unstructured" headers, that is, headers that are defined as '*text'
392 * as per RFC 822.
393 * <p>
394 * The string is decoded using the algorithm specified in RFC 2047, Section
395 * 6.1.1. If the charset-conversion fails for any sequence, an
396 * UnsupportedEncodingException is thrown. If the String is not an RFC 2047
397 * style encoded header, it is returned as-is
398 * <p>
399 * Example of usage:
400 * <pre>
401 MimePart part = ...
402 String rawvalue = null;
403 String value = null;
404 try {
405 if ((rawvalue = part.getHeader("X-mailer")[0]) != null)
406 value = MimeUtility.decodeText(rawvalue);
407 } catch (UnsupportedEncodingException e) {
408 // Don't care
409 value = rawvalue;
410 } catch (MessagingException me) { }
411 return value;
412 <pre>
413 * @param etext the possibly encoded value
414 * @exception UnsupportedEncodingException if the charset conversion failed.
415 */
416 public static String decodeText(String etext)
417 throws UnsupportedEncodingException
418 {
419 String delimiters = "\t\n\r ";
420 if (etext.indexOf("=?") == -1)
421 {
422 return etext;
423 }
424 StringTokenizer st = new StringTokenizer(etext, delimiters, true);
425 StringBuffer buffer = new StringBuffer();
426 StringBuffer extra = new StringBuffer();
427 boolean decoded = false;
428 while (st.hasMoreTokens())
429 {
430 String token = st.nextToken();
431 char c = token.charAt(0);
432 if (delimiters.indexOf(c) > -1)
433 {
434 extra.append(c);
435 }
436 else
437 {
438 try
439 {
440 token = decodeWord(token);
441 if (!decoded && extra.length() > 0)
442 {
443 buffer.append(extra);
444 }
445 decoded = true;
446 }
447 catch (ParseException e)
448 {
449 if (!decodetextStrict())
450 {
451 token = decodeInnerText(token);
452 }
453 if (extra.length() > 0)
454 {
455 buffer.append(extra);
456 }
457 decoded = false;
458 }
459 buffer.append(token);
460 extra.setLength(0);
461 }
462 }
463 return buffer.toString();
464 }
465
466 /**
467 * Encode a RFC 822 "word" token into mail-safe form as per RFC 2047.
468 * <p>
469 * The given Unicode string is examined for non US-ASCII characters.
470 * If the string contains only US-ASCII characters, it is returned as-is.
471 * If the string contains non US-ASCII characters, it is first
472 * character-encoded using the platform's default charset, then
473 * transfer-encoded using either the B or Q encoding.
474 * The resulting bytes are then returned as a Unicode string containing
475 * only ASCII characters.
476 * <p>
477 * This method is meant to be used when creating RFC 822 "phrases". The
478 * InternetAddress class, for example, uses this to encode it's 'phrase'
479 * component.
480 * @param text unicode string
481 * @return Unicode string containing only US-ASCII characters.
482 * @exception UnsupportedEncodingException if the encoding fails
483 */
484 public static String encodeWord(String text)
485 throws UnsupportedEncodingException
486 {
487 return encodeWord(text, null, null);
488 }
489
490 /**
491 * Encode a RFC 822 "word" token into mail-safe form as per RFC 2047.
492 * <p>
493 * The given Unicode string is examined for non US-ASCII characters.
494 * If the string contains only US-ASCII characters, it is returned as-is.
495 * If the string contains non US-ASCII characters, it is first
496 * character-encoded using the platform's default charset, then
497 * transfer-encoded using either the B or Q encoding.
498 * The resulting bytes are then returned as a Unicode string containing
499 * only ASCII characters.
500 * <p>
501 * @param text unicode string
502 * @param charset the MIME charset
503 * @param encoding the encoding to be used.
504 * Currently supported values are "B" and "Q".
505 * If this parameter is null, then the "Q" encoding is used if most of the
506 * characters to be encoded are in the ASCII charset, otherwise "B"
507 * encoding is used.
508 * @return Unicode string containing only US-ASCII characters
509 * @exception UnsupportedEncodingException if the encoding fails
510 */
511 public static String encodeWord(String text, String charset,
512 String encoding)
513 throws UnsupportedEncodingException
514 {
515 return encodeWord(text, charset, encoding, true);
516 }
517
518 private static String encodeWord(String text, String charset,
519 String encoding, boolean word)
520 throws UnsupportedEncodingException
521 {
522 if (asciiStatus(text.getBytes()) == ALL_ASCII)
523 {
524 return text;
525 }
526 String javaCharset;
527 if (charset == null)
528 {
529 javaCharset = getDefaultJavaCharset();
530 charset = mimeCharset(javaCharset);
531 }
532 else
533 {
534 javaCharset = javaCharset(charset);
535 }
536 if (encoding == null)
537 {
538 byte[] bytes = text.getBytes(javaCharset);
539 if (asciiStatus(bytes) != MINORITY_ASCII)
540 {
541 encoding = "Q";
542 }
543 else
544 {
545 encoding = "B";
546 }
547 }
548 boolean bEncoding;
549 if (encoding.equalsIgnoreCase("B"))
550 {
551 bEncoding = true;
552 }
553 else if (encoding.equalsIgnoreCase("Q"))
554 {
555 bEncoding = false;
556 }
557 else
558 {
559 throw new UnsupportedEncodingException("Unknown transfer encoding: " +
560 encoding);
561 }
562
563 StringBuffer encodingBuffer = new StringBuffer();
564 encodingBuffer.append("=?");
565 encodingBuffer.append(charset);
566 encodingBuffer.append("?");
567 encodingBuffer.append(encoding);
568 encodingBuffer.append("?");
569
570 StringBuffer buffer = new StringBuffer();
571 encodeBuffer(buffer,
572 text,
573 javaCharset,
574 bEncoding,
575 68 - charset.length(),
576 encodingBuffer.toString(),
577 true,
578 word);
579 return buffer.toString();
580 }
581
582 private static void encodeBuffer(StringBuffer buffer,
583 String text,
584 String charset,
585 boolean bEncoding,
586 int max,
587 String encoding,
588 boolean keepTogether,
589 boolean word)
590 throws UnsupportedEncodingException
591 {
592 byte[] bytes = text.getBytes(charset);
593 int elen;
594 if (bEncoding)
595 {
596 elen = BOutputStream.encodedLength(bytes);
597 }
598 else
599 {
600 elen = QOutputStream.encodedLength(bytes, word);
601 }
602 int len = text.length();
603 if (elen > max && len > 1)
604 {
605 encodeBuffer(buffer,
606 text.substring(0, len / 2),
607 charset,
608 bEncoding,
609 max,
610 encoding,
611 keepTogether,
612 word);
613 encodeBuffer(buffer,
614 text.substring(len / 2, len),
615 charset,
616 bEncoding,
617 max,
618 encoding,
619 false,
620 word);
621 }
622 else
623 {
624 ByteArrayOutputStream bos = new ByteArrayOutputStream();
625 OutputStream os = null;
626 if (bEncoding)
627 {
628 os = new BOutputStream(bos);
629 }
630 else
631 {
632 os = new QOutputStream(bos, word);
633 }
634 try
635 {
636 os.write(bytes);
637 os.close();
638 }
639 catch (IOException e)
640 {
641 }
642 bytes = bos.toByteArray();
643 if (!keepTogether)
644 {
645 buffer.append("\r\n ");
646 }
647 buffer.append(encoding);
648 for (int i = 0; i < bytes.length; i++)
649 {
650 buffer.append((char) bytes[i]);
651 }
652
653 buffer.append("?=");
654 }
655 }
656
657 /**
658 * The string is parsed using the rules in RFC 2047 for parsing an
659 * "encoded-word".
660 * If the parse fails, a ParseException is thrown. Otherwise, it is
661 * transfer-decoded, and then charset-converted into Unicode. If the
662 * charset-conversion fails, an UnsupportedEncodingException is thrown.
663 * @param eword the possibly encoded value
664 * @exception ParseException if the string is not an encoded-word as per
665 * RFC 2047.
666 * @exception UnsupportedEncodingException if the charset conversion
667 * failed.
668 */
669 public static String decodeWord(String text)
670 throws ParseException, UnsupportedEncodingException
671 {
672 if (!text.startsWith("=?"))
673 {
674 throw new ParseException();
675 }
676 int start = 2;
677 int end = text.indexOf('?', start);
678 if (end < 0)
679 {
680 throw new ParseException();
681 }
682 String charset = text.substring(start, end);
683 // Allow for RFC2231 language
684 int si = charset.indexOf('*');
685 if (si != -1)
686 {
687 charset = charset.substring(0, si);
688 }
689 charset = javaCharset(charset);
690 start = end + 1;
691 end = text.indexOf('?', start);
692 if (end < 0)
693 {
694 throw new ParseException();
695 }
696 String encoding = text.substring(start, end);
697 start = end + 1;
698 end = text.indexOf("?=", start);
699 if (end < 0)
700 {
701 throw new ParseException();
702 }
703 text = text.substring(start, end);
704 try
705 {
706 // The characters in the remaining string must all be 7-bit clean.
707 // Therefore it is safe just to copy them verbatim into a byte array.
708 char[] chars = text.toCharArray();
709 int len = chars.length;
710 byte[] bytes = new byte[len];
711 for (int i = 0; i < len; i++)
712 {
713 bytes[i] = (byte) chars[i];
714 }
715
716 ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
717 InputStream is;
718 if (encoding.equalsIgnoreCase("B"))
719 {
720 is = new Base64InputStream(bis);
721 }
722 else if (encoding.equalsIgnoreCase("Q"))
723 {
724 is = new QInputStream(bis);
725 }
726 else
727 {
728 throw new UnsupportedEncodingException("Unknown encoding: " +
729 encoding);
730 }
731 len = bis.available();
732 bytes = new byte[len];
733 len = is.read(bytes, 0, len);
734 String ret = new String(bytes, 0, len, charset);
735 if (text.length() > end + 2)
736 {
737 String extra = text.substring(end + 2);
738 if (!decodetextStrict())
739 {
740 extra = decodeInnerText(extra);
741 }
742 ret = ret + extra;
743 }
744 return ret;
745 }
746 catch (IOException e)
747 {
748 throw new ParseException();
749 }
750 catch (IllegalArgumentException e)
751 {
752 throw new UnsupportedEncodingException();
753 }
754 }
755
756 /**
757 * Indicates that we should consider a lone CR or LF in a body part
758 * that's not a MIME text type to indicate that the body part
759 * needs to be encoded.
760 * @since JavaMail 1.3
761 */
762 private static boolean encodeeolStrict()
763 {
764 try
765 {
766 String encodeeolStrict =
767 System.getProperty("mail.mime.encodeeol.strict", "false");
768 return Boolean.valueOf(encodeeolStrict).booleanValue();
769 }
770 catch (SecurityException e)
771 {
772 return false;
773 }
774 }
775
776 /**
777 * Indicates if text in the middle of words should be decoded.
778 * @since JavaMail 1.3
779 */
780 private static boolean decodetextStrict()
781 {
782 try
783 {
784 String decodetextStrict =
785 System.getProperty("mail.mime.decodetext.strict", "true");
786 return Boolean.valueOf(decodetextStrict).booleanValue();
787 }
788 catch (SecurityException e)
789 {
790 return true;
791 }
792 }
793
794 /**
795 * Decodes text in the middle of the specified text.
796 * @since JavaMail 1.3
797 */
798 private static String decodeInnerText(String text)
799 throws UnsupportedEncodingException
800 {
801 final String LD = "=?", RD = "?=";
802 int pos = 0;
803 StringBuffer buffer = new StringBuffer();
804 for (int start = text.indexOf(LD, pos); start != -1;
805 start = text.indexOf(LD, pos))
806 {
807 int end = text.indexOf(RD, start + 2);
808 if (end == -1)
809 {
810 break;
811 }
812 buffer.append(text.substring(pos, start));
813 pos = end + 2;
814 String encoded = text.substring(start, pos);
815 try
816 {
817 buffer.append(decodeWord(encoded));
818 }
819 catch (ParseException e)
820 {
821 buffer.append(encoded);
822 }
823 }
824 if (buffer.length() > 0)
825 {
826 if (pos < text.length())
827 {
828 buffer.append(text.substring(pos));
829 }
830 return buffer.toString();
831 }
832 return text;
833 }
834
835 /**
836 * A utility method to quote a word, if the word contains any characters
837 * from the specified 'specials' list.
838 * <p>
839 * The HeaderTokenizer class defines two special sets of delimiters -
840 * MIME and RFC 822.
841 * <p>
842 * This method is typically used during the generation of RFC 822 and MIME
843 * header fields.
844 * @param word word to be quoted
845 * @param specials the set of special characters
846 * @return the possibly quoted word
847 */
848 public static String quote(String text, String specials)
849 {
850 int len = text.length();
851 boolean needsQuotes = false;
852 for (int i = 0; i < len; i++)
853 {
854 char c = text.charAt(i);
855 if (c == '\n' || c == '\r' || c == '"' || c == '\\')
856 {
857 StringBuffer buffer = new StringBuffer(len + 3);
858 buffer.append('"');
859 for (int j = 0; j < len; j++)
860 {
861 char c2 = text.charAt(j);
862 if (c2 == '"' || c2 == '\\' || c2 == '\r' || c2 == '\n')
863 {
864 buffer.append('\\');
865 }
866 buffer.append(c2);
867 }
868
869 buffer.append('"');
870 return buffer.toString();
871 }
872 if (c < ' ' || c > '\177' || specials.indexOf(c) >= 0)
873 {
874 needsQuotes = true;
875 }
876 }
877
878 if (needsQuotes)
879 {
880 StringBuffer buffer = new StringBuffer(len + 2);
881 buffer.append('"');
882 buffer.append(text);
883 buffer.append('"');
884 return buffer.toString();
885 }
886 return text;
887 }
888
889 // -- Java and MIME charset conversions --
890
891 /*
892 * Map of MIME charset names to Java charset names.
893 */
894 private static HashMap mimeCharsets;
895
896 /*
897 * Map of Java charset names to MIME charset names.
898 */
899 private static HashMap javaCharsets;
900
901 /*
902 * Indicates if we are using Java 1.2 - if so, we return "Java" charsets
903 * instead of MIME charsets.
904 */
905 private static boolean java12;
906
907 /*
908 * Load the charset conversion tables.
909 */
910 static
911 {
912 String mappings = "/META-INF/javamail.charset.map";
913 InputStream in = (MimeUtility.class).getResourceAsStream(mappings);
914 if (in != null)
915 {
916 mimeCharsets = new HashMap(10);
917 javaCharsets = new HashMap(20);
918 LineInputStream lin = new LineInputStream(in);
919 parse(mimeCharsets, lin);
920 parse(javaCharsets, lin);
921 }
922 try
923 {
924 String version = System.getProperty("java.version");
925 java12 = (version.startsWith("1.2") ||
926 version.startsWith("1.1"));
927 }
928 catch (SecurityException e)
929 {
930 // TODO
931 }
932 }
933
934 /*
935 * Parse a charset map stream.
936 */
937 private static void parse(HashMap mappings, LineInputStream lin)
938 {
939 try
940 {
941 while (true)
942 {
943 String line = lin.readLine();
944 if (line == null ||
945 (line.startsWith("--") && line.endsWith("--")))
946 {
947 return;
948 }
949
950 if (line.trim().length() != 0 && !line.startsWith("#"))
951 {
952 StringTokenizer st = new StringTokenizer(line, "\t ");
953 try
954 {
955 String key = st.nextToken();
956 String value = st.nextToken();
957 mappings.put(key.toLowerCase(), value);
958 }
959 catch (NoSuchElementException e2)
960 {
961 }
962 }
963 }
964 }
965 catch (IOException e)
966 {
967 e.printStackTrace();
968 }
969 }
970
971 /**
972 * Convert a MIME charset name into a valid Java charset name.
973 * @param charset the MIME charset name
974 * @return the Java charset equivalent.
975 * If a suitable mapping is not available, the passed in charset is
976 * itself returned.
977 */
978 public static String javaCharset(String charset)
979 {
980 if (mimeCharsets == null || charset == null)
981 {
982 return charset;
983 }
984 String jc = (String) mimeCharsets.get(charset.toLowerCase());
985 if (jc != null)
986 {
987 if (java12)
988 {
989 return jc;
990 }
991 else
992 {
993 String mc = (String) javaCharsets.get(jc.toLowerCase());
994 return (mc != null) ? mc : charset;
995 }
996 }
997 return charset;
998 }
999
1000 /**
1001 * Convert a java charset into its MIME charset name.
1002 * <p>
1003 * Note that a future version of JDK(post 1.2) might provide this
1004 * functionality, in which case, we may deprecate this method then.
1005 * @param charset the JDK charset
1006 * @return the MIME/IANA equivalent.
1007 * If a mapping is not possible, the passed in charset itself is returned.
1008 */
1009 public static String mimeCharset(String charset)
1010 {
1011 if (javaCharsets == null || charset == null)
1012 {
1013 return charset;
1014 }
1015 String mc = (String) javaCharsets.get(charset.toLowerCase());
1016 return (mc != null) ? mc : charset;
1017 }
1018
1019 // -- Java default charset --
1020
1021 /*
1022 * Local cache for the system default Java charset.
1023 * @see #getDefaultJavaCharset
1024 */
1025 private static String defaultJavaCharset;
1026
1027 /**
1028 * Get the default charset corresponding to the system's current default
1029 * locale.
1030 * @return the default charset of the system's default locale,
1031 * as a Java charset.
1032 */
1033 public static String getDefaultJavaCharset()
1034 {
1035 if (defaultJavaCharset == null)
1036 {
1037 try
1038 {
1039 // Use mail.mime.charset as of JavaMail 1.3
1040 defaultJavaCharset = System.getProperty("mail.mime.charset");
1041 if (defaultJavaCharset == null)
1042 {
1043 defaultJavaCharset = System.getProperty("file.encoding",
1044 "UTF-8");
1045 }
1046 }
1047 catch (SecurityException e)
1048 {
1049 // InputStreamReader has access to the platform default encoding.
1050 // We create a dummy input stream to feed it with, just to get
1051 // this encoding value.
1052 InputStreamReader isr =
1053 new InputStreamReader(new InputStream() { public int read() { return 0; } });
1054 defaultJavaCharset = isr.getEncoding();
1055
1056 // If all else fails use UTF-8
1057 if (defaultJavaCharset == null)
1058 {
1059 defaultJavaCharset = "UTF-8";
1060 }
1061 }
1062 }
1063 return javaCharset(defaultJavaCharset);
1064 }
1065
1066 // -- Calculating multipart boundaries --
1067
1068 private static int part = 0;
1069
1070 /*
1071 * Returns a suitably unique boundary value.
1072 */
1073 static String getUniqueBoundaryValue()
1074 {
1075 StringBuffer buffer = new StringBuffer();
1076 buffer.append("----=_Part_");
1077 buffer.append(part++);
1078 buffer.append("_");
1079 buffer.append(buffer.hashCode());
1080 buffer.append('.');
1081 buffer.append(System.currentTimeMillis());
1082 return buffer.toString();
1083 }
1084
1085 /*
1086 * Returns a suitably unique Message-ID value.
1087 */
1088 static String getUniqueMessageIDValue(Session session)
1089 {
1090 InternetAddress localAddress = InternetAddress.getLocalAddress(session);
1091 String address = (localAddress != null) ? localAddress.getAddress() :
1092 "javamailuser@localhost";
1093
1094 StringBuffer buffer = new StringBuffer();
1095 buffer.append(buffer.hashCode());
1096 buffer.append('.');
1097 buffer.append(System.currentTimeMillis());
1098 buffer.append('.');
1099 buffer.append("JavaMail.");
1100 buffer.append(address);
1101 return buffer.toString();
1102 }
1103
1104 // These methods provide checks on whether collections of bytes contain
1105 // all-ASCII, majority-ASCII, or minority-ASCII bytes.
1106
1107 // Constants
1108 static final int ALL = -1;
1109 static final int ALL_ASCII = 1;
1110 static final int MAJORITY_ASCII = 2;
1111 static final int MINORITY_ASCII = 3;
1112
1113 static int asciiStatus(byte[] bytes)
1114 {
1115 int asciiCount = 0;
1116 int nonAsciiCount = 0;
1117 for (int i = 0; i < bytes.length; i++)
1118 {
1119 if (isAscii((int) bytes[i]))
1120 {
1121 asciiCount++;
1122 }
1123 else
1124 {
1125 nonAsciiCount++;
1126 }
1127 }
1128
1129 if (nonAsciiCount == 0)
1130 {
1131 return ALL_ASCII;
1132 }
1133 return (asciiCount <= nonAsciiCount) ? MINORITY_ASCII : MAJORITY_ASCII;
1134 }
1135
1136 static int asciiStatus(InputStream is, int len, boolean text)
1137 {
1138 int asciiCount = 0;
1139 int nonAsciiCount = 0;
1140 int blockLen = 4096;
1141 int lineLen = 0;
1142 boolean islong = false;
1143 byte[] bytes = null;
1144 if (len != 0)
1145 {
1146 blockLen = (len != ALL) ? Math.min(len, 4096) : 4096;
1147 bytes = new byte[blockLen];
1148 }
1149 while (len != 0)
1150 {
1151 int readLen;
1152 try
1153 {
1154 readLen = is.read(bytes, 0, blockLen);
1155 if (readLen < 0)
1156 {
1157 break;
1158 }
1159 for (int i = 0; i < readLen; i++)
1160 {
1161 int c = bytes[i] & 0xff;
1162 if (c == 13 || c == 10)
1163 {
1164 lineLen = 0;
1165 }
1166 else
1167 {
1168 lineLen++;
1169 if (lineLen > 998)
1170 {
1171 islong = true;
1172 }
1173 }
1174 if (isAscii(c))
1175 {
1176 asciiCount++;
1177 }
1178 else
1179 {
1180 if (text)
1181 {
1182 return MINORITY_ASCII;
1183 }
1184 nonAsciiCount++;
1185 }
1186 }
1187
1188 }
1189 catch (IOException e)
1190 {
1191 break;
1192 }
1193 if (len != -1)
1194 {
1195 len -= readLen;
1196 }
1197 }
1198 if (len == 0 && text)
1199 {
1200 return MINORITY_ASCII;
1201 }
1202 if (nonAsciiCount == 0)
1203 {
1204 return !islong ? ALL_ASCII : MAJORITY_ASCII;
1205 }
1206 return (asciiCount <= nonAsciiCount) ? MINORITY_ASCII : MAJORITY_ASCII;
1207 }
1208
1209 private static final boolean isAscii(int c)
1210 {
1211 if (c < 0)
1212 {
1213 c += 0xff;
1214 }
1215 return (c < 128 && c > 31) || c == 13 || c == 10 || c == 9;
1216 }
1217
1218 /*
1219 * This is used by the getEncoding(DataHandler) method to ascertain which
1220 * encoding scheme to use. It embodies the same algorithm as the
1221 * asciiStatus methods above.
1222 */
1223 static class AsciiOutputStream extends OutputStream
1224 {
1225
1226 static final int LF = 0x0a;
1227 static final int CR = 0x0d;
1228
1229 private boolean strict;
1230 private boolean eolStrict;
1231 private int asciiCount = 0;
1232 private int nonAsciiCount = 0;
1233 private int ret;
1234 private int len;
1235 private int last = -1;
1236 private boolean islong = false;
1237 private boolean eolCheckFailed = false;
1238
1239 public AsciiOutputStream(boolean strict, boolean eolStrict)
1240 {
1241 this.strict = strict;
1242 this.eolStrict = eolStrict;
1243 }
1244
1245 public void write(int c)
1246 throws IOException
1247 {
1248 check(c);
1249 }
1250
1251 public void write(byte[] bytes)
1252 throws IOException
1253 {
1254 write(bytes, 0, bytes.length);
1255 }
1256
1257 public void write(byte[] bytes, int offset, int length)
1258 throws IOException
1259 {
1260 length += offset;
1261 for (int i = offset; i < length; i++)
1262 {
1263 check(bytes[i]);
1264 }
1265
1266 }
1267
1268 private final void check(int c)
1269 throws IOException
1270 {
1271 c &= 0xff;
1272 if (eolStrict)
1273 {
1274 if (last == CR && c != LF || last != CR && c == LF)
1275 {
1276 eolCheckFailed = true;
1277 }
1278 }
1279 if (c == CR || c == LF)
1280 {
1281 len = 0;
1282 }
1283 else
1284 {
1285 len++;
1286 if (len > 998)
1287 {
1288 islong = true;
1289 }
1290 }
1291 if (c > 127)
1292 {
1293 nonAsciiCount++;
1294 if (strict)
1295 {
1296 ret = MINORITY_ASCII;
1297 throw new EOFException();
1298 }
1299 }
1300 else
1301 {
1302 asciiCount++;
1303 }
1304 last = c;
1305 }
1306
1307 int status()
1308 {
1309 if (ret != 0)
1310 {
1311 return ret;
1312 }
1313 if (eolCheckFailed)
1314 {
1315 return MINORITY_ASCII;
1316 }
1317 if (nonAsciiCount == 0)
1318 {
1319 return !islong ? ALL_ASCII : MAJORITY_ASCII;
1320 }
1321 return (asciiCount <= nonAsciiCount) ? MAJORITY_ASCII : MINORITY_ASCII;
1322 }
1323
1324 }
1325
1326 }

savannah-hackers-public@gnu.org
ViewVC Help
Powered by ViewVC 1.1.26