X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/d7978c0f8af20ff4c3f770589b1bb81568aecff3..f5730918ef684baafbd9e606a1d4eb06914563cc:/src/src/verify.c diff --git a/src/src/verify.c b/src/src/verify.c index 43caac562..125df8d91 100644 --- a/src/src/verify.c +++ b/src/src/verify.c @@ -2,8 +2,10 @@ * Exim - an Internet mail transport agent * *************************************************/ +/* Copyright (c) The Exim Maintainers 2020 - 2022 */ /* Copyright (c) University of Cambridge 1995 - 2018 */ /* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ /* Functions concerned with verifying things. The original code for callout caching was contributed by Kevin Fleming (but I hacked it around a bit). */ @@ -18,27 +20,6 @@ static smtp_context ctctx; uschar ctbuffer[8192]; -/* Structure for caching DNSBL lookups */ - -typedef struct dnsbl_cache_block { - time_t expiry; - dns_address *rhs; - uschar *text; - int rc; - BOOL text_set; -} dnsbl_cache_block; - - -/* Anchor for DNSBL cache */ - -static tree_node *dnsbl_cache = NULL; - - -/* Bits for match_type in one_check_dnsbl() */ - -#define MT_NOT 1 -#define MT_ALL 2 - static uschar cutthrough_response(client_conn_ctx *, char, uschar **, int); @@ -70,7 +51,7 @@ dbdata_callout_cache *cache_record; if (!(cache_record = dbfn_read_with_length(dbm_file, key, &length))) { - HDEBUG(D_verify) debug_printf("callout cache: no %s record found for %s\n", type, key); + HDEBUG(D_verify) debug_printf_indent("callout cache: no %s record found for %s\n", type, key); return NULL; } @@ -84,7 +65,7 @@ now = time(NULL); if (now - cache_record->time_stamp > expire) { - HDEBUG(D_verify) debug_printf("callout cache: %s record expired for %s\n", type, key); + HDEBUG(D_verify) debug_printf_indent("callout cache: %s record expired for %s\n", type, key); return NULL; } @@ -98,7 +79,7 @@ if (type[0] == 'd' && cache_record->result != ccache_reject) { if (length == sizeof(dbdata_callout_cache_obs)) { - dbdata_callout_cache *new = store_get(sizeof(dbdata_callout_cache)); + dbdata_callout_cache * new = store_get(sizeof(dbdata_callout_cache), GET_UNTAINTED); memcpy(new, cache_record, length); new->postmaster_stamp = new->random_stamp = new->time_stamp; cache_record = new; @@ -111,7 +92,7 @@ if (type[0] == 'd' && cache_record->result != ccache_reject) cache_record->random_result = ccache_unknown; } -HDEBUG(D_verify) debug_printf("callout cache: found %s record for %s\n", type, key); +HDEBUG(D_verify) debug_printf_indent("callout cache: found %s record for %s\n", type, key); return cache_record; } @@ -133,16 +114,16 @@ int options = *opt_ptr; open_db dbblock; open_db *dbm_file = NULL; -/* Open the callout cache database, it it exists, for reading only at this +/* Open the callout cache database, if it exists, for reading only at this stage, unless caching has been disabled. */ if (options & vopt_callout_no_cache) { - HDEBUG(D_verify) debug_printf("callout cache: disabled by no_cache\n"); + HDEBUG(D_verify) debug_printf_indent("callout cache: disabled by no_cache\n"); } -else if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE))) +else if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE, TRUE))) { - HDEBUG(D_verify) debug_printf("callout cache: not available\n"); + HDEBUG(D_verify) debug_printf_indent("callout cache: not available\n"); } else { @@ -173,7 +154,7 @@ else || *from_address == 0 && cache_record->result == ccache_reject_mfnull) { HDEBUG(D_verify) - debug_printf("callout cache: domain gave initial rejection, or " + debug_printf_indent("callout cache: domain gave initial rejection, or " "does not accept HELO or MAIL FROM:<>\n"); setflag(addr, af_verify_nsfail); addr->user_message = US"(result of an earlier callout reused)."; @@ -194,14 +175,14 @@ else { case ccache_accept: HDEBUG(D_verify) - debug_printf("callout cache: domain accepts random addresses\n"); + debug_printf_indent("callout cache: domain accepts random addresses\n"); *failure_ptr = US"random"; dbfn_close(dbm_file); return TRUE; /* Default yield is OK */ case ccache_reject: HDEBUG(D_verify) - debug_printf("callout cache: domain rejects random addresses\n"); + debug_printf_indent("callout cache: domain rejects random addresses\n"); *opt_ptr = options & ~vopt_callout_random; new_domain_record->random_result = ccache_reject; new_domain_record->random_stamp = cache_record->random_stamp; @@ -209,7 +190,7 @@ else default: HDEBUG(D_verify) - debug_printf("callout cache: need to check random address handling " + debug_printf_indent("callout cache: need to check random address handling " "(not cached or cache expired)\n"); dbfn_close(dbm_file); return FALSE; @@ -226,7 +207,7 @@ else { setflag(addr, af_verify_pmfail); HDEBUG(D_verify) - debug_printf("callout cache: domain does not accept " + debug_printf_indent("callout cache: domain does not accept " "RCPT TO:\n"); *yield = FAIL; *failure_ptr = US"postmaster"; @@ -238,7 +219,7 @@ else if (cache_record->postmaster_result == ccache_unknown) { HDEBUG(D_verify) - debug_printf("callout cache: need to check RCPT " + debug_printf_indent("callout cache: need to check RCPT " "TO: (not cached or cache expired)\n"); dbfn_close(dbm_file); return FALSE; @@ -249,7 +230,7 @@ else that the value in the cache record is preserved (with its old timestamp). */ - HDEBUG(D_verify) debug_printf("callout cache: domain accepts RCPT " + HDEBUG(D_verify) debug_printf_indent("callout cache: domain accepts RCPT " "TO:\n"); *pm_ptr = NULL; new_domain_record->postmaster_result = ccache_accept; @@ -273,12 +254,12 @@ else if (cache_address_record->result == ccache_accept) { HDEBUG(D_verify) - debug_printf("callout cache: address record is positive\n"); + debug_printf_indent("callout cache: address record is positive\n"); } else { HDEBUG(D_verify) - debug_printf("callout cache: address record is negative\n"); + debug_printf_indent("callout cache: address record is negative\n"); addr->user_message = US"Previous (cached) callout verification failure"; *failure_ptr = US"recipient"; *yield = FAIL; @@ -313,15 +294,15 @@ implying some kind of I/O error. We don't want to write the cache in that case. Otherwise the value is ccache_accept, ccache_reject, or ccache_reject_mfnull. */ if (dom_rec->result != ccache_unknown) - if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE))) + if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE))) { - HDEBUG(D_verify) debug_printf("callout cache: not available\n"); + HDEBUG(D_verify) debug_printf_indent("callout cache: not available\n"); } else { (void)dbfn_write(dbm_file, domain, dom_rec, (int)sizeof(dbdata_callout_cache)); - HDEBUG(D_verify) debug_printf("wrote callout cache domain record for %s:\n" + HDEBUG(D_verify) debug_printf_indent("wrote callout cache domain record for %s:\n" " result=%d postmaster=%d random=%d\n", domain, dom_rec->result, @@ -335,16 +316,16 @@ is disabled. */ if (done && addr_rec->result != ccache_unknown) { if (!dbm_file) - dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE); + dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE); if (!dbm_file) { - HDEBUG(D_verify) debug_printf("no callout cache available\n"); + HDEBUG(D_verify) debug_printf_indent("no callout cache available\n"); } else { (void)dbfn_write(dbm_file, address_key, addr_rec, (int)sizeof(dbdata_callout_cache_address)); - HDEBUG(D_verify) debug_printf("wrote %s callout cache address record for %s\n", + HDEBUG(D_verify) debug_printf_indent("wrote %s callout cache address record for %s\n", addr_rec->result == ccache_accept ? "positive" : "negative", address_key); } @@ -420,7 +401,7 @@ if (addr->transport == cutthrough.addr.transport) if (done) { - address_item * na = store_get(sizeof(address_item)); + address_item * na = store_get(sizeof(address_item), GET_UNTAINTED); *na = cutthrough.addr; cutthrough.addr = *addr; cutthrough.addr.host_used = &cutthrough.host; @@ -465,6 +446,21 @@ return done; } + + +/* A rcpt callout, or cached record of one, verified the address. +Set $domain_data and $local_part_data to detainted versions. +*/ +static void +callout_verified_rcpt(const address_item * addr) +{ +address_item a = {.address = addr->address}; +if (deliver_split_address(&a) != OK) return; +deliver_localpart_data = string_copy_taint(a.local_part, GET_UNTAINTED); +deliver_domain_data = string_copy_taint(a.domain, GET_UNTAINTED); +} + + /************************************************* * Do callout verification for an address * *************************************************/ @@ -567,6 +563,7 @@ if (!addr->transport) { HDEBUG(D_verify) debug_printf("cannot callout via null transport\n"); } + else if (Ustrcmp(addr->transport->driver_name, "smtp") != 0) log_write(0, LOG_MAIN|LOG_PANIC|LOG_CONFIG_FOR, "callout transport '%s': %s is non-smtp", addr->transport->name, addr->transport->driver_name); @@ -574,6 +571,7 @@ else { smtp_transport_options_block *ob = (smtp_transport_options_block *)addr->transport->options_block; + smtp_context * sx = NULL; /* The information wasn't available in the cache, so we have to do a real callout and save the result in the cache for next time, unless no_cache is set, @@ -586,6 +584,10 @@ else log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand " "callout_random_local_part: %s", expand_string_message); + /* Compile regex' used by client-side smtp */ + + smtp_deliver_init(); + /* Default the connect and overall callout timeouts if not set, and record the time we are starting so that we can enforce it. */ @@ -625,8 +627,7 @@ coding means skipping this whole loop and doing the append separately. */ { int host_af; int port = 25; - uschar *interface = NULL; /* Outgoing interface to use; NULL => any */ - smtp_context sx; + uschar * interface = NULL; /* Outgoing interface to use; NULL => any */ if (!host->address) { @@ -666,14 +667,18 @@ coding means skipping this whole loop and doing the append separately. */ log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: %s", addr->address, addr->message); - sx.addrlist = addr; - sx.conn_args.host = host; - sx.conn_args.host_af = host_af, - sx.port = port; - sx.conn_args.interface = interface; - sx.helo_data = tf->helo_data; - sx.conn_args.tblock = addr->transport; - sx.verify = TRUE; + if (!sx) sx = store_get(sizeof(*sx), GET_TAINTED); /* tainted buffers */ + memset(sx, 0, sizeof(*sx)); + + sx->addrlist = sx->first_addr = addr; + sx->conn_args.host = host; + sx->conn_args.host_af = host_af, + sx->port = port; + sx->conn_args.interface = interface; + sx->helo_data = tf->helo_data; + sx->conn_args.tblock = addr->transport; + sx->conn_args.sock = -1; + sx->verify = TRUE; tls_retry_connection: /* Set the address state so that errors are recorded in it */ @@ -686,8 +691,8 @@ tls_retry_connection: SMTP command to send. If we tried TLS but it failed, try again without if permitted */ - yield = smtp_setup_conn(&sx, FALSE); -#ifdef SUPPORT_TLS + yield = smtp_setup_conn(sx, FALSE); +#ifndef DISABLE_TLS if ( yield == DEFER && addr->basic_errno == ERRNO_TLSFAILURE && ob->tls_tempfail_tryclear @@ -698,7 +703,7 @@ tls_retry_connection: "%s: callout unencrypted to %s [%s] (not in hosts_require_tls)", addr->message, host->name, host->address); addr->transport_return = PENDING_DEFER; - yield = smtp_setup_conn(&sx, TRUE); + yield = smtp_setup_conn(sx, TRUE); } #endif if (yield != OK) @@ -728,11 +733,11 @@ tls_retry_connection: addr->authenticator = client_authenticator; addr->auth_id = client_authenticated_id; - sx.from_addr = from_address; - sx.first_addr = sx.sync_addr = addr; - sx.ok = FALSE; /*XXX these 3 last might not be needed for verify? */ - sx.send_rset = TRUE; - sx.completed_addr = FALSE; + sx->from_addr = from_address; + sx->first_addr = sx->sync_addr = addr; + sx->ok = FALSE; /*XXX these 3 last might not be needed for verify? */ + sx->send_rset = TRUE; + sx->completed_addr = FALSE; new_domain_record.result = old_domain_cache_result == ccache_reject_mfnull ? ccache_reject_mfnull : ccache_accept; @@ -789,12 +794,12 @@ tls_retry_connection: Avoid using a SIZE option on the MAIL for all random-rcpt checks. */ - sx.avoid_option = OPTION_SIZE; + sx->avoid_option = OPTION_SIZE; /* Remember when we last did a random test */ new_domain_record.random_stamp = time(NULL); - if (smtp_write_mail_and_rcpt_cmds(&sx, &yield) == 0) + if (smtp_write_mail_and_rcpt_cmds(sx, &yield) == 0) switch(addr->transport_return) { case PENDING_OK: /* random was accepted, unfortunately */ @@ -805,36 +810,36 @@ tls_retry_connection: goto no_conn; case FAIL: /* rejected: the preferred result */ new_domain_record.random_result = ccache_reject; - sx.avoid_option = 0; + sx->avoid_option = 0; /* Between each check, issue RSET, because some servers accept only one recipient after MAIL FROM:<>. XXX We don't care about that for postmaster_full. Should we? */ if ((done = - smtp_write_command(&sx, SCMD_FLUSH, "RSET\r\n") >= 0 && - smtp_read_response(&sx, sx.buffer, sizeof(sx.buffer), '2', callout))) + smtp_write_command(sx, SCMD_FLUSH, "RSET\r\n") >= 0 && + smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2', callout))) break; HDEBUG(D_acl|D_v) debug_printf_indent("problem after random/rset/mfrom; reopen conn\n"); random_local_part = NULL; -#ifdef SUPPORT_TLS - tls_close(sx.cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT); +#ifndef DISABLE_TLS + tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT); #endif HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(close)>>\n"); - (void)close(sx.cctx.sock); - sx.cctx.sock = -1; + (void)close(sx->cctx.sock); + sx->cctx.sock = -1; #ifndef DISABLE_EVENT (void) event_raise(addr->transport->event_action, - US"tcp:close", NULL); + US"tcp:close", NULL, NULL); #endif addr->address = main_address; addr->transport_return = PENDING_DEFER; - sx.first_addr = sx.sync_addr = addr; - sx.ok = FALSE; - sx.send_rset = TRUE; - sx.completed_addr = FALSE; + sx->first_addr = sx->sync_addr = addr; + sx->ok = FALSE; + sx->send_rset = TRUE; + sx->completed_addr = FALSE; goto tls_retry_connection; case DEFER: /* 4xx response to random */ break; /* Just to be clear. ccache_unknown, !done. */ @@ -843,10 +848,10 @@ tls_retry_connection: /* Re-setup for main verify, or for the error message when failing */ addr->address = main_address; addr->transport_return = PENDING_DEFER; - sx.first_addr = sx.sync_addr = addr; - sx.ok = FALSE; - sx.send_rset = TRUE; - sx.completed_addr = FALSE; + sx->first_addr = sx->sync_addr = addr; + sx->ok = FALSE; + sx->send_rset = TRUE; + sx->completed_addr = FALSE; } else done = TRUE; @@ -857,28 +862,28 @@ tls_retry_connection: if (done) { if (!(options & vopt_is_recipient && options & vopt_callout_no_cache)) - sx.avoid_option = OPTION_SIZE; + sx->avoid_option = OPTION_SIZE; done = FALSE; - switch(smtp_write_mail_and_rcpt_cmds(&sx, &yield)) + switch(smtp_write_mail_and_rcpt_cmds(sx, &yield)) { case 0: switch(addr->transport_return) /* ok so far */ { case PENDING_OK: done = TRUE; new_address_record.result = ccache_accept; break; - case FAIL: done = TRUE; + case FAIL: done = TRUE; yield = FAIL; *failure_ptr = US"recipient"; new_address_record.result = ccache_reject; break; - default: break; + default: break; } break; case -1: /* MAIL response error */ *failure_ptr = US"mail"; - if (errno == 0 && sx.buffer[0] == '5') + if (errno == 0 && sx->buffer[0] == '5') { setflag(addr, af_verify_nsfail); if (from_address[0] == 0) @@ -908,8 +913,8 @@ tls_retry_connection: cancel_cutthrough_connection(TRUE, US"postmaster verify"); HDEBUG(D_acl|D_v) debug_printf_indent("Cutthrough cancelled by presence of postmaster verify\n"); - done = smtp_write_command(&sx, SCMD_FLUSH, "RSET\r\n") >= 0 - && smtp_read_response(&sx, sx.buffer, sizeof(sx.buffer), '2', callout); + done = smtp_write_command(sx, SCMD_FLUSH, "RSET\r\n") >= 0 + && smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2', callout); if (done) { @@ -919,23 +924,23 @@ tls_retry_connection: addr->address = string_sprintf("postmaster@%.1000s", addr->domain); addr->transport_return = PENDING_DEFER; - sx.from_addr = pm_mailfrom; - sx.first_addr = sx.sync_addr = addr; - sx.ok = FALSE; - sx.send_rset = TRUE; - sx.completed_addr = FALSE; - sx.avoid_option = OPTION_SIZE; + sx->from_addr = pm_mailfrom; + sx->first_addr = sx->sync_addr = addr; + sx->ok = FALSE; + sx->send_rset = TRUE; + sx->completed_addr = FALSE; + sx->avoid_option = OPTION_SIZE; - if( smtp_write_mail_and_rcpt_cmds(&sx, &yield) == 0 + if( smtp_write_mail_and_rcpt_cmds(sx, &yield) == 0 && addr->transport_return == PENDING_OK ) done = TRUE; else done = (options & vopt_callout_fullpm) != 0 - && smtp_write_command(&sx, SCMD_FLUSH, + && smtp_write_command(sx, SCMD_FLUSH, "RCPT TO:\r\n") >= 0 - && smtp_read_response(&sx, sx.buffer, - sizeof(sx.buffer), '2', callout); + && smtp_read_response(sx, sx->buffer, + sizeof(sx->buffer), '2', callout); /* Sort out the cache record */ @@ -943,7 +948,7 @@ tls_retry_connection: if (done) new_domain_record.postmaster_result = ccache_accept; - else if (errno == 0 && sx.buffer[0] == '5') + else if (errno == 0 && sx->buffer[0] == '5') { *failure_ptr = US"postmaster"; setflag(addr, af_verify_pmfail); @@ -968,7 +973,7 @@ no_conn: { case ETIMEDOUT: HDEBUG(D_verify) debug_printf("SMTP timeout\n"); - sx.send_quit = FALSE; + sx->send_quit = FALSE; break; #ifdef SUPPORT_I18N @@ -976,8 +981,7 @@ no_conn: { extern int acl_where; /* src/acl.c */ errno = 0; - addr->message = string_sprintf( - "response to \"EHLO\" did not include SMTPUTF8"); + addr->message = US"response to \"EHLO\" did not include SMTPUTF8"; addr->user_message = acl_where == ACL_WHERE_RCPT ? US"533 no support for internationalised mailbox name" : US"550 mailbox unavailable"; @@ -985,20 +989,13 @@ no_conn: done = TRUE; } break; -#endif -#if defined(SUPPORT_TLS) && defined(EXPERIMENTAL_REQUIRETLS) - case ERRNO_REQUIRETLS: - addr->user_message = US"530 5.7.4 REQUIRETLS support required"; - yield = FAIL; - done = TRUE; - break; #endif case ECONNREFUSED: - sx.send_quit = FALSE; + sx->send_quit = FALSE; break; case 0: - if (*sx.buffer == 0) Ustrcpy(sx.buffer, US"connection dropped"); + if (*sx->buffer == 0) Ustrcpy(sx->buffer, US"connection dropped"); /*XXX test here is ugly; seem to have a split of responsibility for building this message. Need to rationalise. Where is it done @@ -1007,16 +1004,36 @@ no_conn: */ if (!addr->message) addr->message = string_sprintf("response to \"%s\" was: %s", - big_buffer, string_printing(sx.buffer)); + big_buffer, string_printing(sx->buffer)); + /* RFC 5321 section 4.2: the text portion of the response may have only + HT, SP, Printable US-ASCII. Deal with awkward chars by cutting the + received message off before passing it onward. Newlines are ok; they + just become a multiline response (but wrapped in the error code we + produce). */ + + for (uschar * s = sx->buffer; + *s && s < sx->buffer + sizeof(sx->buffer); + s++) + { + uschar c = *s; + if (c != '\t' && c != '\n' && (c < ' ' || c > '~')) + { + if (s - sx->buffer < sizeof(sx->buffer) - 12) + memcpy(s, "(truncated)", 12); + else + *s = '\0'; + break; + } + } addr->user_message = options & vopt_is_recipient - ? string_sprintf("Callout verification failed:\n%s", sx.buffer) + ? string_sprintf("Callout verification failed:\n%s", sx->buffer) : string_sprintf("Called: %s\nSent: %s\nResponse: %s", - host->address, big_buffer, sx.buffer); + host->address, big_buffer, sx->buffer); /* Hard rejection ends the process */ - if (sx.buffer[0] == '5') /* Address rejected */ + if (sx->buffer[0] == '5') /* Address rejected */ { yield = FAIL; done = TRUE; @@ -1039,6 +1056,8 @@ no_conn: HDEBUG(D_acl|D_v) debug_printf("Cutthrough cancelled by presence of transport filter\n"); } #ifndef DISABLE_DKIM + /* DKIM signing needs to add a header after seeing the whole body, so we cannot just copy + body bytes to the outbound as they are received, which is the intent of cutthrough. */ if (ob->dkim.dkim_domain) { cutthrough.delivery= FALSE; @@ -1063,7 +1082,7 @@ no_conn: && !random_local_part && !pm_mailfrom && cutthrough.cctx.sock < 0 - && !sx.lmtp + && !sx->lmtp ) { HDEBUG(D_acl|D_v) debug_printf_indent("holding verify callout open for %s\n", @@ -1073,7 +1092,7 @@ no_conn: cutthrough.callout_hold_only = !cutthrough.delivery; cutthrough.is_tls = tls_out.active.sock >= 0; /* We assume no buffer in use in the outblock */ - cutthrough.cctx = sx.cctx; + cutthrough.cctx = sx->cctx; cutthrough.nrcpt = 1; cutthrough.transport = addr->transport->name; cutthrough.interface = interface; @@ -1096,7 +1115,7 @@ no_conn: for (address_item * caddr = &cutthrough.addr, * parent = addr->parent; parent; caddr = caddr->parent, parent = parent->parent) - *(caddr->parent = store_get(sizeof(address_item))) = *parent; + *(caddr->parent = store_get(sizeof(address_item), GET_UNTAINTED)) = *parent; ctctx.outblock.buffer = ctbuffer; ctctx.outblock.buffersize = sizeof(ctbuffer); @@ -1109,28 +1128,26 @@ no_conn: /* Ensure no cutthrough on multiple verifies that were incompatible */ if (options & vopt_callout_recipsender) cancel_cutthrough_connection(TRUE, US"not usable for cutthrough"); - if (sx.send_quit) - { - (void) smtp_write_command(&sx, SCMD_FLUSH, "QUIT\r\n"); + if (sx->send_quit) + if (smtp_write_command(sx, SCMD_FLUSH, "QUIT\r\n") != -1) + /* Wait a short time for response, and discard it */ + smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2', 1); - /* Wait a short time for response, and discard it */ - smtp_read_response(&sx, sx.buffer, sizeof(sx.buffer), '2', 1); - } - - if (sx.cctx.sock >= 0) + if (sx->cctx.sock >= 0) { -#ifdef SUPPORT_TLS - if (sx.cctx.tls_ctx) +#ifndef DISABLE_TLS + if (sx->cctx.tls_ctx) { - tls_close(sx.cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT); - sx.cctx.tls_ctx = NULL; + tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT); + sx->cctx.tls_ctx = NULL; } #endif HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(close)>>\n"); - (void)close(sx.cctx.sock); - sx.cctx.sock = -1; + (void)close(sx->cctx.sock); + sx->cctx.sock = -1; + smtp_debug_cmd_report(); #ifndef DISABLE_EVENT - (void) event_raise(addr->transport->event_action, US"tcp:close", NULL); + (void) event_raise(addr->transport->event_action, US"tcp:close", NULL, NULL); #endif } } @@ -1183,7 +1200,7 @@ if (!done) /* Come here from within the cache-reading code on fast-track exit. */ END_CALLOUT: -tls_modify_variables(&tls_in); +tls_modify_variables(&tls_in); /* return variables to inbound values */ return yield; } @@ -1225,7 +1242,7 @@ if(cutthrough.cctx.sock < 0) return TRUE; if( -#ifdef SUPPORT_TLS +#ifndef DISABLE_TLS cutthrough.is_tls ? tls_write(cutthrough.cctx.tls_ctx, ctctx.outblock.buffer, n, FALSE) : @@ -1349,7 +1366,7 @@ cutthrough_predata(void) if(cutthrough.cctx.sock < 0 || cutthrough.callout_hold_only) return FALSE; -HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP>> DATA\n"); +smtp_debug_cmd(US"DATA", 0); cutthrough_puts(US"DATA\r\n", 6); cutthrough_flush_send(); @@ -1417,7 +1434,7 @@ if(fd >= 0) */ client_conn_ctx tmp_ctx = cutthrough.cctx; ctctx.outblock.ptr = ctbuffer; - HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP>> QUIT\n"); + smtp_debug_cmd(US"QUIT", 0); _cutthrough_puts(US"QUIT\r\n", 6); /* avoid recursion */ _cutthrough_flush_send(); cutthrough.cctx.sock = -1; /* avoid recursion via read timeout */ @@ -1426,7 +1443,7 @@ if(fd >= 0) /* Wait a short time for response, and discard it */ cutthrough_response(&tmp_ctx, '2', NULL, 1); -#ifdef SUPPORT_TLS +#ifndef DISABLE_TLS if (cutthrough.is_tls) { tls_close(cutthrough.cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT); @@ -1436,6 +1453,7 @@ if(fd >= 0) #endif HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(close)>>\n"); (void)close(fd); + smtp_debug_cmd_report(); HDEBUG(D_acl) debug_printf_indent("----------- cutthrough shutdown (%s) ------------\n", why); } ctctx.outblock.ptr = ctbuffer; @@ -1539,6 +1557,8 @@ if (addr != vaddr) vaddr->basic_errno = addr->basic_errno; vaddr->more_errno = addr->more_errno; vaddr->prop.address_data = addr->prop.address_data; + vaddr->prop.variables = NULL; + tree_dup((tree_node **)&vaddr->prop.variables, addr->prop.variables); copyflag(vaddr, addr, af_pass_message); } return yield; @@ -1593,7 +1613,7 @@ address testing (-bt), which is indicated by address_test_mode being set. Arguments: vaddr contains the address to verify; the next field in this block must be NULL - f if not NULL, write the result to this file + fp if not NULL, write the result to this file options various option bits: vopt_fake_sender => this sender verify is not for the real sender (it was verify=sender=xxxx or an address from a @@ -1644,9 +1664,9 @@ BOOL expn = (options & vopt_expn) != 0; BOOL success_on_redirect = (options & vopt_success_on_redirect) != 0; int i; int yield = OK; -int verify_type = expn? v_expn : - f.address_test_mode? v_none : - options & vopt_is_recipient? v_recipient : v_sender; +int verify_type = expn ? v_expn : + f.address_test_mode ? v_none : + options & vopt_is_recipient ? v_recipient : v_sender; address_item *addr_list; address_item *addr_new = NULL; address_item *addr_remote = NULL; @@ -1686,7 +1706,8 @@ if (parse_find_at(address) == NULL) *failure_ptr = US"qualify"; return FAIL; } - address = rewrite_address_qualify(address, options & vopt_is_recipient); + /* deconst ok as address was not const */ + address = US rewrite_address_qualify(address, options & vopt_is_recipient); } DEBUG(D_verify) @@ -1701,7 +1722,8 @@ may have been set by domains and local part tests during an ACL. */ if (global_rewrite_rules) { uschar *old = address; - address = rewrite_address(address, options & vopt_is_recipient, FALSE, + /* deconst ok as address was not const */ + address = US rewrite_address(address, options & vopt_is_recipient, FALSE, global_rewrite_rules, rewrite_existflags); if (address != old) { @@ -1827,6 +1849,8 @@ while (addr_new) if (rc == OK) { + BOOL local_verify = FALSE; + if (routed) *routed = TRUE; if (callout > 0) { @@ -1853,72 +1877,76 @@ while (addr_new) transport's options, so as to mimic what would happen if we were really sending a message to this address. */ - if ((tp = addr->transport) && !tp->info->local) - { - (void)(tp->setup)(tp, addr, &tf, 0, 0, NULL); + if ((tp = addr->transport)) + if (!tp->info->local) + { + (void)(tp->setup)(tp, addr, &tf, 0, 0, NULL); - /* If the transport has hosts and the router does not, or if the - transport is configured to override the router's hosts, we must build a - host list of the transport's hosts, and find the IP addresses */ + /* If the transport has hosts and the router does not, or if the + transport is configured to override the router's hosts, we must build a + host list of the transport's hosts, and find the IP addresses */ - if (tf.hosts && (!host_list || tf.hosts_override)) - { - uschar *s; - const uschar *save_deliver_domain = deliver_domain; - uschar *save_deliver_localpart = deliver_localpart; - - host_list = NULL; /* Ignore the router's hosts */ - - deliver_domain = addr->domain; - deliver_localpart = addr->local_part; - s = expand_string(tf.hosts); - deliver_domain = save_deliver_domain; - deliver_localpart = save_deliver_localpart; - - if (!s) - { - log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand list of hosts " - "\"%s\" in %s transport for callout: %s", tf.hosts, - tp->name, expand_string_message); - } - else - { - int flags; - host_build_hostlist(&host_list, s, tf.hosts_randomize); - - /* Just ignore failures to find a host address. If we don't manage - to find any addresses, the callout will defer. Note that more than - one address may be found for a single host, which will result in - additional host items being inserted into the chain. Hence we must - save the next host first. */ - - flags = HOST_FIND_BY_A | HOST_FIND_BY_AAAA; - if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE; - if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS; - - for (host_item * host = host_list, * nexthost; host; host = nexthost) - { - nexthost = host->next; - if (tf.gethostbyname || - string_is_ip_address(host->name, NULL) != 0) - (void)host_find_byname(host, NULL, flags, NULL, TRUE); - else + if (tf.hosts && (!host_list || tf.hosts_override)) + { + uschar *s; + const uschar *save_deliver_domain = deliver_domain; + uschar *save_deliver_localpart = deliver_localpart; + + host_list = NULL; /* Ignore the router's hosts */ + + deliver_domain = addr->domain; + deliver_localpart = addr->local_part; + s = expand_string(tf.hosts); + deliver_domain = save_deliver_domain; + deliver_localpart = save_deliver_localpart; + + if (!s) + { + log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand list of hosts " + "\"%s\" in %s transport for callout: %s", tf.hosts, + tp->name, expand_string_message); + } + else + { + int flags; + host_build_hostlist(&host_list, s, tf.hosts_randomize); + + /* Just ignore failures to find a host address. If we don't manage + to find any addresses, the callout will defer. Note that more than + one address may be found for a single host, which will result in + additional host items being inserted into the chain. Hence we must + save the next host first. */ + + flags = HOST_FIND_BY_A | HOST_FIND_BY_AAAA; + if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE; + if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS; + + for (host_item * host = host_list, * nexthost; host; host = nexthost) { - const dnssec_domains * dsp = NULL; - if (Ustrcmp(tp->driver_name, "smtp") == 0) + nexthost = host->next; + if (tf.gethostbyname || + string_is_ip_address(host->name, NULL) != 0) + (void)host_find_byname(host, NULL, flags, NULL, TRUE); + else { - smtp_transport_options_block * ob = - (smtp_transport_options_block *) tp->options_block; - dsp = &ob->dnssec; - } + const dnssec_domains * dsp = NULL; + if (Ustrcmp(tp->driver_name, "smtp") == 0) + { + smtp_transport_options_block * ob = + (smtp_transport_options_block *) tp->options_block; + dsp = &ob->dnssec; + } - (void) host_find_bydns(host, NULL, flags, NULL, NULL, NULL, - dsp, NULL, NULL); + (void) host_find_bydns(host, NULL, flags, NULL, NULL, NULL, + dsp, NULL, NULL); + } } - } - } - } - } + } + } + } + else if ( options & vopt_quota + && Ustrcmp(tp->driver_name, "appendfile") == 0) + local_verify = TRUE; /* Can only do a callout if we have at least one host! If the callout fails, it will have set ${sender,recipient}_verify_failure. */ @@ -1934,21 +1962,33 @@ while (addr_new) } else { -#ifdef SUPPORT_TLS +#ifndef DISABLE_TLS deliver_set_expansions(addr); #endif rc = do_callout(addr, host_list, &tf, callout, callout_overall, callout_connect, options, se_mailfrom, pm_mailfrom); -#ifdef SUPPORT_TLS +#ifndef DISABLE_TLS deliver_set_expansions(NULL); #endif + if ( options & vopt_is_recipient + && rc == OK + /* set to "random", with OK, for an accepted random */ + && !recipient_verify_failure + ) + callout_verified_rcpt(addr); } } + else if (local_verify) + { + HDEBUG(D_verify) debug_printf("Attempting quota verification\n"); + + deliver_set_expansions(addr); + deliver_local(addr, TRUE); + rc = addr->transport_return; + } else - { HDEBUG(D_verify) debug_printf("Cannot do callout: neither router nor " "transport provided a host list, or transport is not smtp\n"); - } } } @@ -2099,6 +2139,8 @@ while (addr_new) of $address_data to be that of the child */ vaddr->prop.address_data = addr->prop.address_data; + vaddr->prop.variables = NULL; + tree_dup((tree_node **)&vaddr->prop.variables, addr->prop.variables); /* If stopped because more than one new address, cannot cutthrough */ @@ -2135,10 +2177,6 @@ for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++) addr_list = addr->next; fprintf(fp, "%s", CS addr->address); -#ifdef EXPERIMENTAL_SRS - if(addr->prop.srs_sender) - fprintf(fp, " [srs = %s]", addr->prop.srs_sender); -#endif /* If the address is a duplicate, show something about it. */ @@ -2200,7 +2238,7 @@ the -bv or -bt case). */ out: verify_mode = NULL; -tls_modify_variables(&tls_in); +tls_modify_variables(&tls_in); /* return variables to inbound values */ return yield; } @@ -2240,7 +2278,7 @@ for (header_line * h = header_list; h && yield == OK; h = h->next) colon = Ustrchr(h->text, ':'); s = colon + 1; - while (isspace(*s)) s++; + Uskip_whitespace(&s); /* Loop for multiple addresses in the header, enabling group syntax. Note that we have to reset this after the header has been scanned. */ @@ -2274,7 +2312,7 @@ for (header_line * h = header_list; h && yield == OK; h = h->next) { if (!f.allow_unqualified_recipient) recipient = NULL; } - if (recipient == NULL) errmess = US"unqualified address not permitted"; + if (!recipient) errmess = US"unqualified address not permitted"; } /* It's an error if no address could be extracted, except for the special @@ -2319,7 +2357,7 @@ for (header_line * h = header_list; h && yield == OK; h = h->next) /* Advance to the next address */ s = ss + (terminator ? 1 : 0); - while (isspace(*s)) s++; + Uskip_whitespace(&s); } /* Next address */ f.parse_allow_group = FALSE; @@ -2356,7 +2394,7 @@ for (header_line * h = header_list; h; h = h->next) if ((*s < 33) || (*s > 126)) { *msgptr = string_sprintf("Invalid character in header \"%.*s\" found", - colon - h->text, h->text); + (int)(colon - h->text), h->text); return FAIL; } } @@ -2376,13 +2414,13 @@ The original proposed patch did the former, but I have chosen to do the latter, because (a) it requires no memory and (b) will use fewer resources when there are many addresses in To: and/or Cc: and only one or two envelope recipients. -Arguments: none +Arguments: case_sensitive true if case sensitive matching should be used Returns: OK if there are no blind recipients FAIL if there is at least one blind recipient */ int -verify_check_notblind(void) +verify_check_notblind(BOOL case_sensitive) { for (int i = 0; i < recipients_count; i++) { @@ -2397,7 +2435,7 @@ for (int i = 0; i < recipients_count; i++) colon = Ustrchr(h->text, ':'); s = colon + 1; - while (isspace(*s)) s++; + Uskip_whitespace(&s); /* Loop for multiple addresses in the header, enabling group syntax. Note that we have to reset this after the header has been scanned. */ @@ -2406,8 +2444,8 @@ for (int i = 0; i < recipients_count; i++) while (*s) { - uschar *ss = parse_find_address_end(s, FALSE); - uschar *recipient,*errmess; + uschar * ss = parse_find_address_end(s, FALSE); + uschar * recipient, * errmess; int terminator = *ss; int start, end, domain; @@ -2419,22 +2457,23 @@ for (int i = 0; i < recipients_count; i++) *ss = terminator; /* If we found a valid recipient that has a domain, compare it with the - envelope recipient. Local parts are compared case-sensitively, domains - case-insensitively. By comparing from the start with length "domain", we - include the "@" at the end, which ensures that we are comparing the whole - local part of each address. */ - - if (recipient != NULL && domain != 0) - { - found = Ustrncmp(recipient, address, domain) == 0 && - strcmpic(recipient + domain, address + domain) == 0; - if (found) break; - } + envelope recipient. Local parts are compared with case-sensitivity + according to the routine arg, domains case-insensitively. + By comparing from the start with length "domain", we include the "@" at + the end, which ensures that we are comparing the whole local part of each + address. */ + + if (recipient && domain != 0) + if ((found = (case_sensitive + ? Ustrncmp(recipient, address, domain) == 0 + : strncmpic(recipient, address, domain) == 0) + && strcmpic(recipient + domain, address + domain) == 0)) + break; /* Advance to the next address */ - s = ss + (terminator? 1:0); - while (isspace(*s)) s++; + s = ss + (terminator ? 1:0); + Uskip_whitespace(&s); } /* Next address */ f.parse_allow_group = FALSE; @@ -2587,7 +2626,7 @@ for (int i = 0; i < 3 && !done; i++) /* If we found an empty address, just carry on with the next one, but kill the message. */ - if (address == NULL && Ustrcmp(*log_msgptr, "empty address") == 0) + if (!address && Ustrcmp(*log_msgptr, "empty address") == 0) { *log_msgptr = NULL; s = ss; @@ -2598,7 +2637,7 @@ for (int i = 0; i < 3 && !done; i++) function, and ensure that the failing address gets added to the error message. */ - if (address == NULL) + if (!address) { new_ok = FAIL; while (ss > s && isspace(ss[-1])) ss--; @@ -2754,7 +2793,7 @@ for (;;) int size = sizeof(buffer) - (p - buffer); if (size <= 0) goto END_OFF; /* Buffer filled without seeing \n. */ - count = ip_recv(&ident_conn_ctx, p, size, rfc1413_query_timeout); + count = ip_recv(&ident_conn_ctx, p, size, time(NULL) + rfc1413_query_timeout); if (count <= 0) goto END_OFF; /* Read error or EOF */ /* Scan what we just read, to see if we have reached the terminating \r\n. Be @@ -2860,35 +2899,33 @@ Returns: OK matched */ int -check_host(void *arg, const uschar *ss, const uschar **valueptr, uschar **error) +check_host(void * arg, const uschar * ss, const uschar ** valueptr, uschar ** error) { -check_host_block *cb = (check_host_block *)arg; +check_host_block * cb = (check_host_block *)arg; int mlen = -1; int maskoffset; -BOOL iplookup = FALSE; -BOOL isquery = FALSE; -BOOL isiponly = cb->host_name != NULL && cb->host_name[0] == 0; -const uschar *t; -uschar *semicolon; -uschar **aliases; +BOOL iplookup = FALSE, isquery = FALSE; +BOOL isiponly = cb->host_name && !cb->host_name[0]; +const uschar * t; +uschar * semicolon, * endname, * opts; +uschar ** aliases; /* Optimize for the special case when the pattern is "*". */ -if (*ss == '*' && ss[1] == 0) return OK; +if (*ss == '*' && !ss[1]) return OK; /* If the pattern is empty, it matches only in the case when there is no host - this can occur in ACL checking for SMTP input using the -bs option. In this situation, the host address is the empty string. */ -if (cb->host_address[0] == 0) return (*ss == 0)? OK : FAIL; -if (*ss == 0) return FAIL; +if (!cb->host_address[0]) return *ss ? FAIL : OK; +if (!*ss) return FAIL; /* If the pattern is precisely "@" then match against the primary host name, provided that host name matching is permitted; if it's "@[]" match against the local host's IP addresses. */ if (*ss == '@') - { if (ss[1] == 0) { if (isiponly) return ERROR; @@ -2900,7 +2937,6 @@ if (*ss == '@') if (Ustrcmp(ip->address, cb->host_address) == 0) return OK; return FAIL; } - } /* If the pattern is an IP address, optionally followed by a bitmask count, do a (possibly masked) comparison with the current IP address. */ @@ -2920,23 +2956,32 @@ course slashes may be present in lookups, but not preceded only by digits and dots). */ for (t = ss; isdigit(*t) || *t == '.'; ) t++; -if (*t == 0 || (*t == '/' && t != ss)) +if (!*t || (*t == '/' && t != ss)) { *error = US"malformed IPv4 address or address mask"; return ERROR; } -/* See if there is a semicolon in the pattern */ +/* See if there is a semicolon in the pattern, separating a searchtype +prefix. If there is one then check for comma-sep options. */ -semicolon = Ustrchr(ss, ';'); +if ((semicolon = Ustrchr(ss, ';'))) + if ((opts = Ustrchr(ss, ',')) && opts < semicolon) + { + endname = opts++; + opts = string_copyn(opts, semicolon - opts); + } + else + { + endname = semicolon; + opts = NULL; + } /* If we are doing an IP address only match, then all lookups must be IP address lookups, even if there is no "net-". */ if (isiponly) - { iplookup = semicolon != NULL; - } /* Otherwise, if the item is of the form net[n]-lookup; then it is a lookup on a masked IP network, in textual form. We obey this code even if we @@ -2946,12 +2991,12 @@ key is implicit. For query-style lookups the key is specified in the query. From release 4.30, the use of net- for query style is no longer needed, but we retain it for backward compatibility. */ -if (Ustrncmp(ss, "net", 3) == 0 && semicolon != NULL) +if (Ustrncmp(ss, "net", 3) == 0 && semicolon) { mlen = 0; for (t = ss + 3; isdigit(*t); t++) mlen = mlen * 10 + *t - '0'; if (mlen == 0 && t == ss+3) mlen = -1; /* No mask supplied */ - iplookup = (*t++ == '-'); + iplookup = *t++ == '-'; } else t = ss; @@ -2969,7 +3014,7 @@ if (iplookup) /* Find the search type */ - search_type = search_findtype(t, semicolon - t); + search_type = search_findtype(t, endname - t); if (search_type < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", search_error_message); @@ -3012,7 +3057,7 @@ if (iplookup) if (!(handle = search_open(filename, search_type, 0, NULL, NULL))) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", search_error_message); - result = search_find(handle, filename, key, -1, NULL, 0, 0, NULL); + result = search_find(handle, filename, key, -1, NULL, 0, 0, NULL, opts); if (valueptr) *valueptr = result; return result ? OK : f.search_find_defer ? DEFER: FAIL; } @@ -3032,7 +3077,7 @@ digits, full stops, and hyphens (the constituents of domain names). Allow underscores, as they are all too commonly found. Sigh. Also, if allow_utf8_domains is set, allow top-bit characters. */ -for (t = ss; *t != 0; t++) +for (t = ss; *t; t++) if (!isalnum(*t) && *t != '.' && *t != '-' && *t != '_' && (!allow_utf8_domains || *t < 128)) break; @@ -3040,7 +3085,7 @@ for (t = ss; *t != 0; t++) its IP address and match against that. Note that a multi-homed host will add items to the chain. */ -if (*t == 0) +if (!*t) { int rc; host_item h; @@ -3070,9 +3115,9 @@ using the general string matching function. When this function is called for outgoing hosts, the name is always given explicitly. If it is NULL, it means we must use sender_host_name and its aliases, looking them up if necessary. */ -if (cb->host_name != NULL) /* Explicit host name given */ - return match_check_string(cb->host_name, ss, -1, TRUE, TRUE, TRUE, - valueptr); +if (cb->host_name) /* Explicit host name given */ + return match_check_string(cb->host_name, ss, -1, + MCS_PARTIAL | MCS_CASELESS | MCS_AT_SPECIAL | cb->flags, valueptr); /* Host name not given; in principle we need the sender host name and its aliases. However, for query-style lookups, we do not need the name if the @@ -3080,13 +3125,14 @@ query does not contain $sender_host_name. From release 4.23, a reference to $sender_host_name causes it to be looked up, so we don't need to do the lookup on spec. */ -if ((semicolon = Ustrchr(ss, ';')) != NULL) +if ((semicolon = Ustrchr(ss, ';'))) { - const uschar *affix; + const uschar * affix, * opts; int partial, affixlen, starflags, id; *semicolon = 0; - id = search_findtype_partial(ss, &partial, &affix, &affixlen, &starflags); + id = search_findtype_partial(ss, &partial, &affix, &affixlen, &starflags, + &opts); *semicolon=';'; if (id < 0) /* Unknown lookup type */ @@ -3100,7 +3146,9 @@ if ((semicolon = Ustrchr(ss, ';')) != NULL) if (isquery) { - switch(match_check_string(US"", ss, -1, TRUE, TRUE, TRUE, valueptr)) + switch(match_check_string(US"", ss, -1, + MCS_PARTIAL| MCS_CASELESS| MCS_AT_SPECIAL | (cb->flags & MCS_CACHEABLE), + valueptr)) { case OK: return OK; case DEFER: return DEFER; @@ -3126,7 +3174,9 @@ if (!sender_host_name) /* Match on the sender host name, using the general matching function */ -switch(match_check_string(sender_host_name, ss, -1, TRUE, TRUE, TRUE, valueptr)) +switch(match_check_string(sender_host_name, ss, -1, + MCS_PARTIAL| MCS_CASELESS| MCS_AT_SPECIAL | (cb->flags & MCS_CACHEABLE), + valueptr)) { case OK: return OK; case DEFER: return DEFER; @@ -3136,7 +3186,9 @@ switch(match_check_string(sender_host_name, ss, -1, TRUE, TRUE, TRUE, valueptr)) aliases = sender_host_aliases; while (*aliases) - switch(match_check_string(*aliases++, ss, -1, TRUE, TRUE, TRUE, valueptr)) + switch(match_check_string(*aliases++, ss, -1, + MCS_PARTIAL| MCS_CASELESS| MCS_AT_SPECIAL | (cb->flags & MCS_CACHEABLE), + valueptr)) { case OK: return OK; case DEFER: return DEFER; @@ -3212,8 +3264,8 @@ rc = match_check_list( check_host, /* function for testing */ &cb, /* argument for function */ MCL_HOST, /* type of check */ - (host_address == sender_host_address)? - US"host" : host_address, /* text for debugging */ + host_address == sender_host_address + ? US"host" : host_address, /* text for debugging */ valueptr); /* where to pass back data */ deliver_host_address = save_host_address; return rc; @@ -3251,7 +3303,7 @@ int verify_check_host(uschar **listptr) { return verify_check_this_host(CUSS listptr, sender_host_cache, NULL, - (sender_host_address == NULL)? US"" : sender_host_address, NULL); + sender_host_address ? sender_host_address : US"", NULL); } @@ -3322,553 +3374,246 @@ same format string, "%s.%s" */ -/************************************************* -* Perform a single dnsbl lookup * -*************************************************/ +/**************************************************** + Verify a local user account for quota sufficiency +****************************************************/ -/* This function is called from verify_check_dnsbl() below. It is also called -recursively from within itself when domain and domain_txt are different -pointers, in order to get the TXT record from the alternate domain. +/* The real work, done via a re-exec for privs, calls +down to the transport for the quota check. -Arguments: - domain the outer dnsbl domain - domain_txt alternate domain to lookup TXT record on success; when the - same domain is to be used, domain_txt == domain (that is, - the pointers must be identical, not just the text) - keydomain the current keydomain (for debug message) - prepend subdomain to lookup (like keydomain, but - reversed if IP address) - iplist the list of matching IP addresses, or NULL for "any" - bitmask true if bitmask matching is wanted - match_type condition for 'succeed' result - 0 => Any RR in iplist (=) - 1 => No RR in iplist (!=) - 2 => All RRs in iplist (==) - 3 => Some RRs not in iplist (!==) - the two bits are defined as MT_NOT and MT_ALL - defer_return what to return for a defer - -Returns: OK if lookup succeeded - FAIL if not -*/ +Route and transport (in recipient-verify mode) the +given recipient. -static int -one_check_dnsbl(uschar *domain, uschar *domain_txt, uschar *keydomain, - uschar *prepend, uschar *iplist, BOOL bitmask, int match_type, - int defer_return) -{ -dns_answer dnsa; -dns_scan dnss; -tree_node *t; -dnsbl_cache_block *cb; -int old_pool = store_pool; -uschar query[256]; /* DNS domain max length */ - -/* Construct the specific query domainname */ - -if (!string_format(query, sizeof(query), "%s.%s", prepend, domain)) - { - log_write(0, LOG_MAIN|LOG_PANIC, "dnslist query is too long " - "(ignored): %s...", query); - return FAIL; - } - -/* Look for this query in the cache. */ - -if ( (t = tree_search(dnsbl_cache, query)) - && (cb = t->data.ptr)->expiry > time(NULL) - ) - -/* Previous lookup was cached */ +A routing result indicating any transport type other than appendfile +results in a fail. - { - HDEBUG(D_dnsbl) debug_printf("using result of previous DNS lookup\n"); - } +Return, on stdout, a result string containing: +- highlevel result code (OK, DEFER, FAIL) +- errno +- where string +- message string +*/ -/* If not cached from a previous lookup, we must do a DNS lookup, and -cache the result in permanent memory. */ +void +verify_quota(uschar * address) +{ +address_item vaddr = {.address = address}; +BOOL routed; +uschar * msg = US"\0"; +int rc, len = 1; -else +if ((rc = verify_address(&vaddr, NULL, vopt_is_recipient | vopt_quota, + 1, 0, 0, NULL, NULL, &routed)) != OK) { - uint ttl = 3600; - - store_pool = POOL_PERM; - - if (t) + uschar * where = recipient_verify_failure; + msg = acl_verify_message ? acl_verify_message : vaddr.message; + if (!msg) msg = US""; + if (rc == DEFER && vaddr.basic_errno == ERRNO_EXIMQUOTA) { - HDEBUG(D_dnsbl) debug_printf("cached data found but past valid time; "); + rc = FAIL; /* DEFER -> FAIL */ + where = US"quota"; + vaddr.basic_errno = 0; } + else if (!where) where = US""; - else - { /* Set up a tree entry to cache the lookup */ - t = store_get(sizeof(tree_node) + Ustrlen(query)); - Ustrcpy(t->name, query); - t->data.ptr = cb = store_get(sizeof(dnsbl_cache_block)); - (void)tree_insertnode(&dnsbl_cache, t); - } - - /* Do the DNS lookup . */ - - HDEBUG(D_dnsbl) debug_printf("new DNS lookup for %s\n", query); - cb->rc = dns_basic_lookup(&dnsa, query, T_A); - cb->text_set = FALSE; - cb->text = NULL; - cb->rhs = NULL; - - /* If the lookup succeeded, cache the RHS address. The code allows for - more than one address - this was for complete generality and the possible - use of A6 records. However, A6 records are no longer supported. Leave the code - here, just in case. - - Quite apart from one A6 RR generating multiple addresses, there are DNS - lists that return more than one A record, so we must handle multiple - addresses generated in that way as well. - - Mark the cache entry with the "now" plus the minimum of the address TTLs, - or some suitably far-future time if none were found. */ - - if (cb->rc == DNS_SUCCEED) - { - dns_address ** addrp = &(cb->rhs); - for (dns_record * rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); rr; - rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) - if (rr->type == T_A) - { - dns_address *da = dns_address_from_rr(&dnsa, rr); - if (da) - { - *addrp = da; - while (da->next) da = da->next; - addrp = &da->next; - if (ttl > rr->ttl) ttl = rr->ttl; - } - } - - /* If we didn't find any A records, change the return code. This can - happen when there is a CNAME record but there are no A records for what - it points to. */ - - if (!cb->rhs) cb->rc = DNS_NODATA; - } - - cb->expiry = time(NULL)+ttl; - store_pool = old_pool; + len = 5 + Ustrlen(msg) + 1 + Ustrlen(where); + msg = string_sprintf("%c%c%c%c%c%s%c%s", (uschar)rc, + (vaddr.basic_errno >> 24) & 0xff, (vaddr.basic_errno >> 16) & 0xff, + (vaddr.basic_errno >> 8) & 0xff, vaddr.basic_errno & 0xff, + where, '\0', msg); } -/* We now have the result of the DNS lookup, either newly done, or cached -from a previous call. If the lookup succeeded, check against the address -list if there is one. This may be a positive equality list (introduced by -"="), a negative equality list (introduced by "!="), a positive bitmask -list (introduced by "&"), or a negative bitmask list (introduced by "!&").*/ - -if (cb->rc == DNS_SUCCEED) - { - dns_address *da = NULL; - uschar *addlist = cb->rhs->address; - - /* For A and AAAA records, there may be multiple addresses from multiple - records. For A6 records (currently not expected to be used) there may be - multiple addresses from a single record. */ - - for (da = cb->rhs->next; da; da = da->next) - addlist = string_sprintf("%s, %s", addlist, da->address); - - HDEBUG(D_dnsbl) debug_printf("DNS lookup for %s succeeded (yielding %s)\n", - query, addlist); - - /* Address list check; this can be either for equality, or via a bitmask. - In the latter case, all the bits must match. */ - - if (iplist) - { - for (da = cb->rhs; da; da = da->next) - { - int ipsep = ','; - uschar ip[46]; - const uschar *ptr = iplist; - uschar *res; - - /* Handle exact matching */ - - if (!bitmask) - { - while ((res = string_nextinlist(&ptr, &ipsep, ip, sizeof(ip)))) - if (Ustrcmp(CS da->address, ip) == 0) - break; - } - - /* Handle bitmask matching */ - - else - { - int address[4]; - int mask = 0; - - /* At present, all known DNS blocking lists use A records, with - IPv4 addresses on the RHS encoding the information they return. I - wonder if this will linger on as the last vestige of IPv4 when IPv6 - is ubiquitous? Anyway, for now we use paranoia code to completely - ignore IPv6 addresses. The default mask is 0, which always matches. - We change this only for IPv4 addresses in the list. */ - - if (host_aton(da->address, address) == 1) mask = address[0]; - - /* Scan the returned addresses, skipping any that are IPv6 */ - - while ((res = string_nextinlist(&ptr, &ipsep, ip, sizeof(ip)))) - { - if (host_aton(ip, address) != 1) continue; - if ((address[0] & mask) == address[0]) break; - } - } - - /* If either - - (a) An IP address in an any ('=') list matched, or - (b) No IP address in an all ('==') list matched - - then we're done searching. */ - - if (((match_type & MT_ALL) != 0) == (res == NULL)) break; - } - - /* If da == NULL, either - - (a) No IP address in an any ('=') list matched, or - (b) An IP address in an all ('==') list didn't match - - so behave as if the DNSBL lookup had not succeeded, i.e. the host is not on - the list. */ - - if ((match_type == MT_NOT || match_type == MT_ALL) != (da == NULL)) - { - HDEBUG(D_dnsbl) - { - uschar *res = NULL; - switch(match_type) - { - case 0: - res = US"was no match"; break; - case MT_NOT: - res = US"was an exclude match"; break; - case MT_ALL: - res = US"was an IP address that did not match"; break; - case MT_NOT|MT_ALL: - res = US"were no IP addresses that did not match"; break; - } - debug_printf("=> but we are not accepting this block class because\n"); - debug_printf("=> there %s for %s%c%s\n", - res, - ((match_type & MT_ALL) == 0)? "" : "=", - bitmask? '&' : '=', iplist); - } - return FAIL; - } - } - - /* Either there was no IP list, or the record matched, implying that the - domain is on the list. We now want to find a corresponding TXT record. If an - alternate domain is specified for the TXT record, call this function - recursively to look that up; this has the side effect of re-checking that - there is indeed an A record at the alternate domain. */ +DEBUG(D_verify) debug_printf_indent("verify_quota: len %d\n", len); +write(1, msg, len); +return; +} - if (domain_txt != domain) - return one_check_dnsbl(domain_txt, domain_txt, keydomain, prepend, NULL, - FALSE, match_type, defer_return); - /* If there is no alternate domain, look up a TXT record in the main domain - if it has not previously been cached. */ +/******************************************************************************/ - if (!cb->text_set) - { - cb->text_set = TRUE; - if (dns_basic_lookup(&dnsa, query, T_TXT) == DNS_SUCCEED) - for (dns_record * rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); rr; - rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) - if (rr->type == T_TXT) - { - int len = (rr->data)[0]; - if (len > 511) len = 127; - store_pool = POOL_PERM; - cb->text = string_sprintf("%.*s", len, CUS (rr->data+1)); - store_pool = old_pool; - break; - } - } - - dnslist_value = addlist; - dnslist_text = cb->text; - return OK; - } +/* Quota cache lookup. We use the callout hints db also for the quota cache. +Return TRUE if a nonexpired record was found, having filled in the yield +argument. +*/ -/* There was a problem with the DNS lookup */ +static BOOL +cached_quota_lookup(const uschar * rcpt, int * yield, + int pos_cache, int neg_cache) +{ +open_db dbblock, *dbm_file = NULL; +dbdata_callout_cache_address * cache_address_record; -if (cb->rc != DNS_NOMATCH && cb->rc != DNS_NODATA) +if (!pos_cache && !neg_cache) + return FALSE; +if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE, TRUE))) { - log_write(L_dnslist_defer, LOG_MAIN, - "DNS list lookup defer (probably timeout) for %s: %s", query, - (defer_return == OK)? US"assumed in list" : - (defer_return == FAIL)? US"assumed not in list" : - US"returned DEFER"); - return defer_return; + HDEBUG(D_verify) debug_printf_indent("quota cache: not available\n"); + return FALSE; } - -/* No entry was found in the DNS; continue for next domain */ - -HDEBUG(D_dnsbl) +if (!(cache_address_record = (dbdata_callout_cache_address *) + get_callout_cache_record(dbm_file, rcpt, US"address", + pos_cache, neg_cache))) { - debug_printf("DNS lookup for %s failed\n", query); - debug_printf("=> that means %s is not listed at %s\n", - keydomain, domain); + dbfn_close(dbm_file); + return FALSE; } - -return FAIL; +if (cache_address_record->result == ccache_accept) + *yield = OK; +dbfn_close(dbm_file); +return TRUE; } +/* Quota cache write */ +static void +cache_quota_write(const uschar * rcpt, int yield, int pos_cache, int neg_cache) +{ +open_db dbblock, *dbm_file = NULL; +dbdata_callout_cache_address cache_address_record; +if (!pos_cache && !neg_cache) + return; +if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE))) + { + HDEBUG(D_verify) debug_printf_indent("quota cache: not available\n"); + return; + } -/************************************************* -* Check host against DNS black lists * -*************************************************/ - -/* This function runs checks against a list of DNS black lists, until one -matches. Each item on the list can be of the form - - domain=ip-address/key - -The domain is the right-most domain that is used for the query, for example, -blackholes.mail-abuse.org. If the IP address is present, there is a match only -if the DNS lookup returns a matching IP address. Several addresses may be -given, comma-separated, for example: x.y.z=127.0.0.1,127.0.0.2. - -If no key is given, what is looked up in the domain is the inverted IP address -of the current client host. If a key is given, it is used to construct the -domain for the lookup. For example: - - dsn.rfc-ignorant.org/$sender_address_domain - -After finding a match in the DNS, the domain is placed in $dnslist_domain, and -then we check for a TXT record for an error message, and if found, save its -value in $dnslist_text. We also cache everything in a tree, to optimize -multiple lookups. +cache_address_record.result = yield == OK ? ccache_accept : ccache_reject; -The TXT record is normally looked up in the same domain as the A record, but -when many lists are combined in a single DNS domain, this will not be a very -specific message. It is possible to specify a different domain for looking up -TXT records; this is given before the main domain, comma-separated. For -example: +(void)dbfn_write(dbm_file, rcpt, &cache_address_record, + (int)sizeof(dbdata_callout_cache_address)); +HDEBUG(D_verify) debug_printf_indent("wrote %s quota cache record for %s\n", + yield == OK ? "positive" : "negative", rcpt); - dnslists = http.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.2 : \ - socks.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.3 +dbfn_close(dbm_file); +return; +} -The caching ensures that only one lookup in dnsbl.sorbs.net is done. -Note: an address for testing RBL is 192.203.178.39 -Note: an address for testing DUL is 192.203.178.4 -Note: a domain for testing RFCI is example.tld.dsn.rfc-ignorant.org +/* To evaluate a local user's quota, starting in ACL, we need to +fork & exec to regain privileges, to that we can change to the user's +identity for access to their files. Arguments: - where the acl type - listptr the domain/address/data list - log_msgptr log message on error - -Returns: OK successful lookup (i.e. the address is on the list), or - lookup deferred after +include_unknown - FAIL name not found, or no data found for the given type, or - lookup deferred after +exclude_unknown (default) - DEFER lookup failure, if +defer_unknown was set + rcpt Recipient account + pos_cache Number of seconds to cache a positive result (delivery + to be accepted). Zero to disable caching. + neg_cache Number of seconds to cache a negative result. Zero to disable. + msg Pointer to result string pointer + +Return: OK/DEFER/FAIL code */ int -verify_check_dnsbl(int where, const uschar ** listptr, uschar ** log_msgptr) +verify_quota_call(const uschar * rcpt, int pos_cache, int neg_cache, + uschar ** msg) { -int sep = 0; -int defer_return = FAIL; -const uschar *list = *listptr; -uschar *domain; -uschar buffer[1024]; -uschar revadd[128]; /* Long enough for IPv6 address */ - -/* Indicate that the inverted IP address is not yet set up */ - -revadd[0] = 0; - -/* In case this is the first time the DNS resolver is being used. */ - -dns_init(FALSE, FALSE, FALSE); /*XXX dnssec? */ +int pfd[2], pid, save_errno, yield = FAIL; +void (*oldsignal)(int); +const uschar * where = US"socketpair"; -/* Loop through all the domains supplied, until something matches */ +*msg = NULL; -while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) +if (cached_quota_lookup(rcpt, &yield, pos_cache, neg_cache)) { - int rc; - BOOL bitmask = FALSE; - int match_type = 0; - uschar *domain_txt; - uschar *comma; - uschar *iplist; - uschar *key; - - HDEBUG(D_dnsbl) debug_printf("DNS list check: %s\n", domain); - - /* Deal with special values that change the behaviour on defer */ - - if (domain[0] == '+') - { - if (strcmpic(domain, US"+include_unknown") == 0) defer_return = OK; - else if (strcmpic(domain, US"+exclude_unknown") == 0) defer_return = FAIL; - else if (strcmpic(domain, US"+defer_unknown") == 0) defer_return = DEFER; - else - log_write(0, LOG_MAIN|LOG_PANIC, "unknown item in dnslist (ignored): %s", - domain); - continue; - } - - /* See if there's explicit data to be looked up */ - - if ((key = Ustrchr(domain, '/'))) *key++ = 0; - - /* See if there's a list of addresses supplied after the domain name. This is - introduced by an = or a & character; if preceded by = we require all matches - and if preceded by ! we invert the result. */ - - if (!(iplist = Ustrchr(domain, '='))) + HDEBUG(D_verify) debug_printf_indent("quota cache: address record is %s\n", + yield == OK ? "positive" : "negative"); + if (yield != OK) { - bitmask = TRUE; - iplist = Ustrchr(domain, '&'); + recipient_verify_failure = US"quota"; + acl_verify_message = *msg = + US"Previous (cached) quota verification failure"; } + return yield; + } - if (iplist) /* Found either = or & */ - { - if (iplist > domain && iplist[-1] == '!') /* Handle preceding ! */ - { - match_type |= MT_NOT; - iplist[-1] = 0; - } - - *iplist++ = 0; /* Terminate domain, move on */ +if (pipe(pfd) != 0) + goto fail; - /* If we found = (bitmask == FALSE), check for == or =& */ +where = US"fork"; +oldsignal = signal(SIGCHLD, SIG_DFL); +if ((pid = exim_fork(US"quota-verify")) < 0) + { + save_errno = errno; + close(pfd[pipe_write]); + close(pfd[pipe_read]); + errno = save_errno; + goto fail; + } - if (!bitmask && (*iplist == '=' || *iplist == '&')) - { - bitmask = *iplist++ == '&'; - match_type |= MT_ALL; - } - } +if (pid == 0) /* child */ + { + close(pfd[pipe_read]); + force_fd(pfd[pipe_write], 1); /* stdout to pipe */ + close(pfd[pipe_write]); + dup2(1, 0); + if (debug_fd > 0) force_fd(debug_fd, 2); + + child_exec_exim(CEE_EXEC_EXIT, FALSE, NULL, FALSE, 3, + US"-MCq", string_sprintf("%d", message_size), rcpt); + /*NOTREACHED*/ + } +save_errno = errno; +close(pfd[pipe_write]); - /* If there is a comma in the domain, it indicates that a second domain for - looking up TXT records is provided, before the main domain. Otherwise we must - set domain_txt == domain. */ +if (pid < 0) + { + DEBUG(D_verify) debug_printf_indent(" fork: %s\n", strerror(save_errno)); + } +else + { + uschar buf[128]; + int n = read(pfd[pipe_read], buf, sizeof(buf)); + int status; - domain_txt = domain; - comma = Ustrchr(domain, ','); - if (comma != NULL) + waitpid(pid, &status, 0); + if (status == 0) { - *comma++ = 0; - domain = comma; - } - - /* Check that what we have left is a sensible domain name. There is no reason - why these domains should in fact use the same syntax as hosts and email - domains, but in practice they seem to. However, there is little point in - actually causing an error here, because that would no doubt hold up incoming - mail. Instead, I'll just log it. */ - - for (uschar * s = domain; *s; s++) - if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_') - { - log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains " - "strange characters - is this right?", domain); - break; - } + uschar * s; - /* Check the alternate domain if present */ - - if (domain_txt != domain) for (uschar * s = domain_txt; *s; s++) - if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_') + if (n > 0) yield = buf[0]; + if (n > 4) + save_errno = (buf[1] << 24) | (buf[2] << 16) | (buf[3] << 8) | buf[4]; + if ((recipient_verify_failure = n > 5 + ? string_copyn_taint(buf+5, n-5, GET_UNTAINTED) : NULL)) { - log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains " - "strange characters - is this right?", domain_txt); - break; + int m; + s = buf + 5 + Ustrlen(recipient_verify_failure) + 1; + m = n - (s - buf); + acl_verify_message = *msg = + m > 0 ? string_copyn_taint(s, m, GET_UNTAINTED) : NULL; } - /* If there is no key string, construct the query by adding the domain name - onto the inverted host address, and perform a single DNS lookup. */ + DEBUG(D_verify) debug_printf_indent("verify call response:" + " len %d yield %s errno '%s' where '%s' msg '%s'\n", + n, rc_names[yield], strerror(save_errno), recipient_verify_failure, *msg); - if (!key) - { - if (where == ACL_WHERE_NOTSMTP_START || where == ACL_WHERE_NOTSMTP) - { - *log_msgptr = string_sprintf - ("cannot test auto-keyed dnslists condition in %s ACL", - acl_wherenames[where]); - return ERROR; - } - if (sender_host_address == NULL) return FAIL; /* can never match */ - if (revadd[0] == 0) invert_address(revadd, sender_host_address); - rc = one_check_dnsbl(domain, domain_txt, sender_host_address, revadd, - iplist, bitmask, match_type, defer_return); - if (rc == OK) - { - dnslist_domain = string_copy(domain_txt); - dnslist_matched = string_copy(sender_host_address); - HDEBUG(D_dnsbl) debug_printf("=> that means %s is listed at %s\n", - sender_host_address, dnslist_domain); - } - if (rc != FAIL) return rc; /* OK or DEFER */ + if ( yield == OK + || save_errno == 0 && Ustrcmp(recipient_verify_failure, "quota") == 0) + cache_quota_write(rcpt, yield, pos_cache, neg_cache); + else DEBUG(D_verify) + debug_printf_indent("result not cacheable\n"); } - - /* If there is a key string, it can be a list of domains or IP addresses to - be concatenated with the main domain. */ - else { - int keysep = 0; - BOOL defer = FALSE; - uschar *keydomain; - uschar keybuffer[256]; - uschar keyrevadd[128]; - - while ((keydomain = string_nextinlist(CUSS &key, &keysep, keybuffer, - sizeof(keybuffer))) != NULL) - { - uschar *prepend = keydomain; - - if (string_is_ip_address(keydomain, NULL) != 0) - { - invert_address(keyrevadd, keydomain); - prepend = keyrevadd; - } - - rc = one_check_dnsbl(domain, domain_txt, keydomain, prepend, iplist, - bitmask, match_type, defer_return); - - if (rc == OK) - { - dnslist_domain = string_copy(domain_txt); - dnslist_matched = string_copy(keydomain); - HDEBUG(D_dnsbl) debug_printf("=> that means %s is listed at %s\n", - keydomain, dnslist_domain); - return OK; - } - - /* If the lookup deferred, remember this fact. We keep trying the rest - of the list to see if we get a useful result, and if we don't, we return - DEFER at the end. */ - - if (rc == DEFER) defer = TRUE; - } /* continue with next keystring domain/address */ - - if (defer) return DEFER; + DEBUG(D_verify) + debug_printf_indent("verify call response: waitpid status 0x%04x\n", status); } - } /* continue with next dnsdb outer domain */ + } -return FAIL; +close(pfd[pipe_read]); +signal(SIGCHLD, oldsignal); +errno = save_errno; +return yield; + +fail: +DEBUG(D_verify) debug_printf_indent("verify_quota_call fail in %s\n", where); +return yield; } + /* vi: aw ai sw=2 */ /* End of verify.c */