/*
 * 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 org.opengion.fukurou.util.FileUtil;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.File;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.Date;

import javax.mail.Header;
import javax.mail.Part;
import javax.mail.BodyPart;
import javax.mail.Multipart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Flags;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import javax.mail.internet.InternetAddress;

/**
 * MailMessage は、受信メールを処理するためのラッパークラスです。
 *
 * Message オブジェクトを引数にとるコンストラクタによりオブジェクトが作成されます。
 * 日本語処置などを簡易的に扱えるように、ラッパクラス的な使用方法を想定しています。
 * 必要であれば(例えば、添付ファイルを取り出すために、MailAttachFiles を利用する場合など)
 * 内部のMessage オブジェクトを取り出すことが可能です。
 * MailReceiveListener クラスの receive( MailMessage ) メソッドで、メールごとにイベントが
 * 発生して、処理する形態が一般的です。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class MailMessage {

	private static final String CR = System.getProperty("line.separator");
	private static final String MSG_EX = "メッセージ情報のハンドリングに失敗しました。" ;

	private final String  host ;
	private final String  user ;
	private final Message message ;
	private final Map<String,String>     headerMap ;

	private String subject   = null;
	private String content   = null;
	private String messageID = null;

	/**
	 * Message オブジェクトを指定して構築します。
	 *
	 * @param message Message
	 * @param host String
	 * @param user String
	 */
	public MailMessage( final Message message,final String host,final String user ) {
		this.host = host;
		this.user = user;
		this.message = message;
		headerMap    = makeHeaderMap( null );
	}

	/**
	 * 内部の Message オブジェクトを返します。
	 *
	 * @return Message
	 */
	public Message getMessage() {
		return message;
	}

	/**
	 * 内部の host 名を返します。
	 *
	 * @return host
	 */
	public String getHost() {
		return host;
	}

	/**
	 * 内部の user 名を返します。
	 *
	 * @return user
	 */
	public String getUser() {
		return user;
	}

	/**
	 * メールのヘッダー情報を文字列に変換して返します。
	 * キーは、ヘッダー情報の取り出しと同一です。
	 * 例) Return-Path,Delivered-To,Date,From,To,Cc,Subject,Content-Type,Message-Id
	 *
	 * @param	key String
	 * @return	String
	 */
	public String getHeader( final String key ) {
		return headerMap.get( key );
	}

	/**
	 * メールの指定のヘッダー情報を文字列に変換して返します。
	 * ヘッダー情報の取り出しキーと同一の項目を リターンコードで結合しています。
	 * Return-Path,Delivered-To,Date,From,To,Cc,Subject,Content-Type,Message-Id
	 *
	 * @return	String
	 */
	public String getHeaders() {
		String[] keys = headerMap.keySet().toArray( new String[headerMap.size()] );
		StringBuilder buf = new StringBuilder( 200 );
		for( int i=0; i<keys.length; i++ ) {
			buf.append( keys[i] ).append(":").append( headerMap.get( keys[i] ) ).append( CR );
		}
		return buf.toString();
	}

	/**
	 * メールのタイトル(Subject)を返します。
	 * 日本語文字コード処理も行っています。(JIS→unicode変換等)
	 *
	 * @og.rev 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列を mimeDecode でデコードします。
	 *
	 * @return	String
	 */
	public String getSubject() {
		if( subject == null ) {
			try {
				subject = mimeDecode( message.getSubject() );

//				subject = UnicodeCorrecter.correctToCP932( message.getSubject() );
			}
			catch( MessagingException ex ) {
				// メッセージ情報のハンドリングに失敗しました。
				throw new RuntimeException( MSG_EX,ex );
			}
//			catch( UnsupportedEncodingException ex ) {
//				String errMsg = "テキスト情報のデコードに失敗しました。" ;
//				throw new RuntimeException( errMsg,ex );
//			}
		}
		if( subject == null ) { subject = "No Subject" ;}
		return subject;
	}

	/**
	 * メールの本文(Content)を返します。
	 * 日本語文字コード処理も行っています。(JIS→unicode変換等)
	 *
	 * @return	String
	 */
	public String getContent() {
		if( content == null ) {
			content = UnicodeCorrecter.correctToCP932( mime2str( message ) );
		}
		return content;
	}

	/**
	 * メッセージID を取得します。
	 *
	 * 基本的には、メッセージIDをそのまま（前後の &gt;, &lt;）は取り除きます。
	 * メッセージIDのないメールは、"unknown." + SentData + "." + From という文字列を
	 * 作成します。
	 * さらに、送信日やFrom がない場合、または、文字列として取り出せない場合、
	 * "unknown" を返します。
	 *
	 * @og.rev 4.3.3.5 (2008/11/08) 送信時刻がNULLの場合の処理を追加
	 *
	 * @return メッセージID
	 */
	public String getMessageID() {
		if( messageID == null ) {
			try {
				messageID = ((MimeMessage)message).getMessageID();
				if( messageID != null ) {
					messageID = messageID.substring(1,messageID.length()-1) ;
				}
				else {
					// 4.3.3.5 (2008/11/08) SentDate が null のケースがあるため。
//					String date = String.valueOf( message.getSentDate().getTime() );
					Date dt = message.getSentDate();
					if( dt == null ) { dt = message.getReceivedDate(); }
					Long date = (dt == null) ? 0L : dt.getTime();
					String from = ((InternetAddress[])message.getFrom())[0].getAddress() ;
					messageID = "unknown." + date + "." + from ;
				}
			}
			catch( MessagingException ex ) {
				// メッセージ情報のハンドリングに失敗しました。
				throw new RuntimeException( MSG_EX,ex );
			}
		}
		return messageID ;
	}

	/**
	 * メッセージをメールサーバーから削除するかどうかをセットします。
	 *
	 * @param	flag boolean 削除するかどうか	true:行う／false:行わない
	 */
	public void deleteMessage( final boolean flag ) {
		try {
			message.setFlag(Flags.Flag.DELETED, flag);
		}
		catch( MessagingException ex ) {
			// メッセージ情報のハンドリングに失敗しました。
			throw new RuntimeException( MSG_EX,ex );
		}
	}

	/**
	 * メールの内容を文字列として表現します。
	 * デバッグや、簡易的なメールの内容の取り出し、エラー時のメール保存に使用します。
	 *
	 * @return	メールの内容の文字列表現
	 */
	public String getSimpleMessage() {
		StringBuilder buf = new StringBuilder( 200 );

		buf.append( getHeaders() ).append( CR );
		buf.append( "Subject:" ).append( getSubject() ).append( CR );
		buf.append( "===============================" ).append( CR );
		buf.append( getContent() ).append( CR );
		buf.append( "===============================" ).append( CR );

		return buf.toString();
	}

	/**
	 * メールの内容と、あれば添付ファイルを指定のフォルダにセーブします。
	 * saveMessage( dir )と、saveAttachFiles( dir,true ) を同時に呼び出しています。
	 *
	 * @param	dir String メールと添付ファイルをセーブするフォルダ
	 */
	public void saveSimpleMessage( final String dir ) {

		saveMessage( dir );

		saveAttachFiles( dir,true );
	}

	/**
	 * メールの内容を文字列として指定のフォルダにセーブします。
	 * メッセージID.txt という本文にセーブします。
	 * デバッグや、簡易的なメールの内容の取り出し、エラー時のメール保存に使用します。
	 *
	 * @param	dir String メールの内容をセーブするフォルダ
	 */
	public void saveMessage( final String dir ) {

		String msgId = getMessageID() ;

		// 3.8.0.0 (2005/06/07) FileUtil#getPrintWriter を利用。
		File file = new File( dir,msgId + ".txt" );
		PrintWriter writer = FileUtil.getPrintWriter( file,"UTF-8" );
		writer.println( getSimpleMessage() );

		writer.close();
	}

	/**
	 * メールの添付ファイルが存在する場合に、指定のフォルダにセーブします。
	 *
	 * 添付ファイルが存在する場合のみ、処理を実行します。
	 * useMsgId にtrue を設定すると、メッセージID というフォルダを作成し、その下に、
	 * 連番 + "_" + 添付ファイル名 でセーブします。(メールには同一ファイル名を複数添付できる為)
	 * false の場合は、指定のディレクトリ直下に、連番 + "_" + 添付ファイル名 でセーブします。
	 *
	 * @og.rev 4.3.3.5 (2008/11/08) ディレクトリ指定時のセパレータのチェックを追加
	 *
	 * @param	dir String 添付ファイルをセーブするフォルダ
	 * @param	useMsgId boolean メッセージIDフォルダを作成してセーブ場合：true
	 *          指定のディレクトリ直下にセーブする場合：false
	 */
	public void saveAttachFiles( final String dir,final boolean useMsgId ) {

		final String attDirStr ;
		if( useMsgId ) {
			String msgId = getMessageID() ;
	 		// 4.3.3.5 (2008/11/08) ディレクトリ指定時のセパレータのチェックを追加
			if( dir.endsWith( "/" ) ) {
				attDirStr = dir + msgId + "/";
			}
			else {
				attDirStr = dir + "/" + msgId + "/";
			}
		}
		else {
			attDirStr = dir ;
		}

		MailAttachFiles attFiles = new MailAttachFiles( message );
		String[] files = attFiles.getNames();
		if( files.length > 0 ) {
	//		String attDirStr = dir + "/" + msgId + "/";
	//		File attDir = new File( attDirStr );
	//		if( !attDir.exists() ) {
	//			if( ! attDir.mkdirs() ) {
	//				String errMsg = "添付ファイルのディレクトリの作成に失敗しました。[" + attDirStr + "]";
	//				throw new RuntimeException( errMsg );
	//			}
	//		}

			// 添付ファイル名を指定しないと、番号 + "_" + 添付ファイル名になる。
			for( int i=0; i<files.length; i++ ) {
				attFiles.saveFileName( attDirStr,null,i );
			}
		}
	}

	/**
	 * 受領確認がセットされている場合の 返信先アドレスを返します。
	 * セットされていない場合は、null を返します。
	 * 受領確認は、Disposition-Notification-To ヘッダにセットされる事とし、
	 * このヘッダの内容を返します。セットされていなければ、null を返します。
	 *
	 * @return 返信先アドレス(Disposition-Notification-To ヘッダの内容)
	 */
	public String getNotificationTo() {
		return headerMap.get( "Disposition-Notification-To" );
	}

	/**
	 * ヘッダー情報を持った、Enumeration から、ヘッダーと値のペアの文字列を作成します。
	 *
	 * ヘッダー情報は、Message#getAllHeaders() か、Message#getMatchingHeaders( String[] )
	 * で得られる Enumeration に、Header オブジェクトとして取得できます。
	 * このヘッダーオブジェクトから、キー（getName()) と値(getValue()) を取り出します。
	 * 結果は、キー:値 の文字列として、リターンコードで区切ります。
	 *
	 * @og.rev 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列を mimeDecode でデコードします。
	 *
	 * @param headerList String[]
	 * @return ヘッダー情報の キー:値 のMap
	 */
	private Map<String,String> makeHeaderMap( final String[] headerList ) {
		Map<String,String> headMap = new LinkedHashMap<String,String>();
		try {
			final Enumeration<?> enume;		// 4.3.3.6 (2008/11/15) Generics警告対応
			if( headerList == null ) {
				enume = message.getAllHeaders();
			}
			else {
				enume = message.getMatchingHeaders( headerList );
			}

			while( enume.hasMoreElements() ) {
				Header header = (Header)enume.nextElement();
				String name  = header.getName();
	 			// 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列を mimeDecode でデコードします。
				String value = mimeDecode( header.getValue() );
//				String value = header.getValue();

//				if( value.indexOf( "=?" ) >= 0 ) {
//					value = (header.getValue()).replace( '"',' ' );	// メールデコードのミソ
//					value = UnicodeCorrecter.correctToCP932( MimeUtility.decodeText( value ) );
//				}

				String val = headMap.get( name );
				if( val != null ) {
					value = val + "," + value;
				}
				headMap.put( name,value );
			}
		}
//		catch( UnsupportedEncodingException ex ) {
//			String errMsg = "Enumeration より、Header オブジェクトが取り出せませんでした。" ;
//			throw new RuntimeException( errMsg,ex );
//		}
		catch( MessagingException ex2 ) {
			// メッセージ情報のハンドリングに失敗しました。
			throw new RuntimeException( MSG_EX,ex2 );
		}

		return headMap;
	}

	/**
	 * Part オブジェクトから、最初に見つけた text/plain を取り出します。
	 *
	 * Part は、マルチパートというPartに複数のPartを持っていたり、さらにその中にも
	 * Part を持っているような構造をしています。
	 * ここでは、最初に見つけた、MimeType が、text/plain の場合に、文字列に
	 * 変換して、返しています。それ以外の場合、再帰的に、text/plain が
	 * 見つかるまで、処理を続けます。
	 * また、特別に、HN0256 からのトラブルメールは、Content-Type が、text/plain のみに
	 * なっている為 CONTENTS が、JIS のまま、取り出されてしまうため、強制的に
	 * Content-Type を、"text/plain; charset=iso-2022-jp" に変更しています。
	 *
	 * @param part Part
	 * @return 最初の text/plain 文字列。見つからない場合は、null を返します。
	 * @throws MessagingException
	 * @throws IOException
	 */
	private String mime2str( final Part part ) {
		String content = null;

		try {
			if( part.isMimeType("text/plain") ) {
				// HN0256 からのトラブルメールは、Content-Type が、text/plain のみになっている為
				// CONTENTS が、JIS のまま、取り出されてしまう。強制的に変更しています。
				if( (part.getContentType()).equals( "text/plain" ) ) {
					MimeMessage msg = new MimeMessage( (MimeMessage)part );
					msg.setHeader( "Content-Type","text/plain; charset=iso-2022-jp" );
					content = (String)msg.getContent();
				}
				else {
					content = (String)part.getContent();
				}
			}
			else if( part.isMimeType("message/rfc822") ) {		// Nested Message
				content = mime2str( (Part)part.getContent() );
			}
			else if( part.isMimeType("multipart/*") ) {
				Multipart mp = (Multipart)part.getContent();

				int count = mp.getCount();
				for(int i = 0; i < count; i++) {
					BodyPart bp = mp.getBodyPart(i);
					content = mime2str( bp );
					if( content != null ) { break; }
				}
			}
		}
		catch( MessagingException ex ) {
			// メッセージ情報のハンドリングに失敗しました。
			throw new RuntimeException( MSG_EX,ex );
		}
		catch( IOException ex2 ) {
			String errMsg = "テキスト情報の取り出しに失敗しました。" ;
			throw new RuntimeException( errMsg,ex2 );
		}

		return content ;
	}

	/**
	 * エンコードされた文字列を、デコードします。
	 *
	 * MIMEエンコード は、 =? で開始するエンコード文字列 ですが、場合によって、前のスペースが
	 * 存在しない場合があります。
	 * また、メーラーによっては、エンコード文字列を ダブルコーテーションでくくる処理が入っている
	 * 場合もあります。
	 * これらの一連のエンコード文字列をデコードします。
	 *
	 * @og.rev 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列をデコードします。
	 *
	 * @param  text String エンコードされた文字列（されていない場合は、そのまま返します）
	 * @return String デコードされた文字列
	 */
	public static final String mimeDecode( final String text ) {
		if( text == null || text.indexOf( "=?" ) < 0 ) { return text; }

//		String rtnText = text.replace( '"',' ' );		// メールデコードのミソ
		String rtnText = text.replace( '\t',' ' );		// 若干トリッキーな処理
		try {
			// encode-word の =? の前にはスペースが必要。
			// ここでは、分割して、デコード処理を行うことで、対応
			StringBuilder buf = new StringBuilder();
			int pos1 = rtnText.indexOf( "=?" );			// デコードの開始
			int pos2 = 0;								// デコードの終了
			buf.append( rtnText.substring( 0,pos1 ) );
			while( pos1 >= 0 ) {
				pos2 = rtnText.indexOf( "?=",pos1 ) + 2;		// デコードの終了
				String sub = rtnText.substring( pos1,pos2 );
				buf.append( UnicodeCorrecter.correctToCP932( MimeUtility.decodeText( sub ) ) );
				pos1 = rtnText.indexOf( "=?",pos2 );			// デコードの開始
				if( pos1 > 0 ) {
					buf.append( rtnText.substring( pos2,pos1 ) );
				}
			}
			buf.append( rtnText.substring( pos2 ) );
			rtnText = buf.toString() ;
		}
		catch( UnsupportedEncodingException ex ) {
			String errMsg = "テキスト情報のデコードに失敗しました。" ;
			throw new RuntimeException( errMsg,ex );
		}
		return rtnText;
	}
}
