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.InputStream; 019import java.io.OutputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.ByteArrayInputStream; 022import java.io.UnsupportedEncodingException; 023import java.io.IOException; 024 025import javax.activation.DataHandler; 026import javax.activation.DataSource; 027import javax.mail.internet.InternetAddress; 028import javax.mail.internet.MimeMessage; 029import javax.mail.internet.MimeUtility; 030import javax.mail.MessagingException; 031import com.sun.mail.util.BASE64EncoderStream; 032 033import java.nio.charset.Charset; // 5.5.2.6 (2012/05/25) 034 035/** 036 * MailCharset は、E-Mail 送信時のエンコードに応じた処理を行う為の、 037 * インターフェースです。 038 * 039 * E-Mail で日本語を送信する場合、ISO-2022-JP(JISコード)化して、7bit で 040 * エンコードして送信する必要がありますが、Windows系の特殊文字や、unicodeと 041 * 文字のマッピングが異なる文字などが、文字化けします。 042 * 対応方法としては、 043 * 『1.Windows-31J + 8bit 送信』 044 * 『2.ISO-2022-JP に独自変換 + 7bit 送信』 045 * の方法があります。 046 * 今回、この2つの方法について、それぞれサブクラス化を行い、処理できるように 047 * したのが、このインターフェース、および、サブクラスです。 048 * 049 * 『1.Windows-31J + 8bit 送信』の方法は、通常の JavaMail API に準拠して 050 * 処理を行う、Mail_Windows31J_Charset サブクラスで実装しています。 051 * 古いメイラーおよび、古いメールサーバーではメール転送できない為、 052 * この方式は、社内で使用する場合のみに、利用できますが、主としてWindows系の 053 * 社内システムにおいては、こちらの方が、なにかとトラブルは少ないと思います。 054 * 055 * 『2.ISO-2022-JP に独自変換 + 7bit 送信』の実装は、 056 * JAVA PRESS Vol.37 (http://www.gihyo.co.jp/magazines/javapress)の 057 * 【特集1】 決定版! サーバサイドJavaの日本語処理 058 * 第3章:JavaMailの日本語処理プログラミング……木下信 059 *“マルチプラットフォーム”な日本語メール送信術 完全解説 060 * でのサンプルアプリケーション 061 * http://www.gihyo.co.jp/book/2004/225371/download/toku1_3.zip 062 * を、使用して、Mail_ISO2022JP_Charset サブクラスで実装しています。 063 * 064 * これらのサブクラスは、MailCharsetFactory ファクトリクラスより、作成されます。 065 * その場合、引数のキャラクタセット名は、Windows-31J 、MS932 か、それ以外となっています。 066 * それ以外が指定された場合は、ISO-2022-JP を使用します。 067 * 068 * @version 4.0 069 * @author Kazuhiko Hasegawa 070 * @since JDK5.0, 071 */ 072public interface MailCharset { 073 074 /** 075 * テキストをセットします。 076 * Part#setText() の代わりにこちらを使うようにします。 077 * 078 * @param mimeMsg MimeMessage最大取り込み件数 079 * @param text 設定するテキスト 080 * @throws RuntimeException(MessagingException) 081 */ 082 void setTextContent( MimeMessage mimeMsg, String text ) ; 083 084 /** 085 * 日本語を含むヘッダ用テキストを生成します。 086 * 変換結果は ASCII なので、これをそのまま setSubject や InternetAddress 087 * のパラメタとして使用してください。 088 * 089 * @param text 設定するテキスト 090 * 091 * @return 日本語を含むヘッダ用テキスト 092 * @throws RuntimeException(UnsupportedEncodingException) 093 */ 094 String encodeWord( String text ) ; 095 096 /** 097 * 日本語を含むアドレスを生成します。 098 * personal に、日本語が含まれると想定しています。 099 * サブクラスで、日本語処理を行う場合の方法は、それぞれ異なります。 100 * 101 * @param address アドレス部分 102 * @param personal 日本語の説明部分 103 * 104 * @return 日本語を含むアドレス 105 * @throws RuntimeException(UnsupportedEncodingException) 106 */ 107 InternetAddress getAddress( String address,String personal ) ; 108 109 /** 110 * Content-Transfer-Encoding を指定する場合の ビット数を返します。 111 * 112 * Windows系は、8bit / ISO-2022-JP 系は、7bit になります。 113 * 114 * @return ビット数 115 */ 116 String getBit() ; 117} 118 119/** 120 * MailCharsetFactory は、MailCharset インターフェースを実装したサブクラスを 121 * 作成する ファクトリクラスです。 122 * 123 * 引数のキャラクタセット名が、Windows-31J 、MS932 の場合は、 124 * 『1.Windows-31J + 8bit 送信』 の実装である、Mail_Windows31J_Charset 125 * サブクラスを返します。 126 * それ以外が指定された場合は、ISO-2022-JP を使用して、『2.ISO-2022-JP に独自変換 + 7bit 送信』 127 * の実装である、Mail_ISO2022JP_Charset サブクラスを返します。 128 * 129 * @version 4.0 130 * @author Kazuhiko Hasegawa 131 * @since JDK5.0, 132 */ 133class MailCharsetFactory { 134 135 /** 136 * インスタンスの生成を抑止します。 137 */ 138 private MailCharsetFactory() { 139 // 何もありません。(PMD エラー回避) 140 } 141 142 /** 143 * キャラクタセットに応じた、MailCharset オブジェクトを返します。 144 * 145 * Windows-31J 、MS932 、Shift_JIS の場合は、Mail_Windows31J_Charset 146 * その他は、ISO-2022-JP として、Mail_ISO2022JP_Charset を返します。 147 * 148 * 注意:null の場合は、デフォルトではなく、Mail_ISO2022JP_Charset を返します。 149 * 150 * @param charset キャラクタセット[Windows-31J/MS932/Shift_JIS/その他] 151 * 152 * @return MailCharset 153 */ 154 public static MailCharset newInstance( final String charset ) { 155 final MailCharset mcset; 156 157 if( "MS932".equalsIgnoreCase( charset ) || 158 "Shift_JIS".equalsIgnoreCase( charset ) || 159 "Windows-31J".equalsIgnoreCase( charset ) ) { 160 mcset = new Mail_Windows31J_Charset( charset ); 161 } 162 else { 163 mcset = new Mail_ISO2022JP_Charset(); 164 } 165 return mcset ; 166 } 167} 168 169/** 170 * MailCharset インターフェースを実装した Windwos-31J エンコード時のサブクラスです。 171 * 172 * 『1.Windows-31J + 8bit 送信』 の実装です。 173 * 174 * @version 4.0 175 * @author Kazuhiko Hasegawa 176 * @since JDK5.0, 177 */ 178class Mail_Windows31J_Charset implements MailCharset { 179 private final String charset ; // "Windows-31J" or "MS932" 180 181 /** 182 * 引数に、エンコード方式を指定して、作成するコンストラクタです。 183 * 184 * @param charset String 185 */ 186 public Mail_Windows31J_Charset( final String charset ) { 187 this.charset = charset; 188 } 189 190 /** 191 * テキストをセットします。 192 * Part#setText() の代わりにこちらを使うようにします。 193 * 194 * @param mimeMsg MimeMessage 195 * @param text String 196 * @throws RuntimeException(MessagingException) 197 */ 198 public void setTextContent( final MimeMessage mimeMsg, final String text ) { 199 try { 200 mimeMsg.setText( text,charset ); // "text/plain" Content 201 } 202 catch( MessagingException ex ) { 203 String errMsg = "指定のテキストをセットできません。" 204 + "text=" + text + " , charset=" + charset ; 205 throw new RuntimeException( errMsg,ex ); 206 } 207 } 208 209 /** 210 * 日本語を含むヘッダ用テキストを生成します。 211 * 変換結果は ASCII なので、これをそのまま setSubject や InternetAddress 212 * のパラメタとして使用してください。 213 * 214 * @param text String 215 * 216 * @return 日本語を含むヘッダ用テキスト 217 * @throws RuntimeException(UnsupportedEncodingException) 218 */ 219 public String encodeWord( final String text ) { 220 try { 221 return MimeUtility.encodeText( text, charset, "B" ); 222 } 223 catch( UnsupportedEncodingException ex ) { 224 String errMsg = "指定のエンコードが出来ません。" 225 + "text=" + text + " , charset=" + charset ; 226 throw new RuntimeException( errMsg,ex ); 227 } 228 } 229 230 /** 231 * 日本語を含むアドレスを生成します。 232 * personal に、日本語が含まれると想定しています。 233 * サブクラスで、日本語処理を行う場合の方法は、それぞれ異なります。 234 * 235 * @param address String 236 * @param personal String 237 * 238 * @return InternetAddress 239 * @throws RuntimeException(UnsupportedEncodingException) 240 */ 241 public InternetAddress getAddress( final String address,final String personal ) { 242 try { 243 return new InternetAddress( address,personal,charset ); 244 } 245 catch( UnsupportedEncodingException ex ) { 246 String errMsg = "指定のエンコードが出来ません。" 247 + "address=" + address + " , charset=" + charset ; 248 throw new RuntimeException( errMsg,ex ); 249 } 250 } 251 252 /** 253 * Content-Transfer-Encoding を指定する場合の ビット数を返します。 254 * 255 * Windows系は、8bit / ISO-2022-JP 系は、7bit になります。 256 * 257 * @return ビット数("8bit" 固定) 258 */ 259 public String getBit() { 260 return "8bit" ; 261 } 262} 263 264/** 265 * MailCharset インターフェースを実装した ISO-2022-JP エンコード時のサブクラスです。 266 * 267 * 『2.ISO-2022-JP に独自変換 + 7bit 送信』 の実装です。 268 * 269 * @version 4.0 270 * @author Kazuhiko Hasegawa 271 * @since JDK5.0, 272 */ 273class Mail_ISO2022JP_Charset implements MailCharset { 274 275 /** 276 * プラットフォーム依存のデフォルトの Charset です。 277 * プラットフォーム依存性を考慮する場合、エンコード指定で作成しておく事をお勧めします。 278 * 279 * @og.rev 5.5.2.6 (2012/05/25) findbugs対応 280 */ 281 private static final Charset DEFAULT_CHARSET = Charset.defaultCharset() ; 282 283 /** 284 * テキストをセットします。 285 * Part#setText() の代わりにこちらを使うようにします。 286 * 287 * @param mimeMsg MimeMessage 288 * @param text String 289 * @throws RuntimeException(MessagingException) 290 */ 291 public void setTextContent( final MimeMessage mimeMsg, final String text ) { 292 try { 293 // mimeMsg.setText(text, "ISO-2022-JP"); 294 mimeMsg.setDataHandler(new DataHandler(new JISDataSource(text))); 295 } 296 catch( MessagingException ex ) { 297 String errMsg = "指定のテキストをセットできません。" 298 + "text=" + text ; 299 throw new RuntimeException( errMsg,ex ); 300 } 301 } 302 303 /** 304 * 日本語を含むヘッダ用テキストを生成します。 305 * 変換結果は ASCII なので、これをそのまま setSubject や InternetAddress 306 * のパラメタとして使用してください。 307 * 308 * @param text String 309 * 310 * @return 日本語を含むヘッダ用テキスト 311 * @throws RuntimeException(UnsupportedEncodingException) 312 */ 313 public String encodeWord( final String text ) { 314 try { 315 return "=?ISO-2022-JP?B?" + 316 new String( 317 BASE64EncoderStream.encode( 318 CharCodeConverter.sjisToJis( 319 UnicodeCorrecter.correctToCP932(text).getBytes("Windows-31J") 320 ) 321 ) 322 ,DEFAULT_CHARSET ) + "?="; // 5.5.2.6 (2012/05/25) findbugs対応 323 } 324 catch( UnsupportedEncodingException ex ) { 325 String errMsg = "指定のエンコードが出来ません。" 326 + "text=" + text + " , charset=Windows-31J" ; 327 throw new RuntimeException( errMsg,ex ); 328 } 329 } 330 331 /** 332 * 日本語を含むアドレスを生成します。 333 * personal に、日本語が含まれると想定しています。 334 * サブクラスで、日本語処理を行う場合の方法は、それぞれ異なります。 335 * 336 * @param address String 337 * @param personal String 338 * 339 * @return InternetAddress 340 * @throws RuntimeException(UnsupportedEncodingException) 341 */ 342 public InternetAddress getAddress( final String address,final String personal ) { 343 try { 344 return new InternetAddress( address,encodeWord( personal ) ); 345 } 346 catch( UnsupportedEncodingException ex ) { 347 String errMsg = "指定のエンコードが出来ません。" 348 + "address=" + address ; 349 throw new RuntimeException( errMsg,ex ); 350 } 351 } 352 353 /** 354 * Content-Transfer-Encoding を指定する場合の ビット数を返します。 355 * 356 * Windows系は、8bit / ISO-2022-JP 系は、7bit になります。 357 * 358 * @return ビット数("7bit" 固定) 359 */ 360 public String getBit() { 361 return "7bit" ; 362 } 363} 364 365/** 366 * テキストの本文を送信するための DataSource です。 367 * 368 * Windows-31J でバイトコードに変換した後、独自エンコードにて、 369 * Shift-JIS ⇒ JIS 変換しています。 370 * 371 * @version 4.0 372 * @author Kazuhiko Hasegawa 373 * @since JDK5.0, 374 */ 375class JISDataSource implements DataSource { 376 private final byte[] data; 377 378 public JISDataSource( final String str ) { 379 try { 380 data = CharCodeConverter.sjisToJis( 381 UnicodeCorrecter.correctToCP932(str).getBytes("Windows-31J")); 382 383 } catch (UnsupportedEncodingException e) { 384 String errMsg = "Windows-31J でのエンコーディングが出来ません。" + str; 385 throw new RuntimeException( errMsg,e ); 386 } 387 } 388 389 /** 390 * データの MIME タイプを文字列の形で返します。 391 * かならず有効なタイプを返すべきです。 392 * DataSource の実装がデータタイプを 決定できない場合は、 393 * getContentType は "application/octet-stream" を返すことを 提案します。 394 * 395 * @return MIME タイプ 396 */ 397 public String getContentType() { 398 return "text/plain; charset=ISO-2022-JP"; 399 } 400 401 /** 402 * データを表す InputStream を返します。 403 * それができない場合は適切な例外をスローします。 404 * 405 * @return InputStream 406 * @throws IOException 407 */ 408 public InputStream getInputStream() throws IOException { 409 return new ByteArrayInputStream( data ); 410 } 411 412 /** 413 * データが書込可能なら OutputStream を返します。 414 * それができない場合は適切な例外をスローします。 415 * 416 * ※ このクラスでは実装されていません。 417 * 418 * @return OutputStream 419 * @throws IOException 420 */ 421 public OutputStream getOutputStream() throws IOException { 422 String errMsg = "このクラスでは実装されていません。"; 423 // throw new UnsupportedOperationException( errMsg ); 424 throw new IOException( errMsg ); 425 } 426 427 /** 428 * このオブジェクトの '名前' を返します。 429 * この名前は下層のオブジェクトの性質によります。 430 * ファイルをカプセル化する DataSource なら オブジェクトの 431 * ファイル名を返すようにするかもしれません。 432 * 433 * @return オブジェクトの名前 434 */ 435 public String getName() { 436 return "JISDataSource"; 437 } 438} 439 440/** 441 * 文字関係のコンバータです。 442 * 一部コードのオリジナルは<a href="http://www-cms.phys.s.u-tokyo.ac.jp/~naoki/CIPINTRO/CCGI/kanjicod.html">Japanese Kanji Code</a>にて公開されているものです。 443 * また、http://www.sk-jp.com/cgi-bin/treebbs.cgi?kako=1&all=644&s=681 444 * にて YOSI さんが公開されたコードも参考にしています(というか実質同じです)。 445 * 446 * @version 4.0 447 * @author Kazuhiko Hasegawa 448 * @since JDK5.0, 449 */ 450class CharCodeConverter { 451 private static final byte[] SJIS_KANA; // 5.1.9.0 (2010/09/01) public ⇒ private へ変更 452 453 /** 454 * インスタンスの生成を抑止します。 455 */ 456 private CharCodeConverter() { 457 // 何もありません。(PMD エラー回避) 458 } 459 460 static { 461 try { 462 // 全角への変換テーブル 463 SJIS_KANA = "。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゛゜".getBytes("Shift_JIS"); 464 } catch( UnsupportedEncodingException ex ) { 465 throw new RuntimeException( "CANT HAPPEN",ex ); 466 } 467 } 468 469 /** 470 * Shift_JIS エンコーディングスキームに基づくバイト列を 471 * ISO-2022-JP エンコーディングスキームに変換します。 472 * 「半角カナ」は対応する全角文字に変換します。 473 * 474 * @param sjisBytes byte[] エンコードするShift_JISバイト配列 475 * 476 * @return byte[] 変換後のISO-2022-JP(JIS)バイト配列(not null) 477 */ 478 public static byte[] sjisToJis( final byte[] sjisBytes ) { 479 ByteArrayOutputStream out = new ByteArrayOutputStream(); 480 boolean nonAscii = false; 481 int len = sjisBytes.length; 482 for(int i = 0; i < len; i++ ) { 483 if(sjisBytes[i] >= 0) { 484 if(nonAscii) { 485 nonAscii = false; 486 out.write(0x1b); 487 out.write('('); 488 out.write('B'); 489 } 490 out.write(sjisBytes[i]); 491 } else { 492 if(!nonAscii) { 493 nonAscii = true; 494 out.write(0x1b); 495 out.write('$'); 496 out.write('B'); 497 } 498 int bt = sjisBytes[i] & 0xff; 499 if(bt >= 0xa1 && bt <= 0xdf) { 500 // 半角カナは全角に変換 501 int kanaIndex = (bt - 0xA1) * 2; 502 sjisToJis(out, SJIS_KANA[kanaIndex], SJIS_KANA[kanaIndex + 1]); 503 } else { 504 i++; 505 if(i == len) { break; } 506 sjisToJis(out, sjisBytes[i - 1], sjisBytes[i]); 507 } 508 } 509 } 510 if(nonAscii) { 511 out.write(0x1b); 512 out.write('('); 513 out.write('B'); 514 } 515 return out.toByteArray(); 516 } 517 518 /** 519 * 1文字の2バイト Shift_JIS コードを JIS コードに変換して書き出します。 520 */ 521 private static void sjisToJis( 522 final ByteArrayOutputStream out, final byte bhi, final byte blo) { 523 int hi = (bhi << 1) & 0xFF; 524 int lo = blo & 0xFF; 525 if(lo < 0x9F) { 526 if(hi < 0x3F) { hi += 0x1F; } else { hi -= 0x61; } 527 if(lo > 0x7E) { lo -= 0x20; } else { lo -= 0x1F; } 528 } else { 529 if(hi < 0x3F) { hi += 0x20; } else { hi -= 0x60; } 530 lo -= 0x7E; 531 } 532 out.write(hi); 533 out.write(lo); 534 } 535} 536 537/** 538 * unicode と、JIS との文字コードの関係で、変換しています。 539 * 540 * 0x301c(〜) を、0xff5e(~) へ、 541 * 0x2016(‖) を、0x2225(∥) へ、 542 * 0x2212(−) を、0xff0d(-) へ、 543 * それぞれコード変換します。 544 * 545 * @version 4.0 546 * @author Kazuhiko Hasegawa 547 * @since JDK5.0, 548 */ 549class UnicodeCorrecter { 550 551 /** 552 * インスタンスの生成を抑止します。 553 */ 554 private UnicodeCorrecter() { 555 // 何もありません。(PMD エラー回避) 556 } 557 558 /** 559 * Unicode 文字列の補正を行います。 560 * "MS932" コンバータでエンコードしようとした際に 561 * 正常に変換できない部分を補正します。 562 */ 563 public static String correctToCP932( final String str ) { 564 String rtn = ""; 565 566 if( str != null ) { 567 int cnt = str.length(); 568 StringBuilder buf = new StringBuilder( cnt ); 569 for(int i=0; i<cnt; i++) { 570 buf.append(correctToCP932(str.charAt(i))); 571 } 572 rtn = buf.toString() ; 573 } 574 return rtn ; 575 } 576 577 /** 578 * キャラクタ単位に、Unicode 文字列の補正を行います。 579 * 580 * 風間殿のページを参考にしています。 581 * @see <a href="http://www.ingrid.org/java/i18n/encoding/ja-conv.html" target="_blank"> 582 * http://www.ingrid.org/java/i18n/encoding/ja-conv.html</a> 583 */ 584 public static char correctToCP932( final char ch ) { 585 char rtn = ch; 586 587 switch (ch) { 588 // case 0x00a2: return 0xffe0; // ≪ 589 // case 0x00a3: return 0xffe1; //  ̄ 590 // case 0x00ac: return 0xffe2; // μ 591 // case 0x03bc: return 0x00b5; // ・ 592 // case 0x2014: return 0x2015; // , 593 // case 0x2016: return 0x2225; // ≫ 594 // case 0x2212: return 0xff0d; // ― 595 // case 0x226a: return 0x00ab; // ∥ 596 // case 0x226b: return 0x00bb; // ヴ 597 // case 0x301c: return 0xff5e; // - 598 // case 0x30f4: return 0x3094; // ~ 599 // case 0x30fb: return 0x00b7; // ¢ 600 // case 0xff0c: return 0x00b8; // £ 601 // case 0xffe3: return 0x00af; // ¬ 602 603 case 0x00a2: rtn = 0xffe0; break; // ¢ (1-81, CENT SIGN) 604 case 0x00a3: rtn = 0xffe1; break; // £ (1-82, POUND SIGN) 605 case 0x00a5: rtn = 0x005c; break; // \ (D/12, YEN SIGN) 606 case 0x00ac: rtn = 0xffe2; break; // ¬ (2-44, NOT SIGN) 607 case 0x2016: rtn = 0x2225; break; // ∥ (1-34, DOUBLE VERTICAL LINE) 608 case 0x203e: rtn = 0x007e; break; // ~ (F/14, OVERLINE) 609 case 0x2212: rtn = 0xff0d; break; // - (1-61, MINUS SIGN) 610 case 0x301c: rtn = 0xff5e; break; // ~ (1-33, WAVE DASH) 611 612 // case 0x301c: return 0xff5e; 613 // case 0x2016: return 0x2225; 614 // case 0x2212: return 0xff0d; 615 default: break; // 4.0.0 (2005/01/31) 616 } 617 return rtn; 618 } 619}