keep MIME headers in a byte[] until requested
[org.ibex.mail.git] / src / org / ibex / mail / MIME.java
1 package org.ibex.mail;
2 import org.ibex.crypto.*;
3 import org.ibex.util.*;
4 import org.ibex.mail.protocol.*;
5 import org.ibex.io.*;
6 import java.util.*;
7 import java.net.*;
8 import java.io.*;
9
10 // FEATURE: MIME RFC2045, 2046, 2049
11
12 /** This class contains logic for encoding and decoding MIME multipart messages */
13 public class MIME {
14
15     public static class RFC2047 {
16         public static String decode(String s) {
17             /*
18             try { while (s.indexOf("=?") != -1) {
19                 String pre = s.substring(0, s.indexOf("=?"));
20                 s = s.substring(s.indexOf("=?") + 2);
21
22                 // MIME charset; FIXME use this
23                 String charset = s.substring(0, s.indexOf('?')).toLowerCase();
24                 s = s.substring(s.indexOf('?') + 1);
25
26                 String encoding = s.substring(0, s.indexOf('?')).toLowerCase();
27                 s = s.substring(s.indexOf('?') + 1);
28
29                 String encodedText = s.substring(0, s.indexOf("?="));
30
31                 if (encoding.equals("b"))      encodedText = new String(Base64.decode(encodedText));
32
33                 // except that ANY char can be endoed (unlike real qp)
34                 else if (encoding.equals("q")) encodedText = MIME.QuotedPrintable.decode(encodedText, true);
35                 else Log.warn(MIME.class, "unknown RFC2047 encoding \""+encoding+"\"");
36
37                 String post = s.substring(s.indexOf("?=") + 2);
38                 s = pre + encodedText + post;
39
40                 // FIXME re-encode when transmitting
41
42             } } catch (Exception e) {
43                 Log.warn(MIME.class, "error trying to decode RFC2047 encoded-word: \""+s+"\"");
44                 Log.warn(MIME.class, e);
45             }
46             */
47             return s;
48         }
49     }
50
51     public static class QuotedPrintable {
52         public static String decode(String s, boolean lax) {
53         //
54         //   =XX  -> hex representation, must be uppercase
55         //   9, 32, 33-60, 62-126 can be literal
56         //   9, 32 at end-of-line must get encoded
57         //   trailing whitespace must be deleted when decoding
58         //   =\n = soft line break
59         //   lines cannot be more than 76 chars long
60         //
61
62             // lax is used for RFC2047 headers; removes restrictions on which chars you can encode
63             return s;
64         }
65     }
66
67     // multipart/mixed       -- default
68     // multipart/parallel    -- order of components does not matter
69     // multipart/alternative -- same data, different versions
70     // multipart/digest      -- default content-type of components is message/rfc822
71     // message/rfc822        -- FIXME
72     // message/partial       -- not supported; see RFC 2046, section 5.2.2
73     // message/external-body -- not supported; see RFC 2046, section 5.2.3
74     // FIXME charsets  US-ASCII, ISO-8559-X, 
75     public static class Content extends org.ibex.js.JSReflection {
76         public final String    type;
77         public final String    subtype;
78         public final String    description;
79         public final String    id;
80         public final String    transferEncoding;
81         public final String    charset;
82         public final boolean   composite;
83         public final boolean   alternative;
84         public final Hashtable parameters = new Hashtable();
85         public Content(String header, String description, String id, String transferEncoding) {
86             this.id = id;
87             this.description = description;
88             this.transferEncoding = transferEncoding;
89             if (header == null) { type="text"; subtype="plain"; charset="us-ascii"; alternative=false; composite=false; return; }
90             header = header.trim();
91             if (header.indexOf('/') == -1) {
92                 Log.warn(this, "content-type lacks a forward slash: \""+header+"\"");
93                 header = "text/plain";
94             }
95             type = header.substring(0, header.indexOf('/')).toLowerCase();
96             header = header.substring(header.indexOf('/') + 1);
97             subtype = (header.indexOf(';') == -1) ? header.toLowerCase() : header.substring(0, header.indexOf(';')).toLowerCase();
98             composite   = type != null && (type.equals("message") || type.equals("multipart"));
99             alternative = composite && subtype.equals("alternative");
100             charset     = parameters.get("charset") == null ? "us-ascii" : parameters.get("charset").toString();
101             if (header.indexOf(';') == -1) return;
102             StringTokenizer st = new StringTokenizer(header.substring(header.indexOf(';') + 1), ";");
103             while(st.hasMoreTokens()) {
104                 String key = st.nextToken().trim();
105                 if (key.indexOf('=') == -1)
106                     throw new MailException.Malformed("content-type parameter lacks an equals sign: \""+key+"\"");
107                 String val = key.substring(key.indexOf('=')+1).trim();
108                 if (val.startsWith("\"") && val.endsWith("\"")) val = val.substring(1, val.length() - 2);
109                 key = key.substring(0, key.indexOf('=')+1).toLowerCase();
110                 parameters.put(key, val);
111             }
112         }
113     }
114
115     public static class Part extends org.ibex.js.JSReflection {
116         public final Content   content;
117         public final boolean   mime;                // true iff Mime-Version is 1.0
118         public final Headers   headers;
119         public final Part[]    subparts;
120         public final String    body;
121         public final int       lines;
122         private final boolean  last;
123
124         private Part[] parseParts(Stream stream) {
125             Vec v = new Vec();
126             // first part begins with a boundary delimiter
127             for(String s = stream.readln(); s != null; s = stream.readln())
128                 if (s.equals("--" + content.parameters.get("boundary"))) break;
129             while(true) {
130                 Part p = new Part(stream, (String)content.parameters.get("boundary"), true);
131                 v.addElement(p);
132                 //lines += p.lines;
133                 if (p.last) break;
134             }
135             return (Part[])v.copyInto(new Part[v.size()]);
136         }
137        
138         public Part(Stream stream, String boundary, boolean assumeMime) throws MailException.Malformed {
139             this.headers     = new Headers(stream, assumeMime);
140             this.mime        = assumeMime | (headers.gets("mime-version")!=null&&headers.gets("mime-version").trim().equals("1.0"));
141             String ctype     = headers.gets("content-type");
142             String encoding  = headers.gets("content-transfer-encoding");
143             if (!(encoding == null || encoding.equals("7bit") || encoding.equals("8bit") || encoding.equals("binary") ||
144                   encoding.equals("quoted-printable") || encoding.equals("base64"))) {
145                 Log.warn(MIME.class, "unknown TransferEncoding \"" + encoding + "\"");
146                 ctype = "application/octet-stream";
147             }
148             content = new Content(ctype, headers.gets("content-description"), headers.gets("content-id"), encoding);
149             //if (content.composite) { subparts = parseParts(stream); body = null; last = false; lines = 0; return; }
150             subparts = null;
151             boolean last = false;
152             int lines = 0;
153             StringBuffer body = new StringBuffer();
154             for(String s = stream.readln(); s != null; s = stream.readln()) {
155                 if (boundary != null && (s.equals(boundary) || s.equals(boundary + "--"))) {
156                     body.setLength(body.length() - 2);  // preceeding CRLF is part of delimiter
157                     last = s.equals(boundary + "--");
158                     break;
159                 }
160                 body.append(s);
161                 body.append("\r\n");
162                 lines++;
163             }
164             if ("quoted-printable".equals(encoding)) this.body = MIME.QuotedPrintable.decode(body.toString(),false);
165             else if ("base64".equals(encoding)) this.body = new String(Base64.decode(body.toString()));
166             else this.body = body.toString();
167             this.last = last;
168             this.lines = lines + headers.lines;
169         }
170     }
171
172     public static class Headers extends org.ibex.js.JSReflection {
173         private Hashtable head = new Hashtable();
174         public final int lines;
175         public final String raw;
176         public String toString() { return raw; }
177         public Object get(Object s) { return head.get(((String)s).toLowerCase()); }
178         public String gets(String s) { return (String)get(s); }
179         public static String uncomment(String val) {
180             boolean inquotes = false;
181             for(int i=0; i<val.length(); i++) {
182                 if (val.charAt(i) == '\"') inquotes = !inquotes;
183                 if (val.charAt(i) == '(' && !inquotes)
184                     val = val.substring(0, i) + val.substring(val.indexOf(i--, ')') + 1);
185             }
186             return val;
187         }
188         public Headers(Stream stream, boolean assumeMime) throws MailException.Malformed {
189             StringBuffer all = new StringBuffer();
190             String key = null;
191             int lines = 0;
192             for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
193                 all.append(s);
194                 all.append("\r\n");
195                 lines++;
196                 if (Character.isSpace(s.charAt(0))) {
197                     if (key == null) throw new MailException.Malformed("Message began with a blank line; no headers");
198                     head.put(key, head.get(key) + " " + s.trim());
199                     continue;
200                 }
201                 if (s.indexOf(':') == -1) throw new MailException.Malformed("Header line does not contain colon: " + s);
202                 key = s.substring(0, s.indexOf(':')).toLowerCase();
203                 for(int i=0; i<key.length(); i++)
204                     if (key.charAt(i) < 33 || key.charAt(i) > 126)
205                         throw new MailException.Malformed("Header key \""+key+"\" contains invalid character \"" + key.charAt(i) + "\"");
206                 String val = s.substring(s.indexOf(':') + 1).trim();
207                 if (get(key) != null) val = get(key) + " " + val; // just append it to the previous one;
208                 head.put(key, val);
209             }
210             this.raw = all.toString();
211             this.lines = lines;
212
213             Enumeration e = head.keys();
214             boolean mime = assumeMime | (gets("mime-version") != null && gets("mime-version").trim().equals("1.0"));
215             /*
216             while(e.hasMoreElements()) {
217                 String k = (String)e.nextElement();
218                 String v = (String)head.get(k);
219                 if (mime) k = MIME.RFC2047.decode(k);
220                 v = uncomment(v);
221                 if (mime) v = MIME.RFC2047.decode(v);
222                 head.put(k, v);
223             }
224             */
225         }
226     }
227 }