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