implemented bounce messages
authoradam <adam@megacz.com>
Wed, 23 Feb 2005 04:38:20 +0000 (04:38 +0000)
committeradam <adam@megacz.com>
Wed, 23 Feb 2005 04:38:20 +0000 (04:38 +0000)
darcs-hash:20050223043820-5007d-09fd8cd503a883f7b84d70b945b64bae711a22ce.gz

src/org/ibex/mail/ContentType.java [new file with mode: 0644]
src/org/ibex/mail/Headers.java [new file with mode: 0644]
src/org/ibex/mail/MIME.java
src/org/ibex/mail/Message.java
src/org/ibex/mail/protocol/IMAP.java
src/org/ibex/mail/protocol/NNTP.java
src/org/ibex/mail/protocol/SMTP.java
src/org/ibex/mail/target/Drop.java [new file with mode: 0644]
src/org/ibex/mail/target/Script.java

diff --git a/src/org/ibex/mail/ContentType.java b/src/org/ibex/mail/ContentType.java
new file mode 100644 (file)
index 0000000..7fc7ca6
--- /dev/null
@@ -0,0 +1,62 @@
+// 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.io.*;
+import org.ibex.js.*;
+import java.util.*;
+import java.net.*;
+import java.io.*;
+
+// 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 class ContentType 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 Hash      parameters = new Hash();
+    public ContentType(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 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);
+        }
+    }
+}
diff --git a/src/org/ibex/mail/Headers.java b/src/org/ibex/mail/Headers.java
new file mode 100644 (file)
index 0000000..70ad93c
--- /dev/null
@@ -0,0 +1,124 @@
+// 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.*;
+
+public class Headers extends JS.Immutable implements Fountain {
+    private final Hash head = new Hash();
+    private final Hash headModified = new Hash();
+    public        int lines;
+    public  final boolean mime;
+
+    private String raw;
+    private StringFountain fountain;
+
+    public String get(String s) {
+        String ret = (String)headModified.get(s.toLowerCase());
+        if (ret==null) ret = (String)head.get(s.toLowerCase());
+        return ret;
+    }
+    public void put(String k, String v) {
+        Stream stream = getStream();
+        StringBuffer all = new StringBuffer();
+        int lines = 0;
+        boolean good = false;
+        String key = null;
+        for(String s = stream.readln(); s != null && !s.equals(""); s = stream.readln()) {
+            if (Character.isSpace(s.charAt(0))) { all.append(s); all.append("\r\n"); lines++; continue; }
+            if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s);
+            key = s.substring(0, s.indexOf(':')).toLowerCase();
+            lines++;
+            if (key.toLowerCase().equals(k.toLowerCase())) {
+                good = true;
+                all.append(k + ": " + v + "\r\n");
+                continue;
+            }
+            all.append(s);
+            all.append("\r\n");
+        }
+        if (!good) {
+            lines++;
+            all.append(k + ": " + v + "\r\n");
+        }
+        this.raw = all.toString();
+        this.lines = lines;
+        this.fountain = new Fountain.StringFountain(this.raw);
+    }
+    public JS get(JS s) throws JSExn { return JSU.S(get(JSU.toString(s).toLowerCase())); }
+
+    public Stream getStream() { return fountain.getStream(); }
+    public int    getLength() { return fountain.getLength(); }
+    public int    getNumLines() { return fountain.getNumLines(); }
+    public Stream getStreamWithCRLF() { return new Stream(raw+"\r\n"); }
+    
+    // FIXME
+    public String getString() { return raw; }
+
+    public Headers(Stream stream) throws Malformed { this(stream, false); }
+    public Headers(Stream stream, boolean assumeMime) throws 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 Malformed("Message began with a blank line; no headers");
+                head.put(key, head.get(key) + " " + s.trim());
+                continue;
+            }
+            if (s.indexOf(':') == -1) throw new 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 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.fountain = new Fountain.StringFountain(this.raw);
+        this.lines = lines;
+        this.mime = assumeMime | (get("mime-version") != null && get("mime-version").trim().equals("1.0"));
+        /*
+          java.util.Enumeration e = head.keys();
+          while(e.hasNext()) {
+          String k = (String)e.next();
+          String v = (String)head.get(k);
+          if (mime) k = Encode.RFC2047.decode(k);
+          v = uncomment(v);
+          if (mime) v = Encode.RFC2047.decode(v);
+          head.put(k, v);
+          }
+        */
+    }
+
+    // Helpers //////////////////////////////////////////////////////////////////////////////
+
+    public static Stream skip(Stream stream) {
+        for(String s = stream.readln(); s!=null && s.length() > 0;) s = stream.readln();
+        return stream;
+    }
+
+    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;
+    }
+}
index 3635fd1..d395f23 100644 (file)
@@ -25,16 +25,23 @@ public class MIME {
         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 int getLength()    { return all.getLength(); }
+        public Fountain getBody()   { return body; }
 
-        public Stream getBody()   {
-            return /*
-                     "quoted-printable".equals(encoding) ? Encode.QuotedPrintable.decode(body.toString(),false) :
-                     "base64".equals(encoding)           ? Encode.fromBase64(body.toString()) :
-                   */
-                Headers.skip(getStream());
+        private class BodyFountain implements Fountain {
+            public int getNumLines()  { return Stream.countLines(getStream()); }
+            public int getLength()    { return Part.this.getLength() - headers.getLength() - 2; }
+            public Stream getStream() {
+                return /*
+                         "quoted-printable".equals(encoding) ? Encode.QuotedPrintable.decode(body.toString(),false) :
+                         "base64".equals(encoding)           ? Encode.fromBase64(body.toString()) :
+                       */
+                    Headers.skip(all.getStream());
+            }
         }
 
         public Part(Fountain all) {
@@ -48,6 +55,7 @@ public class MIME {
             }
             this.contentType = new ContentType(ctype, headers.get("content-description"), headers.get("content-id"), encoding);
             this.all = all;
+            this.body = new BodyFountain();
         }
 
         /*
index 4d02e43..7c5635f 100644 (file)
@@ -54,7 +54,7 @@ public class Message extends MIME.Part {
             String s = stream.readln();
             if (s == null) break;
             if (to != null && s.toLowerCase().startsWith("envelope-to:")) continue;
-            if (from != null && s.toLowerCase().startsWith("return-path:")) continue;
+            if (s.toLowerCase().startsWith("return-path:")) continue;
             if (s.length() == 0) {
                 if (to != null) sb.append("Envelope-To: " + to.toString(true) + "\r\n");
                 sb.append("\r\n");
@@ -107,12 +107,37 @@ public class Message extends MIME.Part {
         // date/time parsing: see spec, 3.3
         return null;
     }
-   
-    //  use null-sender for error messages (don't send errors to the null addr)
+
+    // this is belived to be compliant with QSBMF (http://cr.yp.to/proto/qsbmf.txt)
     public Message bounce(String reason) {
-        Log.warn(Message.class, "bounce not implemented");
-        return null;
-    }  // FIXME!
+        if (envelopeFrom==null || envelopeFrom.toString().equals("")) return null;
+
+        Headers h = new Headers(headers.getStream());
+        h.put("Envelope-To", envelopeFrom.toString());
+        h.put("Return-Path", "<>");
+        h.put("From",        "MAILER-DAEMON");
+        h.put("To",          envelopeFrom.toString());
+        h.put("Subject",     "failure notice");
+
+        String error =
+            "Hi. This is the Ibex Mail Server.  I'm afraid I wasn't able to deliver\r\n"+
+            "your message to the following addresses. This is a permanent error;\r\n"+
+            "I've given up.  Sorry it didn't work out\r\n."+
+            "\r\n"+
+            "<"+envelopeTo.toString()+">:\r\n"+
+            reason+"\r\n"+
+            "\r\n"+
+            "--- Below this line is a copy of the message.\r\n"+
+            "\r\n";
+
+        try {
+            return newMessage(new Fountain.Concatenate(new Fountain.StringFountain(h.getString()+"\r\n"+error), getBody()));
+        } catch (Message.Malformed e) {
+            Log.error(this, "caught Message.Malformed in Message.bounce(); this should never happen");
+            Log.error(this, e);
+            return null;
+        }
+    }
 
     public String toString() { throw new RuntimeException("Message.toString() called"); }
     public String summary() { return "[" + envelopeFrom + " -> " + envelopeTo + "] " + subject; }
index 3a02998..d8ada8d 100644 (file)
@@ -42,6 +42,13 @@ public class IMAP {
     public IMAP() { }
     public static final float version = (float)0.2;
 
+    // FIXME this is evil
+    public static String getBodyString(Message m) {
+        StringBuffer sb = new StringBuffer();
+        m.getStream().transcribe(sb);
+        return sb.toString();
+    }
+
     // API Class //////////////////////////////////////////////////////////////////////////////
 
     public static interface Client {
@@ -227,7 +234,7 @@ public class IMAP {
             for(Mailbox.Iterator it = selected().iterator(q); it.next(); ) {
                 Message message = ((spec & (BODYSTRUCTURE | ENVELOPE | INTERNALDATE | FIELDS | FIELDSNOT | RFC822 |
                                             RFC822TEXT | RFC822SIZE | HEADERNOT | HEADER)) != 0) ? it.cur() : null;
-                int size = message == null ? 0 : message.size();
+                int size = message == null ? 0 : message.getLength();
                 client.fetch(it.num(), it.flags(), size, message, it.uid());
                 it.recent(false);
             }
@@ -437,9 +444,9 @@ public class IMAP {
                 } else if (s.equals("FLAGS")) {        spec|=FLAGS;        if(e){r.append(" ");r.append(Printer.flags(flags));}
                 } else if (s.equals("INTERNALDATE")) { spec|=INTERNALDATE; if(e){r.append(" ");r.append(Printer.date(m.arrival));}
                 } else if (s.equals("RFC822")) {       spec|=RFC822;       if(e){r.append(" ");r.append(Printer.message(m));}
-                } else if (s.equals("RFC822.TEXT")) {  spec|=RFC822TEXT;   if(e){r.append(" ");r.append(Printer.qq(m.getBodyString()));}
+                } else if (s.equals("RFC822.TEXT")) {  spec|=RFC822TEXT;   if(e){r.append(" ");r.append(Printer.qq(getBodyString(m)));}
                 } else if (s.equals("RFC822.HEADER")){ spec|=HEADER;if(e){r.append(" ");r.append(Printer.qq(m.headers.getString()+"\r\n"));}
-                } else if (s.equals("RFC822.SIZE")) {  spec|=RFC822SIZE;   if(e){r.append(" ");r.append(m.size());}
+                } else if (s.equals("RFC822.SIZE")) {  spec|=RFC822SIZE;   if(e){r.append(" ");r.append(m.getLength());}
                 } else if (s.equals("UID")) {          spec|=UID;          if(e){r.append(" ");r.append(muid); }
                 } else if (!(s.equals("BODY.PEEK") || s.equals("BODY"))) { throw new Server.No("unknown fetch argument: " + s);
                 } else {
@@ -455,9 +462,9 @@ public class IMAP {
                     Parser.Token[] list = t[++i].l();
                     s = list.length == 0 ? "" : list[0].s.toUpperCase();
                     r.append(s);
-                    if (list.length == 0)                   { spec |= RFC822TEXT;   if(e) payload = m.headers.getString()+"\r\n"+m.getBodyString(); }
-                    else if (s.equals("") || s.equals("1")) { spec |= RFC822TEXT;   if(e) payload = m.headers.getString()+"\r\n"+m.getBodyString(); }
-                    else if (s.equals("TEXT"))              { spec |= RFC822TEXT;   if(e) payload = m.getBodyString(); }
+                    if (list.length == 0)                   { spec |= RFC822TEXT;   if(e) payload = m.headers.getString()+"\r\n"+getBodyString(m); }
+                    else if (s.equals("") || s.equals("1")) { spec |= RFC822TEXT;   if(e) payload = m.headers.getString()+"\r\n"+getBodyString(m); }
+                    else if (s.equals("TEXT"))              { spec |= RFC822TEXT;   if(e) payload = getBodyString(m); }
                     else if (s.equals("HEADER"))            { spec |= HEADER;       if(e) payload = m.headers.getString()+"\r\n"; }
                     else if (s.equals("HEADER.FIELDS"))     { spec |= FIELDS;     payload=headers(r,t[i].l()[1].sl(),false,m,e); }
                     else if (s.equals("HEADER.FIELDS.NOT")) { spec |= FIELDSNOT;  payload=headers(r,t[i].l()[1].sl(),true,m,e); }
@@ -787,7 +794,7 @@ public class IMAP {
         }
         static String bodystructure(Message m) {
             // FIXME
-            return "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\") NIL NIL \"7BIT\" "+m.size()+" "+m.lines()+")";
+            return "(\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\") NIL NIL \"7BIT\" "+m.getLength()+" "+m.getNumLines()+")";
         }
         static String message(Message m) { return m.toString(); }
         static String date(Date d) { return "\""+d.toString()+"\""; }
index f1291b6..ddaeeba 100644 (file)
@@ -133,7 +133,7 @@ public class NNTP {
             if (head) println(a.message.headers.getString());
             if (head && body) println();
             if (body) {
-                Stream stream = a.message.getBody();
+                Stream stream = a.message.getBody().getStream();
                 while(true) {
                     s = stream.readln();
                     if (s == null) break;
@@ -201,7 +201,7 @@ public class NNTP {
                         try {
                             Message m = it.cur();
                             println(it.num()+"\t"+m.subject+"\t"+m.from+"\t"+m.date+"\t"+m.messageid+"\t"+
-                                    m.headers.get("references") + "\t" + m.size() + "\t" + m.lines());
+                                    m.headers.get("references") + "\t" + m.getLength() + "\t" + m.getNumLines());
                         } catch (Exception e) { Log.error(this, e); }
                     }
                     println(".");
index a26b2b9..f7463e8 100644 (file)
@@ -192,7 +192,11 @@ public class SMTP {
                     conn.println("HELO " + conn.vhost);
                     check(conn.readln(), conn);
                 }
-                conn.println("MAIL FROM:<" + m.envelopeFrom.user + "@" + m.envelopeFrom.host+">");  check(conn.readln(), conn);
+                if (m.envelopeFrom==null) {
+                    conn.println("MAIL FROM:<>");  check(conn.readln(), conn);
+                } else {
+                    conn.println("MAIL FROM:<" + m.envelopeFrom.user + "@" + m.envelopeFrom.host+">");  check(conn.readln(), conn);
+                }
                 conn.println("RCPT TO:<"   + m.envelopeTo.user + "@" + m.envelopeTo.host+">");      check(conn.readln(), conn);
                 conn.println("DATA");                          check(conn.readln(), conn);
                 Stream stream = m.getStream();
@@ -214,6 +218,7 @@ public class SMTP {
             } catch (Exception e) {
                 if (accepted) return true;
                 Log.warn(SMTP.Outgoing.class, "    unable to send; error=" + e);
+                Log.warn(SMTP.Outgoing.class, "      message: " + m.summary());
                 Log.warn(SMTP.Outgoing.class, e);
                 return false;
             } finally {
diff --git a/src/org/ibex/mail/target/Drop.java b/src/org/ibex/mail/target/Drop.java
new file mode 100644 (file)
index 0000000..c17c64a
--- /dev/null
@@ -0,0 +1,17 @@
+// 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.target;
+import java.io.*;
+import org.ibex.js.*;
+import org.ibex.util.*;
+import org.ibex.mail.*;
+import org.ibex.mail.target.*;
+
+public class Drop extends Target {
+    public static final Drop instance = new Drop();
+    public void accept(Message m) throws IOException, MailException {
+        Log.warn(this, "dropping message " + m.summary());
+    }
+}
index 912c732..d68d91e 100644 (file)
@@ -115,6 +115,8 @@ public class Script extends Target {
             case "mail.forward": return METHOD;
             case "mail.forward2": return METHOD;
             case "mail.send": return METHOD;
+            case "mail.drop": return Drop.instance;
+            case "mail.bounce": return METHOD;
             case "mail.my": return getSub("mail.my");
             case "mail.my.prefs": try {
                     return new org.ibex.js.Directory(new File("/etc/org.ibex.mail.prefs"));
@@ -163,6 +165,18 @@ public class Script extends Target {
                     if (!ok) throw new JSExn("SMTP server rejected message");
                     return JSU.T;
                 }
+                if (name.equals("mail.bounce")) {
+                    return new Target() {
+                            public void accept(Message m) throws MailException {
+                                try {
+                                    Message m2 = m.bounce(JSU.toString(a));
+                                    org.ibex.mail.protocol.SMTP.Outgoing.accept(m2);
+                                    Log.error(this, "BOUNCING! " + m2.summary());
+                                } catch (Exception e) {
+                                    Log.warn(this, e);
+                                }
+                            } };
+                }
                 if (name.equals("mail.forward2") || name.equals("forward2")) {
                     try {
                         Message m2 = Message.newMessage(new org.ibex.io.Fountain.StringFountain(m.toString()),
@@ -175,16 +189,12 @@ public class Script extends Target {
                     }
                     return null;
                 }
-                if (name.equals("mail.forward") || name.equals("forward")) { return new Target() {
-                        public void accept(Message m) throws MailException {
-                            try {
-                                Message m2 = Message.newMessage(m, m.envelopeFrom, new Address(JSU.toString(a)));
-                                org.ibex.mail.protocol.SMTP.Outgoing.accept(m2);
-                            } catch (Exception e) {
-                                throw new MailException(e.toString());
-                            }
-                        }
-                    }; }
+                if (name.equals("mail.forward") || name.equals("forward")) {
+                    Message m = (Message)a;
+                    Message m2 = Message.newMessage(m, m.envelopeFrom, new Address(JSU.toString(a)));
+                    org.ibex.mail.protocol.SMTP.Outgoing.attempt(m2);
+                    return Drop.instance;
+                }
                 if (name.equals("log.debug") || name.equals("debug")) {    JSU.debug(a== null ? "**null**" : JSU.toString(a)); return null; }
                 if (name.equals("log.info") || name.equals("info")) {     JSU.info(a== null ? "**null**" : JSU.toString(a)); return null; }
                 if (name.equals("log.warn") || name.equals("warn")) {     JSU.warn(a== null ? "**null**" : JSU.toString(a)); return null; }