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 |
} |