    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.fukurou.mail;
    017
    018import org.opengion.fukurou.util.StringUtil ;
    019import org.opengion.fukurou.util.HybsEntry ;
    020import org.opengion.fukurou.system.LogWriter;
    021
    022import java.util.Properties;
    023import java.util.List;
    024import java.util.ArrayList;
    025import java.util.Locale;                                                                // 6.3.8.0 (2015/09/11)
    026
    027import jakarta.mail.Session;
    028import jakarta.mail.Store;
    029import jakarta.mail.Folder;
    030import jakarta.mail.Message;
    031import jakarta.mail.Flags;
    032import jakarta.mail.MessagingException;
    033import jakarta.mail.NoSuchProviderException;
    034import jakarta.mail.search.SearchTerm;
    035import jakarta.mail.search.SubjectTerm;
    036import jakarta.mail.search.FromStringTerm;
    037import jakarta.mail.search.BodyTerm;
    038import jakarta.mail.search.HeaderTerm;
    039import jakarta.mail.search.AndTerm;
    040
    041/**
    042 * MailRX は、POP3/IMAPプロトコルによるメール受信プログラムです。
    043 *
    044 * メールへの接続条件(host,user,passwd など)と、選択条件(matchTermなど)を指定し、
    045 * MailReceiveListener をセットして、start() メソッドを呼びます。
    046 * 実際のメール処理は、MailReceiveListener を介して、１メールずつ処理します。
    047 * 添付ファイルを処理する場合は、MailAttachFiles クラスを使用します。
    048 *
    049 *        host          メールサーバー(必須)
    050 *        user          メールを取得するログインユーザー(必須)
    051 *        passwd        メールを取得するログインパスワード(必須)
    052 *        protocol      受信サーバーのプロトコル[imap/pop3]を指定(初期値:{@og.value #PROTOCOL})
    053 *        port          受信サーバーのポートを指定(初期値:{@og.value #PORT})
    054 *        useSSL        SSL接続するかどうか[true:する/false:しない]を指定(初期値:false:しない)
    055 *        mbox          受信サーバーのメールボックスを指定(初期値:{@og.value #MBOX})
    056 *        maxRowCount   受信メールの最大取り込み件数(初期値:{@og.value #MAX_ROW_COUNT})(0:[無制限])
    057 *        charset       メールのデフォルトエンコード(初期値:{@og.value #CHARSET})
    058 *        matchTerm     受信メールを選択する条件のMINEntryオブジェクト
    059 *        delete        検索後、メールをサーバーから削除するかどうかを、true/falseで指定(初期値:{@og.value #DELETE_MESSAGE})。
    060 *
    061 * ※ 6.3.8.0 (2015/09/11)
    062 *    useSSL属性は、protocolに、pop3s/imaps を指定した場合、
    063 *    自動的に、ture に設定するようにしています。
    064 *
    065 * @version  4.0
    066 * @author   Kazuhiko Hasegawa
    067 * @since    JDK5.0,
    068 */
    069public class MailRX {
    070
    071        /** 受信メールの最大取り込み件数を指定します 「={@value}」  */
    072        public static final int MAX_ROW_COUNT = 100 ;
    073
    074        /** 検索後、メールをサーバーから削除するかどうかを、true/falseで指定します 「={@value}」  */
    075        public static final boolean DELETE_MESSAGE = false ;
    076
    077        /** メールサーバーのデフォルトプロトコル 「={@value}」  */
    078        public static final String PROTOCOL = "pop3" ;
    079
    080        /** メールサーバーのデフォルトポート番号 「={@value}」  */
    081        public static final int PORT = -1 ;
    082
    083        /** メールサーバーのデフォルトメールボックス 「={@value}」。  */
    084        public static final String MBOX = "INBOX" ;
    085
    086        /** メールのデフォルトエンコード 「={@value}」
    087         * Windwos-31J , MS932 , UTF-8 , ISO-2022-JP を指定します。
    088         */
    089        public static final String CHARSET = "ISO-2022-JP" ;
    090
    091        // メール受信毎に発生するイベントを伝えるリスナーを登録します。
    092        private MailReceiveListener listener ;
    093
    094        private String  host            ;
    095        private String  user            ;
    096        private String  passwd          ;
    097        private String  protocol        = PROTOCOL;
    098        private int             port            = PORT;
    099        private boolean isUseSSL        ;                                               // 6.3.8.0 (2015/09/11)
    100        private String  mbox            = MBOX;
    101        private boolean deleteFlag      = DELETE_MESSAGE;
    102        private String  charset         = CHARSET;
    103        private int             maxRowCount     = MAX_ROW_COUNT;
    104
    105        private final List<HybsEntry>   matchList       = new ArrayList<>();
    106        private boolean debug           ;
    107
    108        /**
    109         * レシーバーを開始します。
    110         *
    111         * @og.rev 6.3.8.0 (2015/09/11) SSL接続するかどうかを指定するパラメータを追加します。
    112         *
    113         * @throws MessagingException レシーバー処理中に、なんらかのエラーが発生した場合。
    114         * @throws NoSuchProviderException なんらかのエラーが発生した場合。
    115         */
    116        public void start() throws MessagingException,NoSuchProviderException {
    117
    118                // パラメータの解析、取得
    119                debugMsg( "パラメータの解析、取得" );
    120
    121                // 指定の条件にマッチしたメッセージのみ抜き出す為の、SearchTerm オブジェクトの作成
    122                // 6.3.8.0 (2015/09/11) IMAPの場合、条件の有無で、メッセージの取得方法を変える必要がる。
    123                SearchTerm srchTerm = null;
    124                if( !matchList.isEmpty() ) {
    125                        debugMsg( "指定の条件にマッチしたメッセージのみ抜き出す条件を設定します。"  );
    126                        final HybsEntry[] matchs = matchList.toArray( new HybsEntry[matchList.size()] );
    127                        SearchTerm[] term = new SearchTerm[matchs.length];
    128                        for( int i=0; i<matchs.length; i++ ) {
    129                                final String key = matchs[i].getKey();
    130                                if( "Subject".equalsIgnoreCase( key ) ) {
    131                                        term[i] = new SubjectTerm( matchs[i].getValue() );
    132                                }
    133                                else if( "From".equalsIgnoreCase( key ) ) {
    134                                        term[i] = new FromStringTerm( matchs[i].getValue() );
    135                                }
    136                                else if( "Body".equalsIgnoreCase( key ) ) {
    137                                        term[i] = new BodyTerm( matchs[i].getValue() );
    138                                }
    139                                else {
    140                                        term[i] = new HeaderTerm( key,matchs[i].getValue() );
    141                                }
    142                        }
    143                        srchTerm = new AndTerm( term );
    144                }
    145
    146                // 空の properties を設定。気休め程度に、初期値を設定しておきます。
    147                debugMsg( "空の properties を設定"  );
    148                final Properties prop = new Properties();
    149                prop.setProperty("mail.mime.charset"                    , charset );
    150                prop.setProperty("mail.mime.decodetext.strict"  , "false" );
    151                prop.setProperty("mail.mime.address.strict"             , "false" );
    152
    153                // 6.3.8.0 (2015/09/11) SSL接続するかどうかを指定するパラメータを追加します。
    154                if( isUseSSL ) {
    155                        if( protocol.contains( "pop3" ) ) {                     // pop3/pop3s
    156                                prop.setProperty("mail.pop3.socketFactory.class"        , "javax.net.ssl.SSLSocketFactory" );
    157                                prop.setProperty("mail.pop3.socketFactory.fallback"     , "false" );
    158                                prop.setProperty("mail.pop3.socketFactory.port"         , String.valueOf( port ) );             // 995
    159                        }
    160                        // google の IMAP の場合は、下記設定なしでも、protocol=imaps のみで接続できた。
    161                        else if( protocol.contains( "imap" ) ) {        // imap/imaps
    162                                prop.setProperty("mail.imap.ssl.enable"                                 , "true" );
    163                                prop.setProperty("mail.imap.ssl.socketFactory.class"    , "DummySSLSocketFactory" );
    164                                prop.setProperty("mail.imap.ssl.socketFactory.fallback" , "false" );
    165                        }
    166                }
    167
    168                // session を取得
    169                debugMsg( "session を取得" );
    170                final Session session = Session.getInstance( prop, null );
    171
    172                Store store = null;
    173                Folder folder = null;
    174                try {
    175                        // store の取得
    176                        debugMsg( "store の取得 protocol=",protocol );
    177                        store = session.getStore( protocol );
    178
    179                        // サーバーと connect します。
    180                        debugMsg( "サーバーと connect します。" );
    181                        store.connect( host, port, user, passwd );
    182
    183                        // folder の取得
    184                        debugMsg( "folder の取得" );
    185                        folder = store.getFolder( mbox );
    186                        if( deleteFlag ) {
    187                                folder.open( Folder.READ_WRITE );
    188                        }
    189                        else {
    190                                folder.open( Folder.READ_ONLY );
    191                        }
    192
    193                        // メッセージ情報の取得
    194                        debugMsg( "メッセージ情報の取得" );
    195                        // 6.3.8.0 (2015/09/11) IMAPの場合、条件の有無で、メッセージの取得方法を変える必要がる。
    196                        final Message[] message = srchTerm == null ? folder.getMessages() : folder.search( srchTerm ) ;
    197
    198                        final int len = message.length;                         // 6.1.0.0 (2014/12/26) refactoring
    199                        for( int i=0; i<len && i<maxRowCount; i++ ) {
    200                                final MailMessage mailMessage = new MailMessage( message[i],host,user );
    201                                debugMsg( "[" , String.valueOf(i) , "]" , mailMessage.getMessageID() , " 受信中" );
    202
    203                                // メールの削除[true/false]:先にフラグを立てているので、エラーでも削除されます。
    204                                // 6.3.8.0 (2015/09/11) deleteFlag で、READ_ONLY かどうかを指定しているため、セットの判定を入れます。
    205                                if( deleteFlag ) {
    206                                        message[i].setFlag( Flags.Flag.DELETED, true );
    207                                }
    208
    209                                boolean okFlag = true;
    210                                if( listener != null ) {
    211                                        // メール本体の処理
    212                                        okFlag = listener.receive( mailMessage );
    213                                }
    214
    215                                // 受領確認の返信メール
    216                                final String notifyTo = mailMessage.getNotificationTo() ;
    217                                if( okFlag && notifyTo != null ) {
    218                                        final MailTX tx = new MailTX( host );
    219                                        tx.setFrom( user );
    220                                        tx.setTo( StringUtil.csv2Array( notifyTo ) );
    221                                        tx.setSubject( "受領:" + mailMessage.getSubject() );
    222                                        tx.setMessage( mailMessage.getContent() );
    223                                        tx.sendmail();
    224                                }
    225                        }
    226                }
    227                finally {
    228                        // セッション終了
    229                        debugMsg( "セッション終了処理" );
    230                        if( folder != null ) {
    231                                folder.close( deleteFlag );             // true の場合は、終了時に実際に削除します。
    232                        }
    233                        if( store != null ) {
    234                                store.close();
    235                        }
    236                }
    237        }
    238
    239        /**
    240         * メールサーバーをセットします(必須)。
    241         *
    242         * @param       host メールサーバー
    243         * @throws      IllegalArgumentException 引数が null の場合。
    244         */
    245        public void setHost( final String host ) {
    246                if( host == null ) {
    247                        final String errMsg = "host に null はセット出来ません。";
    248                        throw new IllegalArgumentException( errMsg );
    249                }
    250
    251                this.host = host;
    252        }
    253
    254        /**
    255         * 受信ユーザーをセットします(必須)。
    256         *
    257         * @param       user 受信ユーザー
    258         * @throws      IllegalArgumentException 引数が null の場合。
    259         */
    260        public void setUser( final String user ) {
    261                if( user == null ) {
    262                        final String errMsg = "user に null はセット出来ません。";
    263                        throw new IllegalArgumentException( errMsg );
    264                }
    265                this.user = user;
    266        }
    267
    268        /**
    269         * パスワードをセットします(必須)。
    270         *
    271         * @param       passwd パスワード
    272         * @throws      IllegalArgumentException 引数が null の場合。
    273         */
    274        public void setPasswd( final String passwd ) {
    275                if( passwd == null ) {
    276                        final String errMsg = "passwd に null はセット出来ません。";
    277                        throw new IllegalArgumentException( errMsg );
    278                }
    279                this.passwd = passwd;
    280        }
    281
    282        /**
    283         * 受信プロトコル(pop3/imap等)をセットします(初期値:{@og.value #PROTOCOL})。
    284         *
    285         * protocolに、pop3s/imaps を指定した場合、
    286         * useSSL属性は、自動的に、ture に設定されます。
    287         *
    288         * @param       prtcol 受信プロトコル名
    289         * @throws      IllegalArgumentException 引数が null の場合。
    290         * @see         #PROTOCOL
    291         */
    292        public void setProtocol( final String prtcol ) {
    293                if( prtcol == null ) {
    294                        final String errMsg = "protocol に null はセット出来ません。";
    295                        throw new IllegalArgumentException( errMsg );
    296                }
    297                protocol = prtcol.toLowerCase( Locale.JAPAN );
    298
    299                // 6.3.8.0 (2015/09/11) 登録順に影響されない様に、注意
    300                if( port < 0 ) {                // 未設定
    301                        if( "pop3".equalsIgnoreCase(  protocol ) ) { port = 110; }
    302                        if( "imap".equalsIgnoreCase(  protocol ) ) { port = 143; isUseSSL = true; }
    303                        if( "pop3s".equalsIgnoreCase( protocol ) ) { port = 995; }
    304                        if( "imaps".equalsIgnoreCase( protocol ) ) { port = 993; isUseSSL = true; }
    305                }
    306        }
    307
    308        /**
    309         * ポート番号をセットします(初期値:{@og.value #PORT})。
    310         *
    311         * portが、-1 の場合は、protocol に応じたポートが使用されます。
    312         * pop3:110 , imap:143 , pop3s:995 , imaps:993
    313         *
    314         * @param       port ポート番号
    315         * @see         #PORT
    316         */
    317        public void setPort( final int port ) {
    318                this.port = port;
    319        }
    320
    321        /**
    322         * SSL接続するかどうかをセットします(初期値:false:しない)。
    323         *
    324         * protocolに、pop3s/imaps を指定した場合、
    325         * useSSL属性は、自動的に、ture に設定されます。
    326         *
    327         * @og.rev 6.3.8.0 (2015/09/11) SSL接続するかどうかを指定するパラメータを追加します。
    328         *
    329         * @param       isSSL SSL接続するかどうか[true:する/false:しない]を指定
    330         */
    331        public void useSSL( final boolean isSSL ) {
    332                // 6.3.8.0 (2015/09/11) 登録順に影響されない様に、注意
    333                isUseSSL = isSSL || "pop3s".equalsIgnoreCase( protocol ) || "imaps".equalsIgnoreCase( protocol );
    334        }
    335
    336        /**
    337         * 受信メイルボックスをセットします(初期値:{@og.value #MBOX})。
    338         *
    339         * @param       mbox 受信メイルボックス名
    340         * @throws      IllegalArgumentException 引数が null の場合。
    341         * @see         #MBOX
    342         */
    343        public void setMbox( final String mbox ) {
    344                if( mbox == null ) {
    345                        final String errMsg = "mbox に null はセット出来ません。";
    346                        throw new IllegalArgumentException( errMsg );
    347                }
    348                this.mbox = mbox;
    349        }
    350
    351        /**
    352         * メール受信毎に発生するイベントを伝えるリスナーをセットします。
    353         *
    354         * @param       listener MailReceiveリスナー
    355         */
    356        public void setMailReceiveListener( final MailReceiveListener listener ) {
    357                this.listener = listener;
    358        }
    359
    360        /**
    361         * メッセージをメールサーバーから削除するかどうかをセットします(初期値:{@og.value #DELETE_MESSAGE})。
    362         *
    363         * @param       deleteFlag 削除するかどうか[true:行う/false:行わない]
    364         * @see         #DELETE_MESSAGE
    365         */
    366        public void setDelete( final boolean deleteFlag ) {
    367                this.deleteFlag = deleteFlag;
    368        }
    369
    370        /**
    371         * 文字エンコーディングをセットします(初期値:{@og.value #CHARSET})。
    372         *
    373         * 文字エンコーディングには、Windwos-31J , MS932 , ISO-2022-JP を指定できます。
    374         * 初期値は、SystemResource.properties ファイルの MAIL_DEFAULT_CHARSET 属性で
    375         * 設定できます。
    376         *
    377         * @param   charset 文字エンコーディング
    378         * @throws      IllegalArgumentException 引数が null の場合。
    379         * @see         #CHARSET
    380         */
    381        public void setCharset( final String charset ) {
    382                if( charset == null ) {
    383                        final String errMsg = "charset に null はセット出来ません。";
    384                        throw new IllegalArgumentException( errMsg );
    385                }
    386                this.charset = charset;
    387        }
    388
    389        /**
    390         * 最大取り込み件数をセットします(初期値:{@og.value #MAX_ROW_COUNT})(0:[無制限])。
    391         *
    392         * @og.rev 5.5.8.5 (2012/11/27) 0を無制限として処理します。
    393         *
    394         * @param       maxCount 最大取り込み件数
    395         * @see         #MAX_ROW_COUNT
    396         */
    397        public void setMaxRowCount( final int maxCount ) {
    398                maxRowCount = maxCount>0 ? maxCount : Integer.MAX_VALUE ;                       // 6.0.2.5 (2014/10/31) refactoring
    399        }
    400
    401        /**
    402         * メール検索する場合のマッチ条件のキーと値の HybsEntry をセットします。
    403         * Subject,From,Body,それ以外は、Header 文字列をキーにします。
    404         *
    405         * @param       matchTerm HybsEntryオブジェクト
    406         */
    407        public void addMatchTerm( final HybsEntry matchTerm ) {
    408                matchList.add( matchTerm );
    409        }
    410
    411        /**
    412         * デバッグ情報の表示を行うかどうかをセットします。
    413         *
    414         * @param       debug 有無[true/false]
    415         */
    416        public void setDebug( final boolean debug ) {
    417                this.debug = debug;
    418        }
    419
    420        /**
    421         * デバッグ情報の表示を行います。
    422         * 実際の処理は、debug フラグに設定値によります。
    423         *
    424         * @param       msgs デバッグ情報(可変長引数)
    425         */
    426        private void debugMsg( final String... msgs ) {
    427                if( debug ) {
    428                        for( final String msg : msgs ) {
    429                                System.out.print( msg );
    430                        }
    431                        System.out.println();
    432                }
    433        }
    434
    435        /**
    436         * コマンドから実行できる、テスト用の main メソッドです。
    437         *
    438         * Usage: java org.opengion.fukurou.mail.MailTX MailRX host user passwd [saveDir]
    439         * で、複数の添付ファイルを送付することができます。
    440         *
    441         * @og.rev 6.3.9.1 (2015/11/27) A method/constructor shouldnt explicitly throw java.lang.Exception(PMD)。
    442         *
    443         * @param   args 引数配列
    444         * @throws MessagingException なんらかのエラーが発生した場合。
    445         */
    446        public static void main( final String[] args ) throws MessagingException {
    447                if( args.length<3 ) {
    448                        LogWriter.log("Usage: java org.opengion.fukurou.mail.MailRX host user passwd [saveDir]");
    449                        System.exit(1);
    450                }
    451                final String dir = (args.length == 4) ? args[3] : null;
    452
    453                final MailRX recive = new MailRX();
    454
    455                recive.setHost( args[0] );
    456                recive.setUser( args[1] );
    457                recive.setPasswd( args[2] );
    458                recive.setCharset( "ISO-2022-JP" );
    459
    460                final MailReceiveListener listener = new MailReceiveListener() {
    461                        /**
    462                         * メール受信処理で、１メール受信ごとに呼び出されます。
    463                         * 処理結果を、boolean で返します。
    464                         *
    465                         * @param message MailMessageオブジェクト
    466                         * @return      処理結果(正常:true / 異常:false)
    467                         */
    468                        public boolean receive( final MailMessage message ) {
    469                                System.out.println( message.getSimpleMessage() );
    470
    471                                if( dir != null ) {
    472                                        message.saveSimpleMessage( dir );
    473                                }
    474                                return true ;
    475                        }
    476                };
    477                recive.setMailReceiveListener( listener );
    478
    479                recive.start();
    480        }
    481}