/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.mail;

import java.io.InputStream;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.io.IOException;

import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import javax.mail.MessagingException;
import com.sun.mail.util.BASE64EncoderStream;

import java.nio.charset.Charset;		// 5.5.2.6 (2012/05/25)

/**
 * MailCharset は、E-Mail 送信時のエンコードに応じた処理を行う為の、
 * インターフェースです。
 *
 * E-Mail で日本語を送信する場合、ISO-2022-JP(JISコード)化して、7bit で
 * エンコードして送信する必要がありますが、Windows系の特殊文字や、unicodeと
 * 文字のマッピングが異なる文字などが、文字化けします。
 * 対応方法としては、
 * 『１．Windows-31J + 8bit 送信』
 * 『２．ISO-2022-JP に独自変換 + 7bit 送信』
 * の方法があります。
 * 今回、この２つの方法について、それぞれサブクラス化を行い、処理できるように
 * したのが、このインターフェース、および、サブクラスです。
 *
 * 『１．Windows-31J + 8bit 送信』の方法は、通常の JavaMail API に準拠して
 * 処理を行う、Mail_Windows31J_Charset サブクラスで実装しています。
 * 古いメイラーおよび、古いメールサーバーではメール転送できない為、
 * この方式は、社内で使用する場合のみに、利用できますが、主としてWindows系の
 * 社内システムにおいては、こちらの方が、なにかとトラブルは少ないと思います。
 *
 * 『２．ISO-2022-JP に独自変換 + 7bit 送信』の実装は、
 * JAVA PRESS Vol.37 (http://www.gihyo.co.jp/magazines/javapress)の
 * 【特集1】 決定版！ サーバサイドJavaの日本語処理
 *  第3章：JavaMailの日本語処理プログラミング……木下信
 *“マルチプラットフォーム”な日本語メール送信術 完全解説
 * でのサンプルアプリケーション
 * http://www.gihyo.co.jp/book/2004/225371/download/toku1_3.zip
 * を、使用して、Mail_ISO2022JP_Charset サブクラスで実装しています。
 *
 * これらのサブクラスは、MailCharsetFactory ファクトリクラスより、作成されます。
 * その場合、引数のキャラクタセット名は、Windows-31J 、MS932 か、それ以外となっています。
 * それ以外が指定された場合は、ISO-2022-JP を使用します。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public interface MailCharset {

	/**
	 * テキストをセットします。
	 * Part#setText() の代わりにこちらを使うようにします。
	 *
	 * @param mimeMsg MimeMessage最大取り込み件数
	 * @param text    設定するテキスト
	 * @throws RuntimeException(MessagingException)
	 */
	void setTextContent( MimeMessage mimeMsg, String text ) ;

	/**
	 * 日本語を含むヘッダ用テキストを生成します。
	 * 変換結果は ASCII なので、これをそのまま setSubject や InternetAddress
	 * のパラメタとして使用してください。
	 *
	 * @param text    設定するテキスト
	 *
	 * @return	日本語を含むヘッダ用テキスト
	 * @throws RuntimeException(UnsupportedEncodingException)
	 */
	String encodeWord( String text ) ;

	/**
	 * 日本語を含むアドレスを生成します。
	 * personal に、日本語が含まれると想定しています。
	 * サブクラスで、日本語処理を行う場合の方法は、それぞれ異なります。
	 *
	 * @param address    アドレス部分
	 * @param personal   日本語の説明部分
	 *
	 * @return	日本語を含むアドレス
	 * @throws RuntimeException(UnsupportedEncodingException)
	 */
	InternetAddress getAddress( String address,String personal ) ;

	/**
	 * Content-Transfer-Encoding を指定する場合の ビット数を返します。
	 *
	 * Windows系は、8bit / ISO-2022-JP 系は、7bit になります。
	 *
	 * @return	ビット数
	 */
	String getBit() ;
}

/**
 * MailCharsetFactory は、MailCharset インターフェースを実装したサブクラスを
 * 作成する ファクトリクラスです。
 *
 * 引数のキャラクタセット名が、Windows-31J 、MS932 の場合は、
 * 『１．Windows-31J + 8bit 送信』 の実装である、Mail_Windows31J_Charset
 * サブクラスを返します。
 * それ以外が指定された場合は、ISO-2022-JP を使用して、『２．ISO-2022-JP に独自変換 + 7bit 送信』
 * の実装である、Mail_ISO2022JP_Charset サブクラスを返します。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
class MailCharsetFactory {

	/**
	 * インスタンスの生成を抑止します。
	 */
	private MailCharsetFactory() {
		// 何もありません。(PMD エラー回避)
	}

	/**
	 * キャラクタセットに応じた、MailCharset オブジェクトを返します。
	 *
	 * Windows-31J 、MS932 、Shift_JIS の場合は、Mail_Windows31J_Charset
	 * その他は、ISO-2022-JP として、Mail_ISO2022JP_Charset を返します。
	 *
	 * 注意：null の場合は、デフォルトではなく、Mail_ISO2022JP_Charset を返します。
	 *
	 * @param  charset キャラクタセット[Windows-31J/MS932/Shift_JIS/その他]
	 *
	 * @return MailCharset
	 */
	static MailCharset newInstance( final String charset ) {
		final MailCharset mcset;

		if( "MS932".equalsIgnoreCase( charset ) ||
			"Shift_JIS".equalsIgnoreCase( charset ) ||
			"Windows-31J".equalsIgnoreCase( charset ) ) {
				mcset = new Mail_Windows31J_Charset( charset );
		}
		else {
			mcset = new Mail_ISO2022JP_Charset();
		}
		return mcset ;
	}
}

/**
 * MailCharset インターフェースを実装した Windwos-31J エンコード時のサブクラスです。
 *
 * 『１．Windows-31J + 8bit 送信』 の実装です。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
class Mail_Windows31J_Charset implements MailCharset {
	private final String charset ;			// "Windows-31J" or "MS932"

	/**
	 * 引数に、エンコード方式を指定して、作成するコンストラクタです。
	 *
	 * @param charset String
	 */
	public Mail_Windows31J_Charset( final String charset ) {
		this.charset = charset;
	}

	/**
	 * テキストをセットします。
	 * Part#setText() の代わりにこちらを使うようにします。
	 *
	 * @param mimeMsg MimeMessage
	 * @param text    String
	 * @throws RuntimeException(MessagingException)
	 */
	public void setTextContent( final MimeMessage mimeMsg, final String text ) {
		try {
			mimeMsg.setText( text,charset );		// "text/plain" Content
		}
		catch( MessagingException ex ) {
			String errMsg = "指定のテキストをセットできません。"
								+ "text=" + text + " , charset=" + charset ;
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * 日本語を含むヘッダ用テキストを生成します。
	 * 変換結果は ASCII なので、これをそのまま setSubject や InternetAddress
	 * のパラメタとして使用してください。
	 *
	 * @param text    String
	 *
	 * @return	日本語を含むヘッダ用テキスト
	 * @throws RuntimeException(UnsupportedEncodingException)
	 */
	public String encodeWord( final String text ) {
		try {
			return MimeUtility.encodeText( text, charset, "B" );
		}
		catch( UnsupportedEncodingException ex ) {
			String errMsg = "指定のエンコードが出来ません。"
								+ "text=" + text + " , charset=" + charset ;
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * 日本語を含むアドレスを生成します。
	 * personal に、日本語が含まれると想定しています。
	 * サブクラスで、日本語処理を行う場合の方法は、それぞれ異なります。
	 *
	 * @param address    String
	 * @param personal   String
	 *
	 * @return InternetAddress
	 * @throws RuntimeException(UnsupportedEncodingException)
	 */
	public InternetAddress getAddress( final String address,final String personal ) {
		try {
			return new InternetAddress( address,personal,charset );
		}
		catch( UnsupportedEncodingException ex ) {
			String errMsg = "指定のエンコードが出来ません。"
								+ "address=" + address + " , charset=" + charset ;
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * Content-Transfer-Encoding を指定する場合の ビット数を返します。
	 *
	 * Windows系は、8bit / ISO-2022-JP 系は、7bit になります。
	 *
	 * @return	ビット数("8bit" 固定)
	 */
	public String getBit() {
		return "8bit" ;
	}
}

/**
 * MailCharset インターフェースを実装した ISO-2022-JP エンコード時のサブクラスです。
 *
 * 『２．ISO-2022-JP に独自変換 + 7bit 送信』 の実装です。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
class Mail_ISO2022JP_Charset implements MailCharset {

	/**
	 * プラットフォーム依存のデフォルトの Charset です。
	 * プラットフォーム依存性を考慮する場合、エンコード指定で作成しておく事をお勧めします。
	 *
	 * @og.rev 5.5.2.6 (2012/05/25) findbugs対応
	 */
	private static final Charset DEFAULT_CHARSET = Charset.defaultCharset() ;

	/**
	 * テキストをセットします。
	 * Part#setText() の代わりにこちらを使うようにします。
	 *
	 * @param mimeMsg MimeMessage
	 * @param text    String
	 * @throws RuntimeException(MessagingException)
	 */
	public void setTextContent( final MimeMessage mimeMsg, final String text ) {
		try {
			// mimeMsg.setText(text, "ISO-2022-JP");
			mimeMsg.setDataHandler(new DataHandler(new JISDataSource(text)));
		}
		catch( MessagingException ex ) {
			String errMsg = "指定のテキストをセットできません。"
								+ "text=" + text ;
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * 日本語を含むヘッダ用テキストを生成します。
	 * 変換結果は ASCII なので、これをそのまま setSubject や InternetAddress
	 * のパラメタとして使用してください。
	 *
	 * @param text    String
	 *
	 * @return	日本語を含むヘッダ用テキスト
	 * @throws RuntimeException(UnsupportedEncodingException)
	 */
	public String encodeWord( final String text ) {
		try {
			return "=?ISO-2022-JP?B?" +
				new String(
					BASE64EncoderStream.encode(
						CharCodeConverter.sjisToJis(
							UnicodeCorrecter.correctToCP932(text).getBytes("Windows-31J")
						)
					)
				,DEFAULT_CHARSET ) + "?=";		// 5.5.2.6 (2012/05/25) findbugs対応
		}
		catch( UnsupportedEncodingException ex ) {
			String errMsg = "指定のエンコードが出来ません。"
								+ "text=" + text + " , charset=Windows-31J" ;
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * 日本語を含むアドレスを生成します。
	 * personal に、日本語が含まれると想定しています。
	 * サブクラスで、日本語処理を行う場合の方法は、それぞれ異なります。
	 *
	 * @param address    String
	 * @param personal   String
	 *
	 * @return InternetAddress
	 * @throws RuntimeException(UnsupportedEncodingException)
	 */
	public InternetAddress getAddress( final String address,final String personal ) {
		try {
			return new InternetAddress( address,encodeWord( personal ) );
		}
		catch( UnsupportedEncodingException ex ) {
			String errMsg = "指定のエンコードが出来ません。"
								+ "address=" + address ;
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * Content-Transfer-Encoding を指定する場合の ビット数を返します。
	 *
	 * Windows系は、8bit / ISO-2022-JP 系は、7bit になります。
	 *
	 * @return	ビット数("7bit" 固定)
	 */
	public String getBit() {
		return "7bit" ;
	}
}

/**
 * テキストの本文を送信するための DataSource です。
 *
 * Windows-31J でバイトコードに変換した後、独自エンコードにて、
 * Shift-JIS ⇒ JIS 変換しています。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
class JISDataSource implements DataSource {
	private final byte[] data;

	public JISDataSource( final String str ) {
		try {
			data = CharCodeConverter.sjisToJis(
				UnicodeCorrecter.correctToCP932(str).getBytes("Windows-31J"));

		} catch (UnsupportedEncodingException e) {
			String errMsg = "Windows-31J でのエンコーディングが出来ません。" + str;
			throw new RuntimeException( errMsg,e );
		}
	}

	/**
	 * データの MIME タイプを文字列の形で返します。
	 * かならず有効なタイプを返すべきです。
	 * DataSource の実装がデータタイプを 決定できない場合は、
	 * getContentType は "application/octet-stream" を返すことを 提案します。
	 *
	 * @return	MIME タイプ
	 */
	public String getContentType() {
		return "text/plain; charset=ISO-2022-JP";
	}

	/**
	 * データを表す InputStream を返します。
	 * それができない場合は適切な例外をスローします。
	 *
	 * @return InputStream
	 * @throws IOException
	 */
	public InputStream getInputStream() throws IOException {
		return new ByteArrayInputStream( data );
	}

	/**
	 * データが書込可能なら OutputStream を返します。
	 * それができない場合は適切な例外をスローします。
	 *
	 * ※ このクラスでは実装されていません。
	 *
	 * @return OutputStream
	 * @throws IOException
	 */
	public OutputStream getOutputStream() throws IOException {
		String errMsg = "このクラスでは実装されていません。";
	//	throw new UnsupportedOperationException( errMsg );
		throw new IOException( errMsg );
	}

	/**
	 * このオブジェクトの '名前' を返します。
	 * この名前は下層のオブジェクトの性質によります。
	 * ファイルをカプセル化する DataSource なら オブジェクトの
	 * ファイル名を返すようにするかもしれません。
	 *
	 * @return	オブジェクトの名前
	 */
	public String getName() {
		return "JISDataSource";
	}
}

/**
 * 文字関係のコンバータです。
 * 一部コードのオリジナルは<a href="http://www-cms.phys.s.u-tokyo.ac.jp/~naoki/CIPINTRO/CCGI/kanjicod.html">Japanese Kanji Code</a>にて公開されているものです。
 * また、http://www.sk-jp.com/cgi-bin/treebbs.cgi?kako=1&all=644&s=681
 * にて YOSI さんが公開されたコードも参考にしています(というか実質同じです)。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
class CharCodeConverter {
	private static final byte[] SJIS_KANA;	// 5.1.9.0 (2010/09/01) public ⇒ private へ変更

	/**
	 * インスタンスの生成を抑止します。
	 */
	private CharCodeConverter() {
		// 何もありません。(PMD エラー回避)
	}

	static {
		try {
			// 全角への変換テーブル
			SJIS_KANA = "。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゛゜".getBytes("Shift_JIS");
		} catch( UnsupportedEncodingException ex ) {
			throw new RuntimeException( "CANT HAPPEN",ex );
		}
	}

	/**
	 * Shift_JIS エンコーディングスキームに基づくバイト列を
	 * ISO-2022-JP エンコーディングスキームに変換します。
	 * 「半角カナ」は対応する全角文字に変換します。
	 *
	 * @param sjisBytes byte[] エンコードするShift_JISバイト配列
	 *
	 * @return byte[] 変換後のISO-2022-JP(JIS)バイト配列(not null)
	 */
	public static byte[] sjisToJis( final byte[] sjisBytes ) {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		boolean nonAscii = false;
		int len = sjisBytes.length;
		for(int i = 0; i < len; i++ ) {
			if(sjisBytes[i] >= 0) {
				if(nonAscii) {
					nonAscii = false;
					out.write(0x1b);
					out.write('(');
					out.write('B');
				}
				out.write(sjisBytes[i]);
			} else {
				if(!nonAscii) {
					nonAscii = true;
					out.write(0x1b);
					out.write('$');
					out.write('B');
				}
				int bt = sjisBytes[i] & 0xff;
				if(bt >= 0xa1 && bt <= 0xdf) {
					// 半角カナは全角に変換
					int kanaIndex = (bt - 0xA1) * 2;
					sjisToJis(out, SJIS_KANA[kanaIndex], SJIS_KANA[kanaIndex + 1]);
				} else {
					i++;
					if(i == len) { break; }
					sjisToJis(out, sjisBytes[i - 1], sjisBytes[i]);
				}
			}
		}
		if(nonAscii) {
			out.write(0x1b);
			out.write('(');
			out.write('B');
		}
		return out.toByteArray();
	}

	/**
	 * １文字の２バイト Shift_JIS コードを JIS コードに変換して書き出します。
	 */
	private static void sjisToJis(
				final ByteArrayOutputStream out, final byte bhi, final byte blo) {
		int hi = (bhi << 1) & 0xFF;
		int lo = blo & 0xFF;
		if(lo < 0x9F) {
			if(hi < 0x3F) { hi += 0x1F; } else { hi -= 0x61; }
			if(lo > 0x7E) { lo -= 0x20; } else { lo -= 0x1F; }
		} else {
			if(hi < 0x3F) { hi += 0x20; } else { hi -= 0x60; }
			lo -= 0x7E;
		}
		out.write(hi);
		out.write(lo);
	}
}

/**
 * unicode と、JIS との文字コードの関係で、変換しています。
 *
 * 0x301c(&#x301c;) を、0xff5e(&#xff5e;) へ、
 * 0x2016(&#x2016;) を、0x2225(&#x2225;) へ、
 * 0x2212(&#x2212;) を、0xff0d(&#xff0d;) へ、
 * それぞれコード変換します。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
class UnicodeCorrecter {

	/**
	 * インスタンスの生成を抑止します。
	 */
	private UnicodeCorrecter() {
		// 何もありません。(PMD エラー回避)
	}

	/**
	 * Unicode 文字列の補正を行います。
	 * "MS932" コンバータでエンコードしようとした際に
	 * 正常に変換できない部分を補正します。
	 */
	public static String correctToCP932( final String str ) {
		String rtn = "";

		if( str != null ) {
			int cnt = str.length();
			StringBuilder buf = new StringBuilder( cnt );
			for(int i=0; i<cnt; i++) {
				buf.append(correctToCP932(str.charAt(i)));
			}
			rtn = buf.toString() ;
		}
		return rtn ;
	}

	/**
	 * キャラクタ単位に、Unicode 文字列の補正を行います。
	 *
	 * 風間殿のページを参考にしています。
	 * @see <a href="http://www.ingrid.org/java/i18n/encoding/ja-conv.html" target="_blank">
	 * http://www.ingrid.org/java/i18n/encoding/ja-conv.html</a>
	 */
	public static char correctToCP932( final char ch ) {
		char rtn = ch;

		switch (ch) {
	//		case 0x00a2:	return 0xffe0;		// ≪
	//		case 0x00a3:	return 0xffe1;		// ￣
	//		case 0x00ac:	return 0xffe2;		// μ
	//		case 0x03bc:	return 0x00b5;		// ・
	//		case 0x2014:	return 0x2015;		// ，
	//		case 0x2016:	return 0x2225;		// ≫
	//		case 0x2212:	return 0xff0d;		// ―
	//		case 0x226a:	return 0x00ab;		// ∥
	//		case 0x226b:	return 0x00bb;		// ヴ
	//		case 0x301c:	return 0xff5e;		// －
	//		case 0x30f4:	return 0x3094;		// ～
	//		case 0x30fb:	return 0x00b7;		// ￠
	//		case 0xff0c:	return 0x00b8;		// ￡
	//		case 0xffe3:	return 0x00af;		// ￢

			case 0x00a2:	rtn = 0xffe0; break;		// ￠ (1-81, CENT SIGN)
			case 0x00a3:	rtn = 0xffe1; break;		// ￡ (1-82, POUND SIGN)
			case 0x00a5:	rtn = 0x005c; break;		// \ (D/12, YEN SIGN)
			case 0x00ac:	rtn = 0xffe2; break;		// ￢ (2-44, NOT SIGN)
			case 0x2016:	rtn = 0x2225; break;		// ∥ (1-34, DOUBLE VERTICAL LINE)
			case 0x203e:	rtn = 0x007e; break;		// ~ (F/14, OVERLINE)
			case 0x2212:	rtn = 0xff0d; break;		// － (1-61, MINUS SIGN)
			case 0x301c:	rtn = 0xff5e; break;		// ～ (1-33, WAVE DASH)

	//		case 0x301c:	return 0xff5e;
	//		case 0x2016:	return 0x2225;
	//		case 0x2212:	return 0xff0d;
			default:		break;			// 4.0.0 (2005/01/31)
		}
		return rtn;
	}
}
