// Xref header
// LIST EXTENSIONS is probably incomplete
// pull mode (ie suck)
-// control message processing?
+// FEATURE: control message processing?
+// cancel <Message-ID> (do not forward if I am unable to cancel locally)
+// ihave/sendme: do not support
+// newgroup <groupname> [moderated] -- body of message is a description of the group
+// rmgroup <groupname>
package org.ibex.mail;
import org.ibex.util.*;
/** NNTP send/recieve */
public class NNTP {
- public static final DateFormat dateFormat = new SimpleDateFormat("yyyyMMDDhhmmss");
+ public static final DateFormat serverDateFormat = new SimpleDateFormat("yyyyMMDDhhmmss");
+ public static final DateFormat shortNewNewsDateFormat = new SimpleDateFormat("yyMMDD HHMMSS");
+ public static final DateFormat longNewNewsDateFormat = new SimpleDateFormat("yyyyMMDD HHMMSS");
+ static {
+ serverDateFormat.setTimeZone(new SimpleTimeZone(SimpleTimeZone.UTC_TIME, "GMT"));
+ shortNewNewsDateFormat.setTimeZone(new SimpleTimeZone(SimpleTimeZone.UTC_TIME, "GMT"));
+ longNewNewsDateFormat.setTimeZone(new SimpleTimeZone(SimpleTimeZone.UTC_TIME, "GMT"));
+ }
public static class No extends RuntimeException { int code = 400; } // 4xx response codes
public static class Bad extends RuntimeException { int code = 500; public Bad(String s) { super(s); } } // 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 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; // case insensitive
public final boolean post;
public final int first;
public final Message message;
}
+
+ /**
+ * The API exposed by an NNTP server; remote NNTP servers appear
+ * as instances of this class, and implementations of this class
+ * may be exported as NNTP servers.
+ */
public static interface Server {
public Group group(String s);
public boolean ihave(String messageid);
+ public boolean want(String messageid);
public Article next();
public Article last();
public boolean postok();
- public void post(Message m) throws IOException;
+ public void 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 Group[] newgroups(Date d);
+ public String[] newnews(String[] groups, Date d);
}
- public static class MailboxWrapper implements Server {
- private final Mailbox root;
- private Mailbox current;
- private int ptr = 0;
- private boolean post;
- public MailboxWrapper(Mailbox root) { this(root, false); }
- public MailboxWrapper(Mailbox root, boolean post) { this.root = root; this.post = post; }
- public boolean postok() { return post; }
- public void post(Message m) throws IOException { current.post(m); }
+ public static class MailboxServer implements Server {
+ private final MailTree root;
+ private Mailbox current = null;
+ private int currentMessageNumber = 0;
+ public MailboxServer(MailTree root) { this.root = root; }
+ public boolean postok() { return true; }
+ public void post(Message m) { current.post(m); }
public Group group(String s) {
- ptr = 0;
- Group g = getgroup(s);
- if (g==null) return null;
- setgroup(s);
- return g;
+ currentMessageNumber = 0;
+ MailTree ncurrent = resolve(s);
+ if (ncurrent == null) return null;
+ current = ncurrent.getMailbox();
+ return new Group(s, true, 1, current.count(Query.all()), current.count(Query.all()));
+ }
+ public boolean ihave(String messageid) { /* FEATURE */ return false; }
+ public boolean want(String messageid) { /* FEATURE */ return true; }
+ public Group[] newgroups(Date d) { /* FEATURE */ return new Group[] { }; }
+ public Article next() { return article(currentMessageNumber++, false, false); }
+ public Article last() { return article(currentMessageNumber--, false, false); }
+ public String[] newnews(String[] groups, Date d) {
+ Vec ret = new Vec();
+ for(String g : groups) {
+ Mailbox group = resolve(g).getMailbox();
+ for(Mailbox.Iterator mit = group.iterator(Query.arrival(d, null));
+ mit.next();) {
+ ret.add(mit.head().get("message-id"));
+ }
+ }
+ return (String[])ret.copyInto(new String[ret.size()]);
}
-
- public boolean ihave(String messageid) { /* 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.nntpNumber(n,n),h,b); }
+ public Article article(int n, boolean h, boolean b) { currentMessageNumber = n; return article(Query.nntpNumber(n,n),h,b); }
private Article article(Query q, boolean head, boolean body) {
Mailbox.Iterator it = current.iterator(q);
if (!it.next()) return null;
- try {
- // FIXME: UGLY!
- Message m = body ? it.cur() : Message.newMessage(new Fountain.StringFountain(it.head() + "\r\n"));
- return new Article(it.nntpNumber(), m);
- } catch (Exception e) { return null; }
+ return new Article(it.nntpNumber(), body ? it.cur() : Message.newMessage(it.head()));
}
public Group[] list() { return list(root, ""); }
- private Group[] list(Mailbox who, String prefix) {
+ private Group[] list(MailTree who, String prefix) {
Vec v = new Vec();
if (who == null) who = root;
String[] s = who.children();
for(int i=0; i<s.length; i++) {
- v.addElement(new Group(prefix + s[i], true, 0, 0, 0)); // FIXME numbers
- Group[] g2 = list(who.slash(s[i], false), prefix + s[i] + ".");
+ MailTree mtree = who.slash(s[i], false);
+
+ // this is inefficient
+ int low = Integer.MAX_VALUE;
+ int high = 0;
+ int count = 0;
+ for(Mailbox.Iterator mit = mtree.getMailbox().iterator(); mit.next();) {
+ count++;
+ low = Math.min(low, mit.nntpNumber());
+ high = Math.max(low, mit.nntpNumber());
+ }
+ if (count==0) { low = 1; high = 0; }
+
+ v.addElement(new Group(prefix + s[i], true, low, high, count));
+ Group[] g2 = list(mtree, prefix + s[i] + ".");
for(int j=0; j<g2.length; j++) v.addElement(g2[j]);
}
Group[] ret = new Group[v.size()];
v.copyInto(ret);
return ret;
}
-
- private void setgroup(String s) {
- Mailbox ncurrent = root;
- for(StringTokenizer st = new StringTokenizer(s, ".");
- ncurrent != null && st.hasMoreTokens();
- ncurrent = ncurrent.slash(st.nextToken(), false));
- if (ncurrent!=null) current=ncurrent;
- }
- private Group getgroup(String s) {
- Mailbox box = root;
+ private MailTree resolve(String s) {
+ MailTree box = root;
for(StringTokenizer st = new StringTokenizer(s, ".");
box!=null && st.hasMoreTokens();
box = box.slash(st.nextToken(), false));
- if (box==null) return null;
- return new Group(s, true, 1, box.count(Query.all()), box.count(Query.all()));
+ return box;
}
-
- public Group[] newgroups(Date d, String[] distributions) { /* FEATURE */ return new Group[] { }; }
- public String[] newnews(String[] groups, Date d, String[] distributions) { /* FIXME */ return null; }
}
public static class Listener {
private Connection conn;
public Listener(Login l) { this.login = l; }
- private void println(String s) { Log.warn("[nntp-write]", s); conn.println(s); }
- private void println() { Log.warn("[nntp-write]", ""); conn.println(""); }
- private void print(String s) { Log.warn("[nntp-write]", s); conn.print(s); }
+ private void println(String s) { conn.println(s); }
+ private void println() { conn.println(""); }
+ private void print(String s) { conn.print(s); }
private void article(String numOrMessageId, boolean head, boolean body) {
String s = numOrMessageId.trim();
String user = null;
String pass = null;
Account account = login.anonymous();
- this.api = account == null ? null : new MailboxWrapper(account.getMailbox(NNTP.class), true);
+ this.api = account == null ? null : new MailboxServer(account.getMailbox(NNTP.class));
for(String line = conn.readln(); line != null; line = conn.readln()) try {
Log.warn("[nntp-read]", line);
StringTokenizer st = new StringTokenizer(line, " ");
String command = st.nextToken().toUpperCase();
if (command.equals("AUTHINFO")) {
- // FIXME technically the RFC says we need to use this info to generate a SEnder: header...
String uop = st.nextToken().toUpperCase();
if (uop.equals("USER")) {
user = st.nextToken();
pass = st.nextToken();
account = login.login(user, pass);
if (account == null) { println("502 Invalid"); continue; }
- Mailbox box = account.getMailbox(NNTP.class);
- this.api = new MailboxWrapper(box, true);
+ this.api = new MailboxServer(account.getMailbox(NNTP.class));
println("281 Good to go");
continue;
}
}
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("DATE")) {
- // FIXME must be GMT
- println("111 " + dateFormat.format(new Date()));
+ } else if (command.equals("DATE")) { println("111 " + serverDateFormat.format(new Date()));
} else if (command.equals("MODE")) {
if (st.hasMoreTokens()) {
String arg = st.nextToken();
if (arg.equalsIgnoreCase("STREAM"));
- //streaming = true;
println("203 Streaming permitted");
} else {
println("201 Hello, you can post.");
} else if (command.equals("STAT")) { article(st.hasMoreTokens() ? st.nextToken() : null, false, false);
} else if (command.equals("HELP")) { println("100 you are beyond help."); println(".");
} else if (command.equals("SLAVE")) { println("220 SLAVE was removed in RFC3977, you should not use it");
- } else if (command.equals("XOVER")) {
+ } else if (command.equals("XOVER") || command.equals("OVER")) {
println("224 Overview information follows");
- MailboxWrapper api = (MailboxWrapper)this.api;
- String range = st.hasMoreTokens() ? st.nextToken() : (api.ptr+"-"+api.ptr);
+ MailboxServer api = (MailboxServer)this.api;
+ String range = st.hasMoreTokens() ? st.nextToken() : (api.currentMessageNumber+"-"+api.currentMessageNumber);
int start = Integer.parseInt(range.substring(0, range.indexOf('-')));
int end = Integer.parseInt(range.substring(range.indexOf('-') + 1));
Mailbox.Iterator it = api.current.iterator(Query.nntpNumber(start, end));
else println("211 " + g.count + " " + g.first + " " + g.last + " " + g.name);
} else if (command.equals("NEWGROUPS") || command.equals("NEWNEWS")) {
// FIXME: * and ! unsupported
- // NEWNEWS is often not supported
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() : "") : gmt;
- while(st.hasMoreTokens()) distributions += " " + st.nextToken();
- // FIXME deal with GMT
Date d = new Date();
- try {
- d = new SimpleDateFormat("yyMMDD HHMMSS").parse(datetime);
- } 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();
+ try { d = (datetime.length() == 13 ? shortNewNewsDateFormat : longNewNewsDateFormat)
+ .parse(datetime);
+ } catch (ParseException pe) { Log.warn(this, pe); }
if (command.equals("NEWGROUPS")) {
- Group[] g = api.newgroups(d, dists);
+ Group[] g = api.newgroups(d);
println("231 list of groups follows");
for(int i=0; i<g.length; i++)
println(g[i].name + " " + g[i].last + " " + g[i].first + " " + (g[i].post ? "y" : "n"));
st = new StringTokenizer(groups, ",");
String[] g = new String[st.countTokens()];
for(int i=0; st.hasMoreTokens(); i++) g[i] = st.nextToken();
- String[] a = api.newnews(g, d, dists);
+ String[] a = api.newnews(g, d);
println("230 list of article messageids follows");
for(int i=0; i<a.length; i++) println(a[i]);
println(".");
}
} else if (command.equals("POST")) {
- // FIXME
- // add NNTP-Posting-Host header
- // Path header: prepend <myname>, (any punctuation separates the list)
- // Expires header: the date when expiration happens (??) should we ignore this?
- // Control header: body is the command. Inteprert posts to all.all.ctl as control messages,
- // use Subject line if no Cntrol line
- // "Approved" line is used for moderaion
- // Xref: drop this header if you see it
-
- // Control messages
- // cancel <Message-ID> (do not forward if I am unable to cancel locally)
- // ihave/sendme: do not support
- // newgroup <groupname> [moderated] -- body of message is a description of the group
- // rmgroup <groupname>
-
boolean postok = api.postok();
if (!postok) {
println("440 no posting allowed");
} else {
println("340 send the article");
- StringBuffer buf = new StringBuffer();
- // FIXME: streaming?
- while(true) {
- String s = conn.readln();
- if (s == null) throw new RuntimeException("connection closed");
- if (s.equals(".")) break;
- if (s.startsWith(".")) s = s.substring(1);
- buf.append(s + "\r\n");
- }
- String body = buf.toString();
try {
- Message m = Message.newMessage(new Fountain.StringFountain(body));
+ Message m = Message.readDotEncodedMessage(conn);
if (m.headers.get("newsgroups")==null)
println("441 posted messages must have a Newsgroups header per RFC 977");
else if (m.headers.get("newsgroups").indexOf('*')!=-1)
println("441 Newsgroups header in posted messages may not contain wildcards (*) per RFC 977");
else if (m.headers.get("subject")==null)
println("441 posted messages must have a Subject header per RFC 977");
- // else if (m.headers.get("path")==null)
- //println("441 posted messages must have a Path header per RFC 977");
else if (m.headers.get("from")==null)
println("441 posted messages must have a From header per RFC 977");
else if (m.headers.get("date")==null)
} else if (command.equals("XROVER")) {
// equivalent to "XHDR References"
- } else if (command.equals("XHDR")) {
+
+ } else if (command.equals("XHDR") || (command.equals("HDR"))) {
// argument: header name
// argument: 1 | 1- | 1-2 | <mid> | nothing (use current article)
println("221 yep");
// 412 if no group selected and numeric form used
// 430 if <mid> and not found
// 420 if no messages in range
+
} else if (command.equals("XPAT")) {
// just like XHDR, but a pattern follows the last argument (may contain whitespace)
println("221 yep");
// print
println(".");
+
} else if (command.equals("LIST")) {
if (st.hasMoreTokens()) {
String argument = st.nextToken().toUpperCase();
} else if (command.equals("LISTGROUP")) {
String groupname = st.hasMoreTokens() ? st.nextToken() : null;
- // 211, all article numbers in group, period. Set article ptr to first item in group
+ // 211, all article numbers in group, period. Set article currentMessageNumber to first item in group
} else if (command.equals("XGTITLE")) {
String wildmat = st.hasMoreTokens() ? st.nextToken() : null;
// 282, then identical to LIST NEWSGROUP
} else if (command.equals("CHECK")) {
- // FIXME: may be pipelined; must spawn threads
String mid = st.nextToken();
- boolean want = api.ihave(mid);
- if (!want) {
- println("438 "+ mid+" No thanks");
- } else {
- println("238 "+mid+" Yes, I'd like that");
- }
+ boolean want = api.want(mid);
+ if (!want) println("438 "+ mid+" No thanks");
+ else println("238 "+mid+" Yes, I'd like that");
} else if (command.equals("TAKETHIS")) {
- // FIXME: may be pipelined
String mid = st.nextToken();
- // MUST read message here
- /*
- if (!want) {
- println("439 "+ mid+" Transfer failed");
- } else {
+ boolean want = api.want(mid);
+ Message m = Message.readDotEncodedMessage(conn);
+ if (want) {
+ api.post(m);
println("239 "+mid+" Rock on.");
+ } else {
+ println("439 "+ mid+" I really didn't want that; don't send it again.");
}
- */
} else if (command.equals("IHAVE")) {
boolean want = api.ihave(st.nextToken());
println("435 No thanks");
} else {
println("335 Proceed");
- // FIXME read article here
+ api.post(Message.readDotEncodedMessage(conn));
println("235 Got it");
}
} else {