--- /dev/null
+==============================================================================
+org.ibex.mail
+
+
+A mail server offering the same level of flexibility as Java Servlets.
+
+The org.ibex.mail server infrastructure is designed to run within
+Resin. This gives us immediate access to Resin's advanced features
+including:
+
+ - Sophisticated configuration file management in XML
+ - Expression Language expansion within configuration files
+ - JNI-driven SSL
+ - Thread pooling
+ - Shared configuration of virtual hosts between web-apps and mail-apps
+ - Authentication infrastructure
+
--- /dev/null
+package org.ibex.mail;
+// FIXME MIME: RFC2045, 2046, 2049
+// NOTE: always use Win32 line endings
+// hard line limit: 998 chars
+// soft line limit (suggested): 78 chars
+// header fields: ascii 33-126 (but no colon)
+// field body: anything ASCII except CRLF
+// folded headers: can insert CRLF anywhere that whitespace appears (before the whitespace)
+// body needs CRLF; one or the other alone is not acceptable
+// date/time parsing: see 3.3
+
+// FEATURE: PGP-signature-parsing
+// FEATURE: mailing list header parsing
+// FEATURE: delivery status notification (and the sneaky variety)
+// FEATURE: threading as in http://www.jwz.org/doc/threading.html
+public class Message {
+
+ public final String allHeaders; // pristine headers
+ public final Hashtable headers; // hash of headers (not including resent's and traces)
+
+ // parsed header fields
+ public final Date date;
+ public final Address to;
+ public final Address from; // if multiple From entries, this is sender
+ public final Address replyto; // if none provided, this is equal to sender
+ public final String subject;
+ public final String messageid;
+ public final Address[] cc;
+ public final Address[] bcc;
+ public final Hashtable[] resent;
+ public final Trace[] traces;
+
+ // envelope fields
+ public final Address envelopeFrom;
+ public final Address[] envelopeTo;
+
+ public static class StoredMessage extends Message {
+ public final int uid;
+ public boolean deleted = false;
+ public boolean read = false;
+ public boolean answered = false;
+ }
+
+ public static class Address {
+ public final String user;
+ public final String host;
+ public final String description;
+ public Address(String user, String host, String description) {
+ this.user = user; this.host = host; this.description = description;
+ }
+ public Address(String s) {
+ s = s.trim();
+ String descrip = null;
+ if (s.indexOf('<') != -1) {
+ if (s.indexOf('>') == -1) { /* FIXME */ }
+ descrip = s.substring(0, s.indexOf('<')) + s.substring(s.indexOf('>') + 1);
+ s = s.substring(s.indexOf('<') + 1, s.indexOf('>'));
+ }
+ if (s.indexOf('@') == -1) { /* FIXME */ }
+ description = descrip;
+ user = s.substring(0, s.indexOf('@'));
+ host = s.substring(s.indexOf('@')+1);
+ }
+ }
+
+ public class Trace {
+ final String returnPath = null;
+ final Element[] elements;
+ public class Element {
+ final String fromDomain;
+ final String fromIP;
+ final String toDomain;
+ final String forWhom;
+ final Date date;
+ }
+ }
+
+ public static class Base36 {
+ public static String encode(long l) {
+ StringBuffer ret = new StringBuffer();
+ while (l > 0) {
+ if ((l % 36) < 10) ret.append((char)(((int)'0') + (int)(l % 36)));
+ else ret.append((char)(((int)'A') + (int)((l % 36) - 10)));
+ l /= 36;
+ }
+ }
+ }
+
+ public Message(ReadStream rs) {
+ String key = null;
+ StringBuffer all = new StringBuffer();
+ for(String s = rs.readLine(); s != null && !s.equals(""); s = rs.readLine()) {
+ all.append(s);
+ all.append("\r\n");
+ if (Character.isSpace(s.charAt(0))) {
+ if (lastKey == null) { /* FIXME */ }
+ headers.put(lastKey, headers.get(lastKey) + s);
+ continue;
+ }
+ if (s.indexOf(':') == -1) { /* FIXME */ }
+
+ key = s.substring(0, s.indexOf(':'));
+ String val = s.substring(0, s.indexOf(':') + 1);
+ while(Character.isSpace(val.charAt(0))) val = val.substring(1);
+
+ if (headers.get(key) != null)
+ if (key.startsWith("Resent-")) {
+ // FIXME: multi-resent headers
+ } else if (key.startsWith("Return-Path:")) {
+ // FIXME: parse traces, see RFC2821, section 4.4
+ } else if (key.startsWith("Recieved:")) {
+ // FIXME: parse traces, see RFC2821, section 4.4
+ } else {
+ // just append it to the previous one; valid for Comments/Keywords
+ val = headers.get(key) + " " + val;
+ }
+
+ headers.put(key, val);
+ }
+ pristeneHeaders = all.toString();
+ StringBuffer body = new StringBuffer();
+ for(String s = rs.readLine(); s != null && !s.equals(""); s = rs.readLine()) body.append(s);
+ this.body = body.toString();
+ }
+
+ // http://www.jwz.org/doc/mid.html
+ public static String generateFreshMessageId() {
+ StringBuffer ret = new StringBuffer();
+ ret.append('<');
+ ret.append(Base36.encode(System.currentTimeMillis()));
+ ret.append('.');
+ ret.append(Base36.encode(random.nextLong()));
+ ret.append('.');
+ try { ret.append(InetAddress.getLocalHost().getHostName()); } catch (UnknownHostException e) { /* DELIBERATE */ }
+ ret.append('>');
+ return ret.toString();
+ }
+
+ private static final Random = new Random();
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** anonymizes mail messages */
+public class Anonymizer extends Filter {
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** implements the Distributed Checksum Clearinghouse spam filtering protocol */
+public class DCC extends SpamFilter {
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** converts text/html parts into nice ASCII text for mutt/gnus/pine/etc */
+public class HTML2Text extends Filter {
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** checks PGP signatures on incoming mail */
+public class PGP extends Filter {
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** sticks a 1x1 gif in outbound messages so we can tell when the message is read */
+public class ReturnReciept extends Filter {
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** a flexible, regexp-based email address rewriting engine */
+public class RewriteFilter extends Filter {
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** tags outgoing messages with a single-use email address */
+public class SingleUse extends Filter {
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** invokes SpamAssassin */
+public class SpamAssassin extends SpamFilter {
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** base class for spam filters */
+public class SpamFilter extends Filter {
+}
--- /dev/null
+package org.ibex.mail.filter;
+/** invokes Vipul's Razor */
+public class VipulsRazor extends SpamFilter {
+}
--- /dev/null
+package org.ibex.mail.protocol;
+/** Fax send/recieve/gateway */
+public class Fax extends MessageProtocol {
+}
--- /dev/null
+package org.ibex.mail.protocol;
+import java.net.*;
+import java.io.*;
+
+class IMAPException extends IOException {
+}
+
+interface IMAP {
+}
+
+
+public class IMAP extends MessageProtocol {
+
+ public static void main(String[] args) throws Exception {
+ ServerSocket ss = new ServerSocket(143);
+ while(true) {
+ System.out.println("listening");
+ final Socket s = ss.accept();
+ System.out.println("connected");
+ new Thread() {
+ public void run() {
+ try {
+ service(s);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+ }
+
+ public static void service(Socket s) throws IOException {
+ }
+}
--- /dev/null
+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 {
+}
--- /dev/null
+package org.ibex.mail.protocol;
+/** NNTP send/recieve */
+public class NNTP extends MessageProtocol {
+}
--- /dev/null
+package org.ibex.mail.protocol;
+import java.net.*;
+import java.io.*;
+
+// Next step: implement both sides using the POP interface
+
+class POPException extends IOException {
+}
+
+interface POP {
+ public abstract void userpass(String user, String pass) throws POPException;
+ public abstract void apop(String user, String digest) throws POPException;
+ public abstract BufferedReader top(int m, int maxlines);
+ public abstract BufferedReader retr(int m);
+ public abstract long stat(); // top 32 bits is number of messages, bottom 32 is total size
+ public abstract long[] list(); // top 32 bits is message number, bottom 32 is size
+ public abstract long list(int m);
+ public abstract void dele(int m);
+ public abstract void noop(int m);
+ public abstract void rset(int m);
+ public abstract String uidl(int m);
+ public abstract String[] uidl(); // FIXME, also needs message number
+}
+
+public class POP3 extends MessageProtocol {
+
+ public static void main(String[] args) throws Exception {
+ ServerSocket ss = new ServerSocket(110);
+ while(true) {
+ System.out.println("listening");
+ final Socket s = ss.accept();
+ System.out.println("connected");
+ new Thread() {
+ public void run() {
+ try {
+ service(s);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }.start();
+ }
+ }
+
+ public static void service(Socket s) throws IOException {
+ BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
+ PrintWriter pw = new PrintWriter(new OutputStreamWriter(s.getOutputStream()));
+ pw.print("+OK POP3 server ready\r\n");
+ pw.flush();
+ String user = null;
+ String pass = null;
+ while(true) {
+ String command = br.readLine().trim();
+ System.out.println("command: " + command);
+ if (command.toUpperCase().startsWith("QUIT ")) {
+ s.close();
+ return;
+ } else if (command.toUpperCase().startsWith("USER ")) {
+ user = command.substring(5).trim();
+ pw.print("+OK now give me your password\r\n");
+ pw.flush();
+ } else if (command.toUpperCase().startsWith("PASS ")) {
+ if (user == null) {
+ pw.print("-ERR I need your password first\r\n");
+ pw.flush();
+ } else {
+ pass = command.substring(5).trim();
+ break;
+ }
+ }
+ }
+ System.out.println("login from " + user + " / " + pass);
+ String server = user.substring(user.indexOf('@') + 1);
+ user = user.substring(0, user.indexOf('@'));
+ Socket pop = new Socket(InetAddress.getByName(server), 117);
+
+ BufferedReader br2 = new BufferedReader(new InputStreamReader(pop.getInputStream()));
+ PrintWriter pw2 = new PrintWriter(new OutputStreamWriter(pop.getOutputStream()));
+
+ System.out.println("pop said " + br2.readLine());
+ pw2.print("USER " + user + "\r\n");
+ pw2.flush();
+ System.out.println("pop said " + br2.readLine());
+ pw2.print("PASS " + pass + "\r\n");
+ pw2.flush();
+ System.out.println("pop said " + br2.readLine());
+
+ s.close();
+ }
+}
--- /dev/null
+package org.ibex.mail.protocol;
+/** RSS send/recieve/gateway */
+public class RSS extends MessageProtocol {
+}
--- /dev/null
+package org.ibex.mail.protocol;
+/** SMS send/recieve/gateway */
+public class SMS extends MessageProtocol {
+}
--- /dev/null
+package org.ibex.mail.protocol;
+public class SMTP extends MessageProtocol {
+
+ public SMTP() { setProtocolName("SMTP"); }
+ public ServerRequest createRequest(Connection conn) { return new Request((TcpConnection)conn); }
+
+ public static class Outgoing {
+ // recommended retry interval is 30 minutes
+ // give up after 4-5 days
+ // should keep per-host success/failure so we don't retry on every message
+ // exponential backoff on retry time?
+ // check DNS resolvability as soon as domain is provided
+ // only use implicit A-record if there are no MX-records
+ // use null-sender for error messages (don't send errors to the null addr)
+ // to prevent mail loops, drop messages with >100 Recieved headers
+ private final Queue queue = new Queue();
+ public static void send(Message m) { }
+ public static void enqueue(Message m) { }
+ public static void bounce(Message m, String reason) { }
+ private void runq() {
+ MessageStore store = MessageStore.root.slash("smtp").slash("outgoing");
+ int[] outgoing = store.list();
+ for(int i=0; i<outgoing.length; i++) queue.append(store.get(outgoing[i]));
+ while(true) {
+ Message next = queue.dequeue(true);
+ // FIXME
+ }
+ }
+ }
+
+ private class Incoming implements ServerRequest {
+ TcpConnection conn;
+ public Request(TcpConnection conn) { this.conn = conn; conn.getSocket().setSoTimeout(5 * 60 * 1000); }
+ public void init() { }
+
+ public boolean handleRequest() throws IOException {
+ ReadStream rs = conn.getReadStream();
+ WriteStream ws = conn.getWriteStream();
+ ws.setNewLineString("\r\n");
+
+ ws.println("220 " + conn.getVirtualHost() + " ESMTP " + this.getClass().getName());
+
+ Address from = null;
+ Vector to = new Vector();
+ // 551 = no, i won't forward that
+ // 452 = mailbox full
+ // see 4.4 for trace info
+ while(true) {
+ String command = rs.readLine();
+ // FIXME: validate the HELO domain argument
+ // (double check against other end of connection? must not reject though)
+ if (hello.toUpperCase().startsWith("HELO")) {
+ ws.println("250 HELO " + conn.getVirtualHost());
+ from = null;
+ to = new Vector();
+
+ } else if (hello.toUpperCase().startsWith("EHLO")) {
+ ws.pritnln("250-" + conn.getVirtualHost());
+ ws.println("250-SIZE");
+ ws.println("250 PIPELINING");
+ from = null;
+ to = new Vector();
+
+ } else if (command.toUpperCase().startsWith("RSET")) {
+ from = null;
+ to = new Vector();
+ ws.println("250 reset ok");
+
+ } else if (command.toUpperCase().startsWith("MAIL FROM:")) {
+ command = command.substring(10).trim();
+ if(command.indexOf(' ') != -1) command = command.substring(0, command.indexOf(' '));
+ from = RFC2822.Address.parse(command);
+
+ } else if (command.toUpperCase().startsWith("RCPT TO:")) {
+ if (from == null) {
+ ws.println("503 MAIL FROM must precede RCPT TO");
+ continue;
+ }
+ command = command.substring(10).trim();
+ if(command.indexOf(' ') != -1) command = command.substring(0, command.indexOf(' '));
+ to.addElement(RFC2822.Address.parse(command));
+
+ } else if (command.toUpperCase().startsWith("DATA")) {
+ if (from == null) { ws.println("503 MAIL FROM command must precede DATA"); continue; }
+ if (to.size() == null) { ws.println("503 RCPT TO command must precede DATA"); continue; }
+ ws.println("354 Enter message, ending with \".\" on a line by itself");
+ StringBuffer data = new StringBuffer();
+ // move this into the RFC2822 class
+ while(true) {
+ String line = rs.readLine();
+ if (line.equals(".")) break;
+ if (line.startsWith("..")) line = line.substring(1);
+ data.append(line);
+ }
+ // FIXME: commit message to disk here
+ ws.println("250 OK message accepted for delivery");
+ //ws.println("251 user not local; will forward");
+
+ } else if (command.toUpperCase().startsWith("HELP")) {
+ ws.println("214 sorry, you are beyond help. please see a trained professional.");
+
+ } else if (command.toUpperCase().startsWith("VRFY")) { // FIXME, see code 252
+ } else if (command.toUpperCase().startsWith("EXPN")) { ws.println("550 EXPN not available");
+ } else if (command.toUpperCase().startsWith("NOOP")) { ws.println("250 OK");
+ } else if (command.toUpperCase().startsWith("QUIT")) {
+ ws.println("221 " + conn.getVirtualHost() + " closing connection");
+ break;
+
+ } else {
+ ws.println("500 unrecognized command");
+ }
+
+
+ return false; // FIXME: what does this mean?
+ }
+ }
+}
--- /dev/null
+package org.ibex.mail.protocol;
+/** a nice WebMail GUI */
+public class WebMail extends MessageProtocol {
+}
--- /dev/null
+package org.ibex.mail.store;
+import org.ibex.util.*;
+import java.io.*;
+import java.net.*;
+
+// FIXME: appallingly inefficient
+public class MessageStore {
+
+ private final String STORAGE_ROOT = System.getProperty("org.ibex.mail.MessageStore.ROOT", "/var/org.ibex.mail/");
+ public final MessageStore root = new MessageStore(STORAGE_ROOT);
+
+ private String path;
+ private MessageStore(String path) throws IOException { new File(this.path = path).mkdirs(); }
+ public MessageStore slash(String name) { return new MessageStore(path + "/" + name); }
+
+ public int[] list() {
+ String[] names = new File(path).list();
+ int[] ret = new int[names.length];
+ for(int i=0, j=0; j<ret.length; i++, j++) {
+ try {
+ ret[j] = Integer.parseInt(names[i].substring(0, names[i].length - 1));
+ } catch (NumberFormatException nfe) {
+ Log.warn(MessageStore.class, "NumberFormatException: " + names[i].substring(0, names[i].length - 1));
+ j--;
+ int[] newret = new int[ret.length - 1];
+ System.arrayCopy(ret, 0, newret, 0, newret.length);
+ ret = newret;
+ }
+ }
+ return ret;
+ }
+
+ /** returns a message identifier */
+ public synchronized int add(StoredMessage message) throws IOException {
+ int[] all = list();
+ int max = 0;
+ for(int i=0; i<all.length; i++) max = Math.max(max, all[i]);
+ int target = max++;
+ File f = new File(path + File.separatorChar + max + ".-");
+ FileOutputStream fo = new FileOutputStream(f);
+ message.dump(fo);
+ fo.close();
+ f.renameTo(path + File.separatorChar + max + ".");
+ return target;
+ }
+
+ public StoredMessage get(int messagenum) throws IOException {
+ File f = new File(path + File.separatorChar + messagenum + ".");
+ if (!f.exists()) throw new FileNotFoundException(f);
+ return StoredMessage.undump(new FileInputStream(f));
+ }
+
+ // query types: stringmatch (headers, body), header element, deletion status, date range, message size
+ public StoredMessage[] query(int maxResults) {
+ throw new RuntimeException("MessageStore.query() not implemented yet");
+ }
+
+}
--- /dev/null
+package org.ibex.mail.target;
+/** bounces a message */
+public class Bounce extends Target {
+}
--- /dev/null
+package org.ibex.mail.target;
+/** simple class to handle darcs patches */
+public class Darcs extends Pipe {
+}
--- /dev/null
+package org.ibex.mail.target;
+/** implementation of the Local Mail Transport Protocol */
+public class LMTP extends Target {
+}
--- /dev/null
+package org.ibex.mail.target;
+/** A full-featured mailing list manager */
+public class List extends Target {
+}
--- /dev/null
+package org.ibex.mail.target;
+/** generic pipe-the-message-to-a-file target */
+public class Pipe extends Target {
+}
--- /dev/null
+package org.ibex.mail.target;
+/** callout to support legacy .procmailrc files */
+public class Procmail extends Pipe {
+}
--- /dev/null
+package org.ibex.mail.target;
+/** base class for mail message "destinations" */
+public class Target {
+}
--- /dev/null
+package org.ibex.mail.target;
+/** functionality similar to the UNIX vacation command */
+public class Vacation extends Target {
+}
--- /dev/null
+package org.ibex.mail.target;
+/** an XML-RPC <i>server</i> which accepts requests sent via SMTP */
+public class XMLRPC extends Target {
+}