/*
 * @(#) $Id: MailUtility.java,v 1.8 2001/03/04 13:32:57 shin Exp $
 * $Revision: 1.8 $
 * Copyright (c) 2000 Shin Kinoshita All Rights Reserved.
 */
package com.sk_jp.mail;

import java.util.Map;
import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.io.*;
import javax.mail.*;
import javax.mail.internet.*;

import com.sk_jp.text.Formatter;
import com.sk_jp.text.TextFormatter;
import com.sk_jp.text.EntityRefEncoder;
import com.sk_jp.util.StringValues;

/**
 * JavaMailのサポートクラスです。
 * <P>
 * 主にヘッダに対するさまざまな加工機能を提供します。
 * </P>
 * @author Shin
 * @version $Revision: 1.8 $ $Date: 2001/03/04 13:32:57 $
 */
public class MailUtility {
    /** get comma separated E-Mail addresses. */
    public static String getMailAddresses(InternetAddress[] addresses) {
        if (addresses == null) return null;
        StringValues buf = new StringValues();
        for (int i = 0; i < addresses.length; i++) {
            buf.add(addresses[i].getAddress());
        }
        return buf.getString();
    }

    /** get comma separated personal names. */
    public static String getPersonalNames(InternetAddress[] addresses) {
        if (addresses == null) return null;
        StringValues buf = new StringValues();
        String name;
        for (int i = 0; i < addresses.length; i++) {
            try {
                name = decodeText(unfold(addresses[i].getPersonal()));
            } catch (ParseException e) {
                name = unfold(addresses[i].getPersonal());
            }
            if (name == null) {
                name = addresses[i].toString();
            }
            buf.add(name);
        }
        return buf.getString();
    }

    public static String getAddressesHTML(InternetAddress[] addresses) {
        if (addresses == null) return null;
        StringValues buf = new StringValues();
        StringBuffer href = new StringBuffer();
        String name;

        for (int i = 0; i < addresses.length; i++) {
            href.append("<a href=\"mailto:");
            href.append(addresses[i].getAddress());
            href.append("\">");
            name = addresses[i].getPersonal();
            if (name != null) {
                try {
                    name = decodeText(name);
                } catch (ParseException e) {
                }
            }
            if (name == null) {
                name = addresses[i].toString();
            }
            href.append(EntityRefEncoder.encode(name));
            href.append("</a>");
            buf.add(new String(href));
            href.setLength(0);
        }
        return buf.getString();
    }

    /** get the Content-Transfer-Encoding: header value. */
    public static String getTransferEncoding(byte[] b) {
        int nonAscii = 0;
        for (int i = 0; i < b.length; i++) {
            if (b[i] < 0) {
                nonAscii++;
            }
        }
        if (nonAscii == 0) return "7bit";
        if (nonAscii < b.length - nonAscii) return "quoted-printable";
        return "base64";
    }

    /**
     * パートを保有する親Messageオブジェクトを返します。
     * @param part パート
     * @return ツリー構造の最上位にあたるメッセージオブジェクト
     */
    public static Message getParentMessage(Part part) {
        Part current = part;
        Multipart mp;
        while (!(current instanceof Message)) {
            mp = ((BodyPart)current).getParent();
            if (mp == null) return null; // Should it throw exception?
            current = mp.getParent();
            if (current == null) return null; // Should it throw exception?
        }
        return (Message)current;
    }

    //////////////////////////////////////////////////////////////////////////
    // note: JavaMail1.2 later
    private static MailDateFormat mailDateFormat = new MailDateFormat();
    /**
     * Date構文の誤った"JST"タイムゾーンの補正を行います。
     * <P>
     * JavaMailは"JST"と記述されるタイムゾーンを解釈しません。
     * ここは本来"+0900"でなければならないところです。<BR>
     * 仕方がないので" JST"が含まれる文字列の場合は"+0900"を補完して
     * MailDateFormat#parse()を通すようなparse()のラッパを用意します。
     * </P><P>
     * この実装は一時回避的なものであり、完全なものではありません。
     * </P>
     */
    public static Date parseDate(String rfc822DateString) {
        if (rfc822DateString == null) {
            return null;
        }
        try {
            if (rfc822DateString.indexOf(" JST") == -1 ||
                rfc822DateString.indexOf('+') > 0) {
                synchronized (mailDateFormat) {
                    return mailDateFormat.parse(rfc822DateString);
                }
            }
            // correct the pseudo header
            StringBuffer buf = new StringBuffer(
                    rfc822DateString.substring(
                        0, rfc822DateString.indexOf("JST")));
            buf.append("+0900");
            synchronized (mailDateFormat) {
                return mailDateFormat.parse(new String(buf));
            }
        } catch (java.text.ParseException e) {
            return null;
        }
    }

    //////////////////////////////////////////////////////////////////////////
    /**
     * Subject:に"Re: "を付加します。
     * <P>
     * ある程度寛容に"Re: "に近い文字列と"[hoge]"を取り除きます。<BR>
     * ただし、意図しない部分が消されてしまう事もあり得ます。<BR>
     * JavaMailのreply()では"Re: "がエンコードされていた場合に
     * 正しく"Re: "を取り除いてくれません。
     * </P>
     */
    public static String createReplySubject(String src) {
        if (src == null || src.length() == 0) {
            return "Re: (no subject)";
        }
        String work = src;
        if (work.charAt(0) == '[' && work.indexOf(']') > 0) {
            work = work.substring(
                    indexOfNonLWSP(work, work.indexOf(']') + 1, false));
        }
        if (work.length() > 3 &&
                "Re:".equalsIgnoreCase(work.substring(0, 3))) {
            work = work.substring(
                    indexOfNonLWSP(work, 3, false));
        }
        return "Re: " + work;
    }

    /**
     * 「返信」時の引用された本文を生成します。
     * 引用のヘッダ部及び引用記号としてデフォルトのパターンが用いられます。
     */
    public static String createReplyContent(MimeMessage src) {
        return createReplyContent(
                src,
                "In article @##message_id@\r\n@##fromname@ wrote:\r\n",
                ">", -1);
    }

    /**
     * 「返信」時の引用された本文を生成します。
     * 引用のヘッダ部及び引用記号として指定されたパターンが用いられます。
     * quoteTemplateにはTextFormatterで利用可能なキーを指定できます。
     * 使用できる置換キーはMapByMessageクラスの仕様によります。
     */
    public static String createReplyContent(MimeMessage src,
                                            String quoteTemplate,
                                            String quoteSymbol,
                                            int maxLength) {
        if (maxLength == 0) {
            maxLength = Integer.MAX_VALUE;
        } else if (maxLength < 0) {
            maxLength = 76;
        }
        final Map msgMap = new MapByMessage(src);

        StringBuffer buf = new StringBuffer();

        Formatter formatter = new TextFormatter();
        formatter.setSource(quoteTemplate);
        buf.append(formatter.format(msgMap));

        String sourceContent = (String)msgMap.get("content");
        if (sourceContent == null) {
            return new String(buf);
        }
        BufferedReader r = new BufferedReader(new StringReader(sourceContent));
        String line;
        maxLength -= quoteSymbol.length();

        try {
            while ((line = r.readLine()) != null) {
                buf.append(quoteSymbol);
                while (line.length() > maxLength) {
                    buf.append(line.substring(0, maxLength)).append("\r\n");
                    line = line.substring(maxLength);
                    buf.append(quoteSymbol);
                }
                buf.append(line).append("\r\n");
            }
        } catch (IOException e) {
            throw new InternalError();
        }
        return new String(buf);
    }

    //////////////////////////////////////////////////////////////////////////
    /**
     * 入力されたアドレスをInternetAddress形式に変換します。
     * <p>
     * "名無し君<abc@example.com>(コメント)"等の文字列(エンコード無し)を
     * 渡されても、正しくpersonal文字列が設定されるようにします。<br>
     * InternetAddress#parse()はエンコード済みの文字列を前提にしているため、
     * このメソッドの目的には沿いません。
     * </p>
     * @param addresses メイルアドレス文字列(カンマ区切り)
     *
     */
    public static InternetAddress[] parseAddresses(String addressesString)
                throws AddressException {
        return parseAddresses(addressesString, true);
    }
    public static InternetAddress[] parseAddresses(
                String addressesString,
                boolean strict)
                throws AddressException {
        if (addressesString == null) return null;
        try {
            InternetAddress[] addresses =
                    InternetAddress.parse(addressesString, strict);
            // correct personals
            for (int i = 0; i < addresses.length; i++) {
                addresses[i].setPersonal(
                        addresses[i].getPersonal(), "ISO-2022-JP");
            }
            return addresses;
        } catch (UnsupportedEncodingException e) {
            throw new InternalError(e.toString());
        }
    }
// InternetAddress.parse(
//          encodeText(addressesString, "ISO-2022-JP", "B"), strict);
// で良さそうなものだが、これでは・・たしかなんか問題があったはず。

    //////////////////////////////////////////////////////////////////////////
    /**
     * header valueの unfolding を行います。
     */
    public static String unfold(String source) {
        if (source == null) return null;

        StringBuffer buf = new StringBuffer();
        boolean skip = false;
        char c;
        // <CRLF>シーケンスを前提とするならindexOf()で十分ですが、
        // 念のためCR、LFいずれも許容します。
        for (int i = 0; i < source.length(); i++) {
            c = source.charAt(i);
            if (skip) {
                if (isLWSP(c)) {
                    continue;
                }
                skip = false;
            }
            if (c != '\r' && c != '\n') {
                buf.append(c);
            } else {
                buf.append(' ');
                skip = true;
            }
        }
        return new String(buf);
    }

    /**
     * header valueの folding を行います。
     * <P>
     * white spaceをfolding対象にします。<BR>
     * 76bytesを超えないwhite space位置に<CRLF>を挿入します。
     * </P><P>
     * 注:quoteを無視しますので、structured fieldでは不都合が
     * 発生する可能性があります。
     * </P>
     * @param used ヘッダの':'までの文字数。76 - usedが最初のfolding候補桁
     * @return foldingされた(<CRLF>SPACEが挿入された)文字列
     */
    public static String fold(String source, int used) {
        if (source == null) return null;

        StringBuffer buf = new StringBuffer();
        String work = source;
        int lineBreakIndex;

        while (work.length() > 76) {
            lineBreakIndex = work.lastIndexOf(' ', 76);
            if (lineBreakIndex == -1) break;

            buf.append(work.substring(0, lineBreakIndex));
            buf.append("\r\n");
            work = work.substring(lineBreakIndex);
        }
        buf.append(work);
        return new String(buf);
    }

    /**
     * header valueの folding を行います。
     * <P>
     * delimiterをfolding対象にします。<BR>
     * (ただし実装が面倒なので今はdelimiterは先頭の1characterのみ)<BR>
     * delimiterで指定された文字の次の文字が次行のSPACEの後に続きます。
     * これは即ち、その位置にもともと空白がなくても1つの空白が挿入される
     * ことを意味します。元もとその位置に空白が合った場合は
     * その空白を活かし、新たな空白が挿入されることはありません。
     * </P><P>
     * 注:quoteを無視しますので、structured fieldでは不都合が
     * 発生する可能性があります。
     * </P>
     * @param delimiter foldingしても良い区切り文字。nullは空白と等価。
     * @param used ヘッダの':'までの文字数。76 - usedが最初のfolding候補桁
     * @return foldingされた(<CRLF>SPACEが挿入された)文字列
     */
    public static String fold(String source, String delimiter, int used) {
        if (source == null) return null;
        if (delimiter == null || delimiter.length() == 0) {
            delimiter = " ";
        }

        StringBuffer buf = new StringBuffer();
        String work = source;
        int breakIndex;

        while (work.length() > 76) {
            // fixme
            breakIndex = work.lastIndexOf(delimiter.charAt(0), 76);
            if (breakIndex == -1) break;
            if (delimiter.charAt(0) != ' ') breakIndex++;

            buf.append(work.substring(0, breakIndex));
            buf.append("\r\n");
            work = work.substring(breakIndex);
        }
        buf.append(work);
        return new String(buf);
    }

    //////////////////////////////////////////////////////////////////////////
    /**
     * ヘッダ内の文字列をデコードします。
     * <p>
     * MimeUtilityの制約を緩めて日本で流通するエンコード形式に対応。
     * 本来は、encoded-wordとnon-encoded-wordの間にはlinear-white-spaceが必要
     * なのですが、空白が無い場所でエンコードするタコメイラが多いので。
     * </p><p>
     * JISコードをエンコード無しで記述するタコメイラもあります。<br>
     * ソースにESCが含まれていたら生JISと見なします。
     * </p><p>
     * =?utf-8?Q?・・・JISコード・・?=なんてさらにタコなメイラも。<br>
     * 試しにデコード後にまだESCが残ってたらISO-2022-JPと見なすことにします。
     * </p><p>
     * 日本語に特化してますねえ・・・。
     * </p>
     * @param source encoded text
     * @return decoded text
     */
    public static String decodeText(String source) throws ParseException {
        if (source == null) return null;

        // specially for Japanese
        if (source.indexOf('\u001b') > 0) {
            // ISO-2022-JP
            try {
                return new String(
                        source.getBytes("ISO-8859-1"),
                        "ISO-2022-JP");
            } catch (UnsupportedEncodingException e) {
                throw new InternalError();
            }
        }

        // first, decode by JavaMail.
/*
// 日本語をデコードする上で問題があるので、encoded-wordの切り出しはすべて独自に
// Netscapeなどは"()."等の文字でencoded-wordを切ってしまうが、JavaMailは
// このときencoded-wordの終わりを判定できず、一部の文字を欠落させてしまう。
        try {
            source = MimeUtility.decodeText(source);
        } catch (UnsupportedEncodingException e) {
            // do nothing
        }
*/
        int startIndex;
        int endIndex = 0;
        String encodedWord;
        StringBuffer buf = new StringBuffer();

        while (true) {
            startIndex = source.indexOf("=?", endIndex);
            if (startIndex == -1) {
                buf.append(source.substring(endIndex));
                break;
            } else if (startIndex > endIndex) {
                String work = source.substring(endIndex, startIndex);
                if (indexOfNonLWSP(work, 0, false) > -1) {
                    buf.append(work);
                }
                // encoded-word同士の間のLWSPは削除
            }
            // skip "=?..?..?"
            // because In the case of "Q" encoding,
            // it exists that a word is the case of "=?..?Q?=1B...?=".
            endIndex = source.indexOf('?', startIndex + 2);
            if (endIndex == -1) {
                buf.append(source.substring(startIndex));
                break;
            }
            endIndex = source.indexOf('?', endIndex + 1);
            if (endIndex == -1) {
                buf.append(source.substring(startIndex));
                break;
            }
            endIndex = source.indexOf("?=", endIndex + 1);
            if (endIndex == -1) {
                buf.append(source.substring(startIndex));
                break;
            }
            endIndex += 2;

            encodedWord = source.substring(startIndex, endIndex);
            try {
                buf.append(MimeUtility.decodeWord(encodedWord));
            } catch (UnsupportedEncodingException ex) {
                buf.append(encodedWord);
            }
        }
        String decodedText = new String(buf);

        if (decodedText.indexOf('\u001b') > 0) {
            try {
                return new String(
                        decodedText.getBytes("ISO-8859-1"),
                        "ISO-2022-JP");
            } catch (UnsupportedEncodingException e) {
                throw new InternalError();
            }
        }
        return decodedText;
    }

    /**
     * 文字列をエンコードします。
     * <p>
     * MimeUtility(強いてはMimeMessage等も)では、1字でも非ASCII文字が含まれる
     * と文字列全体をエンコードしてしまいます。<br>
     * このメソッドでは空白で区切られた範囲だけをエンコードします。<br>
     * Subjectの"Re: "等がエンコードされていると、この文字列でIn-Reply-To:
     * References:の代わりにスレッドを形成しようとしても失敗することになる
     * ため、こちらのエンコード方式を用いたがる人もいるかもしれません・・。
     * </p><p>
     * 方針は、ASCII部に前後の空白一つを含ませ、それ以外は空白も含めて全て
     * encoded-wordとします。
     * </p>
     * @param source text
     * @return encoded text
     */
    public static String encodeText(String source,
                                    String charset, String encoding)
                throws UnsupportedEncodingException {
        int boundaryIndex;
        int startIndex;
        int endIndex = 0;
        int lastLWSPIndex;
        String encodeTargetText;
        StringBuffer buf = new StringBuffer();

        while (true) {
            boundaryIndex = indexOfNonAscii(source, endIndex);
            if (boundaryIndex == -1) {
                buf.append(source.substring(endIndex));
                return new String(buf);
            }
            // any LWSP has taken.
            lastLWSPIndex = indexOfLWSP(source, boundaryIndex, true, '(');
            startIndex    = indexOfNonLWSP(source, lastLWSPIndex, true) + 1;
            if (source.charAt(startIndex) == '(') {
                startIndex++;
            }
            startIndex = (endIndex > startIndex) ? endIndex : startIndex;
            if (startIndex > endIndex) {
                // ASCII part
                // foldingしない場合はASCII part末尾に空白を含める必要あり
                // startIndex++;
                buf.append("\r\n ");
                buf.append(source.substring(endIndex, startIndex));
                // JavaMailはencodeWord内でfoldingするけどそれはencodedWord
                // に対してのみ。ヘッダそのものに対するfoldingはしてくれない。
                buf.append("\r\n ");
                startIndex++;
            } else if (endIndex > 0) {
                // prev is encoded-word
                buf.append("\r\n ");
            }
            // any LWSP has taken.
            endIndex = indexOfNonLWSP(source, boundaryIndex, false);
            endIndex = indexOfLWSP(source, endIndex, false, ')');
            endIndex = indexOfNonLWSP(source, endIndex, false);
            if (endIndex < 0) {
                endIndex = source.length();
            } else if (source.charAt(endIndex) != ')') {
                endIndex--;
            }

            encodeTargetText = source.substring(startIndex, endIndex);
            buf.append(MimeUtility.encodeWord(
                    encodeTargetText, charset, encoding));
        }
    }

    /**
     * 指定位置から最初に見つかった非ASCII文字のIndexを返します。
     * @param source 検索する文字列
     * @param startIndex 検索開始位置
     * @return 検出した非ASCII文字Index。見つからなければ-1。
     */
    public static int indexOfNonAscii(String source, int startIndex) {
        for (int i = startIndex; i < source.length(); i++) {
            if (source.charAt(i) > 0x7f) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 指定位置から最初に見つかったLWSP以外の文字のIndexを返します。
     * @param source 検索する文字列
     * @param startIndex 検索開始位置
     * @param decrease trueで後方検索
     * @return 検出した非ASCII文字Index。見つからなければ-1。
     */
    public static int indexOfNonLWSP(String source, int startIndex,
                                     boolean decrease) {
        char c;
        int inc = 1;
        if (decrease) inc = -1;

        for (int i = startIndex; i >= 0 && i < source.length(); i += inc) {
            c = source.charAt(i);
            if (!isLWSP(c)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 指定位置から最初に見つかったLWSPのIndexを返します。
     * @param source 検索する文字列
     * @param startIndex 検索開始位置
     * @param decrease trueで後方検索
     * @param additionalDelimiter LWSP以外に区切りとみなす文字(1字のみ)
     * @return 検出した非ASCII文字Index。見つからなければ-1。
     */
    public static int indexOfLWSP(String source, int startIndex,
                                  boolean decrease, char additionalDelimiter) {
        char c;
        int inc = 1;
        if (decrease) inc = -1;

        for (int i = startIndex; i >= 0 && i < source.length(); i += inc) {
            c = source.charAt(i);
            if (isLWSP(c) || c == additionalDelimiter) {
                return i;
            }
        }
        return -1;
    }

    public static boolean isLWSP(char c) {
        return c == '\r' || c == '\n' || c == ' ' || c == '\t';
    }


    //////////////////////////////////////////////////////////////////////////
    /**
     * This method set Content-Disposition: with RFC2231 encoding.
     * It is required JavaMail1.2.
     */
    /**
     * Part#setFileName()のマルチバイト対応版です。
     * JavaMail1.2でなければコンパイルできません
     */
    public static void setFileName(Part part, String filename,
                                   String charset, String lang)
                throws MessagingException {
        // Set the Content-Disposition "filename" parameter
        ContentDisposition disposition;
        String[] strings = part.getHeader("Content-Disposition");
        if (strings == null || strings.length < 1) {
            disposition = new ContentDisposition(Part.ATTACHMENT);
        } else {
            disposition = new ContentDisposition(strings[0]);
            disposition.getParameterList().remove("filename");
        }

        part.setHeader("Content-Disposition",
                disposition.toString() +
                encodeParameter("filename", filename, charset, lang));

        ContentType cType;
        strings = part.getHeader("Content-Type");
        if (strings == null || strings.length < 1) {
            cType = new ContentType(part.getDataHandler().getContentType());
        } else {
            cType = new ContentType(strings[0]);
        }

        try {
            // I want to public the MimeUtility#doEncode()!!!
            String mimeString = MimeUtility.encodeWord(filename, charset, "B");
            // cut <CRLF>...
            StringBuffer sb = new StringBuffer();
            int i;
            while ((i = mimeString.indexOf('\r')) != -1) {
                sb.append(mimeString.substring(0, i));
                mimeString = mimeString.substring(i + 2);
            }
            sb.append(mimeString);

            cType.setParameter("name", new String(sb));
        } catch (UnsupportedEncodingException e) {
            throw new MessagingException("Encoding error", e);
        }
        part.setHeader("Content-Type", cType.toString());
    }

    /**
     * This method encodes the parameter.
     * <P>
     * But most MUA cannot decode the encoded parameters by this method.<BR>
     * I recommend using the "Content-Type:"'s name parameter both.
     * </P>
     */
    /**
     * ヘッダのパラメタ部のエンコードを行います。
     * <P>
     * 現状は受信できないものが多いのでこのメソッドだけでは使えません。<BR>
     * Content-Disposition:のfilenameのみに使用し、さらに
     * Content-Type:のnameにMIME encodingでの記述も行うのが妥当でしょう。<BR>
     * パラメタは必ず行頭から始まるものとします。
     * (ヘッダの開始行から折り返された位置を開始位置とします)
     * </P><P>
     * foldingの方針はascii/non ascii境界のみをチェックします。
     * 現状は連続するascii/non asciiの長さのチェックは現状行っていません。
     * (エンコード後のバイト数でチェックしなければならないのでかなり面倒)
     * </P>
     * @param name パラメタ名
     * @param value エンコード対象のパラメタ値
     * @param encoding 文字エンコーディング
     * @param lang 言語指定子
     * @return エンコード済み文字列
     *         ";\r\n name*0*=ISO-8859-2''・・・;\r\n name*1*=・・"
     */
    // 1.全体をエンコードして長かったら半分に切ってエンコードを繰り返す
    public static String encodeParameter(String name, String value,
                                         String encoding, String lang) {
        StringBuffer result = new StringBuffer();
        StringBuffer encodedPart = new StringBuffer();

        boolean needWriteCES = !isAllAscii(value);
        boolean CESWasWritten = false;
        boolean encoded;
        boolean needFolding = false;
        int sequenceNo = 0;
        int column;

        while (value.length() > 0) {
            // index of boundary of ascii/non ascii
            int lastIndex;
            boolean isAscii = value.charAt(0) < 0x80;
            for (lastIndex = 1; lastIndex < value.length(); lastIndex++) {
                if (value.charAt(lastIndex) < 0x80) {
                    if (!isAscii) break;
                } else {
                    if (isAscii) break;
                }
            }
            if (lastIndex != value.length()) needFolding = true;

RETRY:      while (true) {
                encodedPart.setLength(0);
                String target = value.substring(0, lastIndex);

                byte[] bytes;
                try {
                    if (isAscii) {
                        bytes = target.getBytes("us-ascii");
                    } else {
                        bytes = target.getBytes(encoding);
                    }
                } catch (UnsupportedEncodingException e) {
                    bytes = target.getBytes(); // use default encoding
                    encoding = MimeUtility.mimeCharset(
                                MimeUtility.getDefaultJavaCharset());
                }

                encoded = false;
                // It is not strict.
                column = name.length() + 7; // size of " " and "*nn*=" and ";"

                for (int i = 0; i < bytes.length; i++) {
                    if (bytes[i] > ' ' && bytes[i] < 'z'
                        && HeaderTokenizer.MIME.indexOf((char)bytes[i]) < 0) {
                        encodedPart.append((char)bytes[i]);
                        column++;
                    } else {
                        encoded = true;
                        encodedPart.append('%');
                        String hex  = Integer.toString(bytes[i] & 0xff, 16);
                        if (hex.length() == 1) {
                            encodedPart.append('0');
                        }
                        encodedPart.append(hex);
                        column += 3;
                    }
                    if (column > 76) {
                        needFolding = true;
                        lastIndex /= 2;
                        continue RETRY;
                    }
                }

                result.append(";\r\n ").append(name);
                if (needFolding) {
                    result.append('*').append(sequenceNo);
                    sequenceNo++;
                }
                if (!CESWasWritten && needWriteCES) {
                    result.append("*=");
                    CESWasWritten = true;
                    result.append(encoding).append('\'');
                    if (lang != null) result.append(lang);
                    result.append('\'');
                } else if (encoded) {
                    result.append("*=");
/* 本当にcharacter encodingは先頭パートに書かないとだめなのか?
                if (encoded) {
                    result.append("*=");
                    if (!CESWasWritten && needWriteCES) {
                        CESWasWritten = true;
                        result.append(encoding).append('\'');
                        if (lang != null) result.append(lang);
                        result.append('\'');
                    }
*/
                } else {
                    result.append('=');
                }
                result.append(new String(encodedPart));
                value = value.substring(lastIndex);
                break;
            }
        }
        return new String(result);
    }

    /** check if contains only ascii characters in text. */
    public static boolean isAllAscii(String text) {
        for (int i = 0; i < text.length(); i++) {
            if (text.charAt(i) > 0x7f) { // non-ascii
                return false;
            }
        }
        return true;
    }

    //////////////////////////////////////////////////////////////////////////
    /**
     * This method decode the RFC2231 encoded filename parameter
     * instead of Part#getFileName().
     */
    /**
     * Part#getFileName()のマルチバイト対応版です。
     */
    public static String getFileName(Part part) throws MessagingException {
        String[] disposition = part.getHeader("Content-Disposition");
        if (disposition == null || disposition.length < 1) {
            return null;
        }
        // 本来そのまま返すところだが日本固有のデコードを入れる。
        return decodeParameterSpciallyJapanese(
                getParameter(disposition[0], "filename"));
    }

    static class Encoding {
        String encoding = "us-ascii";
        String lang = "";
    }

    /**
     * This method decodes the parameter
     * which be encoded (folded) by RFC2231 method.
     * <P>
     * The parameter's order should be considered.
     * </P>
     */
    /**
     * ヘッダのパラメタ部のデコードを行います。
     * <P>
     * JavaMailはエンコードもデコードも行いません。<BR>
     * (実は密かにプラットフォームデフォルトエンコーディングでエンコードする)
     * エンコードは取り合えず現状最も多いRFC2047エンコーディングを行うとして
     * (最終的にはRFC2231形式とする必要あり)、デコードに関しては
     * 様々な形式をデコードできるようにしておきます。<BR>
     * 尚、RFC2231にはパラメタの順番に依存するなと書かれていますが、
     * それを実装すると大変面倒(一度分割された全てのパートを
     * 保持してソートしなければならない)なので、
     * シーケンス番号に関係なく(0から)順番に
     * 並んでいるものとみなして処理することにします。
     * </P>
     * @param header ヘッダの値全体
     * @param name 取得したいパラメタ名
     * @return デコード済み文字列
     */
    public static String getParameter(String header, String name) {

        HeaderTokenizer tokenizer =
                new HeaderTokenizer(header, HeaderTokenizer.MIME, true);
        HeaderTokenizer.Token token;
        StringBuffer sb = new StringBuffer();
        // It is specified in first encoded-part.
        Encoding encoding = new Encoding();

        String n;
        String v;

        try {
            while (true) {
                token = tokenizer.next();
                if (token.getType() == token.EOF) break;
                if (token.getType() != ';') continue;

                token = tokenizer.next();
                checkType(token);
                n = token.getValue();

                token = tokenizer.next();
                if (token.getType() != '=') {
                    throw new ParseException(
                            "Illegal token : " + token.getValue());
                }

                token = tokenizer.next();
                checkType(token);
                v = token.getValue();

                if (n.equalsIgnoreCase(name)) {
                    // It is not divided and is not encoded.
                    return v;
                }

                int index = name.length();

                if (!n.startsWith(name) || n.charAt(index) != '*') {
                    // another parameter
                    continue;
                }
                // be folded, or be encoded
                int lastIndex = n.length() - 1;
                if (n.charAt(lastIndex) == '*') {
                    sb.append(decodeRFC2231(v, encoding));
                } else {
                    sb.append(v);
                }
                if (index == lastIndex) {
                    // not folding
                    break;
                }
            }
            return new String(sb);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        throw new InternalError();
    }

    private static void checkType(HeaderTokenizer.Token token)
                throws ParseException {
        int t = token.getType();
        if (t != HeaderTokenizer.Token.ATOM &&
            t != HeaderTokenizer.Token.QUOTEDSTRING) {
            throw new ParseException("Illegal token : " + token.getValue());
        }
    }

    // "lang" tag is ignored...
    private static String decodeRFC2231(String s, Encoding encoding)
                throws ParseException, UnsupportedEncodingException {
        StringBuffer sb = new StringBuffer();
        int i = 0;

        int work = s.indexOf('\'');
        if (work > 0) {
            encoding.encoding = s.substring(0, work);
            work++;
            i = s.indexOf('\'', work);
            encoding.lang = s.substring(work, i);
            i++;
        }

        try {
            for (; i < s.length(); i++) {
                if (s.charAt(i) == '%') {
                    sb.append((char)Integer.parseInt(
                            s.substring(i + 1, i + 3), 16));
                    i += 2;
                    continue;
                }
                sb.append(s.charAt(i));
            }
            return new String(
                    new String(sb).getBytes("ISO-8859-1"),
                    encoding.encoding);
        } catch (IndexOutOfBoundsException e) {
            throw new ParseException(s + " :: this string were not decoded.");
        }
    }


    // 日本語向けデコード
    private static String decodeParameterSpciallyJapanese(String s)
                throws ParseException {
        try {
            boolean unicode = false;
            for (int i = 0; i < s.length(); i++) {
                if (s.charAt(i) > 0xff) { // Unicode
                    unicode = true;
                    break;
                }
            }
            if (!unicode) {
                // decode by character encoding.
                s = new String(s.getBytes("ISO-8859-1"), "JISAutoDetect");
            }
            // decode by RFC2047.
            // if variable s isn't encoded-word, it's ignored.
            return decodeText(s);
        } catch (UnsupportedEncodingException e) {
        }
        throw new ParseException("Unsupported Encoding");
    }


    private MailUtility() {}
}

