Skip to content

Commit

Permalink
net_smtp: Allow mail submissions for non-local domains.
Browse files Browse the repository at this point in the history
Previously, users were only able to submit mail for identites
that corresponded to local mailboxes on the system. However,
it may be desirable for a user to submit mail locally and have
it relayed to an authorized sender for this domain. This
adds the concept of "authorized identities" for local mail users,
which allows sending as any arbitrary identities, particularly
useful for multi-node private mail transit.
  • Loading branch information
InterLinked1 committed Dec 29, 2024
1 parent d31303c commit 23b4859
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 1 deletion.
25 changes: 25 additions & 0 deletions configs/net_smtp.conf
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,31 @@ loglevel=5 ; Log level from 0 to 10 (maximum debug). Default is 5.
;10.1.1.3 = yes ; The actual value does not matter and is ignored.
;10.1.0.0/24 = yes ; CIDR ranges and hostnames are also acceptable.

[authorized_senders] ; Mapping of additional identities as which a user is allowed to send email.
; This is intended for if you want to allow users to submit outgoing mail on this server using these addresses,
; even though their incoming mail may be handled elsewhere. This mail will then be accepted and either
; delivered using an MX record lookup or by the static routes defined in [static_relays].
; The domains of the identites used here do NOT need to be configured in [domains] in mod_mail.conf.
;
; An alternate is using the RELAY MailScript rule to submit mail using the message submission service for that domain.
;
; WARNING: Before adding any identites, you SHOULD ensure that any domains with identities included below
; authorize the host sending mail to the Internet (e.g. via SPF/DKIM). There are typically two scenarios:
; 1. The public IP address of this server's egress to the Internet is authorized. In this case,
; you're good to go.
; 2. This server's public IP address is not authorized. In this case, you should ensure a "smart host"
; is configured through the [static_relays] section, to relay outgoing mail for these domains
; (and possibly all email traffic) to another SMTP server which IS authorized.
;
; In other words, ensure that you have the necessary SPF records set up for your domain,
; and ensure that you have the correct static routes in place to ensure it egresses appropriately
; and not from an unauthorized IP address. If an upstream "smart host" handles DKIM signing
; for domains configured here, then you don't need to do it on this server, which can simply
; configuration in a private mail routing network by allowing you to centralized signing on the egress server.
;
;john = [email protected],*@john.example.net ; Allow local user 'john' to submit outgoing mail additionally using [email protected] or *@john.example.net
;jane = * ; Allow local user 'jane' to submit outgoing mail using ANY identity (DANGEROUS!)

[privs]
;relayin=1 ; Minimum privilege level required to accept external email for a user.
;relayout=1 ; Minimum privilege level required to relay external email outbound for a user.
Expand Down
99 changes: 98 additions & 1 deletion nets/net_smtp.c
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@ struct smtp_relay_host {

static RWLIST_HEAD_STATIC(authorized_relays, smtp_relay_host);

struct smtp_authorized_identity {
const char *username;
struct stringlist identities;
RWLIST_ENTRY(smtp_authorized_identity) entry;
char data[];
};

static RWLIST_HEAD_STATIC(authorized_identities, smtp_authorized_identity);

static void add_authorized_relay(const char *source, const char *domains)
{
struct smtp_relay_host *h;
Expand Down Expand Up @@ -261,6 +270,28 @@ static void relay_free(struct smtp_relay_host *h)
free(h);
}

static void add_authorized_identity(const char *username, const char *identities)
{
struct smtp_authorized_identity *i;

if (STARTS_WITH(identities, "*")) {
bbs_notice("This server is configured as an open mail relay for user '%s' and may be abused!\n", username);
}

i = calloc(1, sizeof(*i) + strlen(username) + 1);
if (ALLOC_FAILURE(i)) {
return;
}

strcpy(i->data, username); /* Safe */
i->username = i->data;
stringlist_init(&i->identities);
stringlist_push_list(&i->identities, identities);

/* Head insert, so later entries override earlier ones, in case multiple match */
RWLIST_INSERT_HEAD(&authorized_identities, i, entry);
}

/*!
* \brief Whether this client is an authorized relay
* \param srcip Source IP of connection
Expand Down Expand Up @@ -296,6 +327,50 @@ int smtp_relay_authorized(const char *srcip, const char *hostname)
return __smtp_relay_authorized(srcip, hostname);
}

/*!
* \brief Whether this user is authorized to send email as a particular identity
* \param username User's username
* \param identity Email address using which the user is attempting to submit mail
* \retval 1 if authorized, 0 if not
*/
static int smtp_user_authorized_for_identity(const char *username, const char *identity)
{
const char *domain;
struct smtp_authorized_identity *i;

RWLIST_RDLOCK(&authorized_identities);
RWLIST_TRAVERSE(&authorized_identities, i, entry) {
if (strcasecmp(username, i->username)) {
continue;
}
/* XXX In theory, we could do just a single traversal of &i->identities,
* and just do each of the 3 checks for each item.
* In the meantime, these checks are ordered from common case to least likely. */
/* First, check for explicit match. */
if (stringlist_case_contains(&i->identities, identity)) {
RWLIST_UNLOCK(&authorized_identities);
return 1;
}
/* Next, check for domain match. */
domain = strchr(identity, '@');
if (domain++ && *domain) {
char searchstr[256];
snprintf(searchstr, sizeof(searchstr), "*@%s", domain);
if (stringlist_case_contains(&i->identities, searchstr)) {
RWLIST_UNLOCK(&authorized_identities);
return 1;
}
}
/* Last check, is the user blank authorized to relay mail for any address? */
if (stringlist_contains(&i->identities, "*")) {
RWLIST_UNLOCK(&authorized_identities);
return 1;
}
}
RWLIST_UNLOCK(&authorized_identities);
return 0;
}

/*
* Status code references:
* - https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml
Expand Down Expand Up @@ -2198,16 +2273,32 @@ static int check_identity(struct smtp_session *smtp, char *s)
char sendersfile[256];
char buf[32];
FILE *fp;
int domain_is_local;

/* Must use bbs_parse_email_address for sure, since From header could contain a name, not just the address that's in the <> */
if (bbs_parse_email_address(s, NULL, &user, &domain)) {
smtp_reply(smtp, 550, 5.7.1, "Malformed From header");
return -1;
}
if (!domain || !mail_domain_is_local(domain)) { /* Wrong domain, or missing domain altogether, yikes */

domain_is_local = domain ? mail_domain_is_local(domain) : 1;
if (!domain) { /* Missing domain altogether, yikes */
smtp_reply(smtp, 550, 5.7.1, "You are not authorized to send email using this identity");
return -1;
}
if (!domain_is_local) { /* Wrong domain? */
/* For non-local domains, if the user is explicitly authorized to send mail as this identity,
* Then allow it. */
char addr[256];
snprintf(addr, sizeof(addr), "%s@%s", user, domain); /* Reconstruct instead of using s, to ensure no <> */
if (bbs_user_is_registered(smtp->node->user) && smtp_user_authorized_for_identity(bbs_username(smtp->node->user), addr)) {
bbs_debug(3, "User '%s' explicitly authorized to submit mail as %s\n", bbs_username(smtp->node->user), addr);
return 0; /* No further checks apply in this case */
}
smtp_reply(smtp, 550, 5.7.1, "You are not authorized to send email using this identity");
return -1;
}

/* Check what mailbox the sending username resolves to.
* One corner case is the catch all address. This user is allowed to send email as any address,
* which makes sense since the catch all is going to be the sysop, if it exists. */
Expand Down Expand Up @@ -3061,6 +3152,12 @@ static int load_config(void)
stringlist_push(&trusted_relays, key);
}
}
} else if (!strcmp(bbs_config_section_name(section), "authorized_senders")) {
while ((keyval = bbs_config_section_walk(section, keyval))) {
key = bbs_keyval_key(keyval);
val = bbs_keyval_val(keyval);
add_authorized_identity(key, val);
}
} else if (!strcmp(bbs_config_section_name(section), "starttls_exempt")) {
while ((keyval = bbs_config_section_walk(section, keyval))) {
key = bbs_keyval_key(keyval);
Expand Down

0 comments on commit 23b4859

Please sign in to comment.