
org.subethamail.smtp.client.SmartClient Maven / Gradle / Ivy
Show all versions of subethasmtp Show documentation
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.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.subethamail.smtp.client.SMTPClient.Response;
import com.github.davidmoten.guavamini.Preconditions;
/**
* A somewhat smarter abstraction of an SMTP client which doesn't require
* knowing anything about the nitty gritty of SMTP.
*
* @author Jeff Schnitzer
*/
// not final so can mock with Mockito
public class SmartClient {
private static final Logger log = LoggerFactory.getLogger(SmartClient.class);
/** The host name which is sent in the HELO and EHLO commands */
private final String heloHost;
/**
* 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 extensions = new HashMap();
/**
* If supplied (not null), then it will be called after EHLO, to
* authenticate this client to the server.
*/
private final Optional authenticator;
private final SMTPClient client;
//mutable state
private int recipientCount;
/**
* 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;
/**
* 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
*/
private SmartClient(String myHost) throws UnknownHostException, IOException, SMTPException {
this(Optional.empty(), myHost, Optional.empty());
}
/**
* Constructor.
*
* @param bindpoint
* @param clientHeloHost
* @param authenticator
* the Authenticator object which will be called after the EHLO
* command to authenticate this client to the server. If is null
* then no authentication will happen.
* @throws UnknownHostException
* if problem looking up hostname
* @throws IOException
* if problem communicating with host
* @throws SMTPException
* if problem reported by the server
*/
private SmartClient(Optional bindpoint, String clientHeloHost, Optional authenticator)
throws UnknownHostException, IOException, SMTPException {
Preconditions.checkNotNull(bindpoint, "bindpoint cannot be null");
Preconditions.checkNotNull(clientHeloHost, "clientHeloHost cannot be null");
Preconditions.checkNotNull(authenticator, "authenticator cannot be null");
this.client = new SMTPClient(bindpoint, Optional.empty());
this.heloHost = clientHeloHost;
this.authenticator = authenticator;
}
public final static SmartClient createAndConnect(String host, int port, String clientHeloHost)
throws UnknownHostException, SMTPException, IOException {
return createAndConnect(host, port, Optional.empty(), clientHeloHost, Optional.empty());
}
public final static SmartClient createAndConnect(String host, int port, Optional bindpoint,
String clientHeloHost, Optional authenticator)
throws UnknownHostException, SMTPException, IOException {
SmartClient client = new SmartClient(bindpoint, clientHeloHost, authenticator);
client.connect(host, port);
return client;
}
/**
* 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.
*/
public void connect(String host, int port)
throws SMTPException, AuthenticationNotSupportedException, IOException {
client.connect(host, port);
try {
client.receiveAndCheck(); // The server announces itself first
this.sendHeloOrEhlo();
if (this.authenticator.isPresent())
this.authenticator.get().authenticate();
} catch (SMTPException e) {
this.quit();
throw e;
} catch (AuthenticationNotSupportedException e) {
this.quit();
throw e;
} catch (IOException e) {
client.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 = client.sendReceive("EHLO " + heloHost);
if (resp.isSuccess()) {
parseEhloResponse(resp);
} else if (resp.getCode() == 500 || resp.getCode() == 502) {
// server does not support EHLO, try HELO
client.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.
*/
protected Response receive() throws IOException {
Response response = client.receive();
if (response.getCode() == 421)
serverClosingTransmissionChannel = true;
return response;
}
public void from(String from) throws IOException, SMTPException {
client.sendAndCheck("MAIL FROM: <" + from + ">");
}
public void to(String to) throws IOException, SMTPException {
client.sendAndCheck("RCPT TO: <" + to + ">");
this.recipientCount++;
}
/**
* Prelude to writing data
*/
public void dataStart() throws IOException, SMTPException {
client.sendAndCheck("DATA");
}
public void bdat(String text, boolean isLast) throws IOException {
client.send("BDAT " + text.length() + (isLast? " LAST": ""));
dataWrite(text.getBytes(StandardCharsets.UTF_8));
client.dataOutput.flush();
client.receiveAndCheck();
}
public void bdat(String text) throws IOException {
bdat(text, false);
}
public void bdatLast(String text) throws IOException {
bdat(text, true);
}
/**
* Actually write some data
*/
public void dataWrite(byte[] data, int numBytes) throws IOException {
client.dataOutput.write(data, 0, numBytes);
}
public void dataWrite(byte[] data) throws IOException {
client.dataOutput.write(data, 0, data.length);
}
/**
* Last step after writing data
*/
public void dataEnd() throws IOException, SMTPException {
client.dataOutput.flush();
client.dotTerminatedOutput.writeTerminatingSequence();
client.dotTerminatedOutput.flush();
client.receiveAndCheck();
}
/**
* Quit and close down the connection. Ignore any errors.
*
* 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 RFC 5321
* Terminating Sessions and Connections
*/
public void quit() {
try {
if (client.isConnected() && !this.serverClosingTransmissionChannel)
client.sendAndCheck("QUIT");
} catch (IOException ex) {
log.warn("Failed to issue QUIT to " + client.getHostPort());
}
client.close();
}
/**
* @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 getExtensions() {
return extensions;
}
/**
* 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 Optional getAuthenticator() {
return authenticator;
}
public void sendAndCheck(String msg) throws SMTPException, IOException {
client.sendAndCheck(msg);
}
}