be more careful about closing ResultSets in Graylist
[org.ibex.mail.git] / src / org / ibex / mail / Headers.java
index 26c12ad..c7813eb 100644 (file)
@@ -14,113 +14,134 @@ 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)
+// FEATURE: construct hash lazily?
 public class Headers extends JS.Immutable implements Fountain {
-    private final Hash head = new Hash();
-    private final Hash headModified = new Hash();
-    public        int lines;
-    public  final boolean mime;
+   
+    /**
+     *  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(String[] keyval)              { this(new Headers(), keyval); }
+
+    public Headers() { this(new String[0]); }
+    public Headers(Fountain fountain) throws Malformed { this(fountain, false); }
+    public Headers(Fountain fountain, boolean assumeMime) throws Malformed { this(extractEntries(fountain), assumeMime); }
+
+
+    // public //////////////////////////////////////////////////////////////////////////////
 
-    private String raw;
-    private StringFountain fountain;
+    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 String get(String s) {
-        String ret = (String)headModified.get(s.toLowerCase());
-        if (ret==null) ret = (String)head.get(s.toLowerCase());
-        return ret;
+    public Stream getStream()   { return fountain().getStream(); }
+    public long   getLength()   { return fountain().getLength(); }
+    public int    getNumLines() { return fountain().getNumLines(); }
+
+
+    // 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;
     }
-    public void remove(String k) { put(k, null); }
-    public void put(String k, String v) {
-        Stream stream = getStream();
-        StringBuffer all = new StringBuffer();
-        int lines = 0;
-        String key = null;
-        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; }
-                continue;
-            }
-            all.append(s);
-            all.append("\r\n");
-            lines++;
+
+    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) +")");
         }
-        if (v != null) {
-            lines++;
-            all.append(k + ": " + v + "\r\n");
+    }
+
+    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 =(
+            // FEATURE
+            //if (mime) k = Encode.RFC2047.decode(k);
+            //if (mime) v = Encode.RFC2047.decode(v);
+            head.put(e.key.toLowerCase(), val);
         }
-        all.append("\r\n");
-        this.raw = all.toString();
-        this.lines = lines;
-        this.fountain = new Fountain.StringFountain(this.raw);
     }
-    public JS get(JS s) throws JSExn { return JSU.S(get(JSU.toString(s).toLowerCase())); }
-
-    public Stream getStream() { return fountain.getStream(); }
-    public int    getLength() { return fountain.getLength(); }
-    public int    getNumLines() { return fountain.getNumLines(); }
-    public Stream getStreamWithCRLF() { return new Stream(raw+"\r\n"); }
-    
-    // FIXME
-    public String getString() { return raw; }
-
-    public Headers(Stream stream) throws Malformed { this(stream, false); }
-    public Headers(Stream stream, boolean assumeMime) throws Malformed {
-        StringBuffer all = new StringBuffer();
+
+    private static Entry[] extractEntries(Fountain fountain) throws Malformed {
         String key = null;
-        int lines = 0;
+        Stream stream = fountain.getStream();
+        ArrayList<Entry> entries = new ArrayList<Entry>();
         for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
-            all.append(s);
-            all.append("\r\n");
-            lines++;
+            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");
-                head.put(key, head.get(key) + " " + s.trim());
+                Entry e = entries.remove(entries.size()-1);
+                entries.add(new Entry(e.key, e.val+s));
                 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)
-                    throw new Malformed("Header key \""+key+"\" contains invalid character \"" + key.charAt(i) + "\"");
-            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);
+            key = s.substring(0, s.indexOf(':'));
+            entries.add(new Entry(key, s.substring(s.indexOf(':') + 1)));
         }
-        this.raw = all.toString();
-        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);
-          }
-        */
+        return (Entry[])entries.toArray(new Entry[entries.size()]);
+    }
+
+    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]);
+        OUTER: 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].toLowerCase())) continue;
+                if (keyval[i+1]==null)
+                    entries.remove(j);
+                else
+                    entries.set(j, new Entry(keyval[i], keyval[i+1]+"\r\n"));
+                continue OUTER;
+            }
+            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 //////////////////////////////////////////////////////////////////////////////
 
     public static Stream skip(Stream stream) {
-        for(String s = stream.readln(); s!=null && s.length() > 0;) s = stream.readln();
+        for(String s = stream.readln(); s!=null && s.trim().length() > 0;)
+            s = stream.readln();
         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;
     }
+
 }