package mireka.transmission.immediate; import java.net.InetAddress; import javax.inject.Inject; import mireka.address.AddressLiteral; import mireka.address.Domain; import mireka.address.DomainPart; import mireka.address.Recipient; import mireka.address.RemotePart; import mireka.address.RemotePartContainingRecipient; import mireka.smtp.SendException; import mireka.smtp.client.ClientFactory; import mireka.smtp.client.MtaAddress; import mireka.smtp.client.SmtpClient; import mireka.transmission.Mail; import mireka.transmission.immediate.dns.AddressLookup; import mireka.transmission.immediate.dns.MxLookup; import mireka.transmission.immediate.host.MailToHostTransmitter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xbill.DNS.Name; /** * DirectImmediateSender synchronously sends a mail directly to an SMTP server * of a single remote domain, which may include attempting delivery to more than * one MX hosts of the domain until a working one is found. * <p> * The remote domain is specified by the remote part of the recipient addresses, * which must be the same for all recipients in case of this implementation. * <p> * The receiving SMTP servers are usually specified by the MX records of the * remote domain, except if the remote part is a literal address, or the domain * has an implicit MX record only. * <p> * If it cannot transmit the mail to any of the MX hosts of the domain, then it * throws an exception, it does not retry later. * <p> * TODO: if a recipient is rejected because of a transient failure, then it * should be retried on another host. */ public class DirectImmediateSender implements ImmediateSender { private final Logger logger = LoggerFactory .getLogger(DirectImmediateSender.class); private MxLookup mxLookup; private AddressLookup addressLookup; private ClientFactory clientFactory; private MailToHostTransmitter mailToHostTransmitter; public DirectImmediateSender() { mxLookup = new MxLookup(); addressLookup = new AddressLookup(); } @Override public boolean singleDomainOnly() { return true; } /** * Transmits mail to a single domain. * * @throws IllegalArgumentException * if the domains of the recipients are not the same, or if the * recipient is the special global postmaster address, which has * no absolute domain. * @throws PostponeException * if transmission to all of the hosts must be postponed, * because all of them are assumed to be busy at this moment. */ @Override public void send(Mail mail) throws SendException, RecipientsWereRejectedException, IllegalArgumentException, PostponeException { RemotePart remotePart = commonRecipientRemotePart(mail); if (remotePart instanceof AddressLiteral) { AddressLiteral addressLiteral = (AddressLiteral) remotePart; sendToAddressLiteral(mail, addressLiteral); } else if (remotePart instanceof DomainPart) { Domain domain = ((DomainPart) remotePart).domain; sendToDomain(mail, domain); } else { throw new RuntimeException(); } } private RemotePart commonRecipientRemotePart(Mail mail) throws IllegalArgumentException { RemotePart result = null; for (Recipient recipient : mail.recipients) { if (!(recipient instanceof RemotePartContainingRecipient)) throw new IllegalArgumentException( "Cannot send mail to non-remote address: " + recipient); RemotePart remotePart = ((RemotePartContainingRecipient) recipient).getMailbox() .getRemotePart(); if (result == null) { result = remotePart; } else { if (!result.equals(remotePart)) throw new IllegalArgumentException( "Recipients are expected to belong to the same domain. " + " Recipient list contains both " + result + " and " + remotePart); } } if (result == null) throw new IllegalArgumentException("recipient list is empty"); return result; } private void sendToAddressLiteral(Mail mail, AddressLiteral target) throws SendException, RecipientsWereRejectedException, PostponeException { MtaAddress mtaAddress = new MtaAddress(target.smtpText(), target.inetAddress()); SmtpClient client = clientFactory.create(); client.setMtaAddress(mtaAddress); mailToHostTransmitter.transmit(mail, client); } /** * Queries MX hosts of the domain and tries to transmit to the hosts until * it is successful or no more hosts remain. * * @throws PostponeException * if transmission to all of the hosts must be postponed, * because all of them are assumed to be busy at this moment. */ private void sendToDomain(Mail mail, Domain domain) throws SendException, RecipientsWereRejectedException, PostponeException { Name[] mxNames = mxLookup.queryMxTargets(domain); // a PostponeException does not prevent successful delivery using // another host, but it must be saved so if there are no more hosts then // this exception instance will be rethrown. PostponeException lastPostponeException = null; // if there is a host which failed, but which should be retried later, // then a following unrecoverable DNS exception on another MX host may // not prevent delivery, so this temporary exception will be returned SendException lastRetryableException = null; // an unrecoverable DNS exception may not prevent delivery (to another // MX host of the domain), so the function will continue, but it must be // saved, because maybe there is no more host. SendException lastUnrecoverableDnsException = null; for (Name name : mxNames) { InetAddress[] addresses; try { addresses = addressLookup.queryAddresses(name); } catch (SendException e) { if (e.errorStatus().shouldRetry()) lastRetryableException = e; else lastUnrecoverableDnsException = e; logger.debug("Looking up address of MX host " + name + " failed, continuing with the next MX host " + "if one is available: ", e.getMessage()); continue; } try { for (InetAddress hostAddress : addresses) { MtaAddress mtaAddress = new MtaAddress(name, hostAddress); SmtpClient client = clientFactory.create(); client.setMtaAddress(mtaAddress); mailToHostTransmitter.transmit(mail, client); return; } } catch (PostponeException e) { lastPostponeException = e; logger.debug("Sending to SMTP host " + name + " must be postponed, continuing with the next " + "MX host if one is available: " + e.getMessage()); } catch (SendException e) { if (e.errorStatus().shouldRetry()) { // lastSendException = e; lastRetryableException = e; logger.debug("Sending to SMTP host " + name + " failed, continuing with the next " + "MX host if one is available: ", e.getMessage()); } else { throw e; } } } // at this point it is known that the transmission was not successful if (lastRetryableException != null) throw lastRetryableException; if (lastPostponeException != null) { // there is at least one host successfully found in DNS but have not // tried throw lastPostponeException; } if (lastUnrecoverableDnsException == null) throw new RuntimeException(); // impossible, but prevents warning // an unrecoverable DNS exception throw lastUnrecoverableDnsException; } /** @x.category GETSET **/ public MxLookup getMxLookup() { return mxLookup; } /** @x.category GETSET **/ public void setMxLookup(MxLookup mxLookup) { this.mxLookup = mxLookup; } /** @x.category GETSET **/ public AddressLookup getAddressLookup() { return addressLookup; } /** @x.category GETSET **/ public void setAddressLookup( AddressLookup addressLookup) { this.addressLookup = addressLookup; } /** @x.category GETSET **/ public ClientFactory getClientFactory() { return clientFactory; } /** @x.category GETSET **/ @Inject public void setClientFactory(ClientFactory clientFactory) { this.clientFactory = clientFactory; } /** @x.category GETSET **/ public MailToHostTransmitter getMailToHostTransmitter() { return mailToHostTransmitter; } /** @x.category GETSET **/ public void setMailToHostTransmitter( MailToHostTransmitter mailToHostTransmitter) { this.mailToHostTransmitter = mailToHostTransmitter; } }