Author: dbkr
Date: 2006-05-04 23:27:32 +0000 (Thu, 04 May 2006)
New Revision: 8604

Added:
   trunk/apps/fnmail/
   trunk/apps/fnmail/README
   trunk/apps/fnmail/build.xml
   trunk/apps/fnmail/src/
   trunk/apps/fnmail/src/fnmail/
   trunk/apps/fnmail/src/fnmail/AccountManager.java
   trunk/apps/fnmail/src/fnmail/Contact.java
   trunk/apps/fnmail/src/fnmail/FNMail.java
   trunk/apps/fnmail/src/fnmail/MailFetcher.java
   trunk/apps/fnmail/src/fnmail/MailMessage.java
   trunk/apps/fnmail/src/fnmail/MessageBank.java
   trunk/apps/fnmail/src/fnmail/MessageSender.java
   trunk/apps/fnmail/src/fnmail/SingleAccountWatcher.java
   trunk/apps/fnmail/src/fnmail/fcp/
   trunk/apps/fnmail/src/fnmail/fcp/FCPBadFileException.java
   trunk/apps/fnmail/src/fnmail/fcp/FCPClient.java
   trunk/apps/fnmail/src/fnmail/fcp/FCPConnection.java
   trunk/apps/fnmail/src/fnmail/fcp/FCPContext.java
   trunk/apps/fnmail/src/fnmail/fcp/FCPErrorMessage.java
   trunk/apps/fnmail/src/fnmail/fcp/FCPInsertErrorMessage.java
   trunk/apps/fnmail/src/fnmail/fcp/FCPMessage.java
   trunk/apps/fnmail/src/fnmail/fcp/HighLevelFCPClient.java
   trunk/apps/fnmail/src/fnmail/fcp/NoNodeConnectionException.java
   trunk/apps/fnmail/src/fnmail/imap/
   trunk/apps/fnmail/src/fnmail/imap/IMAPBadMessageException.java
   trunk/apps/fnmail/src/fnmail/imap/IMAPHandler.java
   trunk/apps/fnmail/src/fnmail/imap/IMAPListener.java
   trunk/apps/fnmail/src/fnmail/imap/IMAPMessage.java
   trunk/apps/fnmail/src/fnmail/imap/IMAPMessageFlags.java
   trunk/apps/fnmail/src/fnmail/smtp/
   trunk/apps/fnmail/src/fnmail/smtp/SMTPBadCommandException.java
   trunk/apps/fnmail/src/fnmail/smtp/SMTPCommand.java
   trunk/apps/fnmail/src/fnmail/smtp/SMTPHandler.java
   trunk/apps/fnmail/src/fnmail/smtp/SMTPListener.java
   trunk/apps/fnmail/src/fnmail/utils/
   trunk/apps/fnmail/src/fnmail/utils/EmailAddress.java
   trunk/apps/fnmail/src/freenet/
   trunk/apps/fnmail/src/freenet/support/
   trunk/apps/fnmail/src/freenet/support/io/
   trunk/apps/fnmail/src/freenet/support/io/LineReader.java
   trunk/apps/fnmail/src/freenet/support/io/LineReadingInputStream.java
   trunk/apps/fnmail/src/freenet/support/io/TooLongException.java
   trunk/apps/fnmail/src/thirdparty/
   trunk/apps/fnmail/src/thirdparty/Base64Coder.java
Log:
Initial insert of fnmail into SVN. fnmail is an application for email-like
communication over Freenet 0.7 through IMAP and SMTP.


Added: trunk/apps/fnmail/README
===================================================================
--- trunk/apps/fnmail/README    2006-05-04 12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/README    2006-05-04 23:27:32 UTC (rev 8604)
@@ -0,0 +1,52 @@
+This is the start of an email-over-Freenet 0.7 implementation. It's by no means
+finished yet, and hence is only provided as source through subversion.
+
+Currently only 'NIM Mode' is supported, 'NIM' coming from the concept of a
+'nearly instant message' which was simply messages posted to KSKs, usually
+used on freesites in older versions of Freenet. I'd also suggest 'Notably
+Insecure Messaging'. It's not secure. At all. Anyone can read your mail.
+You can use PGP or equivalent, but it's still easy to spam and hijack
+addresses and whatnot.
+
+Proper, secure implemenations of the Freemail protocol will come later.
+
+All the data, including your passwd file, will most likley end up world
+readable. Under unix, you could try running fnmail with a modified umask
+if this bothers you, but I don't believe there is a portable way of doing
+this in Java (or I haven't found it).
+
+Finally, there *will* be backwards incompatable changes, so don't get
+attatched to anything just yet.
+
+Now you've read that (you have read that, right?):
+
+compile: (however you compile Java, an ant buildfile is supplied)
+run with --newaccount <account name> to create an account, eg:
+
+java -cp build/ fnmail.FNMail --newaccount fred
+
+Use --passwd <account> <passwd> to set your password
+
+java -cp build/ fnmail.FNMail --passwd fred fredspassword
+
+Run:
+
+java -cp build/ fnmail.FNMail
+
+Set up your email client to point at IMAP port 3143 and SMTP port 3025.
+
+Your address is <accountname>@nim.fnmail
+
+And yes, in case you were wondering, no - there's nothing to stop someone
+else using the same address. I did say it was insecure ;)
+
+Send me a message if you like, I promise to reply if it works :)
+
+dbkr at nim.fnmail
+
+(and since anyone can read fnmail message right now, my fnmail public key
+can be found at USK at 
vjETpEgDH-6EzlngZoO8KgOZm-B8AAlvZ-6oP6aQmow,DZYYfhpOxIrtdCNJiflIPjd0Qy8nA1d3Dwy86dcdhu0,AQABAAE/dbkr/10/contact/pubkey.fnmail.asc,
 or failing that, http://accidentalegg.co.uk/contact/pubkey.fnmail)
+
+If it doesn't, dbkr at freenetproject.org!
+
+Good luck!

Added: trunk/apps/fnmail/build.xml
===================================================================
--- trunk/apps/fnmail/build.xml 2006-05-04 12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/build.xml 2006-05-04 23:27:32 UTC (rev 8604)
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<project name="fnmail" default="compile" basedir=".">
+<!-- set global properties for this build -->
+       <property name="src" location="src"/>
+       <property name="build" location="build"/>
+       <property name="lib"    location="lib"/>
+
+       <target name="mkdir">
+               <mkdir dir="${build}"/>
+               <mkdir dir="${lib}"/>
+       </target>
+
+       <target name="compile" depends="mkdir">
+               <!-- Create the time stamp -->
+               <tstamp/>
+               <!-- Create the build directory structure used by compile -->
+               
+               <javac srcdir="${src}" destdir="${build}" debug="on" 
optimize="on" source="1.4">
+               <include name="fnmail/*.java"/>
+               <include name="fnmail/*/*.java"/>
+               <include name="thirdparty/*.java"/>
+               <include name="freenet/support/io/*.java"/>
+               </javac>
+       </target>
+
+
+       <target name="dist" depends="compile">
+       <jar jarfile="${lib}/fnmail.jar" basedir="${build}">
+       <manifest>
+       <attribute name="Main-Class" value="fnmail.FNMail"/>
+               <attribute name="Built-By" value="${user.name}"/>
+               <section name="common">
+                       <attribute name="Implementation-Title" value="fnmail"/>
+                       <attribute name="Implementation-Version" value="0.0"/> 
+                       <attribute name="Implementation-Vendor" value="Dave 
Baker"/>
+               </section>
+       </manifest>
+       </jar>    
+       </target>
+
+       <target name="clean">
+               <delete dir="${build}"/>
+               <delete dir="${lib}"/>
+       </target>
+       <target name="distclean" description="Delete class files, lib dir and 
docs dir.">
+               <delete dir="${build}"/>
+               <delete dir="${lib}"/>
+       </target>
+</project>

Added: trunk/apps/fnmail/src/fnmail/AccountManager.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/AccountManager.java    2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/AccountManager.java    2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,127 @@
+package fnmail;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class AccountManager {
+       public static String DATADIR = "data";
+       // this really doesn't matter a great deal
+       public static String NIMDIR = "nim";
+       public static String PASSWDFILE = "passwd";
+
+       public static void Create(String username) throws IOException {
+               File datadir = new File(DATADIR);
+               if (!datadir.exists()) {
+                       if (!datadir.mkdir()) throw new IOException("Failed to 
create data directory");
+               }
+               
+               File accountdir = new File(DATADIR, username);
+               if (!accountdir.mkdir()) throw new IOException("Failed to 
create directory "+username+" in "+DATADIR);
+       }
+       
+       public static void setupNIM(String username) throws IOException {
+               File accountdir = new File(DATADIR, username);
+               
+               File contacts_dir = new File(accountdir, 
SingleAccountWatcher.CONTACTS_DIR);
+               if (!contacts_dir.exists()) {
+                       if (!contacts_dir.mkdir()) throw new 
IOException("Failed to create contacts directory");
+               }
+               
+               File nimdir = new File(contacts_dir, NIMDIR);
+               if (!nimdir.exists()) {
+                       if (!nimdir.mkdir()) throw new IOException("Failed to 
create nim directory");
+               }
+               
+               File keyfile = new File(nimdir, Contact.KEYFILE);
+               PrintWriter pw = new PrintWriter(new FileOutputStream(keyfile));
+               
+               pw.println(MessageSender.NIM_KEY_PREFIX + username + "-");
+               
+               pw.close();
+       }
+       
+       public static void ChangePassword(String username, String newpassword) 
throws Exception {
+               MessageDigest md = null;
+               try {
+                       md = MessageDigest.getInstance("MD5");
+               } catch (NoSuchAlgorithmException alge) {
+                       throw new Exception("No MD5 implementation available - 
sorry, fnmail cannot work!");
+               }
+               
+               File accountdir = new File(DATADIR, username);
+               if (!accountdir.exists()) {
+                       throw new Exception("No such account - "+username+".");
+               }
+               
+               File passwdfile = new File(accountdir, PASSWDFILE);
+               FileOutputStream fos = new FileOutputStream(passwdfile);
+               
+               byte[] md5passwd = md.digest(newpassword.getBytes());
+               String strmd5 = bytestoHex(md5passwd);
+               
+               fos.write(strmd5.getBytes());
+               fos.close();
+       }
+       
+       public static boolean authenticate(String username, String password) {
+               if (!validate_username(username)) return false;
+               
+               String sep = System.getProperty("file.separator");
+               
+               FileInputStream fin = null;
+               try {
+                       fin = new 
FileInputStream(DATADIR+sep+username+sep+PASSWDFILE);
+               } catch (FileNotFoundException fnfe) {
+                       return false;
+               }
+               
+               byte[] realmd5 = new byte[32];
+               try {
+                       fin.read(realmd5);
+               } catch (IOException ioe) {
+                       return false;
+               }
+               
+               String realmd5str = new String(realmd5);
+               
+               MessageDigest md = null;
+               try {
+                       md = MessageDigest.getInstance("MD5");
+               } catch (NoSuchAlgorithmException alge) {
+                       System.out.println("No MD5 implementation available - 
logins will not work!");
+                       return false;
+               }
+               byte[] givenmd5 = md.digest(password.getBytes());
+               
+               String givenmd5str = bytestoHex(givenmd5);
+               
+               if (realmd5str.equals(givenmd5str)) {
+                       return true;
+               }
+               return false;
+       }
+       
+       private static boolean validate_username(String username) {
+               if (username.matches("[\\w_]*")) return true;
+               return false;
+       }
+       
+       public static String bytestoHex(byte[] bytes) {
+               String retval = new String("");
+               
+               for (int i = 0; i < bytes.length; i++) {
+                       String b = Integer.toHexString((int)(bytes[i] & 0xFF));
+                       if (b.length() < 2) {
+                               b = "0" + b;
+                       }
+                       retval += b;
+               }
+               return retval;
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/Contact.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/Contact.java   2006-05-04 12:51:55 UTC (rev 
8603)
+++ trunk/apps/fnmail/src/fnmail/Contact.java   2006-05-04 23:27:32 UTC (rev 
8604)
@@ -0,0 +1,101 @@
+package fnmail;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileOutputStream;
+import java.io.BufferedReader;
+import java.io.PrintWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Iterator;
+
+public class Contact {
+       public static final String KEYFILE = "key";
+       private static final String LOGFILE = "log";
+
+       private final File keyfile;
+       private final File logfile;
+       private HashMap messages;
+       private int lastMessageId;
+
+       Contact(File dir) {
+               this.keyfile = new File(dir, KEYFILE);
+               this.logfile = new File(dir, LOGFILE);
+               
+               this.lastMessageId = 0;
+               
+               this.messages = new HashMap();
+               
+               FileReader frdr;
+               try {
+                       frdr = new FileReader(this.logfile);
+               
+               
+                       BufferedReader br = new BufferedReader(frdr);
+                       String line;
+                       
+                       while ( (line = br.readLine()) != null) {
+                               String[] parts = line.split("=");
+                               
+                               if (parts.length != 2) continue;
+                               
+                               int thisnum = Integer.parseInt(parts[0]);
+                               if (thisnum > this.lastMessageId)
+                                       this.lastMessageId = thisnum;
+                               this.messages.put(new Integer(thisnum), 
parts[1]);
+                       }
+                       
+                       frdr.close();
+               } catch (IOException ioe) {
+                       return;
+               }
+       }
+       
+       public String getKey() throws IOException {
+               FileReader frdr = new FileReader(this.keyfile);
+               BufferedReader br = new BufferedReader(frdr);
+               String key =  br.readLine();
+               frdr.close();
+               return key;
+       }
+       
+       public int getNextMessageId() {
+               return this.lastMessageId + 1;
+       }
+       
+       public void addMessage(int num, String checksum) {
+               this.messages.put(new Integer(num), checksum);
+               if (num > this.lastMessageId)
+                       this.lastMessageId = num;
+               this.writeLogFile();
+       }
+       
+       private void writeLogFile() {
+               FileOutputStream fos;
+               try {
+                       fos = new FileOutputStream(this.logfile);
+               } catch (IOException ioe) {
+                       return;
+               }
+               
+               PrintWriter pw = new PrintWriter(fos);
+               
+               Iterator i = this.messages.entrySet().iterator();
+               while (i.hasNext()) {
+                       Map.Entry e = (Map.Entry)i.next();
+                       
+                       Integer num = (Integer)e.getKey();
+                       String checksum = (String)e.getValue();
+                       pw.println(num.toString()+"="+checksum);
+               }
+               
+               pw.flush();
+               
+               try {
+                       fos.close();
+               } catch (IOException ioe) {
+                       return;
+               }
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/FNMail.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/FNMail.java    2006-05-04 12:51:55 UTC (rev 
8603)
+++ trunk/apps/fnmail/src/fnmail/FNMail.java    2006-05-04 23:27:32 UTC (rev 
8604)
@@ -0,0 +1,107 @@
+package fnmail;
+
+import java.io.File;
+import java.io.IOException;
+
+import fnmail.fcp.FCPContext;
+import fnmail.fcp.FCPConnection;
+import fnmail.imap.IMAPListener;
+import fnmail.smtp.SMTPListener;
+
+class FNMail {
+       public static void main(String[] args) {
+               String fcphost = "localhost";
+               int fcpport = 9481;
+               
+               for (int i = 0; i < args.length; i++) {
+                       if (args[i].equals("--newaccount")) {
+                               i++;
+                               if (args.length - 1 < i) {
+                                       System.out.println("Usage: --newaccount 
<account name>");
+                                       return;
+                               }
+                               try {
+                                       AccountManager.Create(args[i]);
+                                       // for now
+                                       AccountManager.setupNIM(args[i]);
+                                       System.out.println("Account created for 
"+args[i]+". You may now set a password with --passwd <password>");
+                                       System.out.println("For the time being, 
you address is "+args[i]+"@nim.fnmail");
+                               } catch (IOException ioe) {
+                                       System.out.println("Couldn't create 
account. Please check write access to fnmail's working directory. Error: 
"+ioe.getMessage());
+                               }
+                               return;
+                       } else if (args[i].equals("--passwd")) {
+                               i = i + 2;
+                               if (args.length - 1 < i) {
+                                       System.out.println("Usage: --passwd 
<account name> <password>");
+                                       return;
+                               }
+                               try {
+                                       AccountManager.ChangePassword(args[i - 
1], args[i]);
+                                       System.out.println("Password changed.");
+                               } catch (Exception e) {
+                                       System.out.println("Couldn't change 
password for "+args[i - 1]+". "+e.getMessage());
+                               }
+                               return;
+                       } else if (args[i].equals("-h")) {
+                               i++;
+                               if (args.length - 1 < i) {
+                                       System.out.println("No hostname 
supplied, using default");
+                                       continue;
+                               }
+                               fcphost = args[i];
+                       } else if (args[i].equals("-p")) {
+                               i++;
+                               if (args.length - 1 < i) {
+                                       System.out.println("No port supplied, 
using default");
+                                       continue;
+                               }
+                               try {
+                                       fcpport = Integer.parseInt(args[i]);
+                               } catch (NumberFormatException nfe) {
+                                       System.out.println("Bad port supplied, 
using default");
+                               }
+                       }
+               }
+               
+               FCPContext fcpctx = new FCPContext(fcphost, fcpport);
+               
+               FCPConnection fcpconn = new FCPConnection(fcpctx);
+               Thread fcpthread  = new Thread(fcpconn);
+               fcpthread.setDaemon(true);
+               fcpthread.start();
+               
+               // start a SingleAccountWatcher for each account
+               File dir = new File("data");
+               if (!dir.exists()) {
+                       System.out.println("Starting fnmail for the first 
time.");
+                       System.out.println("You will probably want to add an 
account by running fnmail with arguments --newaccount <username>");
+                       dir.mkdir();
+               }
+               File[] files = dir.listFiles();
+               for (int i = 0; i < files.length; i++) {
+                       if (files[i].getName().equals(".") || 
files[i].getName().equals(".."))
+                               continue;
+                       
+                       Thread t = new Thread(new SingleAccountWatcher(fcpconn, 
files[i]));
+                       t.setDaemon(true);
+                       t.start();
+               }
+               
+               // and a sender thread
+               MessageSender sender = new MessageSender(dir, fcpconn);
+               Thread senderthread = new Thread(sender);
+               senderthread.setDaemon(true);
+               senderthread.start();
+               
+               // start the SMTP Listener
+               SMTPListener smtpl = new SMTPListener(sender);
+               Thread smtpthread = new Thread(smtpl);
+               smtpthread.setDaemon(true);
+               smtpthread.start();
+               
+               // start the IMAP listener
+               IMAPListener imapl = new IMAPListener();
+               imapl.run();
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/MailFetcher.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/MailFetcher.java       2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/MailFetcher.java       2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,103 @@
+package fnmail;
+
+import fnmail.fcp.FCPConnection;
+import fnmail.fcp.HighLevelFCPClient;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.BufferedReader;
+import java.io.PrintStream;
+import java.io.FileReader;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class MailFetcher {
+       private final MessageBank mb;
+       private File contact_dir;
+       private final FCPConnection fcpconn;
+       private final SimpleDateFormat sdf;
+
+       MailFetcher(MessageBank m, File ctdir, FCPConnection fcpc) {
+               this.mb = m;
+               this.fcpconn = fcpc;
+               this.sdf = new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z");
+               this.contact_dir = ctdir;
+       }
+       
+       public void fetch_from_all() {
+               File[] contactfiles = contact_dir.listFiles();
+               
+               for (int i = 0; i < contactfiles.length; i++) {
+                       Contact contact = new Contact(contactfiles[i]);
+                       this.fetch_from(contact);
+               }
+       }
+       
+       private void fetch_from(Contact contact) {
+               HighLevelFCPClient fcpcli;
+               fcpcli = new HighLevelFCPClient(this.fcpconn);
+               
+               
+               String keybase;
+               try {
+                       keybase = contact.getKey();
+               } catch (IOException ioe) {
+                       // Jinkies, Scoob! No key!
+                       return;
+               }
+               
+               int startnum = contact.getNextMessageId();
+               
+               for (int i = startnum; i < startnum + 3; i++) {
+                       System.out.println("trying to fetch "+keybase+i);
+                       
+                       
+                       File result = fcpcli.fetch(keybase+i);
+                       
+                       if (result != null) {
+                               try {
+                                       String checksum = 
this.storeMessage(result);
+                                       contact.addMessage(i, checksum);
+                               } catch (IOException ioe) {
+                                       continue;
+                               }
+                       }
+               }
+       }
+       
+       private String storeMessage(File file) throws IOException {
+               MailMessage newmsg = this.mb.createMessage();
+               
+               MessageDigest md;
+               try {
+                       md = MessageDigest.getInstance("MD5");
+               } catch (NoSuchAlgorithmException alge) {
+                       System.out.println("No MD5 implementation available - 
can't checksum messages - not storing message.");
+                       return null;
+               }
+               
+               // add our own headers first
+               // recieved and date
+               newmsg.addHeader("Received", "(fnmail); "+this.sdf.format(new 
Date()));
+               
+               BufferedReader rdr = new BufferedReader(new FileReader(file));
+               
+               newmsg.readHeaders(rdr);
+               
+               PrintStream ps = newmsg.writeHeadersAndGetStream();
+               
+               String line;
+               while ( (line = rdr.readLine()) != null) {
+                       ps.println(line);
+               }
+               
+               newmsg.commit();
+               rdr.close();
+               file.delete();
+               
+               byte[] checksum = md.digest();
+               return AccountManager.bytestoHex(checksum);
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/MailMessage.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/MailMessage.java       2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/MailMessage.java       2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,209 @@
+package fnmail;
+
+import java.io.OutputStream;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.BufferedReader;
+import java.io.PrintStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Vector;
+import java.util.Enumeration;
+
+import fnmail.imap.IMAPMessageFlags;
+
+public class MailMessage {
+       private File file;
+       private OutputStream os;
+       private PrintStream ps;
+       private final Vector headers;
+       private BufferedReader brdr;
+       public final IMAPMessageFlags flags;
+       
+       MailMessage(File f) {
+               this.file = f;
+               this.headers = new Vector();
+               
+               // initalise flags from filename
+               String[] parts = f.getName().split(",");
+               if (parts.length < 2 && !f.getName().endsWith(",")) {
+                       // treat it as a new message
+                       this.flags = new IMAPMessageFlags();
+                       this.flags.set("\\Recent", true);
+               } else if (parts.length < 2) {
+                       // just doesn't have any flags set
+                       this.flags = new IMAPMessageFlags();
+               } else {
+                       this.flags = new IMAPMessageFlags(parts[1]);
+               }
+               this.brdr = null;
+       }
+       
+       public void addHeader(String name, String val) {
+               this.headers.add(new MailMessageHeader(name, val));
+       }
+       
+       // get the first header of a given name
+       public String getFirstHeader(String name) {
+               Enumeration e = this.headers.elements();
+               
+               while (e.hasMoreElements()) {
+                       MailMessageHeader h = (MailMessageHeader) 
e.nextElement();
+                       
+                       if (h.name.equalsIgnoreCase(name)) {
+                               return h.val;
+                       }
+               }
+               
+               return null;
+       }
+       
+       public String getHeaders(String name) {
+               StringBuffer buf = new StringBuffer("");
+               
+               Enumeration e = this.headers.elements();
+               
+               while (e.hasMoreElements()) {
+                       MailMessageHeader h = (MailMessageHeader) 
e.nextElement();
+                       
+                       if (h.name.equalsIgnoreCase(name)) {
+                               buf.append(h.name);
+                               buf.append(": ");
+                               buf.append(h.val);
+                               buf.append("\r\n");
+                       }
+               }
+               
+               return buf.toString();
+       }
+       
+       public PrintStream writeHeadersAndGetStream() throws 
FileNotFoundException {
+               this.os = new FileOutputStream(this.file);
+               this.ps = new PrintStream(this.os);
+               
+               Enumeration e = this.headers.elements();
+               
+               while (e.hasMoreElements()) {
+                       MailMessageHeader h = (MailMessageHeader) 
e.nextElement();
+                       
+                       this.ps.println(h.name + ": " + h.val);
+               }
+               
+               this.ps.println("");
+               
+               return this.ps;
+       }
+       
+       public void commit() {
+               try {
+                       this.os.close();
+                       // also potentally move from a temp dir to real inbox
+                       // to do safer inbox access
+               } catch (IOException ioe) {
+                       
+               }
+       }
+       
+       public void readHeaders() throws IOException {
+               BufferedReader bufrdr = new BufferedReader(new 
FileReader(this.file));
+               
+               this.readHeaders(bufrdr);
+               bufrdr.close();
+       }
+       
+       public void readHeaders(BufferedReader bufrdr) throws IOException {
+               String line;
+               String[] parts = null;
+               while ( (line = bufrdr.readLine()) != null) {
+                       if (line.length() == 0) {
+                               if (parts != null)
+                                       this.addHeader(parts[0], parts[1]);
+                               parts = null;
+                               break;
+                       } else if (line.startsWith(" ")) {
+                               // contination of previous line
+                               if (parts == null || parts[1] == null) 
+                                       continue;
+                               parts[1] += line.trim();
+                       } else {
+                               if (parts != null)
+                                       this.addHeader(parts[0], parts[1]);
+                               parts = null;
+                               parts = line.split(": ", 2);
+                               
+                               if (parts.length < 2)
+                                       parts = null;
+                       }
+               }
+               
+               if (parts != null) {
+                       this.addHeader(parts[0], parts[1]);
+               }
+       }
+       
+       public int getUID() {
+               String[] parts = this.file.getName().split(",");
+               
+               return Integer.parseInt(parts[0]);
+       }
+       
+       public long getSize() throws IOException {
+               // this is quite arduous since we have to send the message
+               // with \r\n's, and hence it may not be the size it is on disk
+               BufferedReader br = new BufferedReader(new 
FileReader(this.file));
+               
+               long counter = 0;
+               String line;
+               
+               while ( (line = br.readLine()) != null) {
+                       counter += line.length();
+                       counter += "\r\n".length();
+               }
+               
+               br.close();
+               return counter;
+       }
+       
+       public void closeStream() {
+               try {
+                       if (this.brdr != null) this.brdr.close();
+               } catch (IOException ioe) {
+                       
+               }
+               this.brdr = null;
+       }
+       
+       public String readLine() throws IOException {
+               if (this.brdr == null) {
+                       this.brdr = new BufferedReader(new 
FileReader(this.file));
+               }
+               
+               return this.brdr.readLine();
+       }
+       
+       // programming-by-contract - anything that tries to read the message
+       // or suchlike after calling this method is responsible for the
+       // torrent of exceptions they'll get thrown at them!
+       public void delete() {
+               this.file.delete();
+       }
+       
+       public void storeFlags() {
+               String[] parts = this.file.getName().split(",");
+               
+               String newname = parts[0] + "," + 
this.flags.getShortFlagString();
+               
+               this.file.renameTo(new File(this.file.getParentFile(), 
newname));
+       }
+       
+       private class MailMessageHeader {
+               public String name;
+               public String val;
+               
+               public MailMessageHeader(String n, String v) {
+                       this.name = n;
+                       this.val = v;
+               }
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/MessageBank.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/MessageBank.java       2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/MessageBank.java       2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,126 @@
+package fnmail;
+
+import java.io.IOException;
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.FileReader;
+import java.io.BufferedReader;
+import java.io.FileOutputStream;
+import java.io.PrintStream;
+import java.util.TreeMap;
+import java.util.SortedMap;
+
+public class MessageBank {
+       private static final String MESSAGES_DIR = "inbox";
+       private static final String NIDFILE = ".nextid";
+       private static final String NIDTMPFILE = ".nextid-tmp";
+
+       private final File dir;
+
+       public MessageBank(String username) {
+               this.dir = new File(AccountManager.DATADIR + File.separator + 
username + File.separator + MESSAGES_DIR);
+               
+               if (!this.dir.exists()) {
+                       this.dir.mkdir();
+               }
+       }
+       
+       public MailMessage createMessage() {
+               long newid = this.nextId();
+               File newfile;
+               try {
+                       do {
+                               newfile = new File(this.dir, 
Long.toString(newid));
+                               newid++;
+                       } while (!newfile.createNewFile());
+               } catch (IOException ioe) {
+                       newfile = null;
+               }
+               
+               this.writeNextId(newid);
+               
+               if (newfile != null) {
+                       MailMessage newmsg = new MailMessage(newfile);
+                       return newmsg;
+               }
+               
+               return null;
+       }
+       
+       public SortedMap listMessages() {
+               File[] files = this.dir.listFiles();
+               
+               TreeMap msgs = new TreeMap();
+               
+               for (int i = 0; i < files.length; i++) {
+                       if (files[i].getName().startsWith(".")) continue;
+                       
+                       MailMessage msg = new MailMessage(files[i]);
+                       
+                       msgs.put(new Integer(msg.getUID()), msg);
+               }
+               
+               return msgs;
+       }
+       
+       public MailMessage[] listMessagesArray() {
+               File[] files = this.dir.listFiles(new MessageFileNameFilter());
+               
+               MailMessage[] msgs = new MailMessage[files.length];
+               
+               for (int i = 0; i < files.length; i++) {
+                       //if (files[i].getName().startsWith(".")) continue;
+                       
+                       MailMessage msg = new MailMessage(files[i]);
+                       
+                       msgs[i] = msg;
+               }
+               
+               return msgs;
+       }
+       
+       private long nextId() {
+               File nidfile = new File(this.dir, NIDFILE);
+               long retval;
+               
+               try {
+                       BufferedReader br = new BufferedReader(new 
FileReader(nidfile));
+                       
+                       retval = Long.parseLong(br.readLine());
+               
+                       br.close();
+               } catch (IOException ioe) {
+                       return 1;
+               } catch (NumberFormatException nfe) {
+                       return 1;
+               }
+               
+               return retval;
+       }
+       
+       private void writeNextId(long newid) {
+               // write the new ID to a temporary file
+               File nidfile = new File(this.dir, NIDTMPFILE);
+               try {
+                       PrintStream ps = new PrintStream(new 
FileOutputStream(nidfile));
+                       ps.print(newid);
+                       ps.flush();
+                       ps.close();
+                       
+                       // make sure the old nextid file doesn't contain a
+                       // value greater than our one
+                       if (this.nextId() <= newid) {
+                               nidfile.renameTo(new File(this.dir, NIDFILE));
+                       }
+               } catch (IOException ioe) {
+                       // how to handle this?
+               }
+       }
+       
+       private class MessageFileNameFilter implements FilenameFilter {
+               public boolean accept(File dir, String name) {
+                       if (name.startsWith(".")) return false;
+                       return true;
+               }
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/MessageSender.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/MessageSender.java     2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/MessageSender.java     2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,163 @@
+package fnmail;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Vector;
+import java.util.Enumeration;
+
+import fnmail.fcp.FCPConnection;
+import fnmail.fcp.HighLevelFCPClient;
+import fnmail.fcp.FCPInsertErrorMessage;
+import fnmail.fcp.FCPBadFileException;
+import fnmail.utils.EmailAddress;
+
+public class MessageSender implements Runnable {
+       public static final String OUTBOX_DIR = "outbox";
+       public static final int MIN_RUN_TIME = 60000;
+       public static final String NIM_KEY_PREFIX = "KSK at fnmail-nim-";
+       private final File dir;
+       private final FCPConnection fcpconn;
+       private Thread senderthread;
+       
+       public MessageSender(File d, FCPConnection conn) {
+               this.dir = d;
+               this.fcpconn = conn;
+       }
+       
+       public void send_message(String from_user, Vector to, File msg) throws 
IOException {
+               File user_dir = new File(this.dir, from_user);
+               File outbox = new File(user_dir, OUTBOX_DIR);
+               
+               Enumeration e = to.elements();
+               while (e.hasMoreElements()) {
+                       EmailAddress email = (EmailAddress) e.nextElement();
+                       
+                       this.copyToOutbox(msg, outbox, email.user + "@" + 
email.domain);
+               }
+               this.senderthread.interrupt();
+       }
+       
+       private synchronized void copyToOutbox(File src, File outbox, String 
to) throws IOException {
+               File tempfile = File.createTempFile("fmail-msg-tmp", null);
+               
+               FileOutputStream fos = new FileOutputStream(tempfile);
+               FileInputStream fis = new FileInputStream(src);
+               
+               byte[] buf = new byte[1024];
+               int read;
+               while ( (read = fis.read(buf)) > 0) {
+                       fos.write(buf, 0, read);
+               }
+               fis.close();
+               fos.close();
+               
+               File destfile;
+               int prefix = 1;
+               do {
+                       String filename = prefix + ":" + to;
+                       destfile = new File(outbox, filename);
+                       prefix++;
+               } while (destfile.exists());
+                       
+               tempfile.renameTo(destfile);
+       }
+       
+       public void run() {
+               this.senderthread = Thread.currentThread();
+               while (true) {
+                       long start = System.currentTimeMillis();
+                       
+                       // iterate through users
+                       File[] files = dir.listFiles();
+                       for (int i = 0; i < files.length; i++) {
+                               if (files[i].getName().startsWith("."))
+                                       continue;
+                               File outbox = new File(files[i], OUTBOX_DIR);
+                               if (!outbox.exists())
+                                       outbox.mkdir();
+                               
+                               this.sendDir(outbox);
+                       }
+                       // don't spin around the loop if nothing's
+                       // going on
+                       long runtime = start - System.currentTimeMillis();
+                       
+                       if (MIN_RUN_TIME - runtime > 0) {
+                               try {
+                                       Thread.sleep(MIN_RUN_TIME - runtime);
+                               } catch (InterruptedException ie) {
+                               }
+                       }
+               }
+       }
+       
+       private void sendDir(File dir) {
+               File[] files = dir.listFiles();
+               for (int i = 0; i < files.length; i++) {
+                       if (files[i].getName().startsWith("."))
+                               continue;
+                       
+                       this.sendSingle(files[i]);
+               }
+       }
+       
+       private void sendSingle(File msg) {
+               String parts[] = msg.getName().split(":", 2);
+               EmailAddress addr;
+               if (parts.length < 2) {
+                       addr = new EmailAddress(parts[0]);
+               } else {
+                       addr = new EmailAddress(parts[1]);
+               }
+               
+               if (addr.domain == null || addr.domain.length() == 0) {
+                       msg.delete();
+                       return;
+               }
+               
+               if (addr.domain.equalsIgnoreCase("nim.fnmail")) {
+                       if (this.slotinsert(msg, NIM_KEY_PREFIX+addr.user)) {
+                               msg.delete();
+                       }
+               }
+       }
+       
+       private boolean slotinsert(File data, String basekey) {
+               // TODO: add date to this too
+               HighLevelFCPClient cli = new HighLevelFCPClient(this.fcpconn);
+               
+               int slot = 1;
+               boolean carryon = true;
+               while (carryon) {
+                       System.out.println("trying slotinsert to 
"+basekey+"-"+slot);
+                       FileInputStream fis;
+                       try {
+                               fis = new FileInputStream(data);
+                       } catch (FileNotFoundException fnfe) {
+                               // riiiiiight...
+                               return false;
+                       }
+                       FCPInsertErrorMessage emsg;
+                       try {
+                               emsg = cli.put(fis, basekey+"-"+slot);
+                       } catch (FCPBadFileException bfe) {
+                               return false;
+                       }
+                       if (emsg == null) {
+                               System.out.println("insert successful");
+                               return true;
+                       } else if (emsg.errorcode == 
FCPInsertErrorMessage.COLLISION) {
+                               slot++;
+                               System.out.println("collision");
+                       } else {
+                               System.out.println("nope - error code is 
"+emsg.errorcode);
+                               // try again later
+                               return false;
+                       }
+               }
+               return false;
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/SingleAccountWatcher.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/SingleAccountWatcher.java      2006-05-04 
12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/SingleAccountWatcher.java      2006-05-04 
23:27:32 UTC (rev 8604)
@@ -0,0 +1,39 @@
+package fnmail;
+
+import java.io.File;
+import java.lang.InterruptedException;
+
+import fnmail.fcp.FCPConnection;
+
+public class SingleAccountWatcher implements Runnable {
+       public static final String CONTACTS_DIR = "contacts";
+       private static final int MIN_POLL_DURATION = 60000; // in milliseconds
+       private final MessageBank mb;
+       private final MailFetcher mf;
+       private final FCPConnection fcpconn;
+
+       SingleAccountWatcher(FCPConnection fcpc, File accdir) {
+               this.fcpconn = fcpc;
+               File contacts_dir = new File(accdir, CONTACTS_DIR);
+               
+               this.mb = new MessageBank(accdir.getName());
+               this.mf = new MailFetcher(this.mb, contacts_dir, this.fcpconn);
+       }
+       
+       public void run() {
+               while (true) {
+                       long start = System.currentTimeMillis();
+                       
+                       mf.fetch_from_all();
+                       
+                       long runtime = start - System.currentTimeMillis();
+                       
+                       if (MIN_POLL_DURATION - runtime > 0) {
+                               try {
+                                       Thread.sleep(MIN_POLL_DURATION - 
runtime);
+                               } catch (InterruptedException ie) {
+                               }
+                       }
+               }
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/fcp/FCPBadFileException.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/fcp/FCPBadFileException.java   2006-05-04 
12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/fcp/FCPBadFileException.java   2006-05-04 
23:27:32 UTC (rev 8604)
@@ -0,0 +1,5 @@
+package fnmail.fcp;
+
+public class FCPBadFileException extends Exception {
+
+}
\ No newline at end of file

Added: trunk/apps/fnmail/src/fnmail/fcp/FCPClient.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/fcp/FCPClient.java     2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/fcp/FCPClient.java     2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,7 @@
+package fnmail.fcp;
+
+public interface FCPClient {
+       public void requestFinished(FCPMessage msg);
+       
+       public void requestStatus(FCPMessage msg);
+}

Added: trunk/apps/fnmail/src/fnmail/fcp/FCPConnection.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/fcp/FCPConnection.java 2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/fcp/FCPConnection.java 2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,129 @@
+package fnmail.fcp;
+
+import java.io.OutputStream;
+import java.io.InputStream;
+import java.net.Socket;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+
+public class FCPConnection implements Runnable {
+       private final FCPContext fcpctx;
+       private OutputStream os;
+       private InputStream is;
+       private Socket conn;
+       private int nextMsgId;
+       private final HashMap clients;
+
+       public FCPConnection(FCPContext ctx) {
+               this.fcpctx = ctx;
+               this.clients = new HashMap();
+               this.nextMsgId = 1;
+               
+               try {
+                       this.conn = this.fcpctx.getConn();
+                       this.is = this.conn.getInputStream();
+                       this.os = this.conn.getOutputStream();
+               
+                       FCPMessage hello = new FCPMessage(this.nextMsgId, 
"ClientHello");
+                       this.nextMsgId++;
+                       hello.writeto(this.os);
+                       FCPMessage reply = this.getMessage();
+                       if (reply.getType() == null) {
+                               System.out.println("Connection closed");
+                               this.conn = null;
+                       }
+                       if (!reply.getType().equals("NodeHello")) {
+                               System.out.println("Warning - got 
'"+reply.getType()+"' from node, expecting 'NodeHello'");
+                       }
+               } catch (IOException ioe) {
+                       System.out.println("Warning - could not connect to node 
- "+ioe.getMessage());
+                       try {
+                               if (this.conn != null) {
+                                       this.conn.close();
+                               }
+                       } catch (IOException ioe2) {
+                       }
+                       this.conn = null;
+               } catch (FCPBadFileException bfe) {
+                       // won't be thrown from a hello, so should really
+                       // never get here!
+               }
+       }
+       
+       public void run() {
+               while (true) {
+                       try {
+                               if (this.conn == null) {
+                                       this.conn = this.fcpctx.getConn();
+                                       this.is = this.conn.getInputStream();
+                                       this.os = this.conn.getOutputStream();
+                               }
+                               
+                               FCPMessage msg = this.getMessage();
+                               if (msg == null) throw new IOException();
+                               this.dispatch(msg);
+                       } catch (IOException ioe) {
+                               System.out.println("Warning - error 
communicating with node - "+ioe.getMessage());
+                               this.conn = null;
+                               // tell all our clients it's all over
+                               Iterator i = this.clients.values().iterator();
+                               while (i.hasNext()) {
+                                       FCPClient cli = (FCPClient)i.next();
+                                       cli.requestFinished(null);
+                               }
+                               this.clients.clear();
+                               // wait a bit
+                               try {
+                                       Thread.sleep(10000);
+                               } catch (InterruptedException ie) {
+                               }
+                       }
+               }
+       }
+
+       protected void finalize() throws Throwable {
+               try {
+                       this.conn.close();
+               } catch (Exception e) {
+               }
+               super.finalize();
+       }
+       
+       public synchronized void doRequest(FCPClient cli, FCPMessage msg) 
throws NoNodeConnectionException, FCPBadFileException {
+               if (this.os == null) throw new NoNodeConnectionException();
+               this.clients.put(msg.getId(), cli);
+               try {
+                       msg.writeto(this.os);
+               } catch (IOException ioe) {
+                       throw new NoNodeConnectionException();
+               }
+       }
+       
+       private void dispatch(FCPMessage msg) {
+               FCPClient cli = (FCPClient)this.clients.get(msg.getId());
+               if (cli == null) {
+                       // normally we'd leave it up to the client
+                       // to delete any data, but it looks like
+                       // we'll have to do it
+                       msg.release();
+                       return;
+               }
+               if (msg.isCompletionMessage()) {
+                       this.clients.remove(msg.getId());
+                       cli.requestFinished(msg);
+               } else {
+                       cli.requestStatus(msg);
+               }
+       }
+       
+       public synchronized FCPMessage getMessage(String type) {
+               FCPMessage m = new FCPMessage(this.nextMsgId, type);
+               this.nextMsgId++;
+               return m;
+       }
+       
+       private FCPMessage getMessage() throws IOException {
+               return new FCPMessage(this.is);
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/fcp/FCPContext.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/fcp/FCPContext.java    2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/fcp/FCPContext.java    2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,18 @@
+package fnmail.fcp;
+
+import java.io.IOException;
+import java.net.Socket;
+
+public class FCPContext {
+       private final String hostname;
+       private final int port;
+       
+       public FCPContext(String h, int p) {
+               this.hostname = h;
+               this.port = p;
+       }
+       
+       public Socket getConn() throws IOException {
+               return new Socket(this.hostname, this.port);
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/fcp/FCPErrorMessage.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/fcp/FCPErrorMessage.java       2006-05-04 
12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/fcp/FCPErrorMessage.java       2006-05-04 
23:27:32 UTC (rev 8604)
@@ -0,0 +1,20 @@
+package fnmail.fcp;
+
+public class FCPErrorMessage {
+       public final int errorcode;
+       public final boolean isFatal;
+       
+       FCPErrorMessage(FCPMessage msg) {
+               String code = (String)msg.headers.get("Code");
+               if (code != null)
+                       this.errorcode = Integer.parseInt(code);
+               else
+                       this.errorcode = 0;
+               
+               String fatal = (String)msg.headers.get("Fatal");
+               if (fatal != null)
+                       this.isFatal = (fatal.equalsIgnoreCase("true"));
+               else
+                       this.isFatal = false;
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/fcp/FCPInsertErrorMessage.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/fcp/FCPInsertErrorMessage.java 2006-05-04 
12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/fcp/FCPInsertErrorMessage.java 2006-05-04 
23:27:32 UTC (rev 8604)
@@ -0,0 +1,28 @@
+package fnmail.fcp;
+
+public class FCPInsertErrorMessage extends FCPErrorMessage {
+       /* Caller supplied a URI we cannot use */
+       public static final int INVALID_URI = 1;
+       /* Failed to read from or write to a bucket; a kind of internal error */
+       public static final int BUCKET_ERROR = 2;
+       /* Internal error of some sort */
+       public static final int INTERNAL_ERROR = 3;
+       /* Downstream node was overloaded */
+       public static final int REJECTED_OVERLOAD = 4;
+       /* Couldn't find enough nodes to send the data to */
+       public static final int ROUTE_NOT_FOUND = 5;
+       /* There were fatal errors in a splitfile insert. */
+       public static final int FATAL_ERRORS_IN_BLOCKS = 6;
+       /* Could not insert a splitfile because a block failed too many times */
+       public static final int TOO_MANY_RETRIES_IN_BLOCKS = 7;
+       /* Not able to leave the node at all */
+       public static final int ROUTE_REALLY_NOT_FOUND = 8;
+       /* Collided with pre-existing content */
+       public static final int COLLISION = 9;
+       /* Cancelled by user */
+       public static final int CANCELLED = 10;
+
+       FCPInsertErrorMessage(FCPMessage msg) {
+               super(msg);
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/fcp/FCPMessage.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/fcp/FCPMessage.java    2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/fcp/FCPMessage.java    2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,179 @@
+package fnmail.fcp;
+
+import java.io.OutputStream;
+import java.io.InputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Enumeration;
+import java.util.Collections;
+
+import freenet.support.io.LineReader;
+import freenet.support.io.LineReadingInputStream;
+
+public class FCPMessage {
+       private String messagetype;
+       private String identifier;
+       public final HashMap headers;
+       private File data;
+       private InputStream outData;
+       
+       
+       public FCPMessage(int id, String type) {
+               this.identifier = Integer.toString(id);
+               this.headers = new HashMap();
+               this.messagetype = type;
+               this.data = null;
+               this.outData = null;
+       }
+       
+       public FCPMessage(InputStream is) throws IOException {
+               this.headers = new HashMap();
+               this.outData = null;
+               
+               this.messagetype = null;
+               LineReader r = new LineReadingInputStream(is);
+               
+               String line;
+               while ( (line = r.readLine(200, 200)) != null) {
+                       //System.out.println(line);
+                       if (this.messagetype == null) {
+                               messagetype = this.messagetype = line;
+                       } else if (line.startsWith("End")) {
+                               return;
+                       } else if (line.equals("Data")) {
+                               try {
+                                       int len = 
Integer.decode((String)this.headers.get("DataLength")).intValue();
+                                       this.readData(is, len);
+                               } catch (NumberFormatException nfe) {
+                               }
+                               return;
+                       } else {
+                               String[] parts = line.split("=");
+                               if (parts.length == 2)
+                                       this.addHeader(parts[0], parts[1]);
+                       }
+               }
+       }
+       
+       private void addHeader(String name, String val) {
+               if (name.equalsIgnoreCase("Identifier")) {
+                       this.identifier = val;
+               } else {
+                       this.headers.put(name, val);
+               }
+       }
+       
+       public String getType() {
+               return this.messagetype;
+       }
+       
+       public String getId() {
+               return this.identifier;
+       }
+       
+       public File getData() {
+               return this.data;
+       }
+       
+       public void setData(InputStream d) {
+               this.outData = d;
+       }
+       
+       private void readData(InputStream is, int len) {
+               try {
+                       this.data = File.createTempFile("fnmail-fcp", null);
+               } catch (Exception e) {
+                       this.data = null;
+                       return;
+               }
+               try {
+                       FileOutputStream fos = new FileOutputStream(this.data);
+                       
+                       byte[] buf = new byte[1024];
+                       while (len > 0) {
+                               int toRead = len;
+                               if (toRead > buf.length)
+                                       toRead = buf.length;
+                               int read = is.read(buf, 0, toRead);
+                               fos.write(buf, 0, read);
+                               len -= read;
+                       }
+                       fos.close();
+               } catch (IOException ioe) {
+                       this.data = null;
+                       return;
+               }
+       }
+       
+       public boolean isCompletionMessage() {
+               if (this.messagetype.equalsIgnoreCase("PutFailed"))
+                       return true;
+               if (this.messagetype.equalsIgnoreCase("PutSuccessful"))
+                       return true;
+               if (this.messagetype.equalsIgnoreCase("AllData"))
+                       return true;
+               if (this.messagetype.equalsIgnoreCase("GetFailed"))
+                       return true;
+               if (this.messagetype.equalsIgnoreCase("ProtocolError"))
+                       return true;
+               if (this.messagetype.equalsIgnoreCase("SSKKeypair"))
+                       return true;
+               if (this.messagetype.equalsIgnoreCase("IdentifierCollision"))
+                       return true;
+               return false;
+       }
+       
+       public void release() {
+               if (this.data != null) {
+                       this.data.delete();
+               }
+       }
+       
+       public void writeto(OutputStream os) throws IOException, 
FCPBadFileException {
+               StringBuffer buf = new StringBuffer();
+               
+               buf.append(this.messagetype);
+               buf.append("\r\n");
+               
+               if (this.messagetype.equalsIgnoreCase("ClientHello")) {
+                       buf.append("Name=fnmail\r\n");
+                       buf.append("ExpectedVersion=2.0\r\n");
+               }
+               
+               buf.append("Identifier="+this.identifier+"\r\n");
+               
+               for (Enumeration e = 
Collections.enumeration(this.headers.keySet()); e.hasMoreElements(); ) {
+                       String hdr = (String) e.nextElement();
+                       String val = (String) this.headers.get(hdr);
+                       
+                       buf.append(hdr+"="+val+"\r\n");
+               }
+               
+               if (this.outData != null) {
+                       buf.append("UploadFrom=direct\r\n");
+                       try {
+                               
buf.append("DataLength="+this.outData.available()+"\r\n");
+                       } catch (IOException ioe) {
+                               throw new FCPBadFileException();
+                       }
+                       buf.append("Data\r\n");
+               } else {
+                       buf.append("EndMessage\r\n");
+               }
+               if (buf.length() > 0) {
+                       //System.out.println(buf.toString());
+                       os.write(buf.toString().getBytes());
+               }
+               if (this.outData != null) {
+                       byte[] bytebuf = new byte[1024];
+                       
+                       int read;
+                       while ( (read = this.outData.read(bytebuf)) > 0) {
+                               os.write(bytebuf, 0, read);
+                       }
+                       this.outData.close();
+               }
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/fcp/HighLevelFCPClient.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/fcp/HighLevelFCPClient.java    2006-05-04 
12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/fcp/HighLevelFCPClient.java    2006-05-04 
23:27:32 UTC (rev 8604)
@@ -0,0 +1,98 @@
+package fnmail.fcp;
+
+import java.io.File;
+import java.io.InputStream;
+
+public class HighLevelFCPClient implements FCPClient {
+       private FCPConnection conn;
+       private FCPMessage donemsg;
+       
+       public HighLevelFCPClient(FCPConnection c) {
+               this.conn = c;
+       }
+       
+       // It's up to the client to delete this File once they're
+       // done with it
+       public synchronized File fetch(String key) {
+               FCPMessage msg = this.conn.getMessage("ClientGet");
+               msg.headers.put("URI", key);
+               msg.headers.put("ReturnType", "direct");
+               msg.headers.put("Persistence", "connection");
+               
+               while (true) {
+                       try {
+                               this.conn.doRequest(this, msg);
+                               break;
+                       } catch (NoNodeConnectionException nnce) {
+                               try {
+                                       Thread.sleep(5000);
+                               } catch (InterruptedException ie) {
+                               }
+                       } catch (FCPBadFileException bfe) {
+                               // won't be thrown since this is a get,
+                               // but keep the compiler happy
+                       }
+               }
+               
+               this.donemsg = null;
+               while (this.donemsg == null) {
+                       try {
+                               this.wait();
+                       } catch (InterruptedException ie) {
+                       }
+               }
+               
+               if (this.donemsg.getType().equalsIgnoreCase("AllData")) {
+                       return this.donemsg.getData();
+               } else {
+                       return null;
+               }
+       }
+       
+       public synchronized FCPInsertErrorMessage put(InputStream data, String 
key) throws FCPBadFileException {
+               FCPMessage msg = this.conn.getMessage("ClientPut");
+               msg.headers.put("URI", key);
+               msg.headers.put("Persistence", "connection");
+               msg.setData(data);
+               
+               while (true) {
+                       try {
+                               this.conn.doRequest(this, msg);
+                               break;
+                       } catch (NoNodeConnectionException nnce) {
+                               try {
+                                       System.out.println("sleeping");
+                                       Thread.sleep(5000);
+                                       System.out.println("wake");
+                               } catch (InterruptedException ie) {
+                                       System.out.println("inerrupted");
+                               }
+                       }
+               }
+               
+               this.donemsg = null;
+               while (this.donemsg == null) {
+                       try {
+                               this.wait();
+                       } catch (InterruptedException ie) {
+                       }
+               }
+               
+               if (this.donemsg.getType().equalsIgnoreCase("PutSuccessful")) {
+                       return null;
+               } else {
+                       return new FCPInsertErrorMessage(donemsg);
+               }
+       }
+       
+       public void requestStatus(FCPMessage msg) {
+               
+       }
+       
+       public void requestFinished(FCPMessage msg) {
+               synchronized (this) {
+                       this.donemsg = msg;
+                       this.notifyAll();
+               }
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/fcp/NoNodeConnectionException.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/fcp/NoNodeConnectionException.java     
2006-05-04 12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/fcp/NoNodeConnectionException.java     
2006-05-04 23:27:32 UTC (rev 8604)
@@ -0,0 +1,5 @@
+package fnmail.fcp;
+
+public class NoNodeConnectionException extends Exception {
+
+}

Added: trunk/apps/fnmail/src/fnmail/imap/IMAPBadMessageException.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/imap/IMAPBadMessageException.java      
2006-05-04 12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/imap/IMAPBadMessageException.java      
2006-05-04 23:27:32 UTC (rev 8604)
@@ -0,0 +1,5 @@
+package fnmail.imap;;
+
+public class IMAPBadMessageException extends Exception {
+       // no, this isn't the most exciting class in the world.
+}

Added: trunk/apps/fnmail/src/fnmail/imap/IMAPHandler.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/imap/IMAPHandler.java  2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/imap/IMAPHandler.java  2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,718 @@
+package fnmail.imap;
+
+import java.net.Socket;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.util.SortedMap;
+import java.lang.NumberFormatException;
+
+import fnmail.MessageBank;
+import fnmail.MailMessage;
+import fnmail.AccountManager;
+import fnmail.utils.EmailAddress;
+
+public class IMAPHandler implements Runnable {
+       final Socket client;
+       final OutputStream os;
+       final PrintStream ps;
+       final BufferedReader bufrdr;
+       MessageBank mb;
+       
+
+       IMAPHandler(Socket client) throws IOException {
+               this.client = client;
+               this.os = client.getOutputStream();
+               this.ps = new PrintStream(this.os);
+               this.bufrdr = new BufferedReader(new 
InputStreamReader(client.getInputStream()));
+               this.mb = null;
+       }
+       
+       public void run() {
+               this.sendWelcome();
+               
+               String line;
+               try {
+                       while ( !this.client.isClosed() && (line = 
this.bufrdr.readLine()) != null) {
+                               IMAPMessage msg = null;
+                               try {
+                                       msg = new IMAPMessage(line);
+                               } catch (IMAPBadMessageException bme) {
+                                       continue;
+                               }
+                               
+                               this.dispatch(msg);
+                       }
+               
+                       this.client.close();
+               } catch (IOException ioe) {
+                       
+               }
+       }
+       
+       private void sendWelcome() {
+               this.ps.print("* OK [CAPABILITY IMAP4rev1] fnmail ready - hit 
me with your rhythm stick.\r\n");
+       }
+       
+       private void dispatch(IMAPMessage msg) {
+               //System.out.println(msg.toString());
+               if (msg.type.equals("login")) {
+                       this.handle_login(msg);
+               } else if (msg.type.equals("logout")) {
+                       this.handle_logout(msg);
+               } else if (msg.type.equals("capability")) {
+                       this.handle_capability(msg);
+               } else if (msg.type.equals("list")) {
+                       this.handle_list(msg);
+               } else if (msg.type.equals("select")) {
+                       this.handle_select(msg);
+               } else if (msg.type.equals("noop")) {
+                       this.handle_noop(msg);
+               } else if (msg.type.equals("uid")) {
+                       this.handle_uid(msg);
+               } else if (msg.type.equals("fetch")) {
+                       this.handle_fetch(msg);
+               } else if (msg.type.equals("store")) {
+                       this.handle_store(msg);
+               } else if (msg.type.equals("close")) {
+                       this.handle_close(msg);
+               } else if (msg.type.equals("expunge")) {
+                       this.handle_expunge(msg);
+               } else if (msg.type.equals("namespace")) {
+                       this.handle_namespace(msg);
+               } else if (msg.type.equals("lsub")) {
+                       this.handle_lsub(msg);
+               } else {
+                       this.reply(msg, "NO Sorry - not implemented");
+               }
+       }
+       
+       private void handle_login(IMAPMessage msg) {
+               if (msg.args.length < 2) return;
+               if (AccountManager.authenticate(trimQuotes(msg.args[0]), 
trimQuotes(msg.args[1]))) {
+                       this.mb = new MessageBank(trimQuotes(msg.args[0]));
+                       
+                       this.reply(msg, "OK Logged in");
+               } else {
+                       this.reply(msg, "NO Login failed");
+               }
+       }
+       
+       private void handle_logout(IMAPMessage msg) {
+               this.reply(msg, "OK Bye");
+               try {
+                       this.client.close();
+               } catch (IOException ioe) {
+                       
+               }
+       }
+       
+       private void handle_capability(IMAPMessage msg) {
+               this.sendState("CAPABILITY IMAP4rev1 AUTH=LOGIN");
+               
+               this.reply(msg, "OK Capability completed");
+       }
+       
+       private void handle_lsub(IMAPMessage msg) {
+               this.handle_list(msg);
+       }
+       
+       private void handle_list(IMAPMessage msg) {
+               String refname;
+               String mbname;
+               
+               if (!this.verify_auth(msg)) {
+                       return;
+               }
+               
+               if (msg.args.length < 1) {
+                       refname = null;
+                       mbname = null;
+               } else if (msg.args.length < 2) {
+                       refname = msg.args[0];
+                       mbname = null;
+               } else {
+                       refname = msg.args[0];
+                       mbname = msg.args[1];
+               }
+               
+               refname = trimQuotes(refname);
+               if (refname.length() == 0) refname = null;
+               
+               mbname = trimQuotes(mbname);
+               if (mbname.length() == 0) mbname = null;
+               
+               if (mbname == null) {
+                       // return hierarchy delimiter
+                       this.sendState("LIST (\\Noselect) \".\" \"\"");
+               } else if (mbname.equals("%") || mbname.equals("INBOX")) {
+                       this.sendState("LIST (\\NoInferiors) \".\" \"INBOX\"");
+               }
+               
+               this.reply(msg, "OK LIST completed");
+       }
+       
+       private void handle_select(IMAPMessage msg) {
+               String mbname;
+               
+               if (!this.verify_auth(msg)) {
+                       return;
+               }
+               
+               if (msg.args.length < 1) {
+                       this.reply(msg, "NO What mailbox?");
+                       return;
+               }
+               
+               mbname = trimQuotes(msg.args[0]).toLowerCase();
+               
+               if (mbname.equals("inbox")) {
+                       this.sendState("FLAGS (\\Recent \\Seen)");
+                       this.sendState("OK [PERMANENTFLAGS (\\*)]");
+                       
+                       SortedMap msgs = this.mb.listMessages();
+                       
+                       int numrecent = 0;
+                       int numexists = msgs.size();
+                       while (msgs.size() > 0) {
+                               Integer current = (Integer)(msgs.firstKey());
+                               MailMessage m 
=(MailMessage)msgs.get(msgs.firstKey());
+                               
+                               // if it's recent, add to the tally
+                               if (m.flags.get("\\Recent")) numrecent++;
+                               
+                               // remove the recent flag
+                               m.flags.set("\\Recent", false);
+                               m.storeFlags();
+                               
+                               msgs = msgs.tailMap(new 
Integer(current.intValue()+1));
+                       }
+                       
+                       this.sendState(numexists+" EXISTS");
+                       this.sendState(numrecent+" RECENT");
+                       
+                       
+                       this.reply(msg, "OK [READ-WRITE] Done");
+               } else {
+                       this.reply(msg, "NO No such mailbox");
+               }
+       }
+       
+       private void handle_noop(IMAPMessage msg) {
+               this.reply(msg, "OK NOOP completed");
+       }
+       
+       private void handle_fetch(IMAPMessage msg) {
+               int from;
+               int to;
+               
+               if (!this.verify_auth(msg)) {
+                       return;
+               }
+               
+               SortedMap msgs = this.mb.listMessages();
+               
+               if (msgs.size() == 0) {
+                       this.reply(msg, "OK Fetch completed");
+                       return;
+               }
+               
+               if (msg.args == null || msg.args.length < 2) {
+                       this.reply(msg, "BAD Not enough arguments");
+                       return;
+               }
+               
+               String[] parts = msg.args[0].split(":");
+               try {
+                       from = Integer.parseInt(parts[0]);
+               } catch (NumberFormatException nfe) {
+                       this.reply(msg, "BAD Bad number");
+                       return;
+               }
+               if (parts.length < 2) {
+                       to = from;
+               } else if (parts[1].equals("*")) {
+                       to = msgs.size();
+               } else {
+                       try {
+                               to = Integer.parseInt(parts[1]);
+                       } catch (NumberFormatException nfe) {
+                               this.reply(msg, "BAD Bad number");
+                               return;
+                       }
+               }
+               
+               if (from == 0 || to == 0 || from > msgs.size() || to > 
msgs.size()) {
+                       this.reply(msg, "NO Invalid message ID");
+                       return;
+               }
+               
+               for (int i = 1; msgs.size() > 0; i++) {
+                       Integer current = (Integer)(msgs.firstKey());
+                       if (i < from) {
+                               msgs = msgs.tailMap(new 
Integer(current.intValue()+1));
+                               continue;
+                       }
+                       if (i > to) break;
+                       
+                       if 
(!this.fetch_single((MailMessage)msgs.get(msgs.firstKey()), i, msg.args, 1, 
false)) {
+                               this.reply(msg, "BAD Unknown attribute in list 
or unterminated list");
+                               return;
+                       }
+                       
+                       msgs = msgs.tailMap(new Integer(current.intValue()+1));
+               }
+               
+               this.reply(msg, "OK Fetch completed");
+       }
+       
+       private void handle_uid(IMAPMessage msg) {
+               int from;
+               int to;
+               
+               if (msg.args == null || msg.args.length < 3) {
+                       this.reply(msg, "BAD Not enough arguments to uid 
command");
+                       return;
+               }
+               
+               if (!this.verify_auth(msg)) {
+                       return;
+               }
+               
+               SortedMap msgs = this.mb.listMessages();
+               
+               if (msgs.size() == 0) {
+                       if (msg.args[0].toLowerCase().equals("fetch")) {
+                               this.reply(msg, "OK Fetch completed");
+                       } else if (msg.args[0].toLowerCase().equals("store")) {
+                               // hmm...?
+                               this.reply(msg, "NO No such message");
+                       }
+                       return;
+               }
+               
+               String[] parts = msg.args[1].split(":");
+               try {
+                       from = Integer.parseInt(parts[0]);
+               } catch (NumberFormatException nfe) {
+                       this.reply(msg, "BAD Bad number");
+                       return;
+               }
+               if (parts.length < 2) {
+                       to = from;
+               } else if (parts[1].equals("*")) {
+                       Integer tmp = (Integer)msgs.lastKey();
+                       to = tmp.intValue();
+               } else {
+                       try {
+                               to = Integer.parseInt(parts[1]);
+                       } catch (NumberFormatException nfe) {
+                               this.reply(msg, "BAD Bad number");
+                               return;
+                       }
+               }
+               
+               int msgnum = 1;
+               if (msg.args[0].toLowerCase().equals("fetch")) {
+                       msgs = msgs.tailMap(new Integer(from));
+                       while (msgs.size() > 0) {
+                               Integer curuid = (Integer)msgs.firstKey();
+                               if (curuid.intValue() > to) {
+                                       break;
+                               }
+                               
+                               if 
(!this.fetch_single((MailMessage)msgs.get(msgs.firstKey()), msgnum, msg.args, 
2, true)) {
+                                       this.reply(msg, "BAD Unknown attribute 
in list or unterminated list");
+                                       return;
+                               }
+                               
+                               msgs = msgs.tailMap(new 
Integer(curuid.intValue()+1));
+                               msgnum++;
+                       }
+                       
+                       this.reply(msg, "OK Fetch completed");
+               } else if (msg.args[0].toLowerCase().equals("store")) {
+                       msgs = msgs.tailMap(new Integer(from));
+                       msgs = msgs.headMap(new Integer(to + 1));
+                       
+                       MailMessage[] targetmsgs = new MailMessage[msgs.size()];
+                       
+                       for (int i = 0; i < targetmsgs.length; i++) {
+                               targetmsgs[i] = 
(MailMessage)msgs.values().toArray()[i];
+                       }
+                       
+                       this.do_store(msg.args, 2, targetmsgs, msg, true);
+                       
+                       this.reply(msg, "OK Store completed");
+               } else {
+                       this.reply(msg, "BAD Unknown command");
+               }
+       }
+       
+       private boolean fetch_single(MailMessage msg, int id, String[] args, 
int firstarg, boolean send_uid_too) {
+               String[] imap_args = (String[]) args.clone();
+               this.ps.print("* "+id+" FETCH (");
+               
+               // do the first attribute, if it's a loner.
+               if (!imap_args[firstarg].startsWith("(")) {
+                       // It's a loner
+                       this.ps.flush();
+                       if (!this.send_attr(msg, imap_args[firstarg]))
+                               return false;
+                       
+                       if (send_uid_too && 
!imap_args[firstarg].equalsIgnoreCase("uid")) {
+                               this.ps.print(" UID "+msg.getUID());
+                       }
+                       
+                       this.ps.print(")\r\n");
+                       this.ps.flush();
+                       
+                       return true;
+               } else {
+                       imap_args[firstarg] = imap_args[firstarg].substring(1);
+               }
+               
+               // go through the parenthesised list
+               for (int i = firstarg; i < imap_args.length; i++) {
+                       String attr;
+                       boolean finish = false;
+                       
+                       if (imap_args[i].endsWith(")")) {
+                               finish = true;
+                               attr = imap_args[i].substring(0, 
imap_args[i].length() - 1);
+                       } else {
+                               attr = imap_args[i];
+                       }
+                       
+                       //this.ps.print(attr+" ");
+                       this.ps.flush();
+                       if (!this.send_attr(msg, attr))
+                               return false;
+                       
+                       if (attr.equalsIgnoreCase("uid")) {
+                               send_uid_too = false;
+                       }
+                       
+                       if (finish) {
+                               if (send_uid_too) {
+                                       this.ps.print(" UID "+msg.getUID());
+                               }
+                               
+                               this.ps.print(")\r\n");
+                               this.ps.flush();
+                               return true;
+                       } else {
+                               this.ps.print(" ");
+                       }
+               }
+               
+               // if we get here, we've reached the end of the list without a 
terminating parenthesis. Naughty client.
+               return false;
+       }
+       
+       private boolean send_attr(MailMessage mmsg, String a) {
+               String attr = a.toLowerCase();
+               String val = null;
+               
+               if (attr.equals("uid")) {
+                       val = Integer.toString(mmsg.getUID());
+               } else if (attr.equals("flags")) {
+                       val = "(" + mmsg.flags.getFlags() + ")";
+               } else if (attr.equals("rfc822.size")) {
+                       try {
+                               val = Long.toString(mmsg.getSize());
+                       } catch (IOException ioe) {
+                               val = "0";
+                       }
+               } else if (attr.equals("envelope")) {
+                       val = this.getEnvelope(mmsg);
+               } else if (attr.startsWith("body.peek")) {
+                       this.ps.print(a.substring(0, "body".length()));
+                       this.ps.flush();
+                       attr = attr.substring("body.peek".length());
+                       return this.sendBody(mmsg, attr);
+               } else if (attr.startsWith("body")) {
+                       mmsg.flags.set("\\Seen", true);
+                       
+                       this.ps.print(a.substring(0, "body".length()));
+                       this.ps.flush();
+                       attr = attr.substring("body".length());
+                       return this.sendBody(mmsg, attr);
+               }
+               
+               if (val == null)
+                       return false;
+               this.ps.print(a+" "+val);
+               return true;
+       }
+       
+       private boolean sendBody(MailMessage mmsg, String attr) {
+               if (attr.length() < 1) return false;
+               
+               if (attr.charAt(0) == '[') attr = attr.substring(1);
+               if (attr.charAt(attr.length() - 1) == ']')
+                       attr = attr.substring(0, attr.length() - 1);
+               
+               if (attr.trim().length() == 0) {
+                       try {
+                               this.ps.print("[] ");
+                               this.ps.print("{"+mmsg.getSize()+"}\r\n");
+                               
+                               String line;
+                               while ( (line = mmsg.readLine()) != null) {
+                                       this.ps.print(line+"\r\n");
+                               }
+                               mmsg.closeStream();
+                       } catch (IOException ioe) {
+                               return false;
+                       }
+                       return true;
+               }
+               
+               StringBuffer buf = new StringBuffer("");
+               
+               String[] parts = IMAPMessage.doSplit(attr, '(', ')');
+               for (int i = 0; i < parts.length; i++) {
+                       if (parts[i].toLowerCase().equals("header.fields")) {
+                               i++;
+                               this.ps.print("[HEADER.FIELDS "+parts[i]+"] ");
+                               if (parts[i].charAt(0) == '(')
+                                       parts[i] = parts[i].substring(1);
+                               if (parts[i].charAt(parts[i].length() - 1) == 
')')
+                                       parts[i] = parts[i].substring(0, 
parts[i].length() - 1);
+                               
+                               try {
+                                       mmsg.readHeaders();
+                               } catch (IOException ioe) {
+                                       
+                               }
+                               
+                               String[] fields = parts[i].split(" ");
+                               for (int j = 0; j < fields.length; j++) {
+                                       buf.append(mmsg.getHeaders(fields[j]));
+                               }
+                       }
+                       
+                       this.ps.print("{"+buf.length()+"}\r\n"+buf.toString());
+                       return true;
+               }
+               
+               return false;
+       }
+       
+       public void handle_store(IMAPMessage msg) {
+               if (msg.args.length < 2) {
+                       this.reply(msg, "BAD Not enough arguments");
+                       return;
+               }
+               
+               if (!this.verify_auth(msg)) {
+                       return;
+               }
+               
+               String rangeparts[] = msg.args[0].split(":");
+               
+               Object[] allmsgs = this.mb.listMessages().values().toArray();
+               
+               int from;
+               int to;
+               try {
+                       from = Integer.parseInt(rangeparts[0]);
+               } catch (NumberFormatException nfe) {
+                       this.reply(msg, "BAD That's not a number!");
+                       return;
+               }
+               if (rangeparts.length > 1) {
+                       if (rangeparts[1].equals("*")) {
+                               to = allmsgs.length;
+                       } else {
+                               try {
+                                       to = Integer.parseInt(rangeparts[1]);
+                               } catch (NumberFormatException nfe) {
+                                       this.reply(msg, "BAD That's not a 
number!");
+                                       return;
+                               }
+                       }
+               } else {
+                       to = from;
+               }
+               
+               MailMessage[] msgs = new MailMessage[(to - from) + 1];
+               
+               // convert to zero based array
+               from--;
+               to--;
+               
+               if (from < 0 || to < 0 || from > msgs.length || to > 
msgs.length) {
+                       this.reply(msg, "NO No such message");
+                       return;
+               }
+               
+               for (int i = from; i <= to; i++) {
+                       msgs[i - from] = (MailMessage) allmsgs[i];
+               }
+               
+               do_store(msg.args, 1, msgs, msg, false);
+               
+               this.reply(msg, "OK Store completed");
+       }
+       
+       private void do_store(String[] args, int offset, MailMessage[] mmsgs, 
IMAPMessage msg, boolean UIDMode) {
+               if (args[offset].toLowerCase().indexOf("flags") < 0) {
+                       // IMAP4Rev1 can only store flags, so you're
+                       // trying something crazy
+                       this.reply(msg, "BAD Can't store that");
+                       return;
+               }
+               
+               if (args[offset + 1].startsWith("("))
+                       args[offset + 1] = args[offset + 1].substring(1);
+               
+               boolean setFlagTo;
+               if (args[offset].startsWith("-")) {
+                       setFlagTo = false;
+               } else if (args[offset].startsWith("+")) {
+                       setFlagTo = true;
+               } else {
+                       for (int i = 0; i < mmsgs.length; i++) {
+                               mmsgs[i].flags.clear();
+                       }
+                       setFlagTo = true;
+               }
+               
+               
+               for (int i = offset; i < args.length; i++) {
+                       String flag = args[i];
+                       if (flag.endsWith(")")) {
+                               flag = flag.substring(0, flag.length() - 1);
+                       }
+                       
+                       for (int j = 0; j < mmsgs.length; j++) {
+                               mmsgs[j].flags.set(flag, setFlagTo);
+                               mmsgs[j].storeFlags();
+                       }
+               }
+               
+               if (msg.args[offset].toLowerCase().indexOf("silent") < 0) {
+                       for (int i = 0; i < mmsgs.length; i++) {
+                               StringBuffer buf = new StringBuffer("");
+                               
+                               if (UIDMode) {
+                                       buf.append(mmsgs[i].getUID() + " FETCH 
FLAGS (");
+                               } else {
+                                       buf.append((i+1) + " FETCH FLAGS (");
+                               }
+                               
+                               buf.append(mmsgs[i].flags.getFlags());
+                               
+                               buf.append(")");
+                               
+                               this.sendState(buf.toString());
+                       }
+               }
+       }
+       
+       private void handle_expunge(IMAPMessage msg) {
+               MailMessage[] mmsgs = this.mb.listMessagesArray();
+               
+               for (int i = 0; i < mmsgs.length; i++) {
+                       if (mmsgs[i].flags.get("\\Deleted"))
+                               mmsgs[i].delete();
+                       this.sendState(i+" EXPUNGE");
+               }
+               this.reply(msg, "OK Mailbox closed");
+       }
+       
+       private void handle_close(IMAPMessage msg) {
+               MailMessage[] mmsgs = this.mb.listMessagesArray();
+               
+               for (int i = 0; i < mmsgs.length; i++) {
+                       if (mmsgs[i].flags.get("\\Deleted"))
+                               mmsgs[i].delete();
+               }
+               this.reply(msg, "OK Mailbox closed");
+       }
+       
+       private void handle_namespace(IMAPMessage msg) {
+               this.sendState("NAMESPACE ((\"\" \"/\")) NIL NIL");
+               this.reply(msg, "OK Namespace completed");
+       }
+       
+       private String getEnvelope(MailMessage mmsg) {
+               StringBuffer buf = new StringBuffer("(");
+               
+               try {
+                       mmsg.readHeaders();
+               } catch (IOException ioe) {
+               }
+               
+               buf.append(IMAPifyString(mmsg.getFirstHeader("Date"))+" ");
+               buf.append(IMAPifyString(mmsg.getFirstHeader("Subject"))+" ");
+               // from
+               buf.append(this.IMAPifyAddress(mmsg.getFirstHeader("From"))+" 
");
+               // sender (this should probably be the fnmail address that
+               // we got it from, except I haven't found a mail client that
+               // actually uses this part yet, so it might be pointless
+               
buf.append(this.IMAPifyAddress(mmsg.getFirstHeader("x-fnmail-sender"))+" ");
+               
buf.append(this.IMAPifyAddress(mmsg.getFirstHeader("Reply-To"))+" ");
+               
+               buf.append(this.IMAPifyAddress(mmsg.getFirstHeader("To"))+" ");
+               buf.append(this.IMAPifyAddress(mmsg.getFirstHeader("CC"))+" ");
+               buf.append(this.IMAPifyAddress(mmsg.getFirstHeader("BCC"))+" ");
+               buf.append(IMAPifyString(mmsg.getFirstHeader("In-Reply-To"))+" 
");
+               buf.append(IMAPifyString(mmsg.getFirstHeader("Message-ID")));
+               buf.append(")");
+               
+               return buf.toString();
+       }
+       
+       private String IMAPifyString(String in) {
+               if (in == null) return "NIL";
+               return "\""+in.trim()+"\"";
+       }
+       
+       private String IMAPifyAddress(String address) {
+               if (address == null || address.length() == 0) return "NIL";
+               
+               EmailAddress addr = new EmailAddress(address);
+               
+               String retval = new String("((");
+               retval += this.IMAPifyString(addr.realname)+" ";
+               // SMTP Source Route. Whatever this is, it's not relevant!
+               retval += "NIL ";
+               retval += this.IMAPifyString(addr.user)+" ";
+               retval += this.IMAPifyString(addr.domain);
+               retval += "))";
+               
+               return retval;
+       }
+       
+       private void reply(IMAPMessage msg, String reply) {
+               this.ps.print(msg.tag + " " + reply + "\r\n");
+       }
+       
+       private void sendState(String txt) {
+               this.ps.print("* "+txt+"\r\n");
+       }
+       
+       private static String trimQuotes(String in) {
+               if (in.length() == 0) return in;
+               if (in.charAt(0) == '"') {
+                       in = in.substring(1);
+               }
+               if (in.charAt(in.length() - 1) == '"') {
+                       in = in.substring(0, in.length() - 1);
+               }
+               return in;
+       }
+       
+       private boolean verify_auth(IMAPMessage msg) {
+               if (this.mb == null) {
+                       this.reply(msg, "NO Must be authenticated");
+                       return false;
+               }
+               return true;
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/imap/IMAPListener.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/imap/IMAPListener.java 2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/imap/IMAPListener.java 2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,31 @@
+package fnmail.imap;
+
+import java.net.ServerSocket;
+import java.io.IOException;
+
+public class IMAPListener implements Runnable {
+       private static final int LISTENPORT = 3143;
+
+       public void run() {
+               try {
+                       this.realrun();
+               } catch (IOException ioe) {
+                       System.out.println("Error in IMAP server - 
"+ioe.getMessage());
+               }
+       }
+
+       public void realrun() throws IOException {
+               ServerSocket sock = new ServerSocket(LISTENPORT);
+               
+               while (!sock.isClosed()) {
+                       try {
+                               IMAPHandler newcli = new 
IMAPHandler(sock.accept());
+                               Thread newthread = new Thread(newcli);
+                               newthread.setDaemon(true);
+                               newthread.start();
+                       } catch (IOException ioe) {
+                               
+                       }
+               }
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/imap/IMAPMessage.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/imap/IMAPMessage.java  2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/imap/IMAPMessage.java  2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,71 @@
+package fnmail.imap;
+
+import java.util.Vector;
+
+public class IMAPMessage {
+       public final String tag;
+       public final String type;
+       public final String[] args;
+
+       IMAPMessage(String raw) throws IMAPBadMessageException {
+               String[] parts = doSplit(raw, '[', ']');
+               if (parts.length < 2) {
+                       throw new IMAPBadMessageException();
+               }
+               this.tag = parts[0];
+               this.type = parts[1].toLowerCase();
+               if (parts.length > 2) {
+                       this.args = new String[parts.length - 2];
+                       System.arraycopy(parts, 2, this.args, 0, parts.length - 
2);
+               } else {
+                       this.args = null;
+               }
+       }
+       
+       // split on spaces that aren't between two square given characters
+       public static String[] doSplit(String in, char c1, char c2) {
+               boolean in_brackets = false;
+               Vector parts = new Vector();
+               StringBuffer buf = new StringBuffer("");
+               
+               for (int i = 0; i < in.length(); i++) {
+                       char c = in.charAt(i);
+                       
+                       if (c == c1) {
+                               in_brackets = true;
+                               buf.append(c);
+                       } else if (c == c2) {
+                               in_brackets = false;
+                               buf.append(c);
+                       } else if (c == ' ' && !in_brackets) {
+                               parts.add(buf.toString());
+                               buf = new StringBuffer("");
+                       } else buf.append(c);
+               }
+               
+               parts.add(buf.toString());
+               
+               String[] retval = new String[parts.size()];
+               
+               for (int i = 0; i < parts.size(); i++) {
+                       retval[i] = (String)parts.get(i);
+               }
+               
+               return retval;
+       }
+       
+       // for debugging
+       public String toString() {
+               String retval = new String("");
+               
+               retval += this.tag + " ";
+               retval += this.type;
+               
+               if (this.args == null) return retval;
+               
+               for (int i = 0; i < this.args.length; i++) {
+                       retval += " " + this.args[i];
+               }
+               return retval;
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/imap/IMAPMessageFlags.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/imap/IMAPMessageFlags.java     2006-05-04 
12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/imap/IMAPMessageFlags.java     2006-05-04 
23:27:32 UTC (rev 8604)
@@ -0,0 +1,103 @@
+package fnmail.imap;
+
+import java.util.Vector;
+
+public class IMAPMessageFlags {
+       public static final char[] allShortFlags = {
+               'S',
+               'A',
+               'F',
+               'X',
+               'D',
+               'R',
+       };
+       
+       // these should be in the same order as the last so it's possible
+       // to cross-reference
+       public static final String[] allFlags = {
+               "\\Seen",
+               "\\Answered",
+               "\\Flagged",
+               "\\Deleted",
+               "\\Draft",
+               "\\Recent",
+       };
+
+       private Vector flags;
+       
+       public IMAPMessageFlags() {
+               this.flags = new Vector();
+       }
+       
+       public IMAPMessageFlags(String shortflags) {
+               this.flags = new Vector();
+               for (int i = 0; i < allShortFlags.length; i++) {
+                       if (shortflags.indexOf(allShortFlags[i]) >= 0) {
+                               this.flags.add(allFlags[i]);
+                       }
+               }
+       }
+       
+       public void set(String flag, boolean value) {
+               flag = sanitize_flag(flag);
+               
+               if (flag == null) return;
+               
+               if (value) {
+                       this.flags.add(flag);
+               } else {
+                       this.flags.remove(flag);
+               }
+       }
+       
+       public String getShortFlagString() {
+               String retval = new String("");
+               
+               for (int i = 0; i < allFlags.length; i++) {
+                       if (this.flags.contains(allFlags[i])) {
+                               retval += allShortFlags[i];
+                       }
+               }
+               
+               return retval;
+       }
+       
+       public String getFlags() {
+               String retval = new String("");
+               
+               for (int i = 0; i < allFlags.length; i++) {
+                       if (this.flags.contains(allFlags[i])) {
+                               if (retval.length() > 0) retval += " ";
+                               retval += allFlags[i];
+                       }
+               }
+               
+               return retval;
+       }
+       
+       public void clear() {
+               this.flags.clear();
+       }
+       
+       public boolean get(String flag) {
+               flag = sanitize_flag(flag);
+               
+               if (flag == null) return false;
+               
+               if (this.flags.contains(flag)) return true;
+               return false;
+       }
+       
+       // take a flag, check it's real flag, and if so,
+       // return it in the proper capitalisation
+       private static String sanitize_flag(String flag) {
+               String realFlag = null;
+               
+               for (int i = 0; i < allFlags.length; i++) {
+                       if 
(allFlags[i].toLowerCase().equals(flag.toLowerCase())) {
+                               realFlag = allFlags[i];
+                       }
+               }
+               return realFlag;
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/smtp/SMTPBadCommandException.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/smtp/SMTPBadCommandException.java      
2006-05-04 12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/smtp/SMTPBadCommandException.java      
2006-05-04 23:27:32 UTC (rev 8604)
@@ -0,0 +1,5 @@
+package fnmail.smtp;
+
+public class SMTPBadCommandException extends Exception {
+
+}

Added: trunk/apps/fnmail/src/fnmail/smtp/SMTPCommand.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/smtp/SMTPCommand.java  2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/smtp/SMTPCommand.java  2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,48 @@
+package fnmail.smtp;
+
+import java.util.Vector;
+
+public class SMTPCommand {
+       public final String command;
+       public final String[] args;
+
+       public SMTPCommand(String line) throws SMTPBadCommandException {
+               boolean in_quotes = false;
+               Vector tmp_args = new Vector();
+               StringBuffer buf = new StringBuffer("");
+               
+               for (int i = 0; i < line.length(); i++) {
+                       char c = line.charAt(i);
+                       
+                       switch (c) {
+                               case ' ':
+                                       if (in_quotes) {
+                                               buf.append(c);
+                                       } else if (buf.length() > 0) {
+                                               tmp_args.add(buf.toString());
+                                               buf = new StringBuffer("");
+                                       }
+                                       break;
+                               case '"':
+                                       if (in_quotes)
+                                               in_quotes = false;
+                                       else
+                                               in_quotes = true;
+                                       break;
+                               default:
+                                       buf.append(c);
+                       }
+               }
+               if (buf.length() > 0) {
+                       tmp_args.add(buf.toString());
+               }
+               if (tmp_args.size() == 0) throw new SMTPBadCommandException();
+               String tmpcmd = (String)tmp_args.remove(0);
+               this.command = tmpcmd.toLowerCase();
+               this.args = new String[tmp_args.size()];
+               
+               for (int i = 0; i < tmp_args.size(); i++) {
+                       this.args[i] = (String)tmp_args.get(i);
+               }
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/smtp/SMTPHandler.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/smtp/SMTPHandler.java  2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/smtp/SMTPHandler.java  2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,269 @@
+package fnmail.smtp;
+
+import java.net.Socket;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Vector;
+
+import thirdparty.Base64Coder;
+import fnmail.AccountManager;
+import fnmail.MessageSender;
+import fnmail.utils.EmailAddress;
+
+public class SMTPHandler implements Runnable {
+       private final Socket client;
+       private final OutputStream os;
+       private final PrintStream ps;
+       private final BufferedReader bufrdr;
+       private String username;
+       private final MessageSender msgsender;
+       public static final String MY_HOSTNAME = "localhost";
+       
+       private Vector to;
+       
+       public SMTPHandler(Socket client, MessageSender sender) throws 
IOException {
+               this.client = client;
+               this.msgsender = sender;
+               this.username = null;
+               this.os = client.getOutputStream();
+               this.ps = new PrintStream(this.os);
+               this.bufrdr = new BufferedReader(new 
InputStreamReader(client.getInputStream()));
+               
+               this.to = new Vector();
+       }
+       
+       public void run() {
+               this.sendWelcome();
+               
+               String line;
+               try {
+                       while ( !this.client.isClosed() && (line = 
this.bufrdr.readLine()) != null) {
+                               SMTPCommand msg = null;
+                               try {
+                                       //System.out.println(line);
+                                       msg = new SMTPCommand(line);
+                               } catch (SMTPBadCommandException bce) {
+                                       continue;
+                               }
+                               
+                               this.dispatch(msg);
+                       }
+               
+                       this.client.close();
+               } catch (IOException ioe) {
+                       
+               }
+       }
+       
+       private void dispatch(SMTPCommand cmd) {
+               //System.out.println(cmd.toString());
+               if (cmd.command.equals("helo")) {
+                       this.handle_helo(cmd);
+               } else if (cmd.command.equals("ehlo")) {
+                       this.handle_ehlo(cmd);
+               } else if (cmd.command.equals("quit")) {
+                       this.handle_quit(cmd);
+               } else if (cmd.command.equals("turn")) {
+                       this.handle_turn(cmd);
+               } else if (cmd.command.equals("auth")) {
+                       this.handle_auth(cmd);
+               } else if (cmd.command.equals("mail")) {
+                       this.handle_mail(cmd);
+               } else if (cmd.command.equals("rcpt")) {
+                       this.handle_rcpt(cmd);
+               } else if (cmd.command.equals("data")) {
+                       this.handle_data(cmd);
+               } else if (cmd.command.equals("rset")) {
+                       this.handle_rset(cmd);
+               } else {
+                       this.ps.print("502 Unimplemented\r\n");
+               }
+       }
+       
+       private void handle_helo(SMTPCommand cmd) {
+               this.ps.print("250 "+MY_HOSTNAME+"\r\n");
+       }
+       
+       private void handle_ehlo(SMTPCommand cmd) {
+               this.ps.print("250-"+MY_HOSTNAME+"\r\n");
+               this.ps.print("250 AUTH LOGIN PLAIN \r\n");
+       }
+       
+       private void handle_quit(SMTPCommand cmd) {
+               this.ps.print("221 "+MY_HOSTNAME+"\r\n");
+               try {
+                       this.client.close();
+               } catch (IOException ioe) {
+                       
+               }
+       }
+       
+       private void handle_turn(SMTPCommand cmd) {
+               this.ps.print("502 No\r\n");
+       }
+       
+       private void handle_auth(SMTPCommand cmd) {
+               String uname;
+               String password;
+               
+               if (cmd.args.length == 0) {
+                       this.ps.print("504 No auth type given\r\n");
+                       return;
+               } else if (cmd.args[0].equalsIgnoreCase("login")) {
+                       
this.ps.print("334"+Base64Coder.encode("Username:")+"\r\n");
+                       
+                       String b64username;
+                       String b64password;
+                       try {
+                               b64username = this.bufrdr.readLine();
+                       } catch (IOException ioe) {
+                               return;
+                       }
+                       if (b64username == null) return;
+                       
+                       
this.ps.print("334"+Base64Coder.encode("Password:")+"\r\n");
+                       try {
+                               b64password = this.bufrdr.readLine();
+                       } catch (IOException ioe) {
+                               return;
+                       }
+                       if (b64password == null) return;
+                       
+                       uname = Base64Coder.decode(b64username);
+                       password = Base64Coder.decode(b64password);
+               } else if (cmd.args[0].equalsIgnoreCase("plain")) {
+                       String b64creds;
+                       
+                       if (cmd.args.length > 1) {
+                               b64creds = cmd.args[1];
+                       } else {
+                               this.ps.print("334 Credentials:\r\n");
+                               try {
+                                       b64creds = this.bufrdr.readLine();
+                                       if (b64creds == null) return;
+                               } catch (IOException ioe) {
+                                       return;
+                               }
+                       }
+                       
+                       String[] creds = 
Base64Coder.decode(b64creds).split("\0");
+                       
+                       if (creds.length < 2) return;
+                       
+                       // most documents seem to reckon you send the
+                       // username twice. Some think only once.
+                       // This will work either way.
+                       uname = creds[0];
+                       password = creds[creds.length - 1];
+               } else {
+                       this.ps.print("504 Auth type unimplemented - weren't 
you listening?\r\n");
+                       return;
+               }
+               
+               if (AccountManager.authenticate(uname, password)) {
+                       this.username = uname;
+                       
+                       this.ps.print("235 Authenticated\r\n");
+               } else {
+                       this.ps.print("535 Authentication failed\r\n");
+               }
+       }
+       
+       private void handle_mail(SMTPCommand cmd) {
+               if (this.username == null) {
+                       this.ps.print("530 Authentication required\r\n");
+                       return;
+               }
+               
+               // we don't really care.
+               this.ps.print("250 OK\r\n");
+       }
+       
+       private void handle_rcpt(SMTPCommand cmd) {
+               if (cmd.args.length < 1) {
+                       this.ps.print("504 Insufficient arguments\r\n");
+                       return;
+               }
+               
+               if (this.username == null) {
+                       this.ps.print("530 Authentication required\r\n");
+                       return;
+               }
+               
+               String[] parts = cmd.args[0].split(":", 2);
+               if (parts.length < 2) {
+                       this.ps.print("504 Can't understand that syntax\r\n");
+                       return;
+               }
+               
+               EmailAddress addr = new EmailAddress(parts[1]);
+               if (addr.user == null || addr.domain == null) {
+                       this.ps.print("504 Bad address\r\n");
+                       return;
+               }
+               
+               this.to.add(addr);
+               
+               this.ps.print("250 OK\r\n");
+       }
+       
+       private void handle_data(SMTPCommand cmd) {
+               if (this.username == null) {
+                       this.ps.print("530 Authentication required\r\n");
+                       return;
+               }
+               
+               if (this.to.size() == 0) {
+                       this.ps.print("503 RCPT first\r\n");
+                       return;
+               }
+               
+               try {
+                       File tempfile = File.createTempFile("fnmail-", 
".message");
+                       PrintWriter pw = new PrintWriter(new 
FileOutputStream(tempfile));
+                       
+                       this.ps.print("354 Go crazy\r\n");
+                       
+                       String line;
+                       boolean done = false;
+                       while ( (line = this.bufrdr.readLine()) != null) {
+                               if (line.equals(".")) {
+                                       done = true;
+                                       break;
+                               }
+                               pw.print(line+"\r\n");
+                       }
+                       
+                       pw.close();
+                       if (!done) {
+                               // connection closed before the message was
+                               // finished. bail out.
+                               tempfile.delete();
+                               return;
+                       }
+                       
+                       this.msgsender.send_message(this.username, to, 
tempfile);
+                       
+                       tempfile.delete();
+                       
+                       this.ps.print("250 So be it\r\n");
+               } catch (IOException ioe) {
+                       this.ps.print("452 Can't store message\r\n");
+               }
+       }
+       
+       private void handle_rset(SMTPCommand cmd) {
+               this.to.clear();
+               this.ps.print("250 Reset\r\n");
+       }
+       
+       private void sendWelcome() {
+               this.ps.print("220 "+MY_HOSTNAME+" ready\r\n");
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/smtp/SMTPListener.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/smtp/SMTPListener.java 2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/fnmail/smtp/SMTPListener.java 2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,38 @@
+package fnmail.smtp;
+
+import java.net.ServerSocket;
+import java.io.IOException;
+
+import fnmail.MessageSender;
+
+public class SMTPListener implements Runnable {
+       private static final int LISTENPORT = 3025;
+       private final MessageSender msgsender;
+       
+       public SMTPListener(MessageSender sender) {
+               this.msgsender = sender;
+       }
+       
+       public void run() {
+               try {
+                       this.realrun();
+               } catch (IOException ioe) {
+                       System.out.println("Error in SMTP server - 
"+ioe.getMessage());
+               }
+       }
+       
+       public void realrun() throws IOException {
+               ServerSocket sock = new ServerSocket(LISTENPORT);
+               
+               while (!sock.isClosed()) {
+                       try {
+                               SMTPHandler newcli = new 
SMTPHandler(sock.accept(), this.msgsender);
+                               Thread newthread = new Thread(newcli);
+                               newthread.setDaemon(true);
+                               newthread.start();
+                       } catch (IOException ioe) {
+                               
+                       }
+               }
+       }
+}

Added: trunk/apps/fnmail/src/fnmail/utils/EmailAddress.java
===================================================================
--- trunk/apps/fnmail/src/fnmail/utils/EmailAddress.java        2006-05-04 
12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/fnmail/utils/EmailAddress.java        2006-05-04 
23:27:32 UTC (rev 8604)
@@ -0,0 +1,47 @@
+package fnmail.utils;
+
+public class EmailAddress {
+       public String realname;
+       public String user;
+       public String domain;
+       
+       public EmailAddress(String address) {
+               this.realname = null;
+               this.user = null;
+               this.domain = null;
+               
+               StringBuffer bank = new StringBuffer("");
+               for (int i = 0; i < address.length(); i++) {
+                       char c = address.charAt(i);
+                       
+                       switch (c) {
+                               case '@':
+                                       this.user = bank.toString();
+                                       bank = new StringBuffer("");
+                                       break;
+                               case '<':
+                                       this.realname = bank.toString();
+                                       bank = new StringBuffer("");
+                                       break;
+                               case '>':
+                                       this.domain = bank.toString();
+                                       bank = new StringBuffer("");
+                                       break;
+                               case '(':
+                                       this.domain = bank.toString();
+                                       bank = new StringBuffer("");
+                                       break;
+                               case ')':
+                                       this.realname = bank.toString();
+                                       bank = new StringBuffer("");
+                                       break;
+                               default:
+                                       bank.append(c);
+                       }
+               }
+               
+               if (this.realname == null && this.domain == null) {
+                       this.domain = bank.toString();
+               }
+       }
+}

Added: trunk/apps/fnmail/src/freenet/support/io/LineReader.java
===================================================================
--- trunk/apps/fnmail/src/freenet/support/io/LineReader.java    2006-05-04 
12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/freenet/support/io/LineReader.java    2006-05-04 
23:27:32 UTC (rev 8604)
@@ -0,0 +1,9 @@
+package freenet.support.io;
+
+import java.io.IOException;
+
+public interface LineReader {
+
+       public String readLine(int maxLength, int bufferSize) throws 
IOException;
+       
+}

Added: trunk/apps/fnmail/src/freenet/support/io/LineReadingInputStream.java
===================================================================
--- trunk/apps/fnmail/src/freenet/support/io/LineReadingInputStream.java        
2006-05-04 12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/freenet/support/io/LineReadingInputStream.java        
2006-05-04 23:27:32 UTC (rev 8604)
@@ -0,0 +1,47 @@
+package freenet.support.io;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A FilterInputStream which provides readLine().
+ */
+public class LineReadingInputStream extends FilterInputStream implements 
LineReader {
+       private int lastBytesRead;
+
+       public LineReadingInputStream(InputStream in) {
+               super(in);
+       }
+
+       /**
+        * Read a line of US-ASCII. Used for e.g. HTTP. @return Null if end of 
file.
+        */
+       public String readLine(int maxLength, int bufferSize) throws 
IOException {
+               StringBuffer sb = new StringBuffer(bufferSize);
+               this.lastBytesRead = 0;
+               while(true) {
+                       int x = read();
+                       this.lastBytesRead++;
+                       if(x == -1) {
+                               if(sb.length() == 0) return null;
+                               return sb.toString();
+                       }
+                       char c = (char) x;
+                       if(c == '\n') {
+                               if(sb.length() > 0) {
+                                       if(sb.charAt(sb.length()-1) == '\r')
+                                               sb.setLength(sb.length()-1);
+                               }
+                               return sb.toString();
+                       }
+                       sb.append(c);
+                       if(sb.length() >= maxLength)
+                               throw new TooLongException();
+               }
+       }
+       
+       public int getLastBytesRead() {
+               return this.lastBytesRead;
+       }
+}

Added: trunk/apps/fnmail/src/freenet/support/io/TooLongException.java
===================================================================
--- trunk/apps/fnmail/src/freenet/support/io/TooLongException.java      
2006-05-04 12:51:55 UTC (rev 8603)
+++ trunk/apps/fnmail/src/freenet/support/io/TooLongException.java      
2006-05-04 23:27:32 UTC (rev 8604)
@@ -0,0 +1,8 @@
+package freenet.support.io;
+
+import java.io.IOException;
+
+/** Exception thrown by a LineReadingInputStream when a line is too long. */
+public class TooLongException extends IOException {
+
+}
\ No newline at end of file

Added: trunk/apps/fnmail/src/thirdparty/Base64Coder.java
===================================================================
--- trunk/apps/fnmail/src/thirdparty/Base64Coder.java   2006-05-04 12:51:55 UTC 
(rev 8603)
+++ trunk/apps/fnmail/src/thirdparty/Base64Coder.java   2006-05-04 23:27:32 UTC 
(rev 8604)
@@ -0,0 +1,121 @@
+/**************************************************************************
+*
+* A Base64 Encoder/Decoder.
+*
+* This class is used to encode and decode data in Base64 format
+* as described in RFC 1521.
+*
+* <p>
+* Copyright 2003: Christian d'Heureuse, Inventec Informatik AG, 
Switzerland.<br>
+* License: This is "Open Source" software and released under the <a 
href="http://www.gnu.org/licenses/lgpl.html"; target="_top">GNU/LGPL</a> license.
+* It is provided "as is" without warranty of any kind. Please contact the 
author for other licensing arrangements.<br>
+* Home page: <a href="http://www.source-code.biz"; 
target="_top">www.source-code.biz</a><br>
+*
+* <p>
+* Version history:<br>
+* 2003-07-22 Christian d'Heureuse (chdh): Module created.<br>
+* 2005-08-11 chdh: Lincense changed from GPL to LGPL.
+*
+**************************************************************************/
+
+package thirdparty;
+
+public class Base64Coder {
+
+// Mapping table from 6-bit nibbles to Base64 characters.
+private static char[]    map1 = new char[64];
+   static {
+      int i=0;
+      for (char c='A'; c<='Z'; c++) map1[i++] = c;
+      for (char c='a'; c<='z'; c++) map1[i++] = c;
+      for (char c='0'; c<='9'; c++) map1[i++] = c;
+      map1[i++] = '+'; map1[i++] = '/'; }
+
+// Mapping table from Base64 characters to 6-bit nibbles.
+private static byte[]    map2 = new byte[128];
+   static {
+      for (int i=0; i<map2.length; i++) map2[i] = -1;
+      for (int i=0; i<64; i++) map2[map1[i]] = (byte)i; }
+
+/**
+* Encodes a string into Base64 format.
+* No blanks or line breaks are inserted.
+* @param s  a String to be encoded.
+* @return   A String with the Base64 encoded data.
+*/
+public static String encode (String s) {
+   return new String(encode(s.getBytes())); }
+
+/**
+* Encodes a byte array into Base64 format.
+* No blanks or line breaks are inserted.
+* @param in  an array containing the data bytes to be encoded.
+* @return    A character array with the Base64 encoded data.
+*/
+public static char[] encode (byte[] in) {
+   int iLen = in.length;
+   int oDataLen = (iLen*4+2)/3;       // output length without padding
+   int oLen = ((iLen+2)/3)*4;         // output length including padding
+   char[] out = new char[oLen];
+   int ip = 0;
+   int op = 0;
+   while (ip < iLen) {
+      int i0 = in[ip++] & 0xff;
+      int i1 = ip < iLen ? in[ip++] & 0xff : 0;
+      int i2 = ip < iLen ? in[ip++] & 0xff : 0;
+      int o0 = i0 >>> 2;
+      int o1 = ((i0 &   3) << 4) | (i1 >>> 4);
+      int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
+      int o3 = i2 & 0x3F;
+      out[op++] = map1[o0];
+      out[op++] = map1[o1];
+      out[op] = op < oDataLen ? map1[o2] : '='; op++;
+      out[op] = op < oDataLen ? map1[o3] : '='; op++; }
+   return out; }
+
+/**
+* Decodes a Base64 string.
+* @param s  a Base64 String to be decoded.
+* @return   A String containing the decoded data.
+* @throws   IllegalArgumentException if the input is not valid Base64 encoded 
data.
+*/
+public static String decode (String s) {
+   return new String(decode(s.toCharArray())); }
+
+/**
+* Decodes Base64 data.
+* No blanks or line breaks are allowed within the Base64 encoded data.
+* @param in  a character array containing the Base64 encoded data.
+* @return    An array containing the decoded data bytes.
+* @throws    IllegalArgumentException if the input is not valid Base64 encoded 
data.
+*/
+public static byte[] decode (char[] in) {
+   int iLen = in.length;
+   if (iLen%4 != 0) throw new IllegalArgumentException ("Length of Base64 
encoded input string is not a multiple of 4.");
+   while (iLen > 0 && in[iLen-1] == '=') iLen--;
+   int oLen = (iLen*3) / 4;
+   byte[] out = new byte[oLen];
+   int ip = 0;
+   int op = 0;
+   while (ip < iLen) {
+      int i0 = in[ip++];
+      int i1 = in[ip++];
+      int i2 = ip < iLen ? in[ip++] : 'A';
+      int i3 = ip < iLen ? in[ip++] : 'A';
+      if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127)
+         throw new IllegalArgumentException ("Illegal character in Base64 
encoded data.");
+      int b0 = map2[i0];
+      int b1 = map2[i1];
+      int b2 = map2[i2];
+      int b3 = map2[i3];
+      if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0)
+         throw new IllegalArgumentException ("Illegal character in Base64 
encoded data.");
+      int o0 = ( b0       <<2) | (b1>>>4);
+      int o1 = ((b1 & 0xf)<<4) | (b2>>>2);
+      int o2 = ((b2 &   3)<<6) |  b3;
+      out[op++] = (byte)o0;
+      if (op<oLen) out[op++] = (byte)o1;
+      if (op<oLen) out[op++] = (byte)o2; }
+   return out; }
+
+}


Reply via email to