X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/d8ef35773b4c4c25c62a015928ed92b4a654b501..ce5524496cf016f06c8004e00ce9d043f4ac6aff:/src/src/verify.c diff --git a/src/src/verify.c b/src/src/verify.c index e8d43eed9..ab7e2756f 100644 --- a/src/src/verify.c +++ b/src/src/verify.c @@ -1,10 +1,10 @@ -/* $Cambridge: exim/src/src/verify.c,v 1.5 2004/11/12 16:54:55 ph10 Exp $ */ +/* $Cambridge: exim/src/src/verify.c,v 1.52 2008/09/29 11:41:07 nm4 Exp $ */ /************************************************* * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2004 */ +/* Copyright (c) University of Cambridge 1995 - 2007 */ /* See the file NOTICE for conditions of use and distribution. */ /* Functions concerned with verifying things. The original code for callout @@ -29,6 +29,12 @@ typedef struct dnsbl_cache_block { static tree_node *dnsbl_cache = NULL; +/* Bits for match_type in one_check_dnsbl() */ + +#define MT_NOT 1 +#define MT_ALL 2 + + /************************************************* * Retrieve a callout cache record * @@ -128,6 +134,7 @@ Arguments: options the verification options - these bits are used: vopt_is_recipient => this is a recipient address vopt_callout_no_cache => don't use callout cache + vopt_callout_fullpm => if postmaster check, do full one vopt_callout_random => do the "random" thing vopt_callout_recipsender => use real sender for recipient vopt_callout_recippmaster => use postmaster for recipient @@ -139,7 +146,7 @@ Returns: OK/FAIL/DEFER static int do_callout(address_item *addr, host_item *host_list, transport_feedback *tf, - int callout, int callout_overall, int callout_connect, int options, + int callout, int callout_overall, int callout_connect, int options, uschar *se_mailfrom, uschar *pm_mailfrom) { BOOL is_recipient = (options & vopt_is_recipient) != 0; @@ -147,11 +154,13 @@ BOOL callout_no_cache = (options & vopt_callout_no_cache) != 0; BOOL callout_random = (options & vopt_callout_random) != 0; int yield = OK; +int old_domain_cache_result = ccache_accept; BOOL done = FALSE; uschar *address_key; uschar *from_address; uschar *random_local_part = NULL; -uschar **failure_ptr = is_recipient? +uschar *save_deliver_domain = deliver_domain; +uschar **failure_ptr = is_recipient? &recipient_verify_failure : &sender_verify_failure; open_db dbblock; open_db *dbm_file = NULL; @@ -226,10 +235,18 @@ if (dbm_file != NULL) if (cache_record != NULL) { - /* If an early command (up to and including MAIL FROM:<>) was rejected, - there is no point carrying on. The callout fails. */ - - if (cache_record->result == ccache_reject) + /* In most cases, if an early command (up to and including MAIL FROM:<>) + was rejected, there is no point carrying on. The callout fails. However, if + we are doing a recipient verification with use_sender or use_postmaster + set, a previous failure of MAIL FROM:<> doesn't count, because this time we + will be using a non-empty sender. We have to remember this situation so as + not to disturb the cached domain value if this whole verification succeeds + (we don't want it turning into "accept"). */ + + old_domain_cache_result = cache_record->result; + + if (cache_record->result == ccache_reject || + (*from_address == 0 && cache_record->result == ccache_reject_mfnull)) { setflag(addr, af_verify_nsfail); HDEBUG(D_verify) @@ -238,7 +255,7 @@ if (dbm_file != NULL) setflag(addr, af_verify_nsfail); addr->user_message = US"(result of an earlier callout reused)."; yield = FAIL; - *failure_ptr = US"mail"; + *failure_ptr = US"mail"; goto END_CALLOUT; } @@ -285,7 +302,7 @@ if (dbm_file != NULL) debug_printf("callout cache: domain does not accept " "RCPT TO:\n"); yield = FAIL; - *failure_ptr = US"postmaster"; + *failure_ptr = US"postmaster"; setflag(addr, af_verify_pmfail); addr->user_message = US"(result of earlier verification reused)."; goto END_CALLOUT; @@ -334,7 +351,7 @@ if (dbm_file != NULL) HDEBUG(D_verify) debug_printf("callout cache: address record is negative\n"); addr->user_message = US"Previous (cached) callout verification failure"; - *failure_ptr = US"recipient"; + *failure_ptr = US"recipient"; yield = FAIL; } goto END_CALLOUT; @@ -368,6 +385,14 @@ if (callout_overall < 0) callout_overall = 4 * callout; if (callout_connect < 0) callout_connect = callout; callout_start_time = time(NULL); +/* Before doing a real callout, if this is an SMTP connection, flush the SMTP +output because a callout might take some time. When PIPELINING is active and +there are many recipients, the total time for doing lots of callouts can add up +and cause the client to time out. So in this case we forgo the PIPELINING +optimization. */ + +if (smtp_out != NULL && !disable_callout_flush) mac_smtp_fflush(); + /* Now make connections to the hosts and do real callouts. The list of hosts is passed in as an argument. */ @@ -377,6 +402,8 @@ for (host = host_list; host != NULL && !done; host = host->next) smtp_outblock outblock; int host_af; int port = 25; + BOOL send_quit = TRUE; + uschar *active_hostname = smtp_active_hostname; uschar *helo = US"HELO"; uschar *interface = NULL; /* Outgoing interface to use; NULL => any */ uschar inbuffer[4096]; @@ -407,18 +434,21 @@ for (host = host_list; host != NULL && !done; host = host->next) host_af = (Ustrchr(host->address, ':') == NULL)? AF_INET:AF_INET6; - /* Expand and interpret the interface and port strings. This has to - be delayed till now, because they may expand differently for different - hosts. If there's a failure, log it, but carry on with the defaults. */ + /* Expand and interpret the interface and port strings. The latter will not + be used if there is a host-specific port (e.g. from a manualroute router). + This has to be delayed till now, because they may expand differently for + different hosts. If there's a failure, log it, but carry on with the + defaults. */ deliver_host = host->name; deliver_host_address = host->address; + deliver_domain = addr->domain; + if (!smtp_get_interface(tf->interface, host_af, addr, NULL, &interface, US"callout") || !smtp_get_port(tf->port, addr, &port, US"callout")) log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: %s", addr->address, addr->message); - deliver_host = deliver_host_address = NULL; /* Set HELO string according to the protocol */ @@ -450,53 +480,93 @@ for (host = host_list; host != NULL && !done; host = host->next) { addr->message = string_sprintf("could not connect to %s [%s]: %s", host->name, host->address, strerror(errno)); + deliver_host = deliver_host_address = NULL; + deliver_domain = save_deliver_domain; continue; } - /* Wait for initial response, and then run the initial SMTP commands. The - smtp_write_command() function leaves its command in big_buffer. This is - used in error responses. Initialize it in case the connection is - rejected. */ + /* Expand the helo_data string to find the host name to use. */ + + if (tf->helo_data != NULL) + { + uschar *s = expand_string(tf->helo_data); + if (s == NULL) + log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: failed to expand transport's " + "helo_data value for callout: %s", addr->address, + expand_string_message); + else active_hostname = s; + } + + deliver_host = deliver_host_address = NULL; + deliver_domain = save_deliver_domain; + + /* Wait for initial response, and send HELO. The smtp_write_command() + function leaves its command in big_buffer. This is used in error responses. + Initialize it in case the connection is rejected. */ Ustrcpy(big_buffer, "initial connection"); done = smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), '2', callout) && - smtp_write_command(&outblock, FALSE, "%s %s\r\n", helo, - smtp_active_hostname) >= 0 && + active_hostname) >= 0 && smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), - '2', callout) && + '2', callout); + + /* Failure to accept HELO is cached; this blocks the whole domain for all + senders. I/O errors and defer responses are not cached. */ + + if (!done) + { + *failure_ptr = US"mail"; /* At or before MAIL */ + if (errno == 0 && responsebuffer[0] == '5') + { + setflag(addr, af_verify_nsfail); + new_domain_record.result = ccache_reject; + } + } + /* Send the MAIL command */ + + else done = smtp_write_command(&outblock, FALSE, "MAIL FROM:<%s>\r\n", from_address) >= 0 && smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), '2', callout); - /* If the host gave an initial error, or does not accept HELO or MAIL - FROM:<>, arrange to cache this information, but don't record anything for an - I/O error or a defer. Do not cache rejections when a non-empty sender has - been used, because that blocks the whole domain for all senders. */ + /* If the host does not accept MAIL FROM:<>, arrange to cache this + information, but again, don't record anything for an I/O error or a defer. Do + not cache rejections of MAIL when a non-empty sender has been used, because + that blocks the whole domain for all senders. */ if (!done) { - *failure_ptr = US"mail"; + *failure_ptr = US"mail"; /* At or before MAIL */ if (errno == 0 && responsebuffer[0] == '5') { setflag(addr, af_verify_nsfail); - if (from_address[0] == 0) new_domain_record.result = ccache_reject; + if (from_address[0] == 0) + new_domain_record.result = ccache_reject_mfnull; } } /* Otherwise, proceed to check a "random" address (if required), then the given address, and the postmaster address (if required). Between each check, issue RSET, because some servers accept only one recipient after MAIL - FROM:<>. */ + FROM:<>. + + Before doing this, set the result in the domain cache record to "accept", + unless its previous value was ccache_reject_mfnull. In that case, the domain + rejects MAIL FROM:<> and we want to continue to remember that. When that is + the case, we have got here only in the case of a recipient verification with + a non-null sender. */ else { - new_domain_record.result = ccache_accept; + new_domain_record.result = + (old_domain_cache_result == ccache_reject_mfnull)? + ccache_reject_mfnull: ccache_accept; /* Do the random local part check first */ @@ -535,7 +605,8 @@ for (host = host_list; host != NULL && !done; host = host->next) smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), '2', callout) && - smtp_write_command(&outblock, FALSE, "MAIL FROM:<>\r\n") >= 0 && + smtp_write_command(&outblock, FALSE, "MAIL FROM:<%s>\r\n", + from_address) >= 0 && smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), '2', callout); } @@ -547,9 +618,14 @@ for (host = host_list; host != NULL && !done; host = host->next) if (new_domain_record.random_result != ccache_accept && done) { + /* Get the rcpt_include_affixes flag from the transport if there is one, + but assume FALSE if there is not. */ + done = smtp_write_command(&outblock, FALSE, "RCPT TO:<%.1000s>\r\n", - addr->address) >= 0 && + transport_rcpt_address(addr, + (addr->transport == NULL)? FALSE : + addr->transport->rcpt_include_affixes)) >= 0 && smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), '2', callout); @@ -557,11 +633,12 @@ for (host = host_list; host != NULL && !done; host = host->next) new_address_record.result = ccache_accept; else if (errno == 0 && responsebuffer[0] == '5') { - *failure_ptr = US"recipient"; + *failure_ptr = US"recipient"; new_address_record.result = ccache_reject; - } + } - /* Do postmaster check if requested */ + /* Do postmaster check if requested; if a full check is required, we + check for RCPT TO: (no domain) in accordance with RFC 821. */ if (done && pm_mailfrom != NULL) { @@ -575,10 +652,29 @@ for (host = host_list; host != NULL && !done; host = host->next) smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), '2', callout) && + /* First try using the current domain */ + + (( smtp_write_command(&outblock, FALSE, "RCPT TO:\r\n", addr->domain) >= 0 && smtp_read_response(&inblock, responsebuffer, - sizeof(responsebuffer), '2', callout); + sizeof(responsebuffer), '2', callout) + ) + + || + + /* If that doesn't work, and a full check is requested, + try without the domain. */ + + ( + (options & vopt_callout_fullpm) != 0 && + smtp_write_command(&outblock, FALSE, + "RCPT TO:\r\n") >= 0 && + smtp_read_response(&inblock, responsebuffer, + sizeof(responsebuffer), '2', callout) + )); + + /* Sort out the cache record */ new_domain_record.postmaster_stamp = time(NULL); @@ -586,13 +682,13 @@ for (host = host_list; host != NULL && !done; host = host->next) new_domain_record.postmaster_result = ccache_accept; else if (errno == 0 && responsebuffer[0] == '5') { - *failure_ptr = US"postmaster"; + *failure_ptr = US"postmaster"; setflag(addr, af_verify_pmfail); new_domain_record.postmaster_result = ccache_reject; } } } /* Random not accepted */ - } /* MAIL FROM:<> accepted */ + } /* MAIL FROM: accepted */ /* For any failure of the main check, other than a negative response, we just close the connection and carry on. We can identify a negative response by the @@ -609,6 +705,7 @@ for (host = host_list; host != NULL && !done; host = host->next) if (errno == ETIMEDOUT) { HDEBUG(D_verify) debug_printf("SMTP timeout\n"); + send_quit = FALSE; } else if (errno == 0) { @@ -637,8 +734,8 @@ for (host = host_list; host != NULL && !done; host = host->next) /* End the SMTP conversation and close the connection. */ - (void)smtp_write_command(&outblock, FALSE, "QUIT\r\n"); - close(inblock.sock); + if (send_quit) (void)smtp_write_command(&outblock, FALSE, "QUIT\r\n"); + (void)close(inblock.sock); } /* Loop through all hosts, while !done */ /* If we get here with done == TRUE, a successful callout happened, and yield @@ -647,9 +744,9 @@ Otherwise, we looped through the hosts but couldn't complete the business. However, there may be domain-specific information to cache in both cases. The value of the result field in the new_domain record is ccache_unknown if -there was an error before or with MAIL FROM:<>, and errno was not zero, +there was an error before or with MAIL FROM:, and errno was not zero, implying some kind of I/O error. We don't want to write the cache in that case. -Otherwise the value is ccache_accept or ccache_reject. */ +Otherwise the value is ccache_accept, ccache_reject, or ccache_reject_mfnull. */ if (!callout_no_cache && new_domain_record.result != ccache_unknown) { @@ -757,6 +854,8 @@ if (addr != vaddr) vaddr->user_message = addr->user_message; vaddr->basic_errno = addr->basic_errno; vaddr->more_errno = addr->more_errno; + vaddr->p.address_data = addr->p.address_data; + copyflag(vaddr, addr, af_pass_message); } return yield; } @@ -764,6 +863,42 @@ return yield; +/************************************************** +* printf that automatically handles TLS if needed * +***************************************************/ + +/* This function is used by verify_address() as a substitute for all fprintf() +calls; a direct fprintf() will not produce output in a TLS SMTP session, such +as a response to an EXPN command. smtp_in.c makes smtp_printf available but +that assumes that we always use the smtp_out FILE* when not using TLS or the +ssl buffer when we are. Instead we take a FILE* parameter and check to see if +that is smtp_out; if so, smtp_printf() with TLS support, otherwise regular +fprintf(). + +Arguments: + f the candidate FILE* to write to + format format string + ... optional arguments + +Returns: + nothing +*/ + +static void PRINTF_FUNCTION(2,3) +respond_printf(FILE *f, char *format, ...) +{ +va_list ap; + +va_start(ap, format); +if (smtp_out && (f == smtp_out)) + smtp_vprintf(format, ap); +else + fprintf(f, format, ap); +va_end(ap); +} + + + /************************************************* * Verify an email address * *************************************************/ @@ -784,10 +919,13 @@ Arguments: rewriting and messages from callouts vopt_qualify => qualify an unqualified address; else error vopt_expn => called from SMTP EXPN command + vopt_success_on_redirect => when a new address is generated + the verification instantly succeeds These ones are used by do_callout() -- the options variable is passed to it. + vopt_callout_fullpm => if postmaster check, do full one vopt_callout_no_cache => don't use callout cache vopt_callout_random => do the "random" thing vopt_callout_recipsender => use real sender for recipient @@ -797,7 +935,7 @@ Arguments: for individual commands callout_overall if > 0, gives overall timeout for the callout function; if < 0, a default is used (see do_callout()) - callout_connect the connection timeout for callouts + callout_connect the connection timeout for callouts se_mailfrom when callout is requested to verify a sender, use this in MAIL FROM; NULL => "" pm_mailfrom when callout is requested, if non-NULL, do the postmaster @@ -813,13 +951,14 @@ Returns: OK address verified int verify_address(address_item *vaddr, FILE *f, int options, int callout, - int callout_overall, int callout_connect, uschar *se_mailfrom, + int callout_overall, int callout_connect, uschar *se_mailfrom, uschar *pm_mailfrom, BOOL *routed) { BOOL allok = TRUE; BOOL full_info = (f == NULL)? FALSE : (debug_selector != 0); BOOL is_recipient = (options & vopt_is_recipient) != 0; 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 : @@ -830,7 +969,7 @@ address_item *addr_new = NULL; address_item *addr_remote = NULL; address_item *addr_local = NULL; address_item *addr_succeed = NULL; -uschar **failure_ptr = is_recipient? +uschar **failure_ptr = is_recipient? &recipient_verify_failure : &sender_verify_failure; uschar *ko_prefix, *cr; uschar *address = vaddr->address; @@ -859,9 +998,9 @@ if (parse_find_at(address) == NULL) if ((options & vopt_qualify) == 0) { if (f != NULL) - fprintf(f, "%sA domain is required for \"%s\"%s\n", ko_prefix, address, - cr); - *failure_ptr = US"qualify"; + respond_printf(f, "%sA domain is required for \"%s\"%s\n", + ko_prefix, address, cr); + *failure_ptr = US"qualify"; return FAIL; } address = rewrite_address_qualify(address, is_recipient); @@ -1000,10 +1139,21 @@ while (addr_new != NULL) { host_item *host_list = addr->host_list; - /* Default, if no remote transport, to NULL for the interface (=> any), - "smtp" for the port, and "smtp" for the protocol. */ - - transport_feedback tf = { NULL, US"smtp", US"smtp", NULL, FALSE, FALSE }; + /* Make up some data for use in the case where there is no remote + transport. */ + + transport_feedback tf = { + NULL, /* interface (=> any) */ + US"smtp", /* port */ + US"smtp", /* protocol */ + NULL, /* hosts */ + US"$smtp_active_hostname", /* helo_data */ + FALSE, /* hosts_override */ + FALSE, /* hosts_randomize */ + FALSE, /* gethostbyname */ + TRUE, /* qualify_single */ + FALSE /* search_parents */ + }; /* If verification yielded a remote transport, we want to use that transport's options, so as to mimic what would happen if we were really @@ -1011,7 +1161,7 @@ while (addr_new != NULL) if (addr->transport != NULL && !addr->transport->info->local) { - (void)(addr->transport->setup)(addr->transport, addr, &tf, NULL); + (void)(addr->transport->setup)(addr->transport, 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 @@ -1020,13 +1170,16 @@ while (addr_new != NULL) if (tf.hosts != NULL && (host_list == NULL || tf.hosts_override)) { uschar *s; + 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 = deliver_localpart = NULL; + deliver_domain = save_deliver_domain; + deliver_localpart = save_deliver_localpart; if (s == NULL) { @@ -1036,35 +1189,36 @@ while (addr_new != NULL) } else { + int flags; uschar *canonical_name; host_item *host, *nexthost; 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 + 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; + if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE; + if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS; + for (host = host_list; host != NULL; host = nexthost) { nexthost = host->next; - if (tf.gethostbyname || string_is_ip_address(host->name, NULL)) - (void)host_find_byname(host, NULL, &canonical_name, TRUE); + if (tf.gethostbyname || + string_is_ip_address(host->name, NULL) != 0) + (void)host_find_byname(host, NULL, flags, &canonical_name, TRUE); else - { - int flags = HOST_FIND_BY_A; - if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE; - if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS; (void)host_find_bydns(host, NULL, flags, NULL, NULL, NULL, &canonical_name, NULL); - } } } } } - /* Can only do a callout if we have at least one host! If the callout + /* Can only do a callout if we have at least one host! If the callout fails, it will have set ${sender,recipient}_verify_failure. */ if (host_list != NULL) @@ -1089,10 +1243,10 @@ while (addr_new != NULL) } } } - + /* Otherwise, any failure is a routing failure */ - - else *failure_ptr = US"route"; + + else *failure_ptr = US"route"; /* A router may return REROUTED if it has set up a child address as a result of a change of domain name (typically from widening). In this case we always @@ -1107,16 +1261,27 @@ while (addr_new != NULL) allok = FALSE; if (f != NULL) { - fprintf(f, "%s%s %s", ko_prefix, address, + address_item *p = addr->parent; + + respond_printf(f, "%s%s %s", ko_prefix, + full_info? addr->address : address, address_test_mode? "is undeliverable" : "failed to verify"); if (!expn && admin_user) { if (addr->basic_errno > 0) - fprintf(f, ": %s", strerror(addr->basic_errno)); + respond_printf(f, ": %s", strerror(addr->basic_errno)); if (addr->message != NULL) - fprintf(f, ":\n %s", addr->message); + respond_printf(f, ": %s", addr->message); + } + + /* Show parents iff doing full info */ + + if (full_info) while (p != NULL) + { + respond_printf(f, "%s\n <-- %s", cr, p->address); + p = p->parent; } - fprintf(f, "%s\n", cr); + respond_printf(f, "%s\n", cr); } if (!full_info) return copy_error(vaddr, addr, FAIL); @@ -1130,25 +1295,34 @@ while (addr_new != NULL) allok = FALSE; if (f != NULL) { - fprintf(f, "%s%s cannot be resolved at this time", ko_prefix, address); + address_item *p = addr->parent; + respond_printf(f, "%s%s cannot be resolved at this time", ko_prefix, + full_info? addr->address : address); if (!expn && admin_user) { if (addr->basic_errno > 0) - fprintf(f, ":\n %s", strerror(addr->basic_errno)); + respond_printf(f, ": %s", strerror(addr->basic_errno)); if (addr->message != NULL) - fprintf(f, ":\n %s", addr->message); + respond_printf(f, ": %s", addr->message); else if (addr->basic_errno <= 0) - fprintf(f, ":\n unknown error"); + respond_printf(f, ": unknown error"); } - fprintf(f, "%s\n", cr); + /* Show parents iff doing full info */ + + if (full_info) while (p != NULL) + { + respond_printf(f, "%s\n <-- %s", cr, p->address); + p = p->parent; + } + respond_printf(f, "%s\n", cr); } if (!full_info) return copy_error(vaddr, addr, DEFER); else if (yield == OK) yield = DEFER; } /* If we are handling EXPN, we do not want to continue to route beyond - the top level. */ + the top level (whose address is in "address"). */ else if (expn) { @@ -1156,16 +1330,16 @@ while (addr_new != NULL) if (addr_new == NULL) { if (addr_local == NULL && addr_remote == NULL) - fprintf(f, "250 mail to <%s> is discarded\r\n", address); + respond_printf(f, "250 mail to <%s> is discarded\r\n", address); else - fprintf(f, "250 <%s>\r\n", address); + respond_printf(f, "250 <%s>\r\n", address); } else while (addr_new != NULL) { address_item *addr2 = addr_new; addr_new = addr2->next; if (addr_new == NULL) ok_prefix = US"250 "; - fprintf(f, "%s<%s>\r\n", ok_prefix, addr2->address); + respond_printf(f, "%s<%s>\r\n", ok_prefix, addr2->address); } return OK; } @@ -1188,9 +1362,12 @@ while (addr_new != NULL) generated address. */ if (!full_info && /* Stop if short info wanted AND */ - (addr_new == NULL || /* No new address OR */ - addr_new->next != NULL || /* More than one new address OR */ - testflag(addr_new, af_pfr))) /* New address is pfr */ + (((addr_new == NULL || /* No new address OR */ + addr_new->next != NULL || /* More than one new address OR */ + testflag(addr_new, af_pfr))) /* New address is pfr */ + || /* OR */ + (addr_new != NULL && /* At least one new address AND */ + success_on_redirect))) /* success_on_redirect is set */ { if (f != NULL) fprintf(f, "%s %s\n", address, address_test_mode? "is deliverable" : "verified"); @@ -1214,9 +1391,12 @@ or autoreplies, and there were no errors or deferments, the message is to be discarded, usually because of the use of :blackhole: in an alias file. */ if (allok && addr_local == NULL && addr_remote == NULL) + { fprintf(f, "mail to %s is discarded\n", address); + return yield; + } -else for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++) +for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++) { while (addr_list != NULL) { @@ -1225,6 +1405,23 @@ else for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++) addr_list = addr->next; fprintf(f, "%s", CS addr->address); +#ifdef EXPERIMENTAL_SRS + if(addr->p.srs_sender) + fprintf(f, " [srs = %s]", addr->p.srs_sender); +#endif + + /* If the address is a duplicate, show something about it. */ + + if (!testflag(addr, af_pfr)) + { + tree_node *tnode; + if ((tnode = tree_search(tree_duplicates, addr->unique)) != NULL) + fprintf(f, " [duplicate, would not be delivered]"); + else tree_add_duplicate(addr->unique, addr); + } + + /* Now show its parents */ + while (p != NULL) { fprintf(f, "\n <-- %s", p->address); @@ -1280,10 +1477,10 @@ else for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++) } } -/* Will be DEFER or FAIL if any one address has, only for full_info (which is +/* Will be DEFER or FAIL if any one address has, only for full_info (which is the -bv or -bt case). */ -return yield; +return yield; } @@ -1308,8 +1505,9 @@ verify_check_headers(uschar **msgptr) { header_line *h; uschar *colon, *s; +int yield = OK; -for (h = header_list; h != NULL; h = h->next) +for (h = header_list; h != NULL && yield == OK; h = h->next) { if (h->type != htype_from && h->type != htype_reply_to && @@ -1323,9 +1521,10 @@ for (h = header_list; h != NULL; h = h->next) s = colon + 1; while (isspace(*s)) s++; - parse_allow_group = TRUE; /* Allow group syntax */ + /* Loop for multiple addresses in the header, enabling group syntax. Note + that we have to reset this after the header has been scanned. */ - /* Loop for multiple addresses in the header */ + parse_allow_group = TRUE; while (*s != 0) { @@ -1335,7 +1534,7 @@ for (h = header_list; h != NULL; h = h->next) int start, end, domain; /* Temporarily terminate the string at this point, and extract the - operative address within. */ + operative address within, allowing group syntax. */ *ss = 0; recipient = parse_extract_address(s,&errmess,&start,&end,&domain,FALSE); @@ -1364,14 +1563,16 @@ for (h = header_list; h != NULL; h = h->next) { uschar *verb = US"is"; uschar *t = ss; + uschar *tt = colon; int len; /* Arrange not to include any white space at the end in the - error message. */ + error message or the header name. */ while (t > s && isspace(t[-1])) t--; + while (tt > h->text && isspace(tt[-1])) tt--; - /* Add the address which failed to the error message, since in a + /* Add the address that failed to the error message, since in a header with very many addresses it is sometimes hard to spot which one is at fault. However, limit the amount of address to quote - cases have been seen where, for example, a missing double @@ -1386,10 +1587,11 @@ for (h = header_list; h != NULL; h = h->next) } *msgptr = string_printing( - string_sprintf("%s: failing address in \"%.*s\" header %s: %.*s", - errmess, colon - h->text, h->text, verb, len, s)); + string_sprintf("%s: failing address in \"%.*s:\" header %s: %.*s", + errmess, tt - h->text, h->text, verb, len, s)); - return FAIL; + yield = FAIL; + break; /* Out of address loop */ } /* Advance to the next address */ @@ -1397,13 +1599,103 @@ for (h = header_list; h != NULL; h = h->next) s = ss + (terminator? 1:0); while (isspace(*s)) s++; } /* Next address */ - } /* Next header */ -return OK; + parse_allow_group = FALSE; + parse_found_group = FALSE; + } /* Next header unless yield has been set FALSE */ + +return yield; } +/************************************************* +* Check for blind recipients * +*************************************************/ + +/* This function checks that every (envelope) recipient is mentioned in either +the To: or Cc: header lines, thus detecting blind carbon copies. + +There are two ways of scanning that could be used: either scan the header lines +and tick off the recipients, or scan the recipients and check the header lines. +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 +Returns: OK if there are no blind recipients + FAIL if there is at least one blind recipient +*/ + +int +verify_check_notblind(void) +{ +int i; +for (i = 0; i < recipients_count; i++) + { + header_line *h; + BOOL found = FALSE; + uschar *address = recipients_list[i].address; + + for (h = header_list; !found && h != NULL; h = h->next) + { + uschar *colon, *s; + + if (h->type != htype_to && h->type != htype_cc) continue; + + colon = Ustrchr(h->text, ':'); + s = colon + 1; + while (isspace(*s)) s++; + + /* Loop for multiple addresses in the header, enabling group syntax. Note + that we have to reset this after the header has been scanned. */ + + parse_allow_group = TRUE; + + while (*s != 0) + { + uschar *ss = parse_find_address_end(s, FALSE); + uschar *recipient,*errmess; + int terminator = *ss; + int start, end, domain; + + /* Temporarily terminate the string at this point, and extract the + operative address within, allowing group syntax. */ + + *ss = 0; + recipient = parse_extract_address(s,&errmess,&start,&end,&domain,FALSE); + *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; + } + + /* Advance to the next address */ + + s = ss + (terminator? 1:0); + while (isspace(*s)) s++; + } /* Next address */ + + parse_allow_group = FALSE; + parse_found_group = FALSE; + } /* Next header (if found is false) */ + + if (!found) return FAIL; + } /* Next recipient */ + +return OK; +} + + /************************************************* * Find if verified sender * @@ -1458,10 +1750,11 @@ Arguments: log_msgptr points to where to put a log error message callout timeout for callout check (passed to verify_address()) callout_overall overall callout timeout (ditto) - callout_connect connect callout timeout (ditto) + callout_connect connect callout timeout (ditto) se_mailfrom mailfrom for verify; NULL => "" pm_mailfrom sender for pm callout check (passed to verify_address()) options callout options (passed to verify_address()) + verrno where to put the address basic_errno If log_msgptr is set to something without setting user_msgptr, the caller normally uses log_msgptr for both things. @@ -1472,17 +1765,18 @@ Returns: result of the verification attempt: OK, FAIL, or DEFER; int verify_check_header_address(uschar **user_msgptr, uschar **log_msgptr, - int callout, int callout_overall, int callout_connect, uschar *se_mailfrom, - uschar *pm_mailfrom, int options) + int callout, int callout_overall, int callout_connect, uschar *se_mailfrom, + uschar *pm_mailfrom, int options, int *verrno) { static int header_types[] = { htype_sender, htype_reply_to, htype_from }; +BOOL done = FALSE; int yield = FAIL; int i; -for (i = 0; i < 3; i++) +for (i = 0; i < 3 && !done; i++) { header_line *h; - for (h = header_list; h != NULL; h = h->next) + for (h = header_list; h != NULL && !done; h = h->next) { int terminator, new_ok; uschar *s, *ss, *endname; @@ -1490,6 +1784,11 @@ for (i = 0; i < 3; i++) if (h->type != header_types[i]) continue; s = endname = Ustrchr(h->text, ':') + 1; + /* Scan the addresses in the header, enabling group syntax. Note that we + have to reset this after the header has been scanned. */ + + parse_allow_group = TRUE; + while (*s != 0) { address_item *vaddr; @@ -1532,11 +1831,21 @@ for (i = 0; i < 3; i++) else { int start, end, domain; - uschar *address = parse_extract_address(s, log_msgptr, &start, - &end, &domain, FALSE); + uschar *address = parse_extract_address(s, log_msgptr, &start, &end, + &domain, FALSE); *ss = terminator; + /* 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) + { + *log_msgptr = NULL; + s = ss; + continue; + } + /* If verification failed because of a syntax error, fail this function, and ensure that the failing address gets added to the error message. */ @@ -1544,17 +1853,16 @@ for (i = 0; i < 3; i++) if (address == NULL) { new_ok = FAIL; - if (*log_msgptr != NULL) - { - while (ss > s && isspace(ss[-1])) ss--; - *log_msgptr = string_sprintf("syntax error in '%.*s' header when " - "scanning for sender: %s in \"%.*s\"", - endname - h->text, h->text, *log_msgptr, ss - s, s); - return FAIL; - } + while (ss > s && isspace(ss[-1])) ss--; + *log_msgptr = string_sprintf("syntax error in '%.*s' header when " + "scanning for sender: %s in \"%.*s\"", + endname - h->text, h->text, *log_msgptr, ss - s, s); + yield = FAIL; + done = TRUE; + break; } - /* Else go ahead with the sender verification. But is isn't *the* + /* Else go ahead with the sender verification. But it isn't *the* sender of the message, so set vopt_fake_sender to stop sender_address being replaced after rewriting or qualification. */ @@ -1562,7 +1870,7 @@ for (i = 0; i < 3; i++) { vaddr = deliver_make_addr(address, FALSE); new_ok = verify_address(vaddr, NULL, options | vopt_fake_sender, - callout, callout_overall, callout_connect, se_mailfrom, + callout, callout_overall, callout_connect, se_mailfrom, pm_mailfrom, NULL); } } @@ -1572,24 +1880,37 @@ for (i = 0; i < 3; i++) last of these will be returned to the user if all three fail. We do not set a log message - the generic one below will be used. */ - if (new_ok != OK && smtp_return_error_details) + if (new_ok != OK) { - *user_msgptr = string_sprintf("Rejected after DATA: " - "could not verify \"%.*s\" header address\n%s: %s", - endname - h->text, h->text, vaddr->address, vaddr->message); + *verrno = vaddr->basic_errno; + if (smtp_return_error_details) + { + *user_msgptr = string_sprintf("Rejected after DATA: " + "could not verify \"%.*s\" header address\n%s: %s", + endname - h->text, h->text, vaddr->address, vaddr->message); + } } /* Success or defer */ - if (new_ok == OK) return OK; + if (new_ok == OK) + { + yield = OK; + done = TRUE; + break; + } + if (new_ok == DEFER) yield = DEFER; /* Move on to any more addresses in the header */ s = ss; - } - } - } + } /* Next address */ + + parse_allow_group = FALSE; + parse_found_group = FALSE; + } /* Next header, unless done */ + } /* Next header type unless done */ if (yield == FAIL && *log_msgptr == NULL) *log_msgptr = US"there is no valid sender in any header line"; @@ -1757,7 +2078,7 @@ sender_ident = string_printing(string_copyn(p, 127)); DEBUG(D_ident) debug_printf("sender_ident = %s\n", sender_ident); END_OFF: -close(sock); +(void)close(sock); return; } @@ -1780,25 +2101,34 @@ Arguments: error for error message when returning ERROR The block contains: - host_name the host name or NULL, implying use sender_host_name and - sender_host_aliases, looking them up if required + host_name (a) the host name, or + (b) NULL, implying use sender_host_name and + sender_host_aliases, looking them up if required, or + (c) the empty string, meaning that only IP address matches + are permitted host_address the host address host_ipv4 the IPv4 address taken from an IPv6 one Returns: OK matched FAIL did not match DEFER lookup deferred - ERROR failed to find the host name or IP address - unknown lookup type specified + ERROR (a) failed to find the host name or IP address, or + (b) unknown lookup type specified, or + (c) host name encountered when only IP addresses are + being matched */ -static int +int check_host(void *arg, uschar *ss, uschar **valueptr, uschar **error) { check_host_block *cb = (check_host_block *)arg; +int mlen = -1; int maskoffset; +BOOL iplookup = FALSE; BOOL isquery = FALSE; -uschar *semicolon, *t; +BOOL isiponly = cb->host_name != NULL && cb->host_name[0] == 0; +uschar *t; +uschar *semicolon; uschar **aliases; /* Optimize for the special case when the pattern is "*". */ @@ -1812,12 +2142,17 @@ 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 the pattern is precisely "@" then match against the primary host name; -if it's "@[]" match against the local host's IP addresses. */ +/* 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) ss = primary_hostname; + if (ss[1] == 0) + { + if (isiponly) return ERROR; + ss = primary_hostname; + } else if (Ustrcmp(ss, "@[]") == 0) { ip_address_item *ip; @@ -1830,75 +2165,131 @@ if (*ss == '@') /* If the pattern is an IP address, optionally followed by a bitmask count, do a (possibly masked) comparision with the current IP address. */ -if (string_is_ip_address(ss, &maskoffset)) +if (string_is_ip_address(ss, &maskoffset) != 0) return (host_is_in_net(cb->host_address, ss, maskoffset)? OK : FAIL); -/* If the item is of the form net[n]-lookup; then it is a lookup on -a masked IP network, in textual form. The net- stuff really only applies to -single-key lookups where the 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 = Ustrchr(ss, ';')) != NULL) +/* The pattern is not an IP address. A common error that people make is to omit +one component of an IPv4 address, either by accident, or believing that, for +example, 1.2.3/24 is the same as 1.2.3.0/24, or 1.2.3 is the same as 1.2.3.0, +which it isn't. (Those applications that do accept 1.2.3 as an IP address +interpret it as 1.2.0.3 because the final component becomes 16-bit - this is an +ancient specification.) To aid in debugging these cases, we give a specific +error if the pattern contains only digits and dots or contains a slash preceded +only by digits and dots (a slash at the start indicates a file name and of +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)) { - int mlen = 0; - for (t = ss + 3; isdigit(*t); t++) mlen = mlen * 10 + *t - '0'; - if (*t++ == '-') - { - int insize; - int search_type; - int incoming[4]; - void *handle; - uschar *filename, *key, *result; - uschar buffer[64]; + *error = US"malformed IPv4 address or address mask"; + return ERROR; + } - /* If no mask was supplied, set a negative value */ +/* See if there is a semicolon in the pattern */ - if (mlen == 0 && t == ss+4) mlen = -1; +semicolon = Ustrchr(ss, ';'); - /* Find the search type */ +/* If we are doing an IP address only match, then all lookups must be IP +address lookups, even if there is no "net-". */ - search_type = search_findtype(t, semicolon - t); +if (isiponly) + { + iplookup = semicolon != NULL; + } - if (search_type < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", - search_error_message); +/* 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 +have already set iplookup, so as to skip over the "net-" prefix and to set the +mask length. The net- stuff really only applies to single-key lookups where the +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. */ - /* Adjust parameters for the type of lookup. For a query-style - lookup, there is no file name, and the "key" is just the query. For - a single-key lookup, the key is the current IP address, masked - appropriately, and reconverted to text form, with the mask appended. */ +if (Ustrncmp(ss, "net", 3) == 0 && semicolon != NULL) + { + 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++ == '-'); + } +else t = ss; - if (mac_islookup(search_type, lookup_querystyle)) - { - filename = NULL; - key = semicolon + 1; - } - else - { - insize = host_aton(cb->host_address, incoming); - host_mask(insize, incoming, mlen); - (void)host_nmtoa(insize, incoming, mlen, buffer); - key = buffer; - filename = semicolon + 1; - } +/* Do the IP address lookup if that is indeed what we have */ - /* Now do the actual lookup; note that there is no search_close() because - of the caching arrangements. */ +if (iplookup) + { + int insize; + int search_type; + int incoming[4]; + void *handle; + uschar *filename, *key, *result; + uschar buffer[64]; + + /* Find the search type */ + + search_type = search_findtype(t, semicolon - t); + + if (search_type < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", + search_error_message); + + /* Adjust parameters for the type of lookup. For a query-style lookup, there + is no file name, and the "key" is just the query. For query-style with a file + name, we have to fish the file off the start of the query. For a single-key + lookup, the key is the current IP address, masked appropriately, and + reconverted to text form, with the mask appended. For IPv6 addresses, specify + dot separators instead of colons, except when the lookup type is "iplsearch". + */ - handle = search_open(filename, search_type, 0, NULL, NULL); - if (handle == NULL) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", - search_error_message); - result = search_find(handle, filename, key, -1, NULL, 0, 0, NULL); - if (valueptr != NULL) *valueptr = result; - return (result != NULL)? OK : search_find_defer? DEFER: FAIL; + if (mac_islookup(search_type, lookup_absfilequery)) + { + filename = semicolon + 1; + key = filename; + while (*key != 0 && !isspace(*key)) key++; + filename = string_copyn(filename, key - filename); + while (isspace(*key)) key++; + } + else if (mac_islookup(search_type, lookup_querystyle)) + { + filename = NULL; + key = semicolon + 1; + } + else /* Single-key style */ + { + int sep = (Ustrcmp(lookup_list[search_type].name, "iplsearch") == 0)? + ':' : '.'; + insize = host_aton(cb->host_address, incoming); + host_mask(insize, incoming, mlen); + (void)host_nmtoa(insize, incoming, mlen, buffer, sep); + key = buffer; + filename = semicolon + 1; } + + /* Now do the actual lookup; note that there is no search_close() because + of the caching arrangements. */ + + handle = search_open(filename, search_type, 0, NULL, NULL); + if (handle == NULL) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", + search_error_message); + result = search_find(handle, filename, key, -1, NULL, 0, 0, NULL); + if (valueptr != NULL) *valueptr = result; + return (result != NULL)? OK : search_find_defer? DEFER: FAIL; } /* The pattern is not an IP address or network reference of any kind. That is, -it is a host name pattern. Check the characters of the pattern to see if they -comprise only letters, 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. */ +it is a host name pattern. If this is an IP only match, there's an error in the +host list. */ + +if (isiponly) + { + *error = US"cannot match host name in match_ip list"; + return ERROR; + } + +/* Check the characters of the pattern to see if they comprise only letters, +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++) if (!isalnum(*t) && *t != '.' && *t != '-' && *t != '_' && @@ -1916,15 +2307,14 @@ if (*t == 0) h.name = ss; h.address = NULL; h.mx = MX_NONE; - rc = host_find_byname(&h, NULL, NULL, FALSE); + + rc = host_find_byname(&h, NULL, HOST_FIND_QUALIFY_SINGLE, NULL, FALSE); if (rc == HOST_FOUND || rc == HOST_FOUND_LOCAL) { host_item *hh; for (hh = &h; hh != NULL; hh = hh->next) { - if (Ustrcmp(hh->address, (Ustrchr(hh->address, ':') == NULL)? - cb->host_ipv4 : cb->host_address) == 0) - return OK; + if (host_is_in_net(hh->address, cb->host_address, 0)) return OK; } return FAIL; } @@ -1963,7 +2353,7 @@ if ((semicolon = Ustrchr(ss, ';')) != NULL) search_error_message, ss); return DEFER; } - isquery = mac_islookup(id, lookup_querystyle); + isquery = mac_islookup(id, lookup_querystyle|lookup_absfilequery); } if (isquery) @@ -2070,9 +2460,9 @@ addresses. */ cb.host_ipv4 = (Ustrncmp(host_address, "::ffff:", 7) == 0)? host_address + 7 : host_address; -/* During the running of the check, put the IP address into $host_address. In -the case of calls from the smtp transport, it will already be there. However, -in other calls (e.g. when testing ignore_target_hosts), it won't. Just to be on +/* During the running of the check, put the IP address into $host_address. In +the case of calls from the smtp transport, it will already be there. However, +in other calls (e.g. when testing ignore_target_hosts), it won't. Just to be on the safe side, any existing setting is preserved, though as I write this (November 2004) I can't see any cases where it is actually needed. */ @@ -2085,11 +2475,11 @@ rc = match_check_list( check_host, /* function for testing */ &cb, /* argument for function */ MCL_HOST, /* type of check */ - (host_address == sender_host_address)? + (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; +return rc; } @@ -2178,10 +2568,323 @@ else } } #endif + +/* Remove trailing period -- this is needed so that both arbitrary +dnsbl keydomains and inverted addresses may be combined with the +same format string, "%s.%s" */ + +*(--bptr) = 0; } +/************************************************* +* Perform a single dnsbl lookup * +*************************************************/ + +/* 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. + +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 +*/ + +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. */ + +t = tree_search(dnsbl_cache, query); + +/* If not cached from a previous lookup, we must do a DNS lookup, and +cache the result in permanent memory. */ + +if (t == NULL) + { + store_pool = POOL_PERM; + + /* 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 loopup . */ + + 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 have been reduced to experimental + status (August 2001) and may die out. So they may never get used at all, + let alone in dnsbl records. However, 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. */ + + if (cb->rc == DNS_SUCCEED) + { + dns_record *rr; + dns_address **addrp = &(cb->rhs); + for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); + rr != NULL; + rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) + { + if (rr->type == T_A) + { + dns_address *da = dns_address_from_rr(&dnsa, rr); + if (da != NULL) + { + *addrp = da; + while (da->next != NULL) da = da->next; + addrp = &(da->next); + } + } + } + + /* 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 == NULL) cb->rc = DNS_NODATA; + } + + store_pool = old_pool; + } + +/* Previous lookup was cached */ + +else + { + HDEBUG(D_dnsbl) debug_printf("using result of previous DNS lookup\n"); + cb = t->data.ptr; + } + +/* 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 != NULL; 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 != NULL) + { + for (da = cb->rhs; da != NULL; da = da->next) + { + int ipsep = ','; + uschar ip[46]; + uschar *ptr = iplist; + uschar *res; + + /* Handle exact matching */ + + if (!bitmask) + { + while ((res = string_nextinlist(&ptr, &ipsep, ip, sizeof(ip))) != NULL) + { + 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))) != NULL) + { + 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. */ + + 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) + { + dns_record *rr; + for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); + rr != NULL; + rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) + if (rr->type == T_TXT) break; + if (rr != NULL) + { + int len = (rr->data)[0]; + if (len > 511) len = 127; + store_pool = POOL_PERM; + cb->text = string_sprintf("%.*s", len, (const uschar *)(rr->data+1)); + store_pool = old_pool; + } + } + } + + dnslist_value = addlist; + dnslist_text = cb->text; + return OK; + } + +/* There was a problem with the DNS lookup */ + +if (cb->rc != DNS_NOMATCH && cb->rc != DNS_NODATA) + { + 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; + } + +/* No entry was found in the DNS; continue for next domain */ + +HDEBUG(D_dnsbl) + { + debug_printf("DNS lookup for %s failed\n", query); + debug_printf("=> that means %s is not listed at %s\n", + keydomain, domain); + } + +return FAIL; +} + + + + /************************************************* * Check host against DNS black lists * *************************************************/ @@ -2198,7 +2901,7 @@ 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, +domain for the lookup. For example: dsn.rfc-ignorant.org/$sender_address_domain @@ -2207,6 +2910,17 @@ 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. +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: + + dnslists = http.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.2 : \ + socks.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.3 + +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 @@ -2226,31 +2940,31 @@ verify_check_dnsbl(uschar **listptr) { int sep = 0; int defer_return = FAIL; -int old_pool = store_pool; -BOOL invert_result = FALSE; uschar *list = *listptr; uschar *domain; uschar *s; uschar buffer[1024]; -uschar query[256]; /* DNS domain max length */ 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); + /* Loop through all the domains supplied, until something matches */ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) { - BOOL frc; + int rc; BOOL bitmask = FALSE; - dns_answer dnsa; - dns_scan dnss; + int match_type = 0; + uschar *domain_txt; + uschar *comma; uschar *iplist; uschar *key; - tree_node *t; - dnsbl_cache_block *cb; HDEBUG(D_dnsbl) debug_printf("DNS list check: %s\n", domain); @@ -2273,8 +2987,8 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL if (key != NULL) *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 invert the result. - */ + introduced by an = or a & character; if preceded by = we require all matches + and if preceded by ! we invert the result. */ iplist = Ustrchr(domain, '='); if (iplist == NULL) @@ -2283,14 +2997,35 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL iplist = Ustrchr(domain, '&'); } - if (iplist != NULL) + if (iplist != NULL) /* Found either = or & */ { - if (iplist > domain && iplist[-1] == '!') + if (iplist > domain && iplist[-1] == '!') /* Handle preceding ! */ { - invert_result = TRUE; + match_type |= MT_NOT; iplist[-1] = 0; } - *iplist++ = 0; + + *iplist++ = 0; /* Terminate domain, move on */ + + /* If we found = (bitmask == FALSE), check for == or =& */ + + if (!bitmask && (*iplist == '=' || *iplist == '&')) + { + bitmask = *iplist++ == '&'; + match_type |= MT_ALL; + } + } + + /* 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. */ + + domain_txt = domain; + comma = Ustrchr(domain, ','); + if (comma != NULL) + { + *comma++ = 0; + domain = comma; } /* Check that what we have left is a sensible domain name. There is no reason @@ -2309,251 +3044,81 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL } } - /* Construct the query by adding the domain onto either the sending host - address, or the given key string. */ - - if (key == NULL) - { - if (sender_host_address == NULL) return FAIL; /* can never match */ - if (revadd[0] == 0) invert_address(revadd, sender_host_address); - frc = string_format(query, sizeof(query), "%s%s", revadd, domain); - } - else - { - frc = string_format(query, sizeof(query), "%s.%s", key, domain); - } + /* Check the alternate domain if present */ - if (!frc) + if (domain_txt != domain) for (s = domain_txt; *s != 0; s++) { - log_write(0, LOG_MAIN|LOG_PANIC, "dnslist query is too long " - "(ignored): %s...", query); - continue; + if (!isalnum(*s) && *s != '-' && *s != '.') + { + log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains " + "strange characters - is this right?", domain_txt); + break; + } } - /* Look for this query in the cache. */ + /* 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. */ - t = tree_search(dnsbl_cache, query); - - /* If not cached from a previous lookup, we must do a DNS lookup, and - cache the result in permanent memory. */ - - if (t == NULL) + if (key == NULL) { - store_pool = POOL_PERM; - - /* In case this is the first time the DNS resolver is being used. */ - - dns_init(FALSE, FALSE); - - /* 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 loopup . */ - - 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 have been reduced to experimental - status (August 2001) and may die out. So they may never get used at all, - let alone in dnsbl records. However, 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. */ - - if (cb->rc == DNS_SUCCEED) + 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) { - dns_record *rr; - dns_address **addrp = &(cb->rhs); - for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); - rr != NULL; - rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) - { - if (rr->type == T_A) - { - dns_address *da = dns_address_from_rr(&dnsa, rr); - if (da != NULL) - { - *addrp = da; - while (da->next != NULL) da = da->next; - addrp = &(da->next); - } - } - } - - /* 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 == NULL) cb->rc = DNS_NODATA; + 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); } - - store_pool = old_pool; + if (rc != FAIL) return rc; /* OK or DEFER */ } - /* Previous lookup was cached */ + /* If there is a key string, it can be a list of domains or IP addresses to + be concatenated with the main domain. */ else { - HDEBUG(D_dnsbl) debug_printf("using result of previous DNS lookup\n"); - cb = t->data.ptr; - } - - /* 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 != NULL; 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 != NULL) + int keysep = 0; + BOOL defer = FALSE; + uschar *keydomain; + uschar keybuffer[256]; + uschar keyrevadd[128]; + + while ((keydomain = string_nextinlist(&key, &keysep, keybuffer, + sizeof(keybuffer))) != NULL) { - int ipsep = ','; - uschar ip[46]; - uschar *ptr = iplist; + uschar *prepend = keydomain; - while (string_nextinlist(&ptr, &ipsep, ip, sizeof(ip)) != NULL) + if (string_is_ip_address(keydomain, NULL) != 0) { - /* Handle exact matching */ - if (!bitmask) - { - for (da = cb->rhs; da != NULL; da = da->next) - { - 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(ip, address) == 1) mask = address[0]; - - /* Scan the returned addresses, skipping any that are IPv6 */ - - for (da = cb->rhs; da != NULL; da = da->next) - { - if (host_aton(da->address, address) != 1) continue; - if ((address[0] & mask) == mask) break; - } - } - - /* Break out if a match has been found */ - - if (da != NULL) break; + invert_address(keyrevadd, keydomain); + prepend = keyrevadd; } - /* If either - - (a) No IP address in a positive list matched, or - (b) An IP address in a negative list did match - - then behave as if the DNSBL lookup had not succeeded, i.e. the host is - not on the list. */ + rc = one_check_dnsbl(domain, domain_txt, keydomain, prepend, iplist, + bitmask, match_type, defer_return); - if (invert_result != (da == NULL)) + if (rc == OK) { - HDEBUG(D_dnsbl) - { - debug_printf("=> but we are not accepting this block class because\n"); - debug_printf("=> there was %s match for %c%s\n", - invert_result? "an exclude":"no", bitmask? '&' : '=', iplist); - } - continue; /* With next DNSBL domain */ + 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; } - } - /* Either there was no IP list, or the record matched. Look up a TXT record - if it hasn't previously been done. */ + /* 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 (!cb->text_set) - { - cb->text_set = TRUE; - if (dns_basic_lookup(&dnsa, query, T_TXT) == DNS_SUCCEED) - { - dns_record *rr; - for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); - rr != NULL; - rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) - if (rr->type == T_TXT) break; - if (rr != NULL) - { - int len = (rr->data)[0]; - if (len > 511) len = 127; - store_pool = POOL_PERM; - cb->text = string_sprintf("%.*s", len, (const uschar *)(rr->data+1)); - store_pool = old_pool; - } - } - } + if (rc == DEFER) defer = TRUE; + } /* continue with next keystring domain/address */ - HDEBUG(D_dnsbl) - { - debug_printf("=> that means %s is listed at %s\n", - (key == NULL)? sender_host_address : key, domain); - } - - dnslist_domain = string_copy(domain); - dnslist_value = addlist; - dnslist_text = cb->text; - return OK; - } - - /* There was a problem with the DNS lookup */ - - if (cb->rc != DNS_NOMATCH && cb->rc != DNS_NODATA) - { - 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; - } - - /* No entry was found in the DNS; continue for next domain */ - - HDEBUG(D_dnsbl) - { - debug_printf("DNS lookup for %s failed\n", query); - debug_printf("=> that means %s is not listed at %s\n", - (key == NULL)? sender_host_address : key, domain); + if (defer) return DEFER; } - } /* Continue with next domain */ + } /* continue with next dnsdb outer domain */ return FAIL; }