1 /**
2 *
3 * Copyright 2003-2006 The Apache Software Foundation
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package javax.mail.internet;
19
20 import java.io.BufferedInputStream;
21 import java.io.BufferedReader;
22 import java.io.ByteArrayInputStream;
23 import java.io.ByteArrayOutputStream;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.InputStreamReader;
27 import java.io.OutputStream;
28 import java.io.UnsupportedEncodingException;
29 import java.util.HashMap;
30 import java.util.Map;
31 import java.util.NoSuchElementException;
32 import java.util.StringTokenizer;
33
34 import javax.activation.DataHandler;
35 import javax.activation.DataSource;
36 import javax.mail.MessagingException;
37
38 import org.apache.geronimo.mail.util.ASCIIUtil;
39 import org.apache.geronimo.mail.util.Base64;
40 import org.apache.geronimo.mail.util.Base64DecoderStream;
41 import org.apache.geronimo.mail.util.Base64Encoder;
42 import org.apache.geronimo.mail.util.Base64EncoderStream;
43 import org.apache.geronimo.mail.util.QuotedPrintableDecoderStream;
44 import org.apache.geronimo.mail.util.QuotedPrintableEncoderStream;
45 import org.apache.geronimo.mail.util.QuotedPrintableEncoder;
46 import org.apache.geronimo.mail.util.QuotedPrintable;
47 import org.apache.geronimo.mail.util.SessionUtil;
48 import org.apache.geronimo.mail.util.UUDecoderStream;
49 import org.apache.geronimo.mail.util.UUEncoderStream;
50
51
52
53
54 /**
55 * @version $Rev: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 2006) $
56 */
57 public class MimeUtility {
58
59 private static final String MIME_FOLDENCODEDWORDS = "mail.mime.foldencodedwords";
60 private static final String MIME_DECODE_TEXT_STRICT = "mail.mime.decodetext.strict";
61 private static final String MIME_FOLDTEXT = "mail.mime.foldtext";
62 private static final int FOLD_THRESHOLD = 76;
63
64 private MimeUtility() {
65 }
66
67 public static final int ALL = -1;
68
69 private static String defaultJavaCharset;
70 private static String escapedChars = "\"\\\r\n";
71 private static String linearWhiteSpace = " \t\r\n";
72
73 private static String QP_WORD_SPECIALS = "=_?\"#$%&'(),.:;<>@[\\]^`{|}~";
74 private static String QP_TEXT_SPECIALS = "=_?";
75
76
77
78 private static Map java2mime;
79 private static Map mime2java;
80
81 static {
82
83 loadCharacterSetMappings();
84 }
85
86 public static InputStream decode(InputStream in, String encoding) throws MessagingException {
87 encoding = encoding.toLowerCase();
88
89
90 if (encoding.equals("binary") || encoding.equals("7bit") || encoding.equals("8bit")) {
91 return in;
92 }
93 else if (encoding.equals("base64")) {
94 return new Base64DecoderStream(in);
95 }
96
97 else if (encoding.equals("uuencode") || encoding.equals("x-uuencode") || encoding.equals("x-uue")) {
98 return new UUDecoderStream(in);
99 }
100 else if (encoding.equals("quoted-printable")) {
101 return new QuotedPrintableDecoderStream(in);
102 }
103 else {
104 throw new MessagingException("Unknown encoding " + encoding);
105 }
106 }
107
108 /**
109 * Decode a string of text obtained from a mail header into
110 * it's proper form. The text generally will consist of a
111 * string of tokens, some of which may be encoded using
112 * base64 encoding.
113 *
114 * @param text The text to decode.
115 *
116 * @return The decoded test string.
117 * @exception UnsupportedEncodingException
118 */
119 public static String decodeText(String text) throws UnsupportedEncodingException {
120
121
122 if (text.indexOf("=?") < 0) {
123 return text;
124 }
125
126
127 if (!SessionUtil.getBooleanProperty(MIME_DECODE_TEXT_STRICT, true)) {
128 return decodeTextNonStrict(text);
129 }
130
131 int offset = 0;
132 int endOffset = text.length();
133
134 int startWhiteSpace = -1;
135 int endWhiteSpace = -1;
136
137 StringBuffer decodedText = new StringBuffer(text.length());
138
139 boolean previousTokenEncoded = false;
140
141 while (offset < endOffset) {
142 char ch = text.charAt(offset);
143
144
145 if (linearWhiteSpace.indexOf(ch) != -1) {
146 startWhiteSpace = offset;
147 while (offset < endOffset) {
148
149 ch = text.charAt(offset);
150 if (linearWhiteSpace.indexOf(ch) != -1) {
151 offset++;
152 }
153 else {
154
155
156 endWhiteSpace = offset;
157 break;
158 }
159 }
160 }
161 else {
162
163 int wordStart = offset;
164
165 while (offset < endOffset) {
166
167 ch = text.charAt(offset);
168 if (linearWhiteSpace.indexOf(ch) == -1) {
169 offset++;
170 }
171 else {
172 break;
173 }
174
175
176 }
177
178 String word = text.substring(wordStart, offset);
179
180 if (word.startsWith("=?")) {
181 try {
182
183 String decodedWord = decodeWord(word);
184
185
186 if (!previousTokenEncoded) {
187 if (startWhiteSpace != -1) {
188 decodedText.append(text.substring(startWhiteSpace, endWhiteSpace));
189 startWhiteSpace = -1;
190 }
191 }
192
193 previousTokenEncoded = true;
194
195 decodedText.append(decodedWord);
196
197
198 continue;
199
200 } catch (ParseException e) {
201 }
202 }
203
204
205 if (startWhiteSpace != -1) {
206 decodedText.append(text.substring(startWhiteSpace, endWhiteSpace));
207 startWhiteSpace = -1;
208 }
209
210 previousTokenEncoded = false;
211 decodedText.append(word);
212 }
213 }
214
215 return decodedText.toString();
216 }
217
218
219 /**
220 * Decode a string of text obtained from a mail header into
221 * it's proper form. The text generally will consist of a
222 * string of tokens, some of which may be encoded using
223 * base64 encoding. This is for non-strict decoded for mailers that
224 * violate the RFC 2047 restriction that decoded tokens must be delimited
225 * by linear white space. This will scan tokens looking for inner tokens
226 * enclosed in "=?" -- "?=" pairs.
227 *
228 * @param text The text to decode.
229 *
230 * @return The decoded test string.
231 * @exception UnsupportedEncodingException
232 */
233 private static String decodeTextNonStrict(String text) throws UnsupportedEncodingException {
234 int offset = 0;
235 int endOffset = text.length();
236
237 int startWhiteSpace = -1;
238 int endWhiteSpace = -1;
239
240 StringBuffer decodedText = new StringBuffer(text.length());
241
242 boolean previousTokenEncoded = false;
243
244 while (offset < endOffset) {
245 char ch = text.charAt(offset);
246
247
248 if (linearWhiteSpace.indexOf(ch) != -1) {
249 startWhiteSpace = offset;
250 while (offset < endOffset) {
251
252 ch = text.charAt(offset);
253 if (linearWhiteSpace.indexOf(ch) != -1) {
254 offset++;
255 }
256 else {
257
258
259 endWhiteSpace = offset;
260 break;
261 }
262 }
263 }
264 else {
265
266 int wordStart = offset;
267
268 while (offset < endOffset) {
269
270 ch = text.charAt(offset);
271 if (linearWhiteSpace.indexOf(ch) == -1) {
272 offset++;
273 }
274 else {
275 break;
276 }
277
278
279 }
280
281 String word = text.substring(wordStart, offset);
282
283 int decodeStart = 0;
284
285
286 while (decodeStart < word.length()) {
287 int tokenStart = word.indexOf("=?", decodeStart);
288 if (tokenStart == -1) {
289
290
291 if (startWhiteSpace != -1) {
292 decodedText.append(text.substring(startWhiteSpace, endWhiteSpace));
293 startWhiteSpace = -1;
294 }
295
296 previousTokenEncoded = false;
297 decodedText.append(word.substring(decodeStart));
298
299 break;
300 }
301
302 else {
303
304 if (tokenStart != decodeStart) {
305
306
307 if (startWhiteSpace != -1) {
308 decodedText.append(text.substring(startWhiteSpace, endWhiteSpace));
309 startWhiteSpace = -1;
310 }
311
312 previousTokenEncoded = false;
313 decodedText.append(word.substring(decodeStart, tokenStart));
314 }
315
316
317 int tokenEnd = word.indexOf("?=", tokenStart);
318
319 if (tokenEnd == -1) {
320
321
322 if (startWhiteSpace != -1) {
323 decodedText.append(text.substring(startWhiteSpace, endWhiteSpace));
324 startWhiteSpace = -1;
325 }
326
327 previousTokenEncoded = false;
328 decodedText.append(word.substring(tokenStart));
329
330 break;
331 }
332 else {
333
334 decodeStart = tokenEnd + 2;
335
336 String token = word.substring(tokenStart, tokenEnd);
337 try {
338
339 String decodedWord = decodeWord(token);
340
341
342 if (!previousTokenEncoded) {
343 if (startWhiteSpace != -1) {
344 decodedText.append(text.substring(startWhiteSpace, endWhiteSpace));
345 startWhiteSpace = -1;
346 }
347 }
348
349 previousTokenEncoded = true;
350
351 decodedText.append(decodedWord);
352
353
354 continue;
355
356 } catch (ParseException e) {
357 }
358
359
360 if (startWhiteSpace != -1) {
361 decodedText.append(text.substring(startWhiteSpace, endWhiteSpace));
362 startWhiteSpace = -1;
363 }
364
365 previousTokenEncoded = false;
366 decodedText.append(token);
367 }
368 }
369 }
370 }
371 }
372
373 return decodedText.toString();
374 }
375
376 /**
377 * Parse a string using the RFC 2047 rules for an "encoded-word"
378 * type. This encoding has the syntax:
379 *
380 * encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
381 *
382 * @param word The possibly encoded word value.
383 *
384 * @return The decoded word.
385 * @exception ParseException
386 * @exception UnsupportedEncodingException
387 */
388 public static String decodeWord(String word) throws ParseException, UnsupportedEncodingException {
389
390
391
392 if (!word.startsWith("=?")) {
393 throw new ParseException("Invalid RFC 2047 encoded-word: " + word);
394 }
395
396 int charsetPos = word.indexOf('?', 2);
397 if (charsetPos == -1) {
398 throw new ParseException("Missing charset in RFC 2047 encoded-word: " + word);
399 }
400
401
402 String charset = word.substring(2, charsetPos).toLowerCase();
403
404
405 int encodingPos = word.indexOf('?', charsetPos + 1);
406 if (encodingPos == -1) {
407 throw new ParseException("Missing encoding in RFC 2047 encoded-word: " + word);
408 }
409
410 String encoding = word.substring(charsetPos + 1, encodingPos);
411
412
413 int encodedTextPos = word.indexOf("?=", encodingPos + 1);
414 if (encodedTextPos == -1) {
415 throw new ParseException("Missing encoded text in RFC 2047 encoded-word: " + word);
416 }
417
418 String encodedText = word.substring(encodingPos + 1, encodedTextPos);
419
420
421 if (encodedText.length() == 0) {
422 return "";
423 }
424
425 try {
426
427 ByteArrayOutputStream out = new ByteArrayOutputStream(encodedText.length());
428
429 byte[] encodedData = encodedText.getBytes("US-ASCII");
430
431
432 if (encoding.equals("B")) {
433 Base64.decode(encodedData, out);
434 }
435
436 else if (encoding.equals("Q")) {
437 QuotedPrintableEncoder dataEncoder = new QuotedPrintableEncoder();
438 dataEncoder.decodeWord(encodedData, out);
439 }
440 else {
441 throw new UnsupportedEncodingException("Unknown RFC 2047 encoding: " + encoding);
442 }
443
444 byte[] decodedData = out.toByteArray();
445 return new String(decodedData, javaCharset(charset));
446 } catch (IOException e) {
447 throw new UnsupportedEncodingException("Invalid RFC 2047 encoding");
448 }
449
450 }
451
452 /**
453 * Wrap an encoder around a given output stream.
454 *
455 * @param out The output stream to wrap.
456 * @param encoding The name of the encoding.
457 *
458 * @return A instance of FilterOutputStream that manages on the fly
459 * encoding for the requested encoding type.
460 * @exception MessagingException
461 */
462 public static OutputStream encode(OutputStream out, String encoding) throws MessagingException {
463
464 if (encoding == null) {
465 return out;
466 }
467
468 encoding = encoding.toLowerCase();
469
470
471 if (encoding.equals("binary") || encoding.equals("7bit") || encoding.equals("8bit")) {
472 return out;
473 }
474 else if (encoding.equals("base64")) {
475 return new Base64EncoderStream(out);
476 }
477
478 else if (encoding.equals("uuencode") || encoding.equals("x-uuencode") || encoding.equals("x-uue")) {
479 return new UUEncoderStream(out);
480 }
481 else if (encoding.equals("quoted-printable")) {
482 return new QuotedPrintableEncoderStream(out);
483 }
484 else {
485 throw new MessagingException("Unknown encoding " + encoding);
486 }
487 }
488
489 /**
490 * Wrap an encoder around a given output stream.
491 *
492 * @param out The output stream to wrap.
493 * @param encoding The name of the encoding.
494 * @param filename The filename of the data being sent (only used for UUEncode).
495 *
496 * @return A instance of FilterOutputStream that manages on the fly
497 * encoding for the requested encoding type.
498 * @exception MessagingException
499 */
500 public static OutputStream encode(OutputStream out, String encoding, String filename) throws MessagingException {
501 encoding = encoding.toLowerCase();
502
503
504 if (encoding.equals("binary") || encoding.equals("7bit") || encoding.equals("8bit")) {
505 return out;
506 }
507 else if (encoding.equals("base64")) {
508 return new Base64EncoderStream(out);
509 }
510
511 else if (encoding.equals("uuencode") || encoding.equals("x-uuencode") || encoding.equals("x-uue")) {
512 return new UUEncoderStream(out, filename);
513 }
514 else if (encoding.equals("quoted-printable")) {
515 return new QuotedPrintableEncoderStream(out);
516 }
517 else {
518 throw new MessagingException("Unknown encoding " + encoding);
519 }
520 }
521
522
523 public static String encodeText(String word) throws UnsupportedEncodingException {
524 return encodeText(word, null, null);
525 }
526
527 public static String encodeText(String word, String charset, String encoding) throws UnsupportedEncodingException {
528 return encodeWord(word, charset, encoding, false);
529 }
530
531 public static String encodeWord(String word) throws UnsupportedEncodingException {
532 return encodeWord(word, null, null);
533 }
534
535 public static String encodeWord(String word, String charset, String encoding) throws UnsupportedEncodingException {
536 return encodeWord(word, charset, encoding, true);
537 }
538
539
540 private static String encodeWord(String word, String charset, String encoding, boolean encodingWord) throws UnsupportedEncodingException {
541
542
543 String encoder = ASCIIUtil.getTextTransferEncoding(word);
544
545 if (encoder.equals("7bit")) {
546 return word;
547 }
548
549
550 if (charset == null) {
551 charset = getDefaultMIMECharset();
552 }
553
554
555 if (encoding != null) {
556 if (encoding.equalsIgnoreCase("B")) {
557 encoder = "base64";
558 }
559 else if (encoding.equalsIgnoreCase("Q")) {
560 encoder = "quoted-printable";
561 }
562 else {
563 throw new UnsupportedEncodingException("Unknown transfer encoding: " + encoding);
564 }
565 }
566
567 try {
568
569 InputStream in = new ByteArrayInputStream(word.getBytes( javaCharset(charset)));
570 ByteArrayOutputStream out = new ByteArrayOutputStream();
571
572 if (encoder.equals("base64")) {
573 Base64Encoder dataEncoder = new Base64Encoder();
574 dataEncoder.encodeWord(in, charset, out, SessionUtil.getBooleanProperty(MIME_FOLDENCODEDWORDS, false));
575 }
576 else {
577 QuotedPrintableEncoder dataEncoder = new QuotedPrintableEncoder();
578 dataEncoder.encodeWord(in, charset, encodingWord ? QP_WORD_SPECIALS : QP_TEXT_SPECIALS, out, SessionUtil.getBooleanProperty(MIME_FOLDENCODEDWORDS, false));
579 }
580
581 byte[] bytes = out.toByteArray();
582 return new String(bytes);
583 } catch (IOException e) {
584 throw new UnsupportedEncodingException("Invalid encoding");
585 }
586 }
587
588
589 /**
590 * Examine the content of a data source and decide what type
591 * of transfer encoding should be used. For text streams,
592 * we'll decided between 7bit, quoted-printable, and base64.
593 * For binary content types, we'll use either 7bit or base64.
594 *
595 * @param handler The DataHandler associated with the content.
596 *
597 * @return The string name of an encoding used to transfer the content.
598 */
599 public static String getEncoding(DataHandler handler) {
600
601
602
603
604
605 DataSource ds = handler.getDataSource();
606 if (ds != null) {
607 return getEncoding(ds);
608 }
609
610 try {
611
612 ContentType content = new ContentType(ds.getContentType());
613
614
615
616
617 ContentCheckingOutputStream checker = new ContentCheckingOutputStream();
618
619 handler.writeTo(checker);
620
621
622 if (content.match("text/*")) {
623 return checker.getTextTransferEncoding();
624 }
625 else {
626 return checker.getBinaryTransferEncoding();
627 }
628
629 } catch (Exception e) {
630
631 return "base64";
632 }
633 }
634
635
636 /**
637 * Determine the what transfer encoding should be used for
638 * data retrieved from a DataSource.
639 *
640 * @param source The DataSource for the transmitted data.
641 *
642 * @return The string name of the encoding form that should be used for
643 * the data.
644 */
645 public static String getEncoding(DataSource source) {
646 InputStream in = null;
647
648 try {
649
650 ContentType content = new ContentType(source.getContentType());
651
652
653 in = source.getInputStream();
654
655 if (!content.match("text/*")) {
656
657
658 return ASCIIUtil.getBinaryTransferEncoding(in);
659 }
660 else {
661 return ASCIIUtil.getTextTransferEncoding(in);
662 }
663 } catch (Exception e) {
664
665
666 return "base64";
667 } finally {
668
669 try {
670 if (in != null) {
671 in.close();
672 }
673 } catch (IOException e) {
674 }
675 }
676 }
677
678
679 /**
680 * Quote a "word" value. If the word contains any character from
681 * the specified "specials" list, this value is returned as a
682 * quoted strong. Otherwise, it is returned unchanged (an "atom").
683 *
684 * @param word The word requiring quoting.
685 * @param specials The set of special characters that can't appear in an unquoted
686 * string.
687 *
688 * @return The quoted value. This will be unchanged if the word doesn't contain
689 * any of the designated special characters.
690 */
691 public static String quote(String word, String specials) {
692 int wordLength = word.length();
693 boolean requiresQuoting = false;
694
695 for (int i =0; i < wordLength; i++) {
696 char ch = word.charAt(i);
697
698 if (escapedChars.indexOf(ch) >= 0) {
699 return quoteAndEscapeString(word);
700 }
701
702 if (ch < 32 || ch >= 127 || specials.indexOf(ch) >= 0) {
703
704
705 return quoteAndEscapeString(word);
706 }
707 }
708 return word;
709 }
710
711 /**
712 * Take a string and return it as a formatted quoted string, with
713 * all characters requiring escaping handled properly.
714 *
715 * @param word The string to quote.
716 *
717 * @return The quoted string.
718 */
719 private static String quoteAndEscapeString(String word) {
720 int wordLength = word.length();
721
722 StringBuffer buffer = new StringBuffer(wordLength + 10);
723
724 buffer.append('"');
725
726 for (int i = 0; i < wordLength; i++) {
727 char ch = word.charAt(i);
728
729 if (escapedChars.indexOf(ch) >= 0) {
730
731 buffer.append('\\');
732 }
733 buffer.append(ch);
734 }
735
736 buffer.append('"');
737 return buffer.toString();
738 }
739
740 /**
741 * Translate a MIME standard character set name into the Java
742 * equivalent.
743 *
744 * @param charset The MIME standard name.
745 *
746 * @return The Java equivalent for this name.
747 */
748 public static String javaCharset(String charset) {
749
750 if (charset == null) {
751 return null;
752 }
753
754 String mappedCharset = (String)mime2java.get(charset.toLowerCase());
755
756
757 return mappedCharset == null ? charset : mappedCharset;
758 }
759
760 /**
761 * Map a Java character set name into the MIME equivalent.
762 *
763 * @param charset The java character set name.
764 *
765 * @return The MIME standard equivalent for this character set name.
766 */
767 public static String mimeCharset(String charset) {
768
769 if (charset == null) {
770 return null;
771 }
772
773 String mappedCharset = (String)java2mime.get(charset.toLowerCase());
774
775
776 return mappedCharset == null ? charset : mappedCharset;
777 }
778
779
780 /**
781 * Get the default character set to use, in Java name format.
782 * This either be the value set with the mail.mime.charset
783 * system property or obtained from the file.encoding system
784 * property. If neither of these is set, we fall back to
785 * 8859_1 (basically US-ASCII).
786 *
787 * @return The character string value of the default character set.
788 */
789 public static String getDefaultJavaCharset() {
790 String charset = SessionUtil.getProperty("mail.mime.charset");
791 if (charset != null) {
792 return javaCharset(charset);
793 }
794 return SessionUtil.getProperty("file.encoding", "8859_1");
795 }
796
797 /**
798 * Get the default character set to use, in MIME name format.
799 * This either be the value set with the mail.mime.charset
800 * system property or obtained from the file.encoding system
801 * property. If neither of these is set, we fall back to
802 * 8859_1 (basically US-ASCII).
803 *
804 * @return The character string value of the default character set.
805 */
806 static String getDefaultMIMECharset() {
807
808 String charset = SessionUtil.getProperty("mail.mime.charset");
809 if (charset != null) {
810 return charset;
811 }
812
813
814 return mimeCharset(SessionUtil.getProperty("file.encoding", "8859_1"));
815 }
816
817
818 /**
819 * Load the default mapping tables used by the javaCharset()
820 * and mimeCharset() methods. By default, these tables are
821 * loaded from the /META-INF/javamail.charset.map file. If
822 * something goes wrong loading that file, we configure things
823 * with a default mapping table (which just happens to mimic
824 * what's in the default mapping file).
825 */
826 static private void loadCharacterSetMappings() {
827 java2mime = new HashMap();
828 mime2java = new HashMap();
829
830
831
832 try {
833 InputStream map = javax.mail.internet.MimeUtility.class.getResourceAsStream("/META-INF/javamail.charset.map");
834
835 if (map != null) {
836
837 BufferedReader reader = new BufferedReader(new InputStreamReader(map));
838
839 readMappings(reader, java2mime);
840 readMappings(reader, mime2java);
841 }
842 } catch (Exception e) {
843 }
844
845
846
847
848
849
850 if (java2mime.isEmpty()) {
851 java2mime.put("8859_1", "ISO-8859-1");
852 java2mime.put("iso8859_1", "ISO-8859-1");
853 java2mime.put("iso8859-1", "ISO-8859-1");
854
855 java2mime.put("8859_2", "ISO-8859-2");
856 java2mime.put("iso8859_2", "ISO-8859-2");
857 java2mime.put("iso8859-2", "ISO-8859-2");
858
859 java2mime.put("8859_3", "ISO-8859-3");
860 java2mime.put("iso8859_3", "ISO-8859-3");
861 java2mime.put("iso8859-3", "ISO-8859-3");
862
863 java2mime.put("8859_4", "ISO-8859-4");
864 java2mime.put("iso8859_4", "ISO-8859-4");
865 java2mime.put("iso8859-4", "ISO-8859-4");
866
867 java2mime.put("8859_5", "ISO-8859-5");
868 java2mime.put("iso8859_5", "ISO-8859-5");
869 java2mime.put("iso8859-5", "ISO-8859-5");
870
871 java2mime.put ("8859_6", "ISO-8859-6");
872 java2mime.put("iso8859_6", "ISO-8859-6");
873 java2mime.put("iso8859-6", "ISO-8859-6");
874
875 java2mime.put("8859_7", "ISO-8859-7");
876 java2mime.put("iso8859_7", "ISO-8859-7");
877 java2mime.put("iso8859-7", "ISO-8859-7");
878
879 java2mime.put("8859_8", "ISO-8859-8");
880 java2mime.put("iso8859_8", "ISO-8859-8");
881 java2mime.put("iso8859-8", "ISO-8859-8");
882
883 java2mime.put("8859_9", "ISO-8859-9");
884 java2mime.put("iso8859_9", "ISO-8859-9");
885 java2mime.put("iso8859-9", "ISO-8859-9");
886
887 java2mime.put("sjis", "Shift_JIS");
888 java2mime.put ("jis", "ISO-2022-JP");
889 java2mime.put("iso2022jp", "ISO-2022-JP");
890 java2mime.put("euc_jp", "euc-jp");
891 java2mime.put("koi8_r", "koi8-r");
892 java2mime.put("euc_cn", "euc-cn");
893 java2mime.put("euc_tw", "euc-tw");
894 java2mime.put("euc_kr", "euc-kr");
895 }
896
897 if (mime2java.isEmpty ()) {
898 mime2java.put("iso-2022-cn", "ISO2022CN");
899 mime2java.put("iso-2022-kr", "ISO2022KR");
900 mime2java.put("utf-8", "UTF8");
901 mime2java.put("utf8", "UTF8");
902 mime2java.put("ja_jp.iso2022-7", "ISO2022JP");
903 mime2java.put("ja_jp.eucjp", "EUCJIS");
904 mime2java.put ("euc-kr", "KSC5601");
905 mime2java.put("euckr", "KSC5601");
906 mime2java.put("us-ascii", "ISO-8859-1");
907 mime2java.put("x-us-ascii", "ISO-8859-1");
908 }
909 }
910
911
912 /**
913 * Read a section of a character map table and populate the
914 * target mapping table with the information. The table end
915 * is marked by a line starting with "--" and also ending with
916 * "--". Blank lines and comment lines (beginning with '#') are
917 * ignored.
918 *
919 * @param reader The source of the file information.
920 * @param table The mapping table used to store the information.
921 */
922 static private void readMappings(BufferedReader reader, Map table) throws IOException {
923
924 while (true) {
925 String line = reader.readLine();
926
927 if (line == null) {
928 return;
929 }
930
931
932 line = line.trim();
933
934 if (line.length() == 0 || line.startsWith("#")) {
935 continue;
936 }
937
938
939 if (line.startsWith("--") && line.endsWith("--")) {
940 return;
941 }
942
943
944 StringTokenizer tokenizer = new StringTokenizer(line, " \t");
945
946 try {
947 String from = tokenizer.nextToken().toLowerCase();
948 String to = tokenizer.nextToken();
949
950 table.put(from, to);
951 } catch (NoSuchElementException e) {
952
953 }
954 }
955 }
956
957
958 /**
959 * Perform RFC 2047 text folding on a string of text.
960 *
961 * @param used The amount of text already "used up" on this line. This is
962 * typically the length of a message header that this text
963 * get getting added to.
964 * @param s The text to fold.
965 *
966 * @return The input text, with linebreaks inserted at appropriate fold points.
967 */
968 public static String fold(int used, String s) {
969
970 if (!SessionUtil.getBooleanProperty(MIME_FOLDTEXT, true)) {
971 return s;
972 }
973
974 int end;
975
976
977
978 for (end = s.length() - 1; end >= 0; end--) {
979 int ch = s.charAt(end);
980 if (ch != ' ' && ch != '\t' ) {
981 break;
982 }
983 }
984
985
986 if (end != s.length() - 1) {
987 s = s.substring(0, end + 1);
988 }
989
990
991 if (s.length() + used <= FOLD_THRESHOLD) {
992 return s;
993 }
994
995
996
997
998 StringBuffer newString = new StringBuffer(s.length() + 8);
999
1000
1001
1002 while (used + s.length() > FOLD_THRESHOLD) {
1003 int breakPoint = -1;
1004 char breakChar = 0;
1005
1006
1007 for (int i = 0; i < s.length(); i++) {
1008
1009 if (used + i > FOLD_THRESHOLD) {
1010
1011
1012 if (breakPoint != -1) {
1013 break;
1014 }
1015 }
1016 char ch = s.charAt(i);
1017
1018
1019 if (ch == ' ' || ch == '\t') {
1020
1021 breakPoint = i;
1022
1023 breakChar = ch;
1024 i++;
1025 while (i < s.length()) {
1026 ch = s.charAt(i);
1027 if (ch != ' ' && ch != '\t') {
1028 break;
1029 }
1030 i++;
1031 }
1032 }
1033
1034 else if (ch == '\n') {
1035 newString.append('\\');
1036 newString.append('\n');
1037 }
1038 else if (ch == '\r') {
1039 newString.append('\\');
1040 newString.append('\n');
1041 i++;
1042
1043 if (i < s.length() && s.charAt(i) == '\n') {
1044 newString.append('\r');
1045 }
1046 }
1047
1048 }
1049
1050 if (breakPoint == -1) {
1051 newString.append(s);
1052 return newString.toString();
1053 }
1054 newString.append(s.substring(0, breakPoint));
1055 newString.append("\r\n");
1056 newString.append(breakChar);
1057
1058 s = s.substring(breakPoint + 1);
1059
1060 used = 1;
1061 }
1062
1063
1064 newString.append(s);
1065 return newString.toString();
1066 }
1067
1068 /**
1069 * Unfold a folded string. The unfolding process will remove
1070 * any line breaks that are not escaped and which are also followed
1071 * by whitespace characters.
1072 *
1073 * @param s The folded string.
1074 *
1075 * @return A new string with unfolding rules applied.
1076 */
1077 public static String unfold(String s) {
1078
1079 if (!SessionUtil.getBooleanProperty(MIME_FOLDTEXT, true)) {
1080 return s;
1081 }
1082
1083
1084 if (s.indexOf('\n') < 0 && s.indexOf('\r') < 0) {
1085 return s;
1086 }
1087
1088
1089 int length = s.length();
1090
1091 StringBuffer newString = new StringBuffer(length);
1092
1093
1094 for (int i = 0; i < length; i++) {
1095 char ch = s.charAt(i);
1096
1097
1098
1099 if (ch == '\\') {
1100
1101 if (i == length - 1) {
1102 newString.append(ch);
1103 }
1104 else {
1105 int nextChar = s.charAt(i + 1);
1106
1107
1108 if (nextChar == '\n') {
1109 newString.append('\n');
1110 i++;
1111 }
1112 else if (nextChar == '\r') {
1113
1114 if (i == length - 2 || s.charAt(i + 2) != '\r') {
1115 newString.append('\r');
1116 i++;
1117 }
1118 else {
1119
1120 newString.append('\r');
1121 newString.append('\n');
1122 i += 2;
1123 }
1124 }
1125 else {
1126
1127 newString.append(ch);
1128 }
1129 }
1130 }
1131
1132 else if (ch == '\n' || ch == '\r') {
1133
1134 int lineBreak = i;
1135 boolean CRLF = false;
1136
1137 if (ch == '\r') {
1138
1139 if (i < length - 1 && s.charAt(i + 1) == '\n') {
1140 i++;
1141
1142 CRLF = true;
1143 }
1144 }
1145
1146
1147 int scan = i + 1;
1148
1149
1150
1151 if (scan < length && s.charAt(scan) == ' ') {
1152
1153 newString.append(' ');
1154
1155
1156 i = scan + 1;
1157 while (i < length && s.charAt(i) == ' ') {
1158 i++;
1159 }
1160
1161 i--;
1162 }
1163 else {
1164
1165 if (CRLF) {
1166 newString.append("\r\n");
1167 }
1168 else {
1169 newString.append(ch);
1170 }
1171 }
1172 }
1173 else {
1174
1175 newString.append(ch);
1176 }
1177 }
1178 return newString.toString();
1179 }
1180 }
1181
1182
1183 /**
1184 * Utility class for examining content information written out
1185 * by a DataHandler object. This stream gathers statistics on
1186 * the stream so it can make transfer encoding determinations.
1187 */
1188 class ContentCheckingOutputStream extends OutputStream {
1189 private int asciiChars = 0;
1190 private int nonAsciiChars = 0;
1191 private boolean containsLongLines = false;
1192 private boolean containsMalformedEOL = false;
1193 private int previousChar = 0;
1194 private int span = 0;
1195
1196 ContentCheckingOutputStream() {
1197 }
1198
1199 public void write(byte[] data) throws IOException {
1200 write(data, 0, data.length);
1201 }
1202
1203 public void write(byte[] data, int offset, int length) throws IOException {
1204 for (int i = 0; i < length; i++) {
1205 write(data[offset + i]);
1206 }
1207 }
1208
1209 public void write(int ch) {
1210
1211
1212 if (ch == '\n' || ch == '\r') {
1213
1214 if (ch == '\n') {
1215
1216 if (previousChar != '\r') {
1217 containsMalformedEOL = true;
1218 }
1219 }
1220
1221 span = 0;
1222 }
1223 else {
1224 span++;
1225
1226 if (span > 998) {
1227 containsLongLines = true;
1228 }
1229
1230
1231 if (!ASCIIUtil.isAscii(ch)) {
1232 nonAsciiChars++;
1233 }
1234 else {
1235 asciiChars++;
1236 }
1237 }
1238 previousChar = ch;
1239 }
1240
1241
1242 public String getBinaryTransferEncoding() {
1243 if (nonAsciiChars != 0 || containsLongLines || containsMalformedEOL) {
1244 return "base64";
1245 }
1246 else {
1247 return "7bit";
1248 }
1249 }
1250
1251 public String getTextTransferEncoding() {
1252
1253 if (nonAsciiChars == 0) {
1254
1255
1256 if (containsLongLines) {
1257 return "quoted-printable";
1258 }
1259 else {
1260
1261 return "7bit";
1262 }
1263 }
1264 else {
1265
1266 if (nonAsciiChars > asciiChars) {
1267 return "base64";
1268 }
1269 else {
1270
1271 return "quoted-printable";
1272 }
1273 }
1274 }
1275 }