Added a comma after each msgnum exported. The parser was globbing them all together...
[citadel.git] / citadel / modules / nntp / serv_nntp.c
1 //
2 // NNTP server module (RFC 3977)
3 //
4 // Copyright (c) 2014-2020 by the citadel.org team
5 //
6 // This program is open source software; you can redistribute it and/or modify
7 // it under the terms of the GNU General Public License version 3.
8 //  
9 // This program is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14
15 #include "sysdep.h"
16 #include <stdlib.h>
17 #include <unistd.h>
18 #include <stdio.h>
19 #include <termios.h>
20 #include <fcntl.h>
21 #include <signal.h>
22 #include <pwd.h>
23 #include <errno.h>
24 #include <sys/types.h>
25 #include <syslog.h>
26 #include <time.h>
27 #include <sys/wait.h>
28 #include <ctype.h>
29 #include <string.h>
30 #include <limits.h>
31 #include <sys/socket.h>
32 #include <netinet/in.h>
33 #include <arpa/inet.h>
34 #include <libcitadel.h>
35 #include "citadel.h"
36 #include "server.h"
37 #include "citserver.h"
38 #include "support.h"
39 #include "config.h"
40 #include "control.h"
41 #include "user_ops.h"
42 #include "room_ops.h"
43 #include "database.h"
44 #include "msgbase.h"
45 #include "internet_addressing.h"
46 #include "genstamp.h"
47 #include "domain.h"
48 #include "clientsocket.h"
49 #include "locate_host.h"
50 #include "citadel_dirs.h"
51 #include "ctdl_module.h"
52 #include "serv_nntp.h"
53
54 extern long timezone;
55
56 //
57 // Tests whether the supplied string is a valid newsgroup name
58 // Returns true (nonzero) or false (0)
59 //
60 int is_valid_newsgroup_name(char *name) {
61         char *ptr = name;
62         int has_a_letter = 0;
63         int num_dots = 0;
64
65         if (!ptr) return(0);
66         if (!strncasecmp(name, "ctdl.", 5)) return(0);
67
68         while (*ptr != 0) {
69
70                 if (isalpha(ptr[0])) {
71                         has_a_letter = 1;
72                 }
73
74                 if (ptr[0] == '.') {
75                         ++num_dots;
76                 }
77
78                 if (    (isalnum(ptr[0]))
79                         || (ptr[0] == '.')
80                         || (ptr[0] == '+')
81                         || (ptr[0] == '-')
82                 ) {
83                         ++ptr;
84                 }
85                 else {
86                         return(0);
87                 }
88         }
89         return( (has_a_letter) && (num_dots >= 1) ) ;
90 }
91
92
93 //
94 // Convert a Citadel room name to a valid newsgroup name
95 //
96 void room_to_newsgroup(char *target, char *source, size_t target_size) {
97
98         if (!target) return;
99         if (!source) return;
100
101         if (is_valid_newsgroup_name(source)) {
102                 strncpy(target, source, target_size);
103                 return;
104         }
105
106         strcpy(target, "ctdl.");
107         int len = 5;
108         char *ptr = source;
109         char ch;
110
111         while (ch=*ptr++, ch!=0) {
112                 if (len >= target_size) return;
113                 if (    (isalnum(ch))
114                         || (ch == '.')
115                         || (ch == '-')
116                 ) {
117                         target[len++] = tolower(ch);
118                         target[len] = 0;
119                 }
120                 else {
121                         target[len++] = '+' ;
122                         sprintf(&target[len], "%02x", ch);
123                         len += 2;
124                         target[len] = 0;
125                 }
126         }
127 }
128
129
130 //
131 // Convert a newsgroup name to a Citadel room name.
132 // This function recognizes names converted with room_to_newsgroup() and restores them with full fidelity.
133 //
134 void newsgroup_to_room(char *target, char *source, size_t target_size) {
135
136         if (!target) return;
137         if (!source) return;
138
139         if (strncasecmp(source, "ctdl.", 5)) {                  // not a converted room name; pass through as-is
140                 strncpy(target, source, target_size);
141                 return;
142         }
143
144         target[0] = 0;
145         int len = 0;
146         char *ptr = &source[5];
147         char ch;
148
149         while (ch=*ptr++, ch!=0) {
150                 if (len >= target_size) return;
151                 if (ch == '+') {
152                         char hex[3];
153                         long digit;
154                         hex[0] = *ptr++;
155                         hex[1] = *ptr++;
156                         hex[2] = 0;
157                         digit = strtol(hex, NULL, 16);
158                         ch = (char)digit;
159                 }
160                 target[len++] = ch;
161                 target[len] = 0;
162         }
163 }
164
165
166 //
167 // Here's where our NNTP session begins its happy day.
168 //
169 void nntp_greeting(void) {
170         strcpy(CC->cs_clientname, "NNTP session");
171         CC->cs_flags |= CS_STEALTH;
172
173         CC->session_specific_data = malloc(sizeof(citnntp));
174         citnntp *nntpstate = (citnntp *) CC->session_specific_data;
175         memset(nntpstate, 0, sizeof(citnntp));
176
177         if (CC->nologin==1) {
178                 cprintf("451 Too many connections are already open; please try again later.\r\n");
179                 CC->kill_me = KILLME_MAX_SESSIONS_EXCEEDED;
180                 return;
181         }
182
183         // Display the standard greeting
184         cprintf("200 %s NNTP Citadel server is not finished yet\r\n", CtdlGetConfigStr("c_fqdn"));
185 }
186
187
188 //
189 // NNTPS is just like NNTP, except it goes crypto right away.
190 //
191 void nntps_greeting(void) {
192         CtdlModuleStartCryptoMsgs(NULL, NULL, NULL);
193 #ifdef HAVE_OPENSSL
194         if (!CC->redirect_ssl) CC->kill_me = KILLME_NO_CRYPTO;          /* kill session if no crypto */
195 #endif
196         nntp_greeting();
197 }
198
199
200 //
201 // implements the STARTTLS command
202 //
203 void nntp_starttls(void) {
204         char ok_response[SIZ];
205         char nosup_response[SIZ];
206         char error_response[SIZ];
207
208         sprintf(ok_response, "382 Begin TLS negotiation now\r\n");
209         sprintf(nosup_response, "502 Can not initiate TLS negotiation\r\n");
210         sprintf(error_response, "580 Internal error\r\n");
211         CtdlModuleStartCryptoMsgs(ok_response, nosup_response, error_response);
212 }
213
214
215 //
216 // Implements the CAPABILITY command
217 //
218 void nntp_capabilities(void) {
219         cprintf("101 Capability list:\r\n");
220         cprintf("IMPLEMENTATION Citadel %d\r\n", REV_LEVEL);
221         cprintf("VERSION 2\r\n");
222         cprintf("READER\r\n");
223         cprintf("MODE-READER\r\n");
224         cprintf("LIST ACTIVE NEWSGROUPS\r\n");
225         cprintf("OVER\r\n");
226 #ifdef HAVE_OPENSSL
227         cprintf("STARTTLS\r\n");
228 #endif
229         if (!CC->logged_in) {
230                 cprintf("AUTHINFO USER\r\n");
231         }
232         cprintf(".\r\n");
233 }
234
235
236 // 
237 // Implements the QUIT command
238 //
239 void nntp_quit(void) {
240         cprintf("221 Goodbye...\r\n");
241         CC->kill_me = KILLME_CLIENT_LOGGED_OUT;
242 }
243
244
245 //
246 // Implements the AUTHINFO USER command (RFC 4643)
247 //
248 void nntp_authinfo_user(const char *username) {
249         int a = CtdlLoginExistingUser(username);
250         switch (a) {
251         case login_already_logged_in:
252                 cprintf("482 Already logged in\r\n");
253                 return;
254         case login_too_many_users:
255                 cprintf("481 Too many users are already online (maximum is %d)\r\n", CtdlGetConfigInt("c_maxsessions"));
256                 return;
257         case login_ok:
258                 cprintf("381 Password required for %s\r\n", CC->curr_user);
259                 return;
260         case login_not_found:
261                 cprintf("481 %s not found\r\n", username);
262                 return;
263         default:
264                 cprintf("502 Internal error\r\n");
265         }
266 }
267
268
269 //
270 // Implements the AUTHINFO PASS command (RFC 4643)
271 //
272 void nntp_authinfo_pass(const char *buf) {
273         int a;
274
275         a = CtdlTryPassword(buf, strlen(buf));
276
277         switch (a) {
278         case pass_already_logged_in:
279                 cprintf("482 Already logged in\r\n");
280                 return;
281         case pass_no_user:
282                 cprintf("482 Authentication commands issued out of sequence\r\n");
283                 return;
284         case pass_wrong_password:
285                 cprintf("481 Authentication failed\r\n");
286                 return;
287         case pass_ok:
288                 cprintf("281 Authentication accepted\r\n");
289                 return;
290         }
291 }
292
293
294 //
295 // Implements the AUTHINFO extension (RFC 4643) in USER/PASS mode
296 //
297 void nntp_authinfo(const char *cmd) {
298
299         if (!strncasecmp(cmd, "authinfo user ", 14)) {
300                 nntp_authinfo_user(&cmd[14]);
301         }
302
303         else if (!strncasecmp(cmd, "authinfo pass ", 14)) {
304                 nntp_authinfo_pass(&cmd[14]);
305         }
306
307         else {
308                 cprintf("502 command unavailable\r\n");
309         }
310 }
311
312
313 //
314 // Utility function to fetch the current list of message numbers in a room
315 //
316 struct nntp_msglist nntp_fetch_msglist(struct ctdlroom *qrbuf) {
317         struct nntp_msglist nm;
318         struct cdbdata *cdbfr;
319
320         cdbfr = cdb_fetch(CDB_MSGLISTS, &qrbuf->QRnumber, sizeof(long));
321         if (cdbfr != NULL) {
322                 nm.msgnums = (long*)cdbfr->ptr;
323                 cdbfr->ptr = NULL;
324                 nm.num_msgs = cdbfr->len / sizeof(long);
325                 cdbfr->len = 0;
326                 cdb_free(cdbfr);
327         } else {
328                 nm.num_msgs = 0;
329                 nm.msgnums = NULL;
330         }
331         return(nm);
332 }
333
334
335 //
336 // Output a room name (newsgroup name) in formats required for LIST and NEWGROUPS command
337 //
338 void output_roomname_in_list_format(struct ctdlroom *qrbuf, int which_format, char *wildmat_pattern) {
339         char n_name[1024];
340         struct nntp_msglist nm;
341         long low_water_mark = 0;
342         long high_water_mark = 0;
343
344         room_to_newsgroup(n_name, qrbuf->QRname, sizeof n_name);
345
346         if ((wildmat_pattern != NULL) && (!IsEmptyStr(wildmat_pattern))) {
347                 if (!wildmat(n_name, wildmat_pattern)) {
348                         return;
349                 }
350         }
351
352         nm = nntp_fetch_msglist(qrbuf);
353         if ((nm.num_msgs > 0) && (nm.msgnums != NULL)) {
354                 low_water_mark = nm.msgnums[0];
355                 high_water_mark = nm.msgnums[nm.num_msgs - 1];
356         }
357
358         // Only the mandatory formats are supported
359         switch(which_format) {
360         case NNTP_LIST_ACTIVE:
361                 // FIXME we have hardcoded "n" for "no posting allowed" -- fix when we add posting
362                 cprintf("%s %ld %ld n\r\n", n_name, high_water_mark, low_water_mark);
363                 break;
364         case NNTP_LIST_NEWSGROUPS:
365                 cprintf("%s %s\r\n", n_name, qrbuf->QRname);
366                 break;
367         }
368
369         if (nm.msgnums != NULL) {
370                 free(nm.msgnums);
371         }
372 }
373
374
375 //
376 // Called once per room by nntp_newgroups() to qualify and possibly output a single room
377 //
378 void nntp_newgroups_backend(struct ctdlroom *qrbuf, void *data) {
379         int ra;
380         int view;
381         time_t thetime = *(time_t *)data;
382
383         CtdlRoomAccess(qrbuf, &CC->user, &ra, &view);
384
385         /*
386          * The "created after <date/time>" heuristics depend on the happy coincidence
387          * that for a very long time we have used a unix timestamp as the room record's
388          * generation number (QRgen).  When this module is merged into the master
389          * source tree we should rename QRgen to QR_create_time or something like that.
390          */
391
392         if (ra & UA_KNOWN) {
393                 if (qrbuf->QRgen >= thetime) {
394                         output_roomname_in_list_format(qrbuf, NNTP_LIST_ACTIVE, NULL);
395                 }
396         }
397 }
398
399
400 //
401 // Implements the NEWGROUPS command
402 //
403 void nntp_newgroups(const char *cmd) {
404         if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
405
406         char stringy_date[16];
407         char stringy_time[16];
408         char stringy_gmt[16];
409         struct tm tm;
410         time_t thetime;
411
412         extract_token(stringy_date, cmd, 1, ' ', sizeof stringy_date);
413         extract_token(stringy_time, cmd, 2, ' ', sizeof stringy_time);
414         extract_token(stringy_gmt, cmd, 3, ' ', sizeof stringy_gmt);
415
416         memset(&tm, 0, sizeof tm);
417         if (strlen(stringy_date) == 6) {
418                 sscanf(stringy_date, "%2d%2d%2d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday);
419                 tm.tm_year += 100;
420         }
421         else {
422                 sscanf(stringy_date, "%4d%2d%2d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday);
423                 tm.tm_year -= 1900;
424         }
425         tm.tm_mon -= 1;         // tm_mon is zero based (0=January)
426         tm.tm_isdst = (-1);     // let the C library figure out whether DST is in effect
427         sscanf(stringy_time, "%2d%2d%2d", &tm.tm_hour, &tm.tm_min ,&tm.tm_sec);
428         thetime = mktime(&tm);
429         if (!strcasecmp(stringy_gmt, "GMT")) {
430                 tzset();
431                 thetime += timezone;
432         }
433
434
435         cprintf("231 list of new newsgroups follows\r\n");
436         CtdlGetUser(&CC->user, CC->curr_user);
437         CtdlForEachRoom(nntp_newgroups_backend, &thetime);
438         cprintf(".\r\n");
439 }
440
441
442 //
443 // Called once per room by nntp_list() to qualify and possibly output a single room
444 //
445 void nntp_list_backend(struct ctdlroom *qrbuf, void *data) {
446         int ra;
447         int view;
448         struct nntp_list_data *nld = (struct nntp_list_data *)data;
449
450         CtdlRoomAccess(qrbuf, &CC->user, &ra, &view);
451         if (ra & UA_KNOWN) {
452                 output_roomname_in_list_format(qrbuf, nld->list_format, nld->wildmat_pattern);
453         }
454 }
455
456
457 //
458 // Implements the LIST commands
459 //
460 void nntp_list(const char *cmd) {
461         if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
462
463         char list_format[64];
464         char wildmat_pattern[1024];
465         struct nntp_list_data nld;
466
467         extract_token(list_format, cmd, 1, ' ', sizeof list_format);
468         extract_token(wildmat_pattern, cmd, 2, ' ', sizeof wildmat_pattern);
469
470         if (strlen(wildmat_pattern) > 0) {
471                 nld.wildmat_pattern = wildmat_pattern;
472         }
473         else {
474                 nld.wildmat_pattern = NULL;
475         }
476
477         if ( (strlen(cmd) < 6) || (!strcasecmp(list_format, "ACTIVE")) ) {
478                 nld.list_format = NNTP_LIST_ACTIVE;
479         }
480         else if (!strcasecmp(list_format, "NEWSGROUPS")) {
481                 nld.list_format = NNTP_LIST_NEWSGROUPS;
482         }
483         else if (!strcasecmp(list_format, "OVERVIEW.FMT")) {
484                 nld.list_format = NNTP_LIST_OVERVIEW_FMT;
485         }
486         else {
487                 cprintf("501 syntax error , unsupported list format\r\n");
488                 return;
489         }
490
491         // OVERVIEW.FMT delivers a completely different type of data than all of the
492         // other LIST commands.  It's a stupid place to put it.  But that's how it's
493         // written into RFC3977, so we have to handle it here.
494         if (nld.list_format == NNTP_LIST_OVERVIEW_FMT) {
495                 cprintf("215 Order of fields in overview database.\r\n");
496                 cprintf("Subject:\r\n");
497                 cprintf("From:\r\n");
498                 cprintf("Date:\r\n");
499                 cprintf("Message-ID:\r\n");
500                 cprintf("References:\r\n");
501                 cprintf("Bytes:\r\n");
502                 cprintf("Lines:\r\n");
503                 cprintf(".\r\n");
504                 return;
505         }
506
507         cprintf("215 list of newsgroups follows\r\n");
508         CtdlGetUser(&CC->user, CC->curr_user);
509         CtdlForEachRoom(nntp_list_backend, &nld);
510         cprintf(".\r\n");
511 }
512
513
514 //
515 // Implement HELP command.
516 //
517 void nntp_help(void) {
518         cprintf("100 This is the Citadel NNTP service.\r\n");
519         cprintf("RTFM http://www.ietf.org/rfc/rfc3977.txt\r\n");
520         cprintf(".\r\n");
521 }
522
523
524 //
525 // Implement DATE command.
526 //
527 void nntp_date(void) {
528         time_t now;
529         struct tm nowLocal;
530         struct tm nowUtc;
531         char tsFromUtc[32];
532
533         now = time(NULL);
534         localtime_r(&now, &nowLocal);
535         gmtime_r(&now, &nowUtc);
536
537         strftime(tsFromUtc, sizeof(tsFromUtc), "%Y%m%d%H%M%S", &nowUtc);
538
539         cprintf("111 %s\r\n", tsFromUtc);
540 }
541
542
543 //
544 // back end for the LISTGROUP command , called for each message number
545 //
546 void nntp_listgroup_backend(long msgnum, void *userdata) {
547
548         struct listgroup_range *lr = (struct listgroup_range *)userdata;
549
550         // check range if supplied
551         if (msgnum < lr->lo) return;
552         if ((lr->hi != 0) && (msgnum > lr->hi)) return;
553
554         cprintf("%ld\r\n", msgnum);
555 }
556
557
558 //
559 // Implements the GROUP and LISTGROUP commands
560 //
561 void nntp_group(const char *cmd) {
562         if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
563
564         citnntp *nntpstate = (citnntp *) CC->session_specific_data;
565         char verb[16];
566         char requested_group[1024];
567         char message_range[256];
568         char range_lo[256];
569         char range_hi[256];
570         char requested_room[ROOMNAMELEN];
571         char augmented_roomname[ROOMNAMELEN];
572         int c = 0;
573         int ok = 0;
574         int ra = 0;
575         struct ctdlroom QRscratch;
576         int msgs, new;
577         long oldest,newest;
578         struct listgroup_range lr;
579
580         extract_token(verb, cmd, 0, ' ', sizeof verb);
581         extract_token(requested_group, cmd, 1, ' ', sizeof requested_group);
582         extract_token(message_range, cmd, 2, ' ', sizeof message_range);
583         extract_token(range_lo, message_range, 0, '-', sizeof range_lo);
584         extract_token(range_hi, message_range, 1, '-', sizeof range_hi);
585         lr.lo = atoi(range_lo);
586         lr.hi = atoi(range_hi);
587
588         /* In LISTGROUP mode we can specify an empty name for 'currently selected' */
589         if ((!strcasecmp(verb, "LISTGROUP")) && (IsEmptyStr(requested_group))) {
590                 room_to_newsgroup(requested_group, CC->room.QRname, sizeof requested_group);
591         }
592
593         /* First try a regular match */
594         newsgroup_to_room(requested_room, requested_group, sizeof requested_room);
595         c = CtdlGetRoom(&QRscratch, requested_room);
596
597         /* Then try a mailbox name match */
598         if (c != 0) {
599                 CtdlMailboxName(augmented_roomname, sizeof augmented_roomname, &CC->user, requested_room);
600                 c = CtdlGetRoom(&QRscratch, augmented_roomname);
601                 if (c == 0) {
602                         safestrncpy(requested_room, augmented_roomname, sizeof(requested_room));
603                 }
604         }
605
606         /* If the room exists, check security/access */
607         if (c == 0) {
608                 /* See if there is an existing user/room relationship */
609                 CtdlRoomAccess(&QRscratch, &CC->user, &ra, NULL);
610
611                 /* normal clients have to pass through security */
612                 if (ra & UA_KNOWN) {
613                         ok = 1;
614                 }
615         }
616
617         /* Fail here if no such room */
618         if (!ok) {
619                 cprintf("411 no such newsgroup\r\n");
620                 return;
621         }
622
623
624         /*
625          * CtdlUserGoto() formally takes us to the desired room, happily returning
626          * the number of messages and number of new messages.
627          */
628         memcpy(&CC->room, &QRscratch, sizeof(struct ctdlroom));
629         CtdlUserGoto(NULL, 0, 0, &msgs, &new, &oldest, &newest);
630         cprintf("211 %d %ld %ld %s\r\n", msgs, oldest, newest, requested_group);
631
632         // If this is a GROUP command, set the "current article number" to zero, and then stop here.
633         if (!strcasecmp(verb, "GROUP")) {
634                 nntpstate->current_article_number = oldest;
635                 return;
636         }
637
638         // If we get to this point we are running a LISTGROUP command.  Fetch those message numbers.
639         CtdlForEachMessage(MSGS_ALL, 0L, NULL, NULL, NULL, nntp_listgroup_backend, &lr);
640         cprintf(".\r\n");
641 }
642
643
644 //
645 // Implements the MODE command
646 //
647 void nntp_mode(const char *cmd) {
648
649         char which_mode[16];
650
651         extract_token(which_mode, cmd, 1, ' ', sizeof which_mode);
652
653         if (!strcasecmp(which_mode, "reader")) {
654                 // FIXME implement posting and change to 200
655                 cprintf("201 Reader mode activated\r\n");
656         }
657         else {
658                 cprintf("501 unknown mode\r\n");
659         }
660 }
661
662
663 //
664 // Implements the ARTICLE, HEAD, BODY, and STAT commands.
665 // (These commands all accept the same parameters; they differ only in how they output the retrieved message.)
666 //
667 void nntp_article(const char *cmd) {
668         if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
669
670         citnntp *nntpstate = (citnntp *) CC->session_specific_data;
671         char which_command[16];
672         int acmd = 0;
673         char requested_article[256];
674         long requested_msgnum = 0;
675         char *lb, *rb = NULL;
676         int must_change_currently_selected_article = 0;
677
678         // We're going to store one of these values in the variable 'acmd' so that
679         // we can quickly check later which version of the output we want.
680         enum {
681                 ARTICLE,
682                 HEAD,
683                 BODY,
684                 STAT
685         };
686
687         extract_token(which_command, cmd, 0, ' ', sizeof which_command);
688
689         if (!strcasecmp(which_command, "article")) {
690                 acmd = ARTICLE;
691         }
692         else if (!strcasecmp(which_command, "head")) {
693                 acmd = HEAD;
694         }
695         else if (!strcasecmp(which_command, "body")) {
696                 acmd = BODY;
697         }
698         else if (!strcasecmp(which_command, "stat")) {
699                 acmd = STAT;
700         }
701         else {
702                 cprintf("500 I'm afraid I can't do that.\r\n");
703                 return;
704         }
705
706         // Which NNTP command was issued, determines whether we will fetch headers, body, or both.
707         int                     headers_only = HEADERS_ALL;
708         if (acmd == HEAD)       headers_only = HEADERS_FAST;
709         else if (acmd == BODY)  headers_only = HEADERS_NONE;
710         else if (acmd == STAT)  headers_only = HEADERS_FAST;
711
712         // now figure out what the client is asking for.
713         extract_token(requested_article, cmd, 1, ' ', sizeof requested_article);
714         lb = strchr(requested_article, '<');
715         rb = strchr(requested_article, '>');
716         requested_msgnum = atol(requested_article);
717
718         // If no article number or message-id is specified, the client wants the "currently selected article"
719         if (IsEmptyStr(requested_article)) {
720                 if (nntpstate->current_article_number < 1) {
721                         cprintf("420 No current article selected\r\n");
722                         return;
723                 }
724                 requested_msgnum = nntpstate->current_article_number;
725                 must_change_currently_selected_article = 1;
726                 // got it -- now fall through and keep going
727         }
728
729         // If the requested article is numeric, it maps directly to a message number.  Good.
730         else if (requested_msgnum > 0) {
731                 must_change_currently_selected_article = 1;
732                 // good -- fall through and keep going
733         }
734
735         // If the requested article has angle brackets, the client wants a specific message-id.
736         // We don't know how to do that yet.
737         else if ( (lb != NULL) && (rb != NULL) && (lb < rb) ) {
738                 must_change_currently_selected_article = 0;
739                 cprintf("500 I don't know how to fetch by message-id yet.\r\n");        // FIXME
740                 return;
741         }
742
743         // Anything else is noncompliant gobbledygook and should die in a car fire.
744         else {
745                 must_change_currently_selected_article = 0;
746                 cprintf("500 syntax error\r\n");
747                 return;
748         }
749
750         // At this point we know the message number of the "article" being requested.
751         // We have an awesome API call that does all the heavy lifting for us.
752         char *fetched_message_id = NULL;
753         CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
754         int fetch = CtdlOutputMsg(requested_msgnum,
755                         MT_RFC822,              // output in RFC822 format ... sort of
756                         headers_only,           // headers, body, or both?
757                         0,                      // don't do Citadel protocol responses
758                         1,                      // CRLF newlines
759                         NULL,                   // teh whole thing, not just a section
760                         0,                      // no flags yet ... maybe new ones for Path: etc ?
761                         NULL,
762                         NULL,
763                         &fetched_message_id     // extract the message ID from the message as we go...
764         );
765         StrBuf *msgtext = CC->redirect_buffer;
766         CC->redirect_buffer = NULL;
767
768         if (fetch != om_ok) {
769                 cprintf("423 no article with that number\r\n");
770                 FreeStrBuf(&msgtext);
771                 return;
772         }
773
774         // RFC3977 6.2.1.2 specifes conditions under which the "currently selected article"
775         // MUST or MUST NOT be set to the message we just referenced.
776         if (must_change_currently_selected_article) {
777                 nntpstate->current_article_number = requested_msgnum;
778         }
779
780         // Now give the client what it asked for.
781         if (acmd == ARTICLE) {
782                 cprintf("220 %ld <%s>\r\n", requested_msgnum, fetched_message_id);
783         }
784         if (acmd == HEAD) {
785                 cprintf("221 %ld <%s>\r\n", requested_msgnum, fetched_message_id);
786         }
787         if (acmd == BODY) {
788                 cprintf("222 %ld <%s>\r\n", requested_msgnum, fetched_message_id);
789         }
790         if (acmd == STAT) {
791                 cprintf("223 %ld <%s>\r\n", requested_msgnum, fetched_message_id);
792                 FreeStrBuf(&msgtext);
793                 return;
794         }
795
796         client_write(SKEY(msgtext));
797         cprintf(".\r\n");                       // this protocol uses a dot terminator
798         FreeStrBuf(&msgtext);
799         if (fetched_message_id) free(fetched_message_id);
800 }
801
802
803 //
804 // Utility function for nntp_last_next() that turns a msgnum into a message ID.
805 // The memory for the returned string is pwnz0red by the caller.
806 //
807 char *message_id_from_msgnum(long msgnum) {
808         char *fetched_message_id = NULL;
809         CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
810         CtdlOutputMsg(msgnum,
811                         MT_RFC822,              // output in RFC822 format ... sort of
812                         HEADERS_FAST,           // headers, body, or both?
813                         0,                      // don't do Citadel protocol responses
814                         1,                      // CRLF newlines
815                         NULL,                   // teh whole thing, not just a section
816                         0,                      // no flags yet ... maybe new ones for Path: etc ?
817                         NULL,
818                         NULL,
819                         &fetched_message_id     // extract the message ID from the message as we go...
820         );
821         StrBuf *msgtext = CC->redirect_buffer;
822         CC->redirect_buffer = NULL;
823
824         FreeStrBuf(&msgtext);
825         return(fetched_message_id);
826 }
827
828
829 //
830 // The LAST and NEXT commands are so similar that they are handled by a single function.
831 //
832 void nntp_last_next(const char *cmd) {
833         if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
834
835         citnntp *nntpstate = (citnntp *) CC->session_specific_data;
836         char which_command[16];
837         int acmd = 0;
838
839         // We're going to store one of these values in the variable 'acmd' so that
840         // we can quickly check later which version of the output we want.
841         enum {
842                 NNTP_LAST,
843                 NNTP_NEXT
844         };
845
846         extract_token(which_command, cmd, 0, ' ', sizeof which_command);
847
848         if (!strcasecmp(which_command, "last")) {
849                 acmd = NNTP_LAST;
850         }
851         else if (!strcasecmp(which_command, "next")) {
852                 acmd = NNTP_NEXT;
853         }
854         else {
855                 cprintf("500 I'm afraid I can't do that.\r\n");
856                 return;
857         }
858
859         // ok, here we go ... fetch the msglist so we can figure out our place in the universe
860         struct nntp_msglist nm;
861         int i = 0;
862         long selected_msgnum = 0;
863         char *message_id = NULL;
864
865         nm = nntp_fetch_msglist(&CC->room);
866         if ((nm.num_msgs < 0) || (nm.msgnums == NULL)) {
867                 cprintf("500 something bad happened\r\n");
868                 return;
869         }
870
871         if ( (acmd == NNTP_LAST) && (nm.num_msgs == 0) ) {
872                         cprintf("422 no previous article in this group\r\n");   // nothing here
873         }
874
875         else if ( (acmd == NNTP_LAST) && (nntpstate->current_article_number <= nm.msgnums[0]) ) {
876                         cprintf("422 no previous article in this group\r\n");   // already at the beginning
877         }
878
879         else if (acmd == NNTP_LAST) {
880                 for (i=0; ((i<nm.num_msgs)&&(selected_msgnum<=0)); ++i) {
881                         if ( (nm.msgnums[i] >= nntpstate->current_article_number) && (i > 0) ) {
882                                 selected_msgnum = nm.msgnums[i-1];
883                         }
884                 }
885                 if (selected_msgnum > 0) {
886                         nntpstate->current_article_number = selected_msgnum;
887                         message_id = message_id_from_msgnum(nntpstate->current_article_number);
888                         cprintf("223 %ld <%s>\r\n", nntpstate->current_article_number, message_id);
889                         if (message_id) free(message_id);
890                 }
891                 else {
892                         cprintf("422 no previous article in this group\r\n");
893                 }
894         }
895
896         else if ( (acmd == NNTP_NEXT) && (nm.num_msgs == 0) ) {
897                         cprintf("421 no next article in this group\r\n");       // nothing here
898         }
899
900         else if ( (acmd == NNTP_NEXT) && (nntpstate->current_article_number >= nm.msgnums[nm.num_msgs-1]) ) {
901                         cprintf("421 no next article in this group\r\n");       // already at the end
902         }
903
904         else if (acmd == NNTP_NEXT) {
905                 for (i=0; ((i<nm.num_msgs)&&(selected_msgnum<=0)); ++i) {
906                         if (nm.msgnums[i] > nntpstate->current_article_number) {
907                                 selected_msgnum = nm.msgnums[i];
908                         }
909                 }
910                 if (selected_msgnum > 0) {
911                         nntpstate->current_article_number = selected_msgnum;
912                         message_id = message_id_from_msgnum(nntpstate->current_article_number);
913                         cprintf("223 %ld <%s>\r\n", nntpstate->current_article_number, message_id);
914                         if (message_id) free(message_id);
915                 }
916                 else {
917                         cprintf("421 no next article in this group\r\n");
918                 }
919         }
920
921         // should never get here
922         else {
923                 cprintf("500 internal error\r\n");
924         }
925
926         if (nm.msgnums != NULL) {
927                 free(nm.msgnums);
928         }
929
930 }
931
932
933 //
934 // back end for the XOVER command , called for each message number
935 //
936 void nntp_xover_backend(long msgnum, void *userdata) {
937
938         struct listgroup_range *lr = (struct listgroup_range *)userdata;
939
940         // check range if supplied
941         if (msgnum < lr->lo) return;
942         if ((lr->hi != 0) && (msgnum > lr->hi)) return;
943
944         struct CtdlMessage *msg = CtdlFetchMessage(msgnum, 0);
945         if (msg == NULL) {
946                 return;
947         }
948
949         // Teh RFC says we need:
950         // -------------------------
951         // Subject header content
952         // From header content
953         // Date header content
954         // Message-ID header content
955         // References header content
956         // :bytes metadata item
957         // :lines metadata item
958
959         time_t msgtime = atol(msg->cm_fields[eTimestamp]);
960         char strtimebuf[26];
961         ctime_r(&msgtime, strtimebuf);
962
963         // here we go -- print the line o'data
964         cprintf("%ld\t%s\t%s <%s>\t%s\t%s\t%s\t100\t10\r\n",
965                 msgnum,
966                 msg->cm_fields[eMsgSubject],
967                 msg->cm_fields[eAuthor],
968                 msg->cm_fields[erFc822Addr],
969                 strtimebuf,
970                 msg->cm_fields[emessageId],
971                 msg->cm_fields[eWeferences]
972         );
973
974         CM_Free(msg);
975 }
976
977
978 //
979 //
980 // XOVER is used by some clients, even if we don't offer it
981 //
982 void nntp_xover(const char *cmd) {
983         if (CtdlAccessCheck(ac_logged_in_or_guest)) return;
984
985         citnntp *nntpstate = (citnntp *) CC->session_specific_data;
986         char range[256];
987         struct listgroup_range lr;
988
989         extract_token(range, cmd, 1, ' ', sizeof range);
990         lr.lo = atol(range);
991         if (lr.lo <= 0) {
992                 lr.lo = nntpstate->current_article_number;
993                 lr.hi = nntpstate->current_article_number;
994         }
995         else {
996                 char *dash = strchr(range, '-');
997                 if (dash != NULL) {
998                         ++dash;
999                         lr.hi = atol(dash);
1000                         if (lr.hi == 0) {
1001                                 lr.hi = LONG_MAX;
1002                         }
1003                         if (lr.hi < lr.lo) {
1004                                 lr.hi = lr.lo;
1005                         }
1006                 }
1007                 else {
1008                         lr.hi = lr.lo;
1009                 }
1010         }
1011
1012         cprintf("224 Overview information follows\r\n");
1013         CtdlForEachMessage(MSGS_ALL, 0L, NULL, NULL, NULL, nntp_xover_backend, &lr);
1014         cprintf(".\r\n");
1015 }
1016
1017
1018 // 
1019 // Main command loop for NNTP server sessions.
1020 //
1021 void nntp_command_loop(void) {
1022         StrBuf *Cmd = NewStrBuf();
1023         char cmdname[16];
1024
1025         time(&CC->lastcmd);
1026         if (CtdlClientGetLine(Cmd) < 1) {
1027                 syslog(LOG_CRIT, "NNTP: client disconnected: ending session.\n");
1028                 CC->kill_me = KILLME_CLIENT_DISCONNECTED;
1029                 FreeStrBuf(&Cmd);
1030                 return;
1031         }
1032         syslog(LOG_DEBUG, "NNTP: %s\n", ((!strncasecmp(ChrPtr(Cmd), "AUTHINFO", 8)) ? "AUTHINFO ..." : ChrPtr(Cmd)));
1033         extract_token(cmdname, ChrPtr(Cmd), 0, ' ', sizeof cmdname);
1034
1035         // Rumpelstiltskin lookups are *awesome*
1036
1037         if (!strcasecmp(cmdname, "quit")) {
1038                 nntp_quit();
1039         }
1040
1041         else if (!strcasecmp(cmdname, "help")) {
1042                 nntp_help();
1043         }
1044
1045         else if (!strcasecmp(cmdname, "date")) {
1046                 nntp_date();
1047         }
1048
1049         else if (!strcasecmp(cmdname, "capabilities")) {
1050                 nntp_capabilities();
1051         }
1052
1053         else if (!strcasecmp(cmdname, "starttls")) {
1054                 nntp_starttls();
1055         }
1056
1057         else if (!strcasecmp(cmdname, "authinfo")) {
1058                 nntp_authinfo(ChrPtr(Cmd));
1059         }
1060
1061         else if (!strcasecmp(cmdname, "newgroups")) {
1062                 nntp_newgroups(ChrPtr(Cmd));
1063         }
1064
1065         else if (!strcasecmp(cmdname, "list")) {
1066                 nntp_list(ChrPtr(Cmd));
1067         }
1068
1069         else if (!strcasecmp(cmdname, "group")) {
1070                 nntp_group(ChrPtr(Cmd));
1071         }
1072
1073         else if (!strcasecmp(cmdname, "listgroup")) {
1074                 nntp_group(ChrPtr(Cmd));
1075         }
1076
1077         else if (!strcasecmp(cmdname, "mode")) {
1078                 nntp_mode(ChrPtr(Cmd));
1079         }
1080
1081         else if (
1082                         (!strcasecmp(cmdname, "article"))
1083                         || (!strcasecmp(cmdname, "head"))
1084                         || (!strcasecmp(cmdname, "body"))
1085                         || (!strcasecmp(cmdname, "stat"))
1086                 )
1087         {
1088                 nntp_article(ChrPtr(Cmd));
1089         }
1090
1091         else if (
1092                         (!strcasecmp(cmdname, "last"))
1093                         || (!strcasecmp(cmdname, "next"))
1094                 )
1095         {
1096                 nntp_last_next(ChrPtr(Cmd));
1097         }
1098
1099         else if (
1100                         (!strcasecmp(cmdname, "xover"))
1101                         || (!strcasecmp(cmdname, "over"))
1102                 )
1103         {
1104                 nntp_xover(ChrPtr(Cmd));
1105         }
1106
1107         else {
1108                 cprintf("500 I'm afraid I can't do that.\r\n");
1109         }
1110
1111         FreeStrBuf(&Cmd);
1112 }
1113
1114
1115 //      ****************************************************************************
1116 //                            MODULE INITIALIZATION STUFF
1117 //      ****************************************************************************
1118
1119
1120 //
1121 // This cleanup function blows away the temporary memory used by
1122 // the NNTP server.
1123 //
1124 void nntp_cleanup_function(void) {
1125         /* Don't do this stuff if this is not an NNTP session! */
1126         if (CC->h_command_function != nntp_command_loop) return;
1127
1128         syslog(LOG_DEBUG, "Performing NNTP cleanup hook\n");
1129         citnntp *nntpstate = (citnntp *) CC->session_specific_data;
1130         if (nntpstate != NULL) {
1131                 free(nntpstate);
1132                 nntpstate = NULL;
1133         }
1134 }
1135
1136 const char *CitadelServiceNNTP="NNTP";
1137 const char *CitadelServiceNNTPS="NNTPS";
1138
1139 CTDL_MODULE_INIT(nntp)
1140 {
1141         if (!threading)
1142         {
1143                 CtdlRegisterServiceHook(CtdlGetConfigInt("c_nntp_port"),
1144                                         NULL,
1145                                         nntp_greeting,
1146                                         nntp_command_loop,
1147                                         NULL, 
1148                                         CitadelServiceNNTP);
1149
1150 #ifdef HAVE_OPENSSL
1151                 CtdlRegisterServiceHook(CtdlGetConfigInt("c_nntps_port"),
1152                                         NULL,
1153                                         nntps_greeting,
1154                                         nntp_command_loop,
1155                                         NULL,
1156                                         CitadelServiceNNTPS);
1157 #endif
1158
1159                 CtdlRegisterSessionHook(nntp_cleanup_function, EVT_STOP, PRIO_STOP + 250);
1160         }
1161         
1162         /* return our module name for the log */
1163         return "nntp";
1164 }