total overhaul; were using MIME and MIME.Part now
[org.ibex.mail.git] / src / org / ibex / mail / MIME.java
index 58d0621..8f4b99c 100644 (file)
@@ -1,11 +1,54 @@
 package org.ibex.mail;
+import org.ibex.crypto.*;
+import org.ibex.util.*;
+import org.ibex.mail.protocol.*;
+import org.ibex.io.*;
+import java.util.*;
+import java.net.*;
+import java.io.*;
 
 // FEATURE: MIME RFC2045, 2046, 2049
 
 /** This class contains logic for encoding and decoding MIME multipart messages */
 public class MIME {
-    public static class QuotedPrintable {
 
+    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);
+            }
+            */
+            return s;
+        }
+    }
+
+    public static class QuotedPrintable {
         public static String decode(String s, boolean lax) {
         //
         //   =XX  -> hex representation, must be uppercase
@@ -20,4 +63,148 @@ public class MIME {
             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) throw new MailException.Malformed("content-type lacks a forward slash: \""+header+"\"");
+            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 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;
+        private final boolean  last;
+
+        private Part[] parseParts(Stream stream) {
+            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;
+            while(true) {
+                Part p = new Part(stream, (String)content.parameters.get("boundary"), true);
+                v.addElement(p);
+                if (p.last) break;
+            }
+            return (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.get("mime-version")!=null&&headers.get("mime-version").trim().equals("1.0"));
+            String ctype     = headers.get("content-type");
+            String encoding  = headers.get("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.get("content-description"), headers.get("content-id"), encoding);
+            if (content.composite) { subparts = parseParts(stream); body = null; last = false; return; }
+            subparts = null;
+            boolean last = false;
+            StringBuffer body = new StringBuffer();
+            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 + "--");
+                    break;
+                }
+                body.append(s);
+            }
+            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;
+        }
+    }
+
+    public static class Headers extends org.ibex.js.JSReflection {
+       private Hashtable head = new Hashtable();
+        public final String raw;
+        public String get(String s) { return (String)head.get(s.toLowerCase()); }
+        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;
+            for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
+                all.append(s);
+                all.append("\r\n");
+                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();
+
+            Enumeration e = head.keys();
+            boolean mime = assumeMime | (get("mime-version") != null && get("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);
+            }
+        }
+    }
 }