--- /dev/null
+// 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);
+ }
+ }
+}
--- /dev/null
+// 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;
+ }
+}
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) {
}
this.contentType = new ContentType(ctype, headers.get("content-description"), headers.get("content-id"), encoding);
this.all = all;
+ this.body = new BodyFountain();
}
/*
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");
// 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; }
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 {
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);
}
} 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 {
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); }
}
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()+"\""; }
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;
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(".");
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();
} 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 {
--- /dev/null
+// 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());
+ }
+}
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"));
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()),
}
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; }