}
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 {
// 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);
// 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 {
* 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); }
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);
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"));
// 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;
}
import javax.servlet.*;
import javax.servlet.http.*;
+// FEATURE: store interesting/important stuff in sqlite
public class MailingList extends Mailbox.MailboxWrapper {
// FIXME
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); }
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
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;
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 /////////////////////////////////////////////////////////////////////////////
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('<');
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(),
import java.net.*;
import java.util.*;
+// FIXME: actually implement this...
public interface POP3 {
public static interface Server {
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
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();
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));
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();
}
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;
}
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);
}
return true;
}
+ */
return false;
} catch (Exception e) {
if (accepted) return true;
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();
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);
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;
}
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 {
}.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;
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));
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 {
}
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);
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;
}
}
}
-
}