2 // This module handles self-service subscription/unsubscription to mail lists.
4 // Copyright (c) 2002-2021 by the citadel.org team
6 // This program is open source software. It runs great on the
7 // Linux operating system (and probably elsewhere). You can use,
8 // copy, and run it under the terms of the GNU General Public
11 // This program is distributed in the hope that it will be useful,
12 // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 // GNU General Public License for more details.
25 #include <sys/types.h>
31 #include <libcitadel.h>
34 #include "citserver.h"
40 #include "internet_addressing.h"
41 #include "clientsocket.h"
42 #include "ctdl_module.h"
45 enum { // one of these gets passed to do_subscribe_or_unsubscribe() so it knows what we asked for
52 * This generates an email with a link the user clicks to confirm a list subscription.
54 void send_subscribe_confirmation_email(char *roomname, char *emailaddr, char *url, char *confirmation_token) {
55 // We need a URL-safe representation of the room name
56 char urlroom[ROOMNAMELEN+10];
57 urlesc(urlroom, sizeof(urlroom), roomname);
59 char from_address[1024];
60 snprintf(from_address, sizeof from_address, "noreply@%s", CtdlGetConfigStr("c_fqdn"));
63 snprintf(emailtext, sizeof emailtext,
65 "Content-Type: multipart/alternative; boundary=\"__ctdlmultipart__\"\n"
67 "This is a multipart message in MIME format.\n"
69 "--__ctdlmultipart__\n"
70 "Content-type: text/plain\n"
72 "Someone (probably you) has submitted a request to subscribe\n"
73 "<%s> to the <%s> mailing list.\n"
75 "Please go here to confirm this request:\n"
76 "%s?room=%s&token=%s&cmd=confirm\n"
78 "If this request has been submitted in error and you do not\n"
79 "wish to receive the <%s> mailing list, simply do nothing,\n"
80 "and you will not receive any further mailings.\n"
82 "--__ctdlmultipart__\n"
83 "Content-type: text/html\n"
85 "<html><body><p>Someone (probably you) has submitted a request to subscribe "
86 "<strong>%s</strong> to the <strong>%s</strong> mailing list.</p>"
87 "<p>Please go here to confirm this request:</p>"
88 "<p><a href=\"%s?room=%s&token=%s&cmd=confirm\">"
89 "%s?room=%s&token=%s&cmd=confirm</a></p>"
90 "<p>If this request has been submitted in error and you do not "
91 "wish to receive the <strong>%s<strong> mailing list, simply do nothing, "
92 "and you will not receive any further mailings.</p>"
95 "--__ctdlmultipart__--\n"
98 url, urlroom, confirmation_token,
102 url, urlroom, confirmation_token,
103 url, urlroom, confirmation_token,
107 quickie_message("Citadel", from_address, emailaddr, NULL, emailtext, FMT_RFC822, "Please confirm your list subscription");
112 * This generates an email with a link the user clicks to confirm a list unsubscription.
114 void send_unsubscribe_confirmation_email(char *roomname, char *emailaddr, char *url, char *confirmation_token) {
115 // We need a URL-safe representation of the room name
116 char urlroom[ROOMNAMELEN+10];
117 urlesc(urlroom, sizeof(urlroom), roomname);
119 char from_address[1024];
120 snprintf(from_address, sizeof from_address, "noreply@%s", CtdlGetConfigStr("c_fqdn"));
123 snprintf(emailtext, sizeof emailtext,
124 "MIME-Version: 1.0\n"
125 "Content-Type: multipart/alternative; boundary=\"__ctdlmultipart__\"\n"
127 "This is a multipart message in MIME format.\n"
129 "--__ctdlmultipart__\n"
130 "Content-type: text/plain\n"
132 "Someone (probably you) has submitted a request to unsubscribe\n"
133 "<%s> from the <%s> mailing list.\n"
135 "Please go here to confirm this request:\n"
136 "%s?room=%s&token=%s&cmd=confirm\n"
138 "If this request has been submitted in error and you still\n"
139 "wish to receive the <%s> mailing list, simply do nothing,\n"
140 "and you will remain subscribed.\n"
142 "--__ctdlmultipart__\n"
143 "Content-type: text/html\n"
145 "<html><body><p>Someone (probably you) has submitted a request to unsubscribe "
146 "<strong>%s</strong> from the <strong>%s</strong> mailing list.</p>"
147 "<p>Please go here to confirm this request:</p>"
148 "<p><a href=\"%s?room=%s&token=%s&cmd=confirm\">"
149 "%s?room=%s&token=%s&cmd=confirm</a></p>"
150 "<p>If this request has been submitted in error and you still "
151 "wish to receive the <strong>%s<strong> mailing list, simply do nothing, "
152 "and you will remain subscribed.</p>"
155 "--__ctdlmultipart__--\n"
158 url, urlroom, confirmation_token,
162 url, urlroom, confirmation_token,
163 url, urlroom, confirmation_token,
167 quickie_message("Citadel", from_address, emailaddr, NULL, emailtext, FMT_RFC822, "Please confirm your list unsubscription");
172 * "Subscribe" and "Unsubscribe" operations are so similar that they share a function.
173 * The actual subscription doesn't take place here -- we just send out the confirmation request
174 * and record the address and confirmation token.
176 void do_subscribe_or_unsubscribe(int action, char *emailaddr, char *url) {
180 char confirmation_token[40];
182 // Update this room's netconfig with the updated lastsent
183 begin_critical_section(S_NETCONFIGS);
184 char *oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
186 oldnetconfig = strdup("");
189 // The new netconfig begins with an empty buffer...
190 char *newnetconfig = malloc(strlen(oldnetconfig) + 1024);
194 int is_already_subscribed = 0;
195 int config_lines = num_tokens(oldnetconfig, '\n');
196 for (i=0; i<config_lines; ++i) {
197 extract_token(buf, oldnetconfig, i, '\n', sizeof buf);
198 int keep_this_line =1; // set to nonzero if we are discarding a line
200 if (IsEmptyStr(buf)) {
204 char buf_directive[1024];
205 char buf_email[1024];
206 extract_token(buf_directive, buf, 0, '|', sizeof buf_directive);
207 extract_token(buf_email, buf, 1, '|', sizeof buf_email);
209 if ( ( (!strcasecmp(buf_directive, "listrecp")) || (!strcasecmp(buf_directive, "digestrecp")) )
210 && (!strcasecmp(buf_email, emailaddr))
212 is_already_subscribed = 1;
215 if ( (!strcasecmp(buf_directive, "subpending")) || (!strcasecmp(buf_directive, "unsubpending")) ) {
216 time_t pendingtime = extract_long(buf, 3);
217 if ((time(NULL) - pendingtime) > 259200) {
218 syslog(LOG_DEBUG, "%s %s is %ld seconds old - deleting it", buf_email, buf_directive, time(NULL) - pendingtime);
223 if (keep_this_line) {
224 sprintf(&newnetconfig[strlen(newnetconfig)], "%s\n", buf);
228 // Do we need to send out a confirmation email?
229 if ((action == SUBSCRIBE) && (!is_already_subscribed)) {
230 generate_uuid(confirmation_token);
231 sprintf(&newnetconfig[strlen(newnetconfig)], "subpending|%s|%s|%ld|%s", emailaddr, confirmation_token, time(NULL), url);
232 send_subscribe_confirmation_email(CC->room.QRname, emailaddr, url, confirmation_token);
234 if ((action == UNSUBSCRIBE) && (is_already_subscribed)) {
235 generate_uuid(confirmation_token);
236 sprintf(&newnetconfig[strlen(newnetconfig)], "unsubpending|%s|%s|%ld|%s", emailaddr, confirmation_token, time(NULL), url);
237 send_unsubscribe_confirmation_email(CC->room.QRname, emailaddr, url, confirmation_token);
240 // Write the new netconfig back to disk
241 SaveRoomNetConfigFile(CC->room.QRnumber, newnetconfig);
242 end_critical_section(S_NETCONFIGS);
243 free(newnetconfig); // this was the new netconfig, free it because we're done with it
244 free(oldnetconfig); // this was the old netconfig, free it even if we didn't do anything
246 // Tell the client what happened.
247 if ((action == SUBSCRIBE) && (is_already_subscribed)) {
248 cprintf("%d This email address is already subscribed.\n", ERROR + ALREADY_EXISTS);
250 else if ((action == SUBSCRIBE) && (!is_already_subscribed)) {
251 cprintf("%d Subscription was requested, and a confirmation email was sent.\n", CIT_OK);
253 else if ((action == UNSUBSCRIBE) && (!is_already_subscribed)) {
254 cprintf("%d This email address is not subscribed.\n", ERROR + NO_SUCH_USER);
256 else if ((action == UNSUBSCRIBE) && (is_already_subscribed)) {
257 cprintf("%d Unsubscription was requested, and a confirmation email was sent.\n", CIT_OK);
260 cprintf("%d Nothing happens.\n", ERROR);
266 * Confirm a list subscription or unsubscription
268 void do_confirm(char *token) {
269 int yes_subscribe = 0; // Set to 1 if the confirmation to subscribe is validated.
270 int yes_unsubscribe = 0; // Set to 1 if the confirmation to unsubscribe is validated.
273 int config_lines = 0;
274 char pending_directive[128];
275 char pending_email[256];
276 char pending_token[128];
278 // We will have to do this in two passes. The first pass checks to see if we have a confirmation request matching the token.
279 char *oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
281 cprintf("%d There are no pending requests.\n", ERROR + NO_SUCH_USER);
285 config_lines = num_tokens(oldnetconfig, '\n');
286 for (i=0; i<config_lines; ++i) {
287 extract_token(buf, oldnetconfig, i, '\n', sizeof buf);
288 extract_token(pending_directive, buf, 0, '|', sizeof pending_directive);
289 extract_token(pending_email, buf, 1, '|', sizeof pending_email);
290 extract_token(pending_token, buf, 2, '|', sizeof pending_token);
292 if (!strcasecmp(pending_token, token)) {
293 if (!strcasecmp(pending_directive, "subpending")) {
296 else if (!strcasecmp(pending_directive, "unsubpending")) {
303 // We didn't find a pending subscribe or unsubscribe request with the supplied token.
304 if ((!yes_subscribe) && (!yes_unsubscribe)) {
305 cprintf("%d The request you are trying to confirm was not found.\n", ERROR + NO_SUCH_USER);
309 // The second pass performs the now confirmed operation.
310 // We will have to do this in two passes. The first pass checks to see if we have a confirmation request matching the token.
311 oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
313 oldnetconfig = strdup("");
316 // The new netconfig begins with an empty buffer...
317 begin_critical_section(S_NETCONFIGS);
318 char *newnetconfig = malloc(strlen(oldnetconfig) + 1024);
321 config_lines = num_tokens(oldnetconfig, '\n');
322 for (i=0; i<config_lines; ++i) {
324 extract_token(buf, oldnetconfig, i, '\n', sizeof buf);
325 extract_token(buf_email, buf, 1, '|', sizeof pending_email);
326 if (strcasecmp(buf_email, pending_email)) {
327 sprintf(&newnetconfig[strlen(newnetconfig)], "%s\n", buf); // only keep lines that do not reference this subscriber
331 // We have now removed all lines containing the subscriber's email address. This deletes any pending requests.
332 // If this was an unsubscribe operation, they're now gone from the list.
333 // But if this was a subscribe operation, we now need to add them.
335 sprintf(&newnetconfig[strlen(newnetconfig)], "listrecp|%s\n", pending_email);
338 // FIXME write it back to disk
339 SaveRoomNetConfigFile(CC->room.QRnumber, newnetconfig);
340 end_critical_section(S_NETCONFIGS);
343 cprintf("%d The pending request was confirmed.\n", CIT_OK);
348 * process subscribe/unsubscribe requests and confirmations
350 void cmd_lsub(char *cmdbuf) {
352 char roomname[ROOMNAMELEN];
353 char emailaddr[1024];
358 extract_token(cmd, cmdbuf, 0, '|', sizeof cmd); // token 0 is the sub-command being sent
359 extract_token(roomname, cmdbuf, 1, '|', sizeof roomname); // token 1 is always a room name
361 // First confirm that the caller is referencing a room that actually exists.
362 if (CtdlGetRoom(&CC->room, roomname) != 0) {
363 cprintf("%d There is no list called '%s'\n", ERROR + ROOM_NOT_FOUND, roomname);
367 if ((CC->room.QRflags2 & QR2_SELFLIST) == 0) {
368 cprintf("%d '%s' does not accept subscribe/unsubscribe requests.\n", ERROR + ROOM_NOT_FOUND, roomname);
372 // Room confirmed, now parse the command.
374 if (!strcasecmp(cmd, "subscribe")) {
375 extract_token(emailaddr, cmdbuf, 2, '|', sizeof emailaddr); // token 2 is the subscriber's email address
376 extract_token(url, cmdbuf, 3, '|', sizeof url); // token 3 is the URL at which we subscribed
377 do_subscribe_or_unsubscribe(SUBSCRIBE, emailaddr, url);
380 else if (!strcasecmp(cmd, "unsubscribe")) {
381 extract_token(emailaddr, cmdbuf, 2, '|', sizeof emailaddr); // token 2 is the subscriber's email address
382 extract_token(url, cmdbuf, 3, '|', sizeof url); // token 3 is the URL at which we subscribed
383 do_subscribe_or_unsubscribe(UNSUBSCRIBE, emailaddr, url);
386 else if (!strcasecmp(cmd, "confirm")) {
387 extract_token(token, cmdbuf, 2, '|', sizeof token); // token 2 is the confirmation token
391 else { // sorry man, I can't deal with that
392 cprintf("%d Invalid command '%s'\n", ERROR + ILLEGAL_VALUE, cmd);
400 CTDL_MODULE_INIT(listsub)
404 CtdlRegisterProtoHook(cmd_lsub, "LSUB", "List subscribe/unsubscribe");
407 /* return our module name for the log */