1 // Copyright 2000-2005 the Contributors, as shown in the revision logs.
2 // Licensed under the Apache Public Source License 2.0 ("the License").
3 // You may not use this file except in compliance with the License.
7 // 503 optional subfeature not supported
9 // LIST EXTENSIONS is probably incomplete
10 // pull mode (ie suck)
11 // FEATURE: control message processing?
12 // cancel <Message-ID> (do not forward if I am unable to cancel locally)
13 // ihave/sendme: do not support
14 // newgroup <groupname> [moderated] -- body of message is a description of the group
15 // rmgroup <groupname>
17 package org.ibex.mail;
18 import org.ibex.util.*;
20 import org.ibex.net.*;
21 import org.ibex.mail.target.*;
22 import org.ibex.jinetd.*;
28 /** NNTP send/recieve */
31 public static final DateFormat serverDateFormat = new SimpleDateFormat("yyyyMMDDhhmmss");
32 public static final DateFormat shortNewNewsDateFormat = new SimpleDateFormat("yyMMDD HHMMSS");
33 public static final DateFormat longNewNewsDateFormat = new SimpleDateFormat("yyyyMMDD HHMMSS");
35 serverDateFormat.setTimeZone(new SimpleTimeZone(SimpleTimeZone.UTC_TIME, "GMT"));
36 shortNewNewsDateFormat.setTimeZone(new SimpleTimeZone(SimpleTimeZone.UTC_TIME, "GMT"));
37 longNewNewsDateFormat.setTimeZone(new SimpleTimeZone(SimpleTimeZone.UTC_TIME, "GMT"));
40 public static class No extends RuntimeException { int code = 400; } // 4xx response codes
41 public static class Bad extends RuntimeException { int code = 500; public Bad(String s) { super(s); } } // 5xx response codes
43 public static class Group {
44 public Group(String n, boolean p, int f, int l, int c) {
51 public final String name; // case insensitive
52 public final boolean post;
53 public final int first;
54 public final int last;
55 public final int count; // an approximation; must be >= actual number
58 public static class Article {
59 public Article(int num, Message message) { this.message = message; this.num = num; }
61 public final Message message;
66 * The API exposed by an NNTP server; remote NNTP servers appear
67 * as instances of this class, and implementations of this class
68 * may be exported as NNTP servers.
70 public static interface Server {
71 public Group group(String s);
72 public boolean ihave(String messageid);
73 public boolean want(String messageid);
74 public Article next();
75 public Article last();
76 public boolean postok();
77 public void post(Message m);
78 public Article article(String messageid, boolean head, boolean body);
79 public Article article(int messagenum, boolean head, boolean body);
80 public Group[] list();
81 public Group[] newgroups(Date d);
82 public String[] newnews(String[] groups, Date d);
85 public static class MailboxServer implements Server {
86 private final MailTree root;
87 private Mailbox current = null;
88 private int currentMessageNumber = 0;
89 public MailboxServer(MailTree root) { this.root = root; }
90 public boolean postok() { return true; }
91 public void post(Message m) { current.post(m); }
92 public Group group(String s) {
93 currentMessageNumber = 0;
94 MailTree ncurrent = resolve(s);
95 if (ncurrent == null) return null;
96 current = ncurrent.getMailbox();
97 return new Group(s, true, 1, current.count(Query.all()), current.count(Query.all()));
99 public boolean ihave(String messageid) { /* FEATURE */ return false; }
100 public boolean want(String messageid) { /* FEATURE */ return true; }
101 public Group[] newgroups(Date d) { /* FEATURE */ return new Group[] { }; }
102 public Article next() { return article(currentMessageNumber++, false, false); }
103 public Article last() { return article(currentMessageNumber--, false, false); }
104 public String[] newnews(String[] groups, Date d) {
106 for(String g : groups) {
107 Mailbox group = resolve(g).getMailbox();
108 for(Mailbox.Iterator mit = group.iterator(Query.arrival(d, null));
110 ret.add(mit.head().get("message-id"));
113 return (String[])ret.copyInto(new String[ret.size()]);
115 public Article article(String i, boolean h, boolean b) { return article(Query.header("message-id",i),h,b); }
116 public Article article(int n, boolean h, boolean b) { currentMessageNumber = n; return article(Query.nntpNumber(n,n),h,b); }
117 private Article article(Query q, boolean head, boolean body) {
118 Mailbox.Iterator it = current.iterator(q);
119 if (!it.next()) return null;
120 return new Article(it.nntpNumber(), body ? it.cur() : Message.newMessage(it.head()));
122 public Group[] list() { return list(root, ""); }
123 private Group[] list(MailTree who, String prefix) {
125 if (who == null) who = root;
126 String[] s = who.children();
127 for(int i=0; i<s.length; i++) {
128 MailTree mtree = who.slash(s[i], false);
130 // this is inefficient
131 int low = Integer.MAX_VALUE;
134 for(Mailbox.Iterator mit = mtree.getMailbox().iterator(); mit.next();) {
136 low = Math.min(low, mit.nntpNumber());
137 high = Math.max(low, mit.nntpNumber());
139 if (count==0) { low = 1; high = 0; }
141 v.addElement(new Group(prefix + s[i], true, low, high, count));
142 Group[] g2 = list(mtree, prefix + s[i] + ".");
143 for(int j=0; j<g2.length; j++) v.addElement(g2[j]);
145 Group[] ret = new Group[v.size()];
149 private MailTree resolve(String s) {
151 for(StringTokenizer st = new StringTokenizer(s, ".");
152 box!=null && st.hasMoreTokens();
153 box = box.slash(st.nextToken(), false));
158 public static class Listener {
159 private Server api = null;
161 private Connection conn;
162 public Listener(Login l) { this.login = l; }
164 private void println(String s) { conn.println(s); }
165 private void println() { conn.println(""); }
166 private void print(String s) { conn.print(s); }
168 private void article(String numOrMessageId, boolean head, boolean body) {
169 String s = numOrMessageId.trim();
171 if (s.startsWith("<")) a = api.article(s.substring(0, s.length() - 1), head, body);
172 else a = api.article(Integer.parseInt(s), head, body);
174 println("423 No such article.");
177 int code = (head && body) ? 220 : head ? 221 : body ? 222 : 223;
178 println(code + " " + a.num + " <" + a.message.messageid + "> get ready for some stuff...");
179 if (head) { a.message.headers.getStream().transcribe(conn); println(); }
180 if (head && body) println();
182 Stream stream = a.message.getBody().getStream();
185 if (s == null) break;
186 if (s.startsWith(".")) print(".");
192 public void handleRequest(Connection conn) {
194 conn.setTimeout(30 * 60 * 1000);
195 conn.setNewline("\r\n");
196 println("200 " + conn.vhost + " [" + NNTP.class.getName() + "]");
199 Account account = login.anonymous();
200 this.api = account == null ? null : new MailboxServer(account.getMailbox(NNTP.class));
201 for(String line = conn.readln(); line != null; line = conn.readln()) try {
202 Log.warn("[nntp-read]", line);
203 StringTokenizer st = new StringTokenizer(line, " ");
204 String command = st.nextToken().toUpperCase();
205 if (command.equals("AUTHINFO")) {
206 String uop = st.nextToken().toUpperCase();
207 if (uop.equals("USER")) {
208 user = st.nextToken();
209 println("381 More authentication required");
211 } else if (uop.equals("PASS")) {
212 pass = st.nextToken();
213 account = login.login(user, pass);
214 if (account == null) { println("502 Invalid"); continue; }
215 this.api = new MailboxServer(account.getMailbox(NNTP.class));
216 println("281 Good to go");
219 throw new Bad("wtf are you talking about?");
221 if (this.api == null) {
222 if (user == null) { println("480 Authentication required"); continue; }
223 if (pass == null) { println("381 Password required"); continue; }
225 if (command.equals("ARTICLE")) { article(st.hasMoreTokens() ? st.nextToken() : null, true, true);
226 } else if (command.equals("HEAD")) { article(st.hasMoreTokens() ? st.nextToken() : null, true, false);
227 } else if (command.equals("DATE")) { println("111 " + serverDateFormat.format(new Date()));
228 } else if (command.equals("MODE")) {
229 if (st.hasMoreTokens()) {
230 String arg = st.nextToken();
231 if (arg.equalsIgnoreCase("STREAM"));
232 println("203 Streaming permitted");
234 println("201 Hello, you can post.");
236 } else if (command.equals("BODY")) { article(st.hasMoreTokens() ? st.nextToken() : null, false, true);
237 } else if (command.equals("STAT")) { article(st.hasMoreTokens() ? st.nextToken() : null, false, false);
238 } else if (command.equals("HELP")) { println("100 you are beyond help."); println(".");
239 } else if (command.equals("SLAVE")) { println("220 SLAVE was removed in RFC3977, you should not use it");
240 } else if (command.equals("XOVER") || command.equals("OVER")) {
241 println("224 Overview information follows");
242 MailboxServer api = (MailboxServer)this.api;
243 String range = st.hasMoreTokens() ? st.nextToken() : (api.currentMessageNumber+"-"+api.currentMessageNumber);
244 int start = Integer.parseInt(range.substring(0, range.indexOf('-')));
245 int end = Integer.parseInt(range.substring(range.indexOf('-') + 1));
246 Mailbox.Iterator it = api.current.iterator(Query.nntpNumber(start, end));
249 Message m = it.cur();
250 println(it.nntpNumber()+"\t"+m.subject+"\t"+m.from+"\t"+m.date+"\t"+m.messageid+"\t"+
251 m.headers.get("references") + "\t" + m.getLength() + "\t" + m.getNumLines());
252 } catch (Exception e) { Log.error(this, e); }
255 } else if (command.equals("LAST")) {
256 Article a = api.last(); println("223 "+a.num+" "+a.message.messageid+" ok");
257 } else if (command.equals("NEXT")) {
258 Article a = api.next(); println("223 "+a.num+" "+a.message.messageid+" ok");
259 } else if (command.equals("QUIT")) { println("205 Bye."); conn.close(); return;
260 } else if (command.equals("GROUP")) {
261 Group g = api.group(st.nextToken().toLowerCase());
262 if (g==null) println("411 no such group");
263 else println("211 " + g.count + " " + g.first + " " + g.last + " " + g.name);
264 } else if (command.equals("NEWGROUPS") || command.equals("NEWNEWS")) {
265 // FIXME: * and ! unsupported
266 String groups = command.equals("NEWNEWS") ? st.nextToken() : null;
267 String datetime = st.nextToken() + " " + st.nextToken();
268 String gmt = st.nextToken();
271 try { d = (datetime.length() == 13 ? shortNewNewsDateFormat : longNewNewsDateFormat)
273 } catch (ParseException pe) { Log.warn(this, pe); }
275 if (command.equals("NEWGROUPS")) {
276 Group[] g = api.newgroups(d);
277 println("231 list of groups follows");
278 for(int i=0; i<g.length; i++)
279 println(g[i].name + " " + g[i].last + " " + g[i].first + " " + (g[i].post ? "y" : "n"));
282 st = new StringTokenizer(groups, ",");
283 String[] g = new String[st.countTokens()];
284 for(int i=0; st.hasMoreTokens(); i++) g[i] = st.nextToken();
285 String[] a = api.newnews(g, d);
286 println("230 list of article messageids follows");
287 for(int i=0; i<a.length; i++) println(a[i]);
291 } else if (command.equals("POST")) {
292 boolean postok = api.postok();
294 println("440 no posting allowed");
296 println("340 send the article");
298 Message m = Message.readDotEncodedMessage(conn);
299 if (m.headers.get("newsgroups")==null)
300 println("441 posted messages must have a Newsgroups header per RFC 977");
301 else if (m.headers.get("newsgroups").indexOf('*')!=-1)
302 println("441 Newsgroups header in posted messages may not contain wildcards (*) per RFC 977");
303 else if (m.headers.get("subject")==null)
304 println("441 posted messages must have a Subject header per RFC 977");
305 else if (m.headers.get("from")==null)
306 println("441 posted messages must have a From header per RFC 977");
307 else if (m.headers.get("date")==null)
308 println("441 posted messages must have a Date header per RFC 977");
311 println("240 article posted ok");
313 } catch (Exception e) {
315 println("441 posting failed: " + e);
319 } else if (command.equals("XROVER")) {
320 // equivalent to "XHDR References"
322 } else if (command.equals("XHDR") || (command.equals("HDR"))) {
323 // argument: header name
324 // argument: 1 | 1- | 1-2 | <mid> | nothing (use current article)
326 // print art#+header for all matching messages
328 // 412 if no group selected and numeric form used
329 // 430 if <mid> and not found
330 // 420 if no messages in range
332 } else if (command.equals("XPAT")) {
333 // just like XHDR, but a pattern follows the last argument (may contain whitespace)
338 } else if (command.equals("LIST")) {
339 if (st.hasMoreTokens()) {
340 String argument = st.nextToken().toUpperCase();
341 if (argument.equalsIgnoreCase("EXTENSIONS")) {
342 println("202 Extensions supported:");
343 println("STREAMING");
346 } else if (argument.equals("ACTIVE")) {
347 String wildmat = st.hasMoreTokens() ? st.nextToken() : null;
348 // FIXME: deal with wildmat
349 // just like list, but only show active groups
350 throw new Bad("not implemented yet");
351 } else if (argument.equals("SUBSCRIPTIONS")) {
352 // FIXME: show 215, default subscription list for new users, period
353 } else if (argument.equals("OVERVIEW.FMT")) {
354 println("215 Overview format:");
358 println("Message-ID:");
359 println("References:");
362 //println("Xref:full");
364 } else if (argument.equals("NEWSGROUPS")) {
365 String wildmat = st.hasMoreTokens() ? st.nextToken() : null;
366 // respond 215, print each newsgroup, a space, and the description; end with lone period
371 Group[] g = api.list();
372 println("215 list of groups follows");
373 for(int i=0; i<g.length; i++)
374 println(g[i].name + " " + g[i].last + " " + g[i].first + " " + (g[i].post ? "y" : "n"));
378 } else if (command.equals("LISTGROUP")) {
379 String groupname = st.hasMoreTokens() ? st.nextToken() : null;
380 // 211, all article numbers in group, period. Set article currentMessageNumber to first item in group
382 } else if (command.equals("XGTITLE")) {
383 String wildmat = st.hasMoreTokens() ? st.nextToken() : null;
384 // 282, then identical to LIST NEWSGROUP
386 } else if (command.equals("CHECK")) {
387 String mid = st.nextToken();
388 boolean want = api.want(mid);
389 if (!want) println("438 "+ mid+" No thanks");
390 else println("238 "+mid+" Yes, I'd like that");
392 } else if (command.equals("TAKETHIS")) {
393 String mid = st.nextToken();
394 boolean want = api.want(mid);
395 Message m = Message.readDotEncodedMessage(conn);
398 println("239 "+mid+" Rock on.");
400 println("439 "+ mid+" I really didn't want that; don't send it again.");
403 } else if (command.equals("IHAVE")) {
404 boolean want = api.ihave(st.nextToken());
406 println("435 No thanks");
408 println("335 Proceed");
409 api.post(Message.readDotEncodedMessage(conn));
410 println("235 Got it");
413 throw new Bad("wtf are you talking about?");
415 } catch (No n) { println(n.code + " " + n.getMessage());
416 } catch (Bad b) { println(b.code + " " + b.getMessage()); Log.warn(this, b); }