From 061ffff05bec1bda946025bd87097ecafec4fbe9 Mon Sep 17 00:00:00 2001 From: adam Date: Sun, 20 Jun 2004 11:25:06 +0000 Subject: [PATCH] almost there darcs-hash:20040620112506-5007d-a0b605c38e57ebab700ed2af5ff36886e5ff7bbf.gz --- src/org/ibex/mail/Main.java | 81 ++++++ src/org/ibex/mail/Message.java | 23 +- src/org/ibex/mail/Query.java | 8 +- src/org/ibex/mail/Target.java | 4 +- src/org/ibex/mail/protocol/Connection.java | 104 +++++++ src/org/ibex/mail/protocol/Fax.java | 2 +- src/org/ibex/mail/protocol/IMAP.java | 198 +++++-------- src/org/ibex/mail/protocol/MessageProtocol.java | 7 - src/org/ibex/mail/protocol/NNTP.java | 2 +- src/org/ibex/mail/protocol/RSS.java | 2 +- src/org/ibex/mail/protocol/SMS.java | 2 +- src/org/ibex/mail/protocol/SMTP.java | 344 +++++++---------------- src/org/ibex/mail/protocol/WebMail.java | 2 +- src/org/ibex/mail/target/FileBasedMailbox.java | 3 +- src/org/ibex/mail/target/Mailbox.java | 7 +- 15 files changed, 397 insertions(+), 392 deletions(-) create mode 100644 src/org/ibex/mail/Main.java create mode 100644 src/org/ibex/mail/protocol/Connection.java delete mode 100644 src/org/ibex/mail/protocol/MessageProtocol.java diff --git a/src/org/ibex/mail/Main.java b/src/org/ibex/mail/Main.java new file mode 100644 index 0000000..6a94626 --- /dev/null +++ b/src/org/ibex/mail/Main.java @@ -0,0 +1,81 @@ +package org.ibex.mail; +import org.ibex.mail.target.*; +import org.ibex.mail.protocol.*; +import org.ibex.util.*; +import java.io.*; +import java.net.*; +import java.util.*; + +public class Main { + + public static void main(String[] s) throws Exception { + + // set up logging + String logto = System.getProperty("ibex.mail.root", File.separatorChar + "var" + File.separatorChar + "org.ibex.mail"); + logto += File.separatorChar + "log"; + Log.file(logto); + + new IMAPThread().start(); + new SMTPThread().start(); + + } + + private static class BogusAuthenticator implements IMAP.Server.Authenticator { + final Mailbox root = FileBasedMailbox.getFileBasedMailbox(Mailbox.STORAGE_ROOT, true); + public Mailbox authenticate(String u, String p) { + if (u.equals("megacz") && p.equals("pass")) return root; + return null; + } + } + + private static class IMAPThread extends Thread { + final int port = Integer.parseInt(System.getProperty("ibex.mail.imap.port", "143")); + public void run() { + try { + Log.info(this, "binding to port " + port + "..."); + ServerSocket ss = new ServerSocket(port); + Log.info(this, "listening for connections..."); + for(;;) { + final Socket sock = ss.accept(); + new Thread() { + public void run() { + try { + new IMAP.Listener(sock, "megacz.com", new BogusAuthenticator()).handle(); + } catch (Exception e) { + Log.warn(this, e); + } + } + }.start(); + } + } catch (Exception e) { + Log.warn(this, e); + } + } + } + + private static class SMTPThread extends Thread { + final int port = Integer.parseInt(System.getProperty("ibex.mail.smtp.port", "25")); + public void run() { + try { + Log.info(this, "binding to port " + port + "..."); + ServerSocket ss = new ServerSocket(port); + Log.info(this, "listening for connections..."); + while(true) { + final Socket sock = ss.accept(); + final SMTP.Server smtp = new SMTP.Server(sock, "megacz.com"); + new Thread() { + public void run() { + try { + smtp.handle(); + } catch (Exception e) { + Log.warn(this, e); + } + } + }.start(); + } + } catch (Exception e) { + Log.warn(this, e); + } + } + } +} diff --git a/src/org/ibex/mail/Message.java b/src/org/ibex/mail/Message.java index d7034e2..2aa9331 100644 --- a/src/org/ibex/mail/Message.java +++ b/src/org/ibex/mail/Message.java @@ -11,6 +11,8 @@ import java.io.*; // folded headers: can insert CRLF anywhere that whitespace appears (before the whitespace) // date/time parsing: see spec, 3.3 +// FIXME: messages must NEVER contain 8-bit binary data; this is a violation of IMAP + // FEATURE: PGP-signature-parsing // FEATURE: mailing list header parsing // FEATURE: delivery status notification (and the sneaky variety) @@ -41,15 +43,14 @@ public class Message extends JSReflection { public final Trace[] traces; public final Address envelopeFrom; - public final Address[] envelopeTo; + public final Address envelopeTo; public final Date arrival; // when the message first arrived at this machine; IMAP "internal date message attr" - // FIXME: need to be able to read back in the EnvelopeFrom / EnvelopeTo fields public void dump(OutputStream os) throws IOException { Writer w = new OutputStreamWriter(os); w.write("X-org.ibex.mail-envelopeFrom: " + envelopeFrom + "\r\n"); - w.write("X-org.ibex.mail-envelopeTo: "); for(int i=0;i= min && it.cur().rfc822size() <= max; - case HEADER: return it.cur().headers.get(key) != null && ((String)it.cur().headers.get(key)).indexOf(text) != -1; - case BODY: return it.cur().body.indexOf(text) != -1; - case FULL: return it.cur().body.indexOf(text) != -1 || it.cur().allHeaders.indexOf(text) != -1; + case HEADER: return it.cur().headers.get(key) != null && + ((String)it.cur().headers.get(key)).toLowerCase().indexOf(text.toLowerCase()) != -1; + case BODY: return it.cur().body.toLowerCase().indexOf(text.toLowerCase()) != -1; + case FULL: return it.cur().body.toLowerCase().indexOf(text.toLowerCase()) != -1 || + it.cur().allHeaders.indexOf(text) != -1; case DELETED: return it.deleted(); case SEEN: return it.seen(); case FLAGGED: return it.flagged(); diff --git a/src/org/ibex/mail/Target.java b/src/org/ibex/mail/Target.java index c30d573..f845f9f 100644 --- a/src/org/ibex/mail/Target.java +++ b/src/org/ibex/mail/Target.java @@ -5,5 +5,7 @@ import org.ibex.js.*; /** base class for mail message "destinations" */ public class Target { public static final Target root = Script.root(); - public void accept(Message m) throws IOException, MailException { /* FIXME */ } + public void accept(Message m) throws IOException, MailException { + throw new MailException("Target.accept() unimplemented"); + } } diff --git a/src/org/ibex/mail/protocol/Connection.java b/src/org/ibex/mail/protocol/Connection.java new file mode 100644 index 0000000..fe61ff6 --- /dev/null +++ b/src/org/ibex/mail/protocol/Connection.java @@ -0,0 +1,104 @@ +package org.ibex.mail.protocol; +import org.ibex.mail.*; +import org.ibex.util.*; +import org.ibex.mail.target.*; +import java.util.*; +import java.net.*; +import java.text.*; +import java.io.*; + +/** central place for logging conversations */ +public abstract class Connection { + + protected Socket conn; + protected String vhost; + protected PrintWriter pw; + protected InputStream is; + protected PushbackReader r; + protected LineReader lr; + + private static String convdir; + static { + try { + convdir = System.getProperty("ibex.mail.root", File.separatorChar + "var" + File.separatorChar + "org.ibex.mail"); + convdir += File.separatorChar + "conversation"; + new File(convdir).mkdirs(); + } catch (Exception e) { + Log.warn(Connection.class, e); + } + } + + public Connection(Socket conn, String vhost) throws IOException { + this.vhost = vhost; + this.conn = conn; + this.pw = new PrintWriter(new OutputStreamWriter(conn.getOutputStream())); + this.is = conn.getInputStream(); + Reader isr = new InputStreamReader(is); + this.r = new PushbackReader(isr); + this.lr = new LineReader(isr); + } + + protected abstract boolean handleRequest() throws IOException; + + private String cid; + private StringBuffer inbound = new StringBuffer(); + private PrintWriter conversation; + + public final boolean handle() throws IOException { + cid = getConversation(); + Log.setThreadAnnotation("[conversation " + cid + "] "); + InetSocketAddress remote = (InetSocketAddress)conn.getRemoteSocketAddress(); + Log.info(this, "connection from "+remote.getHostName()+":"+remote.getPort()+" ("+remote.getAddress()+")"); + conversation = new PrintWriter(new OutputStreamWriter(new FileOutputStream(convdir + File.separatorChar + cid + ".txt"))); + boolean ret = handleRequest(); + Log.setThreadAnnotation(""); + conversation.close(); + return ret; + } + + protected void println(String s) throws IOException { + conversation.println("S: " + s); + pw.print(s); + pw.print("\r\n"); + pw.flush(); + } + protected void flush() throws IOException { pw.flush(); } + protected String readln() throws IOException { + String line = lr.readLine(); + conversation.println("C: " + line); + return line; + } + public char getc() throws IOException { + int ret = r.read(); + if (ret == -1) throw new EOFException(); + if (ret == '\n') { if (inbound.length() > 0) { conversation.println("C: " + inbound.toString()); inbound.setLength(0); } } + else if (ret != '\r') inbound.append((char)ret); + return (char)ret; + } + public char peekc() throws IOException { + int ret = r.read(); + if (ret == -1) throw new EOFException(); + r.unread(ret); + return (char)ret; + } + public void fill(byte[] b) throws IOException { + int num = 0; + while (num < b.length) { + int numread = is.read(b, num, b.length - num); + if (numread == -1) throw new EOFException(); + num += numread; + } + } + + private static String lastTime = null; + private static int lastCounter = 0; + static String getConversation() { + String time = new SimpleDateFormat("yy.MMM.dd-hh:mm:ss").format(new Date()); + synchronized (SMTP.class) { + if (lastTime != null && lastTime.equals(time)) time += "." + (++lastCounter); + else lastTime = time; + } + return time; + } + +} diff --git a/src/org/ibex/mail/protocol/Fax.java b/src/org/ibex/mail/protocol/Fax.java index 56271f3..df8d798 100644 --- a/src/org/ibex/mail/protocol/Fax.java +++ b/src/org/ibex/mail/protocol/Fax.java @@ -1,4 +1,4 @@ package org.ibex.mail.protocol; /** Fax send/recieve/gateway */ -public class Fax extends MessageProtocol { +public class Fax { } diff --git a/src/org/ibex/mail/protocol/IMAP.java b/src/org/ibex/mail/protocol/IMAP.java index acf8f03..7c131d2 100644 --- a/src/org/ibex/mail/protocol/IMAP.java +++ b/src/org/ibex/mail/protocol/IMAP.java @@ -7,23 +7,10 @@ import java.net.*; import java.text.*; import java.io.*; -// FIXMEFIXME: recent, seen - -// FIXME: substring searches are case-insensitive - // FIXME: this is valid LSUB "" asdfdas%*%*%*%*SFEFGWEF -// FIXME: recent is ONLY possible in response to a FETCH -// FIXME: p61 red pen -// FIXME: page 11, red pen -// FIXME: page 36 Noselect -// FIXME: be an asshole about which commands are valid when authed/not-authed -// FIXME: must always send BYE before closing connection // FIXME: be very careful about where/when we quotify stuff -// FIXME: we can never send 8-bit binary data, even when the message contains it -// FIXME: TRYCREATE (page 46) -// FIXME: assume UTF-8 -// FIXME: UTF-7 mailbox names (5.1.3) -// FIXME: asynchronous client notifications?!?! +// FIXME: 'UID FOO 100:*' must match at least one message even if all UIDs less than 100 +// FIXME: LIST should return INBOX if applicable // Relevant RFC's: // RFC 2060: IMAPv4 @@ -31,6 +18,7 @@ import java.io.*; // RFC 3691: UNSELECT // RFC 2971: ID +// FEATURE: make sure we're being case-insensitive enough; probably want to upcase atom() // FEATURE: MIME-queries and BODYSTRUCTURE // FEATURE: READ-WRITE / READ-ONLY status on SELECT // FEATURE: pipelining @@ -39,6 +27,7 @@ import java.io.*; // FEATURE: subscriptions // FEATURE: tune for efficiency // FEATURE: STARTTLS +// FEATURE: asynchronous client notifications (need to re-read RFC) public class IMAP { @@ -52,7 +41,7 @@ public class IMAP { public static interface Client { public void expunge(int uid); - public void list(char separator, String mailbox, boolean lsub); + public void list(char separator, String mailbox, boolean lsub, boolean phantom); public void fetch(int num, int flags, int size, Message m, int muid); // m may be null or incomplete } @@ -79,9 +68,9 @@ public class IMAP { public int[] search(Query q, boolean uid); public void rename(String from, String to); public void select(String mailbox, boolean examineOnly); - public void setFlags(Query q, int flags, boolean uid); - public void removeFlags(Query q, int flags, boolean uid); - public void addFlags(Query q, int flags, boolean uid); + public void setFlags(Query q, int flags, boolean uid, boolean silent); + public void removeFlags(Query q, int flags, boolean uid, boolean silent); + public void addFlags(Query q, int flags, boolean uid, boolean silent); public void expunge(); public void fetch(Query q, int spec, String[] headers, int start, int end, boolean uid); public void lsub(String start, String ref); @@ -93,13 +82,6 @@ public class IMAP { } - // SocketWrapper ////////////////////////////////////////////////////////////////////////////// - - public static class SocketWrapper /* implements Server */ { - // eventually this will implement the client side of an IMAP socket conversation - } - - // MailboxWrapper ////////////////////////////////////////////////////////////////////////////// /** wraps an IMAP.Server interface around a Mailbox */ @@ -109,12 +91,12 @@ public class IMAP { Mailbox inbox = null; Mailbox selected = null; + Mailbox root = null; Mailbox selected() { if (selected == null) throw new Bad("no mailbox selected"); return selected; } final Server.Authenticator auth; final Client client; - final Mailbox root; - public MailboxWrapper(Mailbox root, Server.Authenticator auth, Client c) { this.root=root; this.auth=auth; this.client=c;} + public MailboxWrapper(Server.Authenticator auth, Client c) { this.auth=auth; this.client=c;} private Mailbox mailbox(String name, boolean create) { if (name.equalsIgnoreCase("inbox")) return inbox; @@ -124,24 +106,24 @@ public class IMAP { return m; } - // FIXME should return INBOX if applicable // FEATURE: not accurate when a wildcard and subsequent non-wildcards both match a single component public void lsub(String start, String ref) { list(start, ref, true); } public void list(String start, String ref) { list(start, ref, false); } public void list(String start, String ref, boolean lsub) { - if (ref.length() == 0) { client.list(sep, start, lsub); return; } + if (ref.length() == 0) { client.list(sep, start, lsub, false); return; } while (start.endsWith(""+sep)) start = start.substring(0, start.length() - 1); if (ref.endsWith("%")) ref = ref + sep; String[] children = (start.length() == 0 ? root : mailbox(start, false)).children(); for(int i=0; i 0 ? sep+"" : "") + s; + boolean phantom = mailbox(kid, false).phantom(); while(true) { if (pre.length() == 0) { - if (s.length() == 0) client.list(sep, kid, lsub); + if (s.length() == 0) client.list(sep, kid, lsub, phantom); } else switch(pre.charAt(0)) { case sep: if (s.length() == 0) list(kid, pre.substring(1), lsub); break; - case '%': client.list(sep, kid, lsub); pre = pre.substring(1); s = ""; continue; - case '*': client.list(sep, kid, lsub); list(kid, pre, lsub); pre = pre.substring(1); break; + case '%': client.list(sep,kid,lsub,phantom);pre=pre.substring(1); s = ""; continue; + case '*': client.list(sep,kid,lsub,phantom);list(kid,pre,lsub);pre=pre.substring(1); break; default: if (s.length()==0) break; if (s.charAt(0) != pre.charAt(0)) break; s = s.substring(1); pre = pre.substring(1); continue; @@ -164,17 +146,18 @@ public class IMAP { } public void copy(Query q, String to) { copy(q, mailbox(to, false)); } - - // FIXME: TRYCREATE - /* */ void copy(Query q, Mailbox to) { for(Mailbox.Iterator it=selected().iterator(q);it.next();) to.add(it.cur()); } - public void login(String u, String p) { if ((inbox = auth.authenticate(u,p)) == null) throw new No("Login failed."); } + /* */ void copy(Query q, Mailbox to) { + for(Mailbox.Iterator it=selected().iterator(q);it.next();) to.add(it.cur(), it.flags() | Mailbox.Flag.RECENT); } + + public void login(String u, String p) { + if ((root = auth.authenticate(u,p)) == null) throw new No("Login failed."); + inbox = mailbox("INBOX", false); // FEATURE: ?? + } public void unselect() { selected = null; } - public void delete(String m0) { - Mailbox m=mailbox(m0,false); if (m.equals(inbox)) m.destroy(); else throw new Bad("can't delete inbox"); } + public void delete(String m0) { delete(mailbox(m0,false)); } + public void delete(Mailbox m) { if (!m.equals(inbox)) m.destroy(false); else throw new Bad("can't delete inbox"); } public void create(String m) { mailbox(m, true); } - - // FIXME: must always set RECENT flag - public void append(String m, int f, Date a, String b) { mailbox(m, false).add(new Message(null,null,b,a),f); } + public void append(String m,int f,Date a,String b){mailbox(m,false).add(new Message(null,null,b,a),f|Mailbox.Flag.RECENT);} public void check() { } public void noop() { } public void logout() { } @@ -189,31 +172,41 @@ public class IMAP { public int uidNext(String mailbox) { return mailbox(mailbox, false).uidNext(); } public int uidValidity(String mailbox) { return mailbox(mailbox, false).uidValidity(); } public void select(String mailbox, boolean examineOnly) { selected = mailbox(mailbox, false); } + public int[] search(Query q, boolean uid) { Vec.Int vec = new Vec.Int(); - for(Mailbox.Iterator it = selected().iterator(q); it.next();) vec.addElement(uid ? it.uid() : it.num()); + for(Mailbox.Iterator it = selected().iterator(q); it.next();) { + vec.addElement(uid ? it.uid() : it.num()); + it.recent(false); + } return vec.dump(); } - public void setFlags(Query q, int f, boolean uid) { doFlags(q, f, uid, 0); } - public void addFlags(Query q, int f, boolean uid) { doFlags(q, f, uid, 1); } - public void removeFlags(Query q, int f, boolean uid) { doFlags(q, f, uid, -1); } - private void doFlags(Query q, int flags, boolean uid, int style) { + + public void setFlags(Query q, int f, boolean uid, boolean silent) { doFlags(q, f, uid, 0, silent); } + public void addFlags(Query q, int f, boolean uid, boolean silent) { doFlags(q, f, uid, 1, silent); } + public void removeFlags(Query q, int f, boolean uid, boolean silent) { doFlags(q, f, uid, -1, silent); } + + private void doFlags(Query q, int flags, boolean uid, int style, boolean silent) { for(Mailbox.Iterator it = selected().iterator(q);it.next();) { + boolean recent = it.recent(); if (style == -1) it.removeFlags(flags); else if (style == 0) it.setFlags(flags); else if (style == 1) it.addFlags(flags); - client.fetch(it.num(), it.flags(), -1, null, it.uid()); + it.recent(recent); + if (!silent) client.fetch(it.num(), it.flags(), -1, null, it.uid()); } } public void rename(String from0, String to) { Mailbox from = mailbox(from0, false); if (from.equals(inbox)) { from.copy(Query.all(), mailbox(to, true)); } - else if (to.equalsIgnoreCase("inbox")) { from.copy(Query.all(), mailbox(to, true)); from.destroy(); } + else if (to.equalsIgnoreCase("inbox")) { from.copy(Query.all(), mailbox(to, true)); from.destroy(false); } else from.rename(to); } public void fetch(Query q, int spec, String[] headers, int start, int end, boolean uid) { - for(Mailbox.Iterator it = selected().iterator(q); it.next(); ) + for(Mailbox.Iterator it = selected().iterator(q); it.next(); ) { client.fetch(it.num(), it.flags(), it.cur().rfc822size(), it.cur(), it.uid()); + it.recent(false); + } } } @@ -221,31 +214,32 @@ public class IMAP { // Single Session Handler ////////////////////////////////////////////////////////////////////////////// /** takes an IMAP.Server and exposes it to the world as an IMAP server on a TCP socket */ - private static class Listener extends Parser implements Client { + public static class Listener extends Parser implements Client { String selectedName = null; - Mailbox inbox = null; + Mailbox inbox = null, root = null; final Server api; - final Mailbox root; - final Socket conn; - final String vhost; - public void init() { } - public Listener(Socket conn, Mailbox root, Server.Authenticator auth) throws IOException { - this(conn, java.net.InetAddress.getLocalHost().getHostName(), root, auth); } - public Listener(Socket conn, String vhost, Mailbox root, Server.Authenticator auth) throws IOException { - super(conn); - this.api = new IMAP.MailboxWrapper(root, auth, this); - this.vhost = vhost; this.conn = conn; this.root = root; + public Listener(Socket conn, Server.Authenticator auth) throws IOException { + this(conn, java.net.InetAddress.getLocalHost().getHostName(), auth); } + public Listener(Socket conn, String vhost, Server.Authenticator auth) throws IOException { + super(conn, vhost); + this.api = new IMAP.MailboxWrapper(auth, this); } private Token[] lastfetch = null; // hack private boolean lastuid = false; // hack + private boolean selected = false; + private void selected() { if (!selected) throw new Server.Bad("no mailbox selected"); } + private void star(String s) { try { println("* " + s); } catch (IOException e) { throw new MailException.IOException(e); } } + private String qq(String s) { return Printer.qq(s); } // Callbacks ////////////////////////////////////////////////////////////////////////////// public void expunge(int uid) { star(uid + " EXPUNGE"); } - public void list(char sep, String box, boolean lsub) {star((lsub?"LSUB":"LIST")+" () \""+sep+"\" \""+box+"\"");} - public void fetch(int num, int flags, int size, Message m, int muid) { fetch(m, lastfetch, num, flags, size, lastuid, muid); } + public void list(char sep, String box, boolean lsub, boolean phantom) + {star((lsub?"LSUB":"LIST")+" ("+(phantom?"\\Noselect":"")+") \""+sep+"\" \""+box+"\"");} + public void fetch(int num, int flags, int size, Message m, int muid) { + fetch(m, lastfetch, num, flags, size, lastuid, muid); } /** * Parse a fetch request or emit a fetch reply. @@ -294,7 +288,7 @@ public class IMAP { if (r.length() > initlen) r.append(" "); if (t[i] == null || t[i].s == null) continue; String s = t[i].s.toUpperCase(); - r.append(s.equals("BODY.PEEK")?"BODY":s); + r.append(s.equalsIgnoreCase("BODY.PEEK")?"BODY":s); if (s.equals("BODYSTRUCTURE")) { spec|=BODYSTRUCTURE;if(e){r.append(" ");r.append(Printer.bodystructure(m));} } else if (s.equals("ENVELOPE")) { spec|=ENVELOPE; if(e){r.append(" ");r.append(Printer.envelope(m));} } else if (s.equals("FLAGS")) { spec|=FLAGS; if(e){r.append(" ");r.append(Printer.flags(flags));} @@ -307,7 +301,7 @@ public class IMAP { } else if (!(s.equals("BODY.PEEK") || s.equals("BODY"))) { throw new Server.No("unknown fetch argument: " + s); } else { if (s.equalsIgnoreCase("BODY.PEEK")) spec |= PEEK; - else if (e) api.addFlags(Query.num(new int[] { num, num }), Mailbox.Flag.SEEN, false); + else if (e) api.addFlags(Query.num(new int[] { num, num }), Mailbox.Flag.SEEN, false, false); if (i 0) Log.info(IMAP.Client.class, log); log = ""; - Log.debug(IMAP.Server.class, s); - } + public abstract static class Parser extends Connection { + public Parser(Socket conn, String vhost) throws IOException { super(conn, vhost); } protected Query query() { String s = null; boolean not = false; @@ -500,7 +475,7 @@ public class IMAP { Token t = token(); if (t.type == t.LIST) throw new Server.No("nested queries not yet supported FIXME"); else if (t.type == t.SET) return Query.num(t.set()); - s = t.atom(); + s = t.atom().toUpperCase(); if (s.equals("NOT")) { not = true; continue; } if (s.equals("OR")) return Query.or(query(), query()); // FIXME parse rest of list if (s.equals("AND")) return Query.and(query(), query()); @@ -616,30 +591,7 @@ public class IMAP { if (c == '(' || c == ')' || c == '{' || c == ' ' || c == '%' || c == '*' || c == '\"' | c == '\\') bad("invalid char in atom: " + c); } - return s; // FIXME: make sure we're being case-insensitive enough; probably want to upcase here - } - } - - private String log = ""; - public char getc() throws IOException { - int ret = r.read(); - if (ret == -1) throw new EOFException(); - if (ret == '\n') { if (log.length() > 0) { Log.info(IMAP.class, log); log = ""; } } - else if (ret != '\r') log += (char)ret; - return (char)ret; - } - public char peekc() throws IOException { - int ret = r.read(); - if (ret == -1) throw new EOFException(); - r.unread(ret); - return (char)ret; - } - public void fill(byte[] b) throws IOException { - int num = 0; - while (num < b.length) { - int numread = is.read(b, num, b.length - num); - if (numread == -1) throw new EOFException(); - num += numread; + return s; } } @@ -789,10 +741,10 @@ public class IMAP { final Socket s = ss.accept(); new Thread() { public void run() { try { final Mailbox root = FileBasedMailbox.getFileBasedMailbox(Mailbox.STORAGE_ROOT+File.separatorChar+"imap", true); - new Listener(s, root, + new Listener(s, new Server.Authenticator() { public Mailbox authenticate(String u, String p) { - if (u.equals("megacz")&&p.equals("pass")) return root.slash("users",true).slash("megacz",true); + if (u.equals("megacz")&&p.equals("pass")) return root; return null; } } ).handleRequest(); } catch (Exception e) { e.printStackTrace(); } } }.start(); diff --git a/src/org/ibex/mail/protocol/MessageProtocol.java b/src/org/ibex/mail/protocol/MessageProtocol.java deleted file mode 100644 index b5079ef..0000000 --- a/src/org/ibex/mail/protocol/MessageProtocol.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.ibex.mail.protocol; -/** base class for over-the-wire protocols used to send, recieve, and serve messages */ -//import com.caucho.server.connection.*; -//import com.caucho.server.port.*; - -public class MessageProtocol/* extends com.caucho.server.port.Protocol*/ { -} diff --git a/src/org/ibex/mail/protocol/NNTP.java b/src/org/ibex/mail/protocol/NNTP.java index 150c8d0..b13884e 100644 --- a/src/org/ibex/mail/protocol/NNTP.java +++ b/src/org/ibex/mail/protocol/NNTP.java @@ -1,4 +1,4 @@ package org.ibex.mail.protocol; /** NNTP send/recieve */ -public class NNTP extends MessageProtocol { +public class NNTP { } diff --git a/src/org/ibex/mail/protocol/RSS.java b/src/org/ibex/mail/protocol/RSS.java index 2eef456..458c73f 100644 --- a/src/org/ibex/mail/protocol/RSS.java +++ b/src/org/ibex/mail/protocol/RSS.java @@ -1,4 +1,4 @@ package org.ibex.mail.protocol; /** RSS send/recieve/gateway */ -public class RSS extends MessageProtocol { +public class RSS { } diff --git a/src/org/ibex/mail/protocol/SMS.java b/src/org/ibex/mail/protocol/SMS.java index 6cd3dc0..56a2181 100644 --- a/src/org/ibex/mail/protocol/SMS.java +++ b/src/org/ibex/mail/protocol/SMS.java @@ -1,4 +1,4 @@ package org.ibex.mail.protocol; /** SMS send/recieve/gateway */ -public class SMS extends MessageProtocol { +public class SMS { } diff --git a/src/org/ibex/mail/protocol/SMTP.java b/src/org/ibex/mail/protocol/SMTP.java index 8261247..4b0ea72 100644 --- a/src/org/ibex/mail/protocol/SMTP.java +++ b/src/org/ibex/mail/protocol/SMTP.java @@ -9,60 +9,86 @@ import java.text.*; import javax.naming.*; import javax.naming.directory.*; -public class SMTP extends MessageProtocol { - - // FIXME - private static final Mailbox outgoing = null; +// FEATURE: exponential backoff on retry time? +public class SMTP { + private static final Mailbox spool = + FileBasedMailbox.getFileBasedMailbox(Mailbox.STORAGE_ROOT, false).slash("spool", true).slash("smtp", true); static { new Thread() { public void run() { Outgoing.runq(); } }.start(); } - public static String convdir = null; - public static void main(String[] s) throws Exception { - String logto = System.getProperty("ibex.mail.root", File.separatorChar + "var" + File.separatorChar + "org.ibex.mail"); - logto += File.separatorChar + "log"; - Log.file(logto); - convdir = System.getProperty("ibex.mail.root", File.separatorChar + "var" + File.separatorChar + "org.ibex.mail"); - convdir += File.separatorChar + "conversation"; - new File(convdir).mkdirs(); - final SMTP smtp = new SMTP(); - int port = Integer.parseInt(System.getProperty("ibex.mail.port", "25")); - Log.info(SMTP.class, "binding to port " + port + "..."); - ServerSocket ss = new ServerSocket(port); - Log.info(SMTP.class, "listening for connections..."); - while(true) { - final Socket sock = ss.accept(); - new Thread() { public void run() { smtp.handle(sock); } }.start(); + + // Server ////////////////////////////////////////////////////////////////////////////// + + public static class Server extends Connection { + public Server(Socket conn, String vhost) throws IOException { super(conn, vhost); } + public boolean handleRequest() throws IOException, MailException { + conn.setSoTimeout(5 * 60 * 1000); + println("220 " + vhost + " ESMTP " + this.getClass().getName()); + Address from = null; + Vector to = new Vector(); + for(String command = readln(); ; command = readln()) { + String c = command.toUpperCase(); + if (c.startsWith("HELO")) { println("250 HELO " + vhost); from = null; to = new Vector(); + } else if (c.startsWith("EHLO")) { println("250-\r\n250-SIZE\r\n250 PIPELINING"); from = null; to = new Vector(); + } else if (c.startsWith("RSET")) { println("250 reset ok"); from = null; to = new Vector(); + } else if (c.startsWith("HELP")) { println("214 you are beyond help. see a trained professional."); + } else if (c.startsWith("VRFY")) { println("252 We don't VRFY; proceed anyway"); + } else if (c.startsWith("EXPN")) { println("550 EXPN not available"); + } else if (c.startsWith("NOOP")) { println("250 OK"); + } else if (c.startsWith("QUIT")) { println("221 " + vhost + " closing connection"); return false; + } else if (c.startsWith("MAIL FROM:")) { + println("250 " + (from = new Address(command.substring(10).trim())) + " is syntactically correct"); + } else if (c.startsWith("RCPT TO:")) { + if (from == null) { println("503 MAIL FROM must precede RCPT TO"); continue; } + command = command.substring(9).trim(); + if(command.indexOf(' ') != -1) command = command.substring(0, command.indexOf(' ')); + Address addr = new Address(command); + InetAddress[] mx = getMailExchangerIPs(addr.host); + to.addElement(addr); + if (((InetSocketAddress)conn.getRemoteSocketAddress()).getAddress().isLoopbackAddress()) { + println("250 you are connected locally, so I will let you send"); + } else { + boolean good = false; + for(int i=0; !good && i= 100) { - Log.warn(SMTP.Outgoing.class, - "Message with " + m.traces.length + " trace hops; silently dropping\n" + m.summary()); - return; - } - synchronized(Outgoing.class) { - outgoing.add(m); - queue.append(m); + if (m.traces.length >= 100) + Log.warn(SMTP.Outgoing.class, "Message with " + m.traces.length + " trace hops; dropping\n" + m.summary()); + else synchronized(Outgoing.class) { + spool.add(m); Outgoing.class.notify(); } } - // FIXME!!! ignores more than one destination envelope!!!! private static boolean attempt(Message m) throws IOException { - InetAddress[] mx = getMailExchangerIPs(m.envelopeTo[0].host); + InetAddress[] mx = getMailExchangerIPs(m.envelopeTo.host); if (mx.length == 0) { - Log.warn(SMTP.Outgoing.class, "could not resolve " + m.envelopeTo[0].host + "; bouncing it\n" + m.summary()); - send(m.bounce("could not resolve " + m.envelopeTo[0].host)); + Log.warn(SMTP.Outgoing.class, "could not resolve " + m.envelopeTo.host + "; bouncing it\n" + m.summary()); + send(m.bounce("could not resolve " + m.envelopeTo.host)); return true; } if (new Date().getTime() - m.arrival.getTime() > 1000 * 60 * 60 * 24 * 5) { @@ -77,196 +103,56 @@ public class SMTP extends MessageProtocol { return false; } - private static void check(String s) throws IOException { - if (s.startsWith("4") || s.startsWith("5")) throw new IOException("SMTP Error: " + s); } - private static boolean attempt(Message m, InetAddress mx) { + private static void check(String s) { if (s.startsWith("4")||s.startsWith("5")) throw new MailException(s); } + private static boolean attempt(final Message m, final InetAddress mx) { + boolean accepted0 = false; try { - String vhost = InetAddress.getLocalHost().getHostName(); - String cid = getConversation(); - PrintWriter logf = new PrintWriter(new OutputStreamWriter(new FileOutputStream(convdir+File.separatorChar+cid))); - Log.setThreadAnnotation("[outgoing smtp: " + mx + " / " + cid + "] "); Log.info(SMTP.Outgoing.class, "connecting..."); - Socket s = new Socket(mx, 25); - Log.info(SMTP.Outgoing.class, "connected"); - LineReader r = new LoggedLineReader(new InputStreamReader(s.getInputStream()), logf); - PrintWriter w = new LoggedPrintWriter(new OutputStreamWriter(s.getOutputStream()), logf); - check(r.readLine()); // banner - w.print("HELO " + vhost + "\r\n"); check(r.readLine()); - w.print("MAIL FROM: " + m.envelopeFrom + "\r\n"); check(r.readLine()); - w.print("RCPT TO: " + m.envelopeTo + "\r\n"); check(r.readLine()); - w.print("DATA\r\n"); check(r.readLine()); - w.print(m.body); - w.print(".\r\n"); - check(r.readLine()); - Log.info(SMTP.Outgoing.class, "message accepted by " + mx); - // FIXME! - //outgoing.delete(m); - s.close(); - return true; + accepted0 = new Connection(new Socket(mx, 25), InetAddress.getLocalHost().getHostName()) { + public boolean handleRequest() throws IOException { + Log.info(SMTP.Outgoing.class, "connected"); + check(readln()); // banner + println("HELO " + vhost ); check(readln()); + println("MAIL FROM: " + m.envelopeFrom); check(readln()); + println("RCPT TO: " + m.envelopeTo); check(readln()); + println("DATA"); check(readln()); + println(m.body); + println("."); + check(readln()); + Log.info(SMTP.Outgoing.class, "message accepted by " + mx); + boolean accepted = true; + try { + conn.close(); + } finally { + return accepted; + } + } + }.handle(); + return accepted0; } catch (Exception e) { + if (accepted0) return true; Log.warn(SMTP.Outgoing.class, "unable to send; error=" + e); Log.warn(SMTP.Outgoing.class, e); return false; - } finally { - Log.setThreadAnnotation("[outgoing smtp] "); - } + } finally { Log.setThreadAnnotation("[outgoing smtp] "); } } static void runq() { try { Log.setThreadAnnotation("[outgoing smtp] "); - Log.info(SMTP.Outgoing.class, "outgoing thread started; " + outgoing.count(Query.all()) + " messages to send"); - for(Mailbox.Iterator it = outgoing.iterator(); it.cur() != null; it.next()) queue.append(it.cur()); + Log.info(SMTP.Outgoing.class, "outgoing thread started; " + spool.count(Query.all()) + " messages to send"); while(true) { - int num = queue.size(); - for(int i=0; i