diff --git a/include/net_smtp.h b/include/net_smtp.h index 859737c..dbb88e2 100644 --- a/include/net_smtp.h +++ b/include/net_smtp.h @@ -233,6 +233,22 @@ struct smtp_response { r->subcode = #sub; \ r->reply = msg; +int __smtp_register_queue_processor(int (*queue_processor)(struct smtp_session *smtp, const char *cmd, const char *args), void *mod); + +/*! + * \brief Register queue processor + * \param queue_processor Queue processor callback, which returns an SMTP numeric response code + * \retval 0 on success, -1 on failure + */ +#define smtp_register_queue_processor(queue_processor) __smtp_register_queue_processor(queue_processor, BBS_MODULE_SELF) + +/*! + * \brief Unregister queue processor + * \param queue_processor Queue processor callback + * \retval 0 on success, -1 on failure + */ +int smtp_unregister_queue_processor(int (*queue_processor)(struct smtp_session *smtp, const char *cmd, const char *args)); + struct smtp_delivery_agent { /*! \brief RCPT TO handler: can we deliver to this address? */ /*! \retval 0 if this recipient cannot be handled by this delivery agent, 1 if yes, -1 if no and no other handler may handle it */ diff --git a/modules/mod_smtp_delivery_external.c b/modules/mod_smtp_delivery_external.c index 47614d3..7a4d0c8 100644 --- a/modules/mod_smtp_delivery_external.c +++ b/modules/mod_smtp_delivery_external.c @@ -797,7 +797,8 @@ struct mailq_run { time_t runstart; /* Time that queue run started */ struct bbs_parallel *parallel; /* Parallel task set, if running in parallel. NULL if serial. */ /* Queue run filters to control what messages are processed */ - const char *hostsuffix; /* Domain or suffix of domain, e.g. com, example.com, sub.example.com, etc. Queued processing will be restricted to matches. */ + const char *host_match; /* Domain restriction for queue processing */ + const char *host_ends_with; /* Domain or suffix of domain, e.g. com, example.com, sub.example.com, etc. Queued processing will be restricted to matches. */ /* Queue run statistics */ /* processed is a subset of total, delivered + failed + delayed should = processed */ int total; /* Total number of queued messages considered */ @@ -1106,16 +1107,24 @@ static inline time_t queue_retry_threshold(int retrycount) static inline int skip_qfile(struct mailq_run *qrun, struct mailq_file *mqf) { /* This queue run may have filters applied to it */ - if (!strlen_zero(qrun->hostsuffix) && !strlen_zero(mqf->domain) && !bbs_str_ends_with(mqf->domain, qrun->hostsuffix)) { - /* Domain must end in hostsuffix, to match. */ + + /* Yeah, if we have a filter, we're possibly going to open + * all the files in the queue, only to almost immediately close most of them. + * One of our assumptions is the queue isn't going to be super large. + * If it were, it would very much be worth using a single queue "control file" + * with metadata about all the queue files, to avoid unnecessary file I/O. */ + + if (!strlen_zero(qrun->host_match) && !strlen_zero(mqf->domain) && strcmp(mqf->domain, qrun->host_match)) { + /* Exact match required */ #ifdef DEBUG_QUEUES - bbs_debug(8, "Skipping queue file %s (domain '%s' does not match filter '*%s')\n", mqf->fullname, mqf->domain, qrun->hostsuffix); + bbs_debug(8, "Skipping queue file %s (domain '%s' does not match filter '%s')\n", mqf->fullname, mqf->domain, qrun->host_match); +#endif + return 1; + } else if (!strlen_zero(qrun->host_ends_with) && !strlen_zero(mqf->domain) && !bbs_str_ends_with(mqf->domain, qrun->host_ends_with)) { + /* Domain must end in host_ends_with, to match. */ +#ifdef DEBUG_QUEUES + bbs_debug(8, "Skipping queue file %s (domain '%s' does not match filter '*%s')\n", mqf->fullname, mqf->domain, qrun->host_ends_with); #endif - /* Yeah, if we have a filter, we're possibly going to open - * all the files in the queue, only to almost immediately close most of them. - * One of our assumptions is the queue isn't going to be super large. - * If it were, it would very much be worth using a single queue "control file" - * with metadata about all the queue files, to avoid unnecessary file I/O. */ return 1; } @@ -1403,6 +1412,65 @@ static void *queue_handler(void *unused) return NULL; } +/*! \brief Queue processor callback */ +static int queue_processor(struct smtp_session *smtp, const char *cmd, const char *args) +{ + int res = 250; /* Start with 250 OK by default */ + struct mailq_run qrun; + + UNUSED(smtp); /* For now, this is unused, but could be useful in the future */ + + mailq_run_init(&qrun, QUEUE_RUN_FORCED); + + /* The RFCs suggest that queue processing should be done asynchronously + * when requested. Currently, we do it synchronously, since it's simpler. + * If we change it to async, we will need to copy the args and either + * spawn a new thread or make the regular queue_handler thread do this for us. */ + + if (!strcmp(cmd, "ETRN")) { + /* RFC 1985 Remote Message Queue Starting */ + if (*args == '@') { + /* RFC 1985 5.3: subdomain option character */ + args++; + if (strlen_zero(args)) { + res = 501; + } + qrun.host_ends_with = args; + } else if (*args == '#') { + /* RFC 1985 5.3: non-domain queue */ + /* Currently, we don't support any such queues, so reject */ + res = 458; + goto cleanup; + } else { + qrun.host_match = args; + } + run_queue(&qrun, on_queue_file); + /* One benefit of running the queue synchronously + * if that we now have more specific information about what happened. + * (Could also be done if we waited for all tasks to be created and returned + * before running them.) */ + if (qrun.processed) { + res = 252; + /* The 253 response code allows us to say how many messages + * are pending, but the current callback interface doesn't + * give us a way to provide that back to net_smtp, + * since we can't use smtp_reply directly here. + * So we'll only be able say some number pending, rather than the specific number, + * even though we've got that information right here! */ + } else { + res = 251; + } + } else { + bbs_error("SMTP command '%s' is foreign to queue processor\n", cmd); + res = 500; + goto cleanup; + } + +cleanup: + mailq_run_cleanup(&qrun); + return res; +} + static int on_queue_file_cli_mailq(const char *dir_name, const char *filename, void *obj) { struct mailq_run *qrun = obj; @@ -1474,7 +1542,7 @@ static int cli_mailq(struct bbs_cli_args *a) mailq_run_init(&qrun, QUEUE_RUN_STAT); qrun.clifd = a->fdout; if (a->argc >= 2) { - qrun.hostsuffix = a->argv[1]; + qrun.host_ends_with = a->argv[1]; } bbs_dprintf(a->fdout, "%7s %-25s %-25s %-25s %-20s %5s %-35s %s\n", "Retries", "Orig Date", "Last Retry", "Est. Next Retry", "Filename", "Size", "Sender", "Recipient"); @@ -1490,7 +1558,7 @@ static int cli_runq(struct bbs_cli_args *a) mailq_run_init(&qrun, QUEUE_RUN_FORCED); if (a->argc >= 2) { - qrun.hostsuffix = a->argv[1]; + qrun.host_ends_with = a->argv[1]; } /* Process the queue, now, synchronously */ @@ -1869,6 +1937,7 @@ static int load_module(void) return -1; } bbs_cli_register_multiple(cli_commands_mailq); + smtp_register_queue_processor(queue_processor); return smtp_register_delivery_handler(&extdeliver, 90); /* Lowest priority */ } @@ -1876,6 +1945,7 @@ static int unload_module(void) { int res; res = smtp_unregister_delivery_agent(&extdeliver); + smtp_unregister_queue_processor(queue_processor); bbs_cli_unregister_multiple(cli_commands_mailq); bbs_pthread_cancel_kill(queue_thread); bbs_pthread_join(queue_thread, NULL); diff --git a/nets/net_smtp.c b/nets/net_smtp.c index 2a2c688..4fed615 100644 --- a/nets/net_smtp.c +++ b/nets/net_smtp.c @@ -16,22 +16,23 @@ * * \note Supports RFC1870 Message Size Declarations * \note Supports RFC1893 Enhanced Status Codes + * \note Supports RFC1985 ETRN (Remote Message Queue Starting) * \note Supports RFC3207 STARTTLS * \note Supports RFC4954 AUTH * \note Supports RFC4468 BURL * \note Supports RFC6152 8BITMIME * \note Supports RFC6409 Message Submission * - * \todo Not currently supported: + * \todo Not currently supported, but would be nice to support eventually: + * - RFC 2645 ATRN (Authenticated TURN) * - RFC 2852 DELIVERBY * - RFC 3030 CHUNKING, BDAT, BINARYMIME * - RFC 3461 DSN (the format is mostly implemented, but needs fuller integration) * - RFC 3798 Message Disposition Notification * - RFC 6531 SMTPUTF8 * - * \note Not currently supported: - * - VRFY, ETRN (somewhat intentionally) - could be useful when authenticated, though - * - RFC 2645 ATRN (Authenticated TURN), RFC 1985 ETRN + * \note Not currently supported, and no current plans to support: + * - VRFY, EXPN (somewhat intentionally) - could be useful when authenticated, though * * \author Naveen Albert */ @@ -737,6 +738,42 @@ int smtp_unregister_delivery_agent(struct smtp_delivery_agent *agent) return h ? 0 : -1; } +/*! \todo FIXME Any time we have a single callback (not a list like delivery agents, with its own R/W list lock), + * we need a rwlock for the callback. + * Most such callbacks in the BBS don't have this. + * Might be worth creating an abstraction (e.g. bbs_module_callback) to simplify this. */ +static pthread_rwlock_t smtp_queue_processor_lock; +static int (*smtp_queue_processor)(struct smtp_session *smtp, const char *cmd, const char *args) = NULL; +static void *smtp_queue_processor_mod = NULL; + +int __smtp_register_queue_processor(int (*queue_processor)(struct smtp_session *smtp, const char *cmd, const char *args), void *mod) +{ + pthread_rwlock_wrlock(&smtp_queue_processor_lock); + if (smtp_queue_processor) { + pthread_rwlock_unlock(&smtp_queue_processor_lock); + bbs_error("SMTP On Demand Mail Relay already registered\n"); + return -1; + } + smtp_queue_processor = queue_processor; + smtp_queue_processor_mod = mod; + pthread_rwlock_unlock(&smtp_queue_processor_lock); + return 0; +} + +int smtp_unregister_queue_processor(int (*queue_processor)(struct smtp_session *smtp, const char *cmd, const char *args)) +{ + pthread_rwlock_wrlock(&smtp_queue_processor_lock); + if (smtp_queue_processor != queue_processor) { + pthread_rwlock_unlock(&smtp_queue_processor_lock); + bbs_error("SMTP On Demand Relay %p not registered\n", queue_processor); + return -1; + } + smtp_queue_processor_mod = NULL; + smtp_queue_processor = NULL; + pthread_rwlock_unlock(&smtp_queue_processor_lock); + return 0; +} + /*! \brief Parse parameter data in MAIL FROM */ static int parse_mail_parameters(struct smtp_session *smtp, char *s) { @@ -804,6 +841,72 @@ static int parse_mail_parameters(struct smtp_session *smtp, char *s) return 0; } +static int handle_etrn(struct smtp_session *smtp, char *s) +{ + int res; + char *args; + + if (smtp->from) { + /* RFC 1985 5: Illegal during transactions */ + smtp_reply(smtp, 502, 5.5.1, "Unsupported command"); + return 0; + } + + args = strsep(&s, " "); + if (strlen_zero(args)) { + smtp_reply(smtp, 501, 5.5.1, "Missing required parameter for ETRN"); + return 0; + } + + pthread_rwlock_rdlock(&smtp_queue_processor_lock); + if (!smtp_queue_processor) { + /* No queue processor registered, so ETRN not supported */ + pthread_rwlock_unlock(&smtp_queue_processor_lock); + smtp_reply(smtp, 501, 5.5.4, "Unrecognized parameter"); + return 0; + } + bbs_module_ref(smtp_queue_processor_mod, 7); + /* Technically, as soon as we ref the module, + * we are safe to unlock, since the module can no longer + * be unloaded until it's unreffed. However, it does + * no harm to hold a read lock longer, either. */ + res = smtp_queue_processor(smtp, "ETRN", args); + bbs_module_unref(smtp_queue_processor_mod, 7); + pthread_rwlock_unlock(&smtp_queue_processor_lock); + + switch (res) { + case 250: + smtp_reply_nostatus(smtp, 250, "OK, queue processed"); + break; + case 251: + smtp_reply_nostatus(smtp, 251, "OK, no messages waiting"); + break; + case 252: + smtp_reply_nostatus(smtp, 252, "OK, pending messages queued"); + break; + /* 253 allows us to be specific about how many messages, + * but mod_smtp_delivery_external doesn't pass that up to us, + * so this is unused. */ + case 458: + smtp_reply_nostatus(smtp, 458, "Unable to queue messages"); + break; + case 459: + smtp_reply_nostatus(smtp, 459, "Requested queuing not allowed"); + break; + case 500: + smtp_reply_nostatus(smtp, 500, "Syntax Error"); + break; + case 501: + smtp_reply_nostatus(smtp, 501, "Syntax Error in Parameters"); + break; + default: + bbs_error("Unexpected SMTP response code %d\n", res); + smtp_reply_nostatus(smtp, 500, "Syntax Error"); + } + + return 0; +} + /*! \brief Parse MAIL FROM */ static int handle_mail(struct smtp_session *smtp, char *s) { @@ -1208,6 +1311,9 @@ int smtp_filter_write(struct smtp_filter_data *f, const char *fmt, ...) int smtp_filter_add_header(struct smtp_filter_data *f, const char *name, const char *value) { + if (strchr(name, ':')) { + bbs_warning("Invalid header name: %s\n", name); + } return smtp_filter_write(f, "%s: %s\r\n", name, value); } @@ -2577,6 +2683,9 @@ static int smtp_process(struct smtp_session *smtp, char *s, struct readline_data smtp_reply(smtp, 504, 5.7.4, "Unrecognized Authentication Type"); } } + } else if (!strcasecmp(command, "ETRN")) { + REQUIRE_ARGS(s); + return handle_etrn(smtp,s); } else if (!strcasecmp(command, "MAIL")) { return handle_mail(smtp, s); } else if (!strcasecmp(command, "RCPT")) {