massive refactoring of Headers class
[org.ibex.mail.git] / src / org / ibex / mail / Headers.java
1 // Copyright 2000-2005 the Contributors, as shown in the revision logs.
2 // Licensed under the Apache Public Source License 2.0 ("the License").
3 // You may not use this file except in compliance with the License.
4
5 package org.ibex.mail;
6 import static org.ibex.mail.MailException.*;
7 import org.ibex.crypto.*;
8 import org.ibex.util.*;
9 import org.ibex.mail.protocol.*;
10 import org.ibex.js.*;
11 import org.ibex.io.*;
12 import org.ibex.io.Fountain;
13 import java.util.*;
14 import java.net.*;
15 import java.io.*;
16
17 // FEATURE: construct hash lazily?
18 public class Headers extends JS.Immutable implements Fountain {
19    
20     /**
21      *  constructs a new set of Headers based on a preexisting set --
22      *  keyval's even-numbered elements are keys, odd elements are
23      *  values -- a null value deletes, non-null value replaces
24      */
25     public Headers(Headers old, String[] keyval) { this(old.updateHeaders(keyval), false); }
26
27     public Headers(Fountain fountain) throws Malformed { this(fountain, false); }
28     public Headers(Fountain fountain, boolean assumeMime) throws Malformed { this(extractEntries(fountain), assumeMime); }
29
30
31     // public //////////////////////////////////////////////////////////////////////////////
32
33     public String[] getHeaderNames()       { return (String[])head.dumpkeys(new String[head.size()]); }
34     public String   get(String s)          { return (String)head.get(s.toLowerCase()); }
35     public JS       get(JS s) throws JSExn { return JSU.S(get(JSU.toString(s).toLowerCase())); }
36
37     public Stream getStream()   { return fountain().getStream(); }
38     public long   getLength()   { return fountain().getLength(); }
39     public int    getNumLines() { return fountain().getNumLines(); }
40
41
42     // private //////////////////////////////////////////////////////////////////////////////
43
44     private final Hash head = new Hash();
45     private final boolean mime;
46     private final Entry[] entries;
47     private       Fountain fountain = null;
48
49     private synchronized Fountain fountain() {  // lazily constructed
50         if (fountain == null) {
51             StringBuffer sb = new StringBuffer();
52             for(Entry e : entries)
53                 sb.append(e.toString());
54             this.fountain = Fountain.Util.create(sb.toString());
55         }
56         return fountain;
57     }
58
59     private static class Entry {
60         public final String key;
61         public final String val;
62         public String toString() { return key+":"+val; }
63         public Entry(String key, String val) throws Malformed {
64             this.key = key;
65             this.val = val;
66             for(int i=0; i<key.length(); i++)
67                 if (key.charAt(i) < 33 || key.charAt(i) > 126)
68                     throw new Malformed("Header key \""+key+"\" contains invalid character \"" +
69                                         key.charAt(i) + "\" (0x"+Integer.toString(key.charAt(i), 16) +")");
70         }
71     }
72
73     private Headers(Entry[] entries, boolean assumeMime) {
74         this.entries = entries;
75         this.mime = assumeMime | (get("mime-version") != null && get("mime-version").trim().equals("1.0"));
76         for(Entry e : entries) {
77             String val = (String)head.get(e.key.toLowerCase());
78             val = val==null ? e.val.trim() : val+" "+e.val.trim();  // introduce folding whitespace =(
79             //if (mime) k = Encode.RFC2047.decode(k);
80             //if (mime) v = Encode.RFC2047.decode(v);
81             head.put(e.key.toLowerCase(), val);
82         }
83     }
84
85     private static Entry[] extractEntries(Fountain fountain) throws Malformed {
86         String key = null;
87         Stream stream = fountain.getStream();
88         ArrayList<Entry> entries = new ArrayList<Entry>();
89         for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
90             s += "\r\n"; // this is the only place where we introduce manglage -- we normalize EOLs
91             if (Character.isSpace(s.charAt(0))) {
92                 if (key == null) throw new Malformed("Message began with a blank line; no headers");
93                 Entry e = entries.remove(entries.size()-1);
94                 entries.add(new Entry(e.key, e.val+s));
95                 continue;
96             }
97             if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s);
98             key = s.substring(0, s.indexOf(':'));
99             entries.add(new Entry(key, s.substring(s.indexOf(':') + 1)));
100         }
101         return (Entry[])entries.toArray(new Entry[entries.size()]);
102     }
103
104     private Entry[] updateHeaders(String[] keyval) {
105         ArrayList<Entry> entries = new ArrayList<Entry>();
106         for(int i=0; i<this.entries.length; i++)
107             entries.add(this.entries[i]);
108         for(int i=0; i<keyval.length; i+=2) {
109             for(int j=0; j<entries.size(); j++) {
110                 Entry e = entries.get(j);
111                 if (!e.key.toLowerCase().equals(keyval[i])) continue;
112                 if (keyval[i+1]==null)
113                     entries.remove(j);
114                 else
115                     entries.set(j, new Entry(keyval[i], keyval[i+1]+"\r\n"));
116                 break;
117             }
118             if (keyval[i+1]!=null)
119                 entries.add(0, new Entry(keyval[i], keyval[i+1]+"\r\n"));
120         }
121         return (Entry[])entries.toArray(new Entry[entries.size()]);
122     }
123
124     // Helpers //////////////////////////////////////////////////////////////////////////////
125
126     public static Stream skip(Stream stream) {
127         for(String s = stream.readln(); s!=null && s.length() > 0;) s = stream.readln();
128         return stream;
129     }
130
131     // designed to remove CFWS, but doesn't work right
132     public static String removeCFWS(String val) {
133         boolean inquotes = false;
134         for(int i=0; i<val.length(); i++) {
135             if (val.charAt(i) == '\"') inquotes = !inquotes;
136             // FIXME: nested comments
137             if (val.charAt(i) == '(' && !inquotes)
138                 val = val.substring(0, i) + val.substring(val.indexOf(i--, ')') + 1);
139         }
140         return val;
141     }
142
143 }