/*
 * 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 static org.opengion.fukurou.util.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring
import static org.opengion.fukurou.util.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

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 は、受信メールを処理するためのラッパークラスです。
 *
 * メッセージオブジェクトを引数にとるコンストラクタによりオブジェクトが作成されます。
 * 日本語処置などを簡易的に扱えるように、ラッパクラス的な使用方法を想定しています。
 * 必要であれば(例えば、添付ファイルを取り出すために、MailAttachFiles を利用する場合など)
 * 内部のメッセージオブジェクトを取り出すことが可能です。
 * 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   ;
	private String content   ;
	private String messageID ;

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

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

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

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

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

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

	/**
	 * メールのタイトル(Subject)を返します。
	 * 日本語文字コード処理も行っています。(JIS→unicode変換等)
	 *
	 * @og.rev 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列を mimeDecode でデコードします。
	 *
	 * @return	メールのタイトル
	 */
	public String getSubject() {
		if( subject == null ) {
			try {
				subject = mimeDecode( message.getSubject() );
			}
			catch( MessagingException ex ) {
				// メッセージ情報のハンドリングに失敗しました。
				throw new RuntimeException( MSG_EX,ex );
			}
		}
		if( subject == null ) { subject = "No Subject" ;}
		return subject;
	}

	/**
	 * メールの本文(Content)を返します。
	 * 日本語文字コード処理も行っています。(JIS→unicode変換等)
	 *
	 * @return	メールの本文
	 */
	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 のケースがあるため。
					Date dt = message.getSentDate();
					if( dt == null ) { dt = message.getReceivedDate(); }
					final Long date = (dt == null) ? 0L : dt.getTime();
					final String from = ((InternetAddress[])message.getFrom())[0].getAddress() ;
					messageID = "unknown." + date + "." + from ;
				}
			}
			catch( MessagingException ex ) {
				// メッセージ情報のハンドリングに失敗しました。
				throw new RuntimeException( MSG_EX,ex );
			}
		}
		return messageID ;
	}

	/**
	 * メッセージをメールサーバーから削除するかどうかをセットします。
	 *
	 * @param	flag	削除するかどうか [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	メールの内容の文字列表現
	 * @og.rtnNotNull
	 */
	public String getSimpleMessage() {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( getHeaders() ).append( CR )
			.append( "Subject:" ).append( getSubject() ).append( CR )
			.append( "===============================" ).append( CR )
			.append( getContent() ).append( CR )
			.append( "===============================" ).append( CR );

		return buf.toString();
	}

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

		saveMessage( dir );

		saveAttachFiles( dir,true );
	}

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

		final String msgId = getMessageID() ;

		// 3.8.0.0 (2005/06/07) FileUtil#getPrintWriter を利用。
		final File file = new File( dir,msgId + ".txt" );
		final 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	添付ファイルをセーブするフォルダ
	 * @param	useMsgId	メッセージIDフォルダを作成してセーブ場合：true
	 *          指定のディレクトリ直下にセーブする場合：false
	 */
	public void saveAttachFiles( final String dir,final boolean useMsgId ) {

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

		final MailAttachFiles attFiles = new MailAttachFiles( message );
		final String[] files = attFiles.getNames();
		if( files.length > 0 ) {
	//		String attDirStr = dir + "/" + msgId + "/";
	//		File attDir = new File( attDirStr );
	//		if( !attDir.exists() ) {
	//			if( ! attDir.mkdirs() ) {
	//			final 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 ヘッダー情報配列
	 *
	 * @return ヘッダー情報の キー:値 のMap
	 */
	private Map<String,String> makeHeaderMap( final String[] headerList ) {
		final 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() ) {
				final Header header = (Header)enume.nextElement();
				final String name  = header.getName();
	 			// 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列を mimeDecode でデコードします。
				String value = mimeDecode( header.getValue() );

				final String val = headMap.get( name );
				if( val != null ) {
					value = val + "," + value;
				}
				headMap.put( name,value );
			}
		}
		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 javax.mail 関連のエラーが発生したとき
	 * @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( "text/plain".equalsIgnoreCase( part.getContentType() ) ) {
					final 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/*") ) {
				final Multipart mp = (Multipart)part.getContent();

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

		return content ;
	}

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

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