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