diff --git a/configs/net_smtp.conf b/configs/net_smtp.conf index bf448b7..f23f1e8 100644 --- a/configs/net_smtp.conf +++ b/configs/net_smtp.conf @@ -55,6 +55,14 @@ notifyextfirstmsg=yes ; Whether to send an email to a user's external email addr logfile=/var/log/lbbs/smtp.log ; SMTP logfile. If set, SMTP messages up to the SMTP log level will be logged to this file. loglevel=5 ; Log level from 0 to 10 (maximum debug). Default is 5. +; The next three sections define different types of relays. For a simple MTA, you can ignore these sections. +; Some of these settings are complementary, but they are different. In a nutshell: +; [authorized_relays] = hosts allowed to relay outgoing mail through us (per-domain) +; [static_relays] = static definitions of how to deliver mail to the "next hop" per-domain. This is BOTH: +; - hosts allowed to relay incoming mail through us and to what hosts (per-domain) +; - hosts through which all our outgoing mail is relayed +; [trusted_relays] = hosts allowed to relay our incoming mail to us + [authorized_relays] ; Define remote hosts that are allowed to relay outgoing mail using this server as a smart host. ; Configure each authorized relay as an IP/hostname/CIDR range and a list of domains or subdomains for which they are authorized to relay mail. ; If a connection matches multiple entries, the relay is allowed as long as it matches one of the entries. @@ -63,7 +71,40 @@ loglevel=5 ; Log level from 0 to 10 (maximum debug). Default is 5. ; If further verification of messages is required, the submitting SMTP server/client must do it (e.g. checking the sender is authorized to send as a particular user). ; Do not attempt to relay mail for domains that *THIS* server is not authorized to send as (otherwise failed SPF checks, etc. will likely get you blacklisted quickly). ; -;10.1.1.5 = example.com,example.net,example.org ; Messages from 10.1.1.5 may be relayed for example.com, example.net, and example.org. +;10.1.1.5 = example.com,example.net ; Messages from 10.1.1.5 may be relayed for example.com and example.net +;10.1.1.6 = example.org + +[static_relays] ; Define remote hosts for which the BBS will accept and forward incoming mail to another mail transfer agent. These bypass an MX lookup. + ; This can be used both for accepting incoming mail for another mail server or for routing outbound mail via a smart host. + ; + ; You might configure this at a public-facing site to forward mail to other sites that cannot directly receive mail from the Internet on port 25, e.g. over a VPN tunnel. + ; The public MX records for these domains would point to this host, and this host would forward it to the real mail servers for those domains. + ; You will most likely also want to configure the BBS to accept and relay mail for the corresponding IP/domain in [authorized_relays] + ; Only static IP addresses (no hostnames or CIDR ranges) are allowed for values in this section. + ; Domains must be explicitly enumerated; no wildcards for subdomains. + ; + ; On the mail server for domains which are proxied through this host, the '*' rule can be used to route all outgoing mail through another host. +;example.com = 10.1.1.5 +;example.net = 10.1.1.5,10.1.1.6 ; Try 10.1.1.5 first, then 10.1.1.6 as a fallback (like with higher priority MX records) +;example.org = 10.1.1.6:2525 ; If the remote mail transfer agent is listening on a non-standard port (not 25), you can specify the port explicitly. +;* = 10.1.1.4 ; This rule is special. Rather than looking up via MX record, outgoing mail will be relayed via this "smart host" instead. Useful when outgoing port 25 is blocked. + ; You will likely also want to add this server to [trusted_relays] if it also handles your incoming mail. + +[trusted_relays] ; These hosts are allowed to accept mail on our behalf and forward it to us. + ; This applies to ALL mail from ALL originating MTAs. This will inhibit certain + ; checks that are done on incoming mail by default, such as doing a reverse lookup + ; on the sender, which would otherwise fail due to the intermediary SMTP host that + ; originally accepted the message for us from the sending MTA. + ; Adding a host here indicates that that server has already performed these checks, + ; and they will not be performed again here since it would not be possible to do so. + ; If both your incoming and outgoing mail goes through a certain host, it should be listed + ; in both this section as well as the * rule for [static_relays]. + ; However, depending on the networking arrangement between the two MTAs, note that the + ; IP addresses COULD be different, e.g. if using a NATed VPN tunnel. + ; If in doubt, send an email that is received by this host, confirm the immediately upstream IP, + ; and then whitelist that here. +;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. [privs] ;relayin=1 ; Minimum privilege level required to accept external email for a user. diff --git a/modules/mod_smtp_delivery_external.c b/modules/mod_smtp_delivery_external.c index b35fb89..cd949ac 100644 --- a/modules/mod_smtp_delivery_external.c +++ b/modules/mod_smtp_delivery_external.c @@ -73,6 +73,68 @@ struct mx_record { RWLIST_HEAD(mx_records, mx_record); +/*! \brief List of stringlists for static routes */ +struct static_relay { + const char *hostname; + struct stringlist routes; + RWLIST_ENTRY(static_relay) entry; + char data[]; +}; + +static RWLIST_HEAD_STATIC(static_relays, static_relay); + +/*! \note static_relays should be locked when calling */ +static int add_static_relay(const char *hostname, const char *route) +{ + struct static_relay *s; + + s = calloc(1, sizeof(*s) + strlen(hostname) + 1); + if (ALLOC_FAILURE(s)) { + return -1; + } + strcpy(s->data, hostname); /* Safe */ + s->hostname = s->data; + stringlist_push_list(&s->routes, route); + RWLIST_INSERT_TAIL(&static_relays, s, entry); + return 0; +} + +static void free_static_relay(struct static_relay *s) +{ + stringlist_empty(&s->routes); + free(s); +} + +/*! + * \brief Check whether a domain has a defined static route + * \internal + * \param domain + * \return Static routes to use, if defined (override MX lookup) + * \return NULL if no static routes (do MX lookup instead) + */ +static struct stringlist *get_static_routes(const char *domain) +{ + struct stringlist *routes = NULL; + struct static_relay *s, *wildcard = NULL; + + RWLIST_RDLOCK(&static_relays); + RWLIST_TRAVERSE(&static_relays, s, entry) { + if (!strcmp(s->hostname, "*")) { + wildcard = s; + } else if (!strcasecmp(s->hostname, domain)) { + break; + } + } + s = s ? s : wildcard; /* The '*' route is special, and should match last. */ + if (s) { + /* It's okay to return this directly, + * since once added, routes are not removed until the module unloads. */ + routes = &s->routes; + } + RWLIST_UNLOCK(&static_relays); + return routes; +} + /*! * \brief Fill the results list with the MX results in order of priority * \param domain @@ -97,7 +159,8 @@ static int lookup_mx_all(const char *domain, struct stringlist *results) if (strlen_zero(domain)) { bbs_error("Missing domain\n"); return -1; - } else if (bbs_hostname_is_ipv4(domain)) { /* IP address? Just send it there */ + } + if (bbs_hostname_is_ipv4(domain)) { /* IP address? Just send it there */ stringlist_push_tail(results, domain); return 0; } @@ -514,10 +577,15 @@ static int try_send(struct smtp_session *smtp, struct smtp_tx_data *tx, const ch tx->prot = "smtp"; tx->stage = "MAIL FROM"; - if (bbs_hostname_is_ipv4(domain)) { - smtp_client_send(&client, "MAIL FROM:<%s@[%s]>\r\n", user, domain); /* Domain literal for IP address */ + if (!strlen_zero(user)) { + if (bbs_hostname_is_ipv4(domain)) { + smtp_client_send(&client, "MAIL FROM:<%s@[%s]>\r\n", user, domain); /* Domain literal for IP address */ + } else { + smtp_client_send(&client, "MAIL FROM:<%s@%s>\r\n", user, domain); /* sender lacks <>, but recipient has them */ + } } else { - smtp_client_send(&client, "MAIL FROM:<%s@%s>\r\n", user, domain); /* sender lacks <>, but recipient has them */ + /* For non-delivery / postmaster sending */ + smtp_client_send(&client, "MAIL FROM:<>\r\n"); } SMTP_CLIENT_EXPECT_FINAL(&client, MIN_MS(5), "250"); /* RFC 5321 4.5.3.2.2 */ tx->stage = "RCPT FROM"; @@ -589,6 +657,13 @@ static void smtp_trigger_dsn(enum smtp_delivery_action action, struct smtp_tx_da char status[15] = ""; /* Status code should be the 2nd word? */ struct smtp_delivery_outcome *f; + if (strlen_zero(from)) { + /* If this was triggered by a non-delivery report, + * then bail out now, since we can't + * reply if there was no MAIL FROM */ + return; + } + if (action != DELIVERY_FAILED && !notify_queue) { return; } else if (action == DELIVERY_DELIVERED) { @@ -681,6 +756,7 @@ static int mailq_file_load(struct mailq_file *restrict mqf, const char *dir_name mqf->realfrom = strchr(mqf->from, '<'); mqf->realto = strchr(mqf->recipient, '<'); + /* The actual MAIL FROM can be empty if this is a nondelivery report, so we do not validate that it is non-empty (it may be the empty string). */ if (!mqf->realfrom) { bbs_error("Mail queue file MAIL FROM missing <>: %s\n", mqf->fullname); goto cleanup; @@ -702,7 +778,7 @@ static int mailq_file_load(struct mailq_file *restrict mqf, const char *dir_name } safe_strncpy(mqf->todup, mqf->realto, sizeof(mqf->todup)); - if (strlen_zero(mqf->realfrom) || bbs_parse_email_address(mqf->todup, NULL, &mqf->user, &mqf->domain)) { + if (bbs_parse_email_address(mqf->todup, NULL, &mqf->user, &mqf->domain)) { bbs_error("Address parsing error\n"); goto cleanup; } @@ -737,12 +813,48 @@ static int mailq_file_punt(struct mailq_file *mqf) return 0; } +/*! \brief Attempt to send a message via SMTP using static routes instead of doing an MX lookup */ +static int try_static_delivery(struct smtp_session *smtp, struct smtp_tx_data *tx, struct stringlist *static_routes, const char *sender, const char *recipient, int datafd, off_t offset, size_t writelen, char *buf, size_t len) +{ + const char *route; + struct stringitem *i = NULL; + int res = -1; /* Make condition true to start */ + /* Static routes override doing an MX lookup for this domain. + * We have one or more hostnames (with an optionally specified port) to try. */ + while (res < 0 && (route = stringlist_next(static_routes, &i))) { + char hostbuf[256]; + const char *colon; + const char *hostname = route; + int port = DEFAULT_SMTP_PORT; + + /* If this is a hostname:port, we need to split. + * Otherwise, we can use it directly. This is more efficient, + * since no allocations or copies are performed in this case. */ + colon = strchr(route, ':'); + if (colon) { + /* There's a port specified. */ + bbs_strncpy_until(hostbuf, route, sizeof(hostbuf), ':'); /* Copy just the hostname */ + hostname = hostbuf; + colon++; + if (!strlen_zero(colon)) { + port = atoi(colon); /* Parse the port */ + if (port < 1) { + bbs_warning("Invalid port in route '%s', defaulting to port %d\n", route, DEFAULT_SMTP_PORT); + port = DEFAULT_SMTP_PORT; + } + } + } + + res = try_send(smtp, tx, hostname, port, 0, NULL, NULL, sender, recipient, NULL, NULL, 0, datafd, offset, writelen, buf, len); + } + return res; +} + static int on_queue_file(const char *dir_name, const char *filename, void *obj) { int res = -1; - char *hostname; char buf[256] = ""; - struct stringlist mxservers; + struct stringlist *static_routes; struct smtp_tx_data tx; struct mailq_file mqf_stack, *mqf = &mqf_stack; @@ -755,44 +867,49 @@ static int on_queue_file(const char *dir_name, const char *filename, void *obj) return 0; } - bbs_debug(5, "Processing message from %s -> %s\n", mqf->realfrom, mqf->realto); - bbs_debug(2, "Retrying delivery of %s (%s -> %s)\n", mqf->fullname, mqf->realfrom, mqf->realto); - - memset(&mxservers, 0, sizeof(mxservers)); - res = lookup_mx_all(mqf->domain, &mxservers); - if (res == -2) { - smtp_tx_data_reset(&tx); - /* Do not set tx.hostname, since this message is from us, not the remote server */ - snprintf(buf, sizeof(buf), "Domain does not accept mail"); + static_routes = get_static_routes(mqf->domain); + bbs_debug(2, "Processing message %s (%s -> %s), via %s for '%s'\n", mqf->fullname, mqf->realfrom, mqf->realto, static_routes ? "static route(s)" : "MX lookup", mqf->domain); + if (static_routes) { + res = try_static_delivery(NULL, &tx, static_routes, mqf->realfrom, mqf->realto, fileno(mqf->fp), (off_t) mqf->metalen, mqf->size - mqf->metalen, buf, sizeof(buf)); } else { - if (res) { - char a_ip[256]; - /* Fall back to trying the A record */ - if (bbs_resolve_hostname(mqf->domain, a_ip, sizeof(a_ip))) { - bbs_warning("Recipient domain %s does not have any MX or A records\n", mqf->domain); - /* Just treat as undeliverable at this point and return to sender (if no MX records now, probably won't be any the next time we try) */ - /* Send a delivery failure response, then delete the file. */ - bbs_warning("Delivery of message %s from %s to %s has failed permanently (no MX records)\n", mqf->fullname, mqf->realfrom, mqf->realto); - /* There isn't any SMTP level error at this point yet, we have to make our own error message for the bounce message */ - snprintf(buf, sizeof(buf), "No MX record(s) located for hostname %s", mqf->domain); /* No status code */ - smtp_tx_data_reset(&tx); - /* Do not set tx.hostname, since this message is from us, not the remote server */ - smtp_trigger_dsn(DELIVERY_FAILED, &tx, &mqf->created, mqf->realfrom, mqf->realto, buf, fileno(mqf->fp), mqf->metalen, mqf->size - mqf->metalen); - fclose(mqf->fp); - bbs_delete_file(mqf->fullname); - return 0; + struct stringlist mxservers; + memset(&mxservers, 0, sizeof(mxservers)); + res = lookup_mx_all(mqf->domain, &mxservers); + if (res == -2) { + smtp_tx_data_reset(&tx); + /* Do not set tx.hostname, since this message is from us, not the remote server */ + snprintf(buf, sizeof(buf), "Domain does not accept mail"); + } else { + char *hostname; + if (res) { + char a_ip[256]; + /* Fall back to trying the A record */ + if (bbs_resolve_hostname(mqf->domain, a_ip, sizeof(a_ip))) { + bbs_warning("Recipient domain %s does not have any MX or A records\n", mqf->domain); + /* Just treat as undeliverable at this point and return to sender (if no MX records now, probably won't be any the next time we try) */ + /* Send a delivery failure response, then delete the file. */ + bbs_warning("Delivery of message %s from %s to %s has failed permanently (no MX records)\n", mqf->fullname, mqf->realfrom, mqf->realto); + /* There isn't any SMTP level error at this point yet, we have to make our own error message for the bounce message */ + snprintf(buf, sizeof(buf), "No MX record(s) located for hostname %s", mqf->domain); /* No status code */ + smtp_tx_data_reset(&tx); + /* Do not set tx.hostname, since this message is from us, not the remote server */ + smtp_trigger_dsn(DELIVERY_FAILED, &tx, &mqf->created, mqf->realfrom, mqf->realto, buf, fileno(mqf->fp), mqf->metalen, mqf->size - mqf->metalen); + fclose(mqf->fp); + bbs_delete_file(mqf->fullname); + return 0; + } + bbs_warning("Recipient domain %s does not have any MX records, falling back to A record %s\n", mqf->domain, a_ip); + stringlist_push(&mxservers, a_ip); } - bbs_warning("Recipient domain %s does not have any MX records, falling back to A record %s\n", mqf->domain, a_ip); - stringlist_push(&mxservers, a_ip); - } - /* Try all the MX servers in order, if necessary */ - res = -1; /* Make condition true to start */ - while (res < 0 && (hostname = stringlist_pop(&mxservers))) { - res = try_send(NULL, &tx, hostname, DEFAULT_SMTP_PORT, 0, NULL, NULL, mqf->realfrom, mqf->realto, NULL, NULL, 0, fileno(mqf->fp), (off_t) mqf->metalen, mqf->size - mqf->metalen, buf, sizeof(buf)); - free(hostname); + /* Try all the MX servers in order, if necessary */ + res = -1; /* Make condition true to start */ + while (res < 0 && (hostname = stringlist_pop(&mxservers))) { + res = try_send(NULL, &tx, hostname, DEFAULT_SMTP_PORT, 0, NULL, NULL, mqf->realfrom, mqf->realto, NULL, NULL, 0, fileno(mqf->fp), (off_t) mqf->metalen, mqf->size - mqf->metalen, buf, sizeof(buf)); + free(hostname); + } + stringlist_empty(&mxservers); } - stringlist_empty(&mxservers); } mqf->newretries = atoi(mqf->retries); /* This is actually current # of retries, not new # yet */ @@ -961,6 +1078,8 @@ static int external_delivery(struct smtp_session *smtp, struct smtp_response *re if (smtp_is_exempt_relay(smtp)) { bbs_debug(2, "%s is explicitly authorized to relay mail from %s\n", smtp_node(smtp)->ip, smtp_from_domain(smtp)); + } else if (get_static_routes(domain)) { + bbs_debug(2, "%s has static route(s)\n", domain); } else { bbs_assert(fromlocal); /* Shouldn't have slipped through to this point otherwise */ if (!accept_relay_out) { @@ -981,10 +1100,17 @@ static int external_delivery(struct smtp_session *smtp, struct smtp_response *re } if (!always_queue && !send_async) { /* Try to send it synchronously */ - struct stringlist mxservers; - /* Start by trying to deliver it directly, immediately, right now. */ + struct stringlist mxservers, *static_routes; + memset(&mxservers, 0, sizeof(mxservers)); - res = lookup_mx_all(domain, &mxservers); + + /* Start by trying to deliver it directly, immediately, right now. */ + static_routes = get_static_routes(domain); + if (static_routes) { + res = 0; /* If a route exists, we're good so far */ + } else { + res = lookup_mx_all(domain, &mxservers); + } if (res == -2) { smtp_abort(resp, 553, 5.1.2, "Recipient domain does not accept mail."); return -1; @@ -994,12 +1120,16 @@ static int external_delivery(struct smtp_session *smtp, struct smtp_response *re return -1; } #ifndef BUGGY_SEND_IMMEDIATE - /* Try all the MX servers in order, if necessary */ - while (res < 0 && (hostname = stringlist_pop(&mxservers))) { - res = try_send(NULL, &tx, hostname, DEFAULT_SMTP_PORT, 0, NULL, NULL, realfrom, realto, NULL, NULL, 0, srcfd, datalen, size - datalen, buf, sizeof(buf)); - free(hostname); + if (static_routes) { + res = try_static_delivery(NULL, &tx, static_routes, mqf->realfrom, mqf->realto, fileno(mqf->fp), (off_t) mqf->metalen, mqf->size - mqf->metalen, buf, sizeof(buf)); + } else { + /* Try all the MX servers in order, if necessary */ + while (res < 0 && (hostname = stringlist_pop(&mxservers))) { + res = try_send(NULL, &tx, hostname, DEFAULT_SMTP_PORT, 0, NULL, NULL, realfrom, realto, NULL, NULL, 0, srcfd, datalen, size - datalen, buf, sizeof(buf)); + free(hostname); + } + stringlist_empty(&mxservers); } - stringlist_empty(&mxservers); if (res > 0) { /* Permanent error */ /* We've still got the sender on the socket, just relay the error. */ @@ -1144,6 +1274,8 @@ static int relay(struct smtp_session *smtp, struct smtp_msg_process *mproc, int static int exists(struct smtp_session *smtp, struct smtp_response *resp, const char *address, const char *user, const char *domain, int fromlocal, int tolocal) { + struct stringlist *s; + UNUSED(smtp); UNUSED(address); UNUSED(user); @@ -1155,6 +1287,13 @@ static int exists(struct smtp_session *smtp, struct smtp_response *resp, const c return 1; } + s = get_static_routes(domain); + if (s) { + /* e.g. we accept mail for another domain by forwarding it to an SMTP MTA that isn't directly exposed to the Internet */ + bbs_debug(2, "%s has static route(s) defined\n", domain); + return 1; + } + if (!fromlocal) {/* External user trying to send us mail that's not for us. */ /* Built in rejection of relayed mail. If another delivery agent wants to override this, it can, * (e.g. to set up a honeypot), it would just need to have a more urgent priority. */ @@ -1173,6 +1312,7 @@ struct smtp_delivery_agent extdeliver = { static int load_config(void) { struct bbs_config *cfg; + struct bbs_config_section *section = NULL; cfg = bbs_config_load("net_smtp.conf", 1); if (!cfg) { @@ -1192,6 +1332,17 @@ static int load_config(void) bbs_config_val_set_true(cfg, "privs", "relayout", &minpriv_relay_out); + while ((section = bbs_config_walk(cfg, section))) { + if (!strcmp(bbs_config_section_name(section), "static_relays")) { + struct bbs_keyval *keyval = NULL; + RWLIST_WRLOCK(&static_relays); + while ((keyval = bbs_config_section_walk(section, keyval))) { + add_static_relay(bbs_keyval_key(keyval), bbs_keyval_val(keyval)); + } + RWLIST_UNLOCK(&static_relays); + } /* else, ignore. net_smtp will warn about any invalid section names in net_smtp.conf. */ + } + if (queue_interval < 60) { queue_interval = 60; } @@ -1225,6 +1376,7 @@ static int unload_module(void) bbs_pthread_cancel_kill(queue_thread); bbs_pthread_join(queue_thread, NULL); pthread_mutex_destroy(&queue_lock); + RWLIST_WRLOCK_REMOVE_ALL(&static_relays, entry, free_static_relay); return res; } diff --git a/modules/mod_smtp_filter.c b/modules/mod_smtp_filter.c index 0d98614..f9b74eb 100644 --- a/modules/mod_smtp_filter.c +++ b/modules/mod_smtp_filter.c @@ -60,19 +60,28 @@ static int prepend_received(struct smtp_filter_data *f) /*! \brief Separate callback for adding Received header to relayed messages, since these could have multiple recipients */ static int relay_filter_cb(struct smtp_filter_data *f) { - if (smtp_is_exempt_relay(f->smtp)) { - const char *prot; - char timestamp[40]; - char hostname[256]; + const char *prot; + char timestamp[40]; + char hostname[256]; + + prot = smtp_protname(f->smtp); + smtp_timestamp(smtp_received_time(f->smtp), timestamp, sizeof(timestamp)); + bbs_get_hostname(f->node->ip, hostname, sizeof(hostname)); /* Look up the sending IP */ + + /* This is to cover: + * 1) The case of other MTAs that relay their outgoing mail through us (smtp_is_exempt_relay(f->smtp) == 1) + * 2) The case of other MTAs that receive their incoming mail through us. + * + * Note: Originally, only the first case was handled here, but when the ability to forward incoming mail was + * added, the second case had to be considered, and as I can't think of any counterexamples + * to messages running the OUT filter being one or the other, the conditional was removed altogether. + * If we find later that there are OUT messages for which we shouldn't be adding a "Received" header, + * then this logic may need to be refined. + * + * The first hostname is the HELO/EHLO hostname. The second one is the reverse DNS hostname */ + smtp_filter_write(f, "Received: from %s (%s [%s])\r\n\tby %s with %s\r\n\tfor %s; %s\r\n", + f->helohost, hostname, f->node->ip, bbs_hostname(), prot, f->recipient, timestamp); /* recipient already in <> */ - prot = smtp_protname(f->smtp); - smtp_timestamp(smtp_received_time(f->smtp), timestamp, sizeof(timestamp)); - bbs_get_hostname(f->node->ip, hostname, sizeof(hostname)); /* Look up the sending IP */ - /* The first hostname is the HELO/EHLO hostname. - * The second one is the reverse DNS hostname */ - smtp_filter_write(f, "Received: from %s (%s [%s])\r\n\tby %s with %s\r\n\tfor %s; %s\r\n", - f->helohost, hostname, f->node->ip, bbs_hostname(), prot, f->recipient, timestamp); /* recipient already in <> */ - } return 0; } diff --git a/modules/mod_smtp_filter_arc.c b/modules/mod_smtp_filter_arc.c index 5742b10..ebde699 100644 --- a/modules/mod_smtp_filter_arc.c +++ b/modules/mod_smtp_filter_arc.c @@ -179,7 +179,7 @@ static int arc_filter_sign_cb(struct smtp_filter_data *f) /* We sign the message using the same domain that will be used in the outgoing MAIL FROM. */ domain = bbs_strcnext(f->from, '@'); if (!domain) { - bbs_warning("No FROM domain?\n"); + /* If this is a non-delivery report, MAIL FROM is empty so there's no sender at all */ return 0; } diff --git a/nets/net_smtp.c b/nets/net_smtp.c index 39b0e0c..4703a22 100644 --- a/nets/net_smtp.c +++ b/nets/net_smtp.c @@ -85,6 +85,7 @@ static int validatespf = 1; static int add_received_msa = 0; static int archivelists = 1; +struct stringlist trusted_relays; struct stringlist starttls_exempt; static FILE *smtplogfp = NULL; @@ -485,6 +486,19 @@ static int smtp_ip_mismatch(const char *actual, const char *hostname) return 0; } +static int is_trusted_relay(const char *hostname) +{ + const char *s; + struct stringitem *i = NULL; + + while ((s = stringlist_next(&trusted_relays, &i))) { + if (bbs_ip_match_ipv4(hostname, s)) { + return 1; + } + } + return 0; +} + static int exempt_from_starttls(struct smtp_session *smtp) { const char *s; @@ -1106,12 +1120,12 @@ int smtp_is_exempt_relay(struct smtp_session *smtp) int smtp_should_validate_spf(struct smtp_session *smtp) { - return validatespf && !smtp->fromlocal && !smtp->tflags.relay; + return validatespf && !smtp->fromlocal && !smtp->tflags.relay && (!smtp->node || !is_trusted_relay(smtp->node->ip)); } int smtp_should_validate_dkim(struct smtp_session *smtp) { - return smtp->tflags.dkimsig && !smtp->tflags.relay; + return smtp->tflags.dkimsig && !smtp->tflags.relay && (!smtp->node || !is_trusted_relay(smtp->node->ip)); } int smtp_is_message_submission(struct smtp_session *smtp) @@ -1404,7 +1418,7 @@ struct smtp_delivery_outcome *smtp_delivery_outcome_new(const char *recipient, c statuslen = STRING_ALLOC_SIZE(status); errorlen = STRING_ALLOC_SIZE(error); - f = calloc(1, sizeof(*f) + reciplen + hostlen + iplen + errorlen); + f = calloc(1, sizeof(*f) + reciplen + hostlen + iplen + statuslen + errorlen); if (ALLOC_FAILURE(f)) { return NULL; } @@ -1479,6 +1493,7 @@ static int any_failures(struct smtp_delivery_outcome **f, int n) int smtp_dsn(const char *sendinghost, struct tm *arrival, const char *sender, int srcfd, int offset, size_t msglen, struct smtp_delivery_outcome **f, int n) { int i, res; + char full_sender[256]; char tmpattach[32] = "/tmp/bouncemsgXXXXXX"; FILE *fp; char bound[256]; @@ -1490,16 +1505,24 @@ int smtp_dsn(const char *sendinghost, struct tm *arrival, const char *sender, in /* The MAIL FROM in non-delivery reports is always empty to prevent looping. * e.g. if we're bouncing a bounce, just abort. */ if (strlen_zero(sender)) { - bbs_warning("MAIL FROM is empty, cannot deliver non-delivery report\n"); + bbs_warning("MAIL FROM is empty, cannot deliver delivery status notification\n"); return -1; } + if (*sender == '<') { + /* Sender arrives without <>, so this shouldn't happen */ + safe_strncpy(full_sender, sender, sizeof(full_sender)); + } else { + snprintf(full_sender, sizeof(full_sender), "<%s>", sender); + } + bbs_debug(1, "Sending SMTP DSN to %s\n", full_sender); + fp = bbs_mkftemp(tmpattach, 0600); if (!fp) { return -1; } - /* Format of the non-delivery report is defined in RFC 3461 Section 6 */ + /* Format of the DSN report is defined in RFC 3461 Section 6 */ /* Generate headers */ if (!strftime(date, sizeof(date), "%a, %d %b %Y %H:%M:%S %z", localtime_r(&t, &tm))) { @@ -1509,7 +1532,7 @@ int smtp_dsn(const char *sendinghost, struct tm *arrival, const char *sender, in fprintf(fp, "Date: %s\r\n", date); fprintf(fp, "From: \"Mail Delivery Subsystem\" \r\n", bbs_hostname()); fprintf(fp, "Subject: %s\r\n", delivery_subject_name(f, n)); - fprintf(fp, "To: %s\r\n", sender); + fprintf(fp, "To: %s\r\n", full_sender); fprintf(fp, "Auto-Submitted: auto-replied\r\n"); fprintf(fp, "Message-ID: <%s-%u-%d@%s>\r\n", "LBBS-NDR", (unsigned int) random(), (int) getpid(), bbs_hostname()); fprintf(fp, "MIME-Version: 1.0\r\n"); @@ -1612,7 +1635,7 @@ int smtp_dsn(const char *sendinghost, struct tm *arrival, const char *sender, in length = (size_t) ftell(fp); fclose(fp); - res = nosmtp_deliver(tmpattach, "", sender, length); /* Empty MAIL FROM for DSNs */ + res = nosmtp_deliver(tmpattach, "", full_sender, length); /* Empty MAIL FROM for DSNs */ unlink(tmpattach); return res; } @@ -2719,12 +2742,14 @@ static int load_config(void) bbs_config_val_set_port(cfg, "msa", "port", &msa_port); bbs_config_val_set_true(cfg, "msa", "requirestarttls", &require_starttls); - /* Blacklist */ +/*! \brief Section names that are valid but not parsed in the loop */ +#define VALID_SECT_NAME(s) (!strcmp(s, "general") || !strcmp(s, "logging") || !strcmp(s, "privs") || !strcmp(s, "smtp") || !strcmp(s, "smtps") || !strcmp(s, "msa") || !strcmp(s, "static_relays")) + while ((section = bbs_config_walk(cfg, section))) { struct bbs_keyval *keyval = NULL; const char *key, *val; - if (!strcmp(bbs_config_section_name(section), "blacklist")) { + if (!strcmp(bbs_config_section_name(section), "blacklist")) { /* Blacklist */ while ((keyval = bbs_config_section_walk(section, keyval))) { key = bbs_keyval_key(keyval); if (!stringlist_contains(&blacklist, key)) { @@ -2737,6 +2762,13 @@ static int load_config(void) val = bbs_keyval_val(keyval); add_authorized_relay(key, val); } + } else if (!strcmp(bbs_config_section_name(section), "trusted_relays")) { + while ((keyval = bbs_config_section_walk(section, keyval))) { + key = bbs_keyval_key(keyval); + if (!stringlist_contains(&trusted_relays, key)) { + stringlist_push(&trusted_relays, key); + } + } } else if (!strcmp(bbs_config_section_name(section), "starttls_exempt")) { while ((keyval = bbs_config_section_walk(section, keyval))) { key = bbs_keyval_key(keyval); @@ -2745,7 +2777,7 @@ static int load_config(void) stringlist_push(&starttls_exempt, key); } } - } else if (strcasecmp(bbs_config_section_name(section), "general")) { + } else if (!VALID_SECT_NAME(bbs_config_section_name(section))) { bbs_warning("Invalid section name '%s'\n", bbs_config_section_name(section)); } } @@ -2822,6 +2854,7 @@ static int unload_module(void) } stringlist_empty(&blacklist); RWLIST_WRLOCK_REMOVE_ALL(&authorized_relays, entry, relay_free); + stringlist_empty(&trusted_relays); stringlist_empty(&starttls_exempt); if (!RWLIST_EMPTY(&filters)) { bbs_error("Filter(s) still registered at unload?\n");