]> code.citadel.org Git - citadel.git/blobdiff - citadel/server/modules/smtp/serv_smtpclient.c
minor tuning to previous commit
[citadel.git] / citadel / server / modules / smtp / serv_smtpclient.c
index a5e358a05e667ff392d92da214ccf2db21b6a48e..6eedc56d02bb37d3523a6cd1b282c71e077650d0 100644 (file)
@@ -2,7 +2,7 @@
 //
 // This is the new, exciting, clever version that makes libcurl do all the work  :)
 //
-// Copyright (c) 1997-2023 by the citadel.org team
+// Copyright (c) 1997-2024 by the citadel.org team
 //
 // This program is open source software.  Use, duplication, or disclosure
 // is subject to the terms of the GNU General Public License, version 3.
@@ -19,7 +19,7 @@
 #include <libcitadel.h>
 #include <curl/curl.h>
 #include "../../sysconfig.h"
-#include "../../citadel.h"
+#include "../../citadel_defs.h"
 #include "../../server.h"
 #include "../../citserver.h"
 #include "../../support.h"
@@ -32,6 +32,9 @@
 #include "../../citadel_dirs.h"
 #include "../smtp/smtp_util.h"
 
+long last_queue_job_submitted = 0;
+long last_queue_job_processed = 0;
+
 struct smtpmsgsrc {            // Data passed in and out of libcurl for message upload
        StrBuf *TheMessage;
        int bytes_total;
@@ -55,10 +58,10 @@ void smtp_init_spoolout(void) {
 }
 
 
-// For internet mail, generate delivery instructions.
+// For internet mail, generate a delivery job.
 // Yes, this is recursive.  Deal with it.  Infinite recursion does
-// not happen because the delivery instructions message does not
-// contain a recipient.
+// not happen because the message containing the delivery job does not
+// have a recipient.
 int smtp_aftersave(struct CtdlMessage *msg, struct recptypes *recps) {
        if ((recps != NULL) && (recps->num_internet > 0)) {
                struct CtdlMessage *imsg = NULL;
@@ -67,7 +70,7 @@ int smtp_aftersave(struct CtdlMessage *msg, struct recptypes *recps) {
                long nTokens;
                int i;
 
-               syslog(LOG_DEBUG, "smtpclient: generating delivery instructions");
+               syslog(LOG_DEBUG, "smtpclient: generating delivery job");
 
                StrBufPrintf(SpoolMsg,
                             "Content-type: " SPOOLMIME "\n"
@@ -102,11 +105,11 @@ int smtp_aftersave(struct CtdlMessage *msg, struct recptypes *recps) {
                imsg->cm_magic = CTDLMESSAGE_MAGIC;
                imsg->cm_anon_type = MES_NORMAL;
                imsg->cm_format_type = FMT_RFC822;
-               CM_SetField(imsg, eMsgSubject, HKEY("QMSG"));
-               CM_SetField(imsg, eAuthor, HKEY("Citadel"));
-               CM_SetField(imsg, eJournal, HKEY("do not journal"));
-               CM_SetAsFieldSB(imsg, eMesageText, &SpoolMsg);
-               CtdlSubmitMsg(imsg, NULL, SMTP_SPOOLOUT_ROOM);
+               CM_SetField(imsg, eMsgSubject, "QMSG");
+               CM_SetField(imsg, eAuthor, "Citadel");
+               CM_SetField(imsg, eJournal, "do not journal");
+               CM_SetAsFieldSB(imsg, eMessageText, &SpoolMsg);
+               last_queue_job_submitted = CtdlSubmitMsg(imsg, NULL, SMTP_SPOOLOUT_ROOM);
                CM_Free(imsg);
        }
        return 0;
@@ -164,12 +167,6 @@ void trim_response(long response_code, char *response) {
                return;
        }
 
-       char *t = malloc(strlen(response));
-       if (!t) {
-               return;
-       }
-       t[0] = 0;
-
        char *p;
        for (p = response; *p != 0; ++p) {
                if ( (*p != '\n') && (!isprint(*p)) ) {         // expunge any nonprintables except for newlines
@@ -216,29 +213,50 @@ int smtp_attempt_delivery(long msgid, char *recp, char *envelope_from, char *sou
        process_rfc822_addr(recp, user, node, name);    // split recipient address into username, hostname, displayname
        num_mx = getmx(mxes, node);
        if (num_mx < 1) {
-               return (421);
+               return(421);
        }
 
        CC->redirect_buffer = NewStrBufPlain(NULL, SIZ);
+
+       // If we have a source room, it's probably a mailing list message; generate an unsubscribe header
        if (!IsEmptyStr(source_room)) {
-               // If we have a source room, it's probably a mailing list message; generate an unsubscribe header
-               char esc_room[ROOMNAMELEN*2];
-               char esc_email[1024];
-               urlesc(esc_room, sizeof esc_room, source_room);
-               urlesc(esc_email, sizeof esc_email, recp);
-               cprintf("List-Unsubscribe: <http://%s/listsub?cmd=unsubscribe&room=%s&email=%s>\r\n",
-                       CtdlGetConfigStr("c_fqdn"),
-                       esc_room,
-                       esc_email
-               );
+               char base_url[SIZ];
+               char unsubscribe_url[SIZ];
+               snprintf(base_url, sizeof base_url, "https://%s/listsub", CtdlGetConfigStr("c_fqdn"));
+               generate_one_click_url(unsubscribe_url, base_url, "unsubscribe", source_room, recp);
+               cprintf("List-Unsubscribe: <%s>\r\n", unsubscribe_url);                 // RFC 2369
+               cprintf("List-Unsubscribe-Post: List-Unsubscribe=One-Click\r\n");       // RFC 8058
        }
+
        CtdlOutputMsg(msgid, MT_RFC822, HEADERS_ALL, 0, 1, NULL, 0, NULL, &fromaddr, NULL);
        s.TheMessage = CC->redirect_buffer;
-       s.bytes_total = StrLength(CC->redirect_buffer);
-       s.bytes_sent = 0;
        CC->redirect_buffer = NULL;
+
+       // If we have a DKIM key, try to sign the message.
+       char *dkim_private_key = CtdlGetConfigStr("dkim_private_key");
+       char *dkim_selector = CtdlGetConfigStr("dkim_selector");
+       char *dkim_from_domain = (strchr(fromaddr, '@') ? strchr(fromaddr, '@')+1 : NULL);
+       if (
+               !IsEmptyStr(dkim_from_domain)                   // Is the sending domain non-empty?
+               && IsDirectory(fromaddr, 0)                     // and is it one of "our" domains?
+               && !IsEmptyStr(dkim_private_key)                // Do we have a private signing key?
+               && !IsEmptyStr(dkim_selector)                   // and a selector to go with it?
+       ) {
+               // If you answered "yes" to all of the above questions, congratulations!  We get to sign the message!
+               syslog(LOG_DEBUG, "smtpclient: dkim-signing for selector <%s> in domain <%s>", dkim_selector, dkim_from_domain);
+
+               // Remember, the dkim_sign() function is capable of handling a PEM-encoded PKCS#7 private key that
+               // has had all of its newlines replaced by underscores -- which is exactly how we store it.
+               dkim_sign(s.TheMessage,dkim_private_key, dkim_from_domain, dkim_selector);
+       }
+
+       // Prepare the buffer for transmittal
+       s.bytes_total = StrLength(s.TheMessage);
+       s.bytes_sent = 0;
        response_code = 421;
-                                       // keep trying MXes until one works or we run out
+
+
+       // Keep trying MXes until one works or we run out.
        for (i = 0; ((i < num_mx) && ((response_code / 100) == 4)); ++i) {
                response_code = 421;    // default 421 makes non-protocol errors transient
                s.bytes_sent = 0;       // rewind our buffer in case we try multiple MXes
@@ -272,7 +290,7 @@ int smtp_attempt_delivery(long msgid, char *recp, char *envelope_from, char *sou
                        // Construct an SMTP URL in the form of:
                        //      smtp[s]://target_host/source_host
                        // This looks weird but libcurl uses that last part to set our name for EHLO or HELO.
-                       // We check for "smtp://" and "smtps://" because the admin may have put those prefixes in a smart-host entry.
+                       // We check for "smtp://" and "smtps://" because an admin may have put those prefixes in a smart-host entry
                        // If there is no prefix we add "smtp://"
                        extract_token(try_this_mx, mxes, i, '|', (sizeof try_this_mx - 7));
                        snprintf(smtp_url, sizeof smtp_url,
@@ -326,15 +344,15 @@ void smtp_process_one_msg(long qmsgnum) {
 
        msg = CtdlFetchMessage(qmsgnum, 1);
        if (msg == NULL) {
-               syslog(LOG_WARNING, "smtpclient: %ld does not exist", qmsgnum);
+               syslog(LOG_WARNING, "smtpclient: msg#%ld does not exist", qmsgnum);
                return;
        }
 
-       instr = msg->cm_fields[eMesageText];
-       msg->cm_fields[eMesageText] = NULL;
+       instr = msg->cm_fields[eMessageText];
+       msg->cm_fields[eMessageText] = NULL;
        CM_Free(msg);
 
-       // if the queue message has any CRLF's convert them to LF's
+       // if the queue job message has any CRLF's convert them to LF's
        char *crlf = NULL;
        while (crlf = strstr(instr, "\r\n"), crlf != NULL) {
                strcpy(crlf, crlf + 1);
@@ -356,18 +374,12 @@ void smtp_process_one_msg(long qmsgnum) {
        char cfgline[SIZ];
        for (i = 0; i < num_tokens(instr, '\n'); ++i) {
                extract_token(cfgline, instr, i, '\n', sizeof cfgline);
-               if (!strncasecmp(cfgline, HKEY("msgid|")))
-                       msgid = atol(&cfgline[6]);
-               if (!strncasecmp(cfgline, HKEY("submitted|")))
-                       submitted = atol(&cfgline[10]);
-               if (!strncasecmp(cfgline, HKEY("attempted|")))
-                       attempted = atol(&cfgline[10]);
-               if (!strncasecmp(cfgline, HKEY("bounceto|")))
-                       bounceto = strdup(&cfgline[9]);
-               if (!strncasecmp(cfgline, HKEY("envelope_from|")))
-                       envelope_from = strdup(&cfgline[14]);
-               if (!strncasecmp(cfgline, HKEY("source_room|")))
-                       source_room = strdup(&cfgline[12]);
+               if (!strncasecmp(cfgline, HKEY("msgid|")))              msgid = atol(&cfgline[6]);
+               if (!strncasecmp(cfgline, HKEY("submitted|")))          submitted = atol(&cfgline[10]);
+               if (!strncasecmp(cfgline, HKEY("attempted|")))          attempted = atol(&cfgline[10]);
+               if (!strncasecmp(cfgline, HKEY("bounceto|")))           bounceto = strdup(&cfgline[9]);
+               if (!strncasecmp(cfgline, HKEY("envelope_from|")))      envelope_from = strdup(&cfgline[14]);
+               if (!strncasecmp(cfgline, HKEY("source_room|")))        source_room = strdup(&cfgline[12]);
        }
 
        int should_try_now = 0;
@@ -472,7 +484,7 @@ void smtp_process_one_msg(long qmsgnum) {
                }
        }
        else {
-               syslog(LOG_DEBUG, "smtpclient: %ld retry time not reached", qmsgnum);
+               syslog(LOG_DEBUG, "smtpclient: msg#%ld retry time not reached", qmsgnum);
        }
 
        if (bounceto != NULL) {
@@ -495,10 +507,15 @@ void smtp_add_msg(long msgnum, void *userdata) {
 }
 
 
+enum {
+       FULL_QUEUE_RUN,         // try to process the entire queue, including messages that have already been attempted
+       QUICK_QUEUE_RUN         // only process jobs in the queue that have not been tried yet
+};
+
+
 // Run through the queue sending out messages.
-void smtp_do_queue(void) {
+void smtp_do_queue(int type_of_queue_run) {
        static int doing_smtpclient = 0;
-       static long last_queue_msg_processed = 0;
        int i = 0;
 
        // This is a concurrency check to make sure only one smtpclient run is done at a time.
@@ -510,7 +527,10 @@ void smtp_do_queue(void) {
        doing_smtpclient = 1;
        end_critical_section(S_SMTPQUEUE);
 
-       syslog(LOG_DEBUG, "smtpclient: start queue run - last_queue_msg_processed=%ld", last_queue_msg_processed);
+       syslog(LOG_DEBUG, "smtpclient: start %s queue run , last_queue_job_processed=%ld , last_queue_job_submitted=%ld",
+               (type_of_queue_run == QUICK_QUEUE_RUN ? "quick" : "full"),
+               last_queue_job_processed, last_queue_job_submitted
+       );
 
        if (CtdlGetRoom(&CC->room, SMTP_SPOOLOUT_ROOM) != 0) {
                syslog(LOG_WARNING, "smtpclient: cannot find room <%s>", SMTP_SPOOLOUT_ROOM);
@@ -518,7 +538,7 @@ void smtp_do_queue(void) {
                return;
        }
 
-       // This array will hold the list of queue instruction messages
+       // This array will hold the list of queue job messages
        Array *smtp_queue = array_new(sizeof(long));
        if (smtp_queue == NULL) {
                syslog(LOG_WARNING, "smtpclient: cannot allocate queue array");
@@ -527,20 +547,47 @@ void smtp_do_queue(void) {
        }
 
        // Put the queue in memory so we can close the db cursor
-       // Searching for messages with a top level Content-type of SPOOLIME will give us only queue instruction messages.
-       CtdlForEachMessage(MSGS_ALL, 0L, NULL, SPOOLMIME, NULL, smtp_add_msg, (void *)smtp_queue);
+       CtdlForEachMessage(
+               (type_of_queue_run == QUICK_QUEUE_RUN ? MSGS_GT : MSGS_ALL),            // quick = new jobs; full = all jobs
+               (type_of_queue_run == QUICK_QUEUE_RUN ? last_queue_job_processed : 0),  // quick = new jobs; full = all jobs
+               NULL,
+               SPOOLMIME,              // Searching for Content-type of SPOOLIME will give us only queue instruction messages
+               NULL,
+               smtp_add_msg,           // That's our callback function to add a job to the queue
+               (void *)smtp_queue
+       );
 
        // We are ready to run through the queue now.
+       syslog(LOG_DEBUG, "smtpclient: %d messages to be processed", array_len(smtp_queue));
        for (i = 0; i < array_len(smtp_queue); ++i) {
                long m;
                memcpy(&m, array_get_element_at(smtp_queue, i), sizeof(long));
                smtp_process_one_msg(m);
-               last_queue_msg_processed = m;
        }
 
        array_free(smtp_queue);
+       last_queue_job_processed = last_queue_job_submitted;
        doing_smtpclient = 0;
-       syslog(LOG_DEBUG, "smtpclient: end queue run - last_queue_msg_processed=%ld", last_queue_msg_processed);
+       syslog(LOG_DEBUG, "smtpclient: end %s queue run , last_queue_job_processed=%ld , last_queue_job_submitted=%ld",
+               (type_of_queue_run == QUICK_QUEUE_RUN ? "quick" : "full"),
+               last_queue_job_processed, last_queue_job_submitted
+       );
+}
+
+
+// The "full" queue run goes through the entire queue, attempting delivery for newly submitted messages,
+// retrying failed deliveries periodically, and handling undeliverable messages.
+void smtp_do_queue_full(void) {
+       smtp_do_queue(FULL_QUEUE_RUN);
+}
+
+
+// The "quick" queue run only handles newly submitted messages, allowing them to be delivered immediately
+// instead of waiting for the next "full" queue run.
+void smtp_do_queue_quick(void) {
+       if (last_queue_job_submitted > last_queue_job_processed) {
+               smtp_do_queue(QUICK_QUEUE_RUN);
+       }
 }
 
 
@@ -548,8 +595,10 @@ void smtp_do_queue(void) {
 char *ctdl_module_init_smtpclient(void) {
        if (!threading) {
                CtdlRegisterMessageHook(smtp_aftersave, EVT_AFTERSAVE);
-               CtdlRegisterSessionHook(smtp_do_queue, EVT_TIMER, PRIO_AGGR + 51);
+               CtdlRegisterSessionHook(smtp_do_queue_quick, EVT_HOUSE, PRIO_AGGR + 51);
+               CtdlRegisterSessionHook(smtp_do_queue_full, EVT_TIMER, PRIO_AGGR + 51);
                smtp_init_spoolout();
+               dkim_init();
        }
 
        // return our module id for the log