Skip to content

Commit 19702ca

Browse files
committed
net_smtp: Allow 5xx errors and bounces to be inhibited per-recipient.
If sender A sends an email to mail service B, where a user has configured forwarding to a secondary account at mail service C, if server C rejects the message for whatever reason (e.g. explicit reject, DMARC failure, etc.), a bounce will go back to the original sender, A. This is typically because C rejects the message at transaction time, in which case B receives the rejection while trying to forward, and in turn triggers a bounce to A; however, even if we reject after transaction, if B did not use SRS, the original return path would be unmodified and thus the bounce would go back to A. This may be undesirable for several reasons. It causes confusion to the original sender, since the message may well have been delivered successfully to B, yet she receives a bounce nonetheless. More importantly, this kind of indirect bounce, while "normal" in SMTP, may leak sensitive information, such as the address at C to which messages are being forwarded, or even the fact that messages are being forwarded from B in the first place. None of this is the sender's business. In the case where we are acting as server C, we can take action to prevent this behavior by allowing bounces to be inhibited towards the original sender. Assuming a unique email address is used for forwarding messages from B --> C, we can prevent 5xx errors or bounces for transactions involving these addresses, ensuring that as far as B is concerned, the message was delivered successfully, even if it was not (users could choose to manually send the bounce to the mailbox at B, which should not be forwarded back to C again because it's a DSN; however, the bounce could also be sent to any other arbitrary mailbox). A crash fix is also included; inside expand_and_deliver, we were previously passing a pointer rather than the address of the pointer, resulting in a crash when we tried setting it. In all other places where we call smtp_run_delivery_callbacks, we have a void** we pass in; the variable is stack-allocated at this call site (void*), so we need to pass its pointer. This wasn't flagged by the compiler as gcc won't (and shouldn't) complain if you try passing a void* to a function expecting a void**. LBBS-137 #close
1 parent 0edbe0f commit 19702ca

7 files changed

Lines changed: 289 additions & 31 deletions

File tree

configs/net_smtp.conf

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,24 @@ loglevel=5 ; Log level from 0 to 10 (maximum debug). Default is 5.
199199
;
200200
;john = john@example.com,*@john.example.net ; Allow local user 'john' to submit outgoing mail additionally using john@example.com or *@john.example.net
201201
;jane = * ; Allow local user 'jane' to submit outgoing mail using ANY identity (DANGEROUS!)
202+
203+
[bounce_redirects] ; Inhibit 5xx replies and bounces for messages to specified recipients.
204+
; This is intended for use with email addresses that are the target of forwarding/redirects from other mail servers,
205+
; i.e. the sender A sends a message to a recipient at mail service B, which forwards messages to C (this server).
206+
; If a permanent failure occurs, normally we will ideally respond with a 5xx SMTP error code at transaction time, or potentially send our own bounce message.
207+
; However, this can be confusing to sender A, because in the case of a message rejected during the B --> C transaction, B will
208+
; generate a bounce back to sender A. This is misleading and problematic, for two reasons:
209+
; 1. Sender A sent a message to B, which may well have successfully received the message in user B's inbox there. It's only the additional forwarded copy that failed.
210+
; 2. This may leak sensitive information, i.e. the fact that messages are being forwarded from B to C, including the forwarding address.
211+
;
212+
; Even if we do not reply 5xx during the transaction, if we later send a bounce and B did not use SRS (Sender Rewriting Scheme), the bounce
213+
; would still end up going to the original sender if the MAIL FROM sender was unaltered, so this may also lead to undesired bounces going to A.
214+
;
215+
; For these reasons, the recipient at B may wish to prevent bounces of this type from going to the sender in the first place.
216+
; We can do this by ensuring we always reply 250 OK during the transaction, and send a copy of the bounce to a different address, if desired.
217+
218+
; For example, suppose John Smith forwards his messages from jsmith@example.net to forwarded-jsmith@example.com (a unique address only used for forwarding purposes).
219+
; This rule will ensure that we do not cause senders to receive bounces for messages (directly or indirectly) for such forwarded messages,
220+
; and instead will send a copy to some other recipient (this could be any arbitrary address, including the forwarding mailbox, though take care not to create a loop.)
221+
222+
;forwarded-jsmith@example.com = forwardedfail@example.com

nets/net_smtp.c

Lines changed: 207 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,15 @@ struct smtp_authorized_identity {
269269

270270
static RWLIST_HEAD_STATIC(authorized_identities, smtp_authorized_identity);
271271

272+
struct bounce_redirect {
273+
const char *target;
274+
const char *bounceaddr;
275+
RWLIST_ENTRY(bounce_redirect) entry;
276+
char data[];
277+
};
278+
279+
static RWLIST_HEAD_STATIC(bounce_redirects, bounce_redirect);
280+
272281
static void add_authorized_relay(const char *source, const char *domains)
273282
{
274283
struct smtp_relay_host *h;
@@ -326,6 +335,28 @@ static void add_authorized_identity(const char *username, const char *identities
326335
RWLIST_INSERT_HEAD(&authorized_identities, i, entry);
327336
}
328337

338+
static void add_bounce_redirect(const char *target, const char *bounceaddr)
339+
{
340+
struct bounce_redirect *r;
341+
size_t targetlen = strlen(target);
342+
343+
/* Add 2 extra for <> around target (in addition to 2 for the NUL terminators) */
344+
r = calloc(1, sizeof(*r) + targetlen + 2 + strlen(bounceaddr) + 2);
345+
if (ALLOC_FAILURE(r)) {
346+
return;
347+
}
348+
349+
/* We include <> for the target because when we do comparisons with it later,
350+
* the string with which we compare has the <>, so include it here too. */
351+
sprintf(r->data, "<%s>", target); /* Safe */
352+
353+
r->target = r->data;
354+
strcpy(r->data + targetlen + 3, bounceaddr); /* Safe */
355+
r->bounceaddr = r->data + targetlen + 3;
356+
357+
RWLIST_INSERT_HEAD(&bounce_redirects, r, entry);
358+
}
359+
329360
/*!
330361
* \brief Whether this client is an authorized relay
331362
* \param srcip Source IP of connection
@@ -1841,7 +1872,7 @@ void smtp_run_filters(struct smtp_filter_data *fdata, enum smtp_direction dir)
18411872
bbs_module_unref(f->mod, 2);
18421873
lseek(fdata->inputfd, 0, SEEK_SET); /* Rewind to beginning of file */
18431874
if (res == 1) {
1844-
bbs_debug(5, "Aborting execution of filter %s\n", f->name);
1875+
bbs_debug(5, "Aborting filter execution after filter %s\n", f->name);
18451876
break;
18461877
} else if (res < 0) {
18471878
bbs_warning("%s SMTP filter %s %s failed to execute\n", smtp_filter_direction_name(f->direction), smtp_filter_type_name(f->type), f->name);
@@ -2206,6 +2237,8 @@ int smtp_dsn(struct smtp_session_info *sinfo, struct tm *arrival, const char *se
22062237
size_t length;
22072238
time_t t = time(NULL);
22082239

2240+
bbs_assert(n > 0);
2241+
22092242
if (!any_failures(f, n) && !sinfo->msa) {
22102243
/* If this is a failure, we should always dispatch a DSN.
22112244
* However, for other types of notifications, such as delays,
@@ -2214,7 +2247,7 @@ int smtp_dsn(struct smtp_session_info *sinfo, struct tm *arrival, const char *se
22142247
* unless they have specifically requested it using the DSN extension. */
22152248
/*! \todo Once DSN support is fully added, also factor that into this check.
22162249
* \todo For forwarded messages, we should not dispatch DSNs for ANY reason. See RFC 3464 4.2 */
2217-
bbs_debug(2, "Skipping DSN for this transaction\n");
2250+
bbs_debug(2, "Skipping DSN for this transaction (%d original recipient%s)\n", n, ESS(n));
22182251
return 0;
22192252
}
22202253

@@ -2353,11 +2386,6 @@ int smtp_dsn(struct smtp_session_info *sinfo, struct tm *arrival, const char *se
23532386
fprintf(fp, "\r\n");
23542387
fflush(fp);
23552388

2356-
/*! \todo We should send at least the headers, even if we are not attaching the full original message.
2357-
* (Otherwise, it's hard to identify what message this was).
2358-
* The DSN extension also allows this to be controlled, so the full message or just headers should just be a default.
2359-
* Also, if we just include headers, message/rfc822 might not be the best Content-Type, figure out what we should use instead... */
2360-
23612389
bbs_copy_file(srcfd, fileno(fp), 0, (int) msglen);
23622390
fseek(fp, 0, SEEK_END);
23632391
}
@@ -2418,6 +2446,98 @@ int smtp_message_quarantinable(struct smtp_session *smtp)
24182446
return smtp->tflags.quarantine;
24192447
}
24202448

2449+
/*! \note Must be called locked */
2450+
static const char *get_bounce_redirect_address(const char *recipient)
2451+
{
2452+
struct bounce_redirect *r;
2453+
2454+
RWLIST_TRAVERSE(&bounce_redirects, r, entry) {
2455+
if (!strcasecmp(recipient, r->target)) {
2456+
return r->bounceaddr;
2457+
}
2458+
}
2459+
return NULL;
2460+
}
2461+
2462+
/*! \note Must be called locked */
2463+
static int dont_bounce_to_sender(struct smtp_session *smtp)
2464+
{
2465+
const char *recipient;
2466+
struct stringitem *i = NULL;
2467+
2468+
/* If any of the recipients of this transaction are marked as don't bounce to sender,
2469+
* then we cannot reply with a 5xx error code during the transaction. */
2470+
while ((recipient = stringlist_next(&smtp->recipients, &i))) {
2471+
if (get_bounce_redirect_address(recipient)) {
2472+
return 1;
2473+
}
2474+
}
2475+
return 0;
2476+
}
2477+
2478+
/*!
2479+
* \brief Same as dont_bounce_to_sender, but iterate over a smtp_delivery_outcome struct instead
2480+
* \note Must be called locked
2481+
*/
2482+
static int dont_bounce_to_sender_outcome(struct smtp_delivery_outcome **f, int n)
2483+
{
2484+
int i;
2485+
for (i = 0; i < n; i++) {
2486+
if (get_bounce_redirect_address(f[i]->recipient)) {
2487+
return 1;
2488+
}
2489+
}
2490+
return 0;
2491+
}
2492+
2493+
static void process_bounces(struct smtp_session *smtp, int srcfd, size_t datalen, struct smtp_delivery_outcome **bounces, int numbounces)
2494+
{
2495+
struct tm tm;
2496+
struct smtp_session_info sinfo;
2497+
2498+
memset(&sinfo, 0, sizeof(sinfo));
2499+
if (!strlen_zero(smtp->helohost)) {
2500+
safe_strncpy(sinfo.helohost, smtp->helohost, sizeof(sinfo.helohost));
2501+
}
2502+
lseek(srcfd, 0, SEEK_SET);
2503+
localtime_r(&smtp->tflags.received, &tm);
2504+
2505+
RWLIST_RDLOCK(&bounce_redirects);
2506+
if (dont_bounce_to_sender_outcome(bounces, numbounces)) {
2507+
int i;
2508+
/* Uncommon path: at least one recipient was a nobounce address.
2509+
*
2510+
* Because smtp_dsn will batch all recipients together in the report,
2511+
* this means we need to send a separate report for each recipient,
2512+
* so we can alter the bounce address for those cases.
2513+
*
2514+
* (Technically, we could send all of them except for the nobounce ones
2515+
* together, but that would require making new arrays for both,
2516+
* and for such an uncommon path, it's probably not worth it.)
2517+
*/
2518+
if (strlen_zero(smtp->from)) {
2519+
/* If the return path was empty, then we must not autorespond.
2520+
* smtp_dsn normally handles this, but since we may overwrite
2521+
* the sender with the alternate bounce address to use instead,
2522+
* that won't get handled there in that case, so we check now. */
2523+
bbs_debug(1, "Refusing to dispatch DSN report; return path was empty\n");
2524+
} else {
2525+
for (i = 0; i < numbounces; i++) {
2526+
const char *bounceaddress = get_bounce_redirect_address(bounces[i]->recipient);
2527+
if (bounceaddress) {
2528+
bbs_debug(3, "Diverting bounce intended for %s -> %s\n", smtp->from, bounceaddress);
2529+
}
2530+
smtp_dsn(&sinfo, &tm, bounceaddress ? bounceaddress : smtp->from, srcfd, datalen, &bounces[i], 1);
2531+
}
2532+
}
2533+
RWLIST_UNLOCK(&bounce_redirects);
2534+
} else {
2535+
/* Common path */
2536+
RWLIST_UNLOCK(&bounce_redirects);
2537+
smtp_dsn(&sinfo, &tm, smtp->from, srcfd, datalen, bounces, numbounces);
2538+
}
2539+
}
2540+
24212541
/*! \brief "Stand and deliver" that email! */
24222542
static int expand_and_deliver(struct smtp_session *smtp, const char *filename, size_t datalen)
24232543
{
@@ -2430,6 +2550,7 @@ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, s
24302550
int numbounces = 0;
24312551
struct smtp_response resp, *resp_ptr;
24322552
void *freedata = NULL;
2553+
int no_sender_bounce = 0;
24332554
int res;
24342555

24352556
/* Preserve the actual received time for the Received header, in case filters take a moment to run */
@@ -2441,6 +2562,8 @@ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, s
24412562
return -1;
24422563
}
24432564

2565+
memset(&resp, 0, sizeof(resp));
2566+
24442567
/* Ordering here is important. For local delivery:
24452568
* 1) First, filters are run for the message itself (SMTP_SCOPE_COMBINED). Filters may MODIFY the data, but may do nothing at all.
24462569
* This first stage is where spam filtering should be done to prepend spam-related headers.
@@ -2466,10 +2589,53 @@ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, s
24662589
if (filterdata.reject) {
24672590
/* A filter has indicated that this message should be rejected.
24682591
* XXX Currently, this only happens if a DMARC reject occured, so that is hardcoded here for now. */
2469-
close(srcfd);
2470-
bbs_smtp_log(2, smtp, "Message from <%s> rejected due to policy failure\n", smtp->from);
2471-
smtp_reply(smtp, 550, 5.7.1, "Message rejected due to policy failure");
2592+
resp.code = 550; /* Also hardcoded below */
2593+
resp.subcode = "5.7.1"; /* Also hardcoded below */
2594+
resp.reply = "Message rejected due to policy failure"; /* Also hardcoded below */
2595+
24722596
smtp_filter_data_cleanup(&filterdata);
2597+
2598+
RWLIST_RDLOCK(&bounce_redirects);
2599+
if (dont_bounce_to_sender(smtp)) {
2600+
/* If we can't bounce to sender, then we can't reply 5xx in the transaction itself.
2601+
* Instead, we need to send bounces for every recipient individually.
2602+
* The actual recipient(s) marked don't bounce will have the bounce address modified,
2603+
* and the others will be left as is. */
2604+
2605+
/* This is a condensed version of the while loop below, with only the logic needed for sending bounces to all recipients */
2606+
while ((recipient = stringlist_pop(&smtp->recipients))) {
2607+
char *user, *domain, *dup;
2608+
2609+
if (duplicate_loop_avoidance(smtp, recipient)) {
2610+
continue;
2611+
} else if (*recipient != '<') {
2612+
bbs_warning("Malformed recipient (missing <>): %s\n", recipient);
2613+
}
2614+
2615+
dup = strdup(recipient);
2616+
if (ALLOC_SUCCESS(dup) && !bbs_parse_email_address(dup, NULL, &user, &domain)) {
2617+
char bouncemsg[512];
2618+
struct smtp_delivery_outcome *f;
2619+
snprintf(bouncemsg, sizeof(bouncemsg), "%d%s%s %s", resp.code, " ", resp.subcode, resp.reply);
2620+
bbs_smtp_log(2, smtp, "Delivery failed: <%s> -> %s: %s\n", smtp->from, recipient, bouncemsg);
2621+
f = smtp_delivery_outcome_new(recipient, NULL, NULL, resp.subcode, bouncemsg, "x-unix", "end of DATA", DELIVERY_FAILED, NULL);
2622+
if (ALLOC_SUCCESS(f)) {
2623+
bounces[numbounces++] = f;
2624+
}
2625+
}
2626+
free_if(dup);
2627+
free(recipient);
2628+
}
2629+
smtp_reply(smtp, 250, 2.6.0, "Message accepted (not really)");
2630+
bbs_smtp_log(2, smtp, "Message from <%s> rejected due to policy failure; (responded 250; not bouncing to sender)\n", smtp->from);
2631+
process_bounces(smtp, srcfd, datalen, bounces, numbounces);
2632+
} else {
2633+
bbs_smtp_log(2, smtp, "Message from <%s> rejected due to policy failure\n", smtp->from);
2634+
smtp_reply(smtp, 550, 5.7.1, "%s", resp.reply);
2635+
}
2636+
RWLIST_UNLOCK(&bounce_redirects);
2637+
2638+
close(srcfd);
24732639
return 0; /* Return 0 to inhibit normal failure message, since we already responded */
24742640
} else if (filterdata.quarantine) {
24752641
/* This is kind of a clunky hack.
@@ -2499,11 +2665,9 @@ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, s
24992665
return -1;
25002666
}
25012667

2502-
memset(&resp, 0, sizeof(resp)); /* Just in case there are no recipients? Or the first call to smtp_run_delivery_callbacks here returns nonzero. */
2503-
25042668
/* Also allow message processors to be run once, for all recipients */
25052669
resp_ptr = &resp;
2506-
res = smtp_run_delivery_callbacks(smtp, &mproc, NULL, &resp_ptr, SMTP_DIRECTION_IN, SMTP_SCOPE_COMBINED, NULL, datalen, freedata);
2670+
res = smtp_run_delivery_callbacks(smtp, &mproc, NULL, &resp_ptr, SMTP_DIRECTION_IN, SMTP_SCOPE_COMBINED, NULL, datalen, &freedata);
25072671
RWLIST_RDLOCK(&handlers); /* This is correct. When we goto finalize, the list should be locked, so we want to lock either way. */
25082672
if (res) {
25092673
if (res == 1) {
@@ -2604,8 +2768,15 @@ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, s
26042768
snprintf(bouncemsg, sizeof(bouncemsg), "%d%s%s %s",
26052769
resp.code ? resp.code : 451, resp.subcode ? " " : "", S_OR(resp.subcode, ""), replymsg);
26062770
bbs_smtp_log(2, smtp, "Delivery failed: <%s> -> %s: %s\n", smtp->from, recipient, bouncemsg);
2607-
if (total > 1) {
2771+
2772+
/* Normally, if there is only 1 recipient, we never send a bounce, we always reply during the transaction.
2773+
* However, if this is a nobounce recipient, then we can't do that. */
2774+
RWLIST_RDLOCK(&bounce_redirects);
2775+
if (total > 1 || get_bounce_redirect_address(recipient)) {
26082776
struct smtp_delivery_outcome *f;
2777+
2778+
no_sender_bounce = 1;
2779+
26092780
/* Since there's more than one recipient, we need to send a bounce
26102781
* to the sender. This ensures there is only one SMTP reply at the
26112782
* end of the entire operation. We still send at most 1 nondelivery report here.
@@ -2621,24 +2792,17 @@ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, s
26212792
bounces[numbounces++] = f;
26222793
}
26232794
}
2795+
RWLIST_UNLOCK(&bounce_redirects);
26242796
}
26252797
next:
26262798
free_if(dup);
26272799
free(recipient);
26282800
}
26292801

2630-
if (succeeded && total > succeeded) {
2802+
if ((succeeded && total > succeeded) || no_sender_bounce) {
26312803
/* Delivery to some (but not all) recipients failed. We need to send a bounce.
26322804
* We use the MAIL FROM here, and our MAIL FROM is empty (postmaster). */
2633-
struct tm tm;
2634-
struct smtp_session_info sinfo;
2635-
memset(&sinfo, 0, sizeof(sinfo));
2636-
if (!strlen_zero(smtp->helohost)) {
2637-
safe_strncpy(sinfo.helohost, smtp->helohost, sizeof(sinfo.helohost));
2638-
}
2639-
lseek(srcfd, 0, SEEK_SET);
2640-
localtime_r(&smtp->tflags.received, &tm);
2641-
smtp_dsn(&sinfo, &tm, smtp->from, srcfd, datalen, bounces, numbounces);
2805+
process_bounces(smtp, srcfd, datalen, bounces, numbounces);
26422806
} else {
26432807
/* If delivery to all recipients failed, then we can just reply with an SMTP error code.
26442808
* We'll just use the error code for the last recipient attempted, even though that
@@ -2660,11 +2824,21 @@ static int expand_and_deliver(struct smtp_session *smtp, const char *filename, s
26602824
smtp_reply(smtp, 250, 2.6.0, "Message accepted for delivery");
26612825
} else if (resp.code && !strlen_zero(resp.subcode) && !strlen_zero(resp.reply)) { /* All deliveries failed */
26622826
/* We could also send a bounce in this case, but even easier, just do it in the SMTP transaction */
2663-
bbs_smtp_log(2, smtp, "Message from <%s> rejected in full by custom policy: %d %s %s\n", smtp->from, resp.code, resp.subcode, resp.reply);
2664-
smtp_resp_reply(smtp, resp.code, resp.subcode, resp.reply);
2827+
if (no_sender_bounce) { /* At this point, all recipients have been consumed, so we can't use dont_bounce_to_sender(), we need to check this flag */
2828+
smtp_reply(smtp, 250, 2.6.0, "Message accepted for delivery (not really)");
2829+
bbs_smtp_log(2, smtp, "Message from <%s> rejected in full by custom policy: %d %s %s (responded 250; not bouncing to sender)\n", smtp->from, resp.code, resp.subcode, resp.reply);
2830+
/* resp.code is set, so no need to lie and set succeeded (to avoid default failure reply) */
2831+
} else {
2832+
bbs_smtp_log(2, smtp, "Message from <%s> rejected in full by custom policy: %d %s %s\n", smtp->from, resp.code, resp.subcode, resp.reply);
2833+
smtp_resp_reply(smtp, resp.code, resp.subcode, resp.reply);
2834+
}
26652835
} else {
26662836
/* This is reachable if all deliveries fail and no custom failure code was set */
26672837
bbs_smtp_log(2, smtp, "Message from <%s> failed delivery to %d/%d recipient%s\n", smtp->from, succeeded, total, ESS(total));
2838+
if (no_sender_bounce) {
2839+
smtp_reply(smtp, 250, 2.6.0, "Message accepted for delivery (not really)");
2840+
succeeded = 1; /* Since resp.code isn't set, change the return code to avoid default failure reply */
2841+
}
26682842
}
26692843

26702844
RWLIST_UNLOCK(&handlers); /* Can't unlock while resp might still be used, and it's a RDLOCK, so okay */
@@ -3850,6 +4024,12 @@ static int load_config(void)
38504024
val = bbs_keyval_val(keyval);
38514025
add_authorized_identity(key, val);
38524026
}
4027+
} else if (!strcmp(bbs_config_section_name(section), "bounce_redirects")) {
4028+
while ((keyval = bbs_config_section_walk(section, keyval))) {
4029+
key = bbs_keyval_key(keyval);
4030+
val = bbs_keyval_val(keyval);
4031+
add_bounce_redirect(key, val);
4032+
}
38534033
} else if (!strcmp(bbs_config_section_name(section), "starttls_exempt")) {
38544034
while ((keyval = bbs_config_section_walk(section, keyval))) {
38554035
key = bbs_keyval_key(keyval);
@@ -3962,6 +4142,7 @@ static int unload_module(void)
39624142
stringlist_empty_destroy(&unlisted_ips);
39634143
RWLIST_WRLOCK_REMOVE_ALL(&authorized_relays, entry, relay_free);
39644144
RWLIST_WRLOCK_REMOVE_ALL(&authorized_identities, entry, authorized_identity_free);
4145+
RWLIST_WRLOCK_REMOVE_ALL(&bounce_redirects, entry, free);
39654146
stringlist_empty_destroy(&trusted_relays);
39664147
stringlist_empty_destroy(&starttls_exempt);
39674148
if (!RWLIST_EMPTY(&filters)) {

0 commit comments

Comments
 (0)