X-Git-Url: http://git.megacz.com/?a=blobdiff_plain;f=src%2Forg%2Fibex%2Fmail%2Fprotocol%2FNNTP.java;h=284cf0457fd3050478ef81fc5eb8efdc725491f5;hb=4ff26332350edaa3c6ebfa29408c61412a6a2e40;hp=235b236955300b14edf6b3e100d1e5c4950c44b7;hpb=324c3198ad35f62be684e5cc90db1791fb7b22bb;p=org.ibex.mail.git diff --git a/src/org/ibex/mail/protocol/NNTP.java b/src/org/ibex/mail/protocol/NNTP.java index 235b236..284cf04 100644 --- a/src/org/ibex/mail/protocol/NNTP.java +++ b/src/org/ibex/mail/protocol/NNTP.java @@ -2,6 +2,12 @@ // Licensed under the Apache Public Source License 2.0 ("the License"). // You may not use this file except in compliance with the License. +// 500 unrec. command +// 501 syntax error +// 503 optional subfeature not supported +// Xref header +// LIST EXTENSIONS is probably incomplete + package org.ibex.mail.protocol; import org.ibex.util.*; import org.ibex.io.*; @@ -16,23 +22,22 @@ import java.text.*; /** NNTP send/recieve */ public class NNTP { - // FIXME: command lines limited to 512 chars + public static final DateFormat dateFormat = new SimpleDateFormat("yyyyMMDDhhmmss"); - public static class No extends RuntimeException { int code = 400; } // 4xx response codes + 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 final String name; + public final String name; // case insensitive public final boolean post; public final int first; public final int last; - public final int count; + public final int count; // an approximation; must be >= actual number } 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 Article(int num, Message message) { this.message = message; this.num = num; } public final int num; public final Message message; } @@ -42,7 +47,8 @@ public class NNTP { public boolean ihave(String messageid); public Article next(); public Article last(); - public boolean post(Message m); + public boolean postok(); + public void post(Message m) throws IOException; public Article article(String messageid, boolean head, boolean body); public Article article(int messagenum, boolean head, boolean body); public Group[] list(); @@ -54,21 +60,31 @@ public class NNTP { 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); } + 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.accept(m); } + public Group group(String s) { + ptr = 0; + Group g = getgroup(s); + if (g==null) return null; + setgroup(s); + return g; + } + 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) { + public Article article(int n, boolean h, boolean b) { ptr = 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 { - Message m = body ? it.cur() : Message.newMessage(new Stream(it.head() + "\r\n")); - //Message m = it.cur(); // FIXME - return new Article(m.messageid, it.num(), m); + 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; } } public Group[] list() { return list(root, ""); } @@ -87,14 +103,18 @@ public class NNTP { } private void setgroup(String s) { - current = root; + Mailbox ncurrent = root; for(StringTokenizer st = new StringTokenizer(s, "."); - st.hasMoreTokens(); - current = current.slash(st.nextToken(), false)); + ncurrent != null && st.hasMoreTokens(); + ncurrent = ncurrent.slash(st.nextToken(), false)); + if (ncurrent!=null) current=ncurrent; } private Group getgroup(String s) { Mailbox box = root; - for(StringTokenizer st = new StringTokenizer(s, "."); st.hasMoreTokens(); box = box.slash(st.nextToken(), false)); + 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())); } @@ -102,7 +122,7 @@ public class NNTP { public String[] newnews(String[] groups, Date d, String[] distributions) { /* FIXME */ return null; } } - public static class Listener implements Worker { + public static class Listener { private Server api = null; private Login login; private Connection conn; @@ -110,6 +130,7 @@ public class NNTP { 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 article(String numOrMessageId, boolean head, boolean body) { String s = numOrMessageId.trim(); @@ -121,77 +142,110 @@ public class NNTP { return; } int code = (head && body) ? 220 : head ? 221 : body ? 222 : 223; - println(code + " " + a.num + " <" + a.messageid + "> get ready for some stuff..."); - if (head) println(a.message.headers.raw); + println(code + " " + a.num + " <" + a.message.messageid + "> get ready for some stuff..."); + if (head) println(a.message.headers.getString()); if (head && body) println(); - if (body) println(a.message.body); + if (body) { + Stream stream = a.message.getBody().getStream(); + while(true) { + s = stream.readln(); + if (s == null) break; + if (s.startsWith(".")) print("."); + println(s); + } + } println("."); } public void handleRequest(Connection conn) { this.conn = conn; conn.setTimeout(30 * 60 * 1000); + conn.setNewline("\r\n"); println("200 " + conn.vhost + " [" + NNTP.class.getName() + "]"); String user = null; String pass = null; Account account = login.anonymous(); - this.api = account == null ? null : new MailboxWrapper(account.getMailbox(NNTP.class)); + this.api = account == null ? null : new MailboxWrapper(account.getMailbox(NNTP.class), true); 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(); - else if (uop.equals("PASS")) pass = st.nextToken(); - // FIXME error here + if (uop.equals("USER")) { + user = st.nextToken(); + println("381 More authentication required"); + continue; + } else if (uop.equals("PASS")) { + 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); + println("281 Good to go"); + continue; + } + throw new Bad("wtf are you talking about?"); } if (this.api == null) { if (user == null) { println("480 Authentication required"); continue; } if (pass == null) { println("381 Password required"); continue; } - account = login.login(user, pass); - if (account == null) { println("502 Invalid"); continue; } - this.api = new MailboxWrapper(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("MODE")) { println("201 Hello, you can post."); + } else if (command.equals("DATE")) { + // FIXME must be GMT + println("111 " + dateFormat.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("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")) { println("100 you are beyond help."); println("."); - } else if (command.equals("SLAVE")) { println("220 I don't care"); + } else if (command.equals("SLAVE")) { println("220 SLAVE was removed in RFC3977, you should not use it"); } else if (command.equals("XOVER")) { println("224 Overview information follows"); MailboxWrapper api = (MailboxWrapper)this.api; String range = st.hasMoreTokens() ? st.nextToken() : (api.ptr+"-"+api.ptr); 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.messagenum(start, end)); + Mailbox.Iterator it = api.current.iterator(Query.nntpNumber(start, end)); while(it.next()) { try { - Message m = Message.newMessage(new Stream(it.head() + "\r\n")); - println(it.num()+"\t"+m.subject+"\t"+m.from+"\t"+m.date+"\t"+m.messageid+"\t"+ - m.headers.gets("references") + "\t" + m.size() + "\t" + m.lines); + Message m = it.cur(); + println(it.nntpNumber()+"\t"+m.subject+"\t"+m.from+"\t"+m.date+"\t"+m.messageid+"\t"+ + m.headers.get("references") + "\t" + m.getLength() + "\t" + m.getNumLines()); } catch (Exception e) { Log.error(this, e); } } println("."); - } else if (command.equals("LAST")) { Article a = api.last(); println("223 "+a.num+" "+a.messageid+" ok"); - } else if (command.equals("NEXT")) { Article a = api.next(); println("223 "+a.num+" "+a.messageid+" ok"); + } else if (command.equals("LAST")) { + Article a = api.last(); println("223 "+a.num+" "+a.message.messageid+" ok"); + } else if (command.equals("NEXT")) { + Article a = api.next(); println("223 "+a.num+" "+a.message.messageid+" ok"); } else if (command.equals("QUIT")) { println("205 Bye."); conn.close(); return; } else if (command.equals("GROUP")) { Group g = api.group(st.nextToken().toLowerCase()); - println("211 " + g.count + " " + g.first + " " + g.last + " " + g.name); + if (g==null) println("411 no such group"); + 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() : null) : gmt; + 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(gmt); + d = new SimpleDateFormat("yyMMDD HHMMSS").parse(datetime); } catch (ParseException pe) { Log.warn(this, pe); } @@ -220,24 +274,143 @@ public class NNTP { } else if (command.equals("POST")) { // FIXME - /* - boolean postok = api.post(); + // add NNTP-Posting-Host header + // Path header: prepend , (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 (do not forward if I am unable to cancel locally) + // ihave/sendme: do not support + // newgroup [moderated] -- body of message is a description of the group + // rmgroup + + boolean postok = api.postok(); if (!postok) { println("440 no posting allowed"); } else { - */ - println("340 send the article"); - // FIME read the article here - println("240 article posted ok"); - //} + println("340 send the article"); + StringBuffer buf = new StringBuffer(); + 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)); + 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) + println("441 posted messages must have a Date header per RFC 977"); + else { + api.post(m); + println("240 article posted ok"); + } + } catch (Exception e) { + e.printStackTrace(); + println("441 posting failed: " + e); + } + } - } else if (command.equals("LIST")) { - if (st.hasMoreTokens()) throw new Bad("LIST " + st.nextToken() + " not supported"); - Group[] g = api.list(); - println("215 list of groups follows"); - for(int i=0; i | nothing (use current article) + println("221 yep"); + // print art#+header for all matching messages + println("."); + // 412 if no group selected and numeric form used + // 430 if 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(); + if (argument.equalsIgnoreCase("EXTENSIONS")) { + println("202 Extensions supported:"); + println("STREAMING"); + println(""); + println("."); + } else if (argument.equals("ACTIVE")) { + String wildmat = st.hasMoreTokens() ? st.nextToken() : null; + // FIXME: deal with wildmat + // just like list, but only show active groups + throw new Bad("not implemented yet"); + } else if (argument.equals("SUBSCRIPTIONS")) { + // FIXME: show 215, default subscription list for new users, period + } else if (argument.equals("OVERVIEW.FMT")) { + println("215 Overview format:"); + println("Subject:"); + println("From:"); + println("Date:"); + println("Message-ID:"); + println("References:"); + println("Bytes:"); + println("Lines:"); + //println("Xref:full"); + println("."); + } else if (argument.equals("NEWSGROUPS")) { + String wildmat = st.hasMoreTokens() ? st.nextToken() : null; + // respond 215, print each newsgroup, a space, and the description; end with lone period + } else { + // barf here + } + } else { + Group[] g = api.list(); + println("215 list of groups follows"); + for(int i=0; i