6823260cdfa0856451863ddcf4718cc97266603a
[citadel.git] / citadel / modules / listsub / serv_listsub.c
1 /*
2  * $Id$
3  *
4  * This module handles self-service subscription/unsubscription to mail lists.
5  *
6  * Copyright (c) 2002-2009 by the citadel.org team
7  *
8  *  This program is free software; you can redistribute it and/or modify
9  *  it under the terms of the GNU General Public License as published by
10  *  the Free Software Foundation; either version 3 of the License, or
11  *  (at your option) any later version.
12  *
13  *  This program is distributed in the hope that it will be useful,
14  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
15  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  *  GNU General Public License for more details.
17  *
18  *  You should have received a copy of the GNU General Public License
19  *  along with this program; if not, write to the Free Software
20  *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
21  *
22  */
23
24 #include "sysdep.h"
25 #include <stdlib.h>
26 #include <unistd.h>
27 #include <stdio.h>
28 #include <fcntl.h>
29 #include <ctype.h>
30 #include <signal.h>
31 #include <pwd.h>
32 #include <errno.h>
33 #include <sys/types.h>
34 #include <dirent.h>
35 #if TIME_WITH_SYS_TIME
36 # include <sys/time.h>
37 # include <time.h>
38 #else
39 # if HAVE_SYS_TIME_H
40 #  include <sys/time.h>
41 # else
42 #  include <time.h>
43 # endif
44 #endif
45
46 #include <sys/wait.h>
47 #include <string.h>
48 #include <limits.h>
49 #include <libcitadel.h>
50 #include "citadel.h"
51 #include "server.h"
52 #include "citserver.h"
53 #include "support.h"
54 #include "config.h"
55 #include "user_ops.h"
56 #include "policy.h"
57 #include "database.h"
58 #include "msgbase.h"
59 #include "internet_addressing.h"
60 #include "clientsocket.h"
61 #include "file_ops.h"
62
63 #ifndef HAVE_SNPRINTF
64 #include "snprintf.h"
65 #endif
66
67
68
69 #include "ctdl_module.h"
70
71
72 /*
73  * Generate a randomizationalisticized token to use for authentication of
74  * a subscribe or unsubscribe request.
75  */
76 void listsub_generate_token(char *buf) {
77         char sourcebuf[SIZ];
78         static int seq = 0;
79
80         /* Theo, please sit down and shut up.  This key doesn't have to be
81          * tinfoil-hat secure, it just needs to be reasonably unguessable
82          * and unique.
83          */
84         sprintf(sourcebuf, "%lx",
85                 (long) (++seq + getpid() + time(NULL))
86         );
87
88         /* Convert it to base64 so it looks cool */     
89         CtdlEncodeBase64(buf, sourcebuf, strlen(sourcebuf), 0);
90 }
91
92
93 /*
94  * Enter a subscription request
95  */
96 void do_subscribe(char *room, char *email, char *subtype, char *webpage) {
97         struct ctdlroom qrbuf;
98         FILE *ncfp;
99         char filename[256];
100         char token[256];
101         char confirmation_request[2048];
102         char buf[512];
103         char urlroom[ROOMNAMELEN];
104         char scancmd[64];
105         char scanemail[256];
106         int found_sub = 0;
107
108         if (CtdlGetRoom(&qrbuf, room) != 0) {
109                 cprintf("%d There is no list called '%s'\n", ERROR + ROOM_NOT_FOUND, room);
110                 return;
111         }
112
113         if ((qrbuf.QRflags2 & QR2_SELFLIST) == 0) {
114                 cprintf("%d '%s' "
115                         "does not accept subscribe/unsubscribe requests.\n",
116                         ERROR + HIGHER_ACCESS_REQUIRED, qrbuf.QRname);
117                 return;
118         }
119
120         listsub_generate_token(token);
121
122         assoc_file_name(filename, sizeof filename, &qrbuf, ctdl_netcfg_dir);
123
124         /* 
125          * Make sure the requested address isn't already subscribed
126          */
127         begin_critical_section(S_NETCONFIGS);
128         ncfp = fopen(filename, "r");
129         if (ncfp != NULL) {
130                 while (fgets(buf, sizeof buf, ncfp) != NULL) {
131                         buf[strlen(buf)-1] = 0;
132                         extract_token(scancmd, buf, 0, '|', sizeof scancmd);
133                         extract_token(scanemail, buf, 1, '|', sizeof scanemail);
134                         if ((!strcasecmp(scancmd, "listrecp"))
135                            || (!strcasecmp(scancmd, "digestrecp"))) {
136                                 if (!strcasecmp(scanemail, email)) {
137                                         ++found_sub;
138                                 }
139                         }
140                 }
141                 fclose(ncfp);
142         }
143         end_critical_section(S_NETCONFIGS);
144
145         if (found_sub != 0) {
146                 cprintf("%d '%s' is already subscribed to '%s'.\n",
147                         ERROR + ALREADY_EXISTS,
148                         email, qrbuf.QRname);
149                 return;
150         }
151
152         /*
153          * Now add it to the file
154          */     
155         begin_critical_section(S_NETCONFIGS);
156         ncfp = fopen(filename, "a");
157         if (ncfp != NULL) {
158                 fprintf(ncfp, "subpending|%s|%s|%s|%ld|%s\n",
159                         email,
160                         subtype,
161                         token,
162                         time(NULL),
163                         webpage
164                 );
165                 fclose(ncfp);
166         }
167         end_critical_section(S_NETCONFIGS);
168
169         /* Generate and send the confirmation request */
170
171         urlesc(urlroom, ROOMNAMELEN, qrbuf.QRname);
172
173         snprintf(confirmation_request, sizeof confirmation_request,
174
175                 "MIME-Version: 1.0\n"
176                 "Content-Type: multipart/alternative; boundary=\"__ctdlmultipart__\"\n"
177                 "\n"
178                 "This is a multipart message in MIME format.\n"
179                 "\n"
180                 "--__ctdlmultipart__\n"
181                 "Content-type: text/plain\n"
182                 "\n"
183                 "Someone (probably you) has submitted a request to subscribe\n"
184                 "<%s> to the '%s' mailing list.\n"
185                 "\n"
186                 "Please go here to confirm this request:\n"
187                 "  %s?room=%s&token=%s&cmd=confirm  \n"
188                 "\n"
189                 "If this request has been submitted in error and you do not\n"
190                 "wish to receive the '%s' mailing list, simply do nothing,\n"
191                 "and you will not receive any further mailings.\n"
192                 "\n"
193                 "--__ctdlmultipart__\n"
194                 "Content-type: text/html\n"
195                 "\n"
196                 "<HTML><BODY>\n"
197                 "Someone (probably you) has submitted a request to subscribe\n"
198                 "&lt;%s&gt; to the <B>%s</B> mailing list.<BR><BR>\n"
199                 "Please click here to confirm this request:<BR>\n"
200                 "<A HREF=\"%s?room=%s&token=%s&cmd=confirm\">"
201                 "%s?room=%s&token=%s&cmd=confirm</A><BR><BR>\n"
202                 "If this request has been submitted in error and you do not\n"
203                 "wish to receive the '%s' mailing list, simply do nothing,\n"
204                 "and you will not receive any further mailings.\n"
205                 "</BODY></HTML>\n"
206                 "\n"
207                 "--__ctdlmultipart__--\n",
208
209                 email, qrbuf.QRname,
210                 webpage, urlroom, token,
211                 qrbuf.QRname,
212
213                 email, qrbuf.QRname,
214                 webpage, urlroom, token,
215                 webpage, urlroom, token,
216                 qrbuf.QRname
217         );
218
219         quickie_message(        /* This delivers the message */
220                 "Citadel",
221                 NULL,
222                 email,
223                 NULL,
224                 confirmation_request,
225                 FMT_RFC822,
226                 "Please confirm your list subscription"
227         );
228
229         cprintf("%d Subscription entered; confirmation request sent\n", CIT_OK);
230 }
231
232
233 /*
234  * Enter an unsubscription request
235  */
236 void do_unsubscribe(char *room, char *email, char *webpage) {
237         struct ctdlroom qrbuf;
238         FILE *ncfp;
239         char filename[256];
240         char token[256];
241         char buf[512];
242         char confirmation_request[2048];
243         char urlroom[ROOMNAMELEN];
244         char scancmd[256];
245         char scanemail[256];
246         int found_sub = 0;
247
248         if (CtdlGetRoom(&qrbuf, room) != 0) {
249                 cprintf("%d There is no list called '%s'\n",
250                         ERROR + ROOM_NOT_FOUND, room);
251                 return;
252         }
253
254         if ((qrbuf.QRflags2 & QR2_SELFLIST) == 0) {
255                 cprintf("%d '%s' "
256                         "does not accept subscribe/unsubscribe requests.\n",
257                         ERROR + HIGHER_ACCESS_REQUIRED, qrbuf.QRname);
258                 return;
259         }
260
261         listsub_generate_token(token);
262
263         assoc_file_name(filename, sizeof filename, &qrbuf, ctdl_netcfg_dir);
264
265         /* 
266          * Make sure there's actually a subscription there to remove
267          */
268         begin_critical_section(S_NETCONFIGS);
269         ncfp = fopen(filename, "r");
270         if (ncfp != NULL) {
271                 while (fgets(buf, sizeof buf, ncfp) != NULL) {
272                         buf[strlen(buf)-1] = 0;
273                         extract_token(scancmd, buf, 0, '|', sizeof scancmd);
274                         extract_token(scanemail, buf, 1, '|', sizeof scanemail);
275                         if ((!strcasecmp(scancmd, "listrecp"))
276                            || (!strcasecmp(scancmd, "digestrecp"))) {
277                                 if (!strcasecmp(scanemail, email)) {
278                                         ++found_sub;
279                                 }
280                         }
281                 }
282                 fclose(ncfp);
283         }
284         end_critical_section(S_NETCONFIGS);
285
286         if (found_sub == 0) {
287                 cprintf("%d '%s' is not subscribed to '%s'.\n",
288                         ERROR + NO_SUCH_USER,
289                         email, qrbuf.QRname);
290                 return;
291         }
292         
293         /* 
294          * Ok, now enter the unsubscribe-pending entry.
295          */
296         begin_critical_section(S_NETCONFIGS);
297         ncfp = fopen(filename, "a");
298         if (ncfp != NULL) {
299                 fprintf(ncfp, "unsubpending|%s|%s|%ld|%s\n",
300                         email,
301                         token,
302                         time(NULL),
303                         webpage
304                 );
305                 fclose(ncfp);
306         }
307         end_critical_section(S_NETCONFIGS);
308
309         /* Generate and send the confirmation request */
310
311         urlesc(urlroom, ROOMNAMELEN, qrbuf.QRname);
312
313         snprintf(confirmation_request, sizeof confirmation_request,
314
315                 "MIME-Version: 1.0\n"
316                 "Content-Type: multipart/alternative; boundary=\"__ctdlmultipart__\"\n"
317                 "\n"
318                 "This is a multipart message in MIME format.\n"
319                 "\n"
320                 "--__ctdlmultipart__\n"
321                 "Content-type: text/plain\n"
322                 "\n"
323                 "Someone (probably you) has submitted a request to unsubscribe\n"
324                 "<%s> from the '%s' mailing list.\n"
325                 "\n"
326                 "Please go here to confirm this request:\n"
327                 "  %s?room=%s&token=%s&cmd=confirm  \n"
328                 "\n"
329                 "If this request has been submitted in error and you do not\n"
330                 "wish to unsubscribe from the '%s' mailing list, simply do nothing,\n"
331                 "and the request will not be processed.\n"
332                 "\n"
333                 "--__ctdlmultipart__\n"
334                 "Content-type: text/html\n"
335                 "\n"
336                 "<HTML><BODY>\n"
337                 "Someone (probably you) has submitted a request to unsubscribe\n"
338                 "&lt;%s&gt; from the <B>%s</B> mailing list.<BR><BR>\n"
339                 "Please click here to confirm this request:<BR>\n"
340                 "<A HREF=\"%s?room=%s&token=%s&cmd=confirm\">"
341                 "%s?room=%s&token=%s&cmd=confirm</A><BR><BR>\n"
342                 "If this request has been submitted in error and you do not\n"
343                 "wish to unsubscribe from the '%s' mailing list, simply do nothing,\n"
344                 "and the request will not be processed.\n"
345                 "</BODY></HTML>\n"
346                 "\n"
347                 "--__ctdlmultipart__--\n",
348
349                 email, qrbuf.QRname,
350                 webpage, urlroom, token,
351                 qrbuf.QRname,
352
353                 email, qrbuf.QRname,
354                 webpage, urlroom, token,
355                 webpage, urlroom, token,
356                 qrbuf.QRname
357         );
358
359         quickie_message(        /* This delivers the message */
360                 "Citadel",
361                 NULL,
362                 email,
363                 NULL,
364                 confirmation_request,
365                 FMT_RFC822,
366                 "Please confirm your unsubscribe request"
367         );
368
369         cprintf("%d Unubscription noted; confirmation request sent\n", CIT_OK);
370 }
371
372
373 /*
374  * Confirm a subscribe/unsubscribe request.
375  */
376 void do_confirm(char *room, char *token) {
377         struct ctdlroom qrbuf;
378         FILE *ncfp;
379         char filename[256];
380         char line_token[256];
381         long line_offset;
382         int line_length;
383         char buf[512];
384         char cmd[256];
385         char email[256];
386         char subtype[128];
387         int success = 0;
388         char address_to_unsubscribe[256];
389         char scancmd[256];
390         char scanemail[256];
391         char *holdbuf = NULL;
392         int linelen = 0;
393         int buflen = 0;
394
395         strcpy(address_to_unsubscribe, "");
396
397         if (CtdlGetRoom(&qrbuf, room) != 0) {
398                 cprintf("%d There is no list called '%s'\n",
399                         ERROR + ROOM_NOT_FOUND, room);
400                 return;
401         }
402
403         if ((qrbuf.QRflags2 & QR2_SELFLIST) == 0) {
404                 cprintf("%d '%s' "
405                         "does not accept subscribe/unsubscribe requests.\n",
406                         ERROR + HIGHER_ACCESS_REQUIRED, qrbuf.QRname);
407                 return;
408         }
409
410         /*
411          * Now start scanning this room's netconfig file for the
412          * specified token.
413          */
414         assoc_file_name(filename, sizeof filename, &qrbuf, ctdl_netcfg_dir);
415         begin_critical_section(S_NETCONFIGS);
416         ncfp = fopen(filename, "r+");
417         if (ncfp != NULL) {
418                 while (line_offset = ftell(ncfp),
419                       (fgets(buf, sizeof buf, ncfp) != NULL) ) {
420                         buf[strlen(buf)-1] = 0;
421                         line_length = strlen(buf);
422                         extract_token(cmd, buf, 0, '|', sizeof cmd);
423                         if (!strcasecmp(cmd, "subpending")) {
424                                 extract_token(email, buf, 1, '|', sizeof email);
425                                 extract_token(subtype, buf, 2, '|', sizeof subtype);
426                                 extract_token(line_token, buf, 3, '|', sizeof line_token);
427                                 if (!strcasecmp(token, line_token)) {
428                                         if (!strcasecmp(subtype, "digest")) {
429                                                 safestrncpy(buf, "digestrecp|", sizeof buf);
430                                         }
431                                         else {
432                                                 safestrncpy(buf, "listrecp|", sizeof buf);
433                                         }
434                                         strcat(buf, email);
435                                         strcat(buf, "|");
436                                         /* SLEAZY HACK: pad the line out so
437                                          * it's the same length as the line
438                                          * we're replacing.
439                                          */
440                                         while (strlen(buf) < line_length) {
441                                                 strcat(buf, " ");
442                                         }
443                                         fseek(ncfp, line_offset, SEEK_SET);
444                                         fprintf(ncfp, "%s\n", buf);
445                                         ++success;
446                                 }
447                         }
448                         if (!strcasecmp(cmd, "unsubpending")) {
449                                 extract_token(line_token, buf, 2, '|', sizeof line_token);
450                                 if (!strcasecmp(token, line_token)) {
451                                         extract_token(address_to_unsubscribe, buf, 1, '|',
452                                                 sizeof address_to_unsubscribe);
453                                 }
454                         }
455                 }
456                 fclose(ncfp);
457         }
458         end_critical_section(S_NETCONFIGS);
459
460         /*
461          * If "address_to_unsubscribe" contains something, then we have to
462          * make another pass at the file, stripping out lines referring to
463          * that address.
464          */
465         if (!IsEmptyStr(address_to_unsubscribe)) {
466                 holdbuf = malloc(SIZ);
467                 begin_critical_section(S_NETCONFIGS);
468                 ncfp = fopen(filename, "r+");
469                 if (ncfp != NULL) {
470                         while (line_offset = ftell(ncfp),
471                               (fgets(buf, sizeof buf, ncfp) != NULL) ) {
472                                 buf[strlen(buf)-1]=0;
473                                 extract_token(scancmd, buf, 0, '|', sizeof scancmd);
474                                 extract_token(scanemail, buf, 1, '|', sizeof scanemail);
475                                 if ( (!strcasecmp(scancmd, "listrecp"))
476                                    && (!strcasecmp(scanemail,
477                                                 address_to_unsubscribe)) ) {
478                                         ++success;
479                                 }
480                                 else if ( (!strcasecmp(scancmd, "digestrecp"))
481                                    && (!strcasecmp(scanemail,
482                                                 address_to_unsubscribe)) ) {
483                                         ++success;
484                                 }
485                                 else if ( (!strcasecmp(scancmd, "subpending"))
486                                    && (!strcasecmp(scanemail,
487                                                 address_to_unsubscribe)) ) {
488                                         ++success;
489                                 }
490                                 else if ( (!strcasecmp(scancmd, "unsubpending"))
491                                    && (!strcasecmp(scanemail,
492                                                 address_to_unsubscribe)) ) {
493                                         ++success;
494                                 }
495                                 else {  /* Not relevant, so *keep* it! */
496                                         linelen = strlen(buf);
497                                         holdbuf = realloc(holdbuf,
498                                                 (buflen + linelen + 2) );
499                                         strcpy(&holdbuf[buflen], buf);
500                                         buflen += linelen;
501                                         strcpy(&holdbuf[buflen], "\n");
502                                         buflen += 1;
503                                 }
504                         }
505                         fclose(ncfp);
506                 }
507                 ncfp = fopen(filename, "w");
508                 if (ncfp != NULL) {
509                         fwrite(holdbuf, buflen+1, 1, ncfp);
510                         fclose(ncfp);
511                 }
512                 end_critical_section(S_NETCONFIGS);
513                 free(holdbuf);
514         }
515
516         /*
517          * Did we do anything useful today?
518          */
519         if (success) {
520                 cprintf("%d %d operation(s) confirmed.\n", CIT_OK, success);
521                 CtdlLogPrintf(CTDL_NOTICE, 
522                         "Mailing list: %s %ssubscribed to %s with token %s\n", 
523                         email, 
524                         (!IsEmptyStr(address_to_unsubscribe)) ? "un" : "", 
525                         room, 
526                         token);
527         }
528         else {
529                 cprintf("%d Invalid token.\n", ERROR + ILLEGAL_VALUE);
530         }
531
532 }
533
534
535
536 /* 
537  * process subscribe/unsubscribe requests and confirmations
538  */
539 void cmd_subs(char *cmdbuf) {
540
541         char opr[256];
542         char room[ROOMNAMELEN];
543         char email[256];
544         char subtype[256];
545         char token[256];
546         char webpage[256];
547
548         extract_token(opr, cmdbuf, 0, '|', sizeof opr);
549         if (!strcasecmp(opr, "subscribe")) {
550                 extract_token(subtype, cmdbuf, 3, '|', sizeof subtype);
551                 if ( (strcasecmp(subtype, "list"))
552                    && (strcasecmp(subtype, "digest")) ) {
553                         cprintf("%d Invalid subscription type '%s'\n",
554                                 ERROR + ILLEGAL_VALUE, subtype);
555                 }
556                 else {
557                         extract_token(room, cmdbuf, 1, '|', sizeof room);
558                         extract_token(email, cmdbuf, 2, '|', sizeof email);
559                         extract_token(webpage, cmdbuf, 4, '|', sizeof webpage);
560                         do_subscribe(room, email, subtype, webpage);
561                 }
562         }
563         else if (!strcasecmp(opr, "unsubscribe")) {
564                 extract_token(room, cmdbuf, 1, '|', sizeof room);
565                 extract_token(email, cmdbuf, 2, '|', sizeof email);
566                 extract_token(webpage, cmdbuf, 3, '|', sizeof webpage);
567                 do_unsubscribe(room, email, webpage);
568         }
569         else if (!strcasecmp(opr, "confirm")) {
570                 extract_token(room, cmdbuf, 1, '|', sizeof room);
571                 extract_token(token, cmdbuf, 2, '|', sizeof token);
572                 do_confirm(room, token);
573         }
574         else {
575                 cprintf("%d Invalid command\n", ERROR + ILLEGAL_VALUE);
576         }
577 }
578
579
580 /*
581  * Module entry point
582  */
583 CTDL_MODULE_INIT(listsub)
584 {
585         if (!threading)
586         {
587                 CtdlRegisterProtoHook(cmd_subs, "SUBS", "List subscribe/unsubscribe");
588         }
589         
590         /* return our Subversion id for the Log */
591         return "$Id$";
592 }