001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.runtime.net.mail;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.nio.charset.Charset;
024import java.text.Normalizer;
025import java.util.Enumeration;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.Map;
029import java.util.Properties;
030
031import javax.mail.Authenticator;
032import javax.mail.BodyPart;
033import javax.mail.Flags;
034import javax.mail.Folder;
035import javax.mail.Header;
036import javax.mail.Message;
037import javax.mail.MessagingException;
038import javax.mail.Multipart;
039import javax.mail.Part;
040import javax.mail.PasswordAuthentication;
041import javax.mail.Session;
042import javax.mail.Store;
043import javax.mail.URLName;
044import javax.mail.internet.MimeMultipart;
045import javax.mail.internet.MimeUtility;
046
047import lucee.commons.io.CharsetUtil;
048import lucee.commons.io.IOUtil;
049import lucee.commons.io.SystemUtil;
050import lucee.commons.io.res.Resource;
051import lucee.commons.io.res.util.ResourceUtil;
052import lucee.commons.lang.ExceptionUtil;
053import lucee.commons.lang.Md5;
054import lucee.commons.lang.StringUtil;
055import lucee.runtime.exp.PageException;
056import lucee.runtime.net.imap.ImapClient;
057import lucee.runtime.net.pop.PopClient;
058import lucee.runtime.op.Caster;
059import lucee.runtime.op.Operator;
060import lucee.runtime.type.Array;
061import lucee.runtime.type.ArrayImpl;
062import lucee.runtime.type.Collection;
063import lucee.runtime.type.KeyImpl;
064import lucee.runtime.type.Query;
065import lucee.runtime.type.QueryImpl;
066import lucee.runtime.type.Struct;
067import lucee.runtime.type.StructImpl;
068import lucee.runtime.type.util.ArrayUtil;
069import lucee.runtime.type.util.ListUtil;
070
071import com.sun.mail.pop3.POP3SSLStore;
072
073public abstract class MailClient {
074        
075        /**
076         * Simple authenicator implmentation
077         */
078        private final class _Authenticator extends Authenticator {
079
080                private String _fldif = null;
081                private String a = null;
082
083                protected PasswordAuthentication getPasswordAuthentication() {
084                        return new PasswordAuthentication(_fldif, a);
085                }
086
087                public _Authenticator(String s, String s1) {
088                        _fldif = s;
089                        a = s1;
090                }
091        }
092        
093
094        private static final Collection.Key DATE = KeyImpl.init("date");
095        private static final Collection.Key SUBJECT = KeyImpl.init("subject");
096        private static final Collection.Key SIZE = KeyImpl.init("size");
097        private static final Collection.Key FROM = KeyImpl.init("from");
098        private static final Collection.Key MESSAGE_NUMBER = KeyImpl.init("messagenumber");
099        private static final Collection.Key MESSAGE_ID = KeyImpl.init("messageid");
100        private static final Collection.Key REPLYTO = KeyImpl.init("replyto");
101        private static final Collection.Key CC = KeyImpl.init("cc");
102        private static final Collection.Key BCC = KeyImpl.init("bcc");
103        private static final Collection.Key TO = KeyImpl.init("to");
104        private static final Collection.Key UID = KeyImpl.init("uid");
105        private static final Collection.Key HEADER = KeyImpl.init("header");
106        private static final Collection.Key BODY = KeyImpl.init("body");
107        private static final Collection.Key CIDS = KeyImpl.init("cids");
108        private static final Collection.Key TEXT_BODY = KeyImpl.init("textBody");
109        private static final Collection.Key HTML_BODY = KeyImpl.init("HTMLBody");
110        private static final Collection.Key ATTACHMENTS = KeyImpl.init("attachments");
111        private static final Collection.Key ATTACHMENT_FILES = KeyImpl.init("attachmentfiles");
112
113
114        public static final int TYPE_POP3 = 0;
115        public static final int TYPE_IMAP = 1;
116
117
118        private String _flddo[] = {"date", "from", "messagenumber", "messageid", "replyto", "subject", "cc", "to", "size", "header", "uid"};
119        private String _fldnew[] = {"date", "from", "messagenumber", "messageid", "replyto", "subject", "cc", "to", "size", "header", "uid", "body", "textBody", "HTMLBody", "attachments", "attachmentfiles","cids"};
120        private String server = null;
121        private String username = null;
122        private String password = null;
123        private Session _fldtry = null;
124        private Store _fldelse = null;
125        private int port = 0;
126        private int timeout = 0;
127        private int startrow = 0;
128        private int maxrows = 0;
129        private boolean uniqueFilenames = false;
130        private Resource attachmentDirectory = null;
131        private boolean secure = false;
132
133        public static MailClient getInstance(int type,String server, int port, String username, String password, boolean secure){
134                if(TYPE_POP3==type)
135                        return new PopClient(server,port,username,password, secure);
136                if(TYPE_IMAP==type)
137                        return new ImapClient(server,port,username,password);
138                return null;
139        }
140        
141        public static MailClient getInstance(int type,String server, int port, String username, String password){
142                return getInstance(type,server,port,username,password,false);
143        }
144        
145        /**
146         * constructor of the class
147         * @param server
148         * @param port
149         * @param username
150         * @param password
151         */
152        public MailClient(String server, int port, String username, String password, boolean secure) {
153                timeout = 60000;
154                startrow = 0;
155                maxrows = -1;
156                uniqueFilenames = false;
157                this.server = server;
158                this.port = port;
159                this.username = username;
160                this.password = password;
161                this.secure = secure;
162        }
163
164
165    /**
166     * @param maxrows The maxrows to set.
167     */
168    public void setMaxrows(int maxrows) {
169        this.maxrows = maxrows;
170    }
171
172    /**
173     * @param startrow The startrow to set.
174     */
175    public void setStartrow(int startrow) {
176        this.startrow = startrow;
177    }
178
179
180    /**
181     * @param timeout The timeout to set.
182     */
183    public void setTimeout(int timeout) {
184        this.timeout = timeout;
185    }
186
187    /**
188     * @param uniqueFilenames The uniqueFilenames to set.
189     */
190    public void setUniqueFilenames(boolean uniqueFilenames) {
191        this.uniqueFilenames = uniqueFilenames;
192    }
193
194    /**
195     * @param attachmentDirectory The attachmentDirectory to set.
196     */
197    public void setAttachmentDirectory(Resource attachmentDirectory) {
198        this.attachmentDirectory = attachmentDirectory;
199    }
200
201    /**
202     * connects to pop server
203     * @throws MessagingException
204     */
205    public void connect() throws MessagingException {
206                Properties properties = new Properties();
207                String type=getTypeAsString();
208                properties.put("mail."+type+".host", server);
209                properties.put("mail."+type+".port", new Double(port));
210                properties.put("mail."+type+".connectiontimeout", String.valueOf(timeout));
211                properties.put("mail."+type+".timeout", String.valueOf(timeout));
212                //properties.put("mail.mime.charset", "UTF-8");
213                
214                
215                if(TYPE_IMAP==getType()) properties.put("mail.imap.partialfetch", "false" );
216                else {
217                        if(secure) properties.put("mail.pop3.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); // will failover 
218                }
219
220                _fldtry = username != null ? Session.getInstance(properties, new _Authenticator(username, password)) : Session.getInstance(properties);
221                
222                if(TYPE_POP3==getType() && secure) {
223                        URLName url = new URLName("pop3", server, port, "",username, password);
224                        _fldelse = new POP3SSLStore(_fldtry, url);
225                } else {
226                        _fldelse = _fldtry.getStore(type);
227                }
228                if(!StringUtil.isEmpty(username))_fldelse.connect(server,username,password);
229                else _fldelse.connect();
230        }
231
232    protected abstract String getTypeAsString();
233    protected abstract int getType();
234
235
236        /**
237     * delete all message in ibox that match given criteria
238     * @param messageNumbers
239     * @param uIds
240     * @throws MessagingException
241         * @throws IOException 
242     */
243    public void deleteMails(String as[], String as1[]) throws MessagingException, IOException {
244                Folder folder;
245                Message amessage[];
246                folder = _fldelse.getFolder("INBOX");
247                folder.open(2);
248                Map<String, Message> map = getMessages(null,folder, as1, as, startrow, maxrows,false);
249                Iterator<String> iterator = map.keySet().iterator();
250                amessage = new Message[map.size()];
251                int i = 0;
252                while(iterator.hasNext()) {
253                        amessage[i++] =  map.get(iterator.next());
254                }
255                try {
256                        folder.setFlags(amessage, new Flags(javax.mail.Flags.Flag.DELETED), true);
257                }
258                finally {
259                        folder.close(true);
260                }
261        }
262
263    /**
264     * return all messages from inbox
265     * @param messageNumbers all messages with this ids
266     * @param uIds all messages with this uids
267     * @param withBody also return body
268     * @return all messages from inbox
269     * @throws MessagingException
270     * @throws IOException
271     */
272    public Query getMails(String[] messageNumbers, String[] uids, boolean all) throws MessagingException, IOException {
273                Query qry = new QueryImpl(all ? _fldnew : _flddo, 0, "query");
274                Folder folder = _fldelse.getFolder("INBOX");
275                folder.open(Folder.READ_ONLY);
276                try {
277                        getMessages(qry,folder, uids, messageNumbers, startrow, maxrows,all);
278                }
279                finally {
280                        folder.close(false);
281                }
282                return qry;
283        }
284
285    private void toQuery(Query qry, Message message, Object uid, boolean all) 
286     {
287                int row = qry.addRow();
288        // date
289                try {
290                        qry.setAtEL(DATE, row, Caster.toDate(message.getSentDate(), true,null,null));
291                } 
292                catch (MessagingException e) {}
293                
294                // subject
295                try {
296                        qry.setAtEL(SUBJECT, row, message.getSubject());
297                } catch (MessagingException e) {
298                        qry.setAtEL(SUBJECT, row, "MessagingException:"+e.getMessage());
299                }
300                
301                // size
302                try {
303                        qry.setAtEL(SIZE, row, new Double(message.getSize()));
304                } 
305                catch (MessagingException e) {}
306                
307                qry.setAtEL(FROM, row, toList(getHeaderEL(message,"from")));
308                qry.setAtEL(MESSAGE_NUMBER, row, new Double(message.getMessageNumber()));
309                qry.setAtEL(MESSAGE_ID, row, toList(getHeaderEL(message,"Message-ID")));
310                String s = toList(getHeaderEL(message,"reply-to"));
311                if(s.length() == 0) {
312                        s = Caster.toString(qry.getAt(FROM, row,null), "");
313                }
314                qry.setAtEL(REPLYTO, row, s);
315                qry.setAtEL(CC, row, toList(getHeaderEL(message,"cc")));
316                qry.setAtEL(BCC, row, toList(getHeaderEL(message,"bcc")));
317                qry.setAtEL(TO, row, toList(getHeaderEL(message,"to")));
318                qry.setAtEL(UID, row, uid);
319                StringBuffer content = new StringBuffer();
320                try {
321                        for(Enumeration enumeration = message.getAllHeaders(); enumeration.hasMoreElements(); content.append('\n')){
322                                Header header = (Header) enumeration.nextElement();
323                                content.append(header.getName());
324                                content.append(": ");
325                                content.append(header.getValue());
326                        }
327                } 
328                catch (MessagingException e) {}
329                qry.setAtEL(HEADER, row, content.toString());
330                
331                if(all) {
332                        getContentEL(qry, message, row);
333                }
334        }
335
336        private String[] getHeaderEL(Message message, String key) {
337                try {
338                        return message.getHeader(key);
339                } catch (MessagingException e) {
340                        return null;
341                }
342        }
343
344        /**
345     * gets all messages from given Folder that match given criteria
346         * @param qry 
347     * @param folder
348     * @param uIds
349     * @param messageNumbers
350         * @param all 
351     * @param startrow
352     * @param maxrows
353         * @return 
354     * @return matching Messages
355         * @throws MessagingException 
356     */
357    private Map<String,Message> getMessages(Query qry, Folder folder, String[] uids, String[] messageNumbers, int startRow, int maxRow, boolean all) throws MessagingException 
358     {
359                
360        Message[] messages = folder.getMessages();
361                Map<String,Message> map = qry==null?new HashMap<String,Message>():null;
362                int k = 0;
363                if(uids != null || messageNumbers != null) {
364                        startRow = 0;
365                        maxRow = -1;
366                }
367                Message message;
368                for(int l = startRow; l < messages.length; l++) {
369                        if(maxRow != -1 && k == maxRow) {
370                                break;
371                        }
372                        message = messages[l];
373                        int messageNumber = message.getMessageNumber();
374                        String id = getId(folder,message);
375                        
376                        if(uids == null ? messageNumbers == null || contains(messageNumbers, messageNumber) : contains(uids, id)) {
377                                k++;
378                                if(qry!=null){
379                                        toQuery(qry,message,id,all);
380                                }
381                                else map.put(id, message);
382                        }
383                }
384                return map;
385        }
386    protected abstract String getId(Folder folder,Message message) throws MessagingException ;
387
388
389    private void getContentEL(Query query, Message message, int row) {
390        try {
391                        getContent(query, message, row);
392                } catch (Exception e) {
393                        String st = ExceptionUtil.getStacktrace(e,true);
394                        
395                        query.setAtEL(BODY, row, st);
396                }
397    }
398
399        /**
400     * write content data to query
401     * @param qry
402     * @param content
403     * @param row
404     * @throws MessagingException
405     * @throws IOException
406     */
407    private void getContent(Query query, Message message, int row) throws MessagingException, IOException {
408                StringBuffer body = new StringBuffer();
409                Struct cids=new StructImpl();
410                query.setAtEL(CIDS, row, cids);
411                if(message.isMimeType("text/plain")) {
412                        String content=getConent(message);
413                query.setAtEL(TEXT_BODY,row,content);
414                body.append(content);
415                }
416                else if(message.isMimeType("text/html")) {
417                        String content=getConent(message);
418                query.setAtEL(HTML_BODY,row,content);
419                body.append(content);
420                }
421                else {
422                        Object content = message.getContent();
423                        if(content instanceof MimeMultipart) {
424                                Array attachments = new ArrayImpl();
425                                Array attachmentFiles = new ArrayImpl();
426                                getMultiPart(query, row, attachments, attachmentFiles,cids, (MimeMultipart) content, body);
427        
428                                if(attachments.size() > 0) {
429                                        try {
430                                                query.setAtEL(ATTACHMENTS, row, ListUtil.arrayToList(attachments, "\t"));
431                                        }
432                                        catch(PageException pageexception) {
433                                        }
434                                }
435                                if(attachmentFiles.size() > 0) {
436                                        try {
437                                                query.setAtEL(ATTACHMENT_FILES, row, ListUtil.arrayToList(attachmentFiles, "\t"));
438                                        }
439                                        catch(PageException pageexception1) {
440                                        }
441                                }
442                                
443                                
444                        }
445                }
446                query.setAtEL(BODY, row, body.toString());
447        }
448
449        private void getMultiPart(Query query, int row, Array attachments, Array attachmentFiles,Struct cids, Multipart multiPart, StringBuffer body) throws MessagingException, IOException {
450                int j = multiPart.getCount();
451
452                for(int k = 0; k < j; k++) {
453                        BodyPart bodypart = multiPart.getBodyPart(k);
454                        Object content;
455                        
456
457                        if(bodypart.getFileName() != null) {
458                                String filename = bodypart.getFileName();
459                                try{
460                                        filename=Normalizer.normalize(MimeUtility.decodeText(filename),Normalizer.Form.NFC);
461                                }
462                                catch(Throwable t){
463                                        ExceptionUtil.rethrowIfNecessary(t);
464                                }
465                                
466                                if(bodypart.getHeader("Content-ID") != null) {
467                                        String[] ids = bodypart.getHeader("Content-ID");
468                                        String cid=ids[0].substring(1, ids[0].length() - 1);
469                                        cids.setEL(KeyImpl.init(filename), cid);
470                                }
471                                
472                                if(filename != null && ArrayUtil.find(attachments, filename)==0) {
473                                        
474                                        attachments.appendEL(filename);
475                                        if(attachmentDirectory != null) {
476                                                Resource file = attachmentDirectory.getRealResource(filename);
477                                                int l = 1;
478                                                String s2;
479                                                for(; uniqueFilenames && file.exists(); file = attachmentDirectory.getRealResource(s2)) {
480                                                        String as[] = ResourceUtil.splitFileName(filename);
481                                                        s2 = as.length != 1 ? as[0] + l++ + '.' + as[1] : as[0] + l++;
482                                                }
483
484                                                IOUtil.copy(bodypart.getInputStream(), file, true);
485                                                attachmentFiles.appendEL(file.getAbsolutePath());
486                                        }
487                                }
488                        }
489                        else if(bodypart.isMimeType("text/plain")) {
490                                content=getConent(bodypart);
491                        query.setAtEL(TEXT_BODY,row,content);
492                        if(body.length()==0)body.append(content);
493                        }
494                        else if(bodypart.isMimeType("text/html")) {
495                                content=getConent(bodypart);
496                        query.setAtEL(HTML_BODY,row,content);
497                        if(body.length()==0)body.append(content);
498                        }
499                        else if((content=bodypart.getContent()) instanceof Multipart) {
500                                getMultiPart(query, row, attachments, attachmentFiles,cids, (Multipart) content, body);
501                        }
502                        else if(bodypart.getHeader("Content-ID") != null) {
503                                String[] ids = bodypart.getHeader("Content-ID");
504                                String cid=ids[0].substring(1, ids[0].length() - 1);
505                                String filename = "cid:" + cid;
506                                
507                                        attachments.appendEL(filename);
508                                        if(attachmentDirectory != null) {
509                                                filename = "_" + Md5.getDigestAsString(filename);
510                                                Resource file = attachmentDirectory.getRealResource(filename);
511                                                int l = 1;
512                                                String s2;
513                                                for(; uniqueFilenames && file.exists(); file = attachmentDirectory.getRealResource(s2)) {
514                                                        String as[] = ResourceUtil.splitFileName(filename);
515                                                        s2 = as.length != 1 ? as[0] + l++ + '.' + as[1] : as[0] + l++;
516                                                }
517
518                                                IOUtil.copy(bodypart.getInputStream(), file, true);
519                                                attachmentFiles.appendEL(file.getAbsolutePath());
520                                        }
521                                
522                                cids.setEL(KeyImpl.init(filename), cid);
523                        }
524                }
525        }
526
527        /* *
528     * writes BodyTag data to query, if there is a problem with encoding, encoding will removed a do it again
529     * @param qry
530     * @param columnName
531     * @param row
532     * @param bp
533     * @param body
534     * @throws IOException
535     * @throws MessagingException
536     * /
537    private void setBody(Query qry, String columnName, int row, BodyPart bp, StringBuffer body) throws IOException, MessagingException {
538        String content = getConent(bp);
539        
540        qry.setAtEL(columnName,row,content);
541        if(body.length()==0)body.append(content);
542        
543    }*/
544    
545    private String getConent(Part bp) throws MessagingException {
546        InputStream is=null;
547        
548        try {
549                return getContent(is=bp.getInputStream(), CharsetUtil.toCharset(getCharsetFromContentType(bp.getContentType())));
550        }
551        catch(IOException mie) {
552                IOUtil.closeEL(is);
553                try {
554                                return getContent(is=bp.getInputStream(), SystemUtil.getCharset());
555                        } 
556                catch (IOException e) {
557                        return "Can't read body of this message:"+e.getMessage();
558                        } 
559        }
560        finally {
561                IOUtil.closeEL(is);
562        }
563    }
564    
565    private String getContent(InputStream is,Charset charset) throws IOException {
566                return MailUtil.decode(IOUtil.toString(is, charset));
567    }
568    
569    
570        private static String getCharsetFromContentType(String contentType) {
571                Array arr=ListUtil.listToArrayRemoveEmpty(contentType,"; ");
572                
573                for(int i=1;i<=arr.size();i++) {
574                        Array inner = ListUtil.listToArray((String)arr.get(i,null),"= ");
575                        if(inner.size()==2 && ((String)inner.get(1,"")).trim().equalsIgnoreCase("charset")) {
576                                String charset = (String) inner.get(2,"");
577                                charset=charset.trim();
578                                if(!StringUtil.isEmpty(charset)) {
579                                        if(StringUtil.startsWith(charset, '"') && StringUtil.endsWith(charset, '"')) {
580                                                charset=charset.substring(1,charset.length()-1);
581                                        }
582                                        if(StringUtil.startsWith(charset, '\'') && StringUtil.endsWith(charset, '\'')) {
583                                                charset=charset.substring(1,charset.length()-1);
584                                        }
585                                }
586                                return charset;
587                        }
588                }
589                return "us-ascii";
590        }
591
592
593        /**
594     * checks if a String Array (ids) has one element that is equal to id
595     * @param ids
596     * @param id
597     * @return has element found or not
598     */
599        private boolean contains(String ids[], String id) {
600                for(int i = 0; i < ids.length; i++) {
601                        if(Operator.compare(ids[i], id) == 0)   return true;
602                }
603                return false;
604        }
605
606        /**
607     * checks if a String Array (ids) has one element that is equal to id
608     * @param ids
609     * @param id
610     * @return has element found or not
611     */
612        private boolean contains(String ids[], int id) {
613                for(int i = 0; i < ids.length; i++) {
614                        if(Operator.compare(ids[i], id) == 0) return true;
615                }
616                return false;
617        }
618
619        /**
620     * translate a String Array to String List
621     * @param arr Array to translate
622     * @return List from Array
623     */
624        private String toList(String ids[]) {
625                if(ids == null) return "";
626                return ListUtil.arrayToList(ids, ",");
627        }
628
629        /**
630     * disconnect without a exception
631     */
632        public void disconnectEL() {
633                try {
634                        if(_fldelse != null)_fldelse.close();
635                }
636                catch(Exception exception) {}
637        }
638}