/*
 * 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.LogWriter;

import java.io.UnsupportedEncodingException;
import java.util.Properties;
import java.util.Date;

import javax.activation.FileDataSource;
import javax.activation.DataHandler;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.AddressException;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeUtility;
import javax.mail.Authenticator;				// 5.8.7.1 (2015/05/22)
import javax.mail.PasswordAuthentication;		// 5.8.7.1 (2015/05/22)
import javax.mail.Store;
import javax.mail.Transport;
import javax.mail.Session;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.IllegalWriteException;

/**
 * MailTX は、ＳＭＴＰプロトコルによるメール送信プログラムです。
 *
 * E-Mail で日本語を送信する場合、ISO-2022-JP(JISコード)化して、7bit で
 * エンコードして送信する必要がありますが、Windows系の特殊文字や、unicodeと
 * 文字のマッピングが異なる文字などが、文字化けします。
 * 対応方法としては、
 * １．Windows-31J + 8bit 送信
 * ２．ISO-2022-JP に独自変換 + 7bit 送信
 * の方法があります。
 * 今回、この２つの方法について、対応いたしました。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class MailTX {
	private static final String CR = System.getProperty("line.separator");
	private static final String AUTH_PBS   = "POP_BEFORE_SMTP";		// 5.4.3.2
	private static final String AUTH_SMTPA = "SMTP_AUTH";			// 5.4.3.2  5.8.7.1復活

	/** メーラーの名称  {@value} */
	public static final String MAILER = "Hayabusa Mail Ver 4.0";

	private final String	charset	 ;	// Windwos-31J , MS932 , ISO-2022-JP
	private String[]	filename = null;
	private String		message	 = null;
	private Session		session	 = null;
	private MimeMultipart mmPart = null;
	private MimeMessage	mimeMsg	 = null;
	private MailCharset	mcSet	 = null;

	/**
	 * メールサーバーとデフォルト文字エンコーディングを指定して、オブジェクトを構築します。
	 *
	 * デフォルト文字エンコーディングは、ISO-2022-JP です。
	 *
	 * @param	host	メールサーバー
	 * @throws	IllegalArgumentException 引数が null の場合。
	 */
	public MailTX( final String host ) {
		this( host,"ISO-2022-JP" );
	}

	/**
	 * メールサーバーとデフォルト文字エンコーディングを指定して、オブジェクトを構築します。
	 *
	 * 文字エンコーディングには、Windwos-31J , MS932 , ISO-2022-JP を指定できます。
	 *
	 * @og.rev 5.4.3.2 (2012/01/06) 認証対応のため
	 * @og.rev 5.8.1.1 (2014/11/14) 認証ポート追加
	 * @og.rev 5.9.29.2 (2018/02/16) STARTTLS対応 
	 *
	 * @param	host	メールサーバー
	 * @param	charset	文字エンコーディング
	 * @throws	IllegalArgumentException 引数が null の場合。
	 */
	public MailTX( final String host , final String charset ) {
//		this( host,charset,null,null,null,null );
//		this( host,charset,null,null,null,null,null );
		this( host,charset,null,null,null,null,null,false );
	}

	/**
	 * メールサーバーと文字エンコーディングを指定して、オブジェクトを構築します。
	 * 認証を行う場合は認証方法を指定します。
	 *
	 * 文字エンコーディングには、Windwos-31J , MS932 , ISO-2022-JP を指定できます。
	 *
	 * @og.rev 5.1.9.0 (2010/08/01) mail.smtp.localhostの設定追加
	 * @og.rev 5.4.3.2 (2012/01/06) 認証対応(POP Before SMTP)。引数３つ追加(将来的にはAuthentication対応？)
	 * @og.rev 5.8.1.1 (2014/11/14) 認証ポート追加
	 * @og.rev 5.8.7.1 (2015/05/22) SMTP Auth対応
	 * @og.rev 5.9.29.2 (2018/02/16) STARTTLS対応
	 * @og.rev 5.10.20.1 (2020/03/03) 添付ファイル名文字化け対策
	 *
	 * @param	host	メールサーバー
	 * @param	charset	文字エンコーディング
	 * @param	smtpPort	SMTPポート
	 * @param	authType	認証方法 5.4.3.2
	 * @param	authPort	認証ポート 5.4.3.2
	 * @param	authUser	認証ユーザ 5.4.3.2
	 * @param	authPass	認証パスワード 5.4.3.2
	 * @param  useStarttls 暗号化通信設定（STARTTLS) 5.9.29.2
	 * @throws	IllegalArgumentException 引数が null の場合。
	 */
//	public MailTX( final String host , final String charset, final String port
//				,final String auth, final String user, final String pass) {
	public MailTX( final String host , final String charset, final String smtpPort
				,final String authType, final String authPort, final String authUser, final String authPass
				,final boolean useStarttls ) {
		if( host == null ) {
			String errMsg = "host に null はセット出来ません。";
			throw new IllegalArgumentException( errMsg );
		}

		if( charset == null ) {
			String errMsg = "charset に null はセット出来ません。";
			throw new IllegalArgumentException( errMsg );
		}

		this.charset = charset;

		mcSet = MailCharsetFactory.newInstance( charset );

		// 5.10.20.1 (2020/03/03) 添付ファイル名文字化け対策(暫定)
		System.setProperty("mail.mime.splitlongparameters", "false");
		System.setProperty("mail.mime.encodeparameters", "false" );
		
		Properties prop = new Properties();
		prop.setProperty("mail.mime.charset", charset);
		prop.setProperty("mail.mime.decodetext.strict", "false");
		prop.setProperty("mail.mime.address.strict", "false");
		prop.setProperty("mail.smtp.host", host);
		// 5.1.9.0 (2010/08/01) 設定追加
		prop.setProperty("mail.smtp.localhost", host);
		prop.setProperty("mail.host", host);	// MEssage-ID の設定に利用
		// 5.4.3.2 ポート追加
//		if( port != null && port.length() > 0 ){
//			prop.setProperty("mail.smtp.port", port);		// MEssage-ID の設定に利用
//		}
		if( smtpPort != null && smtpPort.length() > 0 ){
			prop.setProperty("mail.smtp.port", smtpPort);	// MEssage-ID の設定に利用
		}

		// SMTP Auth対応 5.8.7.1 (2015/05/22)
		Authenticator myAuth = null;
		if( AUTH_SMTPA.equals( authType ) ) {
			prop.setProperty("mail.smtp.auth", "true" );
			myAuth = new Authenticator() {					// 5.8.7.1 (2015/05/22) SMTP認証用クラス
				@Override
				protected PasswordAuthentication getPasswordAuthentication() {
					return new PasswordAuthentication( authUser,authPass );
				}
			};
		}
		
		// 5.9.29.2 (2018/02/16) STARTTLS対応  
		if ( useStarttls ) {
			prop.setProperty("mail.smtp.starttls.enable", "true");
			prop.setProperty("mail.smtp.starttls.required", "true");
			// SSLの場合
			//prop.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
			//prop.setProperty("mail.smtp.socketFactory.fallback", "false");
		}
		
		session = Session.getInstance( prop, myAuth );

		// POP before SMTP認証処理 5.4.3.2
//		if(AUTH_PBS.equals( auth )){
		if(AUTH_PBS.equals( authType )){
			try{
				// 5.8.1.1 (2014/11/14) 認証ポート追加
				int aPort = (authPass == null || authPass.isEmpty()) ? -1 : Integer.parseInt(authPort) ;
				Store store = session.getStore("pop3");
//				store.connect(host,-1,user,pass);				// 同一ホストとする
				store.connect(host,aPort,authUser,authPass);	// 5.8.1.1 (2014/11/14) 認証ポート追加
				store.close();
			}
			catch(MessagingException ex){
//				String errMsg = "POP3 Auth Exception: "+ host + "/" + user;
				String errMsg = "POP3 Auth Exception: "+ host + "/" + authUser;
				throw new RuntimeException( errMsg,ex );
			}
		}
		
		mimeMsg = new MimeMessage(session);
	}

	/**
	 * メールを送信します。
	 *
	 */
	public void sendmail() {
		try {
			mimeMsg.setSentDate( new Date() );

			if( filename == null || filename.length == 0 ) {
				mcSet.setTextContent( mimeMsg,message );
			}
			else {
				mmPart = new MimeMultipart();
				mimeMsg.setContent( mmPart );
				// テキスト本体の登録
				addMmpText( message );

				// 添付ファイルの登録
				for( int i=0; i<filename.length; i++ ) {
					addMmpFile( filename[i] );
				}
			}

			mimeMsg.setHeader("X-Mailer", MAILER );
			mimeMsg.setHeader("Content-Transfer-Encoding", mcSet.getBit() );
			Transport.send( mimeMsg );

		}
		catch( AddressException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		}
		catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * MimeMessageをリセットします。
	 *
	 * sendmail() でメールを送信後、セッションを閉じずに別のメールを送信する場合、
	 * リセットしてから、各種パラメータを再設定してください。
	 * その場合は、すべてのパラメータが初期化されていますので、もう一度
	 * 設定しなおす必要があります。
	 *
	 */
	public void reset() {
		mimeMsg = new MimeMessage(session);
	}

	/**
	 * 送信元(ＦＲＯＭ)アドレスをセットします。
	 *
	 * @param   from 送信元(ＦＲＯＭ)アドレス
	 */
	public void setFrom( final String from ) {
		try {
			if( from != null ) {
				mimeMsg.setFrom( getAddress( from ) );
			}
		} catch( AddressException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 送信先(ＴＯ)アドレス配列をセットします。
	 *
	 * @param   to 送信先(ＴＯ)アドレス配列
	 */
	public void setTo( final String[] to ) {
		try {
			if( to != null ) {
				mimeMsg.setRecipients( Message.RecipientType.TO, getAddress( to ) );
			}
		} catch( AddressException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 送信先(ＣＣ)アドレス配列をセットします。
	 *
	 * @param   cc 送信先(ＣＣ)アドレス配列
	 */
	public void setCc( final String[] cc ) {
		try {
			if( cc != null ) {
				mimeMsg.setRecipients( Message.RecipientType.CC, getAddress( cc ) );
			}
		} catch( AddressException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 送信先(ＢＣＣ)アドレス配列をセットします。
	 *
	 * @param   bcc 送信先(ＢＣＣ)アドレス配列
	 */
	public void setBcc( final String[] bcc ) {
		try {
			if( bcc != null ) {
				mimeMsg.setRecipients( Message.RecipientType.BCC, getAddress( bcc ) );
			}
		} catch( AddressException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 送信先(ＴＯ)アドレス配列をクリアします。
	 * @og.rev 4.3.6.0 (2009/04/01) 新規追加
	 *
	 */
	public void clearTo() {
		try {
			mimeMsg.setRecipients( Message.RecipientType.TO, (InternetAddress[])null );
		} catch( IllegalWriteException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch( IllegalStateException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 送信先(CC)アドレス配列をクリアします。
	 * @og.rev 4.3.6.0 (2009/04/01) 新規追加
	 *
	 */
	public void clearCc() {
		try {
			mimeMsg.setRecipients( Message.RecipientType.CC, (InternetAddress[])null );
		} catch( IllegalWriteException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch( IllegalStateException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 送信先(BCC)アドレス配列をクリアします。
	 * @og.rev 4.3.6.0 (2009/04/01) 新規追加
	 *
	 */
	public void clearBcc() {
		try {
			mimeMsg.setRecipients( Message.RecipientType.BCC, (InternetAddress[])null );
		} catch( IllegalWriteException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch( IllegalStateException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 返信元(replyTo)アドレス配列をセットします。
	 *
	 * @param   replyTo 返信元(replyTo)アドレス配列
	 */
	public void setReplyTo( final String[] replyTo ) {
		try {
			if( replyTo != null ) {
				mimeMsg.setReplyTo( getAddress( replyTo ) );
			}
		} catch( AddressException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * タイトルをセットします。
	 *
	 * @param   subject タイトル
	 */
	public void setSubject( final String subject ) {
		// Servlet からの読み込みは、iso8859_1 でエンコードされた文字が
		// セットされるので、ユニコードに変更しておかないと文字化けする。
		// JRun 3.0 では、問題なかったが、tomcat3.1 では問題がある。
		try {
			if( subject != null ) {
				mimeMsg.setSubject( mcSet.encodeWord( subject ) );
			}
		} catch( AddressException ex ) {
			String errMsg = "Address Exception: ";
			throw new RuntimeException( errMsg,ex );
		} catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 添付ファイル名配列をセットします。
	 *
	 * @param   fname 添付ファイル名配列
	 */
	public void setFilename( final String[] fname ) {
		if( fname != null && fname.length > 0 ) {
			int size = fname.length;
			filename = new String[size];
			System.arraycopy( fname,0,filename,0,size );
		}
	}

	/**
	 * メッセージ(本文)をセットします。
	 *
	 * @param   msg メッセージ(本文)
	 */
	public void setMessage( final String msg ) {
		// なぜか、メッセージの最後は、<CR><LF>をセットしておく。

		if( msg == null ) { message = CR; }
		else {              message = msg + CR; }
	}

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

	/**
	 * 指定されたファイルをマルチパートに追加します。
	 *
	 * @param   fileStr マルチパートするファイル名
	 */
	private void addMmpFile( final String fileStr ) {
		try {
			MimeBodyPart mbp = new MimeBodyPart();
			FileDataSource fds = new FileDataSource(fileStr);
			mbp.setDataHandler(new DataHandler(fds));
			mbp.setFileName(MimeUtility.encodeText(fds.getName(), charset, "B"));
			mbp.setHeader("Content-Transfer-Encoding", "base64");
			mmPart.addBodyPart(mbp);
		}
		catch( UnsupportedEncodingException ex ) {
			String errMsg = "Multipart UnsupportedEncodingException: ";
			throw new RuntimeException( errMsg,ex );
		}
		catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 指定された文字列をマルチパートに追加します。
	 *
	 * @param   textStr マルチパートする文字列
	 */
	private void addMmpText( final String textStr ) {
		try {
			MimeBodyPart mbp = new MimeBodyPart();
			mbp.setText(textStr, charset);
			mbp.setHeader("Content-Transfer-Encoding", mcSet.getBit());
			mmPart.addBodyPart(mbp, 0);
		}
		catch ( MessagingException mex ) {
			String errMsg = "MessagingException: ";
			throw new RuntimeException( errMsg,mex );
		}
	}

	/**
	 * 文字エンコードを考慮した InternetAddress を作成します。
	 *
	 * @param   adrs オリジナルのアドレス文字列
	 *
	 * @return  文字エンコードを考慮した InternetAddress
	 */
	private InternetAddress getAddress( final String adrs ) {
		final InternetAddress rtnAdrs ;
		int sep = adrs.indexOf( '<' );
		if( sep >= 0 ) {
			String address  = adrs.substring( sep+1,adrs.indexOf( '>' ) ).trim();
			String personal = adrs.substring( 0,sep ).trim();

			rtnAdrs = mcSet.getAddress( address,personal );
		}
		else {
			try {
				rtnAdrs = new InternetAddress( adrs );
			}
			catch( AddressException ex ) {
				String errMsg = "指定のアドレスをセットできません。"
									+ "adrs=" + adrs  ;
				throw new RuntimeException( errMsg,ex );
			}
		}

		return rtnAdrs ;
	}

	/**
	 * 文字エンコードを考慮した InternetAddress を作成します。
	 * これは、アドレス文字配列から、InternetAddress 配列を作成する、
	 * コンビニエンスメソッドです。
	 * 処理そのものは、#getAddress( String ) をループしているだけです。
	 *
	 * @param   adrs アドレス文字配列
	 *
	 * @return  文字エンコード後のInternetAddress配列
	 * @see     #getAddress( String )
	 */
	private InternetAddress[] getAddress( final String[] adrs ) {
		InternetAddress[] rtnAdrs = new InternetAddress[adrs.length];
		for( int i=0; i<adrs.length; i++ ) {
			rtnAdrs[i] = getAddress( adrs[i] );
		}

		return rtnAdrs ;
	}

	/**
	 * コマンドから実行できる、テスト用の main メソッドです。
	 *
	 * Usage: java org.opengion.fukurou.mail.MailTX &lt;from&gt; &lt;to&gt; &lt;host&gt; [&lt;file&gt; ....]
	 * で、複数の添付ファイルを送付することができます。
	 *
	 * @param	args	コマンド引数配列
	 * @throws Exception なんらかのエラーが発生した場合。
	 */
	public static void main( final String[] args ) throws Exception {
		if(args.length < 3) {
			LogWriter.log("Usage: java org.opengion.fukurou.mail.MailTX <from> <to> <host> [<file> ....]");
			return ;
		}

		String host  = args[2] ;
		String chset = "ISO-2022-JP" ;

		MailTX sender = new MailTX( host,chset );

		sender.setFrom( args[0] );
		String[] to = { args[1] };
		sender.setTo( to );

		if( args.length > 3 ) {
			String[] filename = new String[ args.length-3 ];
			for( int i=0; i<args.length-3; i++ ) {
				filename[i] = args[i+3];
			}
			sender.setFilename( filename );
		}

		sender.setSubject( "メール送信テスト" );
		String msg = "これはテストメールです。" + CR +
						"うまく受信できましたか?" + CR;
		sender.setMessage( msg );

		sender.sendmail();
	}
}
