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(&#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 */
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}