/*
 * 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.StringUtil ;
import org.opengion.fukurou.util.HybsEntry ;
import org.opengion.fukurou.system.LogWriter;

import java.util.Properties;
import java.util.List;
import java.util.ArrayList;
import java.util.Locale;								// 6.3.8.0 (2015/09/11)

import javax.mail.Session;
import javax.mail.Store;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.Flags;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.search.SearchTerm;
import javax.mail.search.SubjectTerm;
import javax.mail.search.FromStringTerm;
import javax.mail.search.BodyTerm;
import javax.mail.search.HeaderTerm;
import javax.mail.search.AndTerm;

/**
 * MailRX は、POP3/IMAPプロトコルによるメール受信プログラムです。
 *
 * メールへの接続条件(host,user,passwd など)と、選択条件(matchTermなど)を指定し、
 * MailReceiveListener をセットして、start() メソッドを呼びます。
 * 実際のメール処理は、MailReceiveListener を介して、１メールずつ処理します。
 * 添付ファイルを処理する場合は、MailAttachFiles クラスを使用します。
 *
 *        host          メールサーバー(必須)
 *        user          メールを取得するログインユーザー(必須)
 *        passwd        メールを取得するログインパスワード(必須)
 *        protocol      受信サーバーのプロトコル[imap/pop3]を指定(初期値:{@og.value #PROTOCOL})
 *        port          受信サーバーのポートを指定(初期値:{@og.value #PORT})
 *        useSSL        SSL接続するかどうか[true:する/false:しない]を指定(初期値:false:しない)
 *        mbox          受信サーバーのメールボックスを指定(初期値:{@og.value #MBOX})
 *        maxRowCount   受信メールの最大取り込み件数(初期値:{@og.value #MAX_ROW_COUNT})(0:[無制限])
 *        charset       メールのデフォルトエンコード(初期値:{@og.value #CHARSET})
 *        matchTerm     受信メールを選択する条件のMINEntryオブジェクト
 *        delete        検索後、メールをサーバーから削除するかどうかを、true/falseで指定(初期値:{@og.value #DELETE_MESSAGE})。
 *
 * ※ 6.3.8.0 (2015/09/11)
 *    useSSL属性は、protocolに、pop3s/imaps を指定した場合、
 *    自動的に、ture に設定するようにしています。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class MailRX {

	/** 受信メールの最大取り込み件数を指定します 「={@value}」  */
	public static final int MAX_ROW_COUNT = 100 ;

	/** 検索後、メールをサーバーから削除するかどうかを、true/falseで指定します 「={@value}」  */
	public static final boolean DELETE_MESSAGE = false ;

	/** メールサーバーのデフォルトプロトコル 「={@value}」  */
	public static final String PROTOCOL = "pop3" ;

	/** メールサーバーのデフォルトポート番号 「={@value}」  */
	public static final int PORT = -1 ;

	/** メールサーバーのデフォルトメールボックス 「={@value}」。  */
	public static final String MBOX = "INBOX" ;

	/** メールのデフォルトエンコード 「={@value}」
	 * Windwos-31J , MS932 , UTF-8 , ISO-2022-JP を指定します。
	 */
	public static final String CHARSET = "ISO-2022-JP" ;

	// メール受信毎に発生するイベントを伝えるリスナーを登録します。
	private MailReceiveListener listener ;

	private String	host		;
	private String	user		;
	private String	passwd		;
	private String	protocol	= PROTOCOL;
	private int		port		= PORT;
	private boolean	isUseSSL	;						// 6.3.8.0 (2015/09/11)
	private String	mbox		= MBOX;
	private boolean	deleteFlag	= DELETE_MESSAGE;
	private String	charset		= CHARSET;
	private int		maxRowCount	= MAX_ROW_COUNT;

	private final List<HybsEntry>	matchList	= new ArrayList<>();
	private boolean	debug		;

	/**
	 * レシーバーを開始します。
	 *
	 * @og.rev 6.3.8.0 (2015/09/11) SSL接続するかどうかを指定するパラメータを追加します。
	 *
	 * @throws MessagingException レシーバー処理中に、なんらかのエラーが発生した場合。
	 * @throws NoSuchProviderException なんらかのエラーが発生した場合。
	 */
	public void start() throws MessagingException,NoSuchProviderException {

		// パラメータの解析、取得
		debugMsg( "パラメータの解析、取得" );

		// 指定の条件にマッチしたメッセージのみ抜き出す為の、SearchTerm オブジェクトの作成
		// 6.3.8.0 (2015/09/11) IMAPの場合、条件の有無で、メッセージの取得方法を変える必要がる。
		SearchTerm srchTerm = null;
		if( !matchList.isEmpty() ) {
			debugMsg( "指定の条件にマッチしたメッセージのみ抜き出す条件を設定します。"  );
			final HybsEntry[] matchs = matchList.toArray( new HybsEntry[matchList.size()] );
			SearchTerm[] term = new SearchTerm[matchs.length];
			for( int i=0; i<matchs.length; i++ ) {
				final String key = matchs[i].getKey();
				if( "Subject".equalsIgnoreCase( key ) ) {
					term[i] = new SubjectTerm( matchs[i].getValue() );
				}
				else if( "From".equalsIgnoreCase( key ) ) {
					term[i] = new FromStringTerm( matchs[i].getValue() );
				}
				else if( "Body".equalsIgnoreCase( key ) ) {
					term[i] = new BodyTerm( matchs[i].getValue() );
				}
				else {
					term[i] = new HeaderTerm( key,matchs[i].getValue() );
				}
			}
			srchTerm = new AndTerm( term );
		}

		// 空の properties を設定。気休め程度に、初期値を設定しておきます。
		debugMsg( "空の properties を設定"  );
		final Properties prop = new Properties();
		prop.setProperty("mail.mime.charset"			, charset );
		prop.setProperty("mail.mime.decodetext.strict"	, "false" );
		prop.setProperty("mail.mime.address.strict"		, "false" );

		// 6.3.8.0 (2015/09/11) SSL接続するかどうかを指定するパラメータを追加します。
		if( isUseSSL ) {
			if( protocol.contains( "pop3" ) ) {			// pop3/pop3s
				prop.setProperty("mail.pop3.socketFactory.class"	, "javax.net.ssl.SSLSocketFactory" );
				prop.setProperty("mail.pop3.socketFactory.fallback"	, "false" );
				prop.setProperty("mail.pop3.socketFactory.port"		, String.valueOf( port ) );		// 995
			}
			// google の IMAP の場合は、下記設定なしでも、protocol=imaps のみで接続できた。
			else if( protocol.contains( "imap" ) ) {	// imap/imaps
				prop.setProperty("mail.imap.ssl.enable"					, "true" );
				prop.setProperty("mail.imap.ssl.socketFactory.class"	, "DummySSLSocketFactory" );
				prop.setProperty("mail.imap.ssl.socketFactory.fallback"	, "false" );
			}
		}

		// session を取得
		debugMsg( "session を取得" );
		final Session session = Session.getInstance( prop, null );

		Store store = null;
		Folder folder = null;
		try {
			// store の取得
			debugMsg( "store の取得 protocol=",protocol );
			store = session.getStore( protocol );

			// サーバーと connect します。
			debugMsg( "サーバーと connect します。" );
			store.connect( host, port, user, passwd );

			// folder の取得
			debugMsg( "folder の取得" );
			folder = store.getFolder( mbox );
			if( deleteFlag ) {
				folder.open( Folder.READ_WRITE );
			}
			else {
				folder.open( Folder.READ_ONLY );
			}

			// メッセージ情報の取得
			debugMsg( "メッセージ情報の取得" );
			// 6.3.8.0 (2015/09/11) IMAPの場合、条件の有無で、メッセージの取得方法を変える必要がる。
			final Message[] message = srchTerm == null ? folder.getMessages() : folder.search( srchTerm ) ;

			final int len = message.length;				// 6.1.0.0 (2014/12/26) refactoring
			for( int i=0; i<len && i<maxRowCount; i++ ) {
				final MailMessage mailMessage = new MailMessage( message[i],host,user );
				debugMsg( "[" , String.valueOf(i) , "]" , mailMessage.getMessageID() , " 受信中" );

				// メールの削除[true/false]:先にフラグを立てているので、エラーでも削除されます。
				// 6.3.8.0 (2015/09/11) deleteFlag で、READ_ONLY かどうかを指定しているため、セットの判定を入れます。
				if( deleteFlag ) {
					message[i].setFlag( Flags.Flag.DELETED, true );
				}

				boolean okFlag = true;
				if( listener != null ) {
					// メール本体の処理
					okFlag = listener.receive( mailMessage );
				}

				// 受領確認の返信メール
				final String notifyTo = mailMessage.getNotificationTo() ;
				if( okFlag && notifyTo != null ) {
					final MailTX tx = new MailTX( host );
					tx.setFrom( user );
					tx.setTo( StringUtil.csv2Array( notifyTo ) );
					tx.setSubject( "受領:" + mailMessage.getSubject() );
					tx.setMessage( mailMessage.getContent() );
					tx.sendmail();
				}
			}
		}
		finally {
			// セッション終了
			debugMsg( "セッション終了処理" );
			if( folder != null ) {
				folder.close( deleteFlag );		// true の場合は、終了時に実際に削除します。
			}
			if( store != null ) {
				store.close();
			}
		}
	}

	/**
	 * メールサーバーをセットします(必須)。
	 *
	 * @param	host メールサーバー
	 * @throws	IllegalArgumentException 引数が null の場合。
	 */
	public void setHost( final String host ) {
		if( host == null ) {
			final String errMsg = "host に null はセット出来ません。";
			throw new IllegalArgumentException( errMsg );
		}

		this.host = host;
	}

	/**
	 * 受信ユーザーをセットします(必須)。
	 *
	 * @param	user 受信ユーザー
	 * @throws	IllegalArgumentException 引数が null の場合。
	 */
	public void setUser( final String user ) {
		if( user == null ) {
			final String errMsg = "user に null はセット出来ません。";
			throw new IllegalArgumentException( errMsg );
		}
		this.user = user;
	}

	/**
	 * パスワードをセットします(必須)。
	 *
	 * @param	passwd パスワード
	 * @throws	IllegalArgumentException 引数が null の場合。
	 */
	public void setPasswd( final String passwd ) {
		if( passwd == null ) {
			final String errMsg = "passwd に null はセット出来ません。";
			throw new IllegalArgumentException( errMsg );
		}
		this.passwd = passwd;
	}

	/**
	 * 受信プロトコル(pop3/imap等)をセットします(初期値:{@og.value #PROTOCOL})。
	 *
	 * protocolに、pop3s/imaps を指定した場合、
	 * useSSL属性は、自動的に、ture に設定されます。
	 *
	 * @param	prtcol 受信プロトコル名
	 * @throws	IllegalArgumentException 引数が null の場合。
	 * @see		#PROTOCOL
	 */
	public void setProtocol( final String prtcol ) {
		if( prtcol == null ) {
			final String errMsg = "protocol に null はセット出来ません。";
			throw new IllegalArgumentException( errMsg );
		}
		protocol = prtcol.toLowerCase( Locale.JAPAN );

		// 6.3.8.0 (2015/09/11) 登録順に影響されない様に、注意
		if( port < 0 ) {		// 未設定
			if( "pop3".equalsIgnoreCase(  protocol ) ) { port = 110; }
			if( "imap".equalsIgnoreCase(  protocol ) ) { port = 143; isUseSSL = true; }
			if( "pop3s".equalsIgnoreCase( protocol ) ) { port = 995; }
			if( "imaps".equalsIgnoreCase( protocol ) ) { port = 993; isUseSSL = true; }
		}
	}

	/**
	 * ポート番号をセットします(初期値:{@og.value #PORT})。
	 *
	 * portが、-1 の場合は、protocol に応じたポートが使用されます。
	 * pop3:110 , imap:143 , pop3s:995 , imaps:993
	 *
	 * @param	port ポート番号
	 * @see		#PORT
	 */
	public void setPort( final int port ) {
		this.port = port;
	}

	/**
	 * SSL接続するかどうかをセットします(初期値:false:しない)。
	 *
	 * protocolに、pop3s/imaps を指定した場合、
	 * useSSL属性は、自動的に、ture に設定されます。
	 *
	 * @og.rev 6.3.8.0 (2015/09/11) SSL接続するかどうかを指定するパラメータを追加します。
	 *
	 * @param	isSSL SSL接続するかどうか[true:する/false:しない]を指定
	 */
	public void useSSL( final boolean isSSL ) {
		// 6.3.8.0 (2015/09/11) 登録順に影響されない様に、注意
		isUseSSL = isSSL || "pop3s".equalsIgnoreCase( protocol ) || "imaps".equalsIgnoreCase( protocol );
	}

	/**
	 * 受信メイルボックスをセットします(初期値:{@og.value #MBOX})。
	 *
	 * @param	mbox 受信メイルボックス名
	 * @throws	IllegalArgumentException 引数が null の場合。
	 * @see		#MBOX
	 */
	public void setMbox( final String mbox ) {
		if( mbox == null ) {
			final String errMsg = "mbox に null はセット出来ません。";
			throw new IllegalArgumentException( errMsg );
		}
		this.mbox = mbox;
	}

	/**
	 * メール受信毎に発生するイベントを伝えるリスナーをセットします。
	 *
	 * @param	listener MailReceiveリスナー
	 */
	public void setMailReceiveListener( final MailReceiveListener listener ) {
		this.listener = listener;
	}

	/**
	 * メッセージをメールサーバーから削除するかどうかをセットします(初期値:{@og.value #DELETE_MESSAGE})。
	 *
	 * @param	deleteFlag 削除するかどうか[true:行う/false:行わない]
	 * @see		#DELETE_MESSAGE
	 */
	public void setDelete( final boolean deleteFlag ) {
		this.deleteFlag = deleteFlag;
	}

	/**
	 * 文字エンコーディングをセットします(初期値:{@og.value #CHARSET})。
	 *
	 * 文字エンコーディングには、Windwos-31J , MS932 , ISO-2022-JP を指定できます。
	 * 初期値は、SystemResource.properties ファイルの MAIL_DEFAULT_CHARSET 属性で
	 * 設定できます。
	 *
	 * @param   charset 文字エンコーディング
	 * @throws	IllegalArgumentException 引数が null の場合。
	 * @see		#CHARSET
	 */
	public void setCharset( final String charset ) {
		if( charset == null ) {
			final String errMsg = "charset に null はセット出来ません。";
			throw new IllegalArgumentException( errMsg );
		}
		this.charset = charset;
	}

	/**
	 * 最大取り込み件数をセットします(初期値:{@og.value #MAX_ROW_COUNT})(0:[無制限])。
	 *
	 * @og.rev 5.5.8.5 (2012/11/27) 0を無制限として処理します。
	 *
	 * @param	maxCount 最大取り込み件数
	 * @see		#MAX_ROW_COUNT
	 */
	public void setMaxRowCount( final int maxCount ) {
		maxRowCount = maxCount>0 ? maxCount : Integer.MAX_VALUE ;			// 6.0.2.5 (2014/10/31) refactoring
	}

	/**
	 * メール検索する場合のマッチ条件のキーと値の HybsEntry をセットします。
	 * Subject,From,Body,それ以外は、Header 文字列をキーにします。
	 *
	 * @param	matchTerm HybsEntryオブジェクト
	 */
	public void addMatchTerm( final HybsEntry matchTerm ) {
		matchList.add( matchTerm );
	}

	/**
	 * デバッグ情報の表示を行うかどうかをセットします。
	 *
	 * @param	debug 有無[true/false]
	 */
	public void setDebug( final boolean debug ) {
		this.debug = debug;
	}

	/**
	 * デバッグ情報の表示を行います。
	 * 実際の処理は、debug フラグに設定値によります。
	 *
	 * @param	msgs デバッグ情報(可変長引数)
	 */
	private void debugMsg( final String... msgs ) {
		if( debug ) {
			for( final String msg : msgs ) {
				System.out.print( msg );
			}
			System.out.println();
		}
	}

	/**
	 * コマンドから実行できる、テスト用の main メソッドです。
	 *
	 * Usage: java org.opengion.fukurou.mail.MailTX MailRX host user passwd [saveDir]
	 * で、複数の添付ファイルを送付することができます。
	 *
	 * @og.rev 6.3.9.1 (2015/11/27) A method/constructor shouldnt explicitly throw java.lang.Exception(PMD)。
	 *
	 * @param   args 引数配列
	 * @throws MessagingException なんらかのエラーが発生した場合。
	 */
	public static void main( final String[] args ) throws MessagingException {
		if( args.length<3 ) {
			LogWriter.log("Usage: java org.opengion.fukurou.mail.MailRX host user passwd [saveDir]");
			System.exit(1);
		}
		final String dir = (args.length == 4) ? args[3] : null;

		final MailRX recive = new MailRX();

		recive.setHost( args[0] );
		recive.setUser( args[1] );
		recive.setPasswd( args[2] );
		recive.setCharset( "ISO-2022-JP" );

		final MailReceiveListener listener = new MailReceiveListener() {
			/**
			 * メール受信処理で、１メール受信ごとに呼び出されます。
			 * 処理結果を、boolean で返します。
			 *
			 * @param message MailMessageオブジェクト
			 * @return	処理結果(正常:true / 異常:false)
			 */
			public boolean receive( final MailMessage message ) {
				System.out.println( message.getSimpleMessage() );

				if( dir != null ) {
					message.saveSimpleMessage( dir );
				}
				return true ;
			}
		};
		recive.setMailReceiveListener( listener );

		recive.start();
	}
}
