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; } + +}
