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 は、SMTPプロトコルによるメール送信プログラムです。
043 *
044 * E-Mail で日本語を送信する場合、ISO-2022-JP(JISコード)化して、7bit で
045 * エンコードして送信する必要がありますが、Windows系の特殊文字や、unicodeと
046 * 文字のマッピングが異なる文字などが、文字化けします。
047 * 対応方法としては、
048 * 1.Windows-31J + 8bit 送信
049 * 2.ISO-2022-JP に独自変換 + 7bit 送信
050 * の方法があります。
051 * 今回、この2つの方法について、対応いたしました。
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)。引数3つ追加(将来的には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         * 送信元(FROM)アドレスをセットします。
258         *
259         * @param   from 送信元(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         * 送信先(TO)アドレス配列をセットします。
277         *
278         * @param   to 送信先(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         * 送信先(CC)アドレス配列をセットします。
296         *
297         * @param   cc 送信先(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         * 送信先(BCC)アドレス配列をセットします。
315         *
316         * @param   bcc 送信先(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         * 送信先(TO)アドレス配列をクリアします。
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}