    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 * 『１．Windows-31J + 8bit 送信』
    044 * 『２．ISO-2022-JP に独自変換 + 7bit 送信』
    045 * の方法があります。
    046 * 今回、この２つの方法について、それぞれサブクラス化を行い、処理できるように
    047 * したのが、このインターフェース、および、サブクラスです。
    048 *
    049 * 『１．Windows-31J + 8bit 送信』の方法は、通常の JavaMail API に準拠して
    050 * 処理を行う、Mail_Windows31J_Charset サブクラスで実装しています。
    051 * 古いメイラーおよび、古いメールサーバーではメール転送できない為、
    052 * この方式は、社内で使用する場合のみに、利用できますが、主としてWindows系の
    053 * 社内システムにおいては、こちらの方が、なにかとトラブルは少ないと思います。
    054 *
    055 * 『２．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 * 『１．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 */
    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 * 『１．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 * 『２．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         * １文字の２バイト 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}