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