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