http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/DnsHelper.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/DnsHelper.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/DnsHelper.java new file mode 100644 index 0000000..92cb4e6 --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/DnsHelper.java @@ -0,0 +1,49 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.util.Iterator; + +import org.apache.james.dnsservice.api.DNSService; +import org.apache.james.dnsservice.api.TemporaryResolutionException; +import org.apache.james.dnsservice.library.MXHostAddressIterator; +import org.apache.mailet.HostAddress; + +@SuppressWarnings("deprecation") +public class DnsHelper { + + public static final boolean USE_SEVERAL_IP = false; + private final DNSService dnsServer; + private final RemoteDeliveryConfiguration configuration; + + public DnsHelper(DNSService dnsServer, RemoteDeliveryConfiguration configuration) { + this.dnsServer = dnsServer; + this.configuration = configuration; + } + + public Iterator<HostAddress> retrieveHostAddressIterator(String host) throws TemporaryResolutionException { + if (configuration.getGatewayServer().isEmpty()) { + return new MXHostAddressIterator(dnsServer.findMXRecords(host).iterator(), dnsServer, USE_SEVERAL_IP); + } else { + return new MXHostAddressIterator(configuration.getGatewayServer().iterator(), dnsServer, USE_SEVERAL_IP); + } + } + +}
http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/EnhancedMessagingException.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/EnhancedMessagingException.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/EnhancedMessagingException.java new file mode 100644 index 0000000..06f8a61 --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/EnhancedMessagingException.java @@ -0,0 +1,167 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Optional; +import java.util.function.Function; +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Preconditions; +import com.sun.mail.smtp.SMTPAddressFailedException; +import com.sun.mail.smtp.SMTPSendFailedException; +import com.sun.mail.smtp.SMTPSenderFailedException; + +public class EnhancedMessagingException { + + private static final Logger logger = LoggerFactory.getLogger(EnhancedMessagingException.class); + + private final MessagingException messagingException; + private final Optional<Integer> returnCode; + private final Optional<Integer> nestedReturnCode; + + public EnhancedMessagingException(MessagingException messagingException) { + this.messagingException = messagingException; + this.returnCode = computeReturnCode(); + this.nestedReturnCode = computeNestedReturnCode(); + } + + public boolean hasReturnCode() { + return returnCode.isPresent(); + } + + public boolean hasNestedReturnCode() { + return nestedReturnCode.isPresent(); + } + + public boolean isServerError() { + return isServerError(returnCode) || isServerError(nestedReturnCode); + } + + private boolean isServerError(Optional<Integer> returnCode) { + return (returnCode.isPresent() + && returnCode.get() >= 500 + && returnCode.get() <= 599) + || messageIndicatesServerException(); + } + + private boolean messageIndicatesServerException() { + return Optional.ofNullable(messagingException.getMessage()) + .map(startWith5()) + .orElse(false); + } + + private Function<String, Boolean> startWith5() { + return message -> message.startsWith("5"); + } + + private Optional<Integer> computeReturnCode() { + if (messagingException instanceof SMTPAddressFailedException) { + SMTPAddressFailedException addressFailedException = (SMTPAddressFailedException) this.messagingException; + return Optional.of(addressFailedException.getReturnCode()); + } + if (messagingException instanceof SMTPSendFailedException) { + SMTPSendFailedException sendFailedException = (SMTPSendFailedException) this.messagingException; + return Optional.of(sendFailedException.getReturnCode()); + } + if (messagingException instanceof SMTPSenderFailedException) { + SMTPSenderFailedException senderFailedException = (SMTPSenderFailedException) this.messagingException; + return Optional.of(senderFailedException.getReturnCode()); + } + if (messagingException.getClass().getName().endsWith(".SMTPSendFailedException") + || messagingException.getClass().getName().endsWith(".SMTPAddressSucceededException")) { + try { + return Optional.of((Integer)invokeGetter(messagingException, "getReturnCode")); + } catch (ClassCastException | IllegalArgumentException | IllegalStateException e) { + logger.error("unexpected exception", e); + } + } + return Optional.empty(); + } + + public Optional<String> computeCommand() { + if (hasReturnCode()) { + try { + return Optional.of((String) invokeGetter(messagingException, "getCommand")); + } catch (ClassCastException | IllegalArgumentException | IllegalStateException e) { + logger.error("unexpected exception", e); + } + } + return Optional.empty(); + } + + public Optional<InternetAddress> computeAddress() { + if (hasReturnCode()) { + try { + return Optional.of((InternetAddress) invokeGetter(messagingException, "getAddress")); + } catch (ClassCastException | IllegalArgumentException | IllegalStateException e) { + logger.error("unexpected exception", e); + } + } + return Optional.empty(); + } + + public String computeAction() { + return messagingException.getClass().getName().endsWith(".SMTPAddressFailedException") ? "FAILED" : "SUCCEEDED"; + } + + public Optional<Integer> getReturnCode() { + return returnCode; + } + + private Optional<Integer> computeNestedReturnCode() { + EnhancedMessagingException currentMessagingException = this; + while (true) { + Optional<Integer> returnCode = currentMessagingException.computeReturnCode(); + if (returnCode.isPresent()) { + return returnCode; + } + if (currentMessagingException.hasNestedMessagingException()) { + currentMessagingException = currentMessagingException.getNestedMessagingException(); + } else { + return Optional.empty(); + } + } + } + + private boolean hasNestedMessagingException() { + return messagingException.getNextException() != null + && messagingException.getNextException() instanceof MessagingException; + } + + private EnhancedMessagingException getNestedMessagingException() { + Preconditions.checkState(hasNestedMessagingException()); + return new EnhancedMessagingException((MessagingException) messagingException.getNextException()); + } + + private Object invokeGetter(Object target, String getter) { + try { + Method getAddress = target.getClass().getMethod(getter); + return getAddress.invoke(target); + } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + return new IllegalStateException("Exception invoking " + getter + " on a " + target.getClass() + " object"); + } + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/ExecutionResult.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/ExecutionResult.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/ExecutionResult.java new file mode 100644 index 0000000..3dae211 --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/ExecutionResult.java @@ -0,0 +1,93 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.util.Optional; + +import com.google.common.base.Objects; + +public class ExecutionResult { + + public enum ExecutionState { + SUCCESS, + PERMANENT_FAILURE, + TEMPORARY_FAILURE + } + + public static ExecutionResult success() { + return new ExecutionResult(ExecutionState.SUCCESS, Optional.empty()); + } + + public static ExecutionResult temporaryFailure(Exception e) { + return new ExecutionResult(ExecutionState.TEMPORARY_FAILURE, Optional.of(e)); + } + + public static ExecutionResult permanentFailure(Exception e) { + return new ExecutionResult(ExecutionState.PERMANENT_FAILURE, Optional.of(e)); + } + + public static ExecutionResult temporaryFailure() { + return new ExecutionResult(ExecutionState.TEMPORARY_FAILURE, Optional.empty()); + } + + public static ExecutionResult onFailure(boolean permanent, Exception exeption) { + if (permanent) { + return permanentFailure(exeption); + } else { + return temporaryFailure(exeption); + } + } + + private final ExecutionState executionState; + private final Optional<Exception> exception; + + public ExecutionResult(ExecutionState executionState, Optional<Exception> exception) { + this.executionState = executionState; + this.exception = exception; + } + + public ExecutionState getExecutionState() { + return executionState; + } + + public Optional<Exception> getException() { + return exception; + } + + public boolean isPermanent() { + return executionState == ExecutionState.PERMANENT_FAILURE; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ExecutionResult) { + ExecutionResult that = (ExecutionResult) o; + return Objects.equal(this.executionState, that.executionState) + && Objects.equal(this.exception, that.exception); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hashCode(executionState, exception); + } +} + http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/HeloNameProvider.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/HeloNameProvider.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/HeloNameProvider.java new file mode 100644 index 0000000..9042fbb --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/HeloNameProvider.java @@ -0,0 +1,53 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import org.apache.james.domainlist.api.DomainList; +import org.apache.james.domainlist.api.DomainListException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HeloNameProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(HeloNameProvider.class); + public static final String LOCALHOST = "localhost"; + + private final String heloName; + private final DomainList domainList; + + public HeloNameProvider(String heloName, DomainList domainList) { + this.heloName = heloName; + this.domainList = domainList; + } + + public String getHeloName() { + if (heloName == null) { + // TODO: Maybe we should better just lookup the hostname via dns + try { + return domainList.getDefaultDomain(); + } catch (DomainListException e) { + LOGGER.warn("Unable to access DomainList", e); + return LOCALHOST; + } + } else { + return heloName; + } + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/InternetAddressConverter.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/InternetAddressConverter.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/InternetAddressConverter.java new file mode 100644 index 0000000..c68b9ec --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/InternetAddressConverter.java @@ -0,0 +1,39 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.util.Collection; +import javax.mail.internet.InternetAddress; + +import org.apache.james.core.MailAddress; + +import com.google.common.base.Preconditions; + + +public class InternetAddressConverter { + + public static InternetAddress[] convert(Collection<MailAddress> recipients) { + Preconditions.checkNotNull(recipients); + return recipients.stream() + .map(MailAddress::toInternetAddress) + .toArray(InternetAddress[]::new); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrer.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrer.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrer.java new file mode 100644 index 0000000..9fbb4ba --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrer.java @@ -0,0 +1,273 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; + +import javax.mail.Address; +import javax.mail.MessagingException; +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +import org.apache.james.core.MailAddress; +import org.apache.james.dnsservice.api.DNSService; +import org.apache.james.dnsservice.api.TemporaryResolutionException; +import org.apache.mailet.HostAddress; +import org.apache.mailet.Mail; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Iterables; + +@SuppressWarnings("deprecation") +public class MailDelivrer { + private static final Logger LOGGER = LoggerFactory.getLogger(MailDelivrer.class); + + private final RemoteDeliveryConfiguration configuration; + private final MailDelivrerToHost mailDelivrerToHost; + private final DnsHelper dnsHelper; + private final MessageComposer messageComposer; + private final Bouncer bouncer; + + public MailDelivrer(RemoteDeliveryConfiguration configuration, MailDelivrerToHost mailDelivrerToHost, DNSService dnsServer, Bouncer bouncer) { + this(configuration, mailDelivrerToHost, new DnsHelper(dnsServer, configuration), bouncer); + } + + @VisibleForTesting + MailDelivrer(RemoteDeliveryConfiguration configuration, MailDelivrerToHost mailDelivrerToHost, DnsHelper dnsHelper, Bouncer bouncer) { + this.configuration = configuration; + this.mailDelivrerToHost = mailDelivrerToHost; + this.dnsHelper = dnsHelper; + this.messageComposer = new MessageComposer(configuration); + this.bouncer = bouncer; + } + + /** + * We can assume that the recipients of this message are all going to the same mail server. We will now rely on the + * DNS server to do DNS MX record lookup and try to deliver to the multiple mail servers. If it fails, it should + * throw an exception. + * + * @param mail org.apache.james.core.MailImpl + * @return boolean Whether the delivery was successful and the message can be deleted + */ + public ExecutionResult deliver(Mail mail) { + try { + return tryDeliver(mail); + } catch (SendFailedException sfe) { + return handleSenderFailedException(mail, sfe); + } catch (MessagingException ex) { + // We check whether this is a 5xx error message, which indicates a permanent failure (like account doesn't exist + // or mailbox is full or domain is setup wrong). We fail permanently if this was a 5xx error + boolean isPermanent = new EnhancedMessagingException(ex).isServerError(); + return logAndReturn(mail, ExecutionResult.onFailure(isPermanent, ex)); + } catch (Exception ex) { + LOGGER.error("Generic exception = permanent failure: {}", ex.getMessage(), ex); + return logAndReturn(mail, ExecutionResult.permanentFailure(ex)); + } + } + + private ExecutionResult tryDeliver(Mail mail) throws MessagingException { + if (mail.getRecipients().isEmpty()) { + LOGGER.info("No recipients specified... not sure how this could have happened."); + return ExecutionResult.permanentFailure(new Exception("No recipients specified for " + mail.getName() + " sent by " + mail.getSender())); + } + if (configuration.isDebug()) { + LOGGER.debug("Attempting to deliver {}", mail.getName()); + } + + String host = retrieveTargetHostname(mail); + try { + // Figure out which servers to try to send to. This collection + // will hold all the possible target servers + Iterator<HostAddress> targetServers = dnsHelper.retrieveHostAddressIterator(host); + if (!targetServers.hasNext()) { + return handleNoTargetServer(mail, host); + } + return doDeliver(mail, InternetAddressConverter.convert(mail.getRecipients()), targetServers); + } catch (TemporaryResolutionException e) { + return logAndReturn(mail, ExecutionResult.temporaryFailure(new MessagingException("Temporary problem looking " + + "up mail server for host: " + host + ". I cannot determine where to send this message."))); + } + } + + private String retrieveTargetHostname(Mail mail) { + Preconditions.checkArgument(!mail.getRecipients().isEmpty(), "Mail should have recipients to attempt delivery"); + MailAddress rcpt = Iterables.getFirst(mail.getRecipients(), null); + return rcpt.getDomain(); + } + + private ExecutionResult doDeliver(Mail mail, InternetAddress[] addr, Iterator<HostAddress> targetServers) throws MessagingException { + MessagingException lastError = null; + + while (targetServers.hasNext()) { + try { + return mailDelivrerToHost.tryDeliveryToHost(mail, addr, targetServers.next()); + } catch (SendFailedException sfe) { + lastError = handleSendFailExceptionOnMxIteration(mail, sfe); + } catch (MessagingException me) { + lastError = handleMessagingException(mail, me); + if (configuration.isDebug()) { + LOGGER.debug(me.getMessage(), me.getCause()); + } else { + LOGGER.info(me.getMessage()); + } + } + } + // If we encountered an exception while looping through, + // throw the last MessagingException we caught. We only + // do this if we were unable to send the message to any + // server. If sending eventually succeeded, we exit + // deliver() though the return at the end of the try + // block. + if (lastError != null) { + throw lastError; + } + return ExecutionResult.temporaryFailure(); + } + + private MessagingException handleMessagingException(Mail mail, MessagingException me) throws MessagingException { + LOGGER.debug("Exception delivering message ({}) - {}", mail.getName(), me.getMessage()); + if ((me.getNextException() != null) && (me.getNextException() instanceof IOException)) { + // If it's an IO exception with no nested exception, it's probably + // some socket or weird I/O related problem. + return me; + } else { + // This was not a connection or I/O error particular to one SMTP server of an MX set. Instead, it is almost + // certainly a protocol level error. In this case we assume that this is an error we'd encounter with any of + // the SMTP servers associated with this MX record, and we pass the exception to the code in the outer block + // that determines its severity. + throw me; + } + } + + @VisibleForTesting + ExecutionResult handleSenderFailedException(Mail mail, SendFailedException sfe) { + logSendFailedException(sfe); + EnhancedMessagingException enhancedMessagingException = new EnhancedMessagingException(sfe); + List<MailAddress> invalidAddresses = AddressesArrayToMailAddressListConverter.getAddressesAsMailAddress(sfe.getInvalidAddresses()); + List<MailAddress> validUnsentAddresses = AddressesArrayToMailAddressListConverter.getAddressesAsMailAddress(sfe.getValidUnsentAddresses()); + if (configuration.isDebug()) { + LOGGER.debug("Mail {} has initially recipients: {}", mail.getName(), mail.getRecipients()); + if (!invalidAddresses.isEmpty()) { + LOGGER.debug("Invalid recipients: {}", invalidAddresses); + } + if (!validUnsentAddresses.isEmpty()) { + LOGGER.debug("Unsent recipients: {}", validUnsentAddresses); + } + } + if (!validUnsentAddresses.isEmpty()) { + if (!invalidAddresses.isEmpty()) { + mail.setRecipients(invalidAddresses); + bouncer.bounce(mail, sfe); + } + mail.setRecipients(validUnsentAddresses); + if (enhancedMessagingException.hasReturnCode()) { + boolean isPermanent = enhancedMessagingException.isServerError(); + return logAndReturn(mail, ExecutionResult.onFailure(isPermanent, sfe)); + } else { + return logAndReturn(mail, ExecutionResult.temporaryFailure(sfe)); + } + } + if (!invalidAddresses.isEmpty()) { + mail.setRecipients(invalidAddresses); + return logAndReturn(mail, ExecutionResult.permanentFailure(sfe)); + } + + if (enhancedMessagingException.hasReturnCode() || enhancedMessagingException.hasNestedReturnCode()) { + if (enhancedMessagingException.isServerError()) { + return ExecutionResult.permanentFailure(sfe); + } + } + return ExecutionResult.temporaryFailure(sfe); + } + + private ExecutionResult logAndReturn(Mail mail, ExecutionResult executionResult) { + LOGGER.debug(messageComposer.composeFailLogMessage(mail, executionResult)); + return executionResult; + } + + private MessagingException handleSendFailExceptionOnMxIteration(Mail mail, SendFailedException sfe) throws SendFailedException { + logSendFailedException(sfe); + + if (sfe.getValidSentAddresses() != null) { + Address[] validSent = sfe.getValidSentAddresses(); + if (validSent.length > 0) { + LOGGER.debug("Mail ({}) sent successfully for {}", mail.getName(), validSent); + } + } + + EnhancedMessagingException enhancedMessagingException = new EnhancedMessagingException(sfe); + if (enhancedMessagingException.isServerError()) { + throw sfe; + } + + final Address[] validUnsentAddresses = sfe.getValidUnsentAddresses(); + if (validUnsentAddresses != null && validUnsentAddresses.length > 0) { + if (configuration.isDebug()) { + LOGGER.debug("Send failed, {} valid addresses remain, continuing with any other servers", (Object) validUnsentAddresses); + } + return sfe; + } else { + // There are no valid addresses left to send, so rethrow + throw sfe; + } + } + + private ExecutionResult handleNoTargetServer(Mail mail, String host) { + LOGGER.info("No mail server found for: {}", host); + MessagingException messagingException = new MessagingException("There are no DNS entries for the hostname " + host + ". I cannot determine where to send this message."); + int retry = DeliveryRetriesHelper.retrieveRetries(mail); + if (retry >= configuration.getDnsProblemRetry()) { + return logAndReturn(mail, ExecutionResult.permanentFailure(messagingException)); + } else { + return logAndReturn(mail, ExecutionResult.temporaryFailure(messagingException)); + } + } + + private void logSendFailedException(SendFailedException sfe) { + if (configuration.isDebug()) { + EnhancedMessagingException enhancedMessagingException = new EnhancedMessagingException(sfe); + if (enhancedMessagingException.hasReturnCode()) { + LOGGER.info("SMTP SEND FAILED: Command [{}] RetCode: [{}] Response[{}]", enhancedMessagingException.computeCommand(), + enhancedMessagingException.getReturnCode(), sfe.getMessage()); + } else { + LOGGER.info("Send failed", sfe); + } + logLevels(sfe); + } + } + + private void logLevels(MessagingException me) { + Exception ne; + while ((ne = me.getNextException()) != null && ne instanceof MessagingException) { + me = (MessagingException) ne; + EnhancedMessagingException enhancedMessagingException = new EnhancedMessagingException(me); + if (me.getClass().getName().endsWith(".SMTPAddressFailedException") || me.getClass().getName().endsWith(".SMTPAddressSucceededException")) { + LOGGER.debug("ADDRESS :[{}] Address:[{}] Command : [{}] RetCode[{}] Response [{}]", + enhancedMessagingException.computeAction(), me, enhancedMessagingException.computeAddress(), + enhancedMessagingException.computeCommand(), enhancedMessagingException.getReturnCode()); + } + } + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrerToHost.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrerToHost.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrerToHost.java new file mode 100644 index 0000000..5be0f62 --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MailDelivrerToHost.java @@ -0,0 +1,136 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.io.IOException; +import java.util.Properties; + +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +import org.apache.mailet.HostAddress; +import org.apache.mailet.Mail; +import org.apache.mailet.MailetContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.sun.mail.smtp.SMTPTransport; + +@SuppressWarnings("deprecation") +public class MailDelivrerToHost { + private static final Logger LOGGER = LoggerFactory.getLogger(MailDelivrerToHost.class); + public static final String BIT_MIME_8 = "8BITMIME"; + + private final RemoteDeliveryConfiguration configuration; + private final Converter7Bit converter7Bit; + private final Session session; + + public MailDelivrerToHost(RemoteDeliveryConfiguration remoteDeliveryConfiguration, MailetContext mailetContext) { + this.configuration = remoteDeliveryConfiguration; + this.converter7Bit = new Converter7Bit(mailetContext); + this.session = Session.getInstance(configuration.createFinalJavaxProperties()); + } + + public ExecutionResult tryDeliveryToHost(Mail mail, InternetAddress[] addr, HostAddress outgoingMailServer) throws MessagingException { + Properties props = getPropertiesForMail(mail); + LOGGER.debug("Attempting delivery of {} to host {} at {} from {}", + mail.getName(), outgoingMailServer.getHostName(), outgoingMailServer.getHost(), props.get("mail.smtp.from")); + + // Many of these properties are only in later JavaMail versions + // "mail.smtp.ehlo" //default true + // "mail.smtp.auth" //default false + // "mail.smtp.dsn.ret" //default to nothing... appended as RET= after MAIL FROM line. + // "mail.smtp.dsn.notify" //default to nothing... appended as NOTIFY= after RCPT TO line. + + SMTPTransport transport = null; + try { + transport = (SMTPTransport) session.getTransport(outgoingMailServer); + transport.setLocalHost(props.getProperty("mail.smtp.localhost", configuration.getHeloNameProvider().getHeloName())); + connect(outgoingMailServer, transport); + transport.sendMessage(adaptToTransport(mail.getMessage(), transport), addr); + LOGGER.debug("Mail ({}) sent successfully to {} at {} from {} for {}", mail.getName(), outgoingMailServer.getHostName(), + outgoingMailServer.getHost(), props.get("mail.smtp.from"), mail.getRecipients()); + } finally { + closeTransport(mail, outgoingMailServer, transport); + } + return ExecutionResult.success(); + } + + private Properties getPropertiesForMail(Mail mail) { + Properties props = session.getProperties(); + if (mail.getSender() == null) { + props.put("mail.smtp.from", "<>"); + } else { + String sender = mail.getSender().toString(); + props.put("mail.smtp.from", sender); + } + return props; + } + + private void connect(HostAddress outgoingMailServer, SMTPTransport transport) throws MessagingException { + if (configuration.getAuthUser() != null) { + transport.connect(outgoingMailServer.getHostName(), configuration.getAuthUser(), configuration.getAuthPass()); + } else { + transport.connect(); + } + } + + private MimeMessage adaptToTransport(MimeMessage message, SMTPTransport transport) throws MessagingException { + if (shouldAdapt(transport)) { + try { + converter7Bit.convertTo7Bit(message); + } catch (IOException e) { + LOGGER.error("Error during the conversion to 7 bit.", e); + } + } + return message; + } + + private boolean shouldAdapt(SMTPTransport transport) { + // If the transport is a SMTPTransport (from sun) some performance enhancement can be done. + // If the transport is not the one developed by Sun we are not sure of how it handles the 8 bit mime stuff, so I + // convert the message to 7bit. + return !transport.getClass().getName().endsWith(".SMTPTransport") + || !transport.supportsExtension(BIT_MIME_8); + // if the message is already 8bit or binary and the server doesn't support the 8bit extension it has to be converted + // to 7bit. Javamail api doesn't perform that conversion, but it is required to be a rfc-compliant smtp server. + } + + private void closeTransport(Mail mail, HostAddress outgoingMailServer, SMTPTransport transport) { + if (transport != null) { + try { + // James-899: transport.close() sends QUIT to the server; if that fails + // (e.g. because the server has already closed the connection) the message + // should be considered to be delivered because the error happened outside + // of the mail transaction (MAIL, RCPT, DATA). + transport.close(); + } catch (MessagingException e) { + LOGGER.error("Warning: could not close the SMTP transport after sending mail ({}) to {} at {} for {}; " + + "probably the server has already closed the connection. Message is considered to be delivered. Exception: {}", + mail.getName(), outgoingMailServer.getHostName(), outgoingMailServer.getHost(), mail.getRecipients(), e.getMessage()); + } + transport = null; + } + } + + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MessageComposer.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MessageComposer.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MessageComposer.java new file mode 100644 index 0000000..2dfa0a9 --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/MessageComposer.java @@ -0,0 +1,141 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import javax.mail.MessagingException; +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +import org.apache.mailet.Mail; + +public class MessageComposer { + + private final RemoteDeliveryConfiguration configuration; + + public MessageComposer(RemoteDeliveryConfiguration configuration) { + this.configuration = configuration; + } + + /** + * Try to return a usefull logString created of the Exception which was + * given. Return null if nothing usefull could be done + * + * @param e The MessagingException to use + * @return logString + */ + public String fromException(Exception e) { + if (e.getClass().getName().endsWith(".SMTPSendFailedException")) { + return "RemoteHost said: " + e.getMessage(); + } else if (e instanceof SendFailedException) { + SendFailedException exception = (SendFailedException) e; + + // No error + if (exception.getInvalidAddresses().length == 0 && exception.getValidUnsentAddresses().length == 0) { + return null; + } + + Exception ex; + StringBuilder sb = new StringBuilder(); + boolean smtpExFound = false; + sb.append("RemoteHost said:"); + + if (e instanceof MessagingException) { + while ((ex = ((MessagingException) e).getNextException()) != null && ex instanceof MessagingException) { + e = ex; + if (ex.getClass().getName().endsWith(".SMTPAddressFailedException")) { + try { + InternetAddress ia = (InternetAddress) invokeGetter(ex, "getAddress"); + sb.append(" ( ").append(ia).append(" - [").append(ex.getMessage().replaceAll("\\n", "")).append("] )"); + smtpExFound = true; + } catch (IllegalStateException ise) { + // Error invoking the getAddress method + } catch (ClassCastException cce) { + // The getAddress method returned something + // different than InternetAddress + } + } + } + } + if (!smtpExFound) { + boolean invalidAddr = false; + sb.append(" ( "); + + if (exception.getInvalidAddresses().length > 0) { + sb.append(Arrays.toString(exception.getInvalidAddresses())); + invalidAddr = true; + } + if (exception.getValidUnsentAddresses().length > 0) { + if (invalidAddr) { + sb.append(" "); + } + sb.append(Arrays.toString(exception.getValidUnsentAddresses())); + } + sb.append(" - ["); + sb.append(exception.getMessage().replaceAll("\\n", "")); + sb.append("] )"); + } + return sb.toString(); + } + return null; + } + + public String composeFailLogMessage(Mail mail, ExecutionResult executionResult) { + StringWriter sout = new StringWriter(); + PrintWriter out = new PrintWriter(sout, true); + out.print(permanentAsString(executionResult.isPermanent()) + " exception delivering mail (" + mail.getName() + + ")" + retrieveExceptionLog(executionResult.getException().orElse(null)) + ": "); + if (configuration.isDebug()) { + if (executionResult.getException().isPresent()) { + executionResult.getException().get().printStackTrace(out); + } + } + return sout.toString(); + } + + private String permanentAsString(boolean permanent) { + if (permanent) { + return "Permanent"; + } + return "Temporary"; + } + + private String retrieveExceptionLog(Exception ex) { + String exceptionLog = fromException(ex); + if (exceptionLog != null) { + return ". " + exceptionLog; + } + return ""; + } + + private Object invokeGetter(Object target, String getter) { + try { + Method getAddress = target.getClass().getMethod(getter); + return getAddress.invoke(target); + } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + return new IllegalStateException("Exception invoking " + getter + " on a " + target.getClass() + " object"); + } + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliveryConfiguration.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliveryConfiguration.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliveryConfiguration.java new file mode 100644 index 0000000..63f5451 --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliveryConfiguration.java @@ -0,0 +1,320 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Properties; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.james.domainlist.api.DomainList; +import org.apache.mailet.MailetConfig; +import org.apache.mailet.base.MailetUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.steveash.guavate.Guavate; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; + +public class RemoteDeliveryConfiguration { + + private static final Logger LOGGER = LoggerFactory.getLogger(RemoteDeliveryConfiguration.class); + + public static final String DELIVERY_THREADS = "deliveryThreads"; + public static final String USE_PRIORITY = "usePriority"; + public static final String MAX_DNS_PROBLEM_RETRIES = "maxDnsProblemRetries"; + public static final String HELO_NAME = "heloName"; + public static final String JAVAX_PREFIX = "mail."; + public static final String BIND = "bind"; + public static final String GATEWAY_PASSWORD = "gatewayPassword"; + public static final String GATEWAY_USERNAME_COMPATIBILITY = "gatewayusername"; + public static final String GATEWAY_USERNAME = "gatewayUsername"; + public static final String GATEWAY_PORT = "gatewayPort"; + public static final String GATEWAY = "gateway"; + public static final String SSL_ENABLE = "sslEnable"; + public static final String START_TLS = "startTLS"; + public static final String BOUNCE_PROCESSOR = "bounceProcessor"; + public static final String SENDPARTIAL = "sendpartial"; + public static final String TIMEOUT = "timeout"; + public static final String CONNECTIONTIMEOUT = "connectiontimeout"; + public static final String OUTGOING = "outgoing"; + public static final String MAX_RETRIES = "maxRetries"; + public static final String DELAY_TIME = "delayTime"; + public static final String DEBUG = "debug"; + public static final int DEFAULT_SMTP_TIMEOUT = 180000; + public static final String DEFAULT_OUTGOING_QUEUE_NAME = "outgoing"; + public static final int DEFAULT_CONNECTION_TIMEOUT = 60000; + public static final int DEFAULT_DNS_RETRY_PROBLEM = 0; + public static final int DEFAULT_MAX_RETRY = 5; + public static final String ADDRESS_PORT_SEPARATOR = ":"; + + private final boolean isDebug; + private final boolean usePriority; + private final boolean startTLS; + private final boolean isSSLEnable; + private final boolean isBindUsed; + private final boolean sendPartial; + private final int maxRetries; + private final long smtpTimeout; + private final int dnsProblemRetry; + private final int connectionTimeout; + private final int workersThreadCount; + private final List<Long> delayTimes; + private final HeloNameProvider heloNameProvider; + private final String outGoingQueueName; + private final String bindAddress; + private final String bounceProcessor; + private final Collection<String> gatewayServer; + private final String authUser; + private final String authPass; + private final Properties javaxAdditionalProperties; + + public RemoteDeliveryConfiguration(MailetConfig mailetConfig, DomainList domainList) { + isDebug = MailetUtil.getInitParameter(mailetConfig, DEBUG).orElse(false); + startTLS = MailetUtil.getInitParameter(mailetConfig, START_TLS).orElse(false); + isSSLEnable = MailetUtil.getInitParameter(mailetConfig, SSL_ENABLE).orElse(false); + usePriority = MailetUtil.getInitParameter(mailetConfig, USE_PRIORITY).orElse(false); + sendPartial = MailetUtil.getInitParameter(mailetConfig, SENDPARTIAL).orElse(false); + outGoingQueueName = Optional.ofNullable(mailetConfig.getInitParameter(OUTGOING)).orElse(DEFAULT_OUTGOING_QUEUE_NAME); + bounceProcessor = mailetConfig.getInitParameter(BOUNCE_PROCESSOR); + bindAddress = mailetConfig.getInitParameter(BIND); + + DelaysAndMaxRetry delaysAndMaxRetry = computeDelaysAndMaxRetry(mailetConfig); + maxRetries = delaysAndMaxRetry.getMaxRetries(); + delayTimes = delaysAndMaxRetry.getExpandedDelays(); + smtpTimeout = computeSmtpTimeout(mailetConfig); + connectionTimeout = computeConnectionTimeout(mailetConfig); + dnsProblemRetry = computeDnsProblemRetry(mailetConfig); + heloNameProvider = new HeloNameProvider(mailetConfig.getInitParameter(HELO_NAME), domainList); + workersThreadCount = Integer.valueOf(mailetConfig.getInitParameter(DELIVERY_THREADS)); + + String gatewayPort = mailetConfig.getInitParameter(GATEWAY_PORT); + String gateway = mailetConfig.getInitParameter(GATEWAY); + gatewayServer = computeGatewayServers(gatewayPort, gateway); + if (gateway != null) { + authUser = computeGatewayUser(mailetConfig); + authPass = mailetConfig.getInitParameter(GATEWAY_PASSWORD); + } else { + authUser = null; + authPass = null; + } + isBindUsed = bindAddress != null; + javaxAdditionalProperties = computeJavaxProperties(mailetConfig); + } + + private Properties computeJavaxProperties(MailetConfig mailetConfig) { + Properties result = new Properties(); + // deal with <mail.*> attributes, passing them to javamail + result.putAll( + ImmutableList.copyOf(mailetConfig.getInitParameterNames()) + .stream() + .filter(propertyName -> propertyName.startsWith(JAVAX_PREFIX)) + .map(propertyName -> Pair.of(propertyName, mailetConfig.getInitParameter(propertyName))) + .collect(Guavate.toImmutableMap(Pair::getKey, Pair::getValue))); + return result; + } + + private int computeDnsProblemRetry(MailetConfig mailetConfig) { + String dnsRetry = mailetConfig.getInitParameter(MAX_DNS_PROBLEM_RETRIES); + if (!Strings.isNullOrEmpty(dnsRetry)) { + return Integer.valueOf(dnsRetry); + } else { + return DEFAULT_DNS_RETRY_PROBLEM; + } + } + + private int computeConnectionTimeout(MailetConfig mailetConfig) { + try { + return Integer.valueOf( + Optional.ofNullable(mailetConfig.getInitParameter(CONNECTIONTIMEOUT)) + .orElse(String.valueOf(DEFAULT_CONNECTION_TIMEOUT))); + } catch (Exception e) { + LOGGER.warn("Invalid timeout setting: {}", mailetConfig.getInitParameter(TIMEOUT)); + return DEFAULT_CONNECTION_TIMEOUT; + } + } + + private long computeSmtpTimeout(MailetConfig mailetConfig) { + try { + if (mailetConfig.getInitParameter(TIMEOUT) != null) { + return Integer.valueOf(mailetConfig.getInitParameter(TIMEOUT)); + } else { + return DEFAULT_SMTP_TIMEOUT; + } + } catch (Exception e) { + LOGGER.warn("Invalid timeout setting: {}", mailetConfig.getInitParameter(TIMEOUT)); + return DEFAULT_SMTP_TIMEOUT; + } + } + + private DelaysAndMaxRetry computeDelaysAndMaxRetry(MailetConfig mailetConfig) { + try { + int intendedMaxRetries = Integer.valueOf( + Optional.ofNullable(mailetConfig.getInitParameter(MAX_RETRIES)) + .orElse(String.valueOf(DEFAULT_MAX_RETRY))); + return DelaysAndMaxRetry.from(intendedMaxRetries, mailetConfig.getInitParameter(DELAY_TIME)); + } catch (Exception e) { + LOGGER.warn("Invalid maxRetries setting: {}", mailetConfig.getInitParameter(MAX_RETRIES)); + return DelaysAndMaxRetry.defaults(); + } + } + + private String computeGatewayUser(MailetConfig mailetConfig) { + // backward compatibility with 2.3.x + String user = mailetConfig.getInitParameter(GATEWAY_USERNAME); + if (user == null) { + return mailetConfig.getInitParameter(GATEWAY_USERNAME_COMPATIBILITY); + } + return user; + } + + private List<String> computeGatewayServers(String gatewayPort, String gateway) { + if (gateway != null) { + ImmutableList.Builder<String> builder = ImmutableList.builder(); + Iterable<String> gatewayParts = Splitter.on(',').split(gateway); + for (String gatewayPart : gatewayParts) { + builder.add(parsePart(gatewayPort, gatewayPart)); + } + return builder.build(); + } else { + return ImmutableList.of(); + } + } + + private String parsePart(String gatewayPort, String gatewayPart) { + String address = gatewayPart.trim(); + if (!address.contains(ADDRESS_PORT_SEPARATOR) && gatewayPort != null) { + return address + ADDRESS_PORT_SEPARATOR + gatewayPort; + } + return address; + } + + public Properties createFinalJavaxProperties() { + Properties props = new Properties(); + props.put("mail.debug", "false"); + // Reactivated: javamail 1.3.2 should no more have problems with "250 OK" messages + // (WAS "false": Prevents problems encountered with 250 OK Messages) + props.put("mail.smtp.ehlo", "true"); + // By setting this property to true the transport is allowed to send 8 bit data to the server (if it supports + // the 8bitmime extension). + props.setProperty("mail.smtp.allow8bitmime", "true"); + props.put("mail.smtp.timeout", String.valueOf(smtpTimeout)); + props.put("mail.smtp.connectiontimeout", String.valueOf(connectionTimeout)); + props.put("mail.smtp.sendpartial", String.valueOf(sendPartial)); + props.put("mail.smtp.localhost", heloNameProvider.getHeloName()); + props.put("mail.smtp.starttls.enable", String.valueOf(startTLS)); + props.put("mail.smtp.ssl.enable", String.valueOf(isSSLEnable)); + if (isBindUsed()) { + // undocumented JavaMail 1.2 feature, smtp transport will use + // our socket factory, which will also set the local address + props.put("mail.smtp.socketFactory.class", RemoteDeliverySocketFactory.class.getClass()); + // Don't fallback to the standard socket factory on error, do throw an exception + props.put("mail.smtp.socketFactory.fallback", "false"); + } + if (authUser != null) { + props.put("mail.smtp.auth", "true"); + } + props.putAll(javaxAdditionalProperties); + return props; + } + + public boolean isDebug() { + return isDebug; + } + + public List<Long> getDelayTimes() { + return delayTimes; + } + + public int getMaxRetries() { + return maxRetries; + } + + public long getSmtpTimeout() { + return smtpTimeout; + } + + public boolean isSendPartial() { + return sendPartial; + } + + public int getConnectionTimeout() { + return connectionTimeout; + } + + public int getWorkersThreadCount() { + return workersThreadCount; + } + + public Collection<String> getGatewayServer() { + return gatewayServer; + } + + public String getAuthUser() { + return authUser; + } + + public String getAuthPass() { + return authPass; + } + + public boolean isBindUsed() { + return isBindUsed; + } + + public String getBounceProcessor() { + return bounceProcessor; + } + + public boolean isUsePriority() { + return usePriority; + } + + public boolean isStartTLS() { + return startTLS; + } + + public boolean isSSLEnable() { + return isSSLEnable; + } + + public HeloNameProvider getHeloNameProvider() { + return heloNameProvider; + } + + public String getOutGoingQueueName() { + return outGoingQueueName; + } + + public Properties getJavaxAdditionalProperties() { + return javaxAdditionalProperties; + } + + public int getDnsProblemRetry() { + return dnsProblemRetry; + } + + public String getBindAddress() { + return bindAddress; + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliverySocketFactory.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliverySocketFactory.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliverySocketFactory.java new file mode 100644 index 0000000..60722a2 --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/RemoteDeliverySocketFactory.java @@ -0,0 +1,138 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import javax.net.SocketFactory; + +/** + * <p> + * It is used by RemoteDelivery in order to make possible to bind the client + * socket to a specific ip address. + * </p> + * <p> + * This is not a nice solution because the ip address must be shared by all + * RemoteDelivery instances. It would be better to modify JavaMail (current + * version 1.3) to support a corresonding property, e.g. mail.smtp.bindAdress. + * </p> + * <p> + * This used to not extend javax.net.SocketFactory descendant, because + * <ol> + * <li> + * it was not necessary because JavaMail 1.2 uses reflection when accessing this + * class;</li> + * <li> + * it was not desirable because it would require java 1.4.</li> + * </ol> + * </p> + * <p> + * But since James 2.3.0a1: + * <ol> + * <li>we require Java 1.4 so the dependency on SocketFactory is not really an + * issue;</li> + * <li>Javamail 1.4 cast the object returned by getDefault to SocketFactory and + * fails to create the socket if we don't extend SocketFactory.</li> + * </ol> + * </p> + * <p> + * <strong>Note</strong>: Javamail 1.4 should correctly support + * mail.smtp.localaddr so we could probably get rid of this class and simply add + * that property to the Session. + * </p> + */ +public class RemoteDeliverySocketFactory extends SocketFactory { + + /** + * @param addr + * the ip address or host name the delivery socket will bind to + */ + public static void setBindAdress(String addr) throws UnknownHostException { + if (addr == null) { + bindAddress = null; + } else { + bindAddress = InetAddress.getByName(addr); + } + } + + /** + * the same as the similarly named javax.net.SocketFactory operation. + */ + public static SocketFactory getDefault() { + return new RemoteDeliverySocketFactory(); + } + + /** + * the same as the similarly named javax.net.SocketFactory operation. Just + * to be safe, it is not used by JavaMail 1.3. This is the only method used + * by JavaMail 1.4. + */ + public Socket createSocket() throws IOException { + Socket s = new Socket(); + s.bind(new InetSocketAddress(bindAddress, 0)); + return s; + } + + /** + * the same as the similarly named javax.net.SocketFactory operation. This + * is the one which is used by JavaMail 1.3. This is not used by JavaMail + * 1.4. + */ + public Socket createSocket(String host, int port) throws IOException { + return new Socket(host, port, bindAddress, 0); + } + + /** + * the same as the similarly named javax.net.SocketFactory operation. Just + * to be safe, it is not used by JavaMail 1.3. This is not used by JavaMail + * 1.4. + */ + public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException { + return new Socket(host, port, clientHost == null ? bindAddress : clientHost, clientPort); + } + + /** + * the same as the similarly named javax.net.SocketFactory operation. Just + * to be safe, it is not used by JavaMail 1.3. This is not used by JavaMail + * 1.4. + */ + public Socket createSocket(InetAddress host, int port) throws IOException { + return new Socket(host, port, bindAddress, 0); + } + + /** + * the same as the similarly named javax.net.SocketFactory operation. Just + * to be safe, it is not used by JavaMail 1.3. This is not used by JavaMail + * 1.4. + */ + public Socket createSocket(InetAddress address, int port, InetAddress clientAddress, int clientPort) throws IOException { + return new Socket(address, port, clientAddress == null ? bindAddress : clientAddress, clientPort); + } + + /** + * it should be set by setBindAdress(). Null means the socket is bind to the + * default address. + */ + private static InetAddress bindAddress; +} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/Repeat.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/Repeat.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/Repeat.java new file mode 100644 index 0000000..6926c2d --- /dev/null +++ b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remote/delivery/Repeat.java @@ -0,0 +1,38 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.transport.mailets.remote.delivery; + +import java.util.List; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; + +public class Repeat { + + @SuppressWarnings("unchecked") + public static <T> List<T> repeat(T element, int times) { + Preconditions.checkArgument(times >= 0, "Times argument should be strictly positive"); + return ImmutableList.copyOf( + Iterables.limit( + Iterables.cycle(element), times)); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/AddressesArrayToMailAddressListConverter.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/AddressesArrayToMailAddressListConverter.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/AddressesArrayToMailAddressListConverter.java deleted file mode 100644 index 9fbf7c8..0000000 --- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/AddressesArrayToMailAddressListConverter.java +++ /dev/null @@ -1,60 +0,0 @@ -/**************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one * - * or more contributor license agreements. See the NOTICE file * - * distributed with this work for additional information * - * regarding copyright ownership. The ASF licenses this file * - * to you under the Apache License, Version 2.0 (the * - * "License"); you may not use this file except in compliance * - * with the License. You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - ****************************************************************/ - -package org.apache.james.transport.mailets.remoteDelivery; - -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import javax.mail.Address; -import javax.mail.internet.AddressException; - -import org.apache.james.core.MailAddress; -import org.apache.james.util.OptionalUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.github.steveash.guavate.Guavate; -import com.google.common.collect.ImmutableList; - -public class AddressesArrayToMailAddressListConverter { - private static final Logger LOGGER = LoggerFactory.getLogger(AddressesArrayToMailAddressListConverter.class); - - public static List<MailAddress> getAddressesAsMailAddress(Address[] addresses) { - if (addresses == null) { - return ImmutableList.of(); - } - return Arrays.asList(addresses) - .stream() - .map(address -> toMailAddress(address)) - .flatMap(OptionalUtils::toStream) - .collect(Guavate.toImmutableList()); - } - - private static Optional<MailAddress> toMailAddress(Address address) { - try { - return Optional.of(new MailAddress(address.toString())); - } catch (AddressException e) { - LOGGER.debug("Can't parse unsent address {}", address, e); - return Optional.empty(); - } - } - -} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/Bouncer.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/Bouncer.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/Bouncer.java deleted file mode 100644 index 5b40cc1..0000000 --- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/Bouncer.java +++ /dev/null @@ -1,146 +0,0 @@ -/**************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one * - * or more contributor license agreements. See the NOTICE file * - * distributed with this work for additional information * - * regarding copyright ownership. The ASF licenses this file * - * to you under the Apache License, Version 2.0 (the * - * "License"); you may not use this file except in compliance * - * with the License. You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - ****************************************************************/ - -package org.apache.james.transport.mailets.remoteDelivery; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.net.ConnectException; -import java.net.SocketException; -import java.net.UnknownHostException; - -import javax.mail.MessagingException; -import javax.mail.SendFailedException; - -import org.apache.james.core.MailAddress; -import org.apache.mailet.Mail; -import org.apache.mailet.MailetContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class Bouncer { - private static final Logger LOGGER = LoggerFactory.getLogger(Bouncer.class); - - public static final String DELIVERY_ERROR = "delivery-error"; - private final RemoteDeliveryConfiguration configuration; - private final MailetContext mailetContext; - - public Bouncer(RemoteDeliveryConfiguration configuration, MailetContext mailetContext) { - this.configuration = configuration; - this.mailetContext = mailetContext; - } - - public void bounce(Mail mail, Exception ex) { - if (mail.getSender() == null) { - LOGGER.debug("Null Sender: no bounce will be generated for {}", mail.getName()); - } else { - if (configuration.getBounceProcessor() != null) { - mail.setAttribute(DELIVERY_ERROR, getErrorMsg(ex)); - try { - mailetContext.sendMail(mail, configuration.getBounceProcessor()); - } catch (MessagingException e) { - LOGGER.warn("Exception re-inserting failed mail: ", e); - } - } else { - bounceWithMailetContext(mail, ex); - } - } - } - - - private void bounceWithMailetContext(Mail mail, Exception ex) { - LOGGER.debug("Sending failure message {}", mail.getName()); - try { - mailetContext.bounce(mail, explanationText(mail, ex)); - } catch (MessagingException me) { - LOGGER.warn("Encountered unexpected messaging exception while bouncing message", me); - } catch (Exception e) { - LOGGER.warn("Encountered unexpected exception while bouncing message", e); - } - } - - public String explanationText(Mail mail, Exception ex) { - StringWriter sout = new StringWriter(); - PrintWriter out = new PrintWriter(sout, true); - out.println("Hi. This is the James mail server at " + resolveMachineName() + "."); - out.println("I'm afraid I wasn't able to deliver your message to the following addresses."); - out.println("This is a permanent error; I've given up. Sorry it didn't work out. Below"); - out.println("I include the list of recipients and the reason why I was unable to deliver"); - out.println("your message."); - out.println(); - for (MailAddress mailAddress : mail.getRecipients()) { - out.println(mailAddress); - } - if (ex instanceof MessagingException) { - if (((MessagingException) ex).getNextException() == null) { - out.println(sanitizeExceptionMessage(ex)); - } else { - Exception ex1 = ((MessagingException) ex).getNextException(); - if (ex1 instanceof SendFailedException) { - out.println("Remote mail server told me: " + sanitizeExceptionMessage(ex1)); - } else if (ex1 instanceof UnknownHostException) { - out.println("Unknown host: " + sanitizeExceptionMessage(ex1)); - out.println("This could be a DNS server error, a typo, or a problem with the recipient's mail server."); - } else if (ex1 instanceof ConnectException) { - // Already formatted as "Connection timed out: connect" - out.println(sanitizeExceptionMessage(ex1)); - } else if (ex1 instanceof SocketException) { - out.println("Socket exception: " + sanitizeExceptionMessage(ex1)); - } else { - out.println(sanitizeExceptionMessage(ex1)); - } - } - } - out.println(); - return sout.toString(); - } - - private String sanitizeExceptionMessage(Exception e) { - if (e.getMessage() == null) { - return "null"; - } else { - return e.getMessage().trim(); - } - } - - private String resolveMachineName() { - try { - return configuration.getHeloNameProvider().getHeloName(); - } catch (Exception e) { - return "[address unknown]"; - } - } - - public String getErrorMsg(Exception ex) { - if (ex instanceof MessagingException) { - return getNestedExceptionMessage((MessagingException) ex); - } else { - return sanitizeExceptionMessage(ex); - } - } - - public String getNestedExceptionMessage(MessagingException me) { - if (me.getNextException() == null) { - return sanitizeExceptionMessage(me); - } else { - Exception ex1 = me.getNextException(); - return sanitizeExceptionMessage(ex1); - } - } -} http://git-wip-us.apache.org/repos/asf/james-project/blob/52c18ef6/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/Converter7Bit.java ---------------------------------------------------------------------- diff --git a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/Converter7Bit.java b/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/Converter7Bit.java deleted file mode 100644 index ab798e6..0000000 --- a/server/mailet/mailets/src/main/java/org/apache/james/transport/mailets/remoteDelivery/Converter7Bit.java +++ /dev/null @@ -1,65 +0,0 @@ -/**************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one * - * or more contributor license agreements. See the NOTICE file * - * distributed with this work for additional information * - * regarding copyright ownership. The ASF licenses this file * - * to you under the Apache License, Version 2.0 (the * - * "License"); you may not use this file except in compliance * - * with the License. You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - ****************************************************************/ - -package org.apache.james.transport.mailets.remoteDelivery; - -import java.io.IOException; - -import javax.mail.MessagingException; -import javax.mail.internet.MimeMultipart; -import javax.mail.internet.MimePart; - -import org.apache.mailet.MailetContext; - -public class Converter7Bit { - - private final MailetContext mailetContext; - - public Converter7Bit(MailetContext mailetContext) { - this.mailetContext = mailetContext; - } - - public MimePart convertTo7Bit(MimePart part) throws MessagingException, IOException { - if (part.isMimeType("multipart/*")) { - MimeMultipart parts = (MimeMultipart) part.getContent(); - int count = parts.getCount(); - for (int i = 0; i < count; i++) { - convertTo7Bit((MimePart) parts.getBodyPart(i)); - } - } else if ("8bit".equals(part.getEncoding())) { - // The content may already be in encoded the form (likely with mail - // created from a - // stream). In that case, just changing the encoding to - // quoted-printable will mangle - // the result when this is transmitted. We must first convert the - // content into its - // native format, set it back, and only THEN set the transfer - // encoding to force the - // content to be encoded appropriately. - - // if the part doesn't contain text it will be base64 encoded. - String contentTransferEncoding = part.isMimeType("text/*") ? "quoted-printable" : "base64"; - part.setContent(part.getContent(), part.getContentType()); - part.setHeader("Content-Transfer-Encoding", contentTransferEncoding); - part.addHeader("X-MIME-Autoconverted", "from 8bit to " + contentTransferEncoding + " by " + mailetContext.getServerInfo()); - } - return part; - } - -} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
