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