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         *
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)。引数3つ追加(将来的には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         * 送信元(FROM)アドレスをセットします。
243         *
244         * @param   from 送信元(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         * 送信先(TO)アドレス配列をセットします。
262         *
263         * @param   to 送信先(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         * 送信先(CC)アドレス配列をセットします。
281         *
282         * @param   cc 送信先(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         * 送信先(BCC)アドレス配列をセットします。
300         *
301         * @param   bcc 送信先(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         * 送信先(TO)アドレス配列をクリアします。
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}