massive refactoring of Headers class
authoradam <adam@megacz.com>
Sat, 17 Mar 2007 10:32:59 +0000 (10:32 +0000)
committeradam <adam@megacz.com>
Sat, 17 Mar 2007 10:32:59 +0000 (10:32 +0000)
darcs-hash:20070317103259-5007d-b5bd837abc764f72dd1daeb59254241673830e8e.gz

src/org/ibex/mail/Headers.java
src/org/ibex/mail/MIME.java
src/org/ibex/mail/MailingList.java
src/org/ibex/mail/Message.java
src/org/ibex/mail/SMTP.java

index 2225919..2d2f2ba 100644 (file)
@@ -14,110 +14,112 @@ import java.util.*;
 import java.net.*;
 import java.io.*;
 
-// FIXME: this is important: folded headers: can insert CRLF anywhere that whitespace appears (before the whitespace)
-public abstract class Headers extends JS.Immutable implements Fountain {
+// FEATURE: construct hash lazily?
+public class Headers extends JS.Immutable implements Fountain {
+   
+    /**
+     *  constructs a new set of Headers based on a preexisting set --
+     *  keyval's even-numbered elements are keys, odd elements are
+     *  values -- a null value deletes, non-null value replaces
+     */
+    public Headers(Headers old, String[] keyval) { this(old.updateHeaders(keyval), false); }
+
+    public Headers(Fountain fountain) throws Malformed { this(fountain, false); }
+    public Headers(Fountain fountain, boolean assumeMime) throws Malformed { this(extractEntries(fountain), assumeMime); }
+
+
+    // public //////////////////////////////////////////////////////////////////////////////
+
+    public String[] getHeaderNames()       { return (String[])head.dumpkeys(new String[head.size()]); }
+    public String   get(String s)          { return (String)head.get(s.toLowerCase()); }
+    public JS       get(JS s) throws JSExn { return JSU.S(get(JSU.toString(s).toLowerCase())); }
+
+    public Stream getStream()   { return fountain().getStream(); }
+    public long   getLength()   { return fountain().getLength(); }
+    public int    getNumLines() { return fountain().getNumLines(); }
+
 
-    public Headers set(String k, String v) {
-        Stream stream = getStream();
-        StringBuffer all = new StringBuffer();
-        int lines = 0;
+    // private //////////////////////////////////////////////////////////////////////////////
+
+    private final Hash head = new Hash();
+    private final boolean mime;
+    private final Entry[] entries;
+    private       Fountain fountain = null;
+
+    private synchronized Fountain fountain() {  // lazily constructed
+        if (fountain == null) {
+            StringBuffer sb = new StringBuffer();
+            for(Entry e : entries)
+                sb.append(e.toString());
+            this.fountain = Fountain.Util.create(sb.toString());
+        }
+        return fountain;
+    }
+
+    private static class Entry {
+        public final String key;
+        public final String val;
+        public String toString() { return key+":"+val; }
+        public Entry(String key, String val) throws Malformed {
+            this.key = key;
+            this.val = val;
+            for(int i=0; i<key.length(); i++)
+                if (key.charAt(i) < 33 || key.charAt(i) > 126)
+                    throw new Malformed("Header key \""+key+"\" contains invalid character \"" +
+                                        key.charAt(i) + "\" (0x"+Integer.toString(key.charAt(i), 16) +")");
+        }
+    }
+
+    private Headers(Entry[] entries, boolean assumeMime) {
+        this.entries = entries;
+        this.mime = assumeMime | (get("mime-version") != null && get("mime-version").trim().equals("1.0"));
+        for(Entry e : entries) {
+            String val = (String)head.get(e.key.toLowerCase());
+            val = val==null ? e.val.trim() : val+" "+e.val.trim();  // introduce folding whitespace =(
+            //if (mime) k = Encode.RFC2047.decode(k);
+            //if (mime) v = Encode.RFC2047.decode(v);
+            head.put(e.key.toLowerCase(), val);
+        }
+    }
+
+    private static Entry[] extractEntries(Fountain fountain) throws Malformed {
         String key = null;
+        Stream stream = fountain.getStream();
+        ArrayList<Entry> entries = new ArrayList<Entry>();
         for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
-            if (!Character.isSpace(s.charAt(0))) {
-                if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s);
-                key = s.substring(0, s.indexOf(':')).toLowerCase();
-            }
-            if (key.toLowerCase().equals(k.toLowerCase())) {
-                if (v != null) { all.append(k + ": " + v + "\r\n"); lines++; v = null; }
+            s += "\r\n"; // this is the only place where we introduce manglage -- we normalize EOLs
+            if (Character.isSpace(s.charAt(0))) {
+                if (key == null) throw new Malformed("Message began with a blank line; no headers");
+                Entry e = entries.remove(entries.size()-1);
+                entries.add(new Entry(e.key, e.val+s));
                 continue;
             }
-            all.append(s);
-            all.append("\r\n");
-            lines++;
-        }
-        if (v != null) {
-            lines++;
-            all.append(k + ": " + v + "\r\n");
+            if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s);
+            key = s.substring(0, s.indexOf(':'));
+            entries.add(new Entry(key, s.substring(s.indexOf(':') + 1)));
         }
-        all.append("\r\n");
-        return new Original(new Stream(all.toString()));
-    }
-    
-    // FIXME
-    //public abstract String getString();
-    public abstract String[] getHeaderNames();
-    public abstract String get(String s);
-
-    public Headers set(String[] keyval) {
-        Headers ret = this;
-        for(int i=0; i<keyval.length; i+=2)
-            ret = ret.set(keyval[i], keyval[i+1]);
-        return ret;
+        return (Entry[])entries.toArray(new Entry[entries.size()]);
     }
-    public Headers remove(String key) { return set(key, null); /* FIXME */ }
-   
-    public static class Original extends Headers {
-        private final Hash head = new Hash();
-        public        int lines;
-        public  final boolean mime;
-        private String raw;
-        private Fountain fountain;
-
-        //public String getString() { return raw; }
-
-        public Stream getStream() { return fountain.getStream(); }
-        public long   getLength() { return fountain.getLength(); }
-        public int    getNumLines() { return fountain.getNumLines(); }
-
-        public JS get(JS s) throws JSExn { return JSU.S(get(JSU.toString(s).toLowerCase())); }
-
-        public String[] getHeaderNames() { return (String[])head.dumpkeys(new String[head.size()]); }
-        public String get(String s) { return (String)head.get(s.toLowerCase()); }
-
-        public Original(Stream stream) throws Malformed { this(stream, false); }
-        public Original(Stream stream, boolean assumeMime) throws Malformed {
-            StringBuffer all = new StringBuffer();
-            String key = null;
-            int lines = 0;
-            for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
-                all.append(s);
-                all.append("\r\n");
-                lines++;
-                if (Character.isSpace(s.charAt(0))) {
-                    if (key == null) throw new Malformed("Message began with a blank line; no headers");
-                    head.put(key, head.get(key) + " " + s.trim());
-                    continue;
-                }
-                if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s);
-                key = s.substring(0, s.indexOf(':')).toLowerCase();
-                for(int i=0; i<key.length(); i++)
-                    if (key.charAt(i) < 33 || key.charAt(i) > 126) {
-                        Log.error(null,all);
-                        throw new Malformed("Header key \""+key+"\" contains invalid character \"" +
-                                            key.charAt(i) + "\" (0x"+Integer.toString(key.charAt(i), 16) +")");
-                    }
-                String val = s.substring(s.indexOf(':') + 1).trim();
-                if (get(key) != null) val = get(key) + " " + val; // just append it to the previous one;
-                head.put(key, val);
+
+    private Entry[] updateHeaders(String[] keyval) {
+        ArrayList<Entry> entries = new ArrayList<Entry>();
+        for(int i=0; i<this.entries.length; i++)
+            entries.add(this.entries[i]);
+        for(int i=0; i<keyval.length; i+=2) {
+            for(int j=0; j<entries.size(); j++) {
+                Entry e = entries.get(j);
+                if (!e.key.toLowerCase().equals(keyval[i])) continue;
+                if (keyval[i+1]==null)
+                    entries.remove(j);
+                else
+                    entries.set(j, new Entry(keyval[i], keyval[i+1]+"\r\n"));
+                break;
             }
-            this.raw = all.toString();
-            // FIXME: be more efficient here?
-            this.fountain = new Fountain.StringFountain(this.raw);
-            this.lines = lines;
-            this.mime = assumeMime | (get("mime-version") != null && get("mime-version").trim().equals("1.0"));
-            /*
-              java.util.Enumeration e = head.keys();
-              while(e.hasNext()) {
-              String k = (String)e.next();
-              String v = (String)head.get(k);
-              if (mime) k = Encode.RFC2047.decode(k);
-              v = uncomment(v);
-              if (mime) v = Encode.RFC2047.decode(v);
-              head.put(k, v);
-              }
-            */
+            if (keyval[i+1]!=null)
+                entries.add(0, new Entry(keyval[i], keyval[i+1]+"\r\n"));
         }
-
+        return (Entry[])entries.toArray(new Entry[entries.size()]);
+    }
 
     // Helpers //////////////////////////////////////////////////////////////////////////////
 
@@ -126,14 +128,16 @@ public abstract class Headers extends JS.Immutable implements Fountain {
         return stream;
     }
 
-    public static String uncomment(String val) {
+    // designed to remove CFWS, but doesn't work right
+    public static String removeCFWS(String val) {
         boolean inquotes = false;
         for(int i=0; i<val.length(); i++) {
             if (val.charAt(i) == '\"') inquotes = !inquotes;
+            // FIXME: nested comments
             if (val.charAt(i) == '(' && !inquotes)
                 val = val.substring(0, i) + val.substring(val.indexOf(i--, ')') + 1);
         }
         return val;
     }
-    }
+
 }
index 0da4d53..36d125b 100644 (file)
@@ -19,6 +19,7 @@ import java.io.*;
 /** This class contains logic for encoding and decoding MIME multipart messages */
 public class MIME {
 
+    /** Part = Headers+Body */
     public static class Part extends JSReflection implements Fountain {
         public  final Headers      headers;
         public  final ContentType  contentType;
@@ -45,7 +46,7 @@ public class MIME {
         }
 
         public Part(Fountain all) {
-            this.headers     = new Headers.Original(all.getStream());
+            this.headers     = new Headers(all);
             String ctype     = headers.get("content-type");
             this.encoding    = headers.get("content-transfer-encoding");
             String enc = this.encoding;
index 0a2baa6..f6ceb3d 100644 (file)
@@ -188,9 +188,11 @@ public class MailingList extends Mailbox.MailboxWrapper {
         try {
             StringBuffer buf = new StringBuffer();
             m.getBody().getStream().transcribe(buf);
-            Headers head = new Headers.Original(m.headers.getStream());
-            head = head.set("List-Id", one_line_description + "<"+address+">");
-            head = head.set("Subject", properties.get("prefix") + " " + head.get("Subject"));
+            Headers head = new Headers(m.headers,
+                                       new String[] {
+                                           "List-Id", one_line_description + "<"+address+">",
+                                           "Subject", properties.get("prefix") + " " + m.headers.get("Subject")
+                                       });
             
             m = Message.newMessage(Fountain.Util.concat(new Fountain[] { head, 
                                                                          Fountain.Util.create("\r\n"),
index 7b8ba4f..9d91a18 100644 (file)
@@ -163,8 +163,7 @@ public class Message extends MIME.Part {
         if (envelopeFrom==null || envelopeFrom.toString().equals("")) return null;
 
         Log.warn(Message.class, "bouncing message due to: " + reason);
-        Headers h = new Headers.Original(headers.getStream());
-        h = h.set(new String[] {
+        Headers h = new Headers(headers, new String[] {
             "Envelope-To", envelopeFrom.toString(),
             "Return-Path", "<>",
             "From",        "MAILER-DAEMON <>",
index a23c7de..f8bcf0b 100644 (file)
@@ -349,9 +349,11 @@ public class SMTP {
                 conn.println("RCPT TO:<"   + m.envelopeTo.toString()+">");  check(conn.readln(), conn);
                 conn.println("DATA");                                       check(conn.readln(), conn);
 
-                Headers head = m.headers;
-                head = head.remove("return-path");
-                head = head.remove("bcc");
+                Headers head = new Headers(m.headers,
+                                           new String[] {
+                                               "return-path", null,
+                                               "bcc", null
+                                           });
                 Stream stream = head.getStream();
                 for(String s = stream.readln(); s!=null; s=stream.readln()) {
                     if (s.startsWith(".")) conn.print(".");