/*
* Created on Nov 9, 2004
*
* This file is part of susimail project, see http://susi.i2p/
*
* Copyright (C) 2004-2005 <susi23@mail.i2p>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Revision: 1.3 $
*/
package i2p.susi.webmail.smtp;
import i2p.susi.debug.Debug;
import i2p.susi.webmail.Messages;
import i2p.susi.webmail.encoding.Encoding;
import i2p.susi.webmail.encoding.EncodingException;
import i2p.susi.webmail.encoding.EncodingFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import net.i2p.data.DataHelper;
/**
* @author susi
*/
public class SMTPClient {
private Socket socket;
private final byte buffer[];
public String error;
private String lastResponse;
private boolean supportsPipelining;
private static final Encoding base64;
static {
base64 = EncodingFactory.getEncoding( "base64" );
}
public SMTPClient()
{
buffer = new byte[10240];
error = "";
lastResponse = "";
}
/**
* Wait for response
* @param cmd may be null
* @return result code or 0 for failure
*/
private int sendCmd( String cmd ) {
return sendCmd(cmd, true);
}
/**
* @param cmd may be null
* @param shouldWait if false, don't wait for response, and return 100
* @return result code or 0 for failure
* @since 0.9.13
*/
private int sendCmd(String cmd, boolean shouldWait)
{
if( socket == null )
return 0;
try {
if (cmd != null)
sendCmdNoWait(cmd);
if (!shouldWait)
return 100;
socket.getOutputStream().flush();
return getResult();
} catch (IOException e) {
error += "IOException occured.\n";
return 0;
}
}
/**
* Does not flush, wait, or read
*
* @param cmd non-null
* @since 0.9.13
*/
private void sendCmdNoWait(String cmd) throws IOException
{
Debug.debug( Debug.DEBUG, "SMTP sendCmd(" + cmd +")" );
if( socket == null )
throw new IOException("no socket");
OutputStream out = socket.getOutputStream();
cmd += "\r\n";
out.write(DataHelper.getASCII(cmd));
}
/**
* Pipeline if supported
*
* @param cmds non-null
* @return number of successful commands
* @since 0.9.13
*/
private int sendCmds(List<SendExpect> cmds)
{
int rv = 0;
if (supportsPipelining) {
Debug.debug(Debug.DEBUG, "SMTP pipelining " + cmds.size() + " commands");
try {
for (SendExpect cmd : cmds) {
sendCmdNoWait(cmd.send);
}
socket.getOutputStream().flush();
} catch (IOException ioe) {
return 0;
}
for (SendExpect cmd : cmds) {
int r = getResult();
// stop only on EOF
if (r == 0)
break;
if (r == cmd.expect)
rv++;
}
} else {
for (SendExpect cmd : cmds) {
int r = sendCmd(cmd.send);
// stop at first error
if (r != cmd.expect)
break;
rv++;
}
}
Debug.debug(Debug.DEBUG, "SMTP success in " + rv + " of " + cmds.size() + " commands");
return rv;
}
/**
* @return result code or 0 for failure
* @since 0.9.13
*/
private int getResult() {
return getFullResult().result;
}
/**
* @return result code and string, all lines combined with \r separators,
* first 3 bytes are the ASCII return code or "000" for failure
* Result and Result.recv non null
* @since 0.9.13
*/
private Result getFullResult() {
int result = 0;
StringBuilder fullResponse = new StringBuilder(512);
try {
InputStream in = socket.getInputStream();
StringBuilder buf = new StringBuilder(128);
while (DataHelper.readLine(in, buf)) {
Debug.debug(Debug.DEBUG, "SMTP rcv \"" + buf.toString().trim() + '"');
int len = buf.length();
if (len < 4) {
result = 0;
break; // huh? no nnn\r?
}
if( result == 0 ) {
try {
String r = buf.substring(0, 3);
result = Integer.parseInt(r);
} catch ( NumberFormatException nfe ) {
break;
}
}
fullResponse.append(buf.substring(4));
if (buf.charAt(3) == ' ')
break;
buf.setLength(0);
}
} catch (IOException e) {
error += "IOException occured.\n";
result = 0;
}
lastResponse = fullResponse.toString();
return new Result(result, lastResponse);
}
/**
* @return success
*/
public boolean sendMail( String host, int port, String user, String pass, String sender, Object[] recipients, String body )
{
boolean mailSent = false;
boolean ok = true;
try {
socket = new Socket( host, port );
} catch (IOException e) {
error += _t("Cannot connect") + ": " + e.getMessage() + '\n';
ok = false;
}
try {
// SMTP ref: RFC 821
// Pipelining ref: RFC 2920
// AUTH ref: RFC 4954
if (ok) {
socket.setSoTimeout(120*1000);
int result = sendCmd(null);
if (result != 220) {
error += _t("Server refused connection") + " (" + result + ")\n";
ok = false;
}
}
if (ok) {
sendCmdNoWait( "EHLO localhost" );
socket.getOutputStream().flush();
socket.setSoTimeout(60*1000);
Result r = getFullResult();
if (r.result == 250) {
supportsPipelining = r.recv.contains("PIPELINING");
} else {
error += _t("Server refused connection") + " (" + r.result + ")\n";
ok = false;
}
}
if (ok) {
// RFC 4954 says AUTH must be the last but let's assume
// that includes the user/pass on following lines
List<SendExpect> cmds = new ArrayList<SendExpect>();
cmds.add(new SendExpect("AUTH LOGIN", 334));
cmds.add(new SendExpect(base64.encode(user), 334));
cmds.add(new SendExpect(base64.encode(pass), 235));
if (sendCmds(cmds) != 3) {
error += _t("Login failed") + '\n';
ok = false;
}
}
if (ok) {
List<SendExpect> cmds = new ArrayList<SendExpect>();
cmds.add(new SendExpect("MAIL FROM: " + sender, 250));
for( int i = 0; i < recipients.length; i++ ) {
cmds.add(new SendExpect("RCPT TO: " + recipients[i], 250));
}
cmds.add(new SendExpect("DATA", 354));
if (sendCmds(cmds) != cmds.size()) {
// TODO which recipient?
error += _t("Mail rejected") + '\n';
ok = false;
}
}
if (ok) {
if( body.indexOf( "\r\n.\r\n" ) != -1 )
body = body.replace( "\r\n.\r\n", "\r\n..\r\n" );
socket.getOutputStream().write(DataHelper.getUTF8(body));
socket.getOutputStream().write(DataHelper.getASCII("\r\n.\r\n"));
socket.setSoTimeout(0);
int result = sendCmd(null);
if (result == 250)
mailSent = true;
else
error += _t("Error sending mail") + " (" + result + ")\n";
}
} catch (IOException e) {
error += _t("Error sending mail") + ": " + e.getMessage() + '\n';
} catch (EncodingException e) {
error += e.getMessage();
}
if( !mailSent && lastResponse.length() > 0 ) {
String[] lines = DataHelper.split(lastResponse, "\r");
for( int i = 0; i < lines.length; i++ )
error += lines[i] + '\n';
}
sendCmd("QUIT", false);
if( socket != null ) {
try {
socket.close();
} catch (IOException e1) {}
}
return mailSent;
}
/**
* A command to send and a result code to expect
* @since 0.9.13
*/
private static class SendExpect {
public final String send;
public final int expect;
public SendExpect(String s, int e) {
send = s;
expect = e;
}
}
/**
* A result string and code
* @since 0.9.13
*/
private static class Result {
public final int result;
public final String recv;
public Result(int r, String t) {
result = r;
recv = t;
}
}
/** translate */
private static String _t(String s) {
return Messages.getString(s);
}
}