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}