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をそのまま(前後の >, <)は取り除きます。 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}