Add comment about split_spool_directory to the default configuration.
[exim.git] / src / src / verify.c
index e4b25beaf8fa3bc41a99c348a711dd62eabc115e..8881926b5612fdc2a629e010914bf987e66c8290 100644 (file)
@@ -1,10 +1,10 @@
-/* $Cambridge: exim/src/src/verify.c,v 1.22 2005/06/27 14:29:44 ph10 Exp $ */
+/* $Cambridge: exim/src/src/verify.c,v 1.39 2006/09/25 11:25:37 ph10 Exp $ */
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) University of Cambridge 1995 - 2006 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Functions concerned with verifying things. The original code for callout
@@ -148,10 +148,12 @@ 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 *save_deliver_domain = deliver_domain;
 uschar **failure_ptr = is_recipient?
   &recipient_verify_failure : &sender_verify_failure;
 open_db dbblock;
@@ -227,10 +229,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)
@@ -379,6 +389,7 @@ for (host = host_list; host != NULL && !done; host = host->next)
   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];
@@ -409,18 +420,35 @@ 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);
+
+  /* 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 (active_hostname == NULL)
+      log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: failed to expand transport's "
+        "helo_data value for callout: %s", expand_string_message);
+    else active_hostname = s;
+    }
+
   deliver_host = deliver_host_address = NULL;
+  deliver_domain = save_deliver_domain;
 
   /* Set HELO string according to the protocol */
 
@@ -455,50 +483,73 @@ for (host = host_list; host != NULL && !done; host = host->next)
     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. */
+  /* 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 */
 
@@ -678,7 +729,7 @@ 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,
 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)
   {
@@ -786,6 +837,7 @@ 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;
   }
 return yield;
 }
@@ -813,6 +865,8 @@ 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.
@@ -850,6 +904,7 @@ 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 :
@@ -1030,10 +1085,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
@@ -1041,7 +1107,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
@@ -1050,13 +1116,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)
             {
@@ -1080,7 +1149,7 @@ while (addr_new != NULL)
               {
               nexthost = host->next;
               if (tf.gethostbyname ||
-                  string_is_ip_address(host->name, NULL) > 0)
+                  string_is_ip_address(host->name, NULL) != 0)
                 (void)host_find_byname(host, NULL, &canonical_name, TRUE);
               else
                 {
@@ -1219,9 +1288,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");
@@ -1245,9 +1317,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)
     {
@@ -1260,6 +1335,19 @@ else for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++)
     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);
@@ -1399,14 +1487,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
@@ -1421,8 +1511,8 @@ 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;
       }
@@ -1439,6 +1529,89 @@ return OK;
 
 
 
+/*************************************************
+*          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++;
+
+    parse_allow_group = TRUE;     /* Allow group syntax */
+
+    /* Loop for multiple addresses in the header */
+
+    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. */
+
+      *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 */
+    }     /* Next header (if found is false) */
+
+  if (!found) return FAIL;
+  }       /* Next recipient */
+
+return OK;
+}
+
+
 
 /*************************************************
 *          Find if verified sender               *
@@ -1846,7 +2019,7 @@ int maskoffset;
 BOOL iplookup = FALSE;
 BOOL isquery = FALSE;
 BOOL isiponly = cb->host_name != NULL && cb->host_name[0] == 0;
-uschar *t = ss;
+uschar *t;
 uschar *semicolon;
 uschar **aliases;
 
@@ -1884,15 +2057,33 @@ 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) > 0)
+if (string_is_ip_address(ss, &maskoffset) != 0)
   return (host_is_in_net(cb->host_address, ss, maskoffset)? OK : FAIL);
 
+/* 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))
+  {
+  *error = US"malformed IPv4 address or address mask";
+  return ERROR;
+  }
+
 /* See if there is a semicolon in the pattern */
 
 semicolon = Ustrchr(ss, ';');
 
 /* If we are doing an IP address only match, then all lookups must be IP
-address lookups. */
+address lookups, even if there is no "net-". */
 
 if (isiponly)
   {
@@ -1900,19 +2091,21 @@ if (isiponly)
   }
 
 /* Otherwise, if the item is of the form net[n]-lookup;<file|query> 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. */
-
-else if (Ustrncmp(ss, "net", 3) == 0 && semicolon != NULL)
+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. */
+
+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;
 
 /* Do the IP address lookup if that is indeed what we have */
 
@@ -1932,13 +2125,22 @@ if (iplookup)
   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
-  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. */
+  /* 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. */
 
-  if (mac_islookup(search_type, lookup_querystyle))
+  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;
@@ -1994,15 +2196,14 @@ if (*t == 0)
   h.name = ss;
   h.address = NULL;
   h.mx = MX_NONE;
+
   rc = host_find_byname(&h, NULL, 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;
     }
@@ -2041,7 +2242,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)
@@ -2677,7 +2878,7 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
     while ((keydomain = string_nextinlist(&key, &keysep, keybuffer,
             sizeof(keybuffer))) != NULL)
       {
-      if (string_is_ip_address(keydomain, NULL) > 0)
+      if (string_is_ip_address(keydomain, NULL) != 0)
         {
         uschar keyrevadd[128];
         invert_address(keyrevadd, keydomain);