diff --git a/configs/net_smtp.conf b/configs/net_smtp.conf index 50e0566..f381ba5 100644 --- a/configs/net_smtp.conf +++ b/configs/net_smtp.conf @@ -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 = john@example.com,*@john.example.net ; Allow local user 'john' to submit outgoing mail additionally using john@example.com 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. diff --git a/nets/net_smtp.c b/nets/net_smtp.c index 95093ea..e636dc6 100644 --- a/nets/net_smtp.c +++ b/nets/net_smtp.c @@ -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; @@ -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 @@ -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 @@ -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. */ @@ -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);