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.
6 import static org.ibex.mail.MailException.*;
7 import org.ibex.crypto.*;
8 import org.ibex.util.*;
9 import org.ibex.mail.protocol.*;
12 import org.ibex.io.Fountain;
17 // FEATURE: construct hash lazily?
18 public class Headers extends JS.Immutable implements Fountain {
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
25 public Headers(Headers old, String[] keyval) { this(old.updateHeaders(keyval), false); }
27 public Headers(Fountain fountain) throws Malformed { this(fountain, false); }
28 public Headers(Fountain fountain, boolean assumeMime) throws Malformed { this(extractEntries(fountain), assumeMime); }
31 // public //////////////////////////////////////////////////////////////////////////////
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())); }
37 public Stream getStream() { return fountain().getStream(); }
38 public long getLength() { return fountain().getLength(); }
39 public int getNumLines() { return fountain().getNumLines(); }
42 // private //////////////////////////////////////////////////////////////////////////////
44 private final Hash head = new Hash();
45 private final boolean mime;
46 private final Entry[] entries;
47 private Fountain fountain = null;
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());
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 {
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) +")");
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);
85 private static Entry[] extractEntries(Fountain fountain) throws Malformed {
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));
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)));
101 return (Entry[])entries.toArray(new Entry[entries.size()]);
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)
115 entries.set(j, new Entry(keyval[i], keyval[i+1]+"\r\n"));
118 if (keyval[i+1]!=null)
119 entries.add(0, new Entry(keyval[i], keyval[i+1]+"\r\n"));
121 return (Entry[])entries.toArray(new Entry[entries.size()]);
124 // Helpers //////////////////////////////////////////////////////////////////////////////
126 public static Stream skip(Stream stream) {
127 for(String s = stream.readln(); s!=null && s.length() > 0;) s = stream.readln();
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);