    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.fukurou.mail;
    017
    018import org.opengion.fukurou.util.LogWriter;
    019
    020import java.io.UnsupportedEncodingException;
    021import java.util.Properties;
    022import java.util.Date;
    023
    024import javax.activation.FileDataSource;
    025import javax.activation.DataHandler;
    026import javax.mail.internet.InternetAddress;
    027import javax.mail.internet.AddressException;
    028import javax.mail.internet.MimeMessage;
    029import javax.mail.internet.MimeMultipart;
    030import javax.mail.internet.MimeBodyPart;
    031import javax.mail.internet.MimeUtility;
    032import javax.mail.Store;
    033import javax.mail.Transport;
    034import javax.mail.Session;
    035import javax.mail.Message;
    036import javax.mail.MessagingException;
    037import javax.mail.IllegalWriteException;
    038
    039/**
    040 * MailTX は、ＳＭＴＰプロトコルによるメール送信プログラムです。
    041 *
    042 * E-Mail で日本語を送信する場合、ISO-2022-JP(JISコード)化して、7bit で
    043 * エンコードして送信する必要がありますが、Windows系の特殊文字や、unicodeと
    044 * 文字のマッピングが異なる文字などが、文字化けします。
    045 * 対応方法としては、
    046 * １．Windows-31J + 8bit 送信
    047 * ２．ISO-2022-JP に独自変換 + 7bit 送信
    048 * の方法があります。
    049 * 今回、この２つの方法について、対応いたしました。
    050 *
    051 * @version  4.0
    052 * @author   Kazuhiko Hasegawa
    053 * @since    JDK5.0,
    054 */
    055public class MailTX {
    056        private static final String CR = System.getProperty("line.separator");
    057        private static final String AUTH_PBS   = "POP_BEFORE_SMTP";             // 5.4.3.2
    058
    059        /** メーラーの名称  {@value} */
    060        public static final String MAILER = "Hayabusa Mail Ver 4.0";
    061
    062        private final String    charset  ;      // Windwos-31J , MS932 , ISO-2022-JP
    063        private String[]        filename = null;
    064        private String          message  = null;
    065        private Session         session  = null;
    066        private MimeMultipart mmPart = null;
    067        private MimeMessage     mimeMsg  = null;
    068        private MailCharset     mcSet    = null;
    069
    070        /**
    071         * メールサーバーとデフォルト文字エンコーディングを指定して、オブジェクトを構築します。
    072         *
    073         * デフォルト文字エンコーディングは、ISO-2022-JP です。
    074         *
    075         * @param       host    メールサーバー
    076         * @throws      IllegalArgumentException 引数が null の場合。
    077         */
    078        public MailTX( final String host ) {
    079                this( host,"ISO-2022-JP" );
    080        }
    081
    082        /**
    083         * メールサーバーとデフォルト文字エンコーディングを指定して、オブジェクトを構築します。
    084         *
    085         * 文字エンコーディングには、Windwos-31J , MS932 , ISO-2022-JP を指定できます。
    086         *
    087         * @og.rev 5.4.3.2 (2012/01/06) 認証対応のため
    088         *
    089         * @param       host    メールサーバー
    090         * @param       charset 文字エンコーディング
    091         * @throws      IllegalArgumentException 引数が null の場合。
    092         */
    093        public MailTX( final String host , final String charset ) {
    094                this( host,charset,null,null,null,null );
    095        }
    096
    097        /**
    098         * メールサーバーと文字エンコーディングを指定して、オブジェクトを構築します。
    099         * 認証を行う場合は認証方法を指定します。
    100         *
    101         * 文字エンコーディングには、Windwos-31J , MS932 , ISO-2022-JP を指定できます。
    102         *
    103         * @og.rev 5.1.9.0 (2010/08/01) mail.smtp.localhostの設定追加
    104         * @og.rev 5.4.3.2 (2012/01/06) 認証対応(POP Before SMTP)。引数３つ追加(将来的にはAuthentication対応？)
    105         *
    106         * @param       host    メールサーバー
    107         * @param       charset 文字エンコーディング
    108         * @param       port    SMTPポート
    109         * @param       auth    認証方法 5.4.3.2
    110         * @param       user    認証ユーザ 5.4.3.2
    111         * @param       pass    認証パスワード 5.4.3.2
    112         * @throws      IllegalArgumentException 引数が null の場合。
    113         */
    114        public MailTX( final String host , final String charset, final String port
    115                                ,final String auth, final String user, final String pass) {
    116                if( host == null ) {
    117                        String errMsg = "host に null はセット出来ません。";
    118                        throw new IllegalArgumentException( errMsg );
    119                }
    120
    121                if( charset == null ) {
    122                        String errMsg = "charset に null はセット出来ません。";
    123                        throw new IllegalArgumentException( errMsg );
    124                }
    125
    126                this.charset = charset;
    127
    128                mcSet = MailCharsetFactory.newInstance( charset );
    129
    130                Properties prop = new Properties();
    131                prop.setProperty("mail.mime.charset", charset);
    132                prop.setProperty("mail.mime.decodetext.strict", "false");
    133                prop.setProperty("mail.mime.address.strict", "false");
    134                prop.setProperty("mail.smtp.host", host);
    135                // 5.1.9.0 (2010/08/01) 設定追加
    136                prop.setProperty("mail.smtp.localhost", host);
    137                prop.setProperty("mail.host", host);    // MEssage-ID の設定に利用
    138                // 5.4.3.2 ポート追加
    139                if( port != null && port.length() > 0 ){
    140                        prop.setProperty("mail.smtp.port", port);       // MEssage-ID の設定に利用
    141                }
    142
    143                session = Session.getInstance(prop, null);
    144
    145                // POP before SMTP認証処理 5.4.3.2
    146                if(AUTH_PBS.equals( auth )){
    147                        try{
    148                                Store store = session.getStore("pop3");
    149                                store.connect(host,-1,user,pass); //同一ホストとする
    150                                store.close();
    151                        }
    152                        catch(MessagingException ex){
    153                                String errMsg = "POP3 Auth Exception: "+ host + "/" + user;
    154                                throw new RuntimeException( errMsg,ex );
    155                        }
    156                }
    157
    158                mimeMsg = new MimeMessage(session);
    159        }
    160
    161        /**
    162         * メールを送信します。
    163         *
    164         */
    165        public void sendmail() {
    166                try {
    167                        mimeMsg.setSentDate( new Date() );
    168
    169                        if( filename == null || filename.length == 0 ) {
    170                                mcSet.setTextContent( mimeMsg,message );
    171                        }
    172                        else {
    173                                mmPart = new MimeMultipart();
    174                                mimeMsg.setContent( mmPart );
    175                                // テキスト本体の登録
    176                                addMmpText( message );
    177
    178                                // 添付ファイルの登録
    179                                for( int i=0; i<filename.length; i++ ) {
    180                                        addMmpFile( filename[i] );
    181                                }
    182                        }
    183
    184                        mimeMsg.setHeader("X-Mailer", MAILER );
    185                        mimeMsg.setHeader("Content-Transfer-Encoding", mcSet.getBit() );
    186                        Transport.send( mimeMsg );
    187
    188                }
    189                catch( AddressException ex ) {
    190                        String errMsg = "Address Exception: ";
    191                        throw new RuntimeException( errMsg,ex );
    192                }
    193                catch ( MessagingException mex ) {
    194                        String errMsg = "MessagingException: ";
    195                        throw new RuntimeException( errMsg,mex );
    196                }
    197        }
    198
    199        /**
    200         * MimeMessageをリセットします。
    201         *
    202         * sendmail() でメールを送信後、セッションを閉じずに別のメールを送信する場合、
    203         * リセットしてから、各種パラメータを再設定してください。
    204         * その場合は、すべてのパラメータが初期化されていますので、もう一度
    205         * 設定しなおす必要があります。
    206         *
    207         */
    208        public void reset() {
    209                mimeMsg = new MimeMessage(session);
    210        }
    211
    212        /**
    213         * 送信元(ＦＲＯＭ)アドレスをセットします。
    214         *
    215         * @param   from 送信元(ＦＲＯＭ)アドレス
    216         */
    217        public void setFrom( final String from ) {
    218                try {
    219                        if( from != null ) {
    220                                mimeMsg.setFrom( getAddress( from ) );
    221                        }
    222                } catch( AddressException ex ) {
    223                        String errMsg = "Address Exception: ";
    224                        throw new RuntimeException( errMsg,ex );
    225                } catch ( MessagingException mex ) {
    226                        String errMsg = "MessagingException: ";
    227                        throw new RuntimeException( errMsg,mex );
    228                }
    229        }
    230
    231        /**
    232         * 送信先(ＴＯ)アドレス配列をセットします。
    233         *
    234         * @param   to 送信先(ＴＯ)アドレス配列
    235         */
    236        public void setTo( final String[] to ) {
    237                try {
    238                        if( to != null ) {
    239                                mimeMsg.setRecipients( Message.RecipientType.TO, getAddress( to ) );
    240                        }
    241                } catch( AddressException ex ) {
    242                        String errMsg = "Address Exception: ";
    243                        throw new RuntimeException( errMsg,ex );
    244                } catch ( MessagingException mex ) {
    245                        String errMsg = "MessagingException: ";
    246                        throw new RuntimeException( errMsg,mex );
    247                }
    248        }
    249
    250        /**
    251         * 送信先(ＣＣ)アドレス配列をセットします。
    252         *
    253         * @param   cc 送信先(ＣＣ)アドレス配列
    254         */
    255        public void setCc( final String[] cc ) {
    256                try {
    257                        if( cc != null ) {
    258                                mimeMsg.setRecipients( Message.RecipientType.CC, getAddress( cc ) );
    259                        }
    260                } catch( AddressException ex ) {
    261                        String errMsg = "Address Exception: ";
    262                        throw new RuntimeException( errMsg,ex );
    263                } catch ( MessagingException mex ) {
    264                        String errMsg = "MessagingException: ";
    265                        throw new RuntimeException( errMsg,mex );
    266                }
    267        }
    268
    269        /**
    270         * 送信先(ＢＣＣ)アドレス配列をセットします。
    271         *
    272         * @param   bcc 送信先(ＢＣＣ)アドレス配列
    273         */
    274        public void setBcc( final String[] bcc ) {
    275                try {
    276                        if( bcc != null ) {
    277                                mimeMsg.setRecipients( Message.RecipientType.BCC, getAddress( bcc ) );
    278                        }
    279                } catch( AddressException ex ) {
    280                        String errMsg = "Address Exception: ";
    281                        throw new RuntimeException( errMsg,ex );
    282                } catch ( MessagingException mex ) {
    283                        String errMsg = "MessagingException: ";
    284                        throw new RuntimeException( errMsg,mex );
    285                }
    286        }
    287
    288        /**
    289         * 送信先(ＴＯ)アドレス配列をクリアします。
    290         * @og.rev 4.3.6.0 (2009/04/01) 新規追加
    291         *
    292         */
    293        public void clearTo() {
    294                try {
    295                        mimeMsg.setRecipients( Message.RecipientType.TO, (InternetAddress[])null );
    296                } catch( IllegalWriteException ex ) {
    297                        String errMsg = "Address Exception: ";
    298                        throw new RuntimeException( errMsg,ex );
    299                } catch( IllegalStateException ex ) {
    300                        String errMsg = "Address Exception: ";
    301                        throw new RuntimeException( errMsg,ex );
    302                } catch ( MessagingException mex ) {
    303                        String errMsg = "MessagingException: ";
    304                        throw new RuntimeException( errMsg,mex );
    305                }
    306        }
    307
    308        /**
    309         * 送信先(CC)アドレス配列をクリアします。
    310         * @og.rev 4.3.6.0 (2009/04/01) 新規追加
    311         *
    312         */
    313        public void clearCc() {
    314                try {
    315                        mimeMsg.setRecipients( Message.RecipientType.CC, (InternetAddress[])null );
    316                } catch( IllegalWriteException ex ) {
    317                        String errMsg = "Address Exception: ";
    318                        throw new RuntimeException( errMsg,ex );
    319                } catch( IllegalStateException ex ) {
    320                        String errMsg = "Address Exception: ";
    321                        throw new RuntimeException( errMsg,ex );
    322                } catch ( MessagingException mex ) {
    323                        String errMsg = "MessagingException: ";
    324                        throw new RuntimeException( errMsg,mex );
    325                }
    326        }
    327
    328        /**
    329         * 送信先(BCC)アドレス配列をクリアします。
    330         * @og.rev 4.3.6.0 (2009/04/01) 新規追加
    331         *
    332         */
    333        public void clearBcc() {
    334                try {
    335                        mimeMsg.setRecipients( Message.RecipientType.BCC, (InternetAddress[])null );
    336                } catch( IllegalWriteException ex ) {
    337                        String errMsg = "Address Exception: ";
    338                        throw new RuntimeException( errMsg,ex );
    339                } catch( IllegalStateException ex ) {
    340                        String errMsg = "Address Exception: ";
    341                        throw new RuntimeException( errMsg,ex );
    342                } catch ( MessagingException mex ) {
    343                        String errMsg = "MessagingException: ";
    344                        throw new RuntimeException( errMsg,mex );
    345                }
    346        }
    347
    348        /**
    349         * 返信元(replyTo)アドレス配列をセットします。
    350         *
    351         * @param   replyTo 返信元(replyTo)アドレス配列
    352         */
    353        public void setReplyTo( final String[] replyTo ) {
    354                try {
    355                        if( replyTo != null ) {
    356                                mimeMsg.setReplyTo( getAddress( replyTo ) );
    357                        }
    358                } catch( AddressException ex ) {
    359                        String errMsg = "Address Exception: ";
    360                        throw new RuntimeException( errMsg,ex );
    361                } catch ( MessagingException mex ) {
    362                        String errMsg = "MessagingException: ";
    363                        throw new RuntimeException( errMsg,mex );
    364                }
    365        }
    366
    367        /**
    368         * タイトルをセットします。
    369         *
    370         * @param   subject タイトル
    371         */
    372        public void setSubject( final String subject ) {
    373                // Servlet からの読み込みは、iso8859_1 でエンコードされた文字が
    374                // セットされるので、ユニコードに変更しておかないと文字化けする。
    375                // JRun 3.0 では、問題なかったが、tomcat3.1 では問題がある。
    376                try {
    377                        if( subject != null ) {
    378                                mimeMsg.setSubject( mcSet.encodeWord( subject ) );
    379                        }
    380                } catch( AddressException ex ) {
    381                        String errMsg = "Address Exception: ";
    382                        throw new RuntimeException( errMsg,ex );
    383                } catch ( MessagingException mex ) {
    384                        String errMsg = "MessagingException: ";
    385                        throw new RuntimeException( errMsg,mex );
    386                }
    387        }
    388
    389        /**
    390         * 添付ファイル名配列をセットします。
    391         *
    392         * @param   fname 添付ファイル名配列
    393         */
    394        public void setFilename( final String[] fname ) {
    395                if( fname != null && fname.length > 0 ) {
    396                        int size = fname.length;
    397                        filename = new String[size];
    398                        System.arraycopy( fname,0,filename,0,size );
    399                }
    400        }
    401
    402        /**
    403         * メッセージ(本文)をセットします。
    404         *
    405         * @param   msg メッセージ(本文)
    406         */
    407        public void setMessage( final String msg ) {
    408                // なぜか、メッセージの最後は、<CR><LF>をセットしておく。
    409
    410                if( msg == null ) { message = CR; }
    411                else {              message = msg + CR; }
    412        }
    413
    414        /**
    415         * デバッグ情報の表示を行うかどうかをセットします。
    416         *
    417         * @param   debug 表示有無[true/false]
    418         */
    419        public void setDebug( final boolean debug ) {
    420            session.setDebug( debug );
    421        }
    422
    423        /**
    424         * 指定されたファイルをマルチパートに追加します。
    425         *
    426         * @param   fileStr マルチパートするファイル名
    427         */
    428        private void addMmpFile( final String fileStr ) {
    429                try {
    430                        MimeBodyPart mbp = new MimeBodyPart();
    431                        FileDataSource fds = new FileDataSource(fileStr);
    432                        mbp.setDataHandler(new DataHandler(fds));
    433                        mbp.setFileName(MimeUtility.encodeText(fds.getName(), charset, "B"));
    434                        mbp.setHeader("Content-Transfer-Encoding", "base64");
    435                        mmPart.addBodyPart(mbp);
    436                }
    437                catch( UnsupportedEncodingException ex ) {
    438                        String errMsg = "Multipart UnsupportedEncodingException: ";
    439                        throw new RuntimeException( errMsg,ex );
    440                }
    441                catch ( MessagingException mex ) {
    442                        String errMsg = "MessagingException: ";
    443                        throw new RuntimeException( errMsg,mex );
    444                }
    445        }
    446
    447        /**
    448         * 指定された文字列をマルチパートに追加します。
    449         *
    450         * @param   textStr マルチパートする文字列
    451         */
    452        private void addMmpText( final String textStr ) {
    453                try {
    454                        MimeBodyPart mbp = new MimeBodyPart();
    455                        mbp.setText(textStr, charset);
    456                        mbp.setHeader("Content-Transfer-Encoding", mcSet.getBit());
    457                        mmPart.addBodyPart(mbp, 0);
    458                }
    459                catch ( MessagingException mex ) {
    460                        String errMsg = "MessagingException: ";
    461                        throw new RuntimeException( errMsg,mex );
    462                }
    463        }
    464
    465        /**
    466         * 文字エンコードを考慮した InternetAddress を作成します。
    467         *
    468         * @param   adrs オリジナルのアドレス文字列
    469         *
    470         * @return  文字エンコードを考慮した InternetAddress
    471         */
    472        private InternetAddress getAddress( final String adrs ) {
    473                final InternetAddress rtnAdrs ;
    474                int sep = adrs.indexOf( '<' );
    475                if( sep >= 0 ) {
    476                        String address  = adrs.substring( sep+1,adrs.indexOf( '>' ) ).trim();
    477                        String personal = adrs.substring( 0,sep ).trim();
    478
    479                        rtnAdrs = mcSet.getAddress( address,personal );
    480                }
    481                else {
    482                        try {
    483                                rtnAdrs = new InternetAddress( adrs );
    484                        }
    485                        catch( AddressException ex ) {
    486                                String errMsg = "指定のアドレスをセットできません。"
    487                                                                        + "adrs=" + adrs  ;
    488                                throw new RuntimeException( errMsg,ex );
    489                        }
    490                }
    491
    492                return rtnAdrs ;
    493        }
    494
    495        /**
    496         * 文字エンコードを考慮した InternetAddress を作成します。
    497         * これは、アドレス文字配列から、InternetAddress 配列を作成する、
    498         * コンビニエンスメソッドです。
    499         * 処理そのものは、#getAddress( String ) をループしているだけです。
    500         *
    501         * @param   adrs アドレス文字配列
    502         *
    503         * @return  文字エンコード後のInternetAddress配列
    504         * @see     #getAddress( String )
    505         */
    506        private InternetAddress[] getAddress( final String[] adrs ) {
    507                InternetAddress[] rtnAdrs = new InternetAddress[adrs.length];
    508                for( int i=0; i<adrs.length; i++ ) {
    509                        rtnAdrs[i] = getAddress( adrs[i] );
    510                }
    511
    512                return rtnAdrs ;
    513        }
    514
    515        /**
    516         * コマンドから実行できる、テスト用の main メソッドです。
    517         *
    518         * Usage: java org.opengion.fukurou.mail.MailTX &lt;from&gt; &lt;to&gt; &lt;host&gt; [&lt;file&gt; ....]
    519         * で、複数の添付ファイルを送付することができます。
    520         *
    521         * @param       args    コマンド引数配列
    522         */
    523        public static void main( final String[] args ) {
    524                if(args.length < 3) {
    525                        LogWriter.log("Usage: java org.opengion.fukurou.mail.MailTX <from> <to> <host> [<file> ....]");
    526                        return ;
    527                }
    528
    529                String host  = args[2] ;
    530                String chset = "ISO-2022-JP" ;
    531
    532                MailTX sender = new MailTX( host,chset );
    533
    534                sender.setFrom( args[0] );
    535                String[] to = { args[1] };
    536                sender.setTo( to );
    537
    538                if( args.length > 3 ) {
    539                        String[] filename = new String[ args.length-3 ];
    540                        for( int i=0; i<args.length-3; i++ ) {
    541                                filename[i] = args[i+3];
    542                        }
    543                        sender.setFilename( filename );
    544                }
    545
    546                sender.setSubject( "メール送信テスト" );
    547                String msg = "これはテストメールです。" + CR +
    548                                                "うまく受信できましたか?" + CR;
    549                sender.setMessage( msg );
    550
    551                sender.sendmail();
    552        }
    553}