2 * @(#) $Id: MailUtility.java,v 1.1.2.1 2005/01/18 07:20:59 otsuka Exp $
3 * Copyright (c) 2000-2004 Shin Kinoshita All Rights Reserved.
5 package com.ozacc.mail.fetch.impl.sk_jp;
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;
13 import jakarta.activation.DataHandler;
14 import jakarta.mail.BodyPart;
15 import jakarta.mail.Message;
16 import jakarta.mail.MessagingException;
17 import jakarta.mail.Multipart;
18 import jakarta.mail.Part;
19 import jakarta.mail.internet.AddressException;
20 import jakarta.mail.internet.ContentDisposition;
21 import jakarta.mail.internet.ContentType;
22 import jakarta.mail.internet.HeaderTokenizer;
23 import jakarta.mail.internet.InternetAddress;
24 import jakarta.mail.internet.MailDateFormat;
25 import jakarta.mail.internet.MimeUtility;
26 import jakarta.mail.internet.ParseException;
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;
37 * 主にヘッダに対するさまざまな加工機能を提供します。
40 * @version $Revision: 1.1.2.1 $ $Date: 2005/01/18 07:20:59 $
42 public class MailUtility {
44 public static String getPersonal(InternetAddress a) {
45 if (a.getPersonal() != null)
46 return a.getPersonal();
50 /** get comma separated E-Mail addresses. */
51 public static String getMailAddresses(InternetAddress[] addresses) {
52 if (addresses == null)
54 StringValues buf = new StringValues();
55 for (int i = 0; i < addresses.length; i++) {
56 buf.add(addresses[i].getAddress());
58 return buf.getString();
61 /** get comma separated personal names. */
62 public static String getPersonalNames(InternetAddress[] addresses) {
63 if (addresses == null)
65 StringValues buf = new StringValues();
67 for (int i = 0; i < addresses.length; i++) {
68 name = decodeText(unfold(addresses[i].getPersonal()));
70 name = addresses[i].toString();
74 return buf.getString();
77 public static String getAddressesHTML(InternetAddress[] addresses) {
78 if (addresses == null)
80 StringValues buf = new StringValues();
81 StringBuilder href = new StringBuilder();
83 for (int i = 0; i < addresses.length; i++) {
84 href.append("<a href=\"mailto:");
85 href.append(addresses[i].getAddress());
87 name = addresses[i].getPersonal();
89 name = decodeText(name);
92 name = addresses[i].toString();
94 href.append(EntityRefEncoder.encode(name));
96 buf.add(new String(href));
99 return buf.getString();
102 /** get the Content-Transfer-Encoding: header value. */
103 public static String getTransferEncoding(byte[] b) {
105 for (int i = 0; i < b.length; i++) {
112 if (nonAscii < b.length - nonAscii)
113 return "quoted-printable";
118 * パートを保有する親Messageオブジェクトを返します。
120 * @return ツリー構造の最上位にあたるメッセージオブジェクト
122 public static Message getParentMessage(Part part) {
125 while (!(current instanceof Message)) {
126 mp = ((BodyPart)current).getParent();
128 return null; // Should it throw exception?
129 current = mp.getParent();
131 return null; // Should it throw exception?
133 return (Message)current;
136 //////////////////////////////////////////////////////////////////////////
137 // note: JavaMail1.2 later
138 private static MailDateFormat mailDateFormat = new MailDateFormat();
141 * Date構文の誤った"JST"タイムゾーンの補正を行います。
143 * JavaMailは"JST"と記述されるタイムゾーンを解釈しません。 ここは本来"+0900"でなければならないところです。 <BR>
144 * 仕方がないので" JST"が含まれる文字列の場合は"+0900"を補完して
145 * MailDateFormat#parse()を通すようなparse()のラッパを用意します。
148 * この実装は一時回避的なものであり、完全なものではありません。
151 public static Date parseDate(String rfc822DateString) {
152 if (rfc822DateString == null) {
156 if (rfc822DateString.indexOf(" JST") == -1 || rfc822DateString.indexOf('+') >= 0) {
157 synchronized (mailDateFormat) {
158 return mailDateFormat.parse(rfc822DateString);
161 // correct the pseudo header
162 StringBuilder buf = new StringBuilder(rfc822DateString.substring(0, rfc822DateString
165 synchronized (mailDateFormat) {
166 return mailDateFormat.parse(new String(buf));
168 } catch (java.text.ParseException e) {
173 //////////////////////////////////////////////////////////////////////////
175 * Subject:に"Re: "を付加します。
177 * ある程度寛容に"Re: "に近い文字列と"[hoge]"を取り除きます。 <BR>
178 * ただし、意図しない部分が消されてしまう事もあり得ます。 <BR>
179 * JavaMailのreply()では"Re: "がエンコードされていた場合に 正しく"Re: "を取り除いてくれません。
182 public static String createReplySubject(String src) {
183 if (src == null || src.length() == 0) {
184 return "Re: (no subject)";
187 if (work.charAt(0) == '[' && work.indexOf(']') > 0) {
188 int afterBracket = indexOfNonLWSP(work, work.indexOf(']') + 1, false);
189 if (afterBracket < 0) {
192 work = work.substring(afterBracket);
195 if (work.length() > 3 && "Re:".equalsIgnoreCase(work.substring(0, 3))) {
196 int afterRe = indexOfNonLWSP(work, 3, false);
200 work = work.substring(afterRe);
203 return "Re: " + work;
206 //////////////////////////////////////////////////////////////////////////
208 * 入力されたアドレスをInternetAddress形式に変換します。
210 * "名無し君 <abc@example.com>(コメント)"等の文字列(エンコード無し)を
211 * 渡されても、正しくpersonal文字列が設定されるようにします。 <br>
212 * InternetAddress#parse()はエンコード済みの文字列を前提にしているため、 このメソッドの目的には沿いません。
214 * @param addresses メイルアドレス文字列(カンマ区切り)
216 public static InternetAddress[] parseAddresses(String addressesString) throws AddressException {
217 return parseAddresses(addressesString, true);
220 public static InternetAddress[] parseAddresses(String addressesString, boolean strict)
221 throws AddressException {
222 if (addressesString == null)
225 InternetAddress[] addresses = InternetAddress.parse(addressesString, strict);
227 for (int i = 0; i < addresses.length; i++) {
228 addresses[i].setPersonal(addresses[i].getPersonal(), "ISO-2022-JP");
231 } catch (UnsupportedEncodingException e) {
232 throw new InternalError(e.toString());
236 // InternetAddress.parse(
237 // encodeText(addressesString, "ISO-2022-JP", "B"), strict);
238 // で良さそうなものだが、これでは・・たしかなんか問題があったはず。
239 //////////////////////////////////////////////////////////////////////////
241 * header valueの unfolding を行います。 空白を厳密に扱うためには decodeText より先に呼び出す必要があります。
243 public static String unfold(String source) {
246 StringBuilder buf = new StringBuilder();
247 boolean skip = false;
249 // <CRLF>シーケンスを前提とするならindexOf()で十分ですが、
250 // 念のためCR、LFいずれも許容します。
251 for (int i = 0; i < source.length(); i++) {
252 c = source.charAt(i);
259 if (c != '\r' && c != '\n') {
266 return new String(buf);
270 * header valueの folding を行います。
272 * white spaceをfolding対象にします。 <BR>
273 * 76bytesを超えないwhite space位置に <CRLF>を挿入します。
276 * 注:quoteを無視しますので、structured fieldでは不都合が 発生する可能性があります。
278 * @param used ヘッダの':'までの文字数。76 - usedが最初のfolding候補桁
279 * @return foldingされた( <CRLF>SPACEが挿入された)文字列
281 public static String fold(String source, int used) {
284 StringBuilder buf = new StringBuilder();
285 String work = source;
287 while (work.length() > 76) {
288 lineBreakIndex = work.lastIndexOf(' ', 76);
289 if (lineBreakIndex == -1)
291 buf.append(work.substring(0, lineBreakIndex));
293 work = work.substring(lineBreakIndex);
296 return new String(buf);
299 //////////////////////////////////////////////////////////////////////////
302 * Part#setText() の代わりにこちらを使うことで、
303 * "ISO-2022-JP" コンバータではエンコードできない CP932 の
306 public static void setTextContent(Part p, String s) throws MessagingException {
307 //p.setText(content, "ISO-2022-JP");
308 p.setDataHandler(new DataHandler(new JISDataSource(s)));
309 p.setHeader("Content-Transfer-Encoding", "7bit");
313 * 日本語を含むヘッダ用テキストを生成します。
314 * 変換結果は ASCII なので、これをそのまま setSubject や InternetAddress
316 * "ISO-2022-JP" コンバータではエンコードできない CP932 の
317 * 文字をエンコードできます。ただし、encodeText() と異なり、
318 * folding の意識をしておらず、また ASCII 部分を除いて分割
321 public static String encodeWordJIS(String s) {
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");
332 //////////////////////////////////////////////////////////////////////////
336 * MimeUtilityの制約を緩めて日本で流通するエンコード形式に対応。
337 * 本来は、encoded-wordとnon-encoded-wordの間にはlinear-white-spaceが必要
338 * なのですが、空白が無い場所でエンコードするタコメイラが多いので。
341 * JISコードをエンコード無しで記述するタコメイラもあります。 <br>
342 * ソースにESCが含まれていたら生JISと見なします。
345 * =?utf-8?Q?・・・JISコード・・?=なんてさらにタコなメイラも。 <br>
346 * 試しにデコード後にまだESCが残ってたらISO-2022-JPと見なすことにします。
349 * さらに、multibyte character の前後で別の encoded-word に切ってしまう メイラも…。隣接する
350 * encoded-word の CES が同じ場合はバイト列の 結合を行ってから CES デコードを行うようにした…。
355 * @param source encoded text
356 * @return decoded text
358 public static String decodeText(String source) {
361 // specially for Japanese
362 if (source.indexOf('\u001b') >= 0) {
365 return new String(source.getBytes("ISO-8859-1"), "ISO-2022-JP");
366 } catch (UnsupportedEncodingException e) {
367 throw new InternalError();
370 String decodedText = new RFC2047Decoder(source).get();
371 if (decodedText.indexOf('\u001b') >= 0) {
373 return new String(decodedText.getBytes("ISO-8859-1"), "ISO-2022-JP");
374 } catch (UnsupportedEncodingException e) {
375 throw new InternalError();
381 // 日本語をデコードする上で問題があるので、encoded-wordの切り出しはすべて独自に
382 // Netscapeなどは"()."等の文字でencoded-wordを切ってしまうが、JavaMailは
383 // このときencoded-wordの終わりを判定できず、一部の文字を欠落させてしまう。
384 // また、encoded-word を文字デコードするのを遅延させ、隣接する encoded-word
385 // の CES が同じ場合は、先に TES デコードを行ったバイト列を結合してから
386 // CES に従ったデコードを行う。マルチバイト文字を分断する sender がいるから。
387 static class RFC2047Decoder {
389 private String source;
391 private String pooledCES;
393 private byte[] pooledBytes;
395 private StringBuilder buf;
399 private int startIndex;
401 private int endIndex;
403 public RFC2047Decoder(String source) {
404 this.source = source;
405 buf = new StringBuilder(source.length());
409 private void parse() {
410 while (hasEncodedWord()) {
411 String work = source.substring(pos, startIndex);
412 if (indexOfNonLWSP(work, 0, false) > -1) {
415 } // encoded-word同士の間のLWSPは削除
419 buf.append(source.substring(pos));
422 // encoded-word があった場合、startIndex/endIndex をセットする
423 private boolean hasEncodedWord() {
424 startIndex = source.indexOf("=?", pos);
425 if (startIndex == -1)
427 endIndex = source.indexOf("?=", startIndex + 2);
430 // 本来は encoded-word 中に LWSP があってはいけないが
431 // encoded-word の途中で folding してしまう sender がいるらしい
432 // 以下をコメントにすることで encoded-word の誤認識の可能性も
433 // 出てくるが、誤認識になる確率以上に前記のような illegal な
434 // メッセージの方が多いのが実情のようだ。
436 //int i = indexOfLWSP(source, startIndex + 2, false, (char)0);
437 //if (i >= 0 && i < endIndex)
443 private void parseWord() {
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);
451 "".getBytes(ces); // FIXME: check whether supported or not
452 } catch (UnsupportedEncodingException ex) {
453 ces = "JISAutoDetect";
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)) {
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);
472 } catch (Exception ex) {
473 ex.printStackTrace();
474 // contains RuntimeException
475 buf.append(source.substring(startIndex, endIndex));
480 private void sweepPooledBytes() {
481 if (pooledBytes == null)
484 buf.append(new String(pooledBytes, pooledCES));
485 } catch (UnsupportedEncodingException e) {
486 throw new InternalError("CANT HAPPEN: Illegal encoding = " + pooledCES);
492 public String get() {
493 return new String(buf);
497 private static byte[] decodeByTES(String s, String tes) {
498 // 通常あり得ないが、LWSP を詰める
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 // BASE64DecoderStream は正確にパディングされていないと
504 // IOException になるので、無理やり矯正。
505 switch (4 - s.length() % 4) {
513 if (s.charAt(s.length() - 1) != '=')
516 s = s.substring(0, s.length() - 1);
521 ByteArrayInputStream bis = new ByteArrayInputStream(com.sun.mail.util.ASCIIUtility
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);
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);
539 } catch (IOException e) {
541 throw new RuntimeException("CANT HAPPEN");
548 * MimeUtility(強いてはMimeMessage等も)では、1字でも非ASCII文字が含まれる
549 * と文字列全体をエンコードしてしまいます。
551 * このメソッドでは空白で区切られた範囲だけをエンコードします。 <br>
552 * Subjectの"Re: "等がエンコードされていると、この文字列でIn-Reply-To:
553 * References:の代わりにスレッドを形成しようとしても失敗することになる
554 * ため、こちらのエンコード方式を用いたがる人もいるかもしれません・・。
557 * 方針は、ASCII部に前後の空白一つを含ませ、それ以外は空白も含めて全て
558 * encoded-wordとします。()の内側は空白無しでもエンコード対象です。
561 * @return encoded text
563 // "()" の扱いにこだわりすぎて異常に汚い-_-。
564 // "()"なんか無視してまとめて encode するようにすればすっきるするけど…。
565 public static String encodeText(String source, String charset, String encoding)
566 throws UnsupportedEncodingException {
573 StringBuilder buf = new StringBuilder();
575 // check the end of ASCII part
576 boundaryIndex = indexOfNonAscii(source, endIndex);
577 if (boundaryIndex == -1) {
578 buf.append(source.substring(endIndex));
579 return new String(buf);
581 // any LWSP has taken (back track).
582 lastLWSPIndex = indexOfLWSP(source, boundaryIndex, true, '(');
583 startIndex = indexOfNonLWSP(source, lastLWSPIndex, true) + 1;
584 // ASCII part の終了位置は、次の non ASCII と比べて
585 // 最も ASCII 文字よりの空白文字位置または'('の次位置
586 startIndex = (endIndex > startIndex) ? endIndex : startIndex;
587 if (startIndex > endIndex) {
589 buf.append(source.substring(endIndex, startIndex));
590 // JavaMailはencodeWord内でfoldingするけどそれはencodedWord
591 // に対してのみ。ヘッダそのものに対するfoldingはしてくれない。
592 if (isLWSP(source.charAt(startIndex))) {
593 // folding により 空白一つが確保されるのでスキップ
596 // なお、'('の場合は空白を入れないので folding しない
599 // any LWSP has taken.
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) {
610 int nonAscii = indexOfNonAscii(source, endIndex);
611 if (nonAscii != -1 && nonAscii < nextBoundary) {
612 endIndex = nextBoundary;
618 boolean needFolding = false;
620 endIndex = source.length();
621 } else if (isLWSP(source.charAt(endIndex - 1))) {
622 // folding により 空白一つが確保される(予定)なので減らす
626 String encodeTargetText = source.substring(startIndex, endIndex);
627 buf.append(MimeUtility.encodeWord(encodeTargetText, charset, encoding));
629 // folding により 空白一つが確保されるのでスキップ
637 * 指定位置から最初に見つかった非ASCII文字のIndexを返します。 startIndex が範囲外の場合は -1 を返します。
638 * (IndexOutOfBoundsException ではない)
639 * @param source 検索する文字列
640 * @param startIndex 検索開始位置
641 * @return 検出した非ASCII文字Index。見つからなければ-1。
643 public static int indexOfNonAscii(String source, int startIndex) {
644 for (int i = startIndex; i < source.length(); i++) {
645 if (source.charAt(i) > 0x7f) {
653 * 指定位置から最初に見つかったLWSP以外の文字のIndexを返します。 startIndex が範囲外の場合は -1 を返します。
654 * (IndexOutOfBoundsException ではない)
655 * @param source 検索する文字列
656 * @param startIndex 検索開始位置
657 * @param decrease trueで後方検索
658 * @return 検出した非ASCII文字Index。見つからなければ-1。
660 public static int indexOfNonLWSP(String source, int startIndex, boolean decrease) {
665 for (int i = startIndex; i >= 0 && i < source.length(); i += inc) {
666 c = source.charAt(i);
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。
683 public static int indexOfLWSP(String source, int startIndex, boolean decrease,
684 char additionalDelimiter) {
689 for (int i = startIndex; i >= 0 && i < source.length(); i += inc) {
690 c = source.charAt(i);
691 if (isLWSP(c) || c == additionalDelimiter) {
698 public static boolean isLWSP(char c) {
699 return c == '\r' || c == '\n' || c == ' ' || c == '\t';
702 //////////////////////////////////////////////////////////////////////////
704 * This method set Content-Disposition: with RFC2231 encoding. It is
705 * required JavaMail1.2.
708 * Part#setFileName()のマルチバイト対応版です。 JavaMail1.2でなければコンパイルできません
710 public static void setFileName(Part part, String filename, String charset, String lang)
711 throws MessagingException {
712 // Set the Content-Disposition "filename" parameter
713 ContentDisposition disposition;
714 String[] strings = part.getHeader("Content-Disposition");
715 if (strings == null || strings.length < 1) {
716 disposition = new ContentDisposition(Part.ATTACHMENT);
718 disposition = new ContentDisposition(strings[0]);
719 disposition.getParameterList().remove("filename");
721 part.setHeader("Content-Disposition", disposition.toString()
722 + encodeParameter("filename", filename, charset, lang));
724 strings = part.getHeader("Content-Type");
725 if (strings == null || strings.length < 1) {
726 cType = new ContentType(part.getDataHandler().getContentType());
728 cType = new ContentType(strings[0]);
731 // I want to public the MimeUtility#doEncode()!!!
732 String mimeString = MimeUtility.encodeWord(filename, charset, "B");
734 StringBuilder sb = new StringBuilder();
736 while ((i = mimeString.indexOf('\r')) != -1) {
737 sb.append(mimeString.substring(0, i));
738 mimeString = mimeString.substring(i + 2);
740 sb.append(mimeString);
741 cType.setParameter("name", new String(sb));
742 } catch (UnsupportedEncodingException e) {
743 throw new MessagingException("Encoding error", e);
745 part.setHeader("Content-Type", cType.toString());
749 * This method encodes the parameter.
751 * But most MUA cannot decode the encoded parameters by this method. <BR>
752 * I recommend using the "Content-Type:"'s name parameter both.
756 * ヘッダのパラメタ部のエンコードを行います。
758 * 現状は受信できないものが多いのでこのメソッドだけでは使えません。 <BR>
759 * Content-Disposition:のfilenameのみに使用し、さらに Content-Type:のnameにMIME
760 * encodingでの記述も行うのが妥当でしょう。 <BR>
761 * パラメタは必ず行頭から始まるものとします。 (ヘッダの開始行から折り返された位置を開始位置とします)
764 * foldingの方針はascii/non ascii境界のみをチェックします。 現状は連続するascii/non
765 * asciiの長さのチェックは現状行っていません。 (エンコード後のバイト数でチェックしなければならないのでかなり面倒)
768 * @param value エンコード対象のパラメタ値
769 * @param encoding 文字エンコーディング
771 * @return エンコード済み文字列 ";\r\n name*0*=ISO-8859-2''・・・;\r\n name*1*=・・"
773 // 1.全体をエンコードして長かったら半分に切ってエンコードを繰り返す
774 public static String encodeParameter(String name, String value, String encoding, String lang) {
775 StringBuilder result = new StringBuilder();
776 StringBuilder encodedPart = new StringBuilder();
777 boolean needWriteCES = !isAllAscii(value);
778 boolean CESWasWritten = false;
780 boolean needFolding = false;
783 while (value.length() > 0) {
784 // index of boundary of ascii/non ascii
786 boolean isAscii = value.charAt(0) < 0x80;
787 for (lastIndex = 1; lastIndex < value.length(); lastIndex++) {
788 if (value.charAt(lastIndex) < 0x80) {
796 if (lastIndex != value.length())
798 RETRY: while (true) {
799 encodedPart.setLength(0);
800 String target = value.substring(0, lastIndex);
804 bytes = target.getBytes("us-ascii");
806 bytes = target.getBytes(encoding);
808 } catch (UnsupportedEncodingException e) {
809 bytes = target.getBytes(); // use default encoding
810 encoding = MimeUtility.mimeCharset(MimeUtility.getDefaultJavaCharset());
814 column = name.length() + 7; // size of " " and "*nn*=" and ";"
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 // 2001/09/01 しかるべき文字が符号化されない問題修正
821 // attribute-char(符号化しなくてもよい文字)の定義は
822 // <any (US-ASCII) CHAR except SPACE, CTLs,
823 // "*", "'", "%", or tspecials>
824 // だが、ややこしいので英数字のみとしておく
825 // "$.!"はおまけ^^。エンコード時は大して意識はいらない
826 encodedPart.append((char)bytes[i]);
830 encodedPart.append('%');
831 String hex = Integer.toString(bytes[i] & 0xff, 16);
832 if (hex.length() == 1) {
833 encodedPart.append('0');
835 encodedPart.append(hex);
844 result.append(";\r\n ").append(name);
846 result.append('*').append(sequenceNo);
849 if (!CESWasWritten && needWriteCES) {
851 CESWasWritten = true;
852 result.append(encoding).append('\'');
856 } else if (encoded) {
859 * 本当にcharacter encodingは先頭パートに書かないとだめなのか? if (encoded) {
860 * result.append("*="); if (!CESWasWritten && needWriteCES) {
861 * CESWasWritten = true;
862 * result.append(encoding).append('\''); if (lang != null)
863 * result.append(lang); result.append('\''); }
868 result.append(new String(encodedPart));
869 value = value.substring(lastIndex);
873 return new String(result);
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) { // non-ascii
886 //////////////////////////////////////////////////////////////////////////
888 * This method decode the RFC2231 encoded filename parameter instead of
889 * Part#getFileName().
892 * Part#getFileName()のマルチバイト対応版です。
894 public static String getFileName(Part part) throws MessagingException {
895 String[] disposition = part.getHeader("Content-Disposition");
896 // A patch by YOSI (Thanx)
897 // http://www.sk-jp.com/cgibin/treebbs.cgi?kako=1&all=227&s=227
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);
910 static class Encoding {
912 String encoding = "us-ascii";
918 * This method decodes the parameter which be encoded (folded) by RFC2231
921 * The parameter's order should be considered.
925 * ヘッダのパラメタ部のデコードを行います。
927 * RFC2231形式でfolding(分割)されたパラメタを結合し、デコードします。
928 * 尚、RFC2231にはパラメタの順番に依存するなと書かれていますが、 それを実装すると大変面倒(一度分割された全てのパートを
929 * 保持してソートしなければならない)なので、 シーケンス番号に関係なく(0から)順番に 並んでいるものとみなして処理することにします。
931 * @param header ヘッダの値全体
932 * @param name 取得したいパラメタ名
933 * @return デコード済み文字列 (パラメタが存在しない場合は null)
935 public static String getParameter(String header, String name) throws ParseException {
938 // 本来これは不要。日本固有のデコード処理です。
939 // 2001/07/22 書籍版では"あ.txt"の生JISパラメタ値がデコードできない
940 // これは、ISO-2022-JPバイト列のままHeaderTokenizerにかけると、
941 // "あ"のバイトシーケンスに含まれる0x22がダブルクォートと
943 // JIS/Shift_JISの生バイトと思われるもののデコードを先に行う事で回避
944 header = decodeParameterSpciallyJapanese(header);
945 HeaderTokenizer tokenizer = new HeaderTokenizer(header, ";=\t ", true);
946 HeaderTokenizer.Token token;
947 StringBuilder sb = new StringBuilder();
948 // It is specified in first encoded-part.
949 Encoding encoding = new Encoding();
954 token = tokenizer.next();
955 if (token.getType() == HeaderTokenizer.Token.EOF)
957 if (token.getType() != ';')
959 token = tokenizer.next();
961 n = token.getValue();
962 token = tokenizer.next();
963 if (token.getType() != '=') {
964 throw new ParseException("Illegal token : " + token.getValue());
966 token = tokenizer.next();
968 v = token.getValue();
969 if (n.equalsIgnoreCase(name)) {
970 // It is not divided and is not encoded.
973 int index = name.length();
974 if (!n.startsWith(name) || n.charAt(index) != '*') {
978 // be folded, or be encoded
979 int lastIndex = n.length() - 1;
980 if (n.charAt(lastIndex) == '*') {
981 // http://www.sk-jp.com/cgibin/treebbs.cgi?all=399&s=399
982 if (index == lastIndex || n.charAt(index + 1) == '0') {
983 // decode as initial-section
984 sb.append(decodeRFC2231(v, encoding, true));
986 // decode as other-sections
987 sb.append(decodeRFC2231(v, encoding, false));
992 if (index == lastIndex) {
997 if (sb.length() == 0)
999 return new String(sb);
1000 } catch (UnsupportedEncodingException e) {
1001 throw new ParseException(e.toString());
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());
1012 // "lang" tag is ignored...
1013 private static String decodeRFC2231(String s, Encoding encoding, boolean isInitialSection)
1014 throws ParseException,
1015 UnsupportedEncodingException {
1016 StringBuilder sb = new StringBuilder();
1018 if (isInitialSection) {
1019 int work = s.indexOf('\'');
1021 encoding.encoding = s.substring(0, work);
1023 i = s.indexOf('\'', work);
1025 throw new ParseException("lang tag area was missing.");
1027 encoding.lang = s.substring(work, i);
1032 for (; i < s.length(); i++) {
1033 if (s.charAt(i) == '%') {
1034 sb.append((char)Integer.parseInt(s.substring(i + 1, i + 3), 16));
1038 sb.append(s.charAt(i));
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.");
1047 private static String decodeParameterSpciallyJapanese(String s) throws ParseException {
1049 // decode by character encoding.
1050 // if string are all ASCII, it is not translated.
1051 s = new String(s.getBytes("ISO-8859-1"), "JISAutoDetect");
1052 // decode by RFC2047.
1053 // if string doesn't contain encoded-word, it is not translated.
1054 return decodeText(s);
1055 } catch (UnsupportedEncodingException e) {
1056 throw new ParseException("Unsupported Encoding. " + e.getMessage());
1060 private MailUtility() {}