OSDN Git Service

Migrated to jakarta namespace and bumped version number to 2.0.0
[spring-ext/ozacc-mail.git] / src / main / java / com / ozacc / mail / fetch / impl / sk_jp / MailUtility.java
1 /*
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.
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 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;
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                 StringBuilder href = new StringBuilder();
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; // Should it throw exception?
129                         current = mp.getParent();
130                         if (current == null)
131                                 return null; // Should it throw exception?
132                 }
133                 return (Message)current;
134         }
135
136         //////////////////////////////////////////////////////////////////////////
137         // note: JavaMail1.2 later
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                         // correct the pseudo header
162                         StringBuilder buf = new StringBuilder(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                         // correct personals
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         // InternetAddress.parse(
237         //          encodeText(addressesString, "ISO-2022-JP", "B"), strict);
238         // で良さそうなものだが、これでは・・たしかなんか問題があったはず。
239         //////////////////////////////////////////////////////////////////////////
240         /**
241          * header valueの unfolding を行います。 空白を厳密に扱うためには decodeText より先に呼び出す必要があります。
242          */
243         public static String unfold(String source) {
244                 if (source == null)
245                         return null;
246                 StringBuilder buf = new StringBuilder();
247                 boolean skip = false;
248                 char c;
249                 // <CRLF>シーケンスを前提とするならindexOf()で十分ですが、
250                 // 念のためCR、LFいずれも許容します。
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                 StringBuilder buf = new StringBuilder();
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                 //p.setText(content, "ISO-2022-JP");
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                 // specially for Japanese
362                 if (source.indexOf('\u001b') >= 0) {
363                         // ISO-2022-JP
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         // 日本語をデコードする上で問題があるので、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 {
388
389                 private String source;
390
391                 private String pooledCES;
392
393                 private byte[] pooledBytes;
394
395                 private StringBuilder 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 StringBuilder(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                                 } // encoded-word同士の間のLWSPは削除
416                                 parseWord();
417                         }
418                         sweepPooledBytes();
419                         buf.append(source.substring(pos));
420                 }
421
422                 // encoded-word があった場合、startIndex/endIndex をセットする
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                         // 本来は encoded-word 中に LWSP があってはいけないが
431                         // encoded-word の途中で folding してしまう sender がいるらしい
432                         // 以下をコメントにすることで encoded-word の誤認識の可能性も
433                         // 出てくるが、誤認識になる確率以上に前記のような illegal な
434                         // メッセージの方が多いのが実情のようだ。
435                         // thx > YOSI
436                         //int i = indexOfLWSP(source, startIndex + 2, false, (char)0);
437                         //if (i >= 0 && i < endIndex)
438                         //    return false;
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); // FIXME: check whether supported or not
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                                         // append bytes
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                                 // contains RuntimeException
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                 // 通常あり得ないが、LWSP を詰める
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                         // BASE64DecoderStream は正確にパディングされていないと
504                         // IOException になるので、無理やり矯正。
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         // "()"なんか無視してまとめて encode するようにすればすっきるするけど…。
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                 StringBuilder buf = new StringBuilder();
574                 while (true) {
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);
580                         }
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) {
588                                 // ASCII part
589                                 buf.append(source.substring(endIndex, startIndex));
590                                 // JavaMailはencodeWord内でfoldingするけどそれはencodedWord
591                                 // に対してのみ。ヘッダそのものに対するfoldingはしてくれない。
592                                 if (isLWSP(source.charAt(startIndex))) {
593                                         // folding により 空白一つが確保されるのでスキップ
594                                         buf.append("\r\n ");
595                                         startIndex++;
596                                         // なお、'('の場合は空白を入れないので folding しない
597                                 }
598                         }
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) {
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                                 // folding により 空白一つが確保される(予定)なので減らす
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                                 // folding により 空白一つが確保されるのでスキップ
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                 // 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);
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                         // I want to public the MimeUtility#doEncode()!!!
732                         String mimeString = MimeUtility.encodeWord(filename, charset, "B");
733                         // cut <CRLF>...
734                         StringBuilder sb = new StringBuilder();
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         // 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;
779                 boolean encoded;
780                 boolean needFolding = false;
781                 int sequenceNo = 0;
782                 int column;
783                 while (value.length() > 0) {
784                         // index of boundary of ascii/non ascii
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(); // use default encoding
810                                         encoding = MimeUtility.mimeCharset(MimeUtility.getDefaultJavaCharset());
811                                 }
812                                 encoded = false;
813                                 // It is not strict.
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]);
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                                          * 本当に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('\''); }
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) { // non-ascii
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                 // A patch by YOSI (Thanx)
897                 // http://www.sk-jp.com/cgibin/treebbs.cgi?kako=1&all=227&s=227
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                 // 2001/07/22 書籍版では"あ.txt"の生JISパラメタ値がデコードできない
940                 // これは、ISO-2022-JPバイト列のままHeaderTokenizerにかけると、
941                 // "あ"のバイトシーケンスに含まれる0x22がダブルクォートと
942                 // 解釈されるため。
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();
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                                         // It is not divided and is not encoded.
971                                         return v;
972                                 }
973                                 int index = name.length();
974                                 if (!n.startsWith(name) || n.charAt(index) != '*') {
975                                         // another parameter
976                                         continue;
977                                 }
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));
985                                         } else {
986                                                 // decode as other-sections
987                                                 sb.append(decodeRFC2231(v, encoding, false));
988                                         }
989                                 } else {
990                                         sb.append(v);
991                                 }
992                                 if (index == lastIndex) {
993                                         // not folding
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         // "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();
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                         // 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());
1057                 }
1058         }
1059
1060         private MailUtility() {}
1061 }