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); }
26 public Headers(String[] keyval) { this(new Headers(), keyval); }
28 public Headers() { this(new String[0]); }
29 public Headers(Fountain fountain) throws Malformed { this(fountain, false); }
30 public Headers(Fountain fountain, boolean assumeMime) throws Malformed { this(extractEntries(fountain), assumeMime); }
33 // public //////////////////////////////////////////////////////////////////////////////
35 public String[] getHeaderNames() { return (String[])head.dumpkeys(new String[head.size()]); }
36 public String get(String s) { return (String)head.get(s.toLowerCase()); }
37 public JS get(JS s) throws JSExn { return JSU.S(get(JSU.toString(s).toLowerCase())); }
39 public Stream getStream() { return fountain().getStream(); }
40 public long getLength() { return fountain().getLength(); }
41 public int getNumLines() { return fountain().getNumLines(); }
44 // private //////////////////////////////////////////////////////////////////////////////
46 private final Hash head = new Hash();
47 private final boolean mime;
48 private final Entry[] entries;
49 private Fountain fountain = null;
51 private synchronized Fountain fountain() { // lazily constructed
52 if (fountain == null) {
53 StringBuffer sb = new StringBuffer();
54 for(Entry e : entries)
55 sb.append(e.toString());
56 this.fountain = Fountain.Util.create(sb.toString());
61 private static class Entry {
62 public final String key;
63 public final String val;
64 public String toString() { return key+":"+val; }
65 public Entry(String key, String val) throws Malformed {
68 for(int i=0; i<key.length(); i++)
69 if (key.charAt(i) < 33 || key.charAt(i) > 126)
70 throw new Malformed("Header key \""+key+"\" contains invalid character \"" +
71 key.charAt(i) + "\" (0x"+Integer.toString(key.charAt(i), 16) +")");
75 private Headers(Entry[] entries, boolean assumeMime) {
76 this.entries = entries;
77 this.mime = assumeMime | (get("mime-version") != null && get("mime-version").trim().equals("1.0"));
78 for(Entry e : entries) {
79 String val = (String)head.get(e.key.toLowerCase());
80 val = val==null ? e.val.trim() : val+" "+e.val.trim(); // introduce folding whitespace =(
82 //if (mime) k = Encode.RFC2047.decode(k);
83 //if (mime) v = Encode.RFC2047.decode(v);
84 head.put(e.key.toLowerCase(), val);
88 private static Entry[] extractEntries(Fountain fountain) throws Malformed {
90 Stream stream = fountain.getStream();
91 ArrayList<Entry> entries = new ArrayList<Entry>();
92 for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
93 s += "\r\n"; // this is the only place where we introduce manglage -- we normalize EOLs
94 if (Character.isSpace(s.charAt(0))) {
95 if (key == null) throw new Malformed("Message began with a blank line; no headers");
96 Entry e = entries.remove(entries.size()-1);
97 entries.add(new Entry(e.key, e.val+s));
100 if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s);
101 key = s.substring(0, s.indexOf(':'));
102 entries.add(new Entry(key, s.substring(s.indexOf(':') + 1)));
104 return (Entry[])entries.toArray(new Entry[entries.size()]);
107 private Entry[] updateHeaders(String[] keyval) {
108 ArrayList<Entry> entries = new ArrayList<Entry>();
109 for(int i=0; i<this.entries.length; i++)
110 entries.add(this.entries[i]);
111 OUTER: for(int i=0; i<keyval.length; i+=2) {
112 for(int j=0; j<entries.size(); j++) {
113 Entry e = entries.get(j);
114 if (!e.key.toLowerCase().equals(keyval[i].toLowerCase())) continue;
115 if (keyval[i+1]==null)
118 entries.set(j, new Entry(keyval[i], keyval[i+1]+"\r\n"));
121 if (keyval[i+1]!=null)
122 entries.add(0, new Entry(keyval[i], keyval[i+1]+"\r\n"));
124 return (Entry[])entries.toArray(new Entry[entries.size()]);
127 // Helpers //////////////////////////////////////////////////////////////////////////////
129 public static Stream skip(Stream stream) {
130 for(String s = stream.readln(); s!=null && s.trim().length() > 0;)
135 // designed to remove CFWS, but doesn't work right
136 public static String removeCFWS(String val) {
137 boolean inquotes = false;
138 for(int i=0; i<val.length(); i++) {
139 if (val.charAt(i) == '\"') inquotes = !inquotes;
140 // FIXME: nested comments
141 if (val.charAt(i) == '(' && !inquotes)
142 val = val.substring(0, i) + val.substring(val.indexOf(i--, ')') + 1);