/* * 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); } }