From: adam Date: Tue, 13 Jul 2004 03:06:19 +0000 (+0000) Subject: bogus X-Git-Url: http://git.megacz.com/?p=org.ibex.mail.git;a=commitdiff_plain;h=1c969016351982fef3fc1539ab6d494592da1f3d bogus darcs-hash:20040713030619-5007d-b4ec36a1f042780ffca5d886c21e88c4dc6c2e6b.gz --- diff --git a/doc/README b/doc/README index df2113d..8811727 100644 --- a/doc/README +++ b/doc/README @@ -16,6 +16,14 @@ including: ______________________________________________________________________________ +Compliance + +Ibexmail is believed to be compliant with the following RFCs: + + - + + +______________________________________________________________________________ JavaScript API Each script is invoked with the variable 'm' bound to a message to be diff --git a/src/org/ibex/mail/MIME.java b/src/org/ibex/mail/MIME.java index 76ea5b1..58d0621 100644 --- a/src/org/ibex/mail/MIME.java +++ b/src/org/ibex/mail/MIME.java @@ -4,4 +4,20 @@ package org.ibex.mail; /** This class contains logic for encoding and decoding MIME multipart messages */ public class MIME { + public static class QuotedPrintable { + + public static String decode(String s, boolean lax) { + // + // =XX -> hex representation, must be uppercase + // 9, 32, 33-60, 62-126 can be literal + // 9, 32 at end-of-line must get encoded + // trailing whitespace must be deleted when decoding + // =\n = soft line break + // lines cannot be more than 76 chars long + // + + // lax is used for RFC2047 headers; removes restrictions on which chars you can encode + return s; + } + } } diff --git a/src/org/ibex/mail/Message.java b/src/org/ibex/mail/Message.java index 0406cf6..5fd2e2f 100644 --- a/src/org/ibex/mail/Message.java +++ b/src/org/ibex/mail/Message.java @@ -3,7 +3,6 @@ import org.ibex.crypto.*; import org.ibex.util.*; import org.ibex.mail.protocol.*; import org.ibex.io.*; -import org.ibex.net.*; import java.util.*; import java.net.*; import java.io.*; @@ -48,6 +47,8 @@ public class Message extends org.ibex.js.JSReflection { public final Address envelopeFrom; public final Address envelopeTo; + public final boolean mime; // true iff Mime-Version is 1.0 + public final Date arrival; // when the message first arrived at this machine; IMAP "internal date message attr" public int size() { return allHeaders.length() + 2 /* CRLF */ + body.length(); } @@ -120,9 +121,44 @@ public class Message extends org.ibex.js.JSReflection { all.append(s); lines++; all.append("\r\n"); + + // FIXME RFC822 1,000-char limit per line + + + // FIXME only iff Mime 1.0 (?) + // RFC 2047 + 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(this, "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(this, "error trying to decode RFC2047 encoded-word: \""+s+"\""); + Log.warn(this, e); + } + if (s.length() == 0 || Character.isSpace(s.charAt(0))) { if (key == null) throw new Malformed("Message began with a blank line; no headers"); - ((CaseInsensitiveHash)headers).add(key, headers.get(key) + s); + ((CaseInsensitiveHash)headers).add(key, headers.get(key) + s.trim()); continue; } if (s.indexOf(':') == -1) throw new Malformed("Header line does not contain colon: " + s); @@ -155,6 +191,7 @@ public class Message extends org.ibex.js.JSReflection { this.from = headers.get("From") == null ? envelopeFrom : Address.parse((String)headers.get("From")); this.envelopeFrom = envelopeFrom == null ? this.from : envelopeFrom; this.envelopeTo = envelopeTo == null ? this.to : envelopeTo; + this.mime = headers.get("mime-version") != null && headers.get("mime-version").toString().trim().startsWith("1.0"); this.date = new Date(); // FIXME (Date)headers.get("Date"); this.replyto = headers.get("Reply-To") == null ? null : Address.parse((String)headers.get("Reply-To")); @@ -188,6 +225,114 @@ public class Message extends org.ibex.js.JSReflection { this.lines = lines; this.body = body.toString(); } + + public static class Part { + public final String type; + public final String subtype; + public final Hash parameters = new Hash(); + public final Part[] subparts; + public final String payload; + public final String id; + public final String description; + + public final boolean composite; + public final boolean alternative; + + // FIXME charsets US-ASCII, ISO-8559-X, + public Part(String contentTypeHeader, String transferEncoding, + String id, String description, String payload) throws Malformed { + this.id = id; + this.description = description; + + // NOTE: it is not valid to send a multipart message as base64 or quoted-printable; you must encode each section + if (transferEncoding == null) transferEncoding = "7bit"; + transferEncoding = transferEncoding.toLowerCase(); + if (transferEncoding.equals("quoted-printable")) { + this.payload = MIME.QuotedPrintable.decode(payload, false); + subparts = null; + } else if (transferEncoding.equals("base64")) { + this.payload = new String(Base64.decode(payload)); + subparts = null; + } else if (transferEncoding.equals("7bit") || transferEncoding.equals("8bit") || + transferEncoding.equals("binary")) { + this.payload = payload; + + // 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 + // message/partial -- not supported; see RFC 2046, section 5.2.2 + // message/external-body -- not supported; see RFC 2046, section 5.2.3 + + /* FIXME + if (composite) { + // FIXME: StringTokenizer doesn't really work like this + String boundary = "--" + parameters.get("boundary"); // CRLF, delimiter, optional whitespace, plus CRLF + // preceeding CRLF is part of delimiter + // boundary (including --) cannot be > 70 chars + // delimiter after final part also has a '--' on the end + // first part begins with a boundary delimiter + StringTokenizer st = new StringTokenizer(payload, boundary); + Vec v = new Vec(); + while(st.hasMoreTokens()) { + st.nextToken(); + Part p = new Part(); + v.addElement(p); + } + v.copyInto(subparts = new Part[v.size()]); + } else { + } + */ + subparts = null; + } else { + Log.warn(this, "unknown Content-Transfer-Encoding \"" + transferEncoding + + "\"; forcing Content-Type to application/octet-stream (originally \""+contentTypeHeader+"\""); + contentTypeHeader = "application/octet-stream"; + this.payload = payload; + subparts = null; + } + + if (contentTypeHeader == null) { + type = "text"; + subtype = "plain"; + parameters.put("charset", "us-ascii"); + } else { + contentTypeHeader = contentTypeHeader.trim(); + if (contentTypeHeader.endsWith(")")) // remove RFC822 comments + contentTypeHeader = contentTypeHeader.substring(0, contentTypeHeader.lastIndexOf('(')); + if (contentTypeHeader.indexOf('/') == -1) + throw new Malformed("content-type header lacks a forward slash: \"" + contentTypeHeader + "\""); + type = contentTypeHeader.substring(0, contentTypeHeader.indexOf('/')).toLowerCase(); + contentTypeHeader = contentTypeHeader.substring(contentTypeHeader.indexOf('/') + 1); + if (contentTypeHeader.indexOf(';') == -1) { + subtype = contentTypeHeader.toLowerCase(); + } else { + subtype = contentTypeHeader.substring(0, contentTypeHeader.indexOf(';')).toLowerCase(); + StringTokenizer st = new StringTokenizer(contentTypeHeader.substring(contentTypeHeader.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); + } + } + } + + composite = type.equals("message") || type.equals("multipart"); + alternative = composite && subtype.equals("alternative"); + + + // fixme: message/rfc822 + + // body parts need only contain Content-Foo headers (or none at all); they need not be rfc822 compliant + + } + } // http://www.jwz.org/doc/mid.html private static final Random random = new Random(); diff --git a/src/org/ibex/mail/protocol/IMAP.java b/src/org/ibex/mail/protocol/IMAP.java index d50f1c4..a354445 100644 --- a/src/org/ibex/mail/protocol/IMAP.java +++ b/src/org/ibex/mail/protocol/IMAP.java @@ -2,7 +2,6 @@ package org.ibex.mail.protocol; import org.ibex.io.*; import org.ibex.jinetd.Listener; import org.ibex.jinetd.Worker; -import org.ibex.net.*; import org.ibex.mail.*; import org.ibex.util.*; import org.ibex.mail.target.*; diff --git a/src/org/ibex/mail/protocol/NNTP.java b/src/org/ibex/mail/protocol/NNTP.java index b13884e..ef5fc0c 100644 --- a/src/org/ibex/mail/protocol/NNTP.java +++ b/src/org/ibex/mail/protocol/NNTP.java @@ -1,4 +1,203 @@ package org.ibex.mail.protocol; +import org.ibex.util.*; +import org.ibex.io.*; +import org.ibex.mail.*; +import org.ibex.mail.target.*; +import org.ibex.jinetd.*; +import java.io.*; +import java.net.*; +import java.util.*; +import java.text.*; + /** NNTP send/recieve */ public class NNTP { + + // FIXME: command lines limited to 512 chars + + public static class No extends RuntimeException { int code = 0; } // 4xx response codes + public static class Bad extends RuntimeException { int code = 0; } // 5xx response codes + + public static class Group { + public Group(String n, boolean p, int f, int l, int c) { this.name=n;this.post=p;this.first=f;this.last=l;this.count=c;} + public final String name; + public final boolean post; + public final int first; + public final int last; + public final int count; + } + + public static class Article { + public Article(String messageid, int num, Message message) { this.message=message;this.messageid=messageid;this.num=num;} + public final String messageid; + public final int num; + public final Message message; + } + + public static interface Server { + public Group group(String s); + public boolean ihave(String messageid); + public Article next(); + public Article last(); + public boolean post(Message m); + public Article article(String messageid, boolean head, boolean body); + public Article article(int messagenum, boolean head, boolean body); + public Group[] list(); + public Group[] newgroups(Date d, String[] distributions); + public String[] newnews(String[] groups, Date d, String[] distributions); + } + + public static class MailboxWrapper implements Server { + private final Mailbox root; + private Mailbox current; + private int ptr = 0; + public MailboxWrapper(Mailbox root) { this.root = root; } + public Group group(String s) { ptr = 0; setgroup(s); return getgroup(s); } + public boolean ihave(String messageid) { /* FEATURE */ return false; } + public boolean post(Message m) { /* FEATURE */ return false; } + public Article next() { return article(ptr++, false, false); } + public Article last() { return article(ptr--, false, false); } + public Article article(String i, boolean h, boolean b) { return article(Query.header("message-id",i),h,b); } + public Article article(int n, boolean h, boolean b) { ptr = n; return article(Query.messagenum(n,n),h,b); } + private Article article(Query q, boolean head, boolean body) { + Mailbox.Iterator it = current.iterator(q); + if (!it.next()) return null; + Message m = body ? it.cur() : it.head(); + return new Article(m.messageid, it.num(), m); + } + public Group[] list() { return list(root, ""); } + private Group[] list(Mailbox who, String prefix) { + Vec v = new Vec(); + String[] s = who.children(); + for(int i=0; i get ready for some stuff..."); + if (head) conn.println(a.message.allHeaders); + if (head && body) conn.println(); + if (body) conn.println(a.message.body); + conn.println("."); + } + public void handleRequest(Connection conn) { + this.conn = conn; + api = new NNTP.MailboxWrapper(null); // FIXME + conn.setTimeout(30 * 60 * 1000); + conn.println("200 " + conn.vhost + " [" + NNTP.class.getName() + "]"); + for(String line = conn.readln(); line != null; line = conn.readln()) try { + StringTokenizer st = new StringTokenizer(line, " "); + String command = st.nextToken().toUpperCase(); + if (command.equals("ARTICLE")) { article(st.hasMoreTokens() ? st.nextToken() : null, true, true); + } else if (command.equals("HEAD")) { article(st.hasMoreTokens() ? st.nextToken() : null, true, false); + } else if (command.equals("BODY")) { article(st.hasMoreTokens() ? st.nextToken() : null, false, true); + } else if (command.equals("STAT")) { article(st.hasMoreTokens() ? st.nextToken() : null, false, false); + } else if (command.equals("HELP")) { conn.println("100 you are beyond help."); conn.println("."); + } else if (command.equals("SLAVE")) { conn.println("220 I don't care"); + } else if (command.equals("LAST")) { Article a = api.last(); conn.println("223 "+a.num+" "+a.messageid+" ok"); + } else if (command.equals("NEXT")) { Article a = api.next(); conn.println("223 "+a.num+" "+a.messageid+" ok"); + } else if (command.equals("QUIT")) { conn.println("205 Bye."); conn.close(); return; + } else if (command.equals("GROUP")) { + Group g = api.group(st.nextToken().toLowerCase()); + conn.println("221 " + g.count + " " + g.first + " " + g.last + " " + g.name); + } else if (command.equals("NEWGROUPS") || command.equals("NEWNEWS")) { + String groups = command.equals("NEWNEWS") ? st.nextToken() : null; + String datetime = st.nextToken() + " " + st.nextToken(); + String gmt = st.nextToken(); + String distributions = gmt.equals("GMT") ? (st.hasMoreTokens() ? st.nextToken() : null) : gmt; + while(st.hasMoreTokens()) distributions += " " + st.nextToken(); + // FIXME deal with GMT + Date d = new Date(); + try { + d = new SimpleDateFormat("YYMMDD HHMMSS").parse(gmt); + } catch (ParseException pe) { + Log.warn(this, pe); + } + distributions = distributions.trim(); + if (distributions.startsWith("<")) distributions = distributions.substring(1, distributions.length() - 1); + + st = new StringTokenizer(distributions, ","); + String[] dists = new String[st.countTokens()]; + for(int i=0; st.hasMoreTokens(); i++) dists[i] = st.nextToken(); + + if (command.equals("NEWGROUPS")) { + Group[] g = api.newgroups(d, dists); + conn.println("231 list of groups follows"); + for(int i=0; i= names.length) return null; try { diff --git a/src/org/ibex/mail/target/Mailbox.java b/src/org/ibex/mail/target/Mailbox.java index 275c32e..92d9342 100644 --- a/src/org/ibex/mail/target/Mailbox.java +++ b/src/org/ibex/mail/target/Mailbox.java @@ -95,26 +95,31 @@ public abstract class Mailbox extends Target { // Iterator Definition ////////////////////////////////////////////////////////////////////////////// public static interface Iterator { - public abstract int flags(); public abstract Message cur(); + public abstract Message head(); public abstract boolean next(); public abstract int uid(); public abstract int num(); public abstract void delete(); + public abstract void set(String key, String val); public abstract String get(String key); + public abstract boolean seen(); public abstract boolean deleted(); public abstract boolean flagged(); public abstract boolean draft(); public abstract boolean answered(); public abstract boolean recent(); + public abstract void seen(boolean on); public abstract void deleted(boolean on); public abstract void flagged(boolean on); public abstract void draft(boolean on); public abstract void answered(boolean on); public abstract void recent(boolean on); + + public abstract int flags(); public abstract void addFlags(int flags); public abstract void removeFlags(int flags); public abstract void setFlags(int flags); @@ -123,6 +128,7 @@ public abstract class Mailbox extends Target { private Iterator it; public Wrapper(Iterator it) { this.it = it; } public Message cur() { return it.cur(); } + public Message head() { return it.head(); } public boolean next() { return it.next(); } public int uid() { return it.uid(); } public int flags() { return it.flags(); }