Integrated the DKIM signer into serv_smtpclient, but disabled it
[citadel.git] / citadel / server / modules / listsub / serv_listsub.c
1 // This module handles self-service subscription/unsubscription to mail lists.
2 //
3 // Copyright (c) 2002-2022 by the citadel.org team
4 //
5 // This program is open source software.  It runs great on the
6 // Linux operating system (and probably elsewhere).  You can use,
7 // copy, and run it under the terms of the GNU General Public
8 // License version 3.
9 //
10 // This program is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14
15 #include "../../sysdep.h"
16 #include <stdlib.h>
17 #include <unistd.h>
18 #include <stdio.h>
19 #include <fcntl.h>
20 #include <ctype.h>
21 #include <signal.h>
22 #include <pwd.h>
23 #include <errno.h>
24 #include <sys/types.h>
25 #include <dirent.h>
26 #include <time.h>
27 #include <sys/wait.h>
28 #include <string.h>
29 #include <limits.h>
30 #include <libcitadel.h>
31 #include "../../citadel_defs.h"
32 #include "../../server.h"
33 #include "../../citserver.h"
34 #include "../../support.h"
35 #include "../../config.h"
36 #include "../../user_ops.h"
37 #include "../../database.h"
38 #include "../../msgbase.h"
39 #include "../../internet_addressing.h"
40 #include "../../clientsocket.h"
41 #include "../../ctdl_module.h"
42
43
44 enum {                          // one of these gets passed to do_subscribe_or_unsubscribe() so it knows what we asked for
45         UNSUBSCRIBE,
46         SUBSCRIBE
47 };
48
49
50 // The confirmation token will be generated by combining the room name and email address with the host key,
51 // and then generating a one-way hash of that string.  The hash is included as part of the confirmation link.
52 void generate_confirmation_token(char *token_buf, size_t token_buf_len, char *roomname, char *emailaddr) {
53         char string_to_hash[1024];
54
55         snprintf(string_to_hash, sizeof string_to_hash, "%s|%s|%s", roomname, emailaddr, CtdlGetConfigStr("host_key"));
56         snprintf(token_buf, token_buf_len, "%lx",  FourHash(string_to_hash, strlen(string_to_hash)));
57 }
58
59
60 // Generate a pre-authorized subscribe/unsubscribe URL for a particular email address for a particular room.
61 // This can be used as the second part of a double-opt-in or double-opt-out process.
62 // It can also be used to generate a "one click unsubscribe" link.
63 void generate_one_click_url(char *target_buf, char *base_url, char *action, char *roomname, char *emailaddr) {
64
65         // We need a URL-safe representation of the room name
66         char encoded_roomname[ROOMNAMELEN+10];
67         urlesc(encoded_roomname, sizeof(encoded_roomname), roomname);
68
69         // The confirmation token pre-authorizes the generated URL.  It is hashed by the host key so it can't be guessed.
70         char confirmation_token[128];
71         generate_confirmation_token(confirmation_token, sizeof confirmation_token, roomname, emailaddr);
72
73         // Write to the buffer
74         snprintf(target_buf, SIZ, "%s?cmd=%s&email=%s&room=%s&token=%s",
75                 base_url,
76                 action,
77                 emailaddr,
78                 encoded_roomname,
79                 confirmation_token
80         );
81 }
82
83
84 // This generates an email with a link the user clicks to confirm a list subscription.
85 void send_subscribe_confirmation_email(char *roomname, char *emailaddr, char *url, char *confirmation_token) {
86
87         char confirm_subscribe_url[SIZ];
88         generate_one_click_url(confirm_subscribe_url, url, "confirm_subscribe", roomname, emailaddr);
89
90         char from_address[1024];
91         snprintf(from_address, sizeof from_address, "noreply@%s", CtdlGetConfigStr("c_fqdn"));
92
93         char emailtext[SIZ];
94         snprintf(emailtext, sizeof emailtext,
95                 "MIME-Version: 1.0\n"
96                 "Content-Type: multipart/alternative; boundary=\"__ctdlmultipart__\"\n"
97                 "\n"
98                 "This is a multipart message in MIME format.\n"
99                 "\n"
100                 "--__ctdlmultipart__\n"
101                 "Content-type: text/plain\n"
102                 "\n"
103                 "Someone (probably you) has submitted a request to subscribe\n"
104                 "<%s> to the <%s> mailing list.\n"
105                 "\n"
106                 "Please go here to confirm this request:\n"
107                 "%s\n"
108                 "\n"
109                 "If this request has been submitted in error and you do not\n"
110                 "wish to receive the <%s> mailing list, simply do nothing,\n"
111                 "and you will not receive any further mailings.\n"
112                 "\n"
113                 "--__ctdlmultipart__\n"
114                 "Content-type: text/html\n"
115                 "\n"
116                 "<html><body><p>Someone (probably you) has submitted a request to subscribe\n"
117                 "<strong>%s</strong> to the <strong>%s</strong> mailing list.</p>\n"
118                 "<p>Please go here to confirm this request:</p>\n"
119                 "<p><a href=\"%s\">%s</a></p>\n"
120                 "<p>If this request has been submitted in error and you do not\n"
121                 "wish to receive the <strong>%s</strong> mailing list, simply do nothing,\n"
122                 "and you will not receive any further mailings.</p>\n"
123                 "</body></html>\n"
124                 "\n"
125                 "--__ctdlmultipart__--\n"
126                 ,
127                 emailaddr, roomname, confirm_subscribe_url, roomname,
128                 emailaddr, roomname, confirm_subscribe_url, confirm_subscribe_url, roomname
129         );
130
131         quickie_message("Citadel", from_address, emailaddr, NULL, emailtext, FMT_RFC822, "Please confirm your list subscription");
132         cprintf("%d confirmation email sent\n", CIT_OK);
133 }
134
135
136 // This generates an email with a link the user clicks to confirm a list unsubscription.
137 void send_unsubscribe_confirmation_email(char *roomname, char *emailaddr, char *url, char *confirmation_token) {
138
139         char confirm_unsubscribe_url[SIZ];
140         generate_one_click_url(confirm_unsubscribe_url, url, "confirm_unsubscribe", roomname, emailaddr);
141
142         char from_address[1024];
143         snprintf(from_address, sizeof from_address, "noreply@%s", CtdlGetConfigStr("c_fqdn"));
144
145         char emailtext[SIZ];
146         snprintf(emailtext, sizeof emailtext,
147                 "MIME-Version: 1.0\n"
148                 "Content-Type: multipart/alternative; boundary=\"__ctdlmultipart__\"\n"
149                 "\n"
150                 "This is a multipart message in MIME format.\n"
151                 "\n"
152                 "--__ctdlmultipart__\n"
153                 "Content-type: text/plain\n"
154                 "\n"
155                 "Someone (probably you) has submitted a request to unsubscribe\n"
156                 "<%s> from the <%s> mailing list.\n"
157                 "\n"
158                 "Please go here to confirm this request:\n"
159                 "%s\n"
160                 "\n"
161                 "If this request has been submitted in error and you still\n"
162                 "wish to receive the <%s> mailing list, simply do nothing,\n"
163                 "and you will remain subscribed.\n"
164                 "\n"
165                 "--__ctdlmultipart__\n"
166                 "Content-type: text/html\n"
167                 "\n"
168                 "<html><body><p>Someone (probably you) has submitted a request to unsubscribe\n"
169                 "<strong>%s</strong> from the <strong>%s</strong> mailing list.</p>\n"
170                 "<p>Please go here to confirm this request:</p>\n"
171                 "<p><a href=\"%s\">%s</a></p>\n"
172                 "<p>If this request has been submitted in error and you still\n"
173                 "wish to receive the <strong>%s</strong> mailing list, simply do nothing,\n"
174                 "and you will remain subscribed.</p>\n"
175                 "</body></html>\n"
176                 "\n"
177                 "--__ctdlmultipart__--\n"
178                 ,
179                 emailaddr, roomname, confirm_unsubscribe_url, roomname,
180                 emailaddr, roomname, confirm_unsubscribe_url, confirm_unsubscribe_url, roomname
181         );
182
183         quickie_message("Citadel", from_address, emailaddr, NULL, emailtext, FMT_RFC822, "Please confirm your list unsubscription");
184         cprintf("%d confirmation email sent\n", CIT_OK);
185 }
186
187
188 // Confirm a list subscription or unsubscription
189 void do_confirm(int cmd, char *roomname, char *emailaddr, char *url, char *generated_token, char *supplied_token) {
190         int i;
191         char buf[1024];
192         int config_lines = 0;
193         char *oldnetconfig, *newnetconfig;
194
195         // During opt #1, the server generated a persistent confirmation token for the user+room combination.
196         // Let's see if the user has supplied the same token during opt #2.
197         if (strcmp(generated_token, supplied_token)) {
198                 cprintf("%d This request could not be authenticated.\n", ERROR + PASSWORD_REQUIRED);
199                 return;
200         }
201
202         // If the generated token matches the supplied token, the request is authentic.  Do what it says.
203
204         // Load the room's network configuration...
205         oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
206         if (!oldnetconfig) {
207                 oldnetconfig = strdup("");
208         }
209
210         // The new netconfig begins with an empty buffer...
211         begin_critical_section(S_NETCONFIGS);
212         newnetconfig = malloc(strlen(oldnetconfig) + 1024);
213         newnetconfig[0] = 0;
214
215         // Load the config lines in one by one, skipping any that reference this subscriber.  Also remove blank lines.
216         config_lines = num_tokens(oldnetconfig, '\n');
217         for (i=0; i<config_lines; ++i) {
218                 char buf_email[256];
219                 extract_token(buf, oldnetconfig, i, '\n', sizeof buf);
220                 extract_token(buf_email, buf, 1, '|', sizeof buf_email);
221                 if ( !IsEmptyStr(buf) && (strcasecmp(buf_email, emailaddr)) ) {
222                         sprintf(&newnetconfig[strlen(newnetconfig)], "%s\n", buf);
223                 }
224         }
225
226         // We have now removed all lines containing the subscriber's email address.  This deletes any pending requests.
227         // If this was an unsubscribe operation, they're now gone from the list.
228         // But if this was a subscribe operation, we now need to add them.
229         if (cmd == SUBSCRIBE) {
230                 sprintf(&newnetconfig[strlen(newnetconfig)], "listrecp|%s\n", emailaddr);
231         }
232
233         // write it back to disk
234         SaveRoomNetConfigFile(CC->room.QRnumber, newnetconfig);
235         end_critical_section(S_NETCONFIGS);
236         free(oldnetconfig);
237         free(newnetconfig);
238         cprintf("%d The pending request was confirmed.\n", CIT_OK);
239 }
240
241
242 // process subscribe/unsubscribe requests and confirmations
243 void cmd_lsub(char *cmdbuf) {
244         char cmd[20];
245         char roomname[ROOMNAMELEN];
246         char emailaddr[1024];
247         char url[1024];
248         char generated_token[128];
249         char supplied_token[128];
250
251         extract_token(cmd, cmdbuf, 0, '|', sizeof cmd);                         // token 0 is the sub-command being sent
252         extract_token(roomname, cmdbuf, 1, '|', sizeof roomname);               // token 1 is always a room name
253         extract_token(emailaddr, cmdbuf, 2, '|', sizeof emailaddr);             // token 2 is the subscriber's email address
254         extract_token(url, cmdbuf, 3, '|', sizeof url);                         // token 3 is the URL at which we subscribed
255         extract_token(supplied_token, cmdbuf, 4, '|', sizeof supplied_token);   // token 4 is the token supplied by the caller
256
257         // First confirm that the caller is referencing a room that actually exists.
258         if (CtdlGetRoom(&CC->room, roomname) != 0) {
259                 cprintf("%d There is no list called '%s'\n", ERROR + ROOM_NOT_FOUND, roomname);
260                 return;
261         }
262
263         if ((CC->room.QRflags2 & QR2_SELFLIST) == 0) {
264                 cprintf("%d '%s' does not accept subscribe/unsubscribe requests.\n", ERROR + ROOM_NOT_FOUND, roomname);
265                 return;
266         }
267
268         // Generate a confirmation token -- either to supply to the user for opt #1 or to compare for opt #2
269         generate_confirmation_token(generated_token, sizeof generated_token, roomname, emailaddr);
270
271         // Now parse the command.
272         if (!strcasecmp(cmd, "subscribe")) {
273                 send_subscribe_confirmation_email(roomname, emailaddr, url, generated_token);
274         }
275
276         else if (!strcasecmp(cmd, "unsubscribe")) {
277                 send_unsubscribe_confirmation_email(roomname, emailaddr, url, generated_token);
278         }
279
280         else if (!strcasecmp(cmd, "confirm_subscribe")) {
281                 do_confirm(SUBSCRIBE, roomname, emailaddr, url, generated_token, supplied_token);
282         }
283
284         else if (!strcasecmp(cmd, "confirm_unsubscribe")) {
285                 do_confirm(UNSUBSCRIBE, roomname, emailaddr, url, generated_token, supplied_token);
286         }
287
288         else {                                                                  // sorry man, I can't deal with that
289                 cprintf("%d Invalid command '%s'\n", ERROR + ILLEGAL_VALUE, cmd);
290         }
291 }
292
293
294 // Initialization function, called from modules_init.c
295 char *ctdl_module_init_listsub(void) {
296         if (!threading) {
297                 CtdlRegisterProtoHook(cmd_lsub, "LSUB", "List subscribe/unsubscribe");
298         }
299         
300         /* return our module name for the log */
301         return "listsub";
302 }