Author: dbkr
Date: 2006-08-12 23:34:17 +0000 (Sat, 12 Aug 2006)
New Revision: 10055
Modified:
trunk/apps/Freemail/src/freemail/AccountManager.java
trunk/apps/Freemail/src/freemail/Freemail.java
trunk/apps/Freemail/src/freemail/MailMessage.java
trunk/apps/Freemail/src/freemail/MessageBank.java
trunk/apps/Freemail/src/freemail/imap/IMAPHandler.java
Log:
Build #7: Support for IMAP folders / subfolders
Modified: trunk/apps/Freemail/src/freemail/AccountManager.java
===================================================================
--- trunk/apps/Freemail/src/freemail/AccountManager.java 2006-08-12
23:07:53 UTC (rev 10054)
+++ trunk/apps/Freemail/src/freemail/AccountManager.java 2006-08-12
23:34:17 UTC (rev 10055)
@@ -51,7 +51,6 @@
File accountdir = new File(DATADIR, username);
if (!accountdir.mkdir()) throw new IOException("Failed to
create directory "+username+" in "+DATADIR);
- PropsFile accfile = getAccountFile(accountdir);
putWelcomeMessage(username, getFreemailAddress(accountdir));
}
Modified: trunk/apps/Freemail/src/freemail/Freemail.java
===================================================================
--- trunk/apps/Freemail/src/freemail/Freemail.java 2006-08-12 23:07:53 UTC
(rev 10054)
+++ trunk/apps/Freemail/src/freemail/Freemail.java 2006-08-12 23:34:17 UTC
(rev 10055)
@@ -12,7 +12,7 @@
// version info
public static final int VER_MAJOR = 0;
public static final int VER_MINOR = 1;
- public static final int BUILD_NO = 6;
+ public static final int BUILD_NO = 7;
public static final String VERSION_TAG = "Pet Shop";
private static final String TEMPDIRNAME = "temp";
Modified: trunk/apps/Freemail/src/freemail/MailMessage.java
===================================================================
--- trunk/apps/Freemail/src/freemail/MailMessage.java 2006-08-12 23:07:53 UTC
(rev 10054)
+++ trunk/apps/Freemail/src/freemail/MailMessage.java 2006-08-12 23:34:17 UTC
(rev 10055)
@@ -19,7 +19,7 @@
private PrintStream ps;
private final Vector headers;
private BufferedReader brdr;
- public final IMAPMessageFlags flags;
+ public IMAPMessageFlags flags;
MailMessage(File f) {
this.file = f;
@@ -150,6 +150,13 @@
return this.ps;
}
+ public PrintStream getRawStream() throws FileNotFoundException {
+ this.os = new FileOutputStream(this.file);
+ this.ps = new PrintStream(this.os);
+
+ return this.ps;
+ }
+
public void commit() {
try {
this.os.close();
@@ -247,6 +254,25 @@
return this.brdr.readLine();
}
+ public boolean copyTo(MailMessage msg) {
+ this.closeStream();
+ String line;
+ try {
+ PrintStream copyps = msg.getRawStream();
+ while ( (line = this.readLine()) != null) {
+ copyps.println(line);
+ }
+ msg.commit();
+ } catch (IOException ioe) {
+ msg.cancel();
+ return false;
+ }
+
+ msg.flags = this.flags;
+ msg.storeFlags();
+ return true;
+ }
+
// 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!
Modified: trunk/apps/Freemail/src/freemail/MessageBank.java
===================================================================
--- trunk/apps/Freemail/src/freemail/MessageBank.java 2006-08-12 23:07:53 UTC
(rev 10054)
+++ trunk/apps/Freemail/src/freemail/MessageBank.java 2006-08-12 23:34:17 UTC
(rev 10055)
@@ -9,6 +9,8 @@
import java.io.PrintStream;
import java.util.TreeMap;
import java.util.SortedMap;
+import java.util.Vector;
+import java.util.Enumeration;
public class MessageBank {
private static final String MESSAGES_DIR = "inbox";
@@ -25,6 +27,46 @@
}
}
+ private MessageBank(File d) {
+ this.dir = d;
+ }
+
+ public String getName() {
+ return this.dir.getName();
+ }
+
+ public String getFolderFlagsString() {
+ StringBuffer retval = new StringBuffer("(");
+
+ if (this.listSubFolders().length > 0) {
+ retval.append("\\HasChildren");
+ } else {
+ retval.append("\\HasNoChildren");
+ }
+
+ retval.append(")");
+ return retval.toString();
+ }
+
+ public boolean delete() {
+ File[] files = this.dir.listFiles();
+
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].getName().equals(".")) continue;
+ if (files[i].getName().equals(NIDFILE)) continue;
+ if (files[i].getName().equals("..")) continue;
+
+ // this method should will fail if there are directories
+ // here. It should never be called if this is the case.
+ if (!files[i].delete()) return false;
+ }
+
+ // rename it with a dot in front - we need to preserve the UIDs
+ File newdir = new File(this.dir.getParent(),
"."+this.dir.getName());
+
+ return this.dir.renameTo(newdir);
+ }
+
public MailMessage createMessage() {
long newid = this.nextId();
File newfile;
@@ -54,6 +96,7 @@
for (int i = 0; i < files.length; i++) {
if (files[i].getName().startsWith(".")) continue;
+ if (files[i].isDirectory()) continue;
MailMessage msg = new MailMessage(files[i]);
@@ -79,6 +122,60 @@
return msgs;
}
+ public MessageBank getSubFolder(String name) {
+ if (!name.matches("[\\w_]*")) return null;
+
+ File targetdir = new File(this.dir, name);
+ return new MessageBank(targetdir);
+ }
+
+ public MessageBank makeSubFolder(String name) {
+ if (!name.matches("[\\w_]*")) return null;
+
+ File targetdir = new File(this.dir, name);
+
+ // is there a 'deleted' instance of this folder?
+ File ghostdir = new File(this.dir, "."+name);
+ if (ghostdir.exists()) {
+ if (!ghostdir.renameTo(targetdir)) {
+ return null;
+ }
+ return new MessageBank(ghostdir);
+ }
+
+ if (targetdir.exists()) {
+ return null;
+ }
+
+ if (targetdir.mkdir()) {
+ return new MessageBank(targetdir);
+ }
+ return null;
+ }
+
+ public MessageBank[] listSubFolders() {
+ File[] files = this.dir.listFiles();
+ Vector subfolders = new Vector();
+
+ for (int i = 0; i < files.length; i++) {
+ if (files[i].getName().startsWith(".")) continue;
+
+ if (files[i].isDirectory()) {
+ subfolders.add(files[i]);
+ }
+ }
+
+ MessageBank[] retval = new MessageBank[subfolders.size()];
+
+ Enumeration e = subfolders.elements();
+ int i = 0;
+ while (e.hasMoreElements()) {
+ retval[i] = new MessageBank((File)e.nextElement());
+ i++;
+ }
+ return retval;
+ }
+
private long nextId() {
File nidfile = new File(this.dir, NIDFILE);
long retval;
Modified: trunk/apps/Freemail/src/freemail/imap/IMAPHandler.java
===================================================================
--- trunk/apps/Freemail/src/freemail/imap/IMAPHandler.java 2006-08-12
23:07:53 UTC (rev 10054)
+++ trunk/apps/Freemail/src/freemail/imap/IMAPHandler.java 2006-08-12
23:34:17 UTC (rev 10055)
@@ -17,13 +17,14 @@
import freemail.utils.EmailAddress;
public class IMAPHandler implements Runnable {
- private static final String CAPABILITY = "IMAP4rev1 AUTH=LOGIN";
+ private static final String CAPABILITY = "IMAP4rev1 AUTH=LOGIN CHILDREN
NAMESPACE";
- final Socket client;
- final OutputStream os;
- final PrintStream ps;
- final BufferedReader bufrdr;
- MessageBank mb;
+ private final Socket client;
+ private final OutputStream os;
+ private final PrintStream ps;
+ private final BufferedReader bufrdr;
+ private MessageBank mb;
+ private MessageBank inbox;
IMAPHandler(Socket client) throws IOException {
@@ -90,6 +91,14 @@
this.handle_lsub(msg);
} else if (msg.type.equals("status")) {
this.handle_status(msg);
+ } else if (msg.type.equals("create")) {
+ this.handle_create(msg);
+ } else if (msg.type.equals("delete")) {
+ this.handle_delete(msg);
+ } else if (msg.type.equals("copy")) {
+ this.handle_copy(msg);
+ } else if (msg.type.equals("append")) {
+ this.handle_append(msg);
} else {
this.reply(msg, "NO Sorry - not implemented");
}
@@ -98,7 +107,7 @@
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.inbox = new MessageBank(trimQuotes(msg.args[0]));
this.reply(msg, "OK Logged in");
} else {
@@ -149,22 +158,69 @@
replyprefix = "LSUB";
}
- refname = trimQuotes(refname);
+ if (refname != null) refname = trimQuotes(refname);
if (refname.length() == 0) refname = null;
- mbname = trimQuotes(mbname);
+ if (mbname != null) mbname = trimQuotes(mbname);
if (mbname.length() == 0) mbname = null;
if (mbname == null) {
// return hierarchy delimiter
this.sendState(replyprefix+" (\\Noselect) \".\" \"\"");
- } else if (mbname.equals("%") || mbname.equals("INBOX") ||
mbname.equals("*") || mbname.equals("INBOX*")) {
- this.sendState(replyprefix+" (\\NoInferiors) \".\"
\"INBOX\"");
+ } else {
+ // transform mailbox name into a regex
+
+ // '*' needs to be '.*'
+ mbname = mbname.replace("*", ".*");
+
+ // and % is a wildcard not inclusing the hierarchy
delimiter
+ mbname = mbname.replace("%", "[^\\.]*");
+
+
+ this.list_matching_folders(this.inbox, mbname,
replyprefix, "INBOX.");
+
+ /// and send the inbox too, if it matches
+ if ("INBOX".matches(mbname)) {
+ this.sendState(replyprefix+"
"+this.inbox.getFolderFlagsString()+" \".\" \"INBOX\"");
+ }
}
this.reply(msg, "OK "+replyprefix+" completed");
}
+ private void list_matching_folders(MessageBank folder, String pattern,
String replyprefix, String folderpath) {
+ MessageBank[] folders = folder.listSubFolders();
+
+ for (int i = 0; i < folders.length; i++) {
+ String fullpath = folderpath+folders[i].getName();
+
+ this.list_matching_folders(folders[i], pattern,
replyprefix, fullpath+".");
+ if (fullpath.matches(pattern)) {
+ this.sendState(replyprefix+"
"+folders[i].getFolderFlagsString()+" \".\" \""+fullpath+"\"");
+ }
+ }
+ }
+
+ private MessageBank getMailboxFromPath(String path) {
+ MessageBank tempmb = this.inbox;
+
+ String[] mbparts = path.split("\\.");
+
+ if (!mbparts[0].equalsIgnoreCase("inbox")) {
+ return null;
+ }
+
+ int i;
+ for (i = 1; i < mbparts.length; i++) {
+ tempmb = tempmb.getSubFolder(mbparts[i]);
+ if (tempmb == null) {
+ return null;
+ }
+ }
+
+ return tempmb;
+ }
+
private void handle_select(IMAPMessage msg) {
String mbname;
@@ -177,39 +233,44 @@
return;
}
- mbname = trimQuotes(msg.args[0]).toLowerCase();
+ mbname = trimQuotes(msg.args[0]);
- if (mbname.equals("inbox")) {
- this.sendState("FLAGS
("+IMAPMessageFlags.getAllFlagsAsString()+")");
- this.sendState("OK [PERMANENTFLAGS (\\*
"+IMAPMessageFlags.getPermanentFlagsAsString()+")] Limited");
+ MessageBank tempmb = this.getMailboxFromPath(mbname);
+
+ if (tempmb == null) {
+ this.reply(msg, "NO No such mailbox");
+ return;
+ } else {
+ this.mb = tempmb;
+ }
+
+ this.sendState("FLAGS
("+IMAPMessageFlags.getAllFlagsAsString()+")");
+ this.sendState("OK [PERMANENTFLAGS (\\*
"+IMAPMessageFlags.getPermanentFlagsAsString()+")] Limited");
- SortedMap msgs = this.mb.listMessages();
+ 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());
+ 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++;
+ // 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();
+ // remove the recent flag
+ m.flags.set("\\Recent", false);
+ m.storeFlags();
- msgs = msgs.tailMap(new
Integer(current.intValue()+1));
- }
+ msgs = msgs.tailMap(new Integer(current.intValue()+1));
+ }
- this.sendState(numexists+" EXISTS");
- this.sendState(numrecent+" RECENT");
+ this.sendState(numexists+" EXISTS");
+ this.sendState(numrecent+" RECENT");
- this.sendState("OK [UIDVALIDITY 1] Ok");
+ this.sendState("OK [UIDVALIDITY 1] Ok");
- this.reply(msg, "OK [READ-WRITE] Done");
- } else {
- this.reply(msg, "NO No such mailbox");
- }
+ this.reply(msg, "OK [READ-WRITE] Done");
}
private void handle_noop(IMAPMessage msg) {
@@ -224,6 +285,11 @@
return;
}
+ if (this.mb == null) {
+ this.reply(msg, "NO No mailbox selected");
+ return;
+ }
+
SortedMap msgs = this.mb.listMessages();
if (msgs.size() == 0) {
@@ -293,6 +359,11 @@
return;
}
+ if (this.mb == null) {
+ this.reply(msg, "NO No mailbox selected");
+ return;
+ }
+
SortedMap msgs = this.mb.listMessages();
if (msgs.size() == 0) {
@@ -327,7 +398,7 @@
}
int msgnum = 1;
- if (msg.args[0].toLowerCase().equals("fetch")) {
+ if (msg.args[0].equalsIgnoreCase("fetch")) {
int oldsize = msgs.size();
msgs = msgs.tailMap(new Integer(from));
msgnum += (oldsize - msgs.size());
@@ -347,7 +418,7 @@
}
this.reply(msg, "OK Fetch completed");
- } else if (msg.args[0].toLowerCase().equals("store")) {
+ } else if (msg.args[0].equalsIgnoreCase("store")) {
msgs = msgs.tailMap(new Integer(from));
msgs = msgs.headMap(new Integer(to + 1));
@@ -360,6 +431,43 @@
this.do_store(msg.args, 2, targetmsgs, msg, -1);
this.reply(msg, "OK Store completed");
+ } else if (msg.args[0].equalsIgnoreCase("copy")) {
+ msgs = msgs.tailMap(new Integer(from));
+
+ if (msg.args.length < 3) {
+ this.reply(msg, "BAD Not enough arguments");
+ return;
+ }
+
+ MessageBank target =
getMailboxFromPath(trimQuotes(msg.args[2]));
+ if (target == null) {
+ this.reply(msg, "NO [TRYCREATE] No such
mailbox.");
+ return;
+ }
+
+ int copied = 0;
+
+ while (msgs.size() > 0) {
+ Integer curuid = (Integer)msgs.firstKey();
+ if (curuid.intValue() > to) {
+ break;
+ }
+
+ MailMessage srcmsg =
(MailMessage)msgs.get(msgs.firstKey());
+
+ MailMessage copymsg = target.createMessage();
+ srcmsg.copyTo(copymsg);
+
+ copied++;
+
+ msgs = msgs.tailMap(new
Integer(curuid.intValue()+1));
+ msgnum++;
+ }
+
+ if (copied > 0)
+ this.reply(msg, "OK COPY completed");
+ else
+ this.reply(msg, "NO No messages copied");
} else {
this.reply(msg, "BAD Unknown command");
}
@@ -564,6 +672,11 @@
return;
}
+ if (this.mb == null) {
+ this.reply(msg, "NO No mailbox selected");
+ return;
+ }
+
String rangeparts[] = msg.args[0].split(":");
Object[] allmsgs = this.mb.listMessages().values().toArray();
@@ -666,28 +779,39 @@
}
private void handle_expunge(IMAPMessage msg) {
- MailMessage[] mmsgs = this.mb.listMessagesArray();
+ if (this.mb == null) {
+ this.reply(msg, "NO No mailbox selected");
+ return;
+ }
- for (int i = 0; i < mmsgs.length; i++) {
- if (mmsgs[i].flags.get("\\Deleted"))
- mmsgs[i].delete();
- this.sendState(i+" EXPUNGE");
+ this.expunge(true);
+ this.reply(msg, "OK Expunge complete");
+ }
+
+ private void handle_close(IMAPMessage msg) {
+ if (this.mb == null) {
+ this.reply(msg, "NO No mailbox selected");
+ return;
}
+
+ this.expunge(false);
+ this.mb = null;
+
this.reply(msg, "OK Mailbox closed");
}
- private void handle_close(IMAPMessage msg) {
+ private void expunge(boolean verbose) {
MailMessage[] mmsgs = this.mb.listMessagesArray();
for (int i = 0; i < mmsgs.length; i++) {
if (mmsgs[i].flags.get("\\Deleted"))
mmsgs[i].delete();
+ if (verbose) this.sendState(i+" EXPUNGE");
}
- this.reply(msg, "OK Mailbox closed");
}
private void handle_namespace(IMAPMessage msg) {
- this.sendState("NAMESPACE ((\"\" \"/\")) NIL NIL");
+ this.sendState("NAMESPACE ((\"INBOX.\" \".\")) NIL NIL");
this.reply(msg, "OK Namespace completed");
}
@@ -768,6 +892,198 @@
this.reply(msg, "OK STATUS completed");
}
+ private void handle_create(IMAPMessage msg) {
+ if (!this.verify_auth(msg)) {
+ return;
+ }
+
+ if (msg.args.length < 1) {
+ this.reply(msg, "NO Not enough arguments");
+ return;
+ }
+
+ msg.args[0] = trimQuotes(msg.args[0]);
+
+ if (msg.args[0].endsWith(".")) {
+ // ends with a hierarchy delimiter. Ignore it
+ this.reply(msg, "OK Nothing done");
+ return;
+ }
+
+ String[] mbparts = msg.args[0].split("\\.");
+ if (!mbparts[0].equalsIgnoreCase("inbox")) {
+ this.reply(msg, "NO Invalid mailbox name");
+ return;
+ }
+
+ if (mbparts.length < 2) {
+ this.reply(msg, "NO Inbox already exists!");
+ return;
+ }
+
+ int i;
+ MessageBank tempmb = this.inbox;
+ for (i = 1; i < mbparts.length; i++) {
+ tempmb = tempmb.makeSubFolder(mbparts[i]);
+ if (tempmb == null) {
+ this.reply(msg, "NO couldn't create mailbox");
+ return;
+ }
+ }
+ this.reply(msg, "OK Mailbox created");
+ }
+
+ private void handle_delete(IMAPMessage msg) {
+ if (!this.verify_auth(msg)) {
+ return;
+ }
+
+ if (msg.args.length < 1) {
+ this.reply(msg, "NO Not enough arguments");
+ return;
+ }
+
+ MessageBank target =
getMailboxFromPath(trimQuotes(msg.args[0]));
+ if (target == null) {
+ this.reply(msg, "NO No such mailbox.");
+ return;
+ }
+
+ if (target.listSubFolders().length > 0) {
+ this.reply(msg, "NO Mailbox has inferiors.");
+ return;
+ }
+
+ if (target.delete()) {
+ this.reply(msg, "OK Mailbox deleted");
+ } else {
+ this.reply(msg, "NO Unable to delete mailbox");
+ }
+ return;
+ }
+
+ private void handle_copy(IMAPMessage msg) {
+ if (!this.verify_auth(msg)) {
+ return;
+ }
+
+ if (this.mb == null) {
+ this.reply(msg, "NO No mailbox selected");
+ return;
+ }
+
+ if (msg.args.length < 2) {
+ this.reply(msg, "NO Not enough arguments");
+ return;
+ }
+
+ Object[] allmsgs = this.mb.listMessages().values().toArray();
+ String rangeparts[] = msg.args[0].split(":");
+
+ 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;
+ }
+
+ // convert to zero based array
+ from--;
+ to--;
+
+ if (from < 0 || to < 0 || from > allmsgs.length || to >
allmsgs.length) {
+ this.reply(msg, "NO No such message");
+ return;
+ }
+
+ MessageBank target =
getMailboxFromPath(trimQuotes(msg.args[1]));
+ if (target == null) {
+ this.reply(msg, "NO [TRYCREATE] No such mailbox.");
+ return;
+ }
+
+ for (int i = from; i <= to; i++) {
+ MailMessage src = (MailMessage)allmsgs[i];
+ MailMessage copy = target.createMessage();
+
+ src.copyTo(copy);
+ }
+ this.reply(msg, "OK COPY completed");
+ }
+
+ private void handle_append(IMAPMessage msg) {
+ if (msg.args.length < 3) {
+ this.reply(msg, "NO Not enough arguments");
+ return;
+ }
+
+ String mbname = trimQuotes(msg.args[0]);
+ String sflags = msg.args[1];
+ if (sflags.startsWith("(")) sflags = sflags.substring(1);
+ if (sflags.endsWith(")")) sflags = sflags.substring(0,
sflags.length() - 1);
+ String sdatalen = msg.args[2];
+ if (sdatalen.startsWith("{")) sdatalen = sdatalen.substring(1);
+ if (sdatalen.endsWith("}")) sdatalen = sdatalen.substring(0,
sdatalen.length() - 1);
+ int datalen;
+ try {
+ datalen = Integer.parseInt(sdatalen);
+ } catch (NumberFormatException nfe) {
+ datalen = 0;
+ }
+
+ MessageBank destmb = this.getMailboxFromPath(mbname);
+ if (destmb == null) {
+ this.reply(msg, "NO [TRYCREATE] No such mailbox");
+ return;
+ }
+
+ MailMessage newmsg = destmb.createMessage();
+ this.ps.print("+ OK\r\n");
+ try {
+ PrintStream msgps = newmsg.getRawStream();
+
+ String line;
+ int bytesread = 0;
+ while ( (line = this.bufrdr.readLine()) != null) {
+ msgps.println(line);
+
+ bytesread += line.getBytes().length;
+ bytesread += "\r\n".length();
+
+ if (bytesread >= datalen) break;
+ }
+
+ newmsg.commit();
+ } catch (IOException ioe) {
+ this.reply(msg, "NO Failed to write message");
+ newmsg.cancel();
+ return;
+ }
+
+ String[] flags = sflags.split(" ");
+ for (int i = 0; i < flags.length; i++) {
+ newmsg.flags.set(flags[i], true);
+ }
+ newmsg.storeFlags();
+ this.reply(msg, "OK APPEND completed");
+ }
+
private String getEnvelope(MailMessage mmsg) {
StringBuffer buf = new StringBuffer("(");
@@ -837,7 +1153,7 @@
}
private boolean verify_auth(IMAPMessage msg) {
- if (this.mb == null) {
+ if (this.inbox == null) {
this.reply(msg, "NO Must be authenticated");
return false;
}