Skip to content

Commit

Permalink
mod_smtp_client: Move SMTP client to separate module.
Browse files Browse the repository at this point in the history
Refactor SMTP client code from mod_smtp_delivery_external
into its own module (upon which mod_smtp_delivery_external),
so that it can be used by multiple modules, rather than just
mod_smtp_delivery_external.

Also adjust logic for handling the ETRN command to respond
differently depending on the source IP address of the connection.
  • Loading branch information
InterLinked1 committed Jan 28, 2024
1 parent 90e4bff commit f3ab8fe
Show file tree
Hide file tree
Showing 3 changed files with 394 additions and 179 deletions.
83 changes: 83 additions & 0 deletions include/mod_smtp_client.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* LBBS -- The Lightweight Bulletin Board System
*
* Copyright (C) 2023-2024, Naveen Albert
*
* Naveen Albert <[email protected]>
*
*/

/*! \file
*
* \brief SMTP client
*
* \note This is a somewhat low-level SMTP client, which mainly abstracts the process
* of connecting to an SMTP server. It does not operate at the level of
* "sending a message" or things like that. Other modules build on top of this to do that.
* This is one layer high level than the bbs_tcp_client, but still lower than an application layer.
*
*/

#define SMTP_EXPECT(smtpclient, ms, str) \
res = bbs_tcp_client_expect(&(smtpclient)->client, "\r\n", 1, ms, str); \
if (res) { bbs_warning("Expected '%s', got: %s\n", str, (smtpclient)->client.rldata.buf); goto cleanup; } else { bbs_debug(9, "Found '%s': %s\n", str, (smtpclient)->client.rldata.buf); }

#define bbs_smtp_client_send(smtpclient, fmt, ...) bbs_tcp_client_send(&(smtpclient)->client, fmt, ## __VA_ARGS__); bbs_debug(3, " => " fmt, ## __VA_ARGS__);

#define SMTP_CAPABILITY_STARTTLS (1 << 0)
#define SMTP_CAPABILITY_PIPELINING (1 << 1)
#define SMTP_CAPABILITY_8BITMIME (1 << 2)
#define SMTP_CAPABILITY_ENHANCEDSTATUSCODES (1 << 3)
#define SMTP_CAPABILITY_ETRN (1 << 4)
#define SMTP_CAPABILITY_AUTH_LOGIN (1 << 5)
#define SMTP_CAPABILITY_AUTH_PLAIN (1 << 6)
#define SMTP_CAPABILITY_AUTH_XOAUTH2 (1 << 7)

struct bbs_smtp_client {
struct bbs_tcp_client client;
struct bbs_url url;
const char *helohost; /*!< Hostname to use for HELO/EHLO */
const char *hostname; /*!< Hostname of remote SMTP server */
int caps; /*!< Capabilities supported by remote SMTP server */
int maxsendsize; /*!< Maximum size of messages accepted by remote SMTP server */
unsigned int secure:1; /*!< Connection is secure */
};

/*!
* \brief Initialize and connect to a remote SMTP server
* \param[out] smtpclient
* \param helohost Hostname to use for HELO/EHLO. Must remain valid pointer for duration of SMTP client session.
* \param hostname Server hostname. Must remain valid pointer for duration of SMTP client session.
* \param port Server port
* \param secure Whether to use implicit TLS
* \param buf Readline buffer to use
* \param len Size of buf
* \retval 0 on success, -1 on failure
*/
int bbs_smtp_client_connect(struct bbs_smtp_client *smtpclient, const char *helohost, const char *hostname, int port, int secure, char *buf, size_t len);

/*! \brief Await a final SMTP response code */
int bbs_smtp_client_expect_final(struct bbs_smtp_client *restrict smtpclient, int ms, const char *code, size_t codelen);

#define SMTP_CLIENT_EXPECT_FINAL(smtpclient, ms, code) if ((res = bbs_smtp_client_expect_final(smtpclient, ms, code, STRLEN(code)))) { goto cleanup; }

/*!
* \brief Handshake with an SMTP server, parsing its advertised capabilities
* \param smtpclient
* \param Whether to require a secure connection (e.g. STARTTLS)
*/
int bbs_smtp_client_handshake(struct bbs_smtp_client *restrict smtpclient, int require_secure);

/*!
* \brief Perform STARTTLS on an SMTP connection (explicit TLS)
* \param smtpclient
* \retval 0 on success
* \retval -1 STARTTLS unavailable, connection already encrypted, or TLS failure
*/
int bbs_smtp_client_starttls(struct bbs_smtp_client *restrict smtpclient);

/*!
* \brief Destroy an SMTP client
* \param smtpclient
*/
void bbs_smtp_client_destroy(struct bbs_smtp_client *restrict smtpclient);
195 changes: 195 additions & 0 deletions modules/mod_smtp_client.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/*
* LBBS -- The Lightweight Bulletin Board System
*
* Copyright (C) 2023-2024, Naveen Albert
*
* Naveen Albert <[email protected]>
*
* This program is free software, distributed under the terms of
* the GNU General Public License Version 2. See the LICENSE file
* at the top of the source tree.
*/

/*! \file
*
* \brief SMTP client
*
*
* \author Naveen Albert <[email protected]>
*/

#include "include/bbs.h"

#include <ctype.h>

#include "include/module.h"
#include "include/utils.h"

#include "include/mod_smtp_client.h"

int bbs_smtp_client_connect(struct bbs_smtp_client *smtpclient, const char *helohost, const char *hostname, int port, int secure, char *buf, size_t len)
{
int res;

memset(smtpclient, 0, sizeof(struct bbs_smtp_client));
memset(&smtpclient->client, 0, sizeof(smtpclient->client));
memset(&smtpclient->url, 0, sizeof(smtpclient->url));

smtpclient->helohost = helohost;
smtpclient->hostname = hostname;
smtpclient->url.host = hostname;
smtpclient->url.port = port;
SET_BITFIELD(smtpclient->secure, secure);
res = bbs_tcp_client_connect(&smtpclient->client, &smtpclient->url, secure, buf, len);
if (res) {
bbs_debug(3, "Failed to set up TCP connection to %s\n", hostname);
return res;
}
return 0;
}

static void process_capabilities(int *restrict caps, int *restrict maxsendsize, const char *capname)
{
if (strlen_zero(capname) || !isupper(*capname)) { /* Capabilities are all uppercase XXX but is that required by the RFC? */
return;
}

#define PARSE_CAPABILITY(name, flag) \
else if (!strcmp(capname, name)) { \
*caps |= flag; \
}

if (0) {
/* Unused */
}
PARSE_CAPABILITY("STARTTLS", SMTP_CAPABILITY_STARTTLS)
PARSE_CAPABILITY("PIPELINING", SMTP_CAPABILITY_PIPELINING)
PARSE_CAPABILITY("8BITMIME", SMTP_CAPABILITY_8BITMIME)
PARSE_CAPABILITY("ENHANCEDSTATUSCODES", SMTP_CAPABILITY_ENHANCEDSTATUSCODES)
PARSE_CAPABILITY("ETRN", SMTP_CAPABILITY_ETRN)
#undef PARSE_CAPABILITY
else if (STARTS_WITH(capname, "AUTH ")) {
capname += STRLEN("AUTH ");
if (strstr(capname, "LOGIN")) {
*caps |= SMTP_CAPABILITY_AUTH_LOGIN;
}
if (strstr(capname, "PLAIN")) {
*caps |= SMTP_CAPABILITY_AUTH_PLAIN;
}
if (strstr(capname, "XOAUTH2")) {
bbs_debug(3, "Supports oauth2\n");
*caps |= SMTP_CAPABILITY_AUTH_XOAUTH2;
}
} else if (STARTS_WITH(capname, "SIZE")) { /* The argument containing the size is optional */
const char *size = capname + STRLEN("SIZE");
if (!strlen_zero(size)) {
/* If there's a limit provided in the capabilities, store it and abort early if message length exceeds this */
size++;
if (!strlen_zero(size)) {
*maxsendsize = atoi(size);
}
}
} else if (!strcasecmp(capname, "CHUNKING") || !strcasecmp(capname, "SMTPUTF8") || !strcasecmp(capname, "BINARYMIME")
|| !strcasecmp(capname, "VRFY") || !strcasecmp(capname, "ETRN") || !strcasecmp(capname, "DSN") || !strcasecmp(capname, "HELP")) {
/* Don't care about */
} else if (!strcmp(capname, "PIPECONNECT")) {
/* Don't care about, at the moment, but could be used in the future to optimize:
* https://www.exim.org/exim-html-current/doc/html/spec_html/ch-main_configuration.html */
} else if (!strcmp(capname, "AUTH=LOGIN PLAIN")) {
/* Ignore: this SMTP server advertises this capability (even though it's malformed) to support some broken clients */
} else if (!strcmp(capname, "OK")) {
/* This is not a real capability, just ignore it. Yahoo seems to do this. */
} else {
bbs_warning("Unknown capability advertised: %s\n", capname);
}
}

int bbs_smtp_client_expect_final(struct bbs_smtp_client *restrict smtpclient, int ms, const char *code, size_t codelen)
{
int res;
/* Read until we get a response that isn't the desired code or isn't a nonfinal response */
do {
res = bbs_tcp_client_expect(&smtpclient->client, "\r\n", 1, ms, code);
bbs_debug(3, "Found '%s': %s\n", code, smtpclient->client.rldata.buf);
} while (!strncmp(smtpclient->client.rldata.buf, code, codelen) && smtpclient->client.rldata.buf[codelen] == '-');
if (res > 0) {
bbs_warning("Expected '%s', got: %s\n", code, smtpclient->client.rldata.buf);
} else if (res < 0) {
bbs_warning("Failed to receive '%s'\n", code);
}
return res;
}

int bbs_smtp_client_handshake(struct bbs_smtp_client *restrict smtpclient, int require_secure)
{
int res = 0;

bbs_smtp_client_send(smtpclient, "EHLO %s\r\n", smtpclient->helohost);
/* Don't use bbs_smtp_client_expect_final as we'll miss reading the capabilities */
res = bbs_tcp_client_expect(&smtpclient->client, "\r\n", 1, MIN_MS(5), "250"); /* Won't return 250 if ESMTP not supported */
if (res) { /* Fall back to HELO if EHLO not supported */
if (require_secure && !smtpclient->secure) { /* STARTTLS is only supported by EHLO, not HELO */
bbs_warning("SMTP server %s does not support STARTTLS, but encryption is mandatory. Aborting connection.\n", smtpclient->hostname);
res = 1;
goto cleanup;
}
bbs_debug(3, "SMTP server %s does not support ESMTP, falling back to regular SMTP\n", smtpclient->hostname);
bbs_smtp_client_send(smtpclient, "HELO %s\r\n", smtpclient->helohost);
SMTP_CLIENT_EXPECT_FINAL(smtpclient, MIN_MS(5), "250");
} else {
/* Keep reading the rest of the multiline EHLO */
while (STARTS_WITH(smtpclient->client.rldata.buf, "250-")) {
bbs_debug(9, "<= %s\n", smtpclient->client.rldata.buf);
process_capabilities(&smtpclient->caps, &smtpclient->maxsendsize, smtpclient->client.rldata.buf + 4);
res = bbs_tcp_client_expect(&smtpclient->client, "\r\n", 1, SEC_MS(15), "250");
}
bbs_debug(9, "<= %s\n", smtpclient->client.rldata.buf);
process_capabilities(&smtpclient->caps, &smtpclient->maxsendsize, smtpclient->client.rldata.buf + 4);
bbs_debug(6, "Finished processing multiline EHLO\n");
}

cleanup:
return res;
}

int bbs_smtp_client_starttls(struct bbs_smtp_client *restrict smtpclient)
{
int res;
if (smtpclient->secure) {
bbs_error("Can't do STARTTLS, connection is already secure\n");
return -1;
}
if (smtpclient->caps & SMTP_CAPABILITY_STARTTLS) {
bbs_smtp_client_send(smtpclient, "STARTTLS\r\n");
SMTP_CLIENT_EXPECT_FINAL(smtpclient, 2500, "220");
bbs_debug(3, "Starting TLS\n");
if (bbs_tcp_client_starttls(&smtpclient->client, smtpclient->hostname)) {
return -1; /* Abort if we were told STARTTLS was available but failed to negotiate. */
}
smtpclient->secure = 1;
/* Start over again. */
smtpclient->caps = 0;
return bbs_smtp_client_handshake(smtpclient, 1);
}
/* STARTTLS not supported */

cleanup: /* Used by SMTP_CLIENT_EXPECT_FINAL */
return -1;
}

void bbs_smtp_client_destroy(struct bbs_smtp_client *restrict smtpclient)
{
bbs_tcp_client_cleanup(&smtpclient->client);
}

static int load_module(void)
{
return 0;
}

static int unload_module(void)
{
return 0;
}

BBS_MODULE_INFO_FLAGS("SMTP Client", MODFLAG_GLOBAL_SYMBOLS);
Loading

0 comments on commit f3ab8fe

Please sign in to comment.