// 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. // 500 unrec. command // 501 syntax error // 503 optional subfeature not supported // Xref header // LIST EXTENSIONS is probably incomplete // pull mode (ie suck) // FEATURE: control message processing? // 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 package org.ibex.mail; import org.ibex.util.*; import org.ibex.io.*; import org.ibex.net.*; 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 { 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 final String name; // case insensitive public final boolean post; public final int first; public final int last; public final int count; // an approximation; must be >= actual number } public static class Article { public Article(int num, Message message) { this.message = message; this.num = num; } public final int num; 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); 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); public String[] newnews(String[] groups, Date d); } 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) { 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 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) { 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; return new Article(it.nntpNumber(), body ? it.cur() : Message.newMessage(it.head())); } public Group[] list() { return list(root, ""); } 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 get ready for some stuff..."); if (head) { a.message.headers.getStream().transcribe(conn); println(); } if (head && body) println(); 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 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")) { String uop = st.nextToken().toUpperCase(); 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; } this.api = new MailboxServer(account.getMailbox(NNTP.class)); 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; } } 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")) { println("111 " + serverDateFormat.format(new Date())); } else if (command.equals("MODE")) { if (st.hasMoreTokens()) { String arg = st.nextToken(); if (arg.equalsIgnoreCase("STREAM")); 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 SLAVE was removed in RFC3977, you should not use it"); } else if (command.equals("XOVER") || command.equals("OVER")) { println("224 Overview information follows"); 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)); while(it.next()) { try { 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.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()); 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 String groups = command.equals("NEWNEWS") ? st.nextToken() : null; String datetime = st.nextToken() + " " + st.nextToken(); String gmt = st.nextToken(); Date d = new Date(); 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); println("231 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