diff options
author | Jeremy Harris <jgh146exb@wizmail.org> | 2021-04-18 01:51:28 +0100 |
---|---|---|
committer | Jeremy Harris <jgh146exb@wizmail.org> | 2021-04-18 01:51:28 +0100 |
commit | a1108b5118d32e969c5fe91b2110944f7483a7cb (patch) | |
tree | 3540d45b5de8d0fa2fccac2c6b1903ff69f6a117 /src | |
parent | 4022c6568fa89eb47e0d78f86f7b7cfbcfefcdc4 (diff) |
Experimental: ESMTP LIMITS extension
Diffstat (limited to 'src')
-rw-r--r-- | src/src/config.h.defaults | 1 | ||||
-rw-r--r-- | src/src/dbfn.c | 2 | ||||
-rw-r--r-- | src/src/dbstuff.h | 10 | ||||
-rw-r--r-- | src/src/deliver.c | 3 | ||||
-rw-r--r-- | src/src/exim.c | 16 | ||||
-rw-r--r-- | src/src/functions.h | 7 | ||||
-rw-r--r-- | src/src/globals.c | 12 | ||||
-rw-r--r-- | src/src/globals.h | 12 | ||||
-rw-r--r-- | src/src/readconf.c | 3 | ||||
-rw-r--r-- | src/src/smtp_in.c | 13 | ||||
-rw-r--r-- | src/src/transport.c | 36 | ||||
-rw-r--r-- | src/src/transports/smtp.c | 544 | ||||
-rw-r--r-- | src/src/transports/smtp.h | 17 |
13 files changed, 519 insertions, 157 deletions
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults index 02031b7df..e233fb3e5 100644 --- a/src/src/config.h.defaults +++ b/src/src/config.h.defaults @@ -205,6 +205,7 @@ Do not put spaces between # and the 'define'. #define EXPERIMENTAL_BRIGHTMAIL #define EXPERIMENTAL_DCC #define EXPERIMENTAL_DSN_INFO +#define EXPERIMENTAL_ESMTP_LIMITS #define EXPERIMENTAL_QUEUEFILE #define EXPERIMENTAL_SRS_ALT diff --git a/src/src/dbfn.c b/src/src/dbfn.c index be6a47afc..0f56ad5a6 100644 --- a/src/src/dbfn.c +++ b/src/src/dbfn.c @@ -342,7 +342,7 @@ return yield; /* Read a record. If the length is not as expected then delete it, write -an error log line and return NULL. +an error log line, delete the record and return NULL. Use this for fixed-size records (so not retry or wait records). Arguments: diff --git a/src/src/dbstuff.h b/src/src/dbstuff.h index 5a8441d6a..89934154a 100644 --- a/src/src/dbstuff.h +++ b/src/src/dbstuff.h @@ -792,13 +792,21 @@ typedef struct { #ifndef DISABLE_PIPE_CONNECT /* This structure records the EHLO responses, cleartext and crypted, -for an IP, as bitmasks (cf. OPTION_TLS) */ +for an IP, as bitmasks (cf. OPTION_TLS). For LIMITS, also values +advertised for MAILMAX, RCPTMAX and RCPTDOMAINMAX; zero meaning no +value advertised. */ typedef struct { unsigned short cleartext_features; unsigned short crypted_features; unsigned short cleartext_auths; unsigned short crypted_auths; + +# ifdef EXPERIMENTAL_ESMTP_LIMITS + unsigned int limit_mail; + unsigned int limit_rcpt; + unsigned int limit_rcptdom; +# endif } ehlo_resp_precis; typedef struct { diff --git a/src/src/deliver.c b/src/src/deliver.c index 0cddec758..9ebd281fc 100644 --- a/src/src/deliver.c +++ b/src/src/deliver.c @@ -8570,6 +8570,9 @@ f.tcp_fastopen_ok = TRUE; +/* Called from a commandline, or from the daemon, to do a delivery. +We need to regain privs; do this by exec of the exim binary. */ + void delivery_re_exec(int exec_type) { diff --git a/src/src/exim.c b/src/src/exim.c index 1244aee0b..8526cbbf3 100644 --- a/src/src/exim.c +++ b/src/src/exim.c @@ -1010,6 +1010,9 @@ g = string_cat(NULL, US"Support for:"); #ifdef EXPERIMENTAL_DSN_INFO g = string_cat(g, US" Experimental_DSN_info"); #endif +#ifdef EXPERIMENTAL_ESMTP_LIMITS + g = string_cat(g, US" Experimental_ESMTP_Limits"); +#endif #ifdef EXPERIMENTAL_QUEUEFILE g = string_cat(g, US" Experimental_QUEUEFILE"); #endif @@ -2257,7 +2260,7 @@ on the second character (the one after '-'), to save some effort. */ case 'P': /* -bP config: we need to setup here, because later, - * when list_options is checked, the config is read already */ + when list_options is checked, the config is read already */ if (*argrest) badarg = TRUE; else if (argv[i+1] && Ustrcmp(argv[i+1], "config") == 0) @@ -2788,6 +2791,17 @@ on the second character (the one after '-'), to save some effort. */ case 'K': smtp_peer_options |= OPTION_CHUNKING; break; +#ifdef EXPERIMENTAL_ESMTP_LIMITS + /* -MCL: peer used LIMITS RCPTMAX and/or RCPTDOMAINMAX */ + case 'L': if (++i < argc) continue_limit_mail = Uatoi(argv[i]); + else badarg = TRUE; + if (++i < argc) continue_limit_rcpt = Uatoi(argv[i]); + else badarg = TRUE; + if (++i < argc) continue_limit_rcptdom = Uatoi(argv[i]); + else badarg = TRUE; + break; +#endif + /* -MCP: set the smtp_use_pipelining flag; this is useful only when it preceded -MC (see above) */ diff --git a/src/src/functions.h b/src/src/functions.h index 0f962b313..7d6e3380e 100644 --- a/src/src/functions.h +++ b/src/src/functions.h @@ -593,8 +593,11 @@ extern BOOL transport_check_waiting(const uschar *, const uschar *, int, usch extern void transport_init(void); extern void transport_do_pass_socket(const uschar *, const uschar *, const uschar *, uschar *, int); -extern BOOL transport_pass_socket(const uschar *, const uschar *, const uschar *, uschar *, - int); +extern BOOL transport_pass_socket(const uschar *, const uschar *, const uschar *, uschar *, int +#ifdef EXPERIMENTAL_ESMTP_LIMITS + , unsigned, unsigned, unsigned +#endif + ); extern uschar *transport_rcpt_address(address_item *, BOOL); extern BOOL transport_set_up_command(const uschar ***, uschar *, BOOL, int, address_item *, uschar *, uschar **); diff --git a/src/src/globals.c b/src/src/globals.c index 7ee7c38b5..c45e8a930 100644 --- a/src/src/globals.c +++ b/src/src/globals.c @@ -710,6 +710,10 @@ unsigned chunking_data_left = 0; chunking_state_t chunking_state= CHUNKING_NOT_OFFERED; const pcre *regex_CHUNKING = NULL; +#ifdef EXPERIMENTAL_ESMTP_LIMITS +const pcre *regex_LIMITS = NULL; +#endif + uschar *client_authenticator = NULL; uschar *client_authenticated_id = NULL; uschar *client_authenticated_sender = NULL; @@ -742,6 +746,11 @@ uschar *continue_hostname = NULL; uschar *continue_host_address = NULL; int continue_sequence = 1; uschar *continue_transport = NULL; +#ifdef EXPERIMENTAL_ESMTP_LIMITS +unsigned continue_limit_mail = 0; +unsigned continue_limit_rcpt = 0; +unsigned continue_limit_rcptdom= 0; +#endif uschar *csa_status = NULL; cut_t cutthrough = { @@ -1001,6 +1010,9 @@ uschar *keep_environment = NULL; int keep_malformed = 4*24*60*60; /* 4 days */ uschar *eldap_dn = NULL; +#ifdef EXPERIMENTAL_ESMTP_LIMITS +uschar *limits_advertise_hosts = US"*"; +#endif int load_average = -2; uschar *local_from_prefix = NULL; uschar *local_from_suffix = NULL; diff --git a/src/src/globals.h b/src/src/globals.h index 58bf3c428..ed7cffb76 100644 --- a/src/src/globals.h +++ b/src/src/globals.h @@ -434,6 +434,12 @@ extern uschar *continue_hostname; /* Host for continued delivery */ extern uschar *continue_host_address; /* IP address for ditto */ extern int continue_sequence; /* Sequence num for continued delivery */ extern uschar *continue_transport; /* Transport for continued delivery */ +#ifdef EXPERIMENTAL_ESMTP_LIMITS +extern unsigned continue_limit_mail; /* Peer advertised limit */ +extern unsigned continue_limit_rcpt; +extern unsigned continue_limit_rcptdom; +#endif + extern uschar *csa_status; /* Client SMTP Authorization result */ @@ -652,6 +658,9 @@ extern uschar *keep_environment; /* Whitelist for environment variables */ extern int keep_malformed; /* Time to keep malformed messages */ extern uschar *eldap_dn; /* Where LDAP DNs are left */ +#ifdef EXPERIMENTAL_ESMTP_LIMITS +extern uschar *limits_advertise_hosts; /* for banner/EHLO pipelining */ +#endif extern int load_average; /* Most recently read load average */ extern BOOL local_from_check; /* For adding Sender: (global value) */ extern uschar *local_from_prefix; /* Permitted prefixes */ @@ -854,6 +863,9 @@ extern const pcre *regex_check_dns_names; /* For DNS name checking */ extern const pcre *regex_From; /* For recognizing "From_" lines */ extern const pcre *regex_CHUNKING; /* For recognizing CHUNKING (RFC 3030) */ extern const pcre *regex_IGNOREQUOTA; /* For recognizing IGNOREQUOTA (LMTP) */ +#ifdef EXPERIMENTAL_ESMTP_LIMITS +extern const pcre *regex_LIMITS; /* For recognizing LIMITS */ +#endif extern const pcre *regex_PIPELINING; /* For recognizing PIPELINING */ extern const pcre *regex_SIZE; /* For recognizing SIZE settings */ #ifndef DISABLE_PIPE_CONNECT diff --git a/src/src/readconf.c b/src/src/readconf.c index 0ae3166c3..e8e310beb 100644 --- a/src/src/readconf.c +++ b/src/src/readconf.c @@ -201,6 +201,9 @@ static optionlist optionlist_config[] = { { "ldap_start_tls", opt_bool, {&eldap_start_tls} }, { "ldap_version", opt_int, {&eldap_version} }, #endif +#ifdef EXPERIMENTAL_ESMTP_LIMITS + { "limits_advertise_hosts", opt_stringptr, {&limits_advertise_hosts} }, +#endif { "local_from_check", opt_bool, {&local_from_check} }, { "local_from_prefix", opt_stringptr, {&local_from_prefix} }, { "local_from_suffix", opt_stringptr, {&local_from_suffix} }, diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c index 2bb15b698..d60e7d5c5 100644 --- a/src/src/smtp_in.c +++ b/src/src/smtp_in.c @@ -4346,6 +4346,19 @@ while (done <= 0) g = string_catn(g, US"-SIZE\r\n", 7); } +#ifdef EXPERIMENTAL_ESMTP_LIMITS + if ( (mailmax > 0 || recipients_max) + && verify_check_host(&limits_advertise_hosts) == OK) + { + g = string_fmt_append(g, "%.3s-LIMITS", smtp_code); + if (mailmax > 0) + g = string_fmt_append(g, " MAILMAX=%d", mailmax); + if (recipients_max) + g = string_fmt_append(g, " RCPTMAX=%d", recipients_max); + g = string_catn(g, US"\r\n", 2); + } +#endif + /* Exim does not do protocol conversion or data conversion. It is 8-bit clean; if it has an 8-bit character in its hand, it just sends it. It cannot therefore specify 8BITMIME and remain consistent with the RFCs. diff --git a/src/src/transport.c b/src/src/transport.c index 49a84ccc4..d8fd85855 100644 --- a/src/src/transport.c +++ b/src/src/transport.c @@ -1880,9 +1880,21 @@ void transport_do_pass_socket(const uschar *transport_name, const uschar *hostname, const uschar *hostaddress, uschar *id, int socket_fd) { -int i = 27; +int i = 13; const uschar **argv; +#ifndef DISABLE_TLS +if (smtp_peer_options & OPTION_TLS) i += 6; +#endif +#ifdef EXPERIMENTAL_ESMTP_LIMITS +if (continue_limit_mail || continue_limit_rcpt || continue_limit_rcptdom) + i += 4; +#endif +if (queue_run_pid != (pid_t)0) i += 3; +#ifdef SUPPORT_SOCKS +if (proxy_session) i += 5; +#endif + /* Set up the calling arguments; use the standard function for the basics, but we have a number of extras that may be added. */ @@ -1916,6 +1928,16 @@ if (smtp_peer_options & OPTION_TLS) argv[i++] = US"-MCT"; #endif +#ifdef EXPERIMENTAL_ESMTP_LIMITS +if (continue_limit_rcpt || continue_limit_rcptdom) + { + argv[i++] = US"-MCL"; + argv[i++] = string_sprintf("%u", continue_limit_mail); + argv[i++] = string_sprintf("%u", continue_limit_rcpt); + argv[i++] = string_sprintf("%u", continue_limit_rcptdom); + } +#endif + if (queue_run_pid != (pid_t)0) { argv[i++] = US"-MCQ"; @@ -1976,13 +1998,23 @@ Returns: FALSE if fork fails; TRUE otherwise BOOL transport_pass_socket(const uschar *transport_name, const uschar *hostname, - const uschar *hostaddress, uschar *id, int socket_fd) + const uschar *hostaddress, uschar *id, int socket_fd +#ifdef EXPERIMENTAL_ESMTP_LIMITS + , unsigned peer_limit_mail, unsigned peer_limit_rcpt, unsigned peer_limit_rcptdom +#endif + ) { pid_t pid; int status; DEBUG(D_transport) debug_printf("transport_pass_socket entered\n"); +#ifdef EXPERIMENTAL_ESMTP_LIMITS +continue_limit_mail = peer_limit_mail; +continue_limit_rcpt = peer_limit_rcpt; +continue_limit_rcptdom = peer_limit_rcptdom; +#endif + if ((pid = exim_fork(US"continued-transport-interproc")) == 0) { /* Disconnect entirely from the parent process. If we are running in the diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c index 2a2928c46..52940572b 100644 --- a/src/src/transports/smtp.c +++ b/src/src/transports/smtp.c @@ -274,6 +274,11 @@ if (!regex_IGNOREQUOTA) regex_IGNOREQUOTA = if (!regex_EARLY_PIPE) regex_EARLY_PIPE = regex_must_compile(US"\\n250[\\s\\-]" EARLY_PIPE_FEATURE_NAME "(\\s|\\n|$)", FALSE, TRUE); #endif + +#ifdef EXPERIMENTAL_ESMTP_LIMITS +if (!regex_LIMITS) regex_LIMITS = + regex_must_compile(US"\\n250[\\s\\-]LIMITS\\s", FALSE, TRUE); +#endif } @@ -756,6 +761,82 @@ return TRUE; } +/******************************************************************************/ + +#ifdef EXPERIMENTAL_ESMTP_LIMITS +/* If TLS, or TLS not offered, called with the EHLO response in the buffer. +Check it for a LIMITS keyword and parse values into the smtp context structure. + +We don't bother with peers that we won't talk TLS to, even though they can, +just ignore their LIMITS advice (if any) and treat them as if they do not. +This saves us dealing with a duplicate set of values. */ + +static void +ehlo_response_limits_read(smtp_context * sx) +{ +int ovec[3]; /* results vector for a main-match only */ + +/* matches up to just after the first space after the keyword */ + +if (pcre_exec(regex_LIMITS, NULL, CS sx->buffer, Ustrlen(sx->buffer), + 0, PCRE_EOPT, ovec, nelem(ovec)) >= 0) + for (const uschar * s = sx->buffer + ovec[1]; *s; ) + { + while (isspace(*s)) s++; + if (*s == '\n') break; + + if (strncmpic(s, US"MAILMAX=", 8) == 0) + { + sx->peer_limit_mail = atoi(CS (s += 8)); + while (isdigit(*s)) s++; + } + else if (strncmpic(s, US"RCPTMAX=", 8) == 0) + { + sx->peer_limit_rcpt = atoi(CS (s += 8)); + while (isdigit(*s)) s++; + } + else if (strncmpic(s, US"RCPTDOMAINMAX=", 14) == 0) + { + sx->peer_limit_rcptdom = atoi(CS (s += 14)); + while (isdigit(*s)) s++; + } + else + while (*s && !isspace(*s)) s++; + } +} + +/* Apply given values to the current connection */ +static void +ehlo_limits_apply(smtp_context * sx, + unsigned limit_mail, unsigned limit_rcpt, unsigned limit_rcptdom) +{ +if (limit_mail && limit_mail < sx->max_mail) sx->max_mail = limit_mail; +if (limit_rcpt && limit_rcpt < sx->max_rcpt) sx->max_rcpt = limit_rcpt; +if (limit_rcptdom) + { + DEBUG(D_transport) debug_printf("will treat as !multi_domain\n"); + sx->single_rcpt_domain = TRUE; + } +} + +/* Apply values from EHLO-resp to the current connection */ +static void +ehlo_response_limits_apply(smtp_context * sx) +{ +ehlo_limits_apply(sx, sx->peer_limit_mail, sx->peer_limit_rcpt, + sx->peer_limit_rcptdom); +} + +/* Apply values read from cache to the current connection */ +static void +ehlo_cache_limits_apply(smtp_context * sx) +{ +ehlo_limits_apply(sx, sx->ehlo_resp.limit_mail, sx->ehlo_resp.limit_rcpt, + sx->ehlo_resp.limit_rcptdom); +} +#endif + +/******************************************************************************/ #ifndef DISABLE_PIPE_CONNECT static uschar * @@ -769,19 +850,45 @@ return Ustrchr(host->address, ':') host->port == PORT_NONE ? sx->port : host->port); } +/* Cache EHLO-response info for use by early-pipe. +Called +- During a normal flow on EHLO response (either cleartext or under TLS), + when we are willing to do PIPE_CONNECT and it is offered +- During an early-pipe flow on receiving the actual EHLO response and noting + disparity versus the cached info used, when PIPE_CONNECT is still being offered + +We assume that suitable values have been set in the sx.ehlo_resp structure for +features and auths; we handle the copy of limits. */ + static void -write_ehlo_cache_entry(const smtp_context * sx) +write_ehlo_cache_entry(smtp_context * sx) { open_db dbblock, * dbm_file; +#ifdef EXPERIMENTAL_ESMTP_LIMITS +sx->ehlo_resp.limit_mail = sx->peer_limit_mail; +sx->ehlo_resp.limit_rcpt = sx->peer_limit_rcpt; +sx->ehlo_resp.limit_rcptdom = sx->peer_limit_rcptdom; +#endif + if ((dbm_file = dbfn_open(US"misc", O_RDWR, &dbblock, TRUE, TRUE))) { uschar * ehlo_resp_key = ehlo_cache_key(sx); dbdata_ehlo_resp er = { .data = sx->ehlo_resp }; - HDEBUG(D_transport) debug_printf("writing clr %04x/%04x cry %04x/%04x\n", - sx->ehlo_resp.cleartext_features, sx->ehlo_resp.cleartext_auths, - sx->ehlo_resp.crypted_features, sx->ehlo_resp.crypted_auths); + HDEBUG(D_transport) +#ifdef EXPERIMENTAL_ESMTP_LIMITS + if (sx->ehlo_resp.limit_mail || sx->ehlo_resp.limit_rcpt || sx->ehlo_resp.limit_rcptdom) + debug_printf("writing clr %04x/%04x cry %04x/%04x lim %05d/%05d/%05d\n", + sx->ehlo_resp.cleartext_features, sx->ehlo_resp.cleartext_auths, + sx->ehlo_resp.crypted_features, sx->ehlo_resp.crypted_auths, + sx->ehlo_resp.limit_mail, sx->ehlo_resp.limit_rcpt, + sx->ehlo_resp.limit_rcptdom); + else +#endif + debug_printf("writing clr %04x/%04x cry %04x/%04x\n", + sx->ehlo_resp.cleartext_features, sx->ehlo_resp.cleartext_auths, + sx->ehlo_resp.crypted_features, sx->ehlo_resp.crypted_auths); dbfn_write(dbm_file, ehlo_resp_key, &er, (int)sizeof(er)); dbfn_close(dbm_file); @@ -826,12 +933,26 @@ else } else { + DEBUG(D_transport) +#ifdef EXPERIMENTAL_ESMTP_LIMITS + if (er->data.limit_mail || er->data.limit_rcpt || er->data.limit_rcptdom) + debug_printf("EHLO response bits from cache:" + " cleartext 0x%04x/0x%04x crypted 0x%04x/0x%04x lim %05d/%05d/%05d\n", + er->data.cleartext_features, er->data.cleartext_auths, + er->data.crypted_features, er->data.crypted_auths, + er->data.limit_mail, er->data.limit_rcpt, er->data.limit_rcptdom); + else +#endif + debug_printf("EHLO response bits from cache:" + " cleartext 0x%04x/0x%04x crypted 0x%04x/0x%04x\n", + er->data.cleartext_features, er->data.cleartext_auths, + er->data.crypted_features, er->data.crypted_auths); + sx->ehlo_resp = er->data; +#ifdef EXPERIMENTAL_ESMTP_LIMITS + ehlo_cache_limits_apply(sx); +#endif dbfn_close(dbm_file); - DEBUG(D_transport) debug_printf( - "EHLO response bits from cache: cleartext 0x%04x/0x%04x crypted 0x%04x/0x%04x\n", - er->data.cleartext_features, er->data.cleartext_auths, - er->data.crypted_features, er->data.crypted_auths); return TRUE; } dbfn_close(dbm_file); @@ -933,8 +1054,9 @@ if (pending_EHLO) goto fail; } - /* Compare the actual EHLO response to the cached value we assumed; - on difference, dump or rewrite the cache and arrange for a retry. */ + /* Compare the actual EHLO response extensions and AUTH methods to the cached + value we assumed; on difference, dump or rewrite the cache and arrange for a + retry. */ ap = tls_out.active.sock < 0 ? &sx->ehlo_resp.cleartext_auths : &sx->ehlo_resp.crypted_auths; @@ -944,6 +1066,10 @@ if (pending_EHLO) | OPTION_CHUNKING | OPTION_PRDR | OPTION_DSN | OPTION_PIPE | OPTION_SIZE | OPTION_UTF8 | OPTION_EARLY_PIPE ); +#ifdef EXPERIMENTAL_ESMTP_LIMITS + if (tls_out.active.sock >= 0 || !(peer_offered & OPTION_TLS)) + ehlo_response_limits_read(sx); +#endif if ( peer_offered != sx->peer_offered || (authbits = study_ehlo_auths(sx)) != *ap) { @@ -951,16 +1077,44 @@ if (pending_EHLO) debug_printf("EHLO %s extensions changed, 0x%04x/0x%04x -> 0x%04x/0x%04x\n", tls_out.active.sock < 0 ? "cleartext" : "crypted", sx->peer_offered, *ap, peer_offered, authbits); - *(tls_out.active.sock < 0 - ? &sx->ehlo_resp.cleartext_features : &sx->ehlo_resp.crypted_features) = peer_offered; - *ap = authbits; if (peer_offered & OPTION_EARLY_PIPE) + { + *(tls_out.active.sock < 0 + ? &sx->ehlo_resp.cleartext_features : &sx->ehlo_resp.crypted_features) = + peer_offered; + *ap = authbits; write_ehlo_cache_entry(sx); + } else invalidate_ehlo_cache_entry(sx); return OK; /* just carry on */ } +#ifdef EXPERIMENTAL_ESMTP_LIMITS + /* If we are handling LIMITS, compare the actual EHLO LIMITS values with the + cached values and invalidate cache if different. OK to carry on with + connect since values are advisory. */ + { + if ( (tls_out.active.sock >= 0 || !(peer_offered & OPTION_TLS)) + && ( sx->peer_limit_mail != sx->ehlo_resp.limit_mail + || sx->peer_limit_rcpt != sx->ehlo_resp.limit_rcpt + || sx->peer_limit_rcptdom != sx->ehlo_resp.limit_rcptdom + ) ) + { + HDEBUG(D_transport) + { + debug_printf("EHLO LIMITS changed:"); + if (sx->peer_limit_mail != sx->ehlo_resp.limit_mail) + debug_printf(" MAILMAX %u -> %u\n", sx->ehlo_resp.limit_mail, sx->peer_limit_mail); + else if (sx->peer_limit_rcpt != sx->ehlo_resp.limit_rcpt) + debug_printf(" RCPTMAX %u -> %u\n", sx->ehlo_resp.limit_rcpt, sx->peer_limit_rcpt); + else + debug_printf(" RCPTDOMAINMAX %u -> %u\n", sx->ehlo_resp.limit_rcptdom, sx->peer_limit_rcptdom); + } + invalidate_ehlo_cache_entry(sx); + } + } +#endif } return OK; @@ -1882,7 +2036,8 @@ sx->dane_required = /* sx->pending_BANNER = sx->pending_EHLO = sx->pending_MAIL = FALSE; */ #endif -if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0) sx->max_rcpt = 999999; +if ((sx->max_mail = sx->conn_args.tblock->connection_max_messages) == 0) sx->max_mail = 999999; +if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0) sx->max_rcpt = 999999; /* sx->peer_offered = 0; */ /* sx->avoid_option = 0; */ sx->igquotstr = US""; @@ -2083,6 +2238,9 @@ if (!continue_hostname) sx->cctx.tls_ctx = NULL; sx->inblock.cctx = sx->outblock.cctx = &sx->cctx; +#ifdef EXPERIMENTAL_ESMTP_LIMITS + sx->peer_limit_mail = sx->peer_limit_rcpt = sx->peer_limit_rcptdom = +#endif sx->avoid_option = sx->peer_offered = smtp_peer_options = 0; #ifndef DISABLE_PIPE_CONNECT @@ -2361,6 +2519,13 @@ goto SEND_QUIT; ) #endif ); +#ifdef EXPERIMENTAL_ESMTP_LIMITS + if (tls_out.active.sock >= 0 || !(sx->peer_offered & OPTION_TLS)) + { + ehlo_response_limits_read(sx); + ehlo_response_limits_apply(sx); + } +#endif #ifndef DISABLE_PIPE_CONNECT if (sx->early_pipe_ok) { @@ -2415,6 +2580,13 @@ else sx->inblock.cctx = sx->outblock.cctx = &sx->cctx; smtp_command = big_buffer; sx->peer_offered = smtp_peer_options; +#ifdef EXPERIMENTAL_ESMTP_LIMITS + /* Limits passed by cmdline over exec. */ + ehlo_limits_apply(sx, + sx->peer_limit_mail = continue_limit_mail, + sx->peer_limit_rcpt = continue_limit_rcpt, + sx->peer_limit_rcptdom = continue_limit_rcptdom); +#endif sx->helo_data = NULL; /* ensure we re-expand ob->helo_data */ /* For a continued connection with TLS being proxied for us, or a @@ -2456,7 +2628,8 @@ if ( smtp_peer_options & OPTION_TLS #ifndef DISABLE_PIPE_CONNECT /* If doing early-pipelining reap the banner and EHLO-response but leave - the response for the STARTTLS we just sent alone. */ + the response for the STARTTLS we just sent alone. On fail, assume wrong + cached capability and retry with the pipelining disabled. */ if (sx->early_pipe_active && sync_responses(sx, 2, 0) != 0) { @@ -2592,6 +2765,13 @@ if (tls_out.active.sock >= 0) DEBUG(D_transport) debug_printf("Using cached crypted PIPE_CONNECT\n"); } #endif +#ifdef EXPERIMMENTAL_ESMTP_LIMITS + /* As we are about to send another EHLO, forget any LIMITS received so far. */ + sx->peer_limit_mail = sx->peer_limit_rcpt = sx->peer_limit_rcptdom = 0; + if ((sx->max_mail = sx->conn_args.tblock->connection_max_message) == 0) sx->max_mail = 999999; + if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0) sx->max_rcpt = 999999; + sx->single_rcpt_domain = FALSE; +#endif /* For SMTPS we need to wait for the initial OK response. */ if (sx->smtps) @@ -2721,6 +2901,14 @@ if ( !continue_hostname if (tls_out.active.sock >= 0) sx->ehlo_resp.crypted_features = sx->peer_offered; #endif + +#ifdef EXPERIMENTAL_ESMTP_LIMITS + if (tls_out.active.sock >= 0 || !(sx->peer_offered & OPTION_TLS)) + { + ehlo_response_limits_read(sx); + ehlo_response_limits_apply(sx); + } +#endif } /* Set for IGNOREQUOTA if the response to LHLO specifies support and the @@ -3054,7 +3242,7 @@ if ( sx->peer_offered & OPTION_UTF8 /* check if all addresses have DSN-lasthop flag; do not send RET and ENVID if so */ for (sx->dsn_all_lasthop = TRUE, addr = addrlist, address_count = 0; - addr && address_count < sx->max_rcpt; + addr && address_count < sx->max_rcpt; /*XXX maybe also || sx->single_rcpt_domain ? */ addr = addr->next) if (addr->transport_return == PENDING_DEFER) { address_count++; @@ -3142,6 +3330,9 @@ int smtp_write_mail_and_rcpt_cmds(smtp_context * sx, int * yield) { address_item * addr; +#ifdef EXPERIMENTAL_ESMTP_LIMITS +address_item * restart_addr = NULL; +#endif int address_count, pipe_limit; int rc; @@ -3233,6 +3424,24 @@ for (addr = sx->first_addr, address_count = 0, pipe_limit = 100; BOOL no_flush; uschar * rcpt_addr; +#ifdef EXPERIMENTAL_ESMTP_LIMITS + if ( sx->single_rcpt_domain /* restriction on domains */ + && address_count > 0 /* not first being sent */ + && Ustrcmp(addr->domain, sx->first_addr->domain) != 0 /* dom diff from first */ + ) + { + DEBUG(D_transport) debug_printf("skipping different domain %s\n", addr->domain); + + /* Ensure the smtp-response reaper does not think the address had a RCPT + command sent for it. Reset to PENDING_DEFER in smtp_deliver(), where we + goto SEND_MESSAGE. */ + + addr->transport_return = SKIP; + if (!restart_addr) restart_addr = addr; /* note restart point */ + continue; /* skip this one */ + } +#endif + addr->dsn_aware = sx->peer_offered & OPTION_DSN ? dsn_support_yes : dsn_support_no; @@ -3304,7 +3513,11 @@ for (addr = sx->first_addr, address_count = 0, pipe_limit = 100; } } /* Loop for next address */ +#ifdef EXPERIMENTAL_ESMTP_LIMITS +sx->next_addr = restart_addr ? restart_addr : addr; +#else sx->next_addr = addr; +#endif return 0; } @@ -3493,10 +3706,13 @@ int yield = OK; int save_errno; int rc; -BOOL pass_message = FALSE; uschar *message = NULL; uschar new_message_id[MESSAGE_ID_LENGTH + 1]; smtp_context * sx = store_get(sizeof(*sx), TRUE); /* tainted, for the data buffers */ +BOOL pass_message = FALSE; +#ifdef EXPERIMENTAL_ESMTP_LIMITS +BOOL mail_limit = FALSE; +#endif #ifdef SUPPORT_DANE BOOL dane_held; #endif @@ -3516,8 +3732,8 @@ sx->conn_args.tblock = tblock; gettimeofday(&sx->delivery_start, NULL); sx->sync_addr = sx->first_addr = addrlist; +REPEAT_CONN: #ifdef SUPPORT_DANE -DANE_DOMAINS: dane_held = FALSE; #endif @@ -4090,7 +4306,7 @@ else else sprintf(CS sx->buffer, "%.500s\n", addr->unique); - DEBUG(D_deliver) debug_printf("S:journalling %s\n", sx->buffer); + DEBUG(D_deliver) debug_printf("S:journalling %s", sx->buffer); len = Ustrlen(CS sx->buffer); if (write(journal_fd, sx->buffer, len) != len) log_write(0, LOG_MAIN|LOG_PANIC, "failed to write journal for " @@ -4335,81 +4551,95 @@ DEBUG(D_transport) sx->send_rset, f.continue_more, yield, sx->first_addr ? "not " : ""); if (sx->completed_addr && sx->ok && sx->send_quit) - { - smtp_compare_t t_compare = - {.tblock = tblock, .current_sender_address = sender_address}; +#ifdef EXPERIMENTAL_ESMTP_LIMITS + if (mail_limit = continue_sequence >= sx->max_mail) + { + DEBUG(D_transport) + debug_printf("reached limit %u for MAILs per conn\n", sx->max_mail); + } + else +#endif + { + smtp_compare_t t_compare = + {.tblock = tblock, .current_sender_address = sender_address}; - if ( sx->first_addr /* more addrs for this message */ - || f.continue_more /* more addrs for continued-host */ - || tcw_done && tcw /* more messages for host */ - || ( + if ( sx->first_addr /* more addrs for this message */ + || f.continue_more /* more addrs for continued-host */ + || tcw_done && tcw /* more messages for host */ + || ( #ifndef DISABLE_TLS - ( tls_out.active.sock < 0 && !continue_proxy_cipher - || verify_check_given_host(CUSS &ob->hosts_nopass_tls, host) != OK - ) - && + ( tls_out.active.sock < 0 && !continue_proxy_cipher + || verify_check_given_host(CUSS &ob->hosts_nopass_tls, host) != OK + ) + && #endif - transport_check_waiting(tblock->name, host->name, - tblock->connection_max_messages, new_message_id, - (oicf)smtp_are_same_identities, (void*)&t_compare) - ) ) - { - uschar *msg; - BOOL pass_message; + transport_check_waiting(tblock->name, host->name, + sx->max_mail, new_message_id, + (oicf)smtp_are_same_identities, (void*)&t_compare) + ) ) + { + uschar *msg; + BOOL pass_message; - if (sx->send_rset) - if (! (sx->ok = smtp_write_command(sx, SCMD_FLUSH, "RSET\r\n") >= 0)) - { - msg = US string_sprintf("send() to %s [%s] failed: %s", host->name, - host->address, strerror(errno)); - sx->send_quit = FALSE; - } - else if (! (sx->ok = smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), - '2', ob->command_timeout))) - { - int code; - sx->send_quit = check_response(host, &errno, 0, sx->buffer, &code, &msg, - &pass_message); - if (!sx->send_quit) - { - DEBUG(D_transport) debug_printf("H=%s [%s] %s\n", - host->name, host->address, msg); - } - } + if (sx->send_rset) + if (! (sx->ok = smtp_write_command(sx, SCMD_FLUSH, "RSET\r\n") >= 0)) + { + msg = US string_sprintf("send() to %s [%s] failed: %s", host->name, + host->address, strerror(errno)); + sx->send_quit = FALSE; + } + else if (! (sx->ok = smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), + '2', ob->command_timeout))) + { + int code; + sx->send_quit = check_response(host, &errno, 0, sx->buffer, &code, &msg, + &pass_message); + if (!sx->send_quit) + { + DEBUG(D_transport) debug_printf("H=%s [%s] %s\n", + host->name, host->address, msg); + } + } - /* Either RSET was not needed, or it succeeded */ + /* Either RSET was not needed, or it succeeded */ - if (sx->ok) - { + if (sx->ok) + { #ifndef DISABLE_TLS - int pfd[2]; -#endif - int socket_fd = sx->cctx.sock; - - if (sx->first_addr) /* More addresses still to be sent */ - { /* for this message */ - continue_sequence++; /* for consistency */ - clearflag(sx->first_addr, af_new_conn); - setflag(sx->first_addr, af_cont_conn); /* Causes * in logging */ - pipelining_active = sx->pipelining_used; /* was cleared at DATA */ - goto SEND_MESSAGE; - } + int pfd[2]; +#endif + int socket_fd = sx->cctx.sock; + + if (sx->first_addr) /* More addresses still to be sent */ + { /* for this message */ +#ifdef EXPERIMENTAL_ESMTP_LIMITS + /* Any that we marked as skipped, reset to do now */ + for (address_item * a = sx->first_addr; a; a = a->next) + if (a->transport_return == SKIP) + a->transport_return = PENDING_DEFER; +#endif + continue_sequence++; /* for consistency */ + clearflag(sx->first_addr, af_new_conn); + setflag(sx->first_addr, af_cont_conn); /* Causes * in logging */ + pipelining_active = sx->pipelining_used; /* was cleared at DATA */ + goto SEND_MESSAGE; + } - /* Unless caller said it already has more messages listed for this host, - pass the connection on to a new Exim process (below, the call to - transport_pass_socket). If the caller has more ready, just return with - the connection still open. */ + /* Unless caller said it already has more messages listed for this host, + pass the connection on to a new Exim process (below, the call to + transport_pass_socket). If the caller has more ready, just return with + the connection still open. */ #ifndef DISABLE_TLS - if (tls_out.active.sock >= 0) - if ( f.continue_more - || verify_check_given_host(CUSS &ob->hosts_noproxy_tls, host) == OK) - { - /* Before passing the socket on, or returning to caller with it still - open, we must shut down TLS. Not all MTAs allow for the continuation - of the SMTP session when TLS is shut down. We test for this by sending - a new EHLO. If we don't get a good response, we don't attempt to pass - the socket on. */ + if (tls_out.active.sock >= 0) + if ( f.continue_more + || verify_check_given_host(CUSS &ob->hosts_noproxy_tls, host) == OK) + { + /* Before passing the socket on, or returning to caller with it still + open, we must shut down TLS. Not all MTAs allow for the continuation + of the SMTP session when TLS is shut down. We test for this by sending + a new EHLO. If we don't get a good response, we don't attempt to pass + the socket on. */ tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_WAIT); sx->send_tlsclose = FALSE; @@ -4422,84 +4652,88 @@ if (sx->completed_addr && sx->ok && sx->send_quit) && smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2', ob->command_timeout); - if (sx->ok && f.continue_more) - goto TIDYUP; /* More addresses for another run */ - } - else - { - /* Set up a pipe for proxying TLS for the new transport process */ - - smtp_peer_options |= OPTION_TLS; - if ((sx->ok = socketpair(AF_UNIX, SOCK_STREAM, 0, pfd) == 0)) - socket_fd = pfd[1]; + if (sx->ok && f.continue_more) + goto TIDYUP; /* More addresses for another run */ + } else - set_errno(sx->first_addr, errno, US"internal allocation problem", - DEFER, FALSE, host, + { + /* Set up a pipe for proxying TLS for the new transport process */ + + smtp_peer_options |= OPTION_TLS; + if ((sx->ok = socketpair(AF_UNIX, SOCK_STREAM, 0, pfd) == 0)) + socket_fd = pfd[1]; + else + set_errno(sx->first_addr, errno, US"internal allocation problem", + DEFER, FALSE, host, # ifdef EXPERIMENTAL_DSN_INFO - sx->smtp_greeting, sx->helo_response, + sx->smtp_greeting, sx->helo_response, # endif - &sx->delivery_start); - } - else + &sx->delivery_start); + } + else #endif - if (f.continue_more) - goto TIDYUP; /* More addresses for another run */ - - /* If the socket is successfully passed, we mustn't send QUIT (or - indeed anything!) from here. */ + if (f.continue_more) + goto TIDYUP; /* More addresses for another run */ -/*XXX DSN_INFO: assume likely to do new HELO; but for greet we'll want to -propagate it from the initial -*/ - if (sx->ok && transport_pass_socket(tblock->name, host->name, - host->address, new_message_id, socket_fd)) - { - sx->send_quit = FALSE; + /* If the socket is successfully passed, we mustn't send QUIT (or + indeed anything!) from here. */ - /* We have passed the client socket to a fresh transport process. - If TLS is still active, we need to proxy it for the transport we - just passed the baton to. Fork a child to to do it, and return to - get logging done asap. Which way to place the work makes assumptions - about post-fork prioritisation which may not hold on all platforms. */ -#ifndef DISABLE_TLS - if (tls_out.active.sock >= 0) + /*XXX DSN_INFO: assume likely to do new HELO; but for greet we'll want to + propagate it from the initial + */ + if (sx->ok && transport_pass_socket(tblock->name, host->name, + host->address, new_message_id, socket_fd +#ifdef EXPERIMENTAL_ESMTP_LIMITS + , sx->peer_limit_mail, sx->peer_limit_rcpt, sx->peer_limit_rcptdom +#endif + )) { - int pid = exim_fork(US"tls-proxy-interproc"); - if (pid == 0) /* child; fork again to disconnect totally */ - { - /* does not return */ - smtp_proxy_tls(sx->cctx.tls_ctx, sx->buffer, sizeof(sx->buffer), pfd, - ob->command_timeout); - } + sx->send_quit = FALSE; - if (pid > 0) /* parent */ + /* We have passed the client socket to a fresh transport process. + If TLS is still active, we need to proxy it for the transport we + just passed the baton to. Fork a child to to do it, and return to + get logging done asap. Which way to place the work makes assumptions + about post-fork prioritisation which may not hold on all platforms. */ +#ifndef DISABLE_TLS + if (tls_out.active.sock >= 0) { - close(pfd[0]); - /* tidy the inter-proc to disconn the proxy proc */ - waitpid(pid, NULL, 0); - tls_close(sx->cctx.tls_ctx, TLS_NO_SHUTDOWN); - sx->cctx.tls_ctx = NULL; - (void)close(sx->cctx.sock); - sx->cctx.sock = -1; - continue_transport = NULL; - continue_hostname = NULL; - goto TIDYUP; + int pid = exim_fork(US"tls-proxy-interproc"); + if (pid == 0) /* child; fork again to disconnect totally */ + { + /* does not return */ + smtp_proxy_tls(sx->cctx.tls_ctx, sx->buffer, sizeof(sx->buffer), pfd, + ob->command_timeout); + } + + if (pid > 0) /* parent */ + { + close(pfd[0]); + /* tidy the inter-proc to disconn the proxy proc */ + waitpid(pid, NULL, 0); + tls_close(sx->cctx.tls_ctx, TLS_NO_SHUTDOWN); + sx->cctx.tls_ctx = NULL; + (void)close(sx->cctx.sock); + sx->cctx.sock = -1; + continue_transport = NULL; + continue_hostname = NULL; + goto TIDYUP; + } + log_write(0, LOG_PANIC_DIE, "fork failed"); } - log_write(0, LOG_PANIC_DIE, "fork failed"); - } #endif + } } - } - /* If RSET failed and there are addresses left, they get deferred. */ - else - set_errno(sx->first_addr, errno, msg, DEFER, FALSE, host, + /* If RSET failed and there are addresses left, they get deferred. */ + else + set_errno(sx->first_addr, errno, msg, DEFER, FALSE, host, #ifdef EXPERIMENTAL_DSN_INFO - sx->smtp_greeting, sx->helo_response, + sx->smtp_greeting, sx->helo_response, #endif - &sx->delivery_start); + &sx->delivery_start); + } } - } /* End off tidily with QUIT unless the connection has died or the socket has been passed to another process. */ @@ -4613,12 +4847,24 @@ if (dane_held) } } continue_sequence = 1; /* for consistency */ - goto DANE_DOMAINS; + goto REPEAT_CONN; + } +#endif + +#ifdef EXPERIMENTAL_ESMTP_LIMITS +if (mail_limit && sx->first_addr) + { + /* Reset the sequence count since we closed the connection. This is flagged + on the pipe back to the delivery process so that a non-continued-conn delivery + is logged. */ + + continue_sequence = 1; /* for consistency */ + clearflag(sx->first_addr, af_cont_conn); + setflag(sx->first_addr, af_new_conn); /* clear * from logging */ + goto REPEAT_CONN; } #endif -continue_transport = NULL; -continue_hostname = NULL; return yield; TIDYUP: diff --git a/src/src/transports/smtp.h b/src/src/transports/smtp.h index 1ea2a4d00..aff3f5452 100644 --- a/src/src/transports/smtp.h +++ b/src/src/transports/smtp.h @@ -171,15 +171,25 @@ typedef struct { BOOL pending_BDAT:1; BOOL RCPT_452:1; BOOL good_RCPT:1; +#ifdef EXPERIMENTAL_ESMTP_LIMITS + BOOL single_rcpt_domain:1; +#endif BOOL completed_addr:1; BOOL send_rset:1; BOOL send_quit:1; BOOL send_tlsclose:1; + unsigned peer_offered; +#ifdef EXPERIMENTAL_ESMTP_LIMITS + unsigned peer_limit_mail; + unsigned peer_limit_rcpt; + unsigned peer_limit_rcptdom; +#endif + + unsigned max_mail; int max_rcpt; int cmd_count; - unsigned peer_offered; unsigned avoid_option; uschar * igquotstr; uschar * helo_data; @@ -188,6 +198,11 @@ typedef struct { uschar * helo_response; #endif #ifndef DISABLE_PIPE_CONNECT + /* Info about the EHLO response stored to / retrieved from cache. When + operating early-pipe, we use the cached values. For each of plaintext and + crypted we store bitmaps for ESMTP features and AUTH methods. If the LIMITS + extension is built and usable them at least one of the limits values cached + is nonzero, and we use the values to constrain the connection. */ ehlo_resp_precis ehlo_resp; #endif |