1 package org.ibex.mail.protocol;
2 import org.ibex.mail.*;
3 import org.ibex.mail.store.*;
4 import org.ibex.mail.target.*;
5 import org.ibex.util.*;
11 public class SMTP extends MessageProtocol {
13 public static String convdir = null;
14 public static void main(String[] s) throws Exception {
15 String logto = System.getProperty("ibex.mail.root", File.separatorChar + "var" + File.separatorChar + "org.ibex.mail");
16 logto += File.separatorChar + "log";
18 convdir = System.getProperty("ibex.mail.root", File.separatorChar + "var" + File.separatorChar + "org.ibex.mail");
19 convdir += File.separatorChar + "conversation";
20 new File(convdir).mkdirs();
21 SMTP smtp = new SMTP();
22 int port = Integer.parseInt(System.getProperty("ibex.mail.port", "25"));
23 Log.info(SMTP.class, "binding to port " + port + "...");
24 ServerSocket ss = new ServerSocket(port);
25 Log.info(SMTP.class, "listening for connections...");
27 final Socket sock = ss.accept();
28 new Thread() { public void run() { smtp.handle(sock); } }.start();
32 //public SMTP() { setProtocolName("SMTP"); }
33 //public ServerRequest createRequest(Connection conn) { return new Listener((TcpConnection)conn); }
35 public void handle(Socket s) { new Listener(s, "megacz.com").handleRequest(); }
37 public static class Outgoing {
38 // recommended retry interval is 30 minutes
39 // give up after 4-5 days
40 // should keep per-host success/failure so we don't retry on every message
41 // exponential backoff on retry time?
42 // check DNS resolvability as soon as domain is provided
43 // only use implicit A-record if there are no MX-records
44 // use null-sender for error messages (don't send errors to the null addr)
45 // to prevent mail loops, drop messages with >100 Recieved headers
46 private final org.ibex.util.Queue queue = new org.ibex.util.Queue(100);
47 public static void send(Message m) { }
48 public static void enqueue(Message m) { }
49 public static void bounce(Message m, String reason) { }
52 MessageStore store = MessageStore.root.slash("smtp").slash("outgoing");
53 int[] outgoing = store.list();
54 for(int i=0; i<outgoing.length; i++) queue.append(store.get(outgoing[i]));
56 Message next = queue.remove(true);
63 private static String lastTime = null;
64 private static int lastCounter = 0;
66 private class Listener extends Incoming /*implements ServerRequest*/ {
69 public void init() { }
70 public Listener(Socket conn, String vhost) { this.vhost = vhost; this.conn = conn; }
73 //public Listener(TcpConnection conn) { this.conn = conn; conn.getSocket().setSoTimeout(5 * 60 * 1000); }
74 public boolean handleRequest() {
76 conn.setSoTimeout(5 * 60 * 1000);
77 StringBuffer logMessage = new StringBuffer();
78 String time = new SimpleDateFormat("yy.MMM.dd-hh:mm:ss").format(new Date());
79 synchronized (SMTP.class) {
80 if (lastTime != null && lastTime.equals(time)) {
81 time += "." + (++lastCounter);
86 String conversationId = time;
87 Log.setThreadAnnotation("[conversation/" + conversationId + "] ");
88 InetSocketAddress remote = (InetSocketAddress)conn.getRemoteSocketAddress();
89 Log.info(this, "connection from " + remote.getHostName() + ":" + remote.getPort() +
90 " (" + remote.getAddress() + ")");
92 new PrintWriter(new OutputStreamWriter(new FileOutputStream(convdir + File.separatorChar + conversationId)));
94 return handleRequest(new LoggedLineReader(new InputStreamReader(conn.getInputStream()), logf),
95 new LoggedPrintWriter(new OutputStreamWriter(conn.getOutputStream()), logf));
96 } catch(Throwable t) {
100 Log.setThreadAnnotation("");
102 } catch (Exception e) {
108 private class LoggedLineReader extends LineReader {
110 public LoggedLineReader(Reader r, PrintWriter log) { super(r); this.log = log; }
111 public String readLine() throws IOException {
112 String s = super.readLine();
113 if (s != null) { log.println("C: " + s); log.flush(); }
118 private class LoggedPrintWriter extends PrintWriter {
120 public LoggedPrintWriter(Writer w, PrintWriter log) { super(w); this.log = log; }
121 public void println(String s) {
122 log.println("S: " + s);
128 public boolean handleRequest(LineReader rs, PrintWriter ws) throws IOException, MailException {
129 //ReadStream rs = conn.getReadStream();
130 //WriteStream ws = conn.getWriteStream();
131 //ws.setNewLineString("\r\n");
132 ws.println("220 " + vhost + " ESMTP " + this.getClass().getName());
134 Vector to = new Vector();
136 String command = rs.readLine();
137 String c = command.toUpperCase();
138 if (c.startsWith("HELO")) {
139 ws.println("250 HELO " + vhost);
143 } else if (c.startsWith("EHLO")) {
144 ws.println("250-" + vhost);
145 ws.println("250-SIZE");
146 ws.println("250 PIPELINING");
150 } else if (c.startsWith("RSET")) {
153 ws.println("250 reset ok");
155 } else if (c.startsWith("MAIL FROM:")) {
156 command = command.substring(10).trim();
157 from = new Address(command);
158 ws.println("250 " + from + " is syntactically correct");
160 } else if (c.startsWith("RCPT TO:")) {
162 ws.println("503 MAIL FROM must precede RCPT TO");
165 // FIXME: 551 = no, i won't forward that
166 command = command.substring(9).trim();
167 if(command.indexOf(' ') != -1) command = command.substring(0, command.indexOf(' '));
168 Address addr = new Address(command);
170 ws.println("250 " + addr + " is syntactically correct");
172 } else if (c.startsWith("DATA")) {
173 if (from == null) { ws.println("503 MAIL FROM command must precede DATA"); continue; }
174 if (to == null) { ws.println("503 RCPT TO command must precede DATA"); continue; }
175 ws.println("354 Enter message, ending with \".\" on a line by itself");
176 StringBuffer data = new StringBuffer();
177 // move this into the Message class
178 boolean good = false;
180 accept(new Message(new DotTerminatedLineReader(rs)));
181 } catch (MailException.Malformed mfe) {
182 ws.println("501 " + mfe.toString()); break;
183 } catch (MailException.MailboxFull mbf) {
184 ws.println("452 " + mbf);
185 } catch (IOException ioe) {
186 ws.println("554 " + ioe.toString()); break;
188 ws.println("250 message accepted"); break;
190 } else if (c.startsWith("HELP")) { ws.println("214 you are beyond help. see a trained professional.");
191 } else if (c.startsWith("VRFY")) { ws.println("252 We don't VRFY; proceed anyway");
192 } else if (c.startsWith("EXPN")) { ws.println("550 EXPN not available");
193 } else if (c.startsWith("NOOP")) { ws.println("250 OK");
194 } else if (c.startsWith("QUIT")) { ws.println("221 " + vhost + " closing connection"); break;
195 } else { ws.println("500 unrecognized command");
199 return false; // always tell resin to close the connection
203 private static class DotTerminatedLineReader extends LineReader {
204 private final LineReader r;
205 private boolean done = false;
206 public DotTerminatedLineReader(LineReader r) { super(null); this.r = r; }
207 public String readLine() throws IOException {
208 if (done) return null;
209 String s = r.readLine();
210 if (s.equals(".")) { done = true; return null; }
211 if (s.startsWith(".")) return s.substring(1);