    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.FileUtil;
    019import org.opengion.fukurou.util.UnicodeCorrecter;                                      // 5.9.3.3 (2015/12/26) package を、mail → util に移動のため
    020import org.opengion.fukurou.system.OgRuntimeException ;                         // 6.4.2.0 (2016/01/29)
    021
    022import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.1.0.0 (2014/12/26) refactoring
    023import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
    024
    025import java.io.IOException;
    026import java.io.UnsupportedEncodingException;
    027import java.io.File;
    028import java.io.PrintWriter;
    029import java.util.Date;
    030import java.util.Enumeration;
    031import java.util.Map;
    032import java.util.LinkedHashMap;
    033import java.util.Collections;                                                                           // 6.4.3.1 (2016/02/12) refactoring
    034
    035import jakarta.mail.Header;
    036import jakarta.mail.Part;
    037import jakarta.mail.BodyPart;
    038import jakarta.mail.Multipart;
    039import jakarta.mail.Message;
    040import jakarta.mail.MessagingException;
    041import jakarta.mail.Flags;
    042import jakarta.mail.internet.MimeMessage;
    043import jakarta.mail.internet.MimeUtility;
    044import jakarta.mail.internet.InternetAddress;
    045
    046/**
    047 * MailMessage は、受信メールを処理するためのラッパークラスです。
    048 *
    049 * メッセージオブジェクトを引数にとるコンストラクタによりオブジェクトが作成されます。
    050 * 日本語処置などを簡易的に扱えるように、ラッパクラス的な使用方法を想定しています。
    051 * 必要であれば(例えば、添付ファイルを取り出すために、MailAttachFiles を利用する場合など)
    052 * 内部のメッセージオブジェクトを取り出すことが可能です。
    053 * MailReceiveListener クラスの receive( MailMessage ) メソッドで、メールごとにイベントが
    054 * 発生して、処理する形態が一般的です。
    055 *
    056 * @version  4.0
    057 * @author   Kazuhiko Hasegawa
    058 * @since    JDK5.0,
    059 */
    060public class MailMessage {
    061
    062        private static final String MSG_EX = "メッセージ情報のハンドリングに失敗しました。" ;
    063
    064        private final String  host ;
    065        private final String  user ;
    066        private final Message message ;
    067        /** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。 */
    068        private final Map<String,String> headerMap ;
    069
    070        private String subject   ;
    071        private String content   ;
    072        private String messageID ;
    073
    074        /**
    075         * メッセージオブジェクトを指定して構築します。
    076         *
    077         * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。
    078         *
    079         * @param message メッセージオブジェクト
    080         * @param host ホスト
    081         * @param user ユーザー
    082         */
    083        public MailMessage( final Message message,final String host,final String user ) {
    084                this.host = host;
    085                this.user = user;
    086                this.message = message;
    087                headerMap    = makeHeaderMap( null );                   // 6.4.3.1 (2016/02/12)
    088        }
    089
    090        /**
    091         * 内部の メッセージオブジェクトを返します。
    092         *
    093         * @return メッセージオブジェクト
    094         */
    095        public Message getMessage() {
    096                return message;
    097        }
    098
    099        /**
    100         * 内部の ホスト名を返します。
    101         *
    102         * @return      ホスト名
    103         */
    104        public String getHost() {
    105                return host;
    106        }
    107
    108        /**
    109         * 内部の ユーザー名を返します。
    110         *
    111         * @return      ユーザー名
    112         */
    113        public String getUser() {
    114                return user;
    115        }
    116
    117        /**
    118         * メールのヘッダー情報を文字列に変換して返します。
    119         * キーは、ヘッダー情報の取り出しと同一です。
    120         * 例) Return-Path,Delivered-To,Date,From,To,Cc,Subject,Content-Type,Message-Id
    121         *
    122         * @param       key メールのヘッダーキー
    123         *
    124         * @return      キーに対するメールのヘッダー情報
    125         */
    126        public String getHeader( final String key ) {
    127                return headerMap.get( key );
    128        }
    129
    130        /**
    131         * メールの指定のヘッダー情報を文字列に変換して返します。
    132         * ヘッダー情報の取り出しキーと同一の項目を リターンコードで結合しています。
    133         * Return-Path,Delivered-To,Date,From,To,Cc,Subject,Content-Type,Message-Id
    134         *
    135         * @return      メールの指定のヘッダー情報
    136         * @og.rtnNotNull
    137         */
    138        public String getHeaders() {
    139                final String[] keys = headerMap.keySet().toArray( new String[headerMap.size()] );
    140                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
    141                for( int i=0; i<keys.length; i++ ) {
    142                        buf.append( keys[i] ).append(':').append( headerMap.get( keys[i] ) ).append( CR );              // 6.0.2.5 (2014/10/31) char を append する。
    143                }
    144                return buf.toString();
    145        }
    146
    147        /**
    148         * メールのタイトル(Subject)を返します。
    149         * 日本語文字コード処理も行っています。(JIS→unicode変換等)
    150         *
    151         * @og.rev 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列を mimeDecode でデコードします。
    152         *
    153         * @return      メールのタイトル
    154         */
    155        public String getSubject() {
    156                if( subject == null ) {
    157                        try {
    158                                subject = mimeDecode( message.getSubject() );
    159                        }
    160                        catch( final MessagingException ex ) {
    161                                // メッセージ情報のハンドリングに失敗しました。
    162                                throw new OgRuntimeException( MSG_EX,ex );
    163                        }
    164                }
    165                if( subject == null ) { subject = "No Subject" ;}
    166                return subject;
    167        }
    168
    169        /**
    170         * メールの本文(Content)を返します。
    171         * 日本語文字コード処理も行っています。(JIS→unicode変換等)
    172         *
    173         * @return      メールの本文
    174         */
    175        public String getContent() {
    176                if( content == null ) {
    177                        content = UnicodeCorrecter.correctToCP932( mime2str( message ) );
    178                }
    179                return content;
    180        }
    181
    182        /**
    183         * メッセージID を取得します。
    184         *
    185         * 基本的には、メッセージIDをそのまま(前後の &gt;, &lt;)は取り除きます。
    186         * メッセージIDのないメールは、"unknown." + SentData + "." + From という文字列を
    187         * 作成します。
    188         * さらに、送信日やFrom がない場合、または、文字列として取り出せない場合、
    189         * "unknown" を返します。
    190         *
    191         * @og.rev 4.3.3.5 (2008/11/08) 送信時刻がNULLの場合の処理を追加
    192         *
    193         * @return メッセージID
    194         */
    195        public String getMessageID() {
    196                if( messageID == null ) {
    197                        try {
    198                                messageID = ((MimeMessage)message).getMessageID();
    199                                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
    200                                if( messageID == null ) {
    201                                        // 4.3.3.5 (2008/11/08) SentDate が null のケースがあるため。
    202                                        Date dt = message.getSentDate();
    203                                        if( dt == null ) { dt = message.getReceivedDate(); }
    204                                        final Long date = (dt == null) ? 0L : dt.getTime();
    205                                        final String from = ((InternetAddress[])message.getFrom())[0].getAddress() ;
    206                                        messageID = "unknown." + date + "." + from ;
    207                                }
    208                                else {
    209                                        messageID = messageID.substring(1,messageID.length()-1) ;
    210                                }
    211                        }
    212                        catch( final MessagingException ex ) {
    213                                // メッセージ情報のハンドリングに失敗しました。
    214                                throw new OgRuntimeException( MSG_EX,ex );
    215                        }
    216                }
    217                return messageID ;
    218        }
    219
    220        /**
    221         * メッセージをメールサーバーから削除するかどうかをセットします。
    222         *
    223         * @param       flag    削除するかどうか [true:行う/false:行わない]
    224         */
    225        public void deleteMessage( final boolean flag ) {
    226                try {
    227                        message.setFlag(Flags.Flag.DELETED, flag);
    228                }
    229                catch( final MessagingException ex ) {
    230                        // メッセージ情報のハンドリングに失敗しました。
    231                        throw new OgRuntimeException( MSG_EX,ex );
    232                }
    233        }
    234
    235        /**
    236         * メールの内容を文字列として表現します。
    237         * デバッグや、簡易的なメールの内容の取り出し、エラー時のメール保存に使用します。
    238         *
    239         * @return      メールの内容の文字列表現
    240         * @og.rtnNotNull
    241         */
    242        public String getSimpleMessage() {
    243                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
    244                        .append( getHeaders() ).append( CR )
    245                        .append( "Subject:" ).append( getSubject() ).append( CR )
    246                        .append( "===============================" ).append( CR )
    247                        .append( getContent() ).append( CR )
    248                        .append( "===============================" ).append( CR );
    249
    250                return buf.toString();
    251        }
    252
    253        /**
    254         * メールの内容と、あれば添付ファイルを指定のフォルダにセーブします。
    255         * saveMessage( dir )と、saveAttachFiles( dir,true ) を同時に呼び出しています。
    256         *
    257         * @param       dir     メールと添付ファイルをセーブするフォルダ
    258         */
    259        public void saveSimpleMessage( final String dir ) {
    260
    261                saveMessage( dir );
    262
    263                saveAttachFiles( dir,true );
    264        }
    265
    266        /**
    267         * メールの内容を文字列として指定のフォルダにセーブします。
    268         * メッセージID.txt という本文にセーブします。
    269         * デバッグや、簡易的なメールの内容の取り出し、エラー時のメール保存に使用します。
    270         *
    271         * @param       dir     メールの内容をセーブするフォルダ
    272         */
    273        public void saveMessage( final String dir ) {
    274
    275                final String msgId = getMessageID() ;
    276
    277                // 3.8.0.0 (2005/06/07) FileUtil#getPrintWriter を利用。
    278                final File file = new File( dir,msgId + ".txt" );
    279                final PrintWriter writer = FileUtil.getPrintWriter( file,"UTF-8" );
    280                writer.println( getSimpleMessage() );
    281
    282                writer.close();
    283        }
    284
    285        /**
    286         * メールの添付ファイルが存在する場合に、指定のフォルダにセーブします。
    287         *
    288         * 添付ファイルが存在する場合のみ、処理を実行します。
    289         * useMsgId にtrue を設定すると、メッセージID というフォルダを作成し、その下に、
    290         * 連番 + "_" + 添付ファイル名 でセーブします。(メールには同一ファイル名を複数添付できる為)
    291         * false の場合は、指定のディレクトリ直下に、連番 + "_" + 添付ファイル名 でセーブします。
    292         *
    293         * @og.rev 4.3.3.5 (2008/11/08) ディレクトリ指定時のセパレータのチェックを追加
    294         *
    295         * @param       dir     添付ファイルをセーブするフォルダ
    296         * @param       useMsgId        メッセージIDフォルダを作成してセーブ場合：true
    297         *          指定のディレクトリ直下にセーブする場合：false
    298         */
    299        public void saveAttachFiles( final String dir,final boolean useMsgId ) {
    300
    301                final String attDirStr ;
    302                if( useMsgId ) {
    303                        final String msgId = getMessageID() ;
    304                        // 4.3.3.5 (2008/11/08) ディレクトリ指定時のセパレータのチェックを追加
    305                        if( dir.endsWith( "/" ) ) {
    306                                attDirStr = dir + msgId + "/";
    307                        }
    308                        else {
    309                                attDirStr = dir + "/" + msgId + "/";
    310                        }
    311                }
    312                else {
    313                        attDirStr = dir ;
    314                }
    315
    316                final MailAttachFiles attFiles = new MailAttachFiles( message );
    317                final String[] files = attFiles.getNames();
    318                if( files.length > 0 ) {
    319        //              String attDirStr = dir + "/" + msgId + "/";
    320        //              File attDir = new File( attDirStr );
    321        //              if( !attDir.exists() ) {
    322        //                      if( ! attDir.mkdirs() ) {
    323        //                      final String errMsg = "添付ファイルのディレクトリの作成に失敗しました。[" + attDirStr + "]";
    324        //                              throw new OgRuntimeException( errMsg );
    325        //                      }
    326        //              }
    327
    328                        // 添付ファイル名を指定しないと、番号 + "_" + 添付ファイル名になる。
    329                        for( int i=0; i<files.length; i++ ) {
    330                                attFiles.saveFileName( attDirStr,null,i );
    331                        }
    332                }
    333        }
    334
    335        /**
    336         * 受領確認がセットされている場合の 返信先アドレスを返します。
    337         * セットされていない場合は、null を返します。
    338         * 受領確認は、Disposition-Notification-To ヘッダにセットされる事とし、
    339         * このヘッダの内容を返します。セットされていなければ、null を返します。
    340         *
    341         * @return 返信先アドレス(Disposition-Notification-To ヘッダの内容)
    342         */
    343        public String getNotificationTo() {
    344                return headerMap.get( "Disposition-Notification-To" );
    345        }
    346
    347        /**
    348         * ヘッダー情報を持った、Enumeration から、ヘッダーと値のペアの文字列を作成します。
    349         *
    350         * ヘッダー情報は、Message#getAllHeaders() か、Message#getMatchingHeaders( String[] )
    351         * で得られる Enumeration に、Header オブジェクトとして取得できます。
    352         * このヘッダーオブジェクトから、キー(getName()) と値(getValue()) を取り出します。
    353         * 結果は、キー:値 の文字列として、リターンコードで区切ります。
    354         *
    355         * @og.rev 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列を mimeDecode でデコードします。
    356         * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。
    357         *
    358         * @param headerList ヘッダー情報配列
    359         *
    360         * @return ヘッダー情報の キー:値 のMap
    361         */
    362        private Map<String,String> makeHeaderMap( final String[] headerList ) {
    363                final Map<String,String> headMap = Collections.synchronizedMap( new LinkedHashMap<>() );
    364                try {
    365                        final Enumeration<?> enume;             // 4.3.3.6 (2008/11/15) Generics警告対応
    366                        if( headerList == null ) {
    367                                enume = message.getAllHeaders();
    368                        }
    369                        else {
    370                                enume = message.getMatchingHeaders( headerList );
    371                        }
    372
    373                        while( enume.hasMoreElements() ) {
    374                                final Header header = (Header)enume.nextElement();
    375                                final String name  = header.getName();
    376                                // 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列を mimeDecode でデコードします。
    377                                // 6.4.9.1 (2016/08/05) refactoring
    378                                final String val = headMap.get( name );
    379                                // 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列を mimeDecode でデコードします。
    380                                final String value = ( val == null ) ? mimeDecode( header.getValue() )
    381                                                                                                         : val + "," + mimeDecode( header.getValue() );
    382
    383                                headMap.put( name,value );
    384                        }
    385                }
    386                catch( final MessagingException ex2 ) {
    387                        // メッセージ情報のハンドリングに失敗しました。
    388                        throw new OgRuntimeException( MSG_EX,ex2 );
    389                }
    390
    391                return headMap;
    392        }
    393
    394        /**
    395         * Part オブジェクトから、最初に見つけた text/plain を取り出します。
    396         *
    397         * Part は、マルチパートというPartに複数のPartを持っていたり、さらにその中にも
    398         * Part を持っているような構造をしています。
    399         * ここでは、最初に見つけた、MimeType が、text/plain の場合に、文字列に
    400         * 変換して、返しています。それ以外の場合、再帰的に、text/plain が
    401         * 見つかるまで、処理を続けます。
    402         * また、特別に、HN0256 からのトラブルメールは、Content-Type が、text/plain のみに
    403         * なっている為 CONTENTS が、JIS のまま、取り出されてしまうため、強制的に
    404         * Content-Type を、"text/plain; charset=iso-2022-jp" に変更しています。
    405         *
    406         * @param       part Part最大取り込み件数
    407         *
    408         * @return 最初の text/plain 文字列。見つからない場合は、null を返します。
    409         * @throws MessagingException jakarta.mail 関連のエラーが発生したとき
    410         * @throws IOException 入出力エラーが発生したとき
    411         */
    412        private String mime2str( final Part part ) {
    413                String content = null;
    414
    415                try {
    416                        if( part.isMimeType("text/plain") ) {
    417                                // HN0256 からのトラブルメールは、Content-Type が、text/plain のみになっている為
    418                                // CONTENTS が、JIS のまま、取り出されてしまう。強制的に変更しています。
    419                                if( "text/plain".equalsIgnoreCase( part.getContentType() ) ) {
    420                                        final MimeMessage msg = new MimeMessage( (MimeMessage)part );
    421                                        msg.setHeader( "Content-Type","text/plain; charset=iso-2022-jp" );
    422                                        content = (String)msg.getContent();
    423                                }
    424                                else {
    425                                        content = (String)part.getContent();
    426                                }
    427                        }
    428                        else if( part.isMimeType("message/rfc822") ) {          // Nested Message
    429                                content = mime2str( (Part)part.getContent() );
    430                        }
    431                        else if( part.isMimeType("multipart/*") ) {
    432                                final Multipart mp = (Multipart)part.getContent();
    433
    434                                final int count = mp.getCount();
    435                                for( int i=0; i<count; i++ ) {
    436                                        final BodyPart bp = mp.getBodyPart(i);
    437                                        content = mime2str( bp );
    438                                        if( content != null ) { break; }
    439                                }
    440                        }
    441                }
    442                catch( final MessagingException ex ) {
    443                        // メッセージ情報のハンドリングに失敗しました。
    444                        throw new OgRuntimeException( MSG_EX,ex );
    445                }
    446                catch( final IOException ex2 ) {
    447                        final String errMsg = "テキスト情報の取り出しに失敗しました。" ;
    448                        throw new OgRuntimeException( errMsg,ex2 );
    449                }
    450
    451                return content ;
    452        }
    453
    454        /**
    455         * エンコードされた文字列を、デコードします。
    456         *
    457         * MIMEエンコード は、 =? で開始するエンコード文字列 ですが、場合によって、前のスペースが
    458         * 存在しない場合があります。
    459         * また、メーラーによっては、エンコード文字列を ダブルコーテーションでくくる処理が入っている
    460         * 場合もあります。
    461         * これらの一連のエンコード文字列をデコードします。
    462         *
    463         * @og.rev 4.3.3.5 (2008/11/08) 日本語MIMEエンコードされた文字列をデコードします。
    464         *
    465         * @param       text    エンコードされた文字列(されていない場合は、そのまま返します)
    466         *
    467         * @return      デコードされた文字列
    468         */
    469        public static final String mimeDecode( final String text ) {
    470                if( text == null || text.indexOf( "=?" ) < 0 ) { return text; }
    471
    472                String rtnText = text.replace( '\t',' ' );              // 若干トリッキーな処理
    473                try {
    474                        // encode-word の =? の前にはスペースが必要。
    475                        // ここでは、分割して、デコード処理を行うことで、対応
    476                        int pos1 = rtnText.indexOf( "=?" );                     // デコードの開始
    477                        int pos2 = 0;                                                           // デコードの終了
    478                        final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
    479                                .append( rtnText.substring( 0,pos1 ) );
    480                        while( pos1 >= 0 ) {
    481                                pos2 = rtnText.indexOf( "?=",pos1 ) + 2;                // デコードの終了
    482                                final String sub = rtnText.substring( pos1,pos2 );
    483                                buf.append( UnicodeCorrecter.correctToCP932( MimeUtility.decodeText( sub ) ) );
    484                                pos1 = rtnText.indexOf( "=?",pos2 );                    // デコードの開始
    485                                if( pos1 > 0 ) {
    486                                        buf.append( rtnText.substring( pos2,pos1 ) );
    487                                }
    488                        }
    489                        buf.append( rtnText.substring( pos2 ) );
    490                        rtnText = buf.toString() ;
    491                }
    492                catch( final UnsupportedEncodingException ex ) {
    493                        final String errMsg = "テキスト情報のデコードに失敗しました。" ;
    494                        throw new OgRuntimeException( errMsg,ex );
    495                }
    496                return rtnText;
    497        }
    498}