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