Quota checking during reception. Bug 163
authorJeremy Harris <jgh146exb@wizmail.org>
Mon, 25 May 2020 21:57:57 +0000 (22:57 +0100)
committerJeremy Harris <jgh146exb@wizmail.org>
Tue, 26 May 2020 10:33:14 +0000 (11:33 +0100)
17 files changed:
doc/doc-docbook/spec.xfpt
doc/doc-txt/NewStuff
src/src/acl.c
src/src/child.c
src/src/deliver.c
src/src/exim.c
src/src/functions.h
src/src/globals.h
src/src/macros.h
src/src/smtp_in.c
src/src/transports/appendfile.c
src/src/verify.c
test/confs/0589 [new file with mode: 0644]
test/log/0589 [new file with mode: 0644]
test/rejectlog/0589 [new file with mode: 0644]
test/scripts/0000-Basic/0589 [new file with mode: 0644]
test/stdout/0589 [new file with mode: 0644]

index a1f361339ffa4d491d88dfff4ee74b4fbd10ced6..68f3b5b5a87f54af20f1aeef71e795e679846fd7 100644 (file)
@@ -3906,6 +3906,13 @@ together with the file descriptor number of an open pipe. Closure of the pipe
 signals the final completion of the sequence of processes that are passing
 messages through the same SMTP connection.
 
+.new
+.vitem &%-MCq%&&~<&'recipient&~address'&>&~<&'size'&>
+.oindex "&%-MCq%&"
+This option is not intended for use by external callers. It is used internally
+by Exim to implement quota checking for local users.
+.wen
+
 .vitem &%-MCS%&
 .oindex "&%-MCS%&"
 This option is not intended for use by external callers. It is used internally
@@ -32740,6 +32747,15 @@ The &%success_on_redirect%& option causes verification always to succeed
 immediately after a successful redirection. By default, if a redirection
 generates just one address, that address is also verified. See further
 discussion in section &<<SECTredirwhilveri>>&.
+.new
+.next
+If the &%quota%& option is specified for recipient verify,
+successful routing to an appendfile transport is followed by a call into
+the transport to evaluate the quota status for the recipient.
+No actual delivery is done, but verification will succeed if the quota
+is sufficient for the message (if the sender gave a message size) or
+not already exceeded (otherwise).
+.wen
 .endlist
 
 .cindex "verifying address" "differentiating failures"
@@ -32773,6 +32789,9 @@ connection, HELO, or MAIL).
 &%recipient%&: The RCPT command in a callout was rejected.
 .next
 &%postmaster%&: The postmaster check in a callout was rejected.
+.new
+.next
+&%quota%&: The quota check for a local recipient did non pass.
 .endlist
 
 The main use of these variables is expected to be to distinguish between
@@ -33102,6 +33121,38 @@ behaviour will be the same.
 
 
 
+.new
+.section "Quota caching" "SECTquotacache"
+.cindex "hints database" "quota cache"
+.cindex "quota" "cache, description of"
+.cindex "caching" "quota"
+Exim caches the results of quota verification
+in order to reduce the amount of resources used.
+The &"callout"& hints database is used.
+
+The default cache periods are five minutes for a positive (good) result
+and one hour for a negative result.
+To change the periods the &%quota%& option can be followed by an equals sign
+and a number of optional paramemters, separated by commas.
+For example:
+.code
+verify = recipient/quota=cachepos=1h,cacheneg=1d
+.endd
+Possible parameters are:
+.vlist
+.vitem &*cachepos&~=&~*&<&'time&~interval'&>
+.cindex "quota cache" "positive entry expiry, specifying"
+Set the lifetime for a positive cache entry.
+A value of zero seconds is legitimate.
+
+.vitem &*cacheneg&~=&~*&<&'time&~interval'&>
+.cindex "quota cache" "negative entry expiry, specifying"
+As above, for a negative entry.
+
+.vitem &*no_cache*&
+Set both positive and negative lifetimes to zero.
+.wen
+
 .section "Sender address verification reporting" "SECTsenaddver"
 .cindex "verifying" "suppressing error details"
 See section &<<SECTaddressverification>>& for a general discussion of
index 83b696fe51058e6ad2d7a99b56990f53a91afc07..9797e97db3a5ae9c8caf54e7c4bff387ff9f62c5 100644 (file)
@@ -26,6 +26,9 @@ Version 4.95
 
  6. An option to ignore the cache on a lookup.
 
+ 7. Quota checking during reception (i.e. at SMTP time) for appendfile-
+    transport-managed quotas.
+
 
 Version 4.94
 ------------
index 24716f0d10da82133641f471d7fc5d25bdaeb023..57a07296fd26d617991f00db39287d66f947638d 100644 (file)
 
 #define CALLOUT_TIMEOUT_DEFAULT 30
 
+/* Default quota cache TTLs */
+
+#define QUOTA_POS_DEFAULT (5*60)
+#define QUOTA_NEG_DEFAULT (60*60)
+
+
 /* ACL verb codes - keep in step with the table of verbs that follows */
 
 enum { ACL_ACCEPT, ACL_DEFER, ACL_DENY, ACL_DISCARD, ACL_DROP, ACL_REQUIRE,
@@ -1556,6 +1562,20 @@ static callout_opt_t callout_opt_list[] = {
 
 
 
+static int
+v_period(const uschar * s, const uschar * arg, uschar ** log_msgptr)
+{
+int period;
+if ((period = readconf_readtime(s, 0, FALSE)) < 0)
+  {
+  *log_msgptr = string_sprintf("bad time value in ACL condition "
+    "\"verify %s\"", arg);
+  }
+return period;
+}
+
+
+
 /* This function implements the "verify" condition. It is called when
 encountered in any ACL, because some tests are almost always permitted. Some
 just don't make sense, and always fail (for example, an attempt to test a host
@@ -1590,6 +1610,8 @@ BOOL defer_ok = FALSE;
 BOOL callout_defer_ok = FALSE;
 BOOL no_details = FALSE;
 BOOL success_on_redirect = FALSE;
+BOOL quota = FALSE;
+int quota_pos_cache = QUOTA_POS_DEFAULT, quota_neg_cache = QUOTA_NEG_DEFAULT;
 address_item *sender_vaddr = NULL;
 uschar *verify_sender_address = NULL;
 uschar *pm_mailfrom = NULL;
@@ -1823,12 +1845,8 @@ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)))
               }
             while (isspace(*opt)) opt++;
            }
-         if (op->timeval && (period = readconf_readtime(opt, 0, FALSE)) < 0)
-           {
-           *log_msgptr = string_sprintf("bad time value in ACL condition "
-             "\"verify %s\"", arg);
+         if (op->timeval && (period = v_period(opt, arg, log_msgptr)) < 0)
            return ERROR;
-           }
 
          switch(op->value)
            {
@@ -1861,6 +1879,38 @@ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)))
       }
     }
 
+  /* The quota option has sub-options, comma-separated */
+
+  else if (strncmpic(ss, US"quota", 5) == 0)
+    {
+    quota = TRUE;
+    if (*(ss += 5))
+      {
+      while (isspace(*ss)) ss++;
+      if (*ss++ == '=')
+        {
+       const uschar * sublist = ss;
+        int optsep = ',';
+       int period;
+
+        while (isspace(*sublist)) sublist++;
+        for (uschar * opt; opt = string_nextinlist(&sublist, &optsep, NULL, 0); )
+         if (Ustrncmp(opt, "cachepos=", 9) == 0)
+           if ((period = v_period(opt += 9, arg, log_msgptr)) < 0)
+             return ERROR;
+           else
+             quota_pos_cache = period;
+         else if (Ustrncmp(opt, "cacheneg=", 9) == 0)
+           if ((period = v_period(opt += 9, arg, log_msgptr)) < 0)
+             return ERROR;
+           else
+             quota_neg_cache = period;
+         else if (Ustrcmp(opt, "no_cache") == 0)
+           quota_pos_cache = quota_neg_cache = 0;
+       }
+      }
+    }
+
   /* Option not recognized */
 
   else
@@ -1879,6 +1929,31 @@ if ((verify_options & (vopt_callout_recipsender|vopt_callout_recippmaster)) ==
   return ERROR;
   }
 
+/* Handle quota verification */
+if (quota)
+  {
+  if (vp->value != VERIFY_RCPT)
+    {
+    *log_msgptr = US"can only verify quota of recipient";
+    return ERROR;
+    }
+
+  if ((rc = verify_quota_call(addr->address,
+             quota_pos_cache, quota_neg_cache, log_msgptr)) != OK)
+    {
+    *basic_errno = errno;
+    if (smtp_return_error_details)
+      {
+      if (!*user_msgptr && *log_msgptr)
+        *user_msgptr = string_sprintf("Rejected after %s: %s",
+           smtp_names[smtp_connection_had[smtp_ch_index-1]], *log_msgptr);
+      if (rc == DEFER) f.acl_temp_details = TRUE;
+      }
+    }
+
+  return rc;
+  }
+
 /* Handle sender-in-header verification. Default the user message to the log
 message if giving out verification details. */
 
index b36a96fe1ae3cf0fe98bac3d3fdcf42792f1d036..1407b3718cea54aa830736399428732e809b8967 100644 (file)
@@ -27,7 +27,7 @@ Arguments:
 Returns:       nothing
 */
 
-static void
+void
 force_fd(int oldfd, int newfd)
 {
 if (oldfd == newfd) return;
index acb87bca60b5c75daef46c8752af9e9abd1db340..dd922c72828c79819b3dd4edf1e9b00312078ad3 100644 (file)
@@ -2125,7 +2125,7 @@ Arguments:
 Returns:     nothing
 */
 
-static void
+void
 deliver_local(address_item *addr, BOOL shadowing)
 {
 BOOL use_initgroups;
index 905ef0a7008bbe12078dac39137f823a11e6ff72..4e7aae69a0f7143edd3e510ef9b07a2372eeda96 100644 (file)
@@ -1618,6 +1618,7 @@ BOOL removed_privilege = FALSE;
 BOOL usage_wanted = FALSE;
 BOOL verify_address_mode = FALSE;
 BOOL verify_as_sender = FALSE;
+BOOL rcpt_verify_quota = FALSE;
 BOOL version_printed = FALSE;
 uschar *alias_arg = NULL;
 uschar *called_as = US"";
@@ -2790,6 +2791,13 @@ on the second character (the one after '-'), to save some effort. */
                  else badarg = TRUE;
                  break;
 
+    /* -MCq: do a quota check on the given recipient for the given size
+    of message.  Separate from -MC. */
+       case 'q': rcpt_verify_quota = TRUE;
+                 if (++i < argc) message_size = Uatoi(argv[i]);
+                 else badarg = TRUE;
+                 break;
+
     /* -MCS: set the smtp_use_size flag; this is useful only when it
     precedes -MC (see above) */
 
@@ -4296,6 +4304,7 @@ if (  !unprivileged                               /* originally had root AND */
             || msg_action != MSG_DELIVER       /* not delivering          */
            )                                   /*       and               */
          && (!checking || !f.address_test_mode)        /* not address checking    */
+        && !rcpt_verify_quota                  /* and not quota checking  */
    )  )  )
   exim_setugid(exim_uid, exim_gid, TRUE, US"privilege not needed");
 
@@ -4414,6 +4423,18 @@ needed in transports so we lost the optimisation. */
 #endif
   }
 
+/* Handle a request to check quota */
+if (rcpt_verify_quota)
+  if (real_uid != root_uid && real_uid != exim_uid)
+    exim_fail("exim: Permission denied\n");
+  else if (recipients_arg >= argc)
+    exim_fail("exim: missing recipient for quota check\n");
+  else
+    {
+    verify_quota(argv[recipients_arg]);
+    exim_exit(EXIT_SUCCESS);
+    }
+
 /* Handle the -brt option. This is for checking out retry configurations.
 The next three arguments are a domain name or a complete address, and
 optionally two error numbers. All it does is to call the function that
index 2a4597202fbabfbe5a5e42e1c3595836390351a0..486a91595a09ed9e0e80d68a59166e792c6030cc 100644 (file)
@@ -189,6 +189,7 @@ extern void    debug_print_socket(int);
 extern void    decode_bits(unsigned int *, size_t, int *,
                   uschar *, bit_table *, int, uschar *, int);
 extern void    delete_pid_file(void);
+extern void    deliver_local(address_item *, BOOL);
 extern address_item *deliver_make_addr(uschar *, BOOL);
 extern void    delivery_log(int, address_item *, int, uschar *);
 extern int     deliver_message(uschar *, BOOL, BOOL);
@@ -260,6 +261,7 @@ extern BOOL    filter_runtest(int, uschar *, BOOL, BOOL);
 extern BOOL    filter_system_interpret(address_item **, uschar **);
 
 extern uschar * fn_hdrs_added(void);
+extern void    force_fd(int, int);
 
 extern void    header_add(int, const char *, ...);
 extern header_line *header_add_at_position_internal(BOOL, uschar *, BOOL, int, const char *, ...);
@@ -620,6 +622,8 @@ extern int     verify_check_this_host(const uschar **, unsigned int *,
                 const uschar*, const uschar *, const uschar **);
 extern address_item *verify_checked_sender(uschar *);
 extern void    verify_get_ident(int);
+extern void    verify_quota(uschar *);
+extern int     verify_quota_call(const uschar *, int, int, uschar **);
 extern BOOL    verify_sender(int *, uschar **);
 extern BOOL    verify_sender_preliminary(int *, uschar **);
 extern void    version_init(void);
index 1bdf338a2a11d9907e649fc0950c669dd1a4e2df..3dfbbc2b1889294537e7bcc2e79a789cbc00ad41 100644 (file)
@@ -943,6 +943,7 @@ extern int     smtp_load_reserve;      /* Only from reserved if load > this */
 extern int     smtp_mailcmd_count;     /* Count of MAIL commands */
 extern int     smtp_max_synprot_errors;/* Max syntax/protocol errors */
 extern int     smtp_max_unknown_commands; /* As it says */
+extern uschar *smtp_names[];          /* decode for command codes */
 extern uschar *smtp_notquit_reason;    /* Global for disconnect reason */
 extern FILE   *smtp_out;               /* Incoming SMTP output file */
 extern uschar *smtp_ratelimit_hosts;   /* Rate limit these hosts */
index a507bbf836914a316544b879d0a0d4b1e2892a4f..f6012447d02fd09621dae0d3337ae17b5e81d73f 100644 (file)
@@ -736,6 +736,7 @@ enum { v_none, v_sender, v_recipient, v_expn };
 #define vopt_callout_recippmaster 0x0100   /* use postmaster to verify recip */
 #define vopt_callout_hold        0x0200   /* lazy close connection */
 #define vopt_success_on_redirect  0x0400
+#define vopt_quota                0x0800   /* quota check, to local/appendfile */
 
 /* Values for fields in callout cache records */
 
index 2b4323becebfb495eed44c167d9c3a1d9af50c44..ce27c5f3e520b267f280e2bb227ab0c8903a1284 100644 (file)
@@ -227,7 +227,7 @@ static smtp_cmd_list *cmd_list_end =
 /* This list of names is used for performing the smtp_no_mail logging action.
 It must be kept in step with the SCH_xxx enumerations. */
 
-static uschar *smtp_names[] =
+uschar * smtp_names[] =
   {
   US"NONE", US"AUTH", US"DATA", US"BDAT", US"EHLO", US"ETRN", US"EXPN",
   US"HELO", US"HELP", US"MAIL", US"NOOP", US"QUIT", US"RCPT", US"RSET",
@@ -2058,6 +2058,7 @@ sending_ip_address = NULL;
 return_path = sender_address = NULL;
 deliver_localpart_data = deliver_domain_data =
 recipient_data = sender_data = NULL;                   /* Can be set by ACL */
+recipient_verify_failure = NULL;
 deliver_localpart_parent = deliver_localpart_orig = NULL;
 deliver_domain_parent = deliver_domain_orig = NULL;
 callout_address = NULL;
index f07f3bacbdb992ac4be8abf888420f82b5b04dba..a621de0749f4c77550af6f1ba219529c76be82eb 100644 (file)
@@ -2740,6 +2740,18 @@ if (!disable_quota && ob->quota_value > 0)
 
   }
 
+if (verify_mode)
+  {
+  addr->basic_errno = errno;
+  addr->message = US"Over quota";
+  addr->transport_return = yield;
+  DEBUG(D_transport)
+    debug_printf("appendfile (verify) yields %d with errno=%d more_errno=%d\n",
+      yield, addr->basic_errno, addr->more_errno);
+
+  goto RETURN;
+  }
+
 /* If we are writing in MBX format, what we actually do is to write the message
 to a temporary file, and then copy it to the real file once we know its size.
 This is the most straightforward way of getting the correct length in the
index 7f1752120e8244009f863c6c88313e3ef2ce32d8..3a40cea268d5d5a6f511af29ffe5d830263ac8a8 100644 (file)
@@ -568,6 +568,7 @@ if (!addr->transport)
   {
   HDEBUG(D_verify) debug_printf("cannot callout via null transport\n");
   }
+
 else if (Ustrcmp(addr->transport->driver_name, "smtp") != 0)
   log_write(0, LOG_MAIN|LOG_PANIC|LOG_CONFIG_FOR, "callout transport '%s': %s is non-smtp",
     addr->transport->name, addr->transport->driver_name);
@@ -1846,6 +1847,8 @@ while (addr_new)
 
   if (rc == OK)
     {
+    BOOL local_verify = FALSE;
+
     if (routed) *routed = TRUE;
     if (callout > 0)
       {
@@ -1872,72 +1875,76 @@ while (addr_new)
       transport's options, so as to mimic what would happen if we were really
       sending a message to this address. */
 
-      if ((tp = addr->transport) && !tp->info->local)
-        {
-        (void)(tp->setup)(tp, addr, &tf, 0, 0, NULL);
+      if ((tp = addr->transport))
+       if (!tp->info->local)
+         {
+         (void)(tp->setup)(tp, addr, &tf, 0, 0, NULL);
 
-        /* If the transport has hosts and the router does not, or if the
-        transport is configured to override the router's hosts, we must build a
-        host list of the transport's hosts, and find the IP addresses */
+         /* If the transport has hosts and the router does not, or if the
+         transport is configured to override the router's hosts, we must build a
+         host list of the transport's hosts, and find the IP addresses */
 
-        if (tf.hosts && (!host_list || tf.hosts_override))
-          {
-          uschar *s;
-          const uschar *save_deliver_domain = deliver_domain;
-          uschar *save_deliver_localpart = deliver_localpart;
-
-          host_list = NULL;    /* Ignore the router's hosts */
-
-          deliver_domain = addr->domain;
-          deliver_localpart = addr->local_part;
-          s = expand_string(tf.hosts);
-          deliver_domain = save_deliver_domain;
-          deliver_localpart = save_deliver_localpart;
-
-          if (!s)
-            {
-            log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand list of hosts "
-              "\"%s\" in %s transport for callout: %s", tf.hosts,
-              tp->name, expand_string_message);
-            }
-          else
-            {
-            int flags;
-            host_build_hostlist(&host_list, s, tf.hosts_randomize);
-
-            /* Just ignore failures to find a host address. If we don't manage
-            to find any addresses, the callout will defer. Note that more than
-            one address may be found for a single host, which will result in
-            additional host items being inserted into the chain. Hence we must
-            save the next host first. */
-
-            flags = HOST_FIND_BY_A | HOST_FIND_BY_AAAA;
-            if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE;
-            if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS;
-
-            for (host_item * host = host_list, * nexthost; host; host = nexthost)
-              {
-              nexthost = host->next;
-              if (tf.gethostbyname ||
-                  string_is_ip_address(host->name, NULL) != 0)
-                (void)host_find_byname(host, NULL, flags, NULL, TRUE);
-              else
+         if (tf.hosts && (!host_list || tf.hosts_override))
+           {
+           uschar *s;
+           const uschar *save_deliver_domain = deliver_domain;
+           uschar *save_deliver_localpart = deliver_localpart;
+
+           host_list = NULL;    /* Ignore the router's hosts */
+
+           deliver_domain = addr->domain;
+           deliver_localpart = addr->local_part;
+           s = expand_string(tf.hosts);
+           deliver_domain = save_deliver_domain;
+           deliver_localpart = save_deliver_localpart;
+
+           if (!s)
+             {
+             log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand list of hosts "
+               "\"%s\" in %s transport for callout: %s", tf.hosts,
+               tp->name, expand_string_message);
+             }
+           else
+             {
+             int flags;
+             host_build_hostlist(&host_list, s, tf.hosts_randomize);
+
+             /* Just ignore failures to find a host address. If we don't manage
+             to find any addresses, the callout will defer. Note that more than
+             one address may be found for a single host, which will result in
+             additional host items being inserted into the chain. Hence we must
+             save the next host first. */
+
+             flags = HOST_FIND_BY_A | HOST_FIND_BY_AAAA;
+             if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE;
+             if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS;
+
+             for (host_item * host = host_list, * nexthost; host; host = nexthost)
                {
-               const dnssec_domains * dsp = NULL;
-               if (Ustrcmp(tp->driver_name, "smtp") == 0)
+               nexthost = host->next;
+               if (tf.gethostbyname ||
+                   string_is_ip_address(host->name, NULL) != 0)
+                 (void)host_find_byname(host, NULL, flags, NULL, TRUE);
+               else
                  {
-                 smtp_transport_options_block * ob =
-                     (smtp_transport_options_block *) tp->options_block;
-                 dsp = &ob->dnssec;
-                 }
+                 const dnssec_domains * dsp = NULL;
+                 if (Ustrcmp(tp->driver_name, "smtp") == 0)
+                   {
+                   smtp_transport_options_block * ob =
+                       (smtp_transport_options_block *) tp->options_block;
+                   dsp = &ob->dnssec;
+                   }
 
-                (void) host_find_bydns(host, NULL, flags, NULL, NULL, NULL,
-                 dsp, NULL, NULL);
+                 (void) host_find_bydns(host, NULL, flags, NULL, NULL, NULL,
+                   dsp, NULL, NULL);
+                 }
                }
-              }
-            }
-          }
-        }
+             }
+           }
+         }
+       else if (  options & vopt_quota
+               && Ustrcmp(tp->driver_name, "appendfile") == 0)
+         local_verify = TRUE;
 
       /* Can only do a callout if we have at least one host! If the callout
       fails, it will have set ${sender,recipient}_verify_failure. */
@@ -1963,11 +1970,17 @@ while (addr_new)
 #endif
           }
         }
+      else if (local_verify)
+       {
+        HDEBUG(D_verify) debug_printf("Attempting quota verification\n");
+
+       deliver_set_expansions(addr);
+       deliver_local(addr, TRUE);
+       rc = addr->transport_return;
+       }
       else
-        {
         HDEBUG(D_verify) debug_printf("Cannot do callout: neither router nor "
           "transport provided a host list, or transport is not smtp\n");
-        }
       }
     }
 
@@ -3919,6 +3932,246 @@ while ((domain = string_nextinlist(&list, &sep, NULL, 0)))
 return FAIL;
 }
 
+
+
+/****************************************************
+  Verify a local user account for quota sufficiency
+****************************************************/
+
+/* The real work, done via a re-exec for privs, calls
+down to the transport for the quota check.
+
+Route and transport (in recipient-verify mode) the
+given recipient. 
+
+A routing result indicating any transport type other than appendfile
+results in a fail.
+
+Return, on stdout, a result string containing:
+- highlevel result code (OK, DEFER, FAIL)
+- errno
+- where string
+- message string
+*/
+
+void
+verify_quota(uschar * address)
+{
+address_item vaddr = {.address = address};
+BOOL routed;
+uschar * msg = US"\0";
+int rc, len = 1;
+
+if ((rc = verify_address(&vaddr, NULL, vopt_is_recipient | vopt_quota,
+    1, 0, 0, NULL, NULL, &routed)) != OK)
+  {
+  uschar * where = recipient_verify_failure;
+  msg = acl_verify_message ? acl_verify_message : vaddr.message;
+  if (!msg) msg = US"";
+  if (rc == DEFER && vaddr.basic_errno == ERRNO_EXIMQUOTA)
+    {
+    rc = FAIL;                                 /* DEFER -> FAIL */
+    where = US"quota";
+    vaddr.basic_errno = 0;
+    }
+  else if (!where) where = US"";
+
+  len = 5 + Ustrlen(msg) + 1 + Ustrlen(where);
+  msg = string_sprintf("%c%c%c%c%c%s%c%s", (uschar)rc,
+    (vaddr.basic_errno >> 24) && 0xff, (vaddr.basic_errno >> 16) && 0xff,
+    (vaddr.basic_errno >> 8) && 0xff, vaddr.basic_errno && 0xff,
+    where, '\0', msg);
+  }
+
+DEBUG(D_verify) debug_printf_indent("verify_quota: len %d\n", len);
+write(1, msg, len);
+return;
+}
+
+
+/******************************************************************************/
+
+/* Quota cache lookup.  We use the callout hints db also for the quota cache.
+Return TRUE if a nonexpired record was found, having filled in the yield
+argument.
+*/
+
+static BOOL
+cached_quota_lookup(const uschar * rcpt, int * yield,
+  int pos_cache, int neg_cache)
+{
+open_db dbblock, *dbm_file = NULL;
+dbdata_callout_cache_address * cache_address_record;
+
+if (!pos_cache && !neg_cache)
+  return FALSE;
+if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE, TRUE)))
+  {
+  HDEBUG(D_verify) debug_printf_indent("quota cache: not available\n");
+  return FALSE;
+  }
+if (!(cache_address_record = (dbdata_callout_cache_address *)
+    get_callout_cache_record(dbm_file, rcpt, US"address",
+      pos_cache, neg_cache)))
+  {
+  dbfn_close(dbm_file);
+  return FALSE;
+  }
+if (cache_address_record->result == ccache_accept)
+  *yield = OK;
+dbfn_close(dbm_file);
+return TRUE;
+}
+
+/* Quota cache write */
+
+static void
+cache_quota_write(const uschar * rcpt, int yield, int pos_cache, int neg_cache)
+{
+open_db dbblock, *dbm_file = NULL;
+dbdata_callout_cache_address cache_address_record;
+
+if (!pos_cache && !neg_cache)
+  return;
+if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE)))
+  {
+  HDEBUG(D_verify) debug_printf_indent("quota cache: not available\n");
+  return;
+  }
+
+cache_address_record.result = yield == OK ? ccache_accept : ccache_reject;
+
+(void)dbfn_write(dbm_file, rcpt, &cache_address_record,
+       (int)sizeof(dbdata_callout_cache_address));
+HDEBUG(D_verify) debug_printf_indent("wrote %s quota cache record for %s\n",
+      yield == OK ? "positive" : "negative", rcpt);
+
+dbfn_close(dbm_file);
+return;
+}
+
+
+/* To evaluate a local user's quota, starting in ACL, we need to
+fork & exec to regain privileges, to that we can change to the user's
+identity for access to their files.
+
+Arguments:
+ rcpt          Recipient account
+ pos_cache     Number of seconds to cache a positive result (delivery
+               to be accepted).  Zero to disable caching.
+ neg_cache     Number of seconds to cache a negative result.  Zero to disable.
+ msg           Pointer to result string pointer
+
+Return:                OK/DEFER/FAIL code
+*/
+
+int
+verify_quota_call(const uschar * rcpt, int pos_cache, int neg_cache,
+  uschar ** msg)
+{
+int pfd[2], pid, save_errno, yield = FAIL;
+void (*oldsignal)(int);
+const uschar * where = US"socketpair";
+
+*msg = NULL;
+
+if (cached_quota_lookup(rcpt, &yield, pos_cache, neg_cache))
+  {
+  HDEBUG(D_verify) debug_printf_indent("quota cache: address record is %d\n",
+    yield == OK ? "positive" : "negative");
+  if (yield != OK)
+    {
+    recipient_verify_failure = US"quota";
+    acl_verify_message = *msg =
+      US"Previous (cached) quota verification failure";
+    }
+  return yield;
+  }
+
+if (pipe(pfd) != 0)
+  goto fail;
+
+where = US"fork";
+oldsignal = signal(SIGCHLD, SIG_DFL);
+if ((pid = exim_fork(US"quota-verify")) < 0)
+  {
+  save_errno = errno;
+  close(pfd[pipe_write]);
+  close(pfd[pipe_read]);
+  errno = save_errno;
+  goto fail;
+  }
+
+if (pid == 0)          /* child */
+  {
+  close(pfd[pipe_read]);
+  force_fd(pfd[pipe_write], 1);                /* stdout to pipe */
+  close(pfd[pipe_write]);
+  dup2(1, 0);
+  if (debug_fd > 0) force_fd(debug_fd, 2);
+
+  child_exec_exim(CEE_EXEC_EXIT, FALSE, NULL, FALSE, 3,
+    US"-MCq", string_sprintf("%d", message_size), rcpt);
+  /*NOTREACHED*/
+  }
+
+save_errno = errno;
+close(pfd[pipe_write]);
+
+if (pid < 0)
+  {
+  DEBUG(D_verify) debug_printf_indent(" fork: %s\n", strerror(save_errno));
+  }
+else
+  {
+  uschar buf[128];
+  int n = read(pfd[pipe_read], buf, sizeof(buf));
+  int status;
+
+  waitpid(pid, &status, 0);
+  if (status == 0)
+    {
+    uschar * s;
+
+    if (n > 0) yield = buf[0];
+    if (n > 4)
+      save_errno = (buf[1] << 24) | (buf[2] << 16) | (buf[3] << 8) | buf[4];
+    if ((recipient_verify_failure = n > 5
+       ? string_copyn_taint(buf+5, n-5, FALSE) : NULL))
+      {
+      int m;
+      s = buf + 5 + Ustrlen(recipient_verify_failure) + 1;
+      m = n - (s - buf);
+      acl_verify_message = *msg =
+       m > 0 ? string_copyn_taint(s, m, FALSE) : NULL;
+      }
+
+    DEBUG(D_verify) debug_printf_indent("verify call response:"
+      " len %d yield %s errno '%s' where '%s' msg '%s'\n",
+      n, rc_names[yield], strerror(save_errno), recipient_verify_failure, *msg);
+
+    if (  yield == OK
+       || save_errno == 0 && Ustrcmp(recipient_verify_failure, "quota") == 0)
+      cache_quota_write(rcpt, yield, pos_cache, neg_cache);
+    else DEBUG(D_verify)
+      debug_printf_indent("result not cacheable\n");
+    }
+  else
+    {
+    DEBUG(D_verify)
+      debug_printf_indent("verify call response: waitpid status 0x%04x\n", status);
+    }
+  }
+
+close(pfd[pipe_read]);
+errno = save_errno;
+
+fail:
+
+return yield;
+}
+
+
 /* vi: aw ai sw=2
 */
 /* End of verify.c */
diff --git a/test/confs/0589 b/test/confs/0589
new file mode 100644 (file)
index 0000000..b0f7311
--- /dev/null
@@ -0,0 +1,60 @@
+# Exim test configuration 0589
+
+.include DIR/aux-var/std_conf_prefix
+
+primary_hostname = myhost.test.ex
+
+# ----- Main settings -----
+
+acl_smtp_rcpt = chk_rcpt
+log_selector = +millisec
+
+# --- ACL ---
+
+begin acl
+
+chk_rcpt:
+  deny         !verify = recipient/quota=cachepos=20s,cacheneg=100s
+               logwrite = where='$recipient_verify_failure' msg='$acl_verify_message'
+  accept
+
+loggit:
+  accept       logwrite = $acl_arg1 was called for $local_part@$domain
+               message = /
+
+# ----- Routers -----
+
+begin routers
+
+localuser:
+  driver =     accept
+  local_parts =        !route_fail
+  transport =  appendfile
+  # set address_data purely to get logging of use of the router
+  address_data = ${acl {loggit}{router   }}
+
+failrouter:
+  driver =     accept
+  condition =  ${if eq {${acl {loggit}{router   }}}{} {no}{no}}
+  transport =  appendfile
+
+# ----- Transports -----
+
+begin transports
+
+appendfile:
+  driver =     appendfile
+  file =       DIR/test-mail/themailfile
+  quota =      1k
+  user =       CALLER
+  # set homedir purely to get logging of use of the transport
+  home_directory = ${acl {loggit}{transport}}
+
+# ----- Retry -----
+
+begin retry
+
+* * F,5d,1d
+
+
+# End
diff --git a/test/log/0589 b/test/log/0589
new file mode 100644 (file)
index 0000000..dcbd656
--- /dev/null
@@ -0,0 +1,17 @@
+
+******** SERVER ********
+2017-07-30 18:51:05.712 exim x.yz daemon started: pid=pppp, no queue runs, listening for SMTP on port PORT_D
+2017-07-30 18:51:05.712 router    was called for quota_good@test.ex
+2017-07-30 18:51:05.712 transport was called for quota_good@test.ex
+2017-07-30 18:51:05.712 router    was called for quota_fail@test.ex
+2017-07-30 18:51:05.712 transport was called for quota_fail@test.ex
+2017-07-30 18:51:05.712 where='quota' msg='Over quota'
+2017-07-30 18:51:05.712 H=(test) [127.0.0.1] F=<b@test.ex> rejected RCPT <quota_fail@test.ex>: Over quota
+2017-07-30 18:51:05.712 router    was called for route_fail@test.ex
+2017-07-30 18:51:05.712 where='route' msg='Unrouteable address'
+2017-07-30 18:51:05.712 H=(test) [127.0.0.1] F=<c@test.ex> rejected RCPT <route_fail@test.ex>: Unrouteable address
+2017-07-30 18:51:05.712 where='quota' msg='Previous (cached) quota verification failure'
+2017-07-30 18:51:05.712 H=(test) [127.0.0.1] F=<e@test.ex> rejected RCPT <quota_fail@test.ex>: Previous (cached) quota verification failure
+2017-07-30 18:51:05.712 router    was called for route_fail@test.ex
+2017-07-30 18:51:05.712 where='route' msg='Unrouteable address'
+2017-07-30 18:51:05.712 H=(test) [127.0.0.1] F=<f@test.ex> rejected RCPT <route_fail@test.ex>: Unrouteable address
diff --git a/test/rejectlog/0589 b/test/rejectlog/0589
new file mode 100644 (file)
index 0000000..432b678
--- /dev/null
@@ -0,0 +1,6 @@
+
+******** SERVER ********
+2017-07-30 18:51:05.712 H=(test) [127.0.0.1] F=<b@test.ex> rejected RCPT <quota_fail@test.ex>: Over quota
+2017-07-30 18:51:05.712 H=(test) [127.0.0.1] F=<c@test.ex> rejected RCPT <route_fail@test.ex>: Unrouteable address
+2017-07-30 18:51:05.712 H=(test) [127.0.0.1] F=<e@test.ex> rejected RCPT <quota_fail@test.ex>: Previous (cached) quota verification failure
+2017-07-30 18:51:05.712 H=(test) [127.0.0.1] F=<f@test.ex> rejected RCPT <route_fail@test.ex>: Unrouteable address
diff --git a/test/scripts/0000-Basic/0589 b/test/scripts/0000-Basic/0589
new file mode 100644 (file)
index 0000000..8cea367
--- /dev/null
@@ -0,0 +1,44 @@
+# verify quota at smtp time
+#
+exim -bd -DSERVER=server -oX PORT_D
+****
+client 127.0.0.1 PORT_D
+??? 220
+EHLO test
+??? 250-
+??? 250-SIZE
+??? 250-8BITMIME
+??? 250-PIPELINING
+??? 250 HELP
+MAIL FROM:<a@test.ex>\r\nRCPT TO:<quota_good@test.ex>
+??? 250
+??? 250
+RSET
+??? 250
+MAIL FROM:<b@test.ex> SIZE=2048\r\nRCPT TO:<quota_fail@test.ex>
+??? 250
+??? 550
+RSET
+??? 250
+MAIL FROM:<c@test.ex>\r\nRCPT TO:<route_fail@test.ex>
+??? 250
+??? 550
+RSET
+??? 250
+MAIL FROM:<d@test.ex>\r\nRCPT TO:<quota_good@test.ex>
+??? 250
+??? 250
+RSET
+??? 250
+MAIL FROM:<e@test.ex> SIZE=512\r\nRCPT TO:<quota_fail@test.ex>
+??? 250
+??? 550
+RSET
+??? 250
+MAIL FROM:<f@test.ex>\r\nRCPT TO:<route_fail@test.ex>
+??? 250
+??? 550
+QUIT
+****
+#
+killdaemon
diff --git a/test/stdout/0589 b/test/stdout/0589
new file mode 100644 (file)
index 0000000..9f70b73
--- /dev/null
@@ -0,0 +1,61 @@
+Connecting to 127.0.0.1 port 1225 ... connected
+??? 220
+<<< 220 myhost.test.ex ESMTP Exim x.yz Tue, 2 Mar 1999 09:44:33 +0000
+>>> EHLO test
+??? 250-
+<<< 250-myhost.test.ex Hello test [127.0.0.1]
+??? 250-SIZE
+<<< 250-SIZE 52428800
+??? 250-8BITMIME
+<<< 250-8BITMIME
+??? 250-PIPELINING
+<<< 250-PIPELINING
+??? 250 HELP
+<<< 250 HELP
+>>> MAIL FROM:<a@test.ex>\r\nRCPT TO:<quota_good@test.ex>
+??? 250
+<<< 250 OK
+??? 250
+<<< 250 Accepted
+>>> RSET
+??? 250
+<<< 250 Reset OK
+>>> MAIL FROM:<b@test.ex> SIZE=ssss\r\nRCPT TO:<quota_fail@test.ex>
+??? 250
+<<< 250 OK
+??? 550
+<<< 550 Administrative prohibition
+>>> RSET
+??? 250
+<<< 250 Reset OK
+>>> MAIL FROM:<c@test.ex>\r\nRCPT TO:<route_fail@test.ex>
+??? 250
+<<< 250 OK
+??? 550
+<<< 550 Administrative prohibition
+>>> RSET
+??? 250
+<<< 250 Reset OK
+>>> MAIL FROM:<d@test.ex>\r\nRCPT TO:<quota_good@test.ex>
+??? 250
+<<< 250 OK
+??? 250
+<<< 250 Accepted
+>>> RSET
+??? 250
+<<< 250 Reset OK
+>>> MAIL FROM:<e@test.ex> SIZE=ssss\r\nRCPT TO:<quota_fail@test.ex>
+??? 250
+<<< 250 OK
+??? 550
+<<< 550 Administrative prohibition
+>>> RSET
+??? 250
+<<< 250 Reset OK
+>>> MAIL FROM:<f@test.ex>\r\nRCPT TO:<route_fail@test.ex>
+??? 250
+<<< 250 OK
+??? 550
+<<< 550 Administrative prohibition
+>>> QUIT
+End of script