major revamp due to new Message constructors
[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     public Headers(String[] keyval)              { this(new Headers(), keyval); }
27
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); }
31
32
33     // public //////////////////////////////////////////////////////////////////////////////
34
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())); }
38
39     public Stream getStream()   { return fountain().getStream(); }
40     public long   getLength()   { return fountain().getLength(); }
41     public int    getNumLines() { return fountain().getNumLines(); }
42
43
44     // private //////////////////////////////////////////////////////////////////////////////
45
46     private final Hash head = new Hash();
47     private final boolean mime;
48     private final Entry[] entries;
49     private       Fountain fountain = null;
50
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());
57         }
58         return fountain;
59     }
60
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 {
66             this.key = key;
67             this.val = val;
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) +")");
72         }
73     }
74
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 =(
81             // FEATURE
82             //if (mime) k = Encode.RFC2047.decode(k);
83             //if (mime) v = Encode.RFC2047.decode(v);
84             head.put(e.key.toLowerCase(), val);
85         }
86     }
87
88     private static Entry[] extractEntries(Fountain fountain) throws Malformed {
89         String key = null;
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));
98                 continue;
99             }
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)));
103         }
104         return (Entry[])entries.toArray(new Entry[entries.size()]);
105     }
106
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)
116                     entries.remove(j);
117                 else
118                     entries.set(j, new Entry(keyval[i], keyval[i+1]+"\r\n"));
119                 continue OUTER;
120             }
121             if (keyval[i+1]!=null)
122                 entries.add(0, new Entry(keyval[i], keyval[i+1]+"\r\n"));
123         }
124         return (Entry[])entries.toArray(new Entry[entries.size()]);
125     }
126
127     // Helpers //////////////////////////////////////////////////////////////////////////////
128
129     public static Stream skip(Stream stream) {
130         for(String s = stream.readln(); s!=null && s.trim().length() > 0;)
131             s = stream.readln();
132         return stream;
133     }
134
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);
143         }
144         return val;
145     }
146
147 }