View Javadoc

    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 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; // 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 			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 			// 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 		StringBuffer buf = new StringBuffer();
    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 		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 		//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 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 				} // 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 		StringBuffer buf = new StringBuffer();
    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 			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 	// 1.全体をエンコードして長かったら半分に切ってエンコードを繰り返す
    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 			// 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 		StringBuffer sb = new StringBuffer();
    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 		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 			// 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");
    1057 	}
    1058
    1059 	private MailUtility() {}
    1060 }