Big change to mailing list subscription/unsubscription!
[citadel.git] / citadel / modules / listsub / serv_listsub.c
index 073ddafaee01c7cd8b0e771968db1fd76df6a8e7..0c8335a50fd515faee319a58d72447297ef30a8c 100644 (file)
@@ -1,7 +1,6 @@
-//
 // 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,
@@ -28,6 +27,7 @@
 #include <sys/wait.h>
 #include <string.h>
 #include <limits.h>
+#include <crypt.h>
 #include <libcitadel.h>
 #include "citadel.h"
 #include "server.h"
@@ -48,9 +48,26 @@ enum {                               // one of these gets passed to do_subscribe_or_unsubscribe() so it kno
 };
 
 
-/*
- * 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];
@@ -73,7 +90,7 @@ void send_subscribe_confirmation_email(char *roomname, char *emailaddr, char *ur
                "<%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"
@@ -85,8 +102,8 @@ void send_subscribe_confirmation_email(char *roomname, char *emailaddr, char *ur
                "<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>"
@@ -95,22 +112,21 @@ void send_subscribe_confirmation_email(char *roomname, char *emailaddr, char *ur
                "--__ctdlmultipart__--\n"
                ,
                emailaddr, roomname,
-               url, urlroom, confirmation_token,
+               url, emailaddr, urlroom, confirmation_token,
                roomname
                ,
                emailaddr, roomname,
-               url, urlroom, confirmation_token,
-               url, urlroom, confirmation_token,
+               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];
@@ -133,7 +149,7 @@ void send_unsubscribe_confirmation_email(char *roomname, char *emailaddr, char *
                "<%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"
@@ -145,8 +161,8 @@ void send_unsubscribe_confirmation_email(char *roomname, char *emailaddr, char *
                "<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>"
@@ -155,159 +171,37 @@ void send_unsubscribe_confirmation_email(char *roomname, char *emailaddr, char *
                "--__ctdlmultipart__--\n"
                ,
                emailaddr, roomname,
-               url, urlroom, confirmation_token,
+               url, emailaddr, urlroom, confirmation_token,
                roomname
                ,
                emailaddr, roomname,
-               url, urlroom, confirmation_token,
-               url, urlroom, confirmation_token,
+               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 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) {
-
-       int i;
-       char buf[1024];
-       char confirmation_token[40];
-
-       // Update this room's netconfig with the updated lastsent
-       begin_critical_section(S_NETCONFIGS);
-        char *oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
-        if (!oldnetconfig) {
-               oldnetconfig = strdup("");
-       }
-
-       // The new netconfig begins with an empty buffer...
-       char *newnetconfig = malloc(strlen(oldnetconfig) + 1024);
-       newnetconfig[0] = 0;
-
-       // And then we...
-       int is_already_subscribed = 0;
-       int config_lines = num_tokens(oldnetconfig, '\n');
-       for (i=0; i<config_lines; ++i) {
-               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_directive[1024];
-               char buf_email[1024];
-               extract_token(buf_directive, buf, 0, '|', sizeof buf_directive);
-               extract_token(buf_email, buf, 1, '|', sizeof buf_email);
-
-               if (    ( (!strcasecmp(buf_directive, "listrecp")) || (!strcasecmp(buf_directive, "digestrecp")) )
-                       && (!strcasecmp(buf_email, emailaddr)) 
-               ) {
-                       is_already_subscribed = 1;
-               }
-
-               if ( (!strcasecmp(buf_directive, "subpending")) || (!strcasecmp(buf_directive, "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_directive, time(NULL) - pendingtime);
-                               keep_this_line = 0;
-                       }
-               }
-               
-               if (keep_this_line) {
-                       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);
-       }
-
-       // Write the new netconfig 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 address is already subscribed.\n", ERROR + ALREADY_EXISTS);
-       }
-       else if ((action == SUBSCRIBE) && (!is_already_subscribed)) {
-               cprintf("%d Subscription was requested, and a confirmation email was sent.\n", CIT_OK);
-       }
-       else if ((action == UNSUBSCRIBE) && (!is_already_subscribed)) {
-               cprintf("%d This email address is not subscribed.\n", ERROR + NO_SUCH_USER);
-       }
-       else if ((action == UNSUBSCRIBE) && (is_already_subscribed)) {
-               cprintf("%d Unsubscription was requested, and a confirmation email was sent.\n", CIT_OK);
-       }
-       else {
-               cprintf("%d Nothing happens.\n", ERROR);
-       }
-}
-
-
-/*
- * Confirm a list subscription or unsubscription
- */
-void do_confirm(char *token) {
-       int yes_subscribe = 0;                          // Set to 1 if the confirmation to subscribe is validated.
-       int yes_unsubscribe = 0;                        // Set to 1 if the confirmation to unsubscribe is validated.
+// 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];
        int config_lines = 0;
-       char pending_directive[128];
-       char pending_email[256];
-       char pending_token[128];
+       char *oldnetconfig, *newnetconfig;
 
-       // We will have to do this in two passes.  The first pass checks to see if we have a confirmation request matching the token.
-        char *oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
-        if (!oldnetconfig) {
-               cprintf("%d There are no pending requests.\n", ERROR + NO_SUCH_USER);
+       // The server has generated a persistent confirmation token for the user+room combination.
+       // Let's see if the user has supplied the same token.
+       if (strcmp(generated_token, supplied_token)) {
+               cprintf("%d This request could not be authenticated.\n", ERROR + PASSWORD_REQUIRED);
                return;
        }
 
-       config_lines = num_tokens(oldnetconfig, '\n');
-       for (i=0; i<config_lines; ++i) {
-               extract_token(buf, oldnetconfig, i, '\n', sizeof buf);
-               extract_token(pending_directive, buf, 0, '|', sizeof pending_directive);
-               extract_token(pending_email, buf, 1, '|', sizeof pending_email);
-               extract_token(pending_token, buf, 2, '|', sizeof pending_token);
-
-               if (!strcasecmp(pending_token, token)) {
-                       if (!strcasecmp(pending_directive, "subpending")) {
-                               yes_subscribe = 1;
-                       }
-                       else if (!strcasecmp(pending_directive, "unsubpending")) {
-                               yes_unsubscribe = 1;
-                       }
-               }
-       }
-       free(oldnetconfig);
-
-       // We didn't find a pending subscribe or unsubscribe request with the supplied token.
-       if ((!yes_subscribe) && (!yes_unsubscribe)) {
-               cprintf("%d The request you are trying to confirm was not found.\n", ERROR + NO_SUCH_USER);
-               return;
-       }
+       // If the generated token matches the supplied token, the request is authentic.  Do what it says.
 
-       // The second pass performs the now confirmed operation.
-       // We will have to do this in two passes.  The first pass checks to see if we have a confirmation request matching the token.
+       // Load the room's network configuration...
         oldnetconfig = LoadRoomNetConfigFile(CC->room.QRnumber);
         if (!oldnetconfig) {
                oldnetconfig = strdup("");
@@ -315,27 +209,28 @@ void do_confirm(char *token) {
 
        // The new netconfig begins with an empty buffer...
        begin_critical_section(S_NETCONFIGS);
-       char *newnetconfig = malloc(strlen(oldnetconfig) + 1024);
+       newnetconfig = malloc(strlen(oldnetconfig) + 1024);
        newnetconfig[0] = 0;
 
+       // 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);
-               extract_token(buf_email, buf, 1, '|', sizeof pending_email);
-               if (strcasecmp(buf_email, pending_email)) {
-                       sprintf(&newnetconfig[strlen(newnetconfig)], "%s\n", buf);      // only keep lines that do not reference this subscriber
+               extract_token(buf_email, buf, 1, '|', sizeof buf_email);
+               if ( !IsEmptyStr(buf) && (strcasecmp(buf_email, emailaddr)) ) {
+                       sprintf(&newnetconfig[strlen(newnetconfig)], "%s\n", buf);
                }
        }
 
        // 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 (yes_subscribe) {
-               sprintf(&newnetconfig[strlen(newnetconfig)], "listrecp|%s\n", pending_email);
+       if (cmd == SUBSCRIBE) {
+               sprintf(&newnetconfig[strlen(newnetconfig)], "listrecp|%s\n", emailaddr);
        }
 
-       // FIXME write it back to disk
+       // write it back to disk
        SaveRoomNetConfigFile(CC->room.QRnumber, newnetconfig);
        end_critical_section(S_NETCONFIGS);
        free(oldnetconfig);
@@ -344,18 +239,24 @@ void do_confirm(char *token) {
 }
 
 
-/* 
- * process subscribe/unsubscribe requests and confirmations
- */
+// process subscribe/unsubscribe requests and confirmations
 void cmd_lsub(char *cmdbuf) {
        char cmd[20];
        char roomname[ROOMNAMELEN];
        char emailaddr[1024];
        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
+
+       syslog(LOG_DEBUG, "cmd_lsub(cmd=%s, roomname=%s, emailaddr=%s, url=%s, token=%s",
+               cmd, roomname, emailaddr, url, supplied_token
+       );
 
        // First confirm that the caller is referencing a room that actually exists.
        if (CtdlGetRoom(&CC->room, roomname) != 0) {
@@ -369,22 +270,22 @@ void cmd_lsub(char *cmdbuf) {
        }
 
        // Room confirmed, now parse the command.
+       generate_confirmation_token(generated_token, sizeof generated_token, roomname, emailaddr);
 
        if (!strcasecmp(cmd, "subscribe")) {
-               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
-               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(url, cmdbuf, 3, '|', 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
-               do_confirm(token);
+       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