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     */
016    package org.opengion.fukurou.mail;
017    
018    import java.io.InputStream;
019    import java.io.OutputStream;
020    import java.io.ByteArrayOutputStream;
021    import java.io.ByteArrayInputStream;
022    import java.io.UnsupportedEncodingException;
023    import java.io.IOException;
024    
025    import javax.activation.DataHandler;
026    import javax.activation.DataSource;
027    import javax.mail.internet.InternetAddress;
028    import javax.mail.internet.MimeMessage;
029    import javax.mail.internet.MimeUtility;
030    import javax.mail.MessagingException;
031    import com.sun.mail.util.BASE64EncoderStream;
032    
033    import 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コー?化して?bit で
040     * エンコードして送信する?がありますが、Windows系の特殊文字や、unicodeと
041     * ??マッピングが異なる文字などが??化けします?
042     * 対応方法としては?
043     * 『1.Windows-31J + 8bit 送信?
044     * 『2.ISO-2022-JP に独自変換 + 7bit 送信?
045     * の方法があります?
046     * 今回、この?つの方法につ?、それぞれサブクラス化を行い、??きるように
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     * 【特??決定版??サーバサイド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     */
072    public 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系は?bit / ISO-2022-JP 系は?bit になります?
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 を使用して、??.ISO-2022-JP に独自変換 + 7bit 送信?
127     * の実?ある、Mail_ISO2022JP_Charset サブクラスを返します?
128     *
129     * @version  4.0
130     * @author   Kazuhiko Hasegawa
131     * @since    JDK5.0,
132     */
133    class 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            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     */
178    class 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系は?bit / ISO-2022-JP 系は?bit になります?
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     */
273    class 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系は?bit / ISO-2022-JP 系は?bit になります?
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     */
375    class 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     */
450    class 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             * ?文字??バイ?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(&#x301c;) を?0xff5e(&#xff5e;) へ?
541     * 0x2016(&#x2016;) を?0x2225(&#x2225;) へ?
542     * 0x2212(&#x2212;) を?0xff0d(&#xff0d;) へ?
543     * それぞれコード変換します?
544     *
545     * @version  4.0
546     * @author   Kazuhiko Hasegawa
547     * @since    JDK5.0,
548     */
549    class 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    }