-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
mod_smtp_client: Move SMTP client to separate module.
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
1 parent
90e4bff
commit f3ab8fe
Showing
3 changed files
with
394 additions
and
179 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
Oops, something went wrong.