1 // This module handles self-service subscription/unsubscription to mail lists.
3 // Copyright (c) 2002-2022 by the citadel.org team
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
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.
15 #include "../../sysdep.h"
24 #include <sys/types.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"
44 enum { // one of these gets passed to do_subscribe_or_unsubscribe() so it knows what we asked for
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];
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)));
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) {
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);
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);
73 // Write to the buffer
74 snprintf(target_buf, SIZ, "%s?cmd=%s&email=%s&room=%s&token=%s",
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) {
87 char confirm_subscribe_url[SIZ];
88 generate_one_click_url(confirm_subscribe_url, url, "confirm_subscribe", roomname, emailaddr);
90 char from_address[1024];
91 snprintf(from_address, sizeof from_address, "noreply@%s", CtdlGetConfigStr("c_fqdn"));
94 snprintf(emailtext, sizeof emailtext,
96 "Content-Type: multipart/alternative; boundary=\"__ctdlmultipart__\"\n"
98 "This is a multipart message in MIME format.\n"
100 "--__ctdlmultipart__\n"
101 "Content-type: text/plain\n"
103 "Someone (probably you) has submitted a request to subscribe\n"
104 "<%s> to the <%s> mailing list.\n"
106 "Please go here to confirm this request:\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"
113 "--__ctdlmultipart__\n"
114 "Content-type: text/html\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"
125 "--__ctdlmultipart__--\n"
127 emailaddr, roomname, confirm_subscribe_url, roomname,
128 emailaddr, roomname, confirm_subscribe_url, confirm_subscribe_url, roomname
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);
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) {
139 char confirm_unsubscribe_url[SIZ];
140 generate_one_click_url(confirm_unsubscribe_url, url, "confirm_unsubscribe", roomname, emailaddr);
142 char from_address[1024];
143 snprintf(from_address, sizeof from_address, "noreply@%s", CtdlGetConfigStr("c_fqdn"));
146 snprintf(emailtext, sizeof emailtext,
147 "MIME-Version: 1.0\n"
148 "Content-Type: multipart/alternative; boundary=\"__ctdlmultipart__\"\n"
150 "This is a multipart message in MIME format.\n"
152 "--__ctdlmultipart__\n"
153 "Content-type: text/plain\n"
155 "Someone (probably you) has submitted a request to unsubscribe\n"
156 "<%s> from the <%s> mailing list.\n"
158 "Please go here to confirm this request:\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"
165 "--__ctdlmultipart__\n"
166 "Content-type: text/html\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"
177 "--__ctdlmultipart__--\n"
179 emailaddr, roomname, confirm_unsubscribe_url, roomname,
180 emailaddr, roomname, confirm_unsubscribe_url, confirm_unsubscribe_url, roomname
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);
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) {
192 int config_lines = 0;
193 char *oldnetconfig, *newnetconfig;
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);
202 // If the generated token matches the supplied token, the request is authentic. Do what it says.
204 // Load the room's network configuration...
205 oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
207 oldnetconfig = strdup("");
210 // The new netconfig begins with an empty buffer...
211 begin_critical_section(S_NETCONFIGS);
212 newnetconfig = malloc(strlen(oldnetconfig) + 1024);
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) {
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);
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);
233 // write it back to disk
234 SaveRoomNetConfigFile(CC->room.QRnumber, newnetconfig);
235 end_critical_section(S_NETCONFIGS);
238 cprintf("%d The pending request was confirmed.\n", CIT_OK);
242 // process subscribe/unsubscribe requests and confirmations
243 void cmd_lsub(char *cmdbuf) {
245 char roomname[ROOMNAMELEN];
246 char emailaddr[1024];
248 char generated_token[128];
249 char supplied_token[128];
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
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);
263 if ((CC->room.QRflags2 & QR2_SELFLIST) == 0) {
264 cprintf("%d '%s' does not accept subscribe/unsubscribe requests.\n", ERROR + ROOM_NOT_FOUND, roomname);
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);
271 // Now parse the command.
272 if (!strcasecmp(cmd, "subscribe")) {
273 send_subscribe_confirmation_email(roomname, emailaddr, url, generated_token);
276 else if (!strcasecmp(cmd, "unsubscribe")) {
277 send_unsubscribe_confirmation_email(roomname, emailaddr, url, generated_token);
280 else if (!strcasecmp(cmd, "confirm_subscribe")) {
281 do_confirm(SUBSCRIBE, roomname, emailaddr, url, generated_token, supplied_token);
284 else if (!strcasecmp(cmd, "confirm_unsubscribe")) {
285 do_confirm(UNSUBSCRIBE, roomname, emailaddr, url, generated_token, supplied_token);
288 else { // sorry man, I can't deal with that
289 cprintf("%d Invalid command '%s'\n", ERROR + ILLEGAL_VALUE, cmd);
294 // Initialization function, called from modules_init.c
295 char *ctdl_module_init_listsub(void) {
297 CtdlRegisterProtoHook(cmd_lsub, "LSUB", "List subscribe/unsubscribe");
300 /* return our module name for the log */