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 を介して、1メールずつ処理します。
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                         * メール受信処理で、1メール受信ごとに呼び出されます。
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}