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