improve logic for header-stripping in MIME.java
[org.ibex.mail.git] / src / org / ibex / mail / MIME.java
index 3258b6b..477ce63 100644 (file)
@@ -1,8 +1,15 @@
+// Copyright 2000-2005 the Contributors, as shown in the revision logs.
+// Licensed under the Apache Public Source License 2.0 ("the License").
+// You may not use this file except in compliance with the License.
+
 package org.ibex.mail;
+import static org.ibex.mail.MailException.*;
 import org.ibex.crypto.*;
 import org.ibex.util.*;
 import org.ibex.mail.protocol.*;
+import org.ibex.js.*;
 import org.ibex.io.*;
+import org.ibex.io.Fountain;
 import java.util.*;
 import java.net.*;
 import java.io.*;
@@ -12,216 +19,99 @@ import java.io.*;
 /** This class contains logic for encoding and decoding MIME multipart messages */
 public class MIME {
 
-    public static class RFC2047 {
-        public static String decode(String s) {
-            /*
-            try { while (s.indexOf("=?") != -1) {
-                String pre = s.substring(0, s.indexOf("=?"));
-                s = s.substring(s.indexOf("=?") + 2);
-
-                // MIME charset; FIXME use this
-                String charset = s.substring(0, s.indexOf('?')).toLowerCase();
-                s = s.substring(s.indexOf('?') + 1);
-
-                String encoding = s.substring(0, s.indexOf('?')).toLowerCase();
-                s = s.substring(s.indexOf('?') + 1);
-
-                String encodedText = s.substring(0, s.indexOf("?="));
-
-                if (encoding.equals("b"))      encodedText = new String(Base64.decode(encodedText));
-
-                // except that ANY char can be endoed (unlike real qp)
-                else if (encoding.equals("q")) encodedText = MIME.QuotedPrintable.decode(encodedText, true);
-                else Log.warn(MIME.class, "unknown RFC2047 encoding \""+encoding+"\"");
-
-                String post = s.substring(s.indexOf("?=") + 2);
-                s = pre + encodedText + post;
-
-                // FIXME re-encode when transmitting
-
-            } } catch (Exception e) {
-                Log.warn(MIME.class, "error trying to decode RFC2047 encoded-word: \""+s+"\"");
-                Log.warn(MIME.class, e);
+    /** Part = Headers+Body */
+    public static class Part extends JSReflection implements Fountain {
+        public  final Headers      headers;
+        public  final ContentType  contentType;
+        private final String       encoding;
+
+        private final Fountain     all;
+        private final Fountain     body;
+
+        public Stream getStream()   { return all.getStream(); }
+        public int getNumLines()    { return all.getNumLines(); }
+        public long getLength()     { return all.getLength(); }
+        public Fountain getBody()   { return body; }
+
+        public JS get(JS key) throws JSExn {
+            String k = JSU.toString(key);
+            if ("body".equals(k)) {
+                StringBuffer sb = new StringBuffer();
+                getBody().getStream().transcribe(sb);
+                return JSU.S(sb.toString());
             }
-            */
-            return s;
+            return super.get(key);
         }
-    }
-
-    public static class QuotedPrintable {
-        public static String decode(String s, boolean lax) {
-        //
-        //   =XX  -> hex representation, must be uppercase
-        //   9, 32, 33-60, 62-126 can be literal
-        //   9, 32 at end-of-line must get encoded
-        //   trailing whitespace must be deleted when decoding
-        //   =\n = soft line break
-        //   lines cannot be more than 76 chars long
-        //
-
-            // lax is used for RFC2047 headers; removes restrictions on which chars you can encode
-            return s;
-        }
-    }
 
-    // multipart/mixed       -- default
-    // multipart/parallel    -- order of components does not matter
-    // multipart/alternative -- same data, different versions
-    // multipart/digest      -- default content-type of components is message/rfc822
-    // message/rfc822        -- FIXME
-    // message/partial       -- not supported; see RFC 2046, section 5.2.2
-    // message/external-body -- not supported; see RFC 2046, section 5.2.3
-    // FIXME charsets  US-ASCII, ISO-8559-X, 
-    public static class Content extends org.ibex.js.JSReflection {
-        public final String    type;
-        public final String    subtype;
-        public final String    description;
-        public final String    id;
-        public final String    transferEncoding;
-        public final String    charset;
-        public final boolean   composite;
-        public final boolean   alternative;
-        public final Hashtable parameters = new Hashtable();
-        public Content(String header, String description, String id, String transferEncoding) {
-            this.id = id;
-            this.description = description;
-            this.transferEncoding = transferEncoding;
-            if (header == null) { type="text"; subtype="plain"; charset="us-ascii"; alternative=false; composite=false; return; }
-            header = header.trim();
-            if (header.indexOf('/') == -1) {
-                Log.warn(this, "content-type lacks a forward slash: \""+header+"\"");
-                header = "text/plain";
-            }
-            type = header.substring(0, header.indexOf('/')).toLowerCase();
-            header = header.substring(header.indexOf('/') + 1);
-            subtype = (header.indexOf(';') == -1) ? header.toLowerCase() : header.substring(0, header.indexOf(';')).toLowerCase();
-            composite   = type != null && (type.equals("message") || type.equals("multipart"));
-            alternative = composite && subtype.equals("alternative");
-            charset     = parameters.get("charset") == null ? "us-ascii" : parameters.get("charset").toString();
-            if (header.indexOf(';') == -1) return;
-            StringTokenizer st = new StringTokenizer(header.substring(header.indexOf(';') + 1), ";");
-            while(st.hasMoreTokens()) {
-                String key = st.nextToken().trim();
-                if (key.indexOf('=') == -1)
-                    throw new MailException.Malformed("content-type parameter lacks an equals sign: \""+key+"\"");
-                String val = key.substring(key.indexOf('=')+1).trim();
-                if (val.startsWith("\"") && val.endsWith("\"")) val = val.substring(1, val.length() - 2);
-                key = key.substring(0, key.indexOf('=')+1).toLowerCase();
-                parameters.put(key, val);
+        public Part(final Fountain fount, String[] keyval) {
+            Headers h        = new Headers(fount);
+            this.headers     = keyval==null ? h : new Headers(h, keyval);
+            String ctype     = headers.get("content-type");
+            this.encoding    = headers.get("content-transfer-encoding");
+            String enc = this.encoding;
+            if (enc!=null) enc = enc.toLowerCase();
+            if (!(enc == null || enc.equals("7bit") || enc.equals("8bit") || enc.equals("binary") ||
+                  enc.equals("quoted-printable") || enc.equals("base64"))) {
+                Log.warn(MIME.class, "unknown TransferEncoding \"" + encoding + "\"");
+                ctype = "application/octet-stream";
             }
+            this.contentType = new ContentType(ctype, headers.get("content-description"), headers.get("content-id"), encoding);
+            // FIXME: this is a horrible, tangled mess.
+            this.body = new Fountain() {
+                    public int getNumLines()  { return Stream.countLines(this.getStream()); }
+                    public long getLength()   { return Stream.countBytes(this.getStream()); }
+                    public Stream getStream() { return transformBodyStream(Headers.skip(fount.getStream())); }
+                };
+            this.all =
+                keyval==null
+                ? fount
+                : Fountain.Util.concat(this.headers, Fountain.Util.create("\r\n"), this.body);
         }
-    }
 
-    public static class Part extends org.ibex.js.JSReflection {
-        public final Content   content;
-        public final boolean   mime;                // true iff Mime-Version is 1.0
-        public final Headers   headers;
-        public final Part[]    subparts;
-        public final String    body;
-        public final int       lines;
-        private final boolean  last;
+        private Stream transformBodyStream(Stream body) {
+            //"quoted-printable".equals(encoding) ? Encode.QuotedPrintable.decode(body.toString(),false) :
+            //"base64".equals(encoding)           ? Encode.fromBase64(body.toString()) :
+            return body;
+        }
 
-        private Part[] parseParts(Stream stream) {
+        /*
+        public Part getPart(int i) {
+            Stream stream = body.getStream();
             Vec v = new Vec();
             // first part begins with a boundary delimiter
             for(String s = stream.readln(); s != null; s = stream.readln())
-                if (s.equals("--" + content.parameters.get("boundary"))) break;
+                if (s.equals("--" + contentType.parameters.get("boundary"))) break;
             while(true) {
-                Part p = new Part(stream, (String)content.parameters.get("boundary"), true);
+                Stream substream = new BoundaryStream(stream, (String)contentType.parameters.get("boundary"));
+                Part p = new Part(substream, true, null); // FIXME split off headers
                 v.addElement(p);
-                //lines += p.lines;
-                if (p.last) break;
+                if (substream.isLast()) break;
             }
-            return (Part[])v.copyInto(new Part[v.size()]);
+            return parts = (Part[])v.copyInto(new Part[v.size()]);
         }
-       
-        public Part(Stream stream, String boundary, boolean assumeMime) throws MailException.Malformed {
-            this.headers     = new Headers(stream, assumeMime);
-            this.mime        = assumeMime | (headers.gets("mime-version")!=null&&headers.gets("mime-version").trim().equals("1.0"));
-            String ctype     = headers.gets("content-type");
-            String encoding  = headers.gets("content-transfer-encoding");
-            if (!(encoding == null || encoding.equals("7bit") || encoding.equals("8bit") || encoding.equals("binary") ||
-                  encoding.equals("quoted-printable") || encoding.equals("base64"))) {
-                Log.warn(MIME.class, "unknown TransferEncoding \"" + encoding + "\"");
-                ctype = "application/octet-stream";
-            }
-            content = new Content(ctype, headers.gets("content-description"), headers.gets("content-id"), encoding);
-            //if (content.composite) { subparts = parseParts(stream); body = null; last = false; lines = 0; return; }
-            subparts = null;
-            boolean last = false;
-            int lines = 0;
-            StringBuffer body = new StringBuffer();
+        */
+    }
+
+    /*
+    public static class Boundary implements Stream.Transformer {
+        private final String boundary;
+        private boolean done = false;
+        private boolean last = false;
+        public Boundary(String bounardy) { this.boundary = boundary; }
+        public boolean isLast() { while(!done) readln(); return last; }
+        public Stream transform(Stream stream) {
             for(String s = stream.readln(); s != null; s = stream.readln()) {
                 if (boundary != null && (s.equals(boundary) || s.equals(boundary + "--"))) {
                     body.setLength(body.length() - 2);  // preceeding CRLF is part of delimiter
                     last = s.equals(boundary + "--");
+                    done = true;
                     break;
                 }
                 body.append(s);
                 body.append("\r\n");
-                lines++;
-            }
-            if ("quoted-printable".equals(encoding)) this.body = MIME.QuotedPrintable.decode(body.toString(),false);
-            else if ("base64".equals(encoding)) this.body = new String(Base64.decode(body.toString()));
-            else this.body = body.toString();
-            this.last = last;
-            this.lines = lines + headers.lines;
-        }
-    }
-
-    public static class Headers extends org.ibex.js.JSReflection {
-       private Hashtable head = new Hashtable();
-        public final int lines;
-        public final String raw;
-        public String toString() { return raw; }
-        public Object get(Object s) { return head.get(((String)s).toLowerCase()); }
-        public String gets(String s) { return (String)get(s); }
-        public static String uncomment(String val) {
-            boolean inquotes = false;
-            for(int i=0; i<val.length(); i++) {
-                if (val.charAt(i) == '\"') inquotes = !inquotes;
-                if (val.charAt(i) == '(' && !inquotes)
-                    val = val.substring(0, i) + val.substring(val.indexOf(i--, ')') + 1);
-            }
-            return val;
-        }
-        public Headers(Stream stream, boolean assumeMime) throws MailException.Malformed {
-            StringBuffer all = new StringBuffer();
-            String key = null;
-            int lines = 0;
-            for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
-                all.append(s);
-                all.append("\r\n");
-                lines++;
-                if (Character.isSpace(s.charAt(0))) {
-                    if (key == null) throw new MailException.Malformed("Message began with a blank line; no headers");
-                    head.put(key, head.get(key) + " " + s.trim());
-                    continue;
-                }
-                if (s.indexOf(':') == -1) throw new MailException.Malformed("Header line does not contain colon: " + s);
-                key = s.substring(0, s.indexOf(':')).toLowerCase();
-                for(int i=0; i<key.length(); i++)
-                    if (key.charAt(i) < 33 || key.charAt(i) > 126)
-                        throw new MailException.Malformed("Header key \""+key+"\" contains invalid character \"" + key.charAt(i) + "\"");
-                String val = s.substring(s.indexOf(':') + 1).trim();
-                if (get(key) != null) val = get(key) + " " + val; // just append it to the previous one;
-                head.put(key, val);
-            }
-            this.raw = all.toString();
-            this.lines = lines;
-
-            Enumeration e = head.keys();
-            boolean mime = assumeMime | (gets("mime-version") != null && gets("mime-version").trim().equals("1.0"));
-            /*
-            while(e.hasMoreElements()) {
-                String k = (String)e.nextElement();
-                String v = (String)head.get(k);
-                if (mime) k = MIME.RFC2047.decode(k);
-                v = uncomment(v);
-                if (mime) v = MIME.RFC2047.decode(v);
-                head.put(k, v);
+                //lines++;
             }
-            */
         }
     }
+    */
 }