package org.subethamail.smtp.client;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A somewhat smarter abstraction of an SMTP client which doesn't require knowing
* anything about the nitty gritty of SMTP.
*
* @author Jeff Schnitzer
*/
public class SmartClient extends SMTPClient
{
/** */
private static Logger log = LoggerFactory.getLogger(SmartClient.class);
/** */
boolean sentFrom;
int recipientCount;
/** The host name which is sent in the HELO and EHLO commands */
private String heloHost;
/**
* True if the server sent a 421
* "Service not available, closing transmission channel" response. In this
* case the QUIT command should not be sent.
*/
private boolean serverClosingTransmissionChannel = false;
/**
* SMTP extensions supported by the server, and their parameters as the
* server specified it in response to the EHLO command. Key is the extension
* keyword in upper case, like "AUTH", value is the extension parameters
* string in unparsed form. If the server does not support EHLO, then this
* map is empty.
*/
private final Map<String, String> extensions = new HashMap<String, String>();
/**
* If supplied (not null), then it will be called after EHLO, to
* authenticate this client to the server.
*/
private Authenticator authenticator = null;
/**
* Creates an unconnected client.
*/
public SmartClient()
{
// nothing to do
}
/**
* Connects to the specified server and issues the initial HELO command.
*
* @throws UnknownHostException if problem looking up hostname
* @throws SMTPException if problem reported by the server
* @throws IOException if problem communicating with host
*/
public SmartClient(String host, int port, String myHost) throws UnknownHostException, IOException, SMTPException
{
this(host, port, null, myHost);
}
/**
* Connects to the specified server and issues the initial HELO command.
*
* @throws UnknownHostException if problem looking up hostname
* @throws SMTPException if problem reported by the server
* @throws IOException if problem communicating with host
*/
public SmartClient(String host, int port, SocketAddress bindpoint, String myHost) throws UnknownHostException,
IOException, SMTPException
{
this.setBindpoint(bindpoint);
this.setHeloHost(myHost);
this.connect(host, port);
}
/**
* Connects to the specified server and issues the initial HELO command. It
* gracefully closes the connection if it could be established but
* subsequently it fails or if the server does not accept messages.
*/
@Override
public void connect(String host, int port)
throws SMTPException, AuthenticationNotSupportedException, IOException
{
if (heloHost == null)
throw new IllegalStateException("Helo host must be specified before connecting");
super.connect(host, port);
try
{
this.receiveAndCheck(); // The server announces itself first
this.sendHeloOrEhlo();
if (this.authenticator != null)
this.authenticator.authenticate();
}
catch (SMTPException e)
{
this.quit();
throw e;
}
catch (AuthenticationNotSupportedException e)
{
this.quit();
throw e;
}
catch (IOException e)
{
this.close(); // just close the socket, issuing QUIT is hopeless now
throw e;
}
}
/**
* Sends the EHLO command, or HELO if EHLO is not supported, and saves the
* list of SMTP extensions which are supported by the server.
*/
protected void sendHeloOrEhlo() throws IOException, SMTPException
{
extensions.clear();
Response resp = this.sendReceive("EHLO " + heloHost);
if (resp.isSuccess())
{
parseEhloResponse(resp);
}
else if (resp.getCode() == 500 || resp.getCode() == 502)
{
// server does not support EHLO, try HELO
this.sendAndCheck("HELO " + heloHost);
}
else {
// some serious error
throw new SMTPException(resp);
}
}
/**
* Extracts the list of SMTP extensions from the server's response to EHLO,
* and stores them in {@link #extensions}.
*/
private void parseEhloResponse(Response resp) throws IOException {
BufferedReader reader = new BufferedReader(new StringReader(resp.getMessage()));
// first line contains server name and welcome message, skip it
reader.readLine();
String line;
while (null != (line = reader.readLine()))
{
int iFirstSpace = line.indexOf(' ');
String keyword = iFirstSpace == -1 ? line : line.substring(0, iFirstSpace);
String parameters = iFirstSpace == -1 ? "" : line.substring(iFirstSpace + 1);
extensions.put(keyword.toUpperCase(Locale.ENGLISH), parameters);
}
}
/**
* Returns the server response. It takes note of a 421 response code, so
* QUIT will not be issued unnecessarily.
*/
@Override
protected Response receive() throws IOException
{
Response response = super.receive();
if (response.getCode() == 421)
serverClosingTransmissionChannel = true;
return response;
}
/** */
public void from(String from) throws IOException, SMTPException
{
this.sendAndCheck("MAIL FROM: <" + from + ">");
this.sentFrom = true;
}
/** */
public void to(String to) throws IOException, SMTPException
{
this.sendAndCheck("RCPT TO: <" + to + ">");
this.recipientCount++;
}
/**
* Prelude to writing data
*/
public void dataStart() throws IOException, SMTPException
{
this.sendAndCheck("DATA");
}
/**
* Actually write some data
*/
public void dataWrite(byte[] data, int numBytes) throws IOException
{
this.dataOutput.write(data, 0, numBytes);
}
/**
* Last step after writing data
*/
public void dataEnd() throws IOException, SMTPException
{
this.dataOutput.flush();
this.dotTerminatedOutput.writeTerminatingSequence();
this.dotTerminatedOutput.flush();
this.receiveAndCheck();
}
/**
* Quit and close down the connection. Ignore any errors.
* <p>
* It still closes the connection, but it does not send the QUIT command if
* a 421 Service closing transmission channel is received previously. In
* these cases QUIT would fail anyway.
*
* @see <a href="http://tools.ietf.org/html/rfc5321#section-3.8">RFC 5321
* Terminating Sessions and Connections</a>
*/
public void quit()
{
try
{
if (this.isConnected() && !this.serverClosingTransmissionChannel)
this.sendAndCheck("QUIT");
}
catch (IOException ex)
{
log.warn("Failed to issue QUIT to " + this.hostPort);
}
this.close();
}
/**
* @return true if we have already specified from()
*/
public boolean sentFrom()
{
return this.sentFrom;
}
/**
* @return true if we have already specified to()
*/
public boolean sentTo()
{
return this.recipientCount > 0;
}
/**
* @return the number of recipients that have been accepted by the server
*/
public int getRecipientCount()
{
return this.recipientCount;
}
/**
* Returns the SMTP extensions supported by the server.
*
* @return the extension map. Key is the extension keyword in upper
* case, value is the unparsed string of extension parameters.
*/
public Map<String, String> getExtensions()
{
return extensions;
}
/**
* Sets the domain name or address literal of this system, which name will
* be sent to the server in the parameter of the HELO and EHLO commands.
* This has no default and is required.
*/
public void setHeloHost(String myHost)
{
this.heloHost = myHost;
}
/**
* Returns the HELO name of this system.
*/
public String getHeloHost()
{
return heloHost;
}
/**
* Returns the Authenticator object, which is used to authenticate this
* client to the server, or null, if no authentication is required.
*/
public Authenticator getAuthenticator()
{
return authenticator;
}
/**
* Sets the Authenticator object which will be called after the EHLO command
* to authenticate this client to the server. The default is that no
* authentication will happen.
*/
public void setAuthenticator(Authenticator authenticator)
{
this.authenticator = authenticator;
}
}