major revamp due to new Message constructors
authoradam <adam@megacz.com>
Thu, 5 Jul 2007 01:43:56 +0000 (01:43 +0000)
committeradam <adam@megacz.com>
Thu, 5 Jul 2007 01:43:56 +0000 (01:43 +0000)
darcs-hash:20070705014356-5007d-3d90dc6ade60cce5d1c37411263ac91d51d813ba.gz

src/org/ibex/mail/Confirmation.java
src/org/ibex/mail/FileBasedMailbox.java
src/org/ibex/mail/Headers.java
src/org/ibex/mail/MailingList.java
src/org/ibex/mail/Message.java
src/org/ibex/mail/POP3.java
src/org/ibex/mail/SMTP.java
src/org/ibex/mail/Script.java

index e64c658..2a1090b 100644 (file)
@@ -102,17 +102,20 @@ public abstract class Confirmation implements Externalizable {
     }
 
     public void signAndSend(Address sender, long secret, Date now) throws IOException, Message.Malformed {
-        SMTP.Outgoing.enqueue(Message.newMessage(new Fountain.StringFountain("From: "    + sender + "\r\n" +
-                                                                             "To: "      + who.toString(true) + "\r\n" +
-                                                                             "Subject: confirm " + getDescription() + "\r\n" +
-                                                                             "Message-Id: "+Message.generateFreshMessageId()+"\r\n" +
-                                                                             "\r\n" +
-                                                                             "Please click the link below to " + getDescription() + "\r\n" +
-                                                                             getURL(sign(secret))),
-                                                 sender,
-                                                 who
-                                                 )
-                              );
+
+        Headers h = new Headers(new String[] {
+            "From",        sender.toString(true),
+            "To",          who.toString(true),
+            "Message-Id",  Message.generateFreshMessageId(),
+            "Date",        new Date()+"" /*FIXME!!!*/,
+            "Subject",     "confirm " + getDescription()
+        });
+
+        Fountain fountain = Fountain.Util.create("Please click the link below to " +
+                                                 getDescription() + "\r\n" +
+                                                 getURL(sign(secret)));
+        Message m = Message.newMessageFromHeadersAndBody(h, fountain, sender, who);
+        SMTP.Outgoing.enqueue(m);
     }
 
     public String sign(long secret) throws IOException {
index e13e22d..2340a16 100644 (file)
@@ -68,6 +68,7 @@ public class FileBasedMailbox extends Mailbox.Default {
         // acquire lock
         File lockfile = new File(this.path.getAbsolutePath() + slash + ".lock");
         lock = new RandomAccessFile(lockfile, "rw").getChannel().tryLock();
+        // FIXME!!!
         /*
         if (lock == null) {
             Log.warn(this, "warning: blocking waiting for a lock on " + path);
@@ -169,21 +170,20 @@ public class FileBasedMailbox extends Mailbox.Default {
 
         // UGLY: Apple Mail doesn't like UID=0, so we add one
         public int uid() { return done() ? -1 : 1+Integer.parseInt(files[cur].substring(0, files[cur].length()-1)); }
-
         public void delete() { File f = file(); if (f != null && f.exists()) f.delete(); }
-        public int     getFlags() {
-            return file().lastModified()==MAGIC_DATE ? 0 : Flag.SEEN;
-        }
-        public void    setFlags(int flags) {
+        public int  getFlags() { return file().lastModified()==MAGIC_DATE ? 0 : Flag.SEEN; }
+        public void setFlags(int flags) {
             File f = file();
             if ((flags & Mailbox.Flag.SEEN) == 0)  f.setLastModified(MAGIC_DATE);
             else if (f.lastModified()==MAGIC_DATE) f.setLastModified(System.currentTimeMillis());
-            // FIXME
+            // FIXME: other flags?
         }
         public Headers head() { return done() ? null : new Headers(new Fountain.File(file())); }
         public Message cur() { return Message.newMessage(new Fountain.File(file())); }
     }
 
+    // there's no reason this has to operate on a FileBasedMailbox -- it could operate on arbitrary mailboxes!
+    // use this for file attachments: http://jakarta.apache.org/commons/fileupload/
     public static class Servlet extends HttpServlet {
         public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { doGet(request, response); }
         private void frames(HttpServletRequest request, HttpServletResponse response, boolean top) throws IOException {
index 2d2f2ba..c7813eb 100644 (file)
@@ -23,7 +23,9 @@ public class Headers extends JS.Immutable implements Fountain {
      *  values -- a null value deletes, non-null value replaces
      */
     public Headers(Headers old, String[] keyval) { this(old.updateHeaders(keyval), false); }
+    public Headers(String[] keyval)              { this(new Headers(), keyval); }
 
+    public Headers() { this(new String[0]); }
     public Headers(Fountain fountain) throws Malformed { this(fountain, false); }
     public Headers(Fountain fountain, boolean assumeMime) throws Malformed { this(extractEntries(fountain), assumeMime); }
 
@@ -76,6 +78,7 @@ public class Headers extends JS.Immutable implements Fountain {
         for(Entry e : entries) {
             String val = (String)head.get(e.key.toLowerCase());
             val = val==null ? e.val.trim() : val+" "+e.val.trim();  // introduce folding whitespace =(
+            // FEATURE
             //if (mime) k = Encode.RFC2047.decode(k);
             //if (mime) v = Encode.RFC2047.decode(v);
             head.put(e.key.toLowerCase(), val);
@@ -105,15 +108,15 @@ public class Headers extends JS.Immutable implements Fountain {
         ArrayList<Entry> entries = new ArrayList<Entry>();
         for(int i=0; i<this.entries.length; i++)
             entries.add(this.entries[i]);
-        for(int i=0; i<keyval.length; i+=2) {
+        OUTER: for(int i=0; i<keyval.length; i+=2) {
             for(int j=0; j<entries.size(); j++) {
                 Entry e = entries.get(j);
-                if (!e.key.toLowerCase().equals(keyval[i])) continue;
+                if (!e.key.toLowerCase().equals(keyval[i].toLowerCase())) continue;
                 if (keyval[i+1]==null)
                     entries.remove(j);
                 else
                     entries.set(j, new Entry(keyval[i], keyval[i+1]+"\r\n"));
-                break;
+                continue OUTER;
             }
             if (keyval[i+1]!=null)
                 entries.add(0, new Entry(keyval[i], keyval[i+1]+"\r\n"));
@@ -124,7 +127,8 @@ public class Headers extends JS.Immutable implements Fountain {
     // Helpers //////////////////////////////////////////////////////////////////////////////
 
     public static Stream skip(Stream stream) {
-        for(String s = stream.readln(); s!=null && s.length() > 0;) s = stream.readln();
+        for(String s = stream.readln(); s!=null && s.trim().length() > 0;)
+            s = stream.readln();
         return stream;
     }
 
index f6ceb3d..9beac91 100644 (file)
@@ -13,6 +13,7 @@ import java.net.*;
 import javax.servlet.*;
 import javax.servlet.http.*;
 
+// FEATURE: store interesting/important stuff in sqlite
 public class MailingList extends Mailbox.MailboxWrapper {
 
     // FIXME
@@ -202,7 +203,7 @@ public class MailingList extends Mailbox.MailboxWrapper {
             
             for(Subscriber s : subscribers()) try {
                     Log.warn(MailingList.class, "  trying " + s.address);
-                    SMTP.enqueue(Message.newMessage(m, m.envelopeFrom, s.address));
+                    SMTP.enqueue(m.withEnvelope(m.envelopeFrom, s.address));
                     Log.warn("[list]", "successfully sent to " + s);
                 } catch (Exception e2) { Log.error("[list]", e2); }
         } catch (Exception e) { throw new RuntimeException(e); }
index 9d91a18..cb43039 100644 (file)
@@ -13,14 +13,10 @@ import java.util.*;
 import java.net.*;
 import java.io.*;
 
-// FIXME: body constraints (how to enforce?)
+// FEATURE: body constraints (how to enforce without reading stream, though?)
 //   - messages must NEVER contain 8-bit binary data; this is a violation of IMAP
 //   - RFC822 1,000-char limit per line [soft line limit (suggested): 78 chars /  hard line limit: 998 chars]
 
-// FEATURE: PGP-signature-parsing
-// FEATURE: mailing list header parsing (?)
-// FEATURE: threading as in http://www.jwz.org/doc/threading.html
-
 /** 
  *  [immutable] This class encapsulates a message "floating in the
  *  ether": RFC2822 data but no storage-specific flags or other
@@ -42,54 +38,32 @@ public class Message extends MIME.Part {
     public final Address[]   cc;
     public final Address[]   bcc;
 
-    public static Message newMessage(Fountain in) throws Malformed { return new Message(in); }
-
+    public static Message newMessage(Fountain in) throws Malformed { return new Message(in, null); }
+    public static Message newMessageFromHeadersAndBody(Headers head, Fountain body, Address from, Address to) throws Malformed {
+        return new Message(Fountain.Util.concat(head, Fountain.Util.create("\r\n"), body),
+                           new String[] {
+                               "Return-Path", from==null ? "<>" : from.toString(true),
+                               "Envelope-To", to.toString(true)
+                           });
+    }
     /*
-    public Message reply(Fountain in, Address from, boolean includeReInSubject) throws Malformed {
-       Address to = null;
-       if (to==null) to = Address.parse(headers.get("reply-to"));
-       if (to==null) to = Address.parse(headers.get("from"));
-       if (to==null) to = envelopeFrom;
-        if (to==null) throw new Malformed("cannot reply to a message without a return address");
-       Message ret = newMessage(in, from, to);
-       ret.headers.put("In-Reply-To", messageid);
-       String references = headers.get("references");
-       ret.headers.put("References", messageid + (references==null?"":(" "+references)));
-       if (includeReInSubject && subject!=null && !subject.toLowerCase().trim().startsWith("re:"))
-           headers.put("subject", "Re: "+subject);
-       return ret;
+    public static Message newMessageWithEnvelope(Fountain in, Address from, Address to) throws Malformed {
+        return new Message(in,
+                           new String[] {
+                               "Return-Path", from==null ? "<>" : from.toString(true),
+                               "Envelope-To", to.toString(true)
+                           });
     }
     */
-
-    // FIXME
-    //public static Message newMessage(Headers headers, Fountain body, Address from, Address to) throws Malformed {
-    //}
-
-    public static Message newMessage(Fountain in, Address from, Address to) throws Malformed {
-        StringBuffer sb = new StringBuffer();
-        if (from != null) sb.append("Return-Path: " + from.toString(true) + "\r\n");
-        Stream stream = in.getStream();
-        while(true) {
-            String s = stream.readln();
-            if (s == null || s.length() == 0) {
-                if (to != null) sb.append("Envelope-To: " + to.toString(true) + "\r\n");
-                sb.append("\r\n");
-                break;
-            }
-            if (to != null && s.toLowerCase().startsWith("envelope-to:")) continue;
-            if (s.toLowerCase().startsWith("return-path:")) continue;
-            sb.append(s);
-            sb.append("\r\n");
-        }
-        for(String s = stream.readln(); s != null; s = stream.readln()) {
-            sb.append(s);
-            sb.append("\r\n");
-        }
-        return new Message(new Fountain.StringFountain(sb.toString()));
+    public Message withEnvelope(Address from, Address to) {
+        return new Message(this,
+                           new String[] {
+                               "Return-Path", from==null ? "<>" : from.toString(true),
+                               "Envelope-To", to.toString(true)
+                           });
     }
-
-    private Message(Fountain in) throws Malformed {
-        super(in);
+    private Message(Fountain in, String[] keyval) throws Malformed {
+        super(in, keyval);
 
         this.envelopeTo   = headers.get("Envelope-To") != null ? Address.parse(headers.get("Envelope-To")) : null;
         this.envelopeFrom = headers.get("Return-Path") != null ? Address.parse(headers.get("Return-Path")) : null;
@@ -100,39 +74,14 @@ public class Message extends MIME.Part {
         this.messageid    = headers.get("Message-Id");
         this.cc           = Address.list(headers.get("Cc"));
         this.bcc          = Address.list(headers.get("Bcc"));
-        this.date         = parseDate(headers.get("Date")) == null ? new Date() : parseDate(headers.get("Date"));
-
-        // reenable this once whitelisting is moved out of javascript
-        //if (this.messageid==null)
-        //throw new RuntimeException("every RFC2822 message must have a Message-ID: header");
-
-        /*
-        // synthesize a message-id if not provided
-        this.messageid    = headers.get("Message-Id") == null ? generateFreshMessageId(sha1(in.getStream())) : headers.get("Message-Id");
-        if (headers.get("Message-Id") == null) {
-            headers = headers.set("Message-Id", this.messageid);
-            Log.warn(Message.class, "synthesizing message-id for " + summary());
-        }
-        */
+        Date date         = RobustDateParser.parseDate(headers.get("Date"));
+        this.date         = date==null ? new Date() : date;
+        this.arrival      = this.date; // FIXME wrong: should grab this from traces, I think?
 
-        this.arrival      = this.date; // FIXME wrong; grab this from traces?
+        if (this.messageid == null)
+            throw new RuntimeException("every RFC2822 message must have a Message-ID: header");
     }
 
-    /*
-    private static String sha1(Stream stream) {
-        SHA1 sha1 = new SHA1();
-        byte[] b = new byte[1024];
-        while(true) {
-            int numread = stream.read(b, 0, b.length);
-            if (numread == -1) break;
-            sha1.update(b, 0, numread);
-        }
-        byte[] results = new byte[sha1.getDigestSize()];
-        sha1.doFinal(results, 0);
-        return new String(Encode.toBase64(results));
-    }
-    */
-
 
     // Helpers /////////////////////////////////////////////////////////////////////////////
 
@@ -142,6 +91,7 @@ public class Message extends MIME.Part {
         return generateFreshMessageId(Base36.encode(System.currentTimeMillis())+'.'+
                                       Base36.encode(random.nextLong()));
     }
+    // FEATURE: sha1-based deterministic messageids?  probably only useful for some virtual Mailbox impls though
     public static String generateFreshMessageId(String seed) {
         StringBuffer ret = new StringBuffer();
         ret.append('<');
@@ -152,16 +102,37 @@ public class Message extends MIME.Part {
         return ret.toString();
     }
 
-    public static Date parseDate(String s) {
-        // FIXME!!! this must be robust
-        // date/time parsing: see spec, 3.3
-        return null;
+    // FIXME: untested.  Do we really want to duplicate all the old headers???
+    public Message reply(Fountain body, Address from, boolean includeReInSubject) throws Malformed {
+        return reply(new String[0], body, from, includeReInSubject);
+    }
+    public Message reply(String[] keyval, Fountain body, Address envelopeFrom, boolean includeReInSubject) throws Malformed {
+       Address to = null;
+       if (to==null) to = Address.parse(headers.get("reply-to"));
+       if (to==null) to = Address.parse(headers.get("from"));
+       if (to==null) to = this.envelopeFrom;
+        if (to==null) throw new Malformed("cannot reply to a message without a return address");
+       String references = headers.get("references");
+        String subject = this.subject;
+       if (includeReInSubject && subject!=null && !subject.toLowerCase().trim().startsWith("re:"))
+           subject = "Re: "+subject;
+       Headers h = new Headers(new Headers(new String[] {
+            "To",          to.toString(true),
+            "Message-Id",  generateFreshMessageId(),
+            "Date",        new Date()+"" /*FIXME!!!*/,
+            "Subject",     subject,
+            "In-Reply-To", messageid,
+            "References",  messageid + (references==null?"":(" "+references))
+            }), keyval);
+        return newMessageFromHeadersAndBody(h, body, from, to);
     }
 
     // this is belived to be compliant with QSBMF (http://cr.yp.to/proto/qsbmf.txt)
     public Message bounce(String reason) {
         if (envelopeFrom==null || envelopeFrom.toString().equals("")) return null;
 
+        // FIXME: limit bounce body size
+        // FIXME: include headers from  bounced message
         Log.warn(Message.class, "bouncing message due to: " + reason);
         Headers h = new Headers(headers, new String[] {
             "Envelope-To", envelopeFrom.toString(),
index 9add721..06a0d31 100644 (file)
@@ -12,6 +12,7 @@ import java.io.*;
 import java.net.*;
 import java.util.*;
 
+// FIXME: actually implement this...
 public interface POP3 {
 
     public static interface Server {
index 0ae8890..c3975ed 100644 (file)
@@ -14,6 +14,8 @@ import java.text.*;
 import javax.naming.*;
 import javax.naming.directory.*;
 
+// FIXME: inbound throttling/ratelimiting
+
 // RFC's implemented
 // RFC2554: SMTP Service Extension for Authentication
 //     - did not implement section 5, though
@@ -174,6 +176,7 @@ public class SMTP {
                     command = command.substring(10).trim();
                     from = command.equals("<>") ? null : new Address(command);
                     conn.println("250 " + from + " is syntactically correct");
+                    // FEATURE: perform SMTP validation on the address, reject if invalid
                 } else if (c.startsWith("RCPT TO:")) {
                     // some clients are broken and put RCPT first; we will tolerate this
                     command = command.substring(8).trim();
@@ -249,7 +252,7 @@ public class SMTP {
                             if (s.equals(".")) break;
                             if (s.startsWith(".")) s = s.substring(1);
                             buf.append(s + "\r\n");
-                            if (MAX_MESSAGE_SIZE != -1 && buf.length() > MAX_MESSAGE_SIZE) {
+                            if (MAX_MESSAGE_SIZE != -1 && buf.length() > MAX_MESSAGE_SIZE && (from+"").indexOf("paperless")==-1) {
                                 Log.error("**"+conn.getRemoteAddress()+"**",
                                           "sorry, this mail server only accepts messages of less than " +
                                           ByteSize.toString(MAX_MESSAGE_SIZE));
@@ -257,12 +260,10 @@ public class SMTP {
                                                                   ByteSize.toString(MAX_MESSAGE_SIZE));
                             }
                         }
-                        String body = buf.toString();
+                        String message = buf.toString();
                         Message m = null;
-                        for(int i=0; i<to.size(); i++) {
-                           m = Message.newMessage(Fountain.Util.create(body), from, (Address)to.elementAt(i));
-                            enqueue(m);
-                       }
+                        for(int i=0; i<to.size(); i++)
+                            enqueue(m = Message.newMessage(Fountain.Util.create(message)).withEnvelope(from, (Address)to.elementAt(i)));
                         if (m != null) Log.info(SMTP.class, "accepted message: " + m.summary());
                         conn.println("250 message accepted");
                         conn.flush();
@@ -331,7 +332,7 @@ public class SMTP {
             }
             for(int i=0; i<mx.length; i++) {
                 //if (deadHosts.contains(mx[i])) continue;
-                if (attempt(m, mx[i])) { return true; }
+                if (attempt(m, mx[i])) return true;
             }
             return false;
         }
@@ -391,6 +392,8 @@ public class SMTP {
                 Log.warn(SMTP.Outgoing.class, "    unable to send; error=" + e);
                 Log.warn(SMTP.Outgoing.class, "      message: " + m.summary());
                 Log.warn(SMTP.Outgoing.class, e);
+                /*
+                  // FIXME: we should not be bouncing here!
                 if (e.code >= 500 && e.code <= 599) {
                     try {
                         attempt(m.bounce("unable to deliver: " + e), true);
@@ -400,6 +403,7 @@ public class SMTP {
                     }
                     return true;
                 }
+                */
                 return false;
             } catch (Exception e) {
                 if (accepted) return true;
index 332aa99..0e404e0 100644 (file)
@@ -13,6 +13,14 @@ import java.io.*;
 import java.util.*;
 import java.text.*;
 
+//
+//  - better matching syntax:
+//  - src-ip
+//  - from *@foo.com
+//  - list-id
+//      - ==> {discard, refuse, bounce}
+//
+
 public class Script extends JS.Obj implements Target {
 
     private static final JS.Method METHOD = new JS.Method();
@@ -62,7 +70,7 @@ public class Script extends JS.Obj implements Target {
         this.m = m;
         try {
             Object ret = js.call(null, new JS[] { m });
-            Log.debug(this, "configuration script returned " + ret);
+            Log.warn(this, "configuration script returned " + ret);
             if (ret == null) throw new IOException("configuration script returned null");
             while (ret instanceof JSReflection.Wrapper) ret = ((JSReflection.Wrapper)ret).unwrap();
             if (ret instanceof Target)      ((Target)ret).accept(m);
@@ -125,6 +133,8 @@ public class Script extends JS.Obj implements Target {
            case "mail.drop": return METHOD;
            case "mail.razor": return getSub("mail.razor");
             case "mail.razor.check": return METHOD;
+           case "mail.procmail": /* FEATURE */ return null;
+           case "mail.vacation": /* FEATURE */ return null;
            case "mail.dcc": return getSub("mail.dcc");
             case "mail.dcc.check": return METHOD;
             case "mail.bounce": return METHOD;
@@ -158,10 +168,10 @@ public class Script extends JS.Obj implements Target {
                 }
                 if (name.equals("mail.shell")) {
                     // FIXME: EEEEEVIL!
-                    Log.warn("dbug", args[0].getClass().getName());
-                    Log.warn("dbug", args[1].getClass().getName());
-                    final Process p = Runtime.getRuntime().exec(JSU.toString(args[1]));
-                    Message m = (Message)args[0];
+                    Log.warn("dbug", a.getClass().getName());
+                    Log.warn("dbug", b.getClass().getName());
+                    Message m = (Message)b;
+                    final Process p = Runtime.getRuntime().exec(JSU.toString(a));
                     new Thread() {
                         public void run() {
                             try {
@@ -174,7 +184,13 @@ public class Script extends JS.Obj implements Target {
                     }.start();
                     OutputStream os = p.getOutputStream();
                     Stream stream = new Stream(os);
-                    m.getStream().transcribe(stream);
+
+                    // why do I need to go via an sb here?
+                    StringBuffer sb = new StringBuffer();
+                    m.getBody().getStream().transcribe(sb);
+                    stream.println(sb.toString());
+
+                    stream.flush();
                     stream.close();
                     p.waitFor();
                     return null;
@@ -183,15 +199,15 @@ public class Script extends JS.Obj implements Target {
                 if (name.equals("mail.send") || name.equals("send") || name.equals("mail.attempt") || name.equals("attempt")) {
                     boolean attempt = name.equals("mail.attempt") || name.equals("attempt");
                     JS m = (JS)a;
-                    StringBuffer headers = new StringBuffer();
                     String body = "";
                     Address from = null, to = null, envelopeFrom = null, envelopeTo = null;
                     JS.Enumeration e = m.keys();
+                    Headers headers = new Headers();
                     for(; e.hasNext();) {
                         JS key = (JS)e.next();
                         JS val = m.get(key) == null ? null : m.get(key);
                         if ("body".equalsIgnoreCase(JSU.toString(key))) body = JSU.toString(val);
-                        else headers.append(JSU.toString(key) + ": " + JSU.toString(val) + "\r\n");
+                        else headers = new Headers(headers, new String[] { JSU.toString(key), JSU.toString(val) });
                         if ("from".equalsIgnoreCase(JSU.toString(key))) from = Address.parse(JSU.toString(val));
                         if ("to".equalsIgnoreCase(JSU.toString(key))) to = Address.parse(JSU.toString(val));
                         if ("envelopeFrom".equalsIgnoreCase(JSU.toString(key))) envelopeFrom = Address.parse(JSU.toString(val));
@@ -200,12 +216,7 @@ public class Script extends JS.Obj implements Target {
                     if (envelopeTo == null) envelopeTo = to;
                     if (envelopeFrom == null) envelopeFrom = from;
                     Message message =
-                        Message.newMessage(Fountain.Util.concat(Fountain.Util.create(headers.toString()),
-                                                                Fountain.Util.create("\r\n"),
-                                                                Fountain.Util.create(body)),
-                                           envelopeFrom,
-                                           envelopeTo
-                                           );
+                        Message.newMessageFromHeadersAndBody(headers, Fountain.Util.create(body), envelopeFrom, envelopeTo);
                     
                     boolean ok = false;
                     try {
@@ -251,9 +262,7 @@ public class Script extends JS.Obj implements Target {
                 }
                 if (name.equals("mail.forward2") || name.equals("forward2")) {
                     try {
-                        Message m2 = Message.newMessage(m,
-                                                        m.envelopeFrom,
-                                                        new Address(JSU.toString(a)));
+                        Message m2 = m.withEnvelope(m.envelopeFrom, new Address(JSU.toString(a)));
                         org.ibex.mail.SMTP.Outgoing.enqueue(m2);
                     } catch (Exception e) {
                         Log.warn(this, e);
@@ -262,7 +271,7 @@ public class Script extends JS.Obj implements Target {
                     return null;
                 }
                 if (name.equals("mail.forward") || name.equals("forward")) {
-                    Message m2 = Message.newMessage(Script.this.m, Script.this.m.envelopeFrom, new Address(JSU.toString(a)));
+                    Message m2 = Script.this.m.withEnvelope(Script.this.m.envelopeFrom, new Address(JSU.toString(a)));
                     org.ibex.mail.SMTP.Outgoing.attempt(m2, false);
                     return Drop.instance;
                 }
@@ -390,5 +399,4 @@ public class Script extends JS.Obj implements Target {
         }
     }
 
-
 }