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