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