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