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