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