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