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.LogWriter; 019 020import java.io.UnsupportedEncodingException; 021import java.util.Properties; 022import java.util.Date; 023 024import javax.activation.FileDataSource; 025import javax.activation.DataHandler; 026import javax.mail.internet.InternetAddress; 027import javax.mail.internet.AddressException; 028import javax.mail.internet.MimeMessage; 029import javax.mail.internet.MimeMultipart; 030import javax.mail.internet.MimeBodyPart; 031import javax.mail.internet.MimeUtility; 032import javax.mail.Authenticator; // 5.8.7.1 (2015/05/22) 033import javax.mail.PasswordAuthentication; // 5.8.7.1 (2015/05/22) 034import javax.mail.Store; 035import javax.mail.Transport; 036import javax.mail.Session; 037import javax.mail.Message; 038import javax.mail.MessagingException; 039import javax.mail.IllegalWriteException; 040 041/** 042 * MailTX は、SMTPプロトコルによるメール送信プログラムです。 043 * 044 * E-Mail で日本語を送信する場合、ISO-2022-JP(JISコード)化して、7bit で 045 * エンコードして送信する必要がありますが、Windows系の特殊文字や、unicodeと 046 * 文字のマッピングが異なる文字などが、文字化けします。 047 * 対応方法としては、 048 * 1.Windows-31J + 8bit 送信 049 * 2.ISO-2022-JP に独自変換 + 7bit 送信 050 * の方法があります。 051 * 今回、この2つの方法について、対応いたしました。 052 * 053 * @version 4.0 054 * @author Kazuhiko Hasegawa 055 * @since JDK5.0, 056 */ 057public class MailTX { 058 private static final String CR = System.getProperty("line.separator"); 059 private static final String AUTH_PBS = "POP_BEFORE_SMTP"; // 5.4.3.2 060 private static final String AUTH_SMTPA = "SMTP_AUTH"; // 5.4.3.2 5.8.7.1復活 061 062 /** メーラーの名称 {@value} */ 063 public static final String MAILER = "Hayabusa Mail Ver 4.0"; 064 065 private final String charset ; // Windwos-31J , MS932 , ISO-2022-JP 066 private String[] filename = null; 067 private String message = null; 068 private Session session = null; 069 private MimeMultipart mmPart = null; 070 private MimeMessage mimeMsg = null; 071 private MailCharset mcSet = null; 072 073 /** 074 * メールサーバーとデフォルト文字エンコーディングを指定して、オブジェクトを構築します。 075 * 076 * デフォルト文字エンコーディングは、ISO-2022-JP です。 077 * 078 * @param host メールサーバー 079 * @throws IllegalArgumentException 引数が null の場合。 080 */ 081 public MailTX( final String host ) { 082 this( host,"ISO-2022-JP" ); 083 } 084 085 /** 086 * メールサーバーとデフォルト文字エンコーディングを指定して、オブジェクトを構築します。 087 * 088 * 文字エンコーディングには、Windwos-31J , MS932 , ISO-2022-JP を指定できます。 089 * 090 * @og.rev 5.4.3.2 (2012/01/06) 認証対応のため 091 * @og.rev 5.8.1.1 (2014/11/14) 認証ポート追加 092 * @og.rev 5.9.29.2 (2018/02/16) STARTTLS対応 093 * 094 * @param host メールサーバー 095 * @param charset 文字エンコーディング 096 * @throws IllegalArgumentException 引数が null の場合。 097 */ 098 public MailTX( final String host , final String charset ) { 099// this( host,charset,null,null,null,null ); 100// this( host,charset,null,null,null,null,null ); 101 this( host,charset,null,null,null,null,null,false ); 102 } 103 104 /** 105 * メールサーバーと文字エンコーディングを指定して、オブジェクトを構築します。 106 * 認証を行う場合は認証方法を指定します。 107 * 108 * 文字エンコーディングには、Windwos-31J , MS932 , ISO-2022-JP を指定できます。 109 * 110 * @og.rev 5.1.9.0 (2010/08/01) mail.smtp.localhostの設定追加 111 * @og.rev 5.4.3.2 (2012/01/06) 認証対応(POP Before SMTP)。引数3つ追加(将来的にはAuthentication対応?) 112 * @og.rev 5.8.1.1 (2014/11/14) 認証ポート追加 113 * @og.rev 5.8.7.1 (2015/05/22) SMTP Auth対応 114 * @og.rev 5.9.29.2 (2018/02/16) STARTTLS対応 115 * @og.rev 5.10.20.1 (2020/03/03) 添付ファイル名文字化け対策 116 * 117 * @param host メールサーバー 118 * @param charset 文字エンコーディング 119 * @param smtpPort SMTPポート 120 * @param authType 認証方法 5.4.3.2 121 * @param authPort 認証ポート 5.4.3.2 122 * @param authUser 認証ユーザ 5.4.3.2 123 * @param authPass 認証パスワード 5.4.3.2 124 * @param useStarttls 暗号化通信設定(STARTTLS) 5.9.29.2 125 * @throws IllegalArgumentException 引数が null の場合。 126 */ 127// public MailTX( final String host , final String charset, final String port 128// ,final String auth, final String user, final String pass) { 129 public MailTX( final String host , final String charset, final String smtpPort 130 ,final String authType, final String authPort, final String authUser, final String authPass 131 ,final boolean useStarttls ) { 132 if( host == null ) { 133 String errMsg = "host に null はセット出来ません。"; 134 throw new IllegalArgumentException( errMsg ); 135 } 136 137 if( charset == null ) { 138 String errMsg = "charset に null はセット出来ません。"; 139 throw new IllegalArgumentException( errMsg ); 140 } 141 142 this.charset = charset; 143 144 mcSet = MailCharsetFactory.newInstance( charset ); 145 146 // 5.10.20.1 (2020/03/03) 添付ファイル名文字化け対策(暫定) 147 System.setProperty("mail.mime.splitlongparameters", "false"); 148 System.setProperty("mail.mime.encodeparameters", "false" ); 149 150 Properties prop = new Properties(); 151 prop.setProperty("mail.mime.charset", charset); 152 prop.setProperty("mail.mime.decodetext.strict", "false"); 153 prop.setProperty("mail.mime.address.strict", "false"); 154 prop.setProperty("mail.smtp.host", host); 155 // 5.1.9.0 (2010/08/01) 設定追加 156 prop.setProperty("mail.smtp.localhost", host); 157 prop.setProperty("mail.host", host); // MEssage-ID の設定に利用 158 // 5.4.3.2 ポート追加 159// if( port != null && port.length() > 0 ){ 160// prop.setProperty("mail.smtp.port", port); // MEssage-ID の設定に利用 161// } 162 if( smtpPort != null && smtpPort.length() > 0 ){ 163 prop.setProperty("mail.smtp.port", smtpPort); // MEssage-ID の設定に利用 164 } 165 166 // SMTP Auth対応 5.8.7.1 (2015/05/22) 167 Authenticator myAuth = null; 168 if( AUTH_SMTPA.equals( authType ) ) { 169 prop.setProperty("mail.smtp.auth", "true" ); 170 myAuth = new Authenticator() { // 5.8.7.1 (2015/05/22) SMTP認証用クラス 171 @Override 172 protected PasswordAuthentication getPasswordAuthentication() { 173 return new PasswordAuthentication( authUser,authPass ); 174 } 175 }; 176 } 177 178 // 5.9.29.2 (2018/02/16) STARTTLS対応 179 if ( useStarttls ) { 180 prop.setProperty("mail.smtp.starttls.enable", "true"); 181 prop.setProperty("mail.smtp.starttls.required", "true"); 182 // SSLの場合 183 //prop.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); 184 //prop.setProperty("mail.smtp.socketFactory.fallback", "false"); 185 } 186 187 session = Session.getInstance( prop, myAuth ); 188 189 // POP before SMTP認証処理 5.4.3.2 190// if(AUTH_PBS.equals( auth )){ 191 if(AUTH_PBS.equals( authType )){ 192 try{ 193 // 5.8.1.1 (2014/11/14) 認証ポート追加 194 int aPort = (authPass == null || authPass.isEmpty()) ? -1 : Integer.parseInt(authPort) ; 195 Store store = session.getStore("pop3"); 196// store.connect(host,-1,user,pass); // 同一ホストとする 197 store.connect(host,aPort,authUser,authPass); // 5.8.1.1 (2014/11/14) 認証ポート追加 198 store.close(); 199 } 200 catch(MessagingException ex){ 201// String errMsg = "POP3 Auth Exception: "+ host + "/" + user; 202 String errMsg = "POP3 Auth Exception: "+ host + "/" + authUser; 203 throw new RuntimeException( errMsg,ex ); 204 } 205 } 206 207 mimeMsg = new MimeMessage(session); 208 } 209 210 /** 211 * メールを送信します。 212 * 213 */ 214 public void sendmail() { 215 try { 216 mimeMsg.setSentDate( new Date() ); 217 218 if( filename == null || filename.length == 0 ) { 219 mcSet.setTextContent( mimeMsg,message ); 220 } 221 else { 222 mmPart = new MimeMultipart(); 223 mimeMsg.setContent( mmPart ); 224 // テキスト本体の登録 225 addMmpText( message ); 226 227 // 添付ファイルの登録 228 for( int i=0; i<filename.length; i++ ) { 229 addMmpFile( filename[i] ); 230 } 231 } 232 233 mimeMsg.setHeader("X-Mailer", MAILER ); 234 mimeMsg.setHeader("Content-Transfer-Encoding", mcSet.getBit() ); 235 Transport.send( mimeMsg ); 236 237 } 238 catch( AddressException ex ) { 239 String errMsg = "Address Exception: "; 240 throw new RuntimeException( errMsg,ex ); 241 } 242 catch ( MessagingException mex ) { 243 String errMsg = "MessagingException: "; 244 throw new RuntimeException( errMsg,mex ); 245 } 246 } 247 248 /** 249 * MimeMessageをリセットします。 250 * 251 * sendmail() でメールを送信後、セッションを閉じずに別のメールを送信する場合、 252 * リセットしてから、各種パラメータを再設定してください。 253 * その場合は、すべてのパラメータが初期化されていますので、もう一度 254 * 設定しなおす必要があります。 255 * 256 */ 257 public void reset() { 258 mimeMsg = new MimeMessage(session); 259 } 260 261 /** 262 * 送信元(FROM)アドレスをセットします。 263 * 264 * @param from 送信元(FROM)アドレス 265 */ 266 public void setFrom( final String from ) { 267 try { 268 if( from != null ) { 269 mimeMsg.setFrom( getAddress( from ) ); 270 } 271 } catch( AddressException ex ) { 272 String errMsg = "Address Exception: "; 273 throw new RuntimeException( errMsg,ex ); 274 } catch ( MessagingException mex ) { 275 String errMsg = "MessagingException: "; 276 throw new RuntimeException( errMsg,mex ); 277 } 278 } 279 280 /** 281 * 送信先(TO)アドレス配列をセットします。 282 * 283 * @param to 送信先(TO)アドレス配列 284 */ 285 public void setTo( final String[] to ) { 286 try { 287 if( to != null ) { 288 mimeMsg.setRecipients( Message.RecipientType.TO, getAddress( to ) ); 289 } 290 } catch( AddressException ex ) { 291 String errMsg = "Address Exception: "; 292 throw new RuntimeException( errMsg,ex ); 293 } catch ( MessagingException mex ) { 294 String errMsg = "MessagingException: "; 295 throw new RuntimeException( errMsg,mex ); 296 } 297 } 298 299 /** 300 * 送信先(CC)アドレス配列をセットします。 301 * 302 * @param cc 送信先(CC)アドレス配列 303 */ 304 public void setCc( final String[] cc ) { 305 try { 306 if( cc != null ) { 307 mimeMsg.setRecipients( Message.RecipientType.CC, getAddress( cc ) ); 308 } 309 } catch( AddressException ex ) { 310 String errMsg = "Address Exception: "; 311 throw new RuntimeException( errMsg,ex ); 312 } catch ( MessagingException mex ) { 313 String errMsg = "MessagingException: "; 314 throw new RuntimeException( errMsg,mex ); 315 } 316 } 317 318 /** 319 * 送信先(BCC)アドレス配列をセットします。 320 * 321 * @param bcc 送信先(BCC)アドレス配列 322 */ 323 public void setBcc( final String[] bcc ) { 324 try { 325 if( bcc != null ) { 326 mimeMsg.setRecipients( Message.RecipientType.BCC, getAddress( bcc ) ); 327 } 328 } catch( AddressException ex ) { 329 String errMsg = "Address Exception: "; 330 throw new RuntimeException( errMsg,ex ); 331 } catch ( MessagingException mex ) { 332 String errMsg = "MessagingException: "; 333 throw new RuntimeException( errMsg,mex ); 334 } 335 } 336 337 /** 338 * 送信先(TO)アドレス配列をクリアします。 339 * @og.rev 4.3.6.0 (2009/04/01) 新規追加 340 * 341 */ 342 public void clearTo() { 343 try { 344 mimeMsg.setRecipients( Message.RecipientType.TO, (InternetAddress[])null ); 345 } catch( IllegalWriteException ex ) { 346 String errMsg = "Address Exception: "; 347 throw new RuntimeException( errMsg,ex ); 348 } catch( IllegalStateException ex ) { 349 String errMsg = "Address Exception: "; 350 throw new RuntimeException( errMsg,ex ); 351 } catch ( MessagingException mex ) { 352 String errMsg = "MessagingException: "; 353 throw new RuntimeException( errMsg,mex ); 354 } 355 } 356 357 /** 358 * 送信先(CC)アドレス配列をクリアします。 359 * @og.rev 4.3.6.0 (2009/04/01) 新規追加 360 * 361 */ 362 public void clearCc() { 363 try { 364 mimeMsg.setRecipients( Message.RecipientType.CC, (InternetAddress[])null ); 365 } catch( IllegalWriteException ex ) { 366 String errMsg = "Address Exception: "; 367 throw new RuntimeException( errMsg,ex ); 368 } catch( IllegalStateException ex ) { 369 String errMsg = "Address Exception: "; 370 throw new RuntimeException( errMsg,ex ); 371 } catch ( MessagingException mex ) { 372 String errMsg = "MessagingException: "; 373 throw new RuntimeException( errMsg,mex ); 374 } 375 } 376 377 /** 378 * 送信先(BCC)アドレス配列をクリアします。 379 * @og.rev 4.3.6.0 (2009/04/01) 新規追加 380 * 381 */ 382 public void clearBcc() { 383 try { 384 mimeMsg.setRecipients( Message.RecipientType.BCC, (InternetAddress[])null ); 385 } catch( IllegalWriteException ex ) { 386 String errMsg = "Address Exception: "; 387 throw new RuntimeException( errMsg,ex ); 388 } catch( IllegalStateException ex ) { 389 String errMsg = "Address Exception: "; 390 throw new RuntimeException( errMsg,ex ); 391 } catch ( MessagingException mex ) { 392 String errMsg = "MessagingException: "; 393 throw new RuntimeException( errMsg,mex ); 394 } 395 } 396 397 /** 398 * 返信元(replyTo)アドレス配列をセットします。 399 * 400 * @param replyTo 返信元(replyTo)アドレス配列 401 */ 402 public void setReplyTo( final String[] replyTo ) { 403 try { 404 if( replyTo != null ) { 405 mimeMsg.setReplyTo( getAddress( replyTo ) ); 406 } 407 } catch( AddressException ex ) { 408 String errMsg = "Address Exception: "; 409 throw new RuntimeException( errMsg,ex ); 410 } catch ( MessagingException mex ) { 411 String errMsg = "MessagingException: "; 412 throw new RuntimeException( errMsg,mex ); 413 } 414 } 415 416 /** 417 * タイトルをセットします。 418 * 419 * @param subject タイトル 420 */ 421 public void setSubject( final String subject ) { 422 // Servlet からの読み込みは、iso8859_1 でエンコードされた文字が 423 // セットされるので、ユニコードに変更しておかないと文字化けする。 424 // JRun 3.0 では、問題なかったが、tomcat3.1 では問題がある。 425 try { 426 if( subject != null ) { 427 mimeMsg.setSubject( mcSet.encodeWord( subject ) ); 428 } 429 } catch( AddressException ex ) { 430 String errMsg = "Address Exception: "; 431 throw new RuntimeException( errMsg,ex ); 432 } catch ( MessagingException mex ) { 433 String errMsg = "MessagingException: "; 434 throw new RuntimeException( errMsg,mex ); 435 } 436 } 437 438 /** 439 * 添付ファイル名配列をセットします。 440 * 441 * @param fname 添付ファイル名配列 442 */ 443 public void setFilename( final String[] fname ) { 444 if( fname != null && fname.length > 0 ) { 445 int size = fname.length; 446 filename = new String[size]; 447 System.arraycopy( fname,0,filename,0,size ); 448 } 449 } 450 451 /** 452 * メッセージ(本文)をセットします。 453 * 454 * @param msg メッセージ(本文) 455 */ 456 public void setMessage( final String msg ) { 457 // なぜか、メッセージの最後は、<CR><LF>をセットしておく。 458 459 if( msg == null ) { message = CR; } 460 else { message = msg + CR; } 461 } 462 463 /** 464 * デバッグ情報の表示を行うかどうかをセットします。 465 * 466 * @param debug 表示有無[true/false] 467 */ 468 public void setDebug( final boolean debug ) { 469 session.setDebug( debug ); 470 } 471 472 /** 473 * 指定されたファイルをマルチパートに追加します。 474 * 475 * @param fileStr マルチパートするファイル名 476 */ 477 private void addMmpFile( final String fileStr ) { 478 try { 479 MimeBodyPart mbp = new MimeBodyPart(); 480 FileDataSource fds = new FileDataSource(fileStr); 481 mbp.setDataHandler(new DataHandler(fds)); 482 mbp.setFileName(MimeUtility.encodeText(fds.getName(), charset, "B")); 483 mbp.setHeader("Content-Transfer-Encoding", "base64"); 484 mmPart.addBodyPart(mbp); 485 } 486 catch( UnsupportedEncodingException ex ) { 487 String errMsg = "Multipart UnsupportedEncodingException: "; 488 throw new RuntimeException( errMsg,ex ); 489 } 490 catch ( MessagingException mex ) { 491 String errMsg = "MessagingException: "; 492 throw new RuntimeException( errMsg,mex ); 493 } 494 } 495 496 /** 497 * 指定された文字列をマルチパートに追加します。 498 * 499 * @param textStr マルチパートする文字列 500 */ 501 private void addMmpText( final String textStr ) { 502 try { 503 MimeBodyPart mbp = new MimeBodyPart(); 504 mbp.setText(textStr, charset); 505 mbp.setHeader("Content-Transfer-Encoding", mcSet.getBit()); 506 mmPart.addBodyPart(mbp, 0); 507 } 508 catch ( MessagingException mex ) { 509 String errMsg = "MessagingException: "; 510 throw new RuntimeException( errMsg,mex ); 511 } 512 } 513 514 /** 515 * 文字エンコードを考慮した InternetAddress を作成します。 516 * 517 * @param adrs オリジナルのアドレス文字列 518 * 519 * @return 文字エンコードを考慮した InternetAddress 520 */ 521 private InternetAddress getAddress( final String adrs ) { 522 final InternetAddress rtnAdrs ; 523 int sep = adrs.indexOf( '<' ); 524 if( sep >= 0 ) { 525 String address = adrs.substring( sep+1,adrs.indexOf( '>' ) ).trim(); 526 String personal = adrs.substring( 0,sep ).trim(); 527 528 rtnAdrs = mcSet.getAddress( address,personal ); 529 } 530 else { 531 try { 532 rtnAdrs = new InternetAddress( adrs ); 533 } 534 catch( AddressException ex ) { 535 String errMsg = "指定のアドレスをセットできません。" 536 + "adrs=" + adrs ; 537 throw new RuntimeException( errMsg,ex ); 538 } 539 } 540 541 return rtnAdrs ; 542 } 543 544 /** 545 * 文字エンコードを考慮した InternetAddress を作成します。 546 * これは、アドレス文字配列から、InternetAddress 配列を作成する、 547 * コンビニエンスメソッドです。 548 * 処理そのものは、#getAddress( String ) をループしているだけです。 549 * 550 * @param adrs アドレス文字配列 551 * 552 * @return 文字エンコード後のInternetAddress配列 553 * @see #getAddress( String ) 554 */ 555 private InternetAddress[] getAddress( final String[] adrs ) { 556 InternetAddress[] rtnAdrs = new InternetAddress[adrs.length]; 557 for( int i=0; i<adrs.length; i++ ) { 558 rtnAdrs[i] = getAddress( adrs[i] ); 559 } 560 561 return rtnAdrs ; 562 } 563 564 /** 565 * コマンドから実行できる、テスト用の main メソッドです。 566 * 567 * Usage: java org.opengion.fukurou.mail.MailTX <from> <to> <host> [<file> ....] 568 * で、複数の添付ファイルを送付することができます。 569 * 570 * @param args コマンド引数配列 571 * @throws Exception なんらかのエラーが発生した場合。 572 */ 573 public static void main( final String[] args ) throws Exception { 574 if(args.length < 3) { 575 LogWriter.log("Usage: java org.opengion.fukurou.mail.MailTX <from> <to> <host> [<file> ....]"); 576 return ; 577 } 578 579 String host = args[2] ; 580 String chset = "ISO-2022-JP" ; 581 582 MailTX sender = new MailTX( host,chset ); 583 584 sender.setFrom( args[0] ); 585 String[] to = { args[1] }; 586 sender.setTo( to ); 587 588 if( args.length > 3 ) { 589 String[] filename = new String[ args.length-3 ]; 590 for( int i=0; i<args.length-3; i++ ) { 591 filename[i] = args[i+3]; 592 } 593 sender.setFilename( filename ); 594 } 595 596 sender.setSubject( "メール送信テスト" ); 597 String msg = "これはテストメールです。" + CR + 598 "うまく受信できましたか?" + CR; 599 sender.setMessage( msg ); 600 601 sender.sendmail(); 602 } 603}