001 /* 002 * Copyright 2010 the original author or authors. 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, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 package com.hs.mail.mailet; 017 018 import java.io.IOException; 019 import java.net.ConnectException; 020 import java.net.SocketException; 021 import java.net.UnknownHostException; 022 import java.util.ArrayList; 023 import java.util.Arrays; 024 import java.util.Collection; 025 import java.util.Date; 026 import java.util.Hashtable; 027 import java.util.Iterator; 028 import java.util.List; 029 import java.util.Locale; 030 import java.util.Properties; 031 import java.util.Set; 032 033 import javax.mail.Address; 034 import javax.mail.MessagingException; 035 import javax.mail.SendFailedException; 036 import javax.mail.Session; 037 import javax.mail.Transport; 038 import javax.mail.internet.InternetAddress; 039 import javax.mail.internet.MimeMessage; 040 041 import org.apache.commons.lang.ArrayUtils; 042 import org.apache.log4j.Logger; 043 044 import com.hs.mail.container.config.ComponentManager; 045 import com.hs.mail.container.config.Config; 046 import com.hs.mail.dns.DnsServer; 047 import com.hs.mail.smtp.message.HostAddress; 048 import com.hs.mail.smtp.message.Recipient; 049 import com.hs.mail.smtp.message.SmtpMessage; 050 import com.sun.mail.smtp.SMTPAddressFailedException; 051 import com.sun.mail.smtp.SMTPSendFailedException; 052 053 /** 054 * Receives a Mail from SmtpMessageConsumer and takes care of delivery of the 055 * message to remote hosts. If for some reason mail can't be delivered store it 056 * with undelivered recipients and current retry count in the "spool" 057 * repository. After the next "retryDelayTime" the SpoolRepository will try to 058 * send it again. After "maxRetries" the mail will be considered undeliverable 059 * and will be returned to sender. 060 * 061 * @author Won Chul Doh 062 * @since Jun 7, 2010 063 * 064 */ 065 public class RemoteDelivery extends AbstractMailet { 066 067 static Logger logger = Logger.getLogger(RemoteDelivery.class); 068 069 private Session session = null; 070 private boolean debug = false; 071 private int maxRetries = 3; // default number of retries 072 private long smtpTimeout = 5000; //default number of ms to timeout on smtp delivery 073 private boolean sendPartial = true; // If false then ANY address errors will cause the transmission to fail 074 private long connectionTimeout = 60000; // The amount of time JavaMail will wait before giving up on a socket connect() 075 076 public void setDebug(boolean debug) { 077 this.debug = debug; 078 } 079 080 /** 081 * Initialize the mailet 082 */ 083 public void init(MailetContext context) { 084 super.init(context); 085 Properties props = new Properties(); 086 Properties sysprops = System.getProperties(); 087 for (Object key : sysprops.keySet()) { 088 if (((String) key).startsWith("mail.")) { 089 props.put(key, sysprops.get(key)); 090 } 091 } 092 if (this.debug) 093 props.setProperty("mail.debug", "true"); 094 props.setProperty("mail.smtp.timeout", smtpTimeout + ""); 095 props.setProperty("mail.smtp.connectiontimeout", connectionTimeout + ""); 096 props.setProperty("mail.smtp.sendpartial", String.valueOf(sendPartial)); 097 props.setProperty("mail.smtp.localhost", Config.getHelloName()); 098 099 Logger.getLogger("console").info("Config: " + Config.getHelloName()); 100 this.maxRetries = (int) Config.getNumberProperty("max_retry_count", 3); 101 102 this.session = Session.getInstance(props, null); 103 this.session.setDebug(this.debug); 104 } 105 106 public boolean accept(Set<Recipient> recipients, SmtpMessage message) { 107 return message.getNode() == SmtpMessage.REMOTE 108 || message.getNode() == SmtpMessage.ALL; 109 } 110 111 /** 112 * For this message, we take the list of recipients, organize these into 113 * distinct servers, and deliver the message for each of these servers. 114 */ 115 public void service(Set<Recipient> recipients, SmtpMessage message) 116 throws MessagingException { 117 // Organize the recipients into distinct servers (name made case insensitive) 118 Hashtable<String, Collection<Recipient>> targets = new Hashtable<String, Collection<Recipient>>(); 119 for (Recipient recipient : recipients) { 120 String targetServer = recipient.getHost().toLowerCase(Locale.US); 121 if (!Config.isLocal(targetServer)) { 122 Collection<Recipient> temp = targets.get(targetServer); 123 if (temp == null) { 124 temp = new ArrayList<Recipient>(); 125 targets.put(targetServer, temp); 126 } 127 temp.add(recipient); 128 } 129 } 130 131 if (targets.isEmpty()) { 132 return; 133 } 134 135 // We have the recipients organized into distinct servers 136 List<Recipient> undelivered = new ArrayList<Recipient>(); 137 MimeMessage mimemsg = message.getMimeMessage(); 138 boolean deleteMessage = true; 139 for (String targetServer : targets.keySet()) { 140 Collection<Recipient> temp = targets.get(targetServer); 141 if (!deliver(targetServer, temp, message, mimemsg)) { 142 // Retry this message later 143 deleteMessage = false; 144 if (!temp.isEmpty()) { 145 undelivered.addAll(temp); 146 } 147 } 148 } 149 if (!deleteMessage) { 150 try { 151 message.setRetryCount(message.getRetryCount() + 1); 152 message.setLastUpdate(new Date()); 153 recipients.clear(); 154 recipients.addAll(undelivered); 155 message.store(); 156 } catch (IOException e) { 157 } 158 } 159 } 160 161 /** 162 * We arranged that the recipients are all going to the same mail server. We 163 * will now rely on the DNS server to do DNS MX record lookup and try to 164 * deliver to the multiple mail servers. If it fails, we should decide that 165 * the failure is permanent or temporary. 166 * 167 * @param host 168 * the same host of recipients 169 * @param recipients 170 * recipients who are all going to the same mail server 171 * @param message 172 * Mail object to be delivered 173 * @param mimemsg 174 * MIME message representation of the message 175 * @return true if the delivery was successful or permanent failure so the 176 * message should be deleted, otherwise false and the message will 177 * be tried to send again later 178 */ 179 private boolean deliver(String host, Collection<Recipient> recipients, 180 SmtpMessage message, MimeMessage mimemsg) { 181 // Prepare javamail recipients 182 InternetAddress[] addresses = new InternetAddress[recipients.size()]; 183 Iterator<Recipient> it = recipients.iterator(); 184 for (int i = 0; it.hasNext(); i++) { 185 Recipient rcpt = it.next(); 186 addresses[i] = rcpt.toInternetAddress(); 187 } 188 189 try { 190 // Lookup the possible targets 191 Iterator<HostAddress> targetServers = getSmtpHostAddress(host); 192 if (!targetServers.hasNext()) { 193 logger.info("No mail server found for: " + host); 194 StringBuilder exceptionBuffer = new StringBuilder(128).append( 195 "There are no DNS entries for the hostname ").append( 196 host).append( 197 ". I cannot determine where to send this message."); 198 return failMessage(message, addresses, new MessagingException( 199 exceptionBuffer.toString()), false); 200 } 201 202 Properties props = session.getProperties(); 203 if (message.isNotificationMessage()) { 204 props.put("mail.smtp.from", "<>"); 205 } else { 206 props.put("mail.smtp.from", message.getFrom().getMailbox()); 207 } 208 209 MessagingException lastError = null; 210 StringBuilder logBuffer = null; 211 HostAddress outgoingMailServer = null; 212 while (targetServers.hasNext()) { 213 try { 214 outgoingMailServer = targetServers.next(); 215 logBuffer = new StringBuilder(256) 216 .append("Attempting to deliver message to host ") 217 .append(outgoingMailServer.getHostname()) 218 .append(" at ") 219 .append(outgoingMailServer.getHost()) 220 .append(" for addresses ") 221 .append(Arrays.asList(addresses)); 222 logger.info(logBuffer.toString()); 223 Transport transport = null; 224 try { 225 transport = session.getTransport(outgoingMailServer); 226 try { 227 transport.connect(); 228 } catch (MessagingException e) { 229 // Any error on connect should cause the mailet to 230 // attempt to connect to the next SMTP server 231 // associated with this MX record. 232 logger.error(e.getMessage()); 233 continue; 234 } 235 transport.sendMessage(mimemsg, addresses); 236 } finally { 237 if (transport != null) { 238 try { 239 transport.close(); 240 } catch (MessagingException e) { 241 } 242 transport = null; 243 } 244 } 245 logBuffer = new StringBuilder(256) 246 .append("Successfully sent message to host ") 247 .append(outgoingMailServer.getHostname()) 248 .append(" at ") 249 .append(outgoingMailServer.getHost()) 250 .append(" for addresses ") 251 .append(Arrays.asList(addresses)); 252 logger.info(logBuffer.toString()); 253 recipients.clear(); 254 return true; 255 } catch (SendFailedException sfe) { 256 if (sfe.getValidSentAddresses() != null) { 257 Address[] validSent = sfe.getValidSentAddresses(); 258 if (validSent.length > 0) { 259 logBuffer = new StringBuilder(256) 260 .append("Successfully sent message to host ") 261 .append(outgoingMailServer.getHostname()) 262 .append(" at ") 263 .append(outgoingMailServer.getHost()) 264 .append(" for addresses ") 265 .append(Arrays.asList(validSent)); 266 logger.info(logBuffer.toString()); 267 // Remove the addresses to which this message was 268 // sent successfully 269 List<InternetAddress> temp = new ArrayList<InternetAddress>(); 270 for (int i = 0; i < addresses.length; i++) { 271 if (!ArrayUtils.contains(validSent, addresses[i])) { 272 if (addresses[i] != null) { 273 temp.add(addresses[i]); 274 } 275 } 276 } 277 addresses = temp.toArray(new InternetAddress[temp.size()]); 278 removeAll(recipients, validSent); 279 } 280 } 281 282 if (sfe instanceof SMTPSendFailedException) { 283 SMTPSendFailedException ssfe = (SMTPSendFailedException) sfe; 284 // If permanent error 5xx, terminate this delivery 285 // attempt by re-throwing the exception 286 if (ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599) 287 throw sfe; 288 } 289 290 if (!ArrayUtils.isEmpty(sfe.getValidUnsentAddresses())) { 291 // Valid addresses remained, so continue with any other server. 292 if (logger.isDebugEnabled()) 293 logger 294 .debug("Send failed, " 295 + sfe.getValidUnsentAddresses().length 296 + " valid recipients(" 297 + Arrays.asList(sfe 298 .getValidUnsentAddresses()) 299 + ") remain, continuing with any other servers"); 300 lastError = sfe; 301 continue; 302 } else { 303 // There are no valid addresses left to send, so re-throw 304 throw sfe; 305 } 306 } catch (MessagingException me) { 307 Exception ne; 308 if ((ne = me.getNextException()) != null 309 && ne instanceof IOException) { 310 // It can be some socket or weird I/O related problem. 311 lastError = me; 312 continue; 313 } 314 throw me; 315 } 316 } // end while 317 if (lastError != null) { 318 throw lastError; 319 } 320 } catch (SendFailedException sfe) { 321 boolean deleteMessage = false; 322 323 if (sfe instanceof SMTPSendFailedException) { 324 SMTPSendFailedException ssfe = (SMTPSendFailedException) sfe; 325 deleteMessage = (ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599); 326 } else { 327 // Sometimes we'll get a normal SendFailedException with nested 328 // SMTPAddressFailedException, so use the latter RetCode 329 MessagingException me = sfe; 330 Exception ne; 331 while ((ne = me.getNextException()) != null && ne instanceof MessagingException) { 332 me = (MessagingException)ne; 333 if (me instanceof SMTPAddressFailedException) { 334 SMTPAddressFailedException ssfe = (SMTPAddressFailedException)me; 335 deleteMessage = (ssfe.getReturnCode() >= 500 && ssfe.getReturnCode() <= 599); 336 } 337 } 338 } 339 if (!ArrayUtils.isEmpty(sfe.getInvalidAddresses())) { 340 // Invalid addresses should be considered permanent 341 Address[] invalid = sfe.getInvalidAddresses(); 342 removeAll(recipients, invalid); 343 deleteMessage = failMessage(message, invalid, sfe, true); 344 } 345 if (!ArrayUtils.isEmpty(sfe.getValidUnsentAddresses())) { 346 // Vaild-unsent addresses should be considered temporary 347 deleteMessage = failMessage(message, sfe.getValidUnsentAddresses(), sfe, false); 348 } 349 return deleteMessage; 350 } catch (MessagingException mex) { 351 // Check whether this is a permanent error (like account doesn't 352 // exist or mailbox is full or domain is setup wrong) 353 // We fail permanently if this was 5xx error. 354 return failMessage(message, addresses, mex, ('5' == mex 355 .getMessage().charAt(0))); 356 } 357 // If we get here, we've exhausted the loop of servers without sending 358 // the message or throwing an exception. 359 // One case where this might happen is if there is no server we can 360 // connect. So this should be considered temporary 361 return failMessage(message, addresses, new MessagingException( 362 "No mail server(s) available at this time."), false); 363 } 364 365 /** 366 * Build error messages and make sure the error is permanent. 367 * 368 * @param message 369 * com.hs.mail.smtp.message.SmtpMessage 370 * @param addresses 371 * addresses not delivered 372 * @param ex 373 * javax.mail.MessagingException 374 * @param permanent 375 * true if the exception is permanent error, false otherwise 376 * @return Whether the message failed fully and can be deleted 377 */ 378 private boolean failMessage(SmtpMessage message, Address[] addresses, 379 MessagingException ex, boolean permanent) { 380 StringBuffer logBuffer = 381 new StringBuffer(64) 382 .append((permanent) ? "Permanent" : "Temporary") 383 .append(" exception delivering mail (") 384 .append(message.getName()) 385 .append("): ") 386 .append(ex.getMessage().trim()); 387 logger.error(logBuffer.toString()); 388 if (!permanent) { 389 int retries = message.getRetryCount(); 390 if (retries < maxRetries) { 391 return false; 392 } 393 } 394 if (message.isNotificationMessage()) { 395 if (logger.isDebugEnabled()) 396 logger.debug( 397 "Null reverse-path: no bounce will be generated for " 398 + message.getName()); 399 return true; 400 } 401 String errorMessage = buildErrorMessage(addresses, ex); 402 message.appendErrorMessage(errorMessage); 403 return true; 404 } 405 406 /** 407 * Build error message from the exception. 408 */ 409 private String buildErrorMessage(Address[] addresses, MessagingException ex) { 410 StringBuilder errorBuffer = new StringBuilder(); 411 if (!ArrayUtils.isEmpty(addresses)) { 412 for (int i = 0; i < addresses.length; i++) { 413 errorBuffer.append(addresses[i].toString()).append("\r\n"); 414 } 415 } 416 Exception ne = ex.getNextException(); 417 if (ne == null) { 418 errorBuffer.append(ex.getMessage().trim()); 419 } else if (ne instanceof SendFailedException) { 420 errorBuffer.append("Remote server told me: " 421 + ne.getMessage().trim()); 422 } else if (ne instanceof UnknownHostException) { 423 errorBuffer.append("Unknown host: " + ne.getMessage().trim()); 424 } else if (ne instanceof ConnectException) { 425 // Already formatted as "Connection timed out: connect" 426 errorBuffer.append(ne.getMessage().trim()); 427 } else if (ne instanceof SocketException) { 428 errorBuffer.append("Socket exception: " + ne.getMessage().trim()); 429 } else { 430 errorBuffer.append(ne.getMessage().trim()); 431 } 432 errorBuffer.append("\r\n"); 433 return errorBuffer.toString(); 434 } 435 436 public static Iterator<HostAddress> getSmtpHostAddress(String domainName) { 437 DnsServer dns = (DnsServer) ComponentManager.getBean("dns.server"); 438 return dns.getSmtpHostAddress(domainName); 439 } 440 441 public static void removeAll(Collection<Recipient> recipients, 442 Address[] addresses) { 443 for (Iterator<Recipient> it = recipients.iterator(); it.hasNext();) { 444 Recipient rcpt = it.next(); 445 for (Address address : addresses) { 446 if (rcpt.getMailbox().equals( 447 ((InternetAddress) address).getAddress())) { 448 it.remove(); 449 break; 450 } 451 } 452 } 453 } 454 455 }