1
2
3
4
5 package com.ozacc.mail.fetch.impl.sk_jp;
6
7 import java.io.ByteArrayInputStream;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.io.UnsupportedEncodingException;
11 import java.util.Date;
12
13 import javax.activation.DataHandler;
14 import javax.mail.BodyPart;
15 import javax.mail.Message;
16 import javax.mail.MessagingException;
17 import javax.mail.Multipart;
18 import javax.mail.Part;
19 import javax.mail.internet.AddressException;
20 import javax.mail.internet.ContentDisposition;
21 import javax.mail.internet.ContentType;
22 import javax.mail.internet.HeaderTokenizer;
23 import javax.mail.internet.InternetAddress;
24 import javax.mail.internet.MailDateFormat;
25 import javax.mail.internet.MimeUtility;
26 import javax.mail.internet.ParseException;
27
28 import com.ozacc.mail.fetch.impl.sk_jp.io.CharCodeConverter;
29 import com.ozacc.mail.fetch.impl.sk_jp.io.UnicodeCorrector;
30 import com.ozacc.mail.fetch.impl.sk_jp.text.EntityRefEncoder;
31 import com.ozacc.mail.fetch.impl.sk_jp.util.StringValues;
32 import com.sun.mail.util.BASE64EncoderStream;
33
34 /***
35 * JavaMailのサポートクラスです。
36 * <P>
37 * 主にヘッダに対するさまざまな加工機能を提供します。
38 * </P>
39 * @author Shin
40 * @version $Revision: 1.1.2.1 $ $Date: 2005/01/18 07:20:59 $
41 */
42 public class MailUtility {
43
44 public static String getPersonal(InternetAddress a) {
45 if (a.getPersonal() != null)
46 return a.getPersonal();
47 return a.toString();
48 }
49
50 /*** get comma separated E-Mail addresses. */
51 public static String getMailAddresses(InternetAddress[] addresses) {
52 if (addresses == null)
53 return null;
54 StringValues buf = new StringValues();
55 for (int i = 0; i < addresses.length; i++) {
56 buf.add(addresses[i].getAddress());
57 }
58 return buf.getString();
59 }
60
61 /*** get comma separated personal names. */
62 public static String getPersonalNames(InternetAddress[] addresses) {
63 if (addresses == null)
64 return null;
65 StringValues buf = new StringValues();
66 String name;
67 for (int i = 0; i < addresses.length; i++) {
68 name = decodeText(unfold(addresses[i].getPersonal()));
69 if (name == null) {
70 name = addresses[i].toString();
71 }
72 buf.add(name);
73 }
74 return buf.getString();
75 }
76
77 public static String getAddressesHTML(InternetAddress[] addresses) {
78 if (addresses == null)
79 return null;
80 StringValues buf = new StringValues();
81 StringBuffer href = new StringBuffer();
82 String name;
83 for (int i = 0; i < addresses.length; i++) {
84 href.append("<a href=\"mailto:");
85 href.append(addresses[i].getAddress());
86 href.append("\">");
87 name = addresses[i].getPersonal();
88 if (name != null) {
89 name = decodeText(name);
90 }
91 if (name == null) {
92 name = addresses[i].toString();
93 }
94 href.append(EntityRefEncoder.encode(name));
95 href.append("</a>");
96 buf.add(new String(href));
97 href.setLength(0);
98 }
99 return buf.getString();
100 }
101
102 /*** get the Content-Transfer-Encoding: header value. */
103 public static String getTransferEncoding(byte[] b) {
104 int nonAscii = 0;
105 for (int i = 0; i < b.length; i++) {
106 if (b[i] < 0) {
107 nonAscii++;
108 }
109 }
110 if (nonAscii == 0)
111 return "7bit";
112 if (nonAscii < b.length - nonAscii)
113 return "quoted-printable";
114 return "base64";
115 }
116
117 /***
118 * パートを保有する親Messageオブジェクトを返します。
119 * @param part パート
120 * @return ツリー構造の最上位にあたるメッセージオブジェクト
121 */
122 public static Message getParentMessage(Part part) {
123 Part current = part;
124 Multipart mp;
125 while (!(current instanceof Message)) {
126 mp = ((BodyPart)current).getParent();
127 if (mp == null)
128 return null;
129 current = mp.getParent();
130 if (current == null)
131 return null;
132 }
133 return (Message)current;
134 }
135
136
137
138 private static MailDateFormat mailDateFormat = new MailDateFormat();
139
140 /***
141 * Date構文の誤った"JST"タイムゾーンの補正を行います。
142 * <P>
143 * JavaMailは"JST"と記述されるタイムゾーンを解釈しません。 ここは本来"+0900"でなければならないところです。 <BR>
144 * 仕方がないので" JST"が含まれる文字列の場合は"+0900"を補完して
145 * MailDateFormat#parse()を通すようなparse()のラッパを用意します。
146 * </P>
147 * <P>
148 * この実装は一時回避的なものであり、完全なものではありません。
149 * </P>
150 */
151 public static Date parseDate(String rfc822DateString) {
152 if (rfc822DateString == null) {
153 return null;
154 }
155 try {
156 if (rfc822DateString.indexOf(" JST") == -1 || rfc822DateString.indexOf('+') >= 0) {
157 synchronized (mailDateFormat) {
158 return mailDateFormat.parse(rfc822DateString);
159 }
160 }
161
162 StringBuffer buf = new StringBuffer(rfc822DateString.substring(0, rfc822DateString
163 .indexOf("JST")));
164 buf.append("+0900");
165 synchronized (mailDateFormat) {
166 return mailDateFormat.parse(new String(buf));
167 }
168 } catch (java.text.ParseException e) {
169 return null;
170 }
171 }
172
173
174 /***
175 * Subject:に"Re: "を付加します。
176 * <P>
177 * ある程度寛容に"Re: "に近い文字列と"[hoge]"を取り除きます。 <BR>
178 * ただし、意図しない部分が消されてしまう事もあり得ます。 <BR>
179 * JavaMailのreply()では"Re: "がエンコードされていた場合に 正しく"Re: "を取り除いてくれません。
180 * </P>
181 */
182 public static String createReplySubject(String src) {
183 if (src == null || src.length() == 0) {
184 return "Re: (no subject)";
185 }
186 String work = src;
187 if (work.charAt(0) == '[' && work.indexOf(']') > 0) {
188 int afterBracket = indexOfNonLWSP(work, work.indexOf(']') + 1, false);
189 if (afterBracket < 0) {
190 work = "";
191 } else {
192 work = work.substring(afterBracket);
193 }
194 }
195 if (work.length() > 3 && "Re:".equalsIgnoreCase(work.substring(0, 3))) {
196 int afterRe = indexOfNonLWSP(work, 3, false);
197 if (afterRe < 0) {
198 work = "";
199 } else {
200 work = work.substring(afterRe);
201 }
202 }
203 return "Re: " + work;
204 }
205
206
207 /***
208 * 入力されたアドレスをInternetAddress形式に変換します。
209 * <p>
210 * "名無し君 <abc@example.com>(コメント)"等の文字列(エンコード無し)を
211 * 渡されても、正しくpersonal文字列が設定されるようにします。 <br>
212 * InternetAddress#parse()はエンコード済みの文字列を前提にしているため、 このメソッドの目的には沿いません。
213 * </p>
214 * @param addresses メイルアドレス文字列(カンマ区切り)
215 */
216 public static InternetAddress[] parseAddresses(String addressesString) throws AddressException {
217 return parseAddresses(addressesString, true);
218 }
219
220 public static InternetAddress[] parseAddresses(String addressesString, boolean strict)
221 throws AddressException {
222 if (addressesString == null)
223 return null;
224 try {
225 InternetAddress[] addresses = InternetAddress.parse(addressesString, strict);
226
227 for (int i = 0; i < addresses.length; i++) {
228 addresses[i].setPersonal(addresses[i].getPersonal(), "ISO-2022-JP");
229 }
230 return addresses;
231 } catch (UnsupportedEncodingException e) {
232 throw new InternalError(e.toString());
233 }
234 }
235
236
237
238
239
240 /***
241 * header valueの unfolding を行います。 空白を厳密に扱うためには decodeText より先に呼び出す必要があります。
242 */
243 public static String unfold(String source) {
244 if (source == null)
245 return null;
246 StringBuffer buf = new StringBuffer();
247 boolean skip = false;
248 char c;
249
250
251 for (int i = 0; i < source.length(); i++) {
252 c = source.charAt(i);
253 if (skip) {
254 if (isLWSP(c)) {
255 continue;
256 }
257 skip = false;
258 }
259 if (c != '\r' && c != '\n') {
260 buf.append(c);
261 } else {
262 buf.append(' ');
263 skip = true;
264 }
265 }
266 return new String(buf);
267 }
268
269 /***
270 * header valueの folding を行います。
271 * <P>
272 * white spaceをfolding対象にします。 <BR>
273 * 76bytesを超えないwhite space位置に <CRLF>を挿入します。
274 * </P>
275 * <P>
276 * 注:quoteを無視しますので、structured fieldでは不都合が 発生する可能性があります。
277 * </P>
278 * @param used ヘッダの':'までの文字数。76 - usedが最初のfolding候補桁
279 * @return foldingされた( <CRLF>SPACEが挿入された)文字列
280 */
281 public static String fold(String source, int used) {
282 if (source == null)
283 return null;
284 StringBuffer buf = new StringBuffer();
285 String work = source;
286 int lineBreakIndex;
287 while (work.length() > 76) {
288 lineBreakIndex = work.lastIndexOf(' ', 76);
289 if (lineBreakIndex == -1)
290 break;
291 buf.append(work.substring(0, lineBreakIndex));
292 buf.append("\r\n");
293 work = work.substring(lineBreakIndex);
294 }
295 buf.append(work);
296 return new String(buf);
297 }
298
299
300 /***
301 * パートにテキストをセットします。
302 * Part#setText() の代わりにこちらを使うことで、
303 * "ISO-2022-JP" コンバータではエンコードできない CP932 の
304 * 文字をエンコードできます。
305 */
306 public static void setTextContent(Part p, String s) throws MessagingException {
307
308 p.setDataHandler(new DataHandler(new JISDataSource(s)));
309 p.setHeader("Content-Transfer-Encoding", "7bit");
310 }
311
312 /***
313 * 日本語を含むヘッダ用テキストを生成します。
314 * 変換結果は ASCII なので、これをそのまま setSubject や InternetAddress
315 * のパラメタとして使用してください。
316 * "ISO-2022-JP" コンバータではエンコードできない CP932 の
317 * 文字をエンコードできます。ただし、encodeText() と異なり、
318 * folding の意識をしておらず、また ASCII 部分を除いて分割
319 * エンコードを行うこともできません。
320 */
321 public static String encodeWordJIS(String s) {
322 try {
323 return "=?ISO-2022-JP?B?"
324 + new String(BASE64EncoderStream.encode(CharCodeConverter
325 .sjisToJis(UnicodeCorrector.getInstance("Windows-31J").correct(s)
326 .getBytes("Windows-31J")))) + "?=";
327 } catch (UnsupportedEncodingException e) {
328 throw new RuntimeException("CANT HAPPEN");
329 }
330 }
331
332
333 /***
334 * ヘッダ内の文字列をデコードします。
335 * <p>
336 * MimeUtilityの制約を緩めて日本で流通するエンコード形式に対応。
337 * 本来は、encoded-wordとnon-encoded-wordの間にはlinear-white-spaceが必要
338 * なのですが、空白が無い場所でエンコードするタコメイラが多いので。
339 * </p>
340 * <p>
341 * JISコードをエンコード無しで記述するタコメイラもあります。 <br>
342 * ソースにESCが含まれていたら生JISと見なします。
343 * </p>
344 * <p>
345 * =?utf-8?Q?・・・JISコード・・?=なんてさらにタコなメイラも。 <br>
346 * 試しにデコード後にまだESCが残ってたらISO-2022-JPと見なすことにします。
347 * </p>
348 * <p>
349 * さらに、multibyte character の前後で別の encoded-word に切ってしまう メイラも…。隣接する
350 * encoded-word の CES が同じ場合はバイト列の 結合を行ってから CES デコードを行うようにした…。
351 * </p>
352 * <p>
353 * 日本語に特化してますねえ・・・。
354 * </p>
355 * @param source encoded text
356 * @return decoded text
357 */
358 public static String decodeText(String source) {
359 if (source == null)
360 return null;
361
362 if (source.indexOf('\u001b') >= 0) {
363
364 try {
365 return new String(source.getBytes("ISO-8859-1"), "ISO-2022-JP");
366 } catch (UnsupportedEncodingException e) {
367 throw new InternalError();
368 }
369 }
370 String decodedText = new RFC2047Decoder(source).get();
371 if (decodedText.indexOf('\u001b') >= 0) {
372 try {
373 return new String(decodedText.getBytes("ISO-8859-1"), "ISO-2022-JP");
374 } catch (UnsupportedEncodingException e) {
375 throw new InternalError();
376 }
377 }
378 return decodedText;
379 }
380
381
382
383
384
385
386
387 static class RFC2047Decoder {
388
389 private String source;
390
391 private String pooledCES;
392
393 private byte[] pooledBytes;
394
395 private StringBuffer buf;
396
397 private int pos = 0;
398
399 private int startIndex;
400
401 private int endIndex;
402
403 public RFC2047Decoder(String source) {
404 this.source = source;
405 buf = new StringBuffer(source.length());
406 parse();
407 }
408
409 private void parse() {
410 while (hasEncodedWord()) {
411 String work = source.substring(pos, startIndex);
412 if (indexOfNonLWSP(work, 0, false) > -1) {
413 sweepPooledBytes();
414 buf.append(work);
415 }
416 parseWord();
417 }
418 sweepPooledBytes();
419 buf.append(source.substring(pos));
420 }
421
422
423 private boolean hasEncodedWord() {
424 startIndex = source.indexOf("=?", pos);
425 if (startIndex == -1)
426 return false;
427 endIndex = source.indexOf("?=", startIndex + 2);
428 if (endIndex == -1)
429 return false;
430
431
432
433
434
435
436
437
438
439 endIndex += 2;
440 return true;
441 }
442
443 private void parseWord() {
444 try {
445 int s = startIndex + 2;
446 int e = source.indexOf('?', s);
447 if (e == endIndex - 2)
448 throw new RuntimeException();
449 String ces = source.substring(s, e);
450 try {
451 "".getBytes(ces);
452 } catch (UnsupportedEncodingException ex) {
453 ces = "JISAutoDetect";
454 }
455 s = e + 1;
456 e = source.indexOf('?', s);
457 if (e == endIndex - 2)
458 throw new RuntimeException();
459 String tes = source.substring(s, e);
460 byte[] bytes = decodeByTES(source.substring(e + 1, endIndex - 2), tes);
461 if (ces.equals(pooledCES)) {
462
463 byte[] w = new byte[pooledBytes.length + bytes.length];
464 System.arraycopy(pooledBytes, 0, w, 0, pooledBytes.length);
465 System.arraycopy(bytes, 0, w, pooledBytes.length, bytes.length);
466 pooledBytes = w;
467 } else {
468 sweepPooledBytes();
469 pooledCES = ces;
470 pooledBytes = bytes;
471 }
472 } catch (Exception ex) {
473 ex.printStackTrace();
474
475 buf.append(source.substring(startIndex, endIndex));
476 }
477 pos = endIndex;
478 }
479
480 private void sweepPooledBytes() {
481 if (pooledBytes == null)
482 return;
483 try {
484 buf.append(new String(pooledBytes, pooledCES));
485 } catch (UnsupportedEncodingException e) {
486 throw new InternalError("CANT HAPPEN: Illegal encoding = " + pooledCES);
487 }
488 pooledCES = null;
489 pooledBytes = null;
490 }
491
492 public String get() {
493 return new String(buf);
494 }
495 }
496
497 private static byte[] decodeByTES(String s, String tes) {
498
499 int i;
500 while ((i = indexOfLWSP(s, 0, false, (char)0)) >= 0)
501 s = s.substring(0, i) + s.substring(i + 1);
502 if (tes.equalsIgnoreCase("B") && s.length() % 4 != 0) {
503
504
505 switch (4 - s.length() % 4) {
506 case 1:
507 s += '=';
508 break;
509 case 2:
510 s += "==";
511 break;
512 case 3:
513 if (s.charAt(s.length() - 1) != '=')
514 s += "===";
515 else
516 s = s.substring(0, s.length() - 1);
517 break;
518 }
519 }
520 try {
521 ByteArrayInputStream bis = new ByteArrayInputStream(com.sun.mail.util.ASCIIUtility
522 .getBytes(s));
523 InputStream is;
524 if (tes.equalsIgnoreCase("B"))
525 is = new com.sun.mail.util.BASE64DecoderStream(bis);
526 else if (tes.equalsIgnoreCase("Q"))
527 is = new com.sun.mail.util.QDecoderStream(bis);
528 else
529 throw new UnsupportedEncodingException(tes);
530 int count = bis.available();
531 byte[] bytes = new byte[count];
532 count = is.read(bytes, 0, count);
533 if (count != bytes.length) {
534 byte[] w = new byte[count];
535 System.arraycopy(bytes, 0, w, 0, count);
536 bytes = w;
537 }
538 return bytes;
539 } catch (IOException e) {
540 e.printStackTrace();
541 throw new RuntimeException("CANT HAPPEN");
542 }
543 }
544
545 /***
546 * 文字列をエンコードします。
547 * <p>
548 * MimeUtility(強いてはMimeMessage等も)では、1字でも非ASCII文字が含まれる
549 * と文字列全体をエンコードしてしまいます。
550 * <br>
551 * このメソッドでは空白で区切られた範囲だけをエンコードします。 <br>
552 * Subjectの"Re: "等がエンコードされていると、この文字列でIn-Reply-To:
553 * References:の代わりにスレッドを形成しようとしても失敗することになる
554 * ため、こちらのエンコード方式を用いたがる人もいるかもしれません・・。
555 * </p>
556 * <p>
557 * 方針は、ASCII部に前後の空白一つを含ませ、それ以外は空白も含めて全て
558 * encoded-wordとします。()の内側は空白無しでもエンコード対象です。
559 * </p>
560 * @param source text
561 * @return encoded text
562 */
563
564
565 public static String encodeText(String source, String charset, String encoding)
566 throws UnsupportedEncodingException {
567 if (source == null)
568 return null;
569 int boundaryIndex;
570 int startIndex;
571 int endIndex = 0;
572 int lastLWSPIndex;
573 StringBuffer buf = new StringBuffer();
574 while (true) {
575
576 boundaryIndex = indexOfNonAscii(source, endIndex);
577 if (boundaryIndex == -1) {
578 buf.append(source.substring(endIndex));
579 return new String(buf);
580 }
581
582 lastLWSPIndex = indexOfLWSP(source, boundaryIndex, true, '(');
583 startIndex = indexOfNonLWSP(source, lastLWSPIndex, true) + 1;
584
585
586 startIndex = (endIndex > startIndex) ? endIndex : startIndex;
587 if (startIndex > endIndex) {
588
589 buf.append(source.substring(endIndex, startIndex));
590
591
592 if (isLWSP(source.charAt(startIndex))) {
593
594 buf.append("\r\n ");
595 startIndex++;
596
597 }
598 }
599
600 endIndex = indexOfNonLWSP(source, boundaryIndex, false);
601 while ((endIndex = indexOfLWSP(source, endIndex, false, ')')) != -1) {
602 endIndex = indexOfNonLWSP(source, endIndex, false);
603 int nextBoundary = indexOfLWSP(source, endIndex, false, (char)0);
604 if (nextBoundary == -1) {
605 if (indexOfNonAscii(source, endIndex) != -1) {
606 endIndex = -1;
607 break;
608 }
609 } else {
610 int nonAscii = indexOfNonAscii(source, endIndex);
611 if (nonAscii != -1 && nonAscii < nextBoundary) {
612 endIndex = nextBoundary;
613 continue;
614 }
615 }
616 break;
617 }
618 boolean needFolding = false;
619 if (endIndex < 0) {
620 endIndex = source.length();
621 } else if (isLWSP(source.charAt(endIndex - 1))) {
622
623 endIndex--;
624 needFolding = true;
625 }
626 String encodeTargetText = source.substring(startIndex, endIndex);
627 buf.append(MimeUtility.encodeWord(encodeTargetText, charset, encoding));
628 if (needFolding) {
629
630 endIndex++;
631 buf.append("\r\n ");
632 }
633 }
634 }
635
636 /***
637 * 指定位置から最初に見つかった非ASCII文字のIndexを返します。 startIndex が範囲外の場合は -1 を返します。
638 * (IndexOutOfBoundsException ではない)
639 * @param source 検索する文字列
640 * @param startIndex 検索開始位置
641 * @return 検出した非ASCII文字Index。見つからなければ-1。
642 */
643 public static int indexOfNonAscii(String source, int startIndex) {
644 for (int i = startIndex; i < source.length(); i++) {
645 if (source.charAt(i) > 0x7f) {
646 return i;
647 }
648 }
649 return -1;
650 }
651
652 /***
653 * 指定位置から最初に見つかったLWSP以外の文字のIndexを返します。 startIndex が範囲外の場合は -1 を返します。
654 * (IndexOutOfBoundsException ではない)
655 * @param source 検索する文字列
656 * @param startIndex 検索開始位置
657 * @param decrease trueで後方検索
658 * @return 検出した非ASCII文字Index。見つからなければ-1。
659 */
660 public static int indexOfNonLWSP(String source, int startIndex, boolean decrease) {
661 char c;
662 int inc = 1;
663 if (decrease)
664 inc = -1;
665 for (int i = startIndex; i >= 0 && i < source.length(); i += inc) {
666 c = source.charAt(i);
667 if (!isLWSP(c)) {
668 return i;
669 }
670 }
671 return -1;
672 }
673
674 /***
675 * 指定位置から最初に見つかったLWSPのIndexを返します。 startIndex が範囲外の場合は -1 を返します。
676 * (IndexOutOfBoundsException ではない)
677 * @param source 検索する文字列
678 * @param startIndex 検索開始位置
679 * @param decrease trueで後方検索
680 * @param additionalDelimiter LWSP以外に区切りとみなす文字(1字のみ)
681 * @return 検出した非ASCII文字Index。見つからなければ-1。
682 */
683 public static int indexOfLWSP(String source, int startIndex, boolean decrease,
684 char additionalDelimiter) {
685 char c;
686 int inc = 1;
687 if (decrease)
688 inc = -1;
689 for (int i = startIndex; i >= 0 && i < source.length(); i += inc) {
690 c = source.charAt(i);
691 if (isLWSP(c) || c == additionalDelimiter) {
692 return i;
693 }
694 }
695 return -1;
696 }
697
698 public static boolean isLWSP(char c) {
699 return c == '\r' || c == '\n' || c == ' ' || c == '\t';
700 }
701
702
703 /***
704 * This method set Content-Disposition: with RFC2231 encoding. It is
705 * required JavaMail1.2.
706 */
707 /***
708 * Part#setFileName()のマルチバイト対応版です。 JavaMail1.2でなければコンパイルできません
709 */
710 public static void setFileName(Part part, String filename, String charset, String lang)
711 throws MessagingException {
712
713 ContentDisposition disposition;
714 String[] strings = part.getHeader("Content-Disposition");
715 if (strings == null || strings.length < 1) {
716 disposition = new ContentDisposition(Part.ATTACHMENT);
717 } else {
718 disposition = new ContentDisposition(strings[0]);
719 disposition.getParameterList().remove("filename");
720 }
721 part.setHeader("Content-Disposition", disposition.toString()
722 + encodeParameter("filename", filename, charset, lang));
723 ContentType cType;
724 strings = part.getHeader("Content-Type");
725 if (strings == null || strings.length < 1) {
726 cType = new ContentType(part.getDataHandler().getContentType());
727 } else {
728 cType = new ContentType(strings[0]);
729 }
730 try {
731
732 String mimeString = MimeUtility.encodeWord(filename, charset, "B");
733
734 StringBuffer sb = new StringBuffer();
735 int i;
736 while ((i = mimeString.indexOf('\r')) != -1) {
737 sb.append(mimeString.substring(0, i));
738 mimeString = mimeString.substring(i + 2);
739 }
740 sb.append(mimeString);
741 cType.setParameter("name", new String(sb));
742 } catch (UnsupportedEncodingException e) {
743 throw new MessagingException("Encoding error", e);
744 }
745 part.setHeader("Content-Type", cType.toString());
746 }
747
748 /***
749 * This method encodes the parameter.
750 * <P>
751 * But most MUA cannot decode the encoded parameters by this method. <BR>
752 * I recommend using the "Content-Type:"'s name parameter both.
753 * </P>
754 */
755 /***
756 * ヘッダのパラメタ部のエンコードを行います。
757 * <P>
758 * 現状は受信できないものが多いのでこのメソッドだけでは使えません。 <BR>
759 * Content-Disposition:のfilenameのみに使用し、さらに Content-Type:のnameにMIME
760 * encodingでの記述も行うのが妥当でしょう。 <BR>
761 * パラメタは必ず行頭から始まるものとします。 (ヘッダの開始行から折り返された位置を開始位置とします)
762 * </P>
763 * <P>
764 * foldingの方針はascii/non ascii境界のみをチェックします。 現状は連続するascii/non
765 * asciiの長さのチェックは現状行っていません。 (エンコード後のバイト数でチェックしなければならないのでかなり面倒)
766 * </P>
767 * @param name パラメタ名
768 * @param value エンコード対象のパラメタ値
769 * @param encoding 文字エンコーディング
770 * @param lang 言語指定子
771 * @return エンコード済み文字列 ";\r\n name*0*=ISO-8859-2''・・・;\r\n name*1*=・・"
772 */
773
774 public static String encodeParameter(String name, String value, String encoding, String lang) {
775 StringBuffer result = new StringBuffer();
776 StringBuffer encodedPart = new StringBuffer();
777 boolean needWriteCES = !isAllAscii(value);
778 boolean CESWasWritten = false;
779 boolean encoded;
780 boolean needFolding = false;
781 int sequenceNo = 0;
782 int column;
783 while (value.length() > 0) {
784
785 int lastIndex;
786 boolean isAscii = value.charAt(0) < 0x80;
787 for (lastIndex = 1; lastIndex < value.length(); lastIndex++) {
788 if (value.charAt(lastIndex) < 0x80) {
789 if (!isAscii)
790 break;
791 } else {
792 if (isAscii)
793 break;
794 }
795 }
796 if (lastIndex != value.length())
797 needFolding = true;
798 RETRY: while (true) {
799 encodedPart.setLength(0);
800 String target = value.substring(0, lastIndex);
801 byte[] bytes;
802 try {
803 if (isAscii) {
804 bytes = target.getBytes("us-ascii");
805 } else {
806 bytes = target.getBytes(encoding);
807 }
808 } catch (UnsupportedEncodingException e) {
809 bytes = target.getBytes();
810 encoding = MimeUtility.mimeCharset(MimeUtility.getDefaultJavaCharset());
811 }
812 encoded = false;
813
814 column = name.length() + 7;
815 for (int i = 0; i < bytes.length; i++) {
816 if ((bytes[i] >= '0' && bytes[i] <= '9')
817 || (bytes[i] >= 'A' && bytes[i] <= 'Z')
818 || (bytes[i] >= 'a' && bytes[i] <= 'z') || bytes[i] == '$'
819 || bytes[i] == '.' || bytes[i] == '!') {
820
821
822
823
824
825
826 encodedPart.append((char)bytes[i]);
827 column++;
828 } else {
829 encoded = true;
830 encodedPart.append('%');
831 String hex = Integer.toString(bytes[i] & 0xff, 16);
832 if (hex.length() == 1) {
833 encodedPart.append('0');
834 }
835 encodedPart.append(hex);
836 column += 3;
837 }
838 if (column > 76) {
839 needFolding = true;
840 lastIndex /= 2;
841 continue RETRY;
842 }
843 }
844 result.append(";\r\n ").append(name);
845 if (needFolding) {
846 result.append('*').append(sequenceNo);
847 sequenceNo++;
848 }
849 if (!CESWasWritten && needWriteCES) {
850 result.append("*=");
851 CESWasWritten = true;
852 result.append(encoding).append('\'');
853 if (lang != null)
854 result.append(lang);
855 result.append('\'');
856 } else if (encoded) {
857 result.append("*=");
858
859
860
861
862
863
864
865 } else {
866 result.append('=');
867 }
868 result.append(new String(encodedPart));
869 value = value.substring(lastIndex);
870 break;
871 }
872 }
873 return new String(result);
874 }
875
876 /*** check if contains only ascii characters in text. */
877 public static boolean isAllAscii(String text) {
878 for (int i = 0; i < text.length(); i++) {
879 if (text.charAt(i) > 0x7f) {
880 return false;
881 }
882 }
883 return true;
884 }
885
886
887 /***
888 * This method decode the RFC2231 encoded filename parameter instead of
889 * Part#getFileName().
890 */
891 /***
892 * Part#getFileName()のマルチバイト対応版です。
893 */
894 public static String getFileName(Part part) throws MessagingException {
895 String[] disposition = part.getHeader("Content-Disposition");
896
897
898 String filename;
899 if (disposition == null || disposition.length < 1
900 || (filename = getParameter(disposition[0], "filename")) == null) {
901 filename = part.getFileName();
902 if (filename != null) {
903 return decodeParameterSpciallyJapanese(filename);
904 }
905 return null;
906 }
907 return filename;
908 }
909
910 static class Encoding {
911
912 String encoding = "us-ascii";
913
914 String lang = "";
915 }
916
917 /***
918 * This method decodes the parameter which be encoded (folded) by RFC2231
919 * method.
920 * <P>
921 * The parameter's order should be considered.
922 * </P>
923 */
924 /***
925 * ヘッダのパラメタ部のデコードを行います。
926 * <P>
927 * RFC2231形式でfolding(分割)されたパラメタを結合し、デコードします。
928 * 尚、RFC2231にはパラメタの順番に依存するなと書かれていますが、 それを実装すると大変面倒(一度分割された全てのパートを
929 * 保持してソートしなければならない)なので、 シーケンス番号に関係なく(0から)順番に 並んでいるものとみなして処理することにします。
930 * </P>
931 * @param header ヘッダの値全体
932 * @param name 取得したいパラメタ名
933 * @return デコード済み文字列 (パラメタが存在しない場合は null)
934 */
935 public static String getParameter(String header, String name) throws ParseException {
936 if (header == null)
937 return null;
938
939
940
941
942
943
944 header = decodeParameterSpciallyJapanese(header);
945 HeaderTokenizer tokenizer = new HeaderTokenizer(header, ";=\t ", true);
946 HeaderTokenizer.Token token;
947 StringBuffer sb = new StringBuffer();
948
949 Encoding encoding = new Encoding();
950 String n;
951 String v;
952 try {
953 while (true) {
954 token = tokenizer.next();
955 if (token.getType() == HeaderTokenizer.Token.EOF)
956 break;
957 if (token.getType() != ';')
958 continue;
959 token = tokenizer.next();
960 checkType(token);
961 n = token.getValue();
962 token = tokenizer.next();
963 if (token.getType() != '=') {
964 throw new ParseException("Illegal token : " + token.getValue());
965 }
966 token = tokenizer.next();
967 checkType(token);
968 v = token.getValue();
969 if (n.equalsIgnoreCase(name)) {
970
971 return v;
972 }
973 int index = name.length();
974 if (!n.startsWith(name) || n.charAt(index) != '*') {
975
976 continue;
977 }
978
979 int lastIndex = n.length() - 1;
980 if (n.charAt(lastIndex) == '*') {
981
982 if (index == lastIndex || n.charAt(index + 1) == '0') {
983
984 sb.append(decodeRFC2231(v, encoding, true));
985 } else {
986
987 sb.append(decodeRFC2231(v, encoding, false));
988 }
989 } else {
990 sb.append(v);
991 }
992 if (index == lastIndex) {
993
994 break;
995 }
996 }
997 if (sb.length() == 0)
998 return null;
999 return new String(sb);
1000 } catch (UnsupportedEncodingException e) {
1001 throw new ParseException(e.toString());
1002 }
1003 }
1004
1005 private static void checkType(HeaderTokenizer.Token token) throws ParseException {
1006 int t = token.getType();
1007 if (t != HeaderTokenizer.Token.ATOM && t != HeaderTokenizer.Token.QUOTEDSTRING) {
1008 throw new ParseException("Illegal token : " + token.getValue());
1009 }
1010 }
1011
1012
1013 private static String decodeRFC2231(String s, Encoding encoding, boolean isInitialSection)
1014 throws ParseException,
1015 UnsupportedEncodingException {
1016 StringBuffer sb = new StringBuffer();
1017 int i = 0;
1018 if (isInitialSection) {
1019 int work = s.indexOf('\'');
1020 if (work > 0) {
1021 encoding.encoding = s.substring(0, work);
1022 work++;
1023 i = s.indexOf('\'', work);
1024 if (i < 0) {
1025 throw new ParseException("lang tag area was missing.");
1026 }
1027 encoding.lang = s.substring(work, i);
1028 i++;
1029 }
1030 }
1031 try {
1032 for (; i < s.length(); i++) {
1033 if (s.charAt(i) == '%') {
1034 sb.append((char)Integer.parseInt(s.substring(i + 1, i + 3), 16));
1035 i += 2;
1036 continue;
1037 }
1038 sb.append(s.charAt(i));
1039 }
1040 return new String(new String(sb).getBytes("ISO-8859-1"), encoding.encoding);
1041 } catch (IndexOutOfBoundsException e) {
1042 throw new ParseException(s + " :: this string were not decoded.");
1043 }
1044 }
1045
1046
1047 private static String decodeParameterSpciallyJapanese(String s) throws ParseException {
1048 try {
1049
1050
1051 s = new String(s.getBytes("ISO-8859-1"), "JISAutoDetect");
1052
1053
1054 return decodeText(s);
1055 } catch (UnsupportedEncodingException e) {}
1056 throw new ParseException("Unsupported Encoding");
1057 }
1058
1059 private MailUtility() {}
1060 }