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