-//
// This module handles self-service subscription/unsubscription to mail lists.
//
-// Copyright (c) 2002-2021 by the citadel.org team
+// Copyright (c) 2002-2022 by the citadel.org team
//
// This program is open source software. It runs great on the
// Linux operating system (and probably elsewhere). You can use,
// copy, and run it under the terms of the GNU General Public
-// License version 3. Richard Stallman is an asshole communist.
+// License version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
#include <sys/wait.h>
#include <string.h>
#include <limits.h>
+#include <crypt.h>
#include <libcitadel.h>
#include "citadel.h"
#include "server.h"
};
-/*
- * This generates an email with a link the user clicks to confirm a list subscription.
- */
+// The confirmation token will be generated by combining the room name and email address with the host key,
+// and then generating an encrypted hash of that string. The encrypted hash is included as part of the
+// confirmation link.
+void generate_confirmation_token(char *token_buf, size_t token_buf_len, char *roomname, char *emailaddr) {
+ char string_to_hash[1024];
+ struct crypt_data cd;
+ char *ptr;
+
+ snprintf(string_to_hash, sizeof string_to_hash, "%s|%s|%s", roomname, emailaddr, CtdlGetConfigStr("host_key"));
+ memset(&cd, 0, sizeof cd);
+
+ strncpy(token_buf, crypt_r(string_to_hash, "$1$ctdl", &cd), token_buf_len);
+
+ for (ptr=token_buf; *ptr; ++ptr) {
+ if (!isalnum((char)*ptr)) *ptr='X';
+ }
+}
+
+
+// This generates an email with a link the user clicks to confirm a list subscription.
void send_subscribe_confirmation_email(char *roomname, char *emailaddr, char *url, char *confirmation_token) {
// We need a URL-safe representation of the room name
char urlroom[ROOMNAMELEN+10];
"<%s> to the <%s> mailing list.\n"
"\n"
"Please go here to confirm this request:\n"
- "%s?room=%s&token=%s&cmd=confirm\n"
+ "%s?cmd=confirm_subscribe&email=%s&room=%s&token=%s\n"
"\n"
"If this request has been submitted in error and you do not\n"
"wish to receive the <%s> mailing list, simply do nothing,\n"
"<html><body><p>Someone (probably you) has submitted a request to subscribe "
"<strong>%s</strong> to the <strong>%s</strong> mailing list.</p>"
"<p>Please go here to confirm this request:</p>"
- "<p><a href=\"%s?room=%s&token=%s&cmd=confirm\">"
- "%s?room=%s&token=%s&cmd=confirm</a></p>"
+ "<p><a href=\"%s?cmd=confirm_subscribe&email=%s&room=%s&token=%s\">"
+ "%s?cmd=confirm_subscribe&email=%s&room=%s&token=%s</a></p>"
"<p>If this request has been submitted in error and you do not "
"wish to receive the <strong>%s<strong> mailing list, simply do nothing, "
"and you will not receive any further mailings.</p>"
"\n"
"--__ctdlmultipart__--\n"
,
- emailaddr, roomname, url, urlroom, confirmation_token, roomname,
emailaddr, roomname,
- url, urlroom, confirmation_token,
- url, urlroom, confirmation_token,
+ url, emailaddr, urlroom, confirmation_token,
+ roomname
+ ,
+ emailaddr, roomname,
+ url, emailaddr, urlroom, confirmation_token,
+ url, emailaddr, urlroom, confirmation_token,
roomname
);
quickie_message("Citadel", from_address, emailaddr, NULL, emailtext, FMT_RFC822, "Please confirm your list subscription");
+ cprintf("%d confirmation email sent\n", CIT_OK);
}
-/*
- * This generates an email with a link the user clicks to confirm a list unsubscription.
- */
+// This generates an email with a link the user clicks to confirm a list unsubscription.
void send_unsubscribe_confirmation_email(char *roomname, char *emailaddr, char *url, char *confirmation_token) {
// We need a URL-safe representation of the room name
char urlroom[ROOMNAMELEN+10];
"<%s> from the <%s> mailing list.\n"
"\n"
"Please go here to confirm this request:\n"
- "%s?room=%s&token=%s&cmd=confirm\n"
+ "%s?cmd=confirm_unsubscribe&email=%s&room=%s&token=%s\n"
"\n"
"If this request has been submitted in error and you still\n"
"wish to receive the <%s> mailing list, simply do nothing,\n"
"<html><body><p>Someone (probably you) has submitted a request to unsubscribe "
"<strong>%s</strong> from the <strong>%s</strong> mailing list.</p>"
"<p>Please go here to confirm this request:</p>"
- "<p><a href=\"%s?room=%s&token=%s&cmd=confirm\">"
- "%s?room=%s&token=%s&cmd=confirm</a></p>"
+ "<p><a href=\"%s?cmd=confirm_unsubscribe&email=%s&room=%s&token=%s\">"
+ "%s?cmd=confirm_unsubscribe&email=%s&room=%s&token=%s</a></p>"
"<p>If this request has been submitted in error and you still "
"wish to receive the <strong>%s<strong> mailing list, simply do nothing, "
"and you will remain subscribed.</p>"
"\n"
"--__ctdlmultipart__--\n"
,
- emailaddr, roomname, url, urlroom, confirmation_token, roomname,
emailaddr, roomname,
- url, urlroom, confirmation_token,
- url, urlroom, confirmation_token,
+ url, emailaddr, urlroom, confirmation_token,
+ roomname
+ ,
+ emailaddr, roomname,
+ url, emailaddr, urlroom, confirmation_token,
+ url, emailaddr, urlroom, confirmation_token,
roomname
);
- quickie_message("Citadel", from_address, emailaddr, NULL, emailtext, FMT_RFC822, "Please confirm your list subscription");
+ quickie_message("Citadel", from_address, emailaddr, NULL, emailtext, FMT_RFC822, "Please confirm your list unsubscription");
+ cprintf("%d confirmation email sent\n", CIT_OK);
}
-/*
- * "Subscribe" and "Unsubscribe" operations are so similar that they share a function.
- * The actual subscription doesn't take place here -- we just send out the confirmation request
- * and record the address and confirmation token.
- */
-void do_subscribe_or_unsubscribe(int action, char *emailaddr, char *url) {
-
+// Confirm a list subscription or unsubscription
+void do_confirm(int cmd, char *roomname, char *emailaddr, char *url, char *generated_token, char *supplied_token) {
int i;
char buf[1024];
- char confirmation_token[40];
+ int config_lines = 0;
+ char *oldnetconfig, *newnetconfig;
- // Update this room's netconfig with the updated lastsent
- begin_critical_section(S_NETCONFIGS);
- char *oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
+ // During opt #1, the server generated a persistent confirmation token for the user+room combination.
+ // Let's see if the user has supplied the same token during opt #2.
+ if (strcmp(generated_token, supplied_token)) {
+ cprintf("%d This request could not be authenticated.\n", ERROR + PASSWORD_REQUIRED);
+ return;
+ }
+
+ // If the generated token matches the supplied token, the request is authentic. Do what it says.
+
+ // Load the room's network configuration...
+ oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
if (!oldnetconfig) {
oldnetconfig = strdup("");
}
// The new netconfig begins with an empty buffer...
- char *newnetconfig = malloc(strlen(oldnetconfig) + 1024);
+ begin_critical_section(S_NETCONFIGS);
+ newnetconfig = malloc(strlen(oldnetconfig) + 1024);
newnetconfig[0] = 0;
- // And then we...
- int is_already_subscribed = 0;
- int config_lines = num_tokens(oldnetconfig, '\n');
+ // Load the config lines in one by one, skipping any that reference this subscriber. Also remove blank lines.
+ config_lines = num_tokens(oldnetconfig, '\n');
for (i=0; i<config_lines; ++i) {
+ char buf_email[256];
extract_token(buf, oldnetconfig, i, '\n', sizeof buf);
- int keep_this_line =1; // set to nonzero if we are discarding a line
-
- if (IsEmptyStr(buf)) {
- keep_this_line = 0;
- }
-
- char buf_token[1024];
- char buf_email[1024];
- extract_token(buf_token, buf, 0, '|', sizeof buf_token);
extract_token(buf_email, buf, 1, '|', sizeof buf_email);
-
- if ( ( (!strcasecmp(buf_token, "listrecp")) || (!strcasecmp(buf_token, "digestrecp")) )
- && (!strcasecmp(buf_email, emailaddr))
- ) {
- is_already_subscribed = 1;
- }
-
- if ( (!strcasecmp(buf_token, "subpending")) || (!strcasecmp(buf_token, "unsubpending")) ) {
- time_t pendingtime = extract_long(buf, 3);
- if ((time(NULL) - pendingtime) > 259200) {
- syslog(LOG_DEBUG, "%s %s is %ld seconds old - deleting it", buf_email, buf_token, time(NULL) - pendingtime);
- keep_this_line = 0;
- }
- }
-
- if (keep_this_line) {
+ if ( !IsEmptyStr(buf) && (strcasecmp(buf_email, emailaddr)) ) {
sprintf(&newnetconfig[strlen(newnetconfig)], "%s\n", buf);
}
}
- // Do we need to send out a confirmation email?
- if ((action == SUBSCRIBE) && (!is_already_subscribed)) {
- generate_uuid(confirmation_token);
- sprintf(&newnetconfig[strlen(newnetconfig)], "subpending|%s|%s|%ld|%s", emailaddr, confirmation_token, time(NULL), url);
- send_subscribe_confirmation_email(CC->room.QRname, emailaddr, url, confirmation_token);
- }
- if ((action == UNSUBSCRIBE) && (is_already_subscribed)) {
- generate_uuid(confirmation_token);
- sprintf(&newnetconfig[strlen(newnetconfig)], "unsubpending|%s|%s|%ld|%s", emailaddr, confirmation_token, time(NULL), url);
- send_unsubscribe_confirmation_email(CC->room.QRname, emailaddr, url, confirmation_token);
+ // We have now removed all lines containing the subscriber's email address. This deletes any pending requests.
+ // If this was an unsubscribe operation, they're now gone from the list.
+ // But if this was a subscribe operation, we now need to add them.
+ if (cmd == SUBSCRIBE) {
+ sprintf(&newnetconfig[strlen(newnetconfig)], "listrecp|%s\n", emailaddr);
}
- // Write the new netconfig back to disk
- syslog(LOG_DEBUG, "old: <\033[31m%s\033[0m>", oldnetconfig);
- syslog(LOG_DEBUG, "new: <\033[32m%s\033[0m>", newnetconfig);
+ // write it back to disk
SaveRoomNetConfigFile(CC->room.QRnumber, newnetconfig);
end_critical_section(S_NETCONFIGS);
- free(newnetconfig); // this was the new netconfig, free it because we're done with it
- free(oldnetconfig); // this was the old netconfig, free it even if we didn't do anything
-
- // Tell the client what happened.
- if ((action == SUBSCRIBE) && (is_already_subscribed)) {
- cprintf("%d This email is already subscribed.\n", ERROR + ALREADY_EXISTS);
- }
- else if ((action == SUBSCRIBE) && (!is_already_subscribed)) {
- cprintf("%d Confirmation email sent.\n", CIT_OK);
- }
- else if ((action == UNSUBSCRIBE) && (!is_already_subscribed)) {
- cprintf("%d This email is not subscribed.\n", ERROR + NO_SUCH_USER);
- }
- else if ((action == UNSUBSCRIBE) && (is_already_subscribed)) {
- cprintf("%d Confirmation email sent.\n", CIT_OK);
- }
- else {
- cprintf("%d FIXME tell the client what we did\n", ERROR);
- }
+ free(oldnetconfig);
+ free(newnetconfig);
+ cprintf("%d The pending request was confirmed.\n", CIT_OK);
}
-/*
- * process subscribe/unsubscribe requests and confirmations
- */
-void cmd_subs(char *cmdbuf) {
+// process subscribe/unsubscribe requests and confirmations
+void cmd_lsub(char *cmdbuf) {
char cmd[20];
char roomname[ROOMNAMELEN];
char emailaddr[1024];
- char options[256];
char url[1024];
- char token[128];
+ char generated_token[128];
+ char supplied_token[128];
extract_token(cmd, cmdbuf, 0, '|', sizeof cmd); // token 0 is the sub-command being sent
extract_token(roomname, cmdbuf, 1, '|', sizeof roomname); // token 1 is always a room name
+ extract_token(emailaddr, cmdbuf, 2, '|', sizeof emailaddr); // token 2 is the subscriber's email address
+ extract_token(url, cmdbuf, 3, '|', sizeof url); // token 3 is the URL at which we subscribed
+ extract_token(supplied_token, cmdbuf, 4, '|', sizeof supplied_token); // token 4 is the token supplied by the caller
// First confirm that the caller is referencing a room that actually exists.
if (CtdlGetRoom(&CC->room, roomname) != 0) {
return;
}
- // Room confirmed, now parse the command.
+ // Generate a confirmation token -- either to supply to the user for opt #1 or to compare for opt #2
+ generate_confirmation_token(generated_token, sizeof generated_token, roomname, emailaddr);
+ // Now parse the command.
if (!strcasecmp(cmd, "subscribe")) {
- extract_token(emailaddr, cmdbuf, 2, '|', sizeof emailaddr); // token 2 is the subscriber's email address
- extract_token(options, cmdbuf, 3, '|', sizeof options); // there are no options ... ignore this token
- extract_token(url, cmdbuf, 4, '|', sizeof url); // token 3 is the URL at which we subscribed
- do_subscribe_or_unsubscribe(SUBSCRIBE, emailaddr, url);
+ send_subscribe_confirmation_email(roomname, emailaddr, url, generated_token);
}
else if (!strcasecmp(cmd, "unsubscribe")) {
- extract_token(emailaddr, cmdbuf, 2, '|', sizeof emailaddr); // token 2 is the subscriber's email address
- extract_token(options, cmdbuf, 3, '|', sizeof options); // there are no options ... ignore this token
- extract_token(url, cmdbuf, 4, '|', sizeof url); // token 3 is the URL at which we subscribed
- do_subscribe_or_unsubscribe(UNSUBSCRIBE, emailaddr, url);
+ send_unsubscribe_confirmation_email(roomname, emailaddr, url, generated_token);
+ }
+
+ else if (!strcasecmp(cmd, "confirm_subscribe")) {
+ do_confirm(SUBSCRIBE, roomname, emailaddr, url, generated_token, supplied_token);
}
- else if (!strcasecmp(cmd, "confirm")) {
- extract_token(token, cmdbuf, 2, '|', sizeof token); // token 2 is the confirmation token
- cprintf("%d not implemented\n", ERROR);
+ else if (!strcasecmp(cmd, "confirm_unsubscribe")) {
+ do_confirm(UNSUBSCRIBE, roomname, emailaddr, url, generated_token, supplied_token);
}
else { // sorry man, I can't deal with that
{
if (!threading)
{
- CtdlRegisterProtoHook(cmd_subs, "SUBS", "List subscribe/unsubscribe");
+ CtdlRegisterProtoHook(cmd_lsub, "LSUB", "List subscribe/unsubscribe");
}
/* return our module name for the log */