Skip to content

Commit

Permalink
net_smtp: Add ETRN command support.
Browse files Browse the repository at this point in the history
Add support for ETRN, as specified in RFC 1985.
  • Loading branch information
InterLinked1 committed Jan 27, 2024
1 parent ee0fbee commit 22140cf
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 15 deletions.
16 changes: 16 additions & 0 deletions include/net_smtp.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
92 changes: 81 additions & 11 deletions modules/mod_smtp_delivery_external.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand All @@ -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 */
Expand Down Expand Up @@ -1869,13 +1937,15 @@ 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 */
}

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);
Expand Down
117 changes: 113 additions & 4 deletions nets/net_smtp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]>
*/
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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")) {
Expand Down

0 comments on commit 22140cf

Please sign in to comment.