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