package mireka.smtp.client; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.regex.Pattern; import javax.inject.Inject; import mireka.smtp.EnhancedStatus; import mireka.smtp.SendException; import mireka.transmission.immediate.Upstream; import org.subethamail.smtp.client.PlainAuthenticator; /** * BackendServer specifies another SMTP server which is used as a proxy target * or smarthost. It may be part of an {@link Upstream}. */ public class BackendServer { private static final String IPV6_PREFIX = "[IPv6:"; private static final Pattern dottedQuad = Pattern .compile("\\d{1,3}(\\.\\d{1,3}){3}"); private ClientFactory clientFactory; /** * The host name in the format as it appears in the configuration. This * format is theoretically ambiguous, so it is only appropriate for * informational purposes. */ private String host; /** * The host name formatted in the same way as the remote part of a mailbox. * For example: * <ul> * <li>mail.example.com * <li>[192.0.2.0] * <li>[IPv6:::1] * </ul> */ private String smtpFormattedHost; /** * The IP address which was specified in the host field. It the host field * contains a domain name, then this field is null. */ private InetAddress fixedAddress; private int port = 25; private String user; private String password; /** * {@link BackendServer#setWeight} */ private double weight = 1; /** * {@link BackendServer#setBackup} */ private boolean backup = false; /** * * @throws SendException * if the IP address of the backend server could not be * determined based on its domain name. */ public SmtpClient createClient() throws SendException { SmtpClient client = clientFactory.create(); if (user != null) { PlainAuthenticator authenticator = new PlainAuthenticator(client, user, password); client.setAuthenticator(authenticator); } InetAddress address; try { address = fixedAddress != null ? fixedAddress : InetAddress .getByName(host); } catch (UnknownHostException e) { // without detailed information, assume it is a temporary failure throw new SendException("Resolving the backend " + this.toString() + " domain failed.", e, new EnhancedStatus(450, "4.4.0", "Domain name resolution failed")); } client.setMtaAddress(new MtaAddress(smtpFormattedHost, address, port)); return client; } @Override public String toString() { return host + ":" + port; } /** * @x.category GETSET */ public String getHost() { return host; } /** * Sets the domain name or IP address of the backend server. The name may * contain a domain name or IPv4 or IPv6 literals in various forms. It * guesses the actual type of the name. * <p> * Examples for legal values: * <ul> * <li>mail.example.com * <li>[192.0.2.0] * <li>192.0.2.0 * <li>[IPv6:::1] * <li>[::1] * <li>::1 * </ul> */ public void setHost(String host) { this.host = host; if (host == null) throw new NullPointerException(); if (host.isEmpty()) throw new IllegalArgumentException(); if (host.charAt(0) == '[') { // literal if (host.charAt(host.length() - 1) != ']') throw new IllegalArgumentException(); if (host.length() > IPV6_PREFIX.length() && IPV6_PREFIX.equalsIgnoreCase(host.substring(0, IPV6_PREFIX.length()))) setHostByAddress(host.substring(IPV6_PREFIX.length(), host.length())); setHostByAddress(host.substring(1, host.length())); } else { if (dottedQuad.matcher(host).matches()) setHostByAddress(host); if (host.contains(":")) setHostByAddress(host); setHostByDomain(host); } } private void setHostByDomain(String domain) { smtpFormattedHost = domain; fixedAddress = null; } private void setHostByAddress(String address) { try { InetAddress inetAddress = InetAddress.getByName(address); if (inetAddress instanceof Inet4Address) { smtpFormattedHost = "[" + address + "]"; } else if (inetAddress instanceof Inet6Address) { smtpFormattedHost = "[IPv6:" + address + "]"; } else { throw new RuntimeException(); } fixedAddress = inetAddress; } catch (UnknownHostException e) { // impossible, the argument is an IP address, not a domain name. throw new RuntimeException("Assertion failed"); } } public ClientFactory getClientFactory() { return clientFactory; } @Inject public void setClientFactory(ClientFactory clientFactory) { this.clientFactory = clientFactory; } /** * @x.category GETSET */ public int getPort() { return port; } /** * @x.category GETSET */ public void setPort(int port) { this.port = port; } /** * @x.category GETSET */ public String getUser() { return user; } /** * @x.category GETSET */ public void setUser(String user) { this.user = user; } /** * @x.category GETSET */ public String getPassword() { return password; } /** * @x.category GETSET */ public void setPassword(String password) { this.password = password; } /** * @x.category GETSET */ public double getWeight() { return weight; } /** * Relative weight of the server in an Upstream. Default is 1. * * @x.category GETSET */ public void setWeight(double weight) { this.weight = weight; } /** * @x.category GETSET */ public boolean isBackup() { return backup; } /** * True indicates that the server should only be used in an Upstream if all * non-backup servers failed. Default is false. * * @x.category GETSET */ public void setBackup(boolean backup) { this.backup = backup; } }