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