-/* $Cambridge: exim/src/src/acl.c,v 1.3 2004/10/19 11:04:26 ph10 Exp $ */
-
/*************************************************
* Exim - an Internet mail transport agent *
*************************************************/
-/* Copyright (c) University of Cambridge 1995 - 2004 */
+/* Copyright (c) University of Cambridge 1995 - 2012 */
/* See the file NOTICE for conditions of use and distribution. */
/* Code for handling Access Control Lists (ACLs) */
{ US"accept", US"defer", US"deny", US"discard", US"drop", US"require",
US"warn" };
-/* For each verb, the condition for which "message" is used */
-
-static int msgcond[] = { FAIL, OK, OK, FAIL, OK, FAIL, OK };
+/* For each verb, the conditions for which "message" or "log_message" are used
+are held as a bitmap. This is to avoid expanding the strings unnecessarily. For
+"accept", the FAIL case is used only after "endpass", but that is selected in
+the code. */
+
+static int msgcond[] = {
+ (1<<OK) | (1<<FAIL) | (1<<FAIL_DROP), /* accept */
+ (1<<OK), /* defer */
+ (1<<OK), /* deny */
+ (1<<OK) | (1<<FAIL) | (1<<FAIL_DROP), /* discard */
+ (1<<OK), /* drop */
+ (1<<FAIL) | (1<<FAIL_DROP), /* require */
+ (1<<OK) /* warn */
+ };
/* ACL condition and modifier codes - keep in step with the table that
-follows. */
-
-enum { ACLC_ACL, ACLC_AUTHENTICATED, ACLC_CONDITION, ACLC_CONTROL, ACLC_DELAY,
- ACLC_DNSLISTS, ACLC_DOMAINS, ACLC_ENCRYPTED, ACLC_ENDPASS, ACLC_HOSTS,
- ACLC_LOCAL_PARTS, ACLC_LOG_MESSAGE, ACLC_LOGWRITE, ACLC_MESSAGE,
- ACLC_RECIPIENTS, ACLC_SENDER_DOMAINS, ACLC_SENDERS, ACLC_SET, ACLC_VERIFY };
+follows, and the cond_expand_at_top and uschar cond_modifiers tables lower
+down. */
+
+enum { ACLC_ACL,
+ ACLC_ADD_HEADER,
+ ACLC_AUTHENTICATED,
+#ifdef EXPERIMENTAL_BRIGHTMAIL
+ ACLC_BMI_OPTIN,
+#endif
+ ACLC_CONDITION,
+ ACLC_CONTINUE,
+ ACLC_CONTROL,
+#ifdef EXPERIMENTAL_DCC
+ ACLC_DCC,
+#endif
+#ifdef WITH_CONTENT_SCAN
+ ACLC_DECODE,
+#endif
+ ACLC_DELAY,
+#ifdef WITH_OLD_DEMIME
+ ACLC_DEMIME,
+#endif
+#ifndef DISABLE_DKIM
+ ACLC_DKIM_SIGNER,
+ ACLC_DKIM_STATUS,
+#endif
+ ACLC_DNSLISTS,
+ ACLC_DOMAINS,
+ ACLC_ENCRYPTED,
+ ACLC_ENDPASS,
+ ACLC_HOSTS,
+ ACLC_LOCAL_PARTS,
+ ACLC_LOG_MESSAGE,
+ ACLC_LOG_REJECT_TARGET,
+ ACLC_LOGWRITE,
+#ifdef WITH_CONTENT_SCAN
+ ACLC_MALWARE,
+#endif
+ ACLC_MESSAGE,
+#ifdef WITH_CONTENT_SCAN
+ ACLC_MIME_REGEX,
+#endif
+ ACLC_RATELIMIT,
+ ACLC_RECIPIENTS,
+#ifdef WITH_CONTENT_SCAN
+ ACLC_REGEX,
+#endif
+ ACLC_SENDER_DOMAINS,
+ ACLC_SENDERS,
+ ACLC_SET,
+#ifdef WITH_CONTENT_SCAN
+ ACLC_SPAM,
+#endif
+#ifdef EXPERIMENTAL_SPF
+ ACLC_SPF,
+ ACLC_SPF_GUESS,
+#endif
+ ACLC_VERIFY };
+
+/* ACL conditions/modifiers: "delay", "control", "continue", "endpass",
+"message", "log_message", "log_reject_target", "logwrite", and "set" are
+modifiers that look like conditions but always return TRUE. They are used for
+their side effects. */
+
+static uschar *conditions[] = {
+ US"acl",
+ US"add_header",
+ US"authenticated",
+#ifdef EXPERIMENTAL_BRIGHTMAIL
+ US"bmi_optin",
+#endif
+ US"condition",
+ US"continue",
+ US"control",
+#ifdef EXPERIMENTAL_DCC
+ US"dcc",
+#endif
+#ifdef WITH_CONTENT_SCAN
+ US"decode",
+#endif
+ US"delay",
+#ifdef WITH_OLD_DEMIME
+ US"demime",
+#endif
+#ifndef DISABLE_DKIM
+ US"dkim_signers",
+ US"dkim_status",
+#endif
+ US"dnslists",
+ US"domains",
+ US"encrypted",
+ US"endpass",
+ US"hosts",
+ US"local_parts",
+ US"log_message",
+ US"log_reject_target",
+ US"logwrite",
+#ifdef WITH_CONTENT_SCAN
+ US"malware",
+#endif
+ US"message",
+#ifdef WITH_CONTENT_SCAN
+ US"mime_regex",
+#endif
+ US"ratelimit",
+ US"recipients",
+#ifdef WITH_CONTENT_SCAN
+ US"regex",
+#endif
+ US"sender_domains", US"senders", US"set",
+#ifdef WITH_CONTENT_SCAN
+ US"spam",
+#endif
+#ifdef EXPERIMENTAL_SPF
+ US"spf",
+ US"spf_guess",
+#endif
+ US"verify" };
-/* ACL conditions/modifiers: "delay", "control", "endpass", "message",
-"log_message", "logwrite", and "set" are modifiers that look like conditions
-but always return TRUE. They are used for their side effects. */
-static uschar *conditions[] = { US"acl", US"authenticated", US"condition",
- US"control", US"delay", US"dnslists", US"domains", US"encrypted",
- US"endpass", US"hosts", US"local_parts", US"log_message", US"logwrite",
- US"message", US"recipients", US"sender_domains", US"senders", US"set",
- US"verify" };
-
-/* ACL control names */
+/* Return values from decode_control(); keep in step with the table of names
+that follows! */
+
+enum {
+ CONTROL_AUTH_UNADVERTISED,
+ #ifdef EXPERIMENTAL_BRIGHTMAIL
+ CONTROL_BMI_RUN,
+ #endif
+ CONTROL_DEBUG,
+ #ifndef DISABLE_DKIM
+ CONTROL_DKIM_VERIFY,
+ #endif
+ CONTROL_ERROR,
+ CONTROL_CASEFUL_LOCAL_PART,
+ CONTROL_CASELOWER_LOCAL_PART,
+ CONTROL_ENFORCE_SYNC,
+ CONTROL_NO_ENFORCE_SYNC,
+ CONTROL_FREEZE,
+ CONTROL_QUEUE_ONLY,
+ CONTROL_SUBMISSION,
+ CONTROL_SUPPRESS_LOCAL_FIXUPS,
+ #ifdef WITH_CONTENT_SCAN
+ CONTROL_NO_MBOX_UNSPOOL,
+ #endif
+ CONTROL_FAKEDEFER,
+ CONTROL_FAKEREJECT,
+ CONTROL_NO_MULTILINE,
+ CONTROL_NO_PIPELINING,
+ CONTROL_NO_DELAY_FLUSH,
+ CONTROL_NO_CALLOUT_FLUSH
+};
-static uschar *controls[] = { US"error", US"caseful_local_part",
- US"caselower_local_part", US"enforce_sync", US"no_enforce_sync", US"freeze",
- US"queue_only", US"submission", US"no_multiline"};
+/* ACL control names; keep in step with the table above! This list is used for
+turning ids into names. The actual list of recognized names is in the variable
+control_def controls_list[] below. The fact that there are two lists is a mess
+and should be tidied up. */
+
+static uschar *controls[] = {
+ US"allow_auth_unadvertised",
+ #ifdef EXPERIMENTAL_BRIGHTMAIL
+ US"bmi_run",
+ #endif
+ US"debug",
+ #ifndef DISABLE_DKIM
+ US"dkim_disable_verify",
+ #endif
+ US"error",
+ US"caseful_local_part",
+ US"caselower_local_part",
+ US"enforce_sync",
+ US"no_enforce_sync",
+ US"freeze",
+ US"queue_only",
+ US"submission",
+ US"suppress_local_fixups",
+ #ifdef WITH_CONTENT_SCAN
+ US"no_mbox_unspool",
+ #endif
+ US"fakedefer",
+ US"fakereject",
+ US"no_multiline_responses",
+ US"no_pipelining",
+ US"no_delay_flush",
+ US"no_callout_flush"
+};
-/* Flags to indicate for which conditions /modifiers a string expansion is done
+/* Flags to indicate for which conditions/modifiers a string expansion is done
at the outer level. In the other cases, expansion already occurs in the
checking functions. */
static uschar cond_expand_at_top[] = {
TRUE, /* acl */
+ TRUE, /* add_header */
FALSE, /* authenticated */
+#ifdef EXPERIMENTAL_BRIGHTMAIL
+ TRUE, /* bmi_optin */
+#endif
TRUE, /* condition */
+ TRUE, /* continue */
TRUE, /* control */
+#ifdef EXPERIMENTAL_DCC
+ TRUE, /* dcc */
+#endif
+#ifdef WITH_CONTENT_SCAN
+ TRUE, /* decode */
+#endif
TRUE, /* delay */
+#ifdef WITH_OLD_DEMIME
+ TRUE, /* demime */
+#endif
+#ifndef DISABLE_DKIM
+ TRUE, /* dkim_signers */
+ TRUE, /* dkim_status */
+#endif
TRUE, /* dnslists */
FALSE, /* domains */
FALSE, /* encrypted */
FALSE, /* hosts */
FALSE, /* local_parts */
TRUE, /* log_message */
+ TRUE, /* log_reject_target */
TRUE, /* logwrite */
+#ifdef WITH_CONTENT_SCAN
+ TRUE, /* malware */
+#endif
TRUE, /* message */
+#ifdef WITH_CONTENT_SCAN
+ TRUE, /* mime_regex */
+#endif
+ TRUE, /* ratelimit */
FALSE, /* recipients */
+#ifdef WITH_CONTENT_SCAN
+ TRUE, /* regex */
+#endif
FALSE, /* sender_domains */
FALSE, /* senders */
TRUE, /* set */
+#ifdef WITH_CONTENT_SCAN
+ TRUE, /* spam */
+#endif
+#ifdef EXPERIMENTAL_SPF
+ TRUE, /* spf */
+ TRUE, /* spf_guess */
+#endif
TRUE /* verify */
};
static uschar cond_modifiers[] = {
FALSE, /* acl */
+ TRUE, /* add_header */
FALSE, /* authenticated */
+#ifdef EXPERIMENTAL_BRIGHTMAIL
+ TRUE, /* bmi_optin */
+#endif
FALSE, /* condition */
+ TRUE, /* continue */
TRUE, /* control */
+#ifdef EXPERIMENTAL_DCC
+ FALSE, /* dcc */
+#endif
+#ifdef WITH_CONTENT_SCAN
+ FALSE, /* decode */
+#endif
TRUE, /* delay */
+#ifdef WITH_OLD_DEMIME
+ FALSE, /* demime */
+#endif
+#ifndef DISABLE_DKIM
+ FALSE, /* dkim_signers */
+ FALSE, /* dkim_status */
+#endif
FALSE, /* dnslists */
FALSE, /* domains */
FALSE, /* encrypted */
FALSE, /* hosts */
FALSE, /* local_parts */
TRUE, /* log_message */
- TRUE, /* log_write */
+ TRUE, /* log_reject_target */
+ TRUE, /* logwrite */
+#ifdef WITH_CONTENT_SCAN
+ FALSE, /* malware */
+#endif
TRUE, /* message */
+#ifdef WITH_CONTENT_SCAN
+ FALSE, /* mime_regex */
+#endif
+ FALSE, /* ratelimit */
FALSE, /* recipients */
+#ifdef WITH_CONTENT_SCAN
+ FALSE, /* regex */
+#endif
FALSE, /* sender_domains */
FALSE, /* senders */
TRUE, /* set */
+#ifdef WITH_CONTENT_SCAN
+ FALSE, /* spam */
+#endif
+#ifdef EXPERIMENTAL_SPF
+ FALSE, /* spf */
+ FALSE, /* spf_guess */
+#endif
FALSE /* verify */
};
-/* Bit map vector of which conditions are not allowed at certain times. For
-each condition, there's a bitmap of dis-allowed times. */
+/* Bit map vector of which conditions and modifiers are not allowed at certain
+times. For each condition and modifier, there's a bitmap of dis-allowed times.
+For some, it is easier to specify the negation of a small number of allowed
+times. */
static unsigned int cond_forbids[] = {
0, /* acl */
- (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_CONNECT)| /* authenticated */
- (1<<ACL_WHERE_HELO),
- 0, /* condition */
- /* Certain types of control are always allowed, so we let it through
- always and check in the control processing itself */
+ (unsigned int)
+ ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)| /* add_header */
+ (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
+ (1<<ACL_WHERE_MIME)|(1<<ACL_WHERE_NOTSMTP)|
+ (1<<ACL_WHERE_DKIM)|
+ (1<<ACL_WHERE_NOTSMTP_START)),
- 0, /* control */
- 0, /* delay */
- (1<<ACL_WHERE_NOTSMTP), /* dnslists */
+ (1<<ACL_WHERE_NOTSMTP)| /* authenticated */
+ (1<<ACL_WHERE_NOTSMTP_START)|
+ (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO),
- (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_AUTH)| /* domains */
+ #ifdef EXPERIMENTAL_BRIGHTMAIL
+ (1<<ACL_WHERE_AUTH)| /* bmi_optin */
(1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
- (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_PREDATA)|
+ (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_MIME)|
(1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
- (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
+ (1<<ACL_WHERE_MAILAUTH)|
(1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
- (1<<ACL_WHERE_VRFY),
+ (1<<ACL_WHERE_VRFY)|(1<<ACL_WHERE_PREDATA)|
+ (1<<ACL_WHERE_NOTSMTP_START),
+ #endif
+
+ 0, /* condition */
+
+ 0, /* continue */
+
+ /* Certain types of control are always allowed, so we let it through
+ always and check in the control processing itself. */
+
+ 0, /* control */
+
+ #ifdef EXPERIMENTAL_DCC
+ (unsigned int)
+ ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)), /* dcc */
+ #endif
+
+ #ifdef WITH_CONTENT_SCAN
+ (unsigned int)
+ ~(1<<ACL_WHERE_MIME), /* decode */
+ #endif
- (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_CONNECT)| /* encrypted */
+ (1<<ACL_WHERE_NOTQUIT), /* delay */
+
+ #ifdef WITH_OLD_DEMIME
+ (unsigned int)
+ ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)), /* demime */
+ #endif
+
+ #ifndef DISABLE_DKIM
+ (unsigned int)
+ ~(1<<ACL_WHERE_DKIM), /* dkim_signers */
+
+ (unsigned int)
+ ~(1<<ACL_WHERE_DKIM), /* dkim_status */
+ #endif
+
+ (1<<ACL_WHERE_NOTSMTP)| /* dnslists */
+ (1<<ACL_WHERE_NOTSMTP_START),
+
+ (unsigned int)
+ ~(1<<ACL_WHERE_RCPT), /* domains */
+
+ (1<<ACL_WHERE_NOTSMTP)| /* encrypted */
+ (1<<ACL_WHERE_CONNECT)|
+ (1<<ACL_WHERE_NOTSMTP_START)|
(1<<ACL_WHERE_HELO),
+
0, /* endpass */
- (1<<ACL_WHERE_NOTSMTP), /* hosts */
- (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_AUTH)| /* local_parts */
- (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
- (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_PREDATA)|
- (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
- (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
- (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
- (1<<ACL_WHERE_VRFY),
+ (1<<ACL_WHERE_NOTSMTP)| /* hosts */
+ (1<<ACL_WHERE_NOTSMTP_START),
+
+ (unsigned int)
+ ~(1<<ACL_WHERE_RCPT), /* local_parts */
0, /* log_message */
+
+ 0, /* log_reject_target */
+
0, /* logwrite */
+
+ #ifdef WITH_CONTENT_SCAN
+ (unsigned int)
+ ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)), /* malware */
+ #endif
+
0, /* message */
- (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_AUTH)| /* recipients */
- (1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)|
- (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_PREDATA)|
- (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
- (1<<ACL_WHERE_MAILAUTH)|(1<<ACL_WHERE_QUIT)|
- (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_STARTTLS)|
- (1<<ACL_WHERE_VRFY),
+ #ifdef WITH_CONTENT_SCAN
+ (unsigned int)
+ ~(1<<ACL_WHERE_MIME), /* mime_regex */
+ #endif
+
+ 0, /* ratelimit */
+
+ (unsigned int)
+ ~(1<<ACL_WHERE_RCPT), /* recipients */
+
+ #ifdef WITH_CONTENT_SCAN
+ (unsigned int)
+ ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)| /* regex */
+ (1<<ACL_WHERE_MIME)),
+ #endif
(1<<ACL_WHERE_AUTH)|(1<<ACL_WHERE_CONNECT)| /* sender_domains */
(1<<ACL_WHERE_HELO)|
0, /* set */
+ #ifdef WITH_CONTENT_SCAN
+ (unsigned int)
+ ~((1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)), /* spam */
+ #endif
+
+ #ifdef EXPERIMENTAL_SPF
+ (1<<ACL_WHERE_AUTH)|(1<<ACL_WHERE_CONNECT)| /* spf */
+ (1<<ACL_WHERE_HELO)|
+ (1<<ACL_WHERE_MAILAUTH)|
+ (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
+ (1<<ACL_WHERE_STARTTLS)|(1<<ACL_WHERE_VRFY)|
+ (1<<ACL_WHERE_NOTSMTP)|
+ (1<<ACL_WHERE_NOTSMTP_START),
+
+ (1<<ACL_WHERE_AUTH)|(1<<ACL_WHERE_CONNECT)| /* spf_guess */
+ (1<<ACL_WHERE_HELO)|
+ (1<<ACL_WHERE_MAILAUTH)|
+ (1<<ACL_WHERE_ETRN)|(1<<ACL_WHERE_EXPN)|
+ (1<<ACL_WHERE_STARTTLS)|(1<<ACL_WHERE_VRFY)|
+ (1<<ACL_WHERE_NOTSMTP)|
+ (1<<ACL_WHERE_NOTSMTP_START),
+ #endif
+
/* Certain types of verify are always allowed, so we let it through
always and check in the verify function itself */
};
-/* Return values from decode_control() */
-
-enum { CONTROL_ERROR, CONTROL_CASEFUL_LOCAL_PART, CONTROL_CASELOWER_LOCAL_PART,
- CONTROL_ENFORCE_SYNC, CONTROL_NO_ENFORCE_SYNC, CONTROL_FREEZE,
- CONTROL_QUEUE_ONLY, CONTROL_SUBMISSION, CONTROL_NO_MULTILINE };
-
/* Bit map vector of which controls are not allowed at certain times. For
each control, there's a bitmap of dis-allowed times. For some, it is easier to
specify the negation of a small number of allowed times. */
static unsigned int control_forbids[] = {
+ (unsigned int)
+ ~((1<<ACL_WHERE_CONNECT)|(1<<ACL_WHERE_HELO)), /* allow_auth_unadvertised */
+
+ #ifdef EXPERIMENTAL_BRIGHTMAIL
+ 0, /* bmi_run */
+ #endif
+
+ 0, /* debug */
+
+ #ifndef DISABLE_DKIM
+ (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP)| /* dkim_disable_verify */
+ (1<<ACL_WHERE_NOTSMTP_START),
+ #endif
+
0, /* error */
+
+ (unsigned int)
~(1<<ACL_WHERE_RCPT), /* caseful_local_part */
+
+ (unsigned int)
~(1<<ACL_WHERE_RCPT), /* caselower_local_part */
- (1<<ACL_WHERE_NOTSMTP), /* enforce_sync */
- (1<<ACL_WHERE_NOTSMTP), /* no_enforce_sync */
-
+
+ (1<<ACL_WHERE_NOTSMTP)| /* enforce_sync */
+ (1<<ACL_WHERE_NOTSMTP_START),
+
+ (1<<ACL_WHERE_NOTSMTP)| /* no_enforce_sync */
+ (1<<ACL_WHERE_NOTSMTP_START),
+
+ (unsigned int)
~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)| /* freeze */
(1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
- (1<<ACL_WHERE_NOTSMTP)),
-
+ (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_MIME)),
+
+ (unsigned int)
~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)| /* queue_only */
(1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
- (1<<ACL_WHERE_NOTSMTP)),
-
+ (1<<ACL_WHERE_NOTSMTP)|(1<<ACL_WHERE_MIME)),
+
+ (unsigned int)
~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)| /* submission */
- (1<<ACL_WHERE_PREDATA)),
-
- (1<<ACL_WHERE_NOTSMTP) /* no_multiline */
+ (1<<ACL_WHERE_PREDATA)),
+
+ (unsigned int)
+ ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)| /* suppress_local_fixups */
+ (1<<ACL_WHERE_PREDATA)|
+ (1<<ACL_WHERE_NOTSMTP_START)),
+
+ #ifdef WITH_CONTENT_SCAN
+ (unsigned int)
+ ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)| /* no_mbox_unspool */
+ (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
+ (1<<ACL_WHERE_MIME)),
+ #endif
+
+ (unsigned int)
+ ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)| /* fakedefer */
+ (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
+ (1<<ACL_WHERE_MIME)),
+
+ (unsigned int)
+ ~((1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)| /* fakereject */
+ (1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|
+ (1<<ACL_WHERE_MIME)),
+
+ (1<<ACL_WHERE_NOTSMTP)| /* no_multiline */
+ (1<<ACL_WHERE_NOTSMTP_START),
+
+ (1<<ACL_WHERE_NOTSMTP)| /* no_pipelining */
+ (1<<ACL_WHERE_NOTSMTP_START),
+
+ (1<<ACL_WHERE_NOTSMTP)| /* no_delay_flush */
+ (1<<ACL_WHERE_NOTSMTP_START),
+
+ (1<<ACL_WHERE_NOTSMTP)| /* no_callout_flush */
+ (1<<ACL_WHERE_NOTSMTP_START)
};
/* Structure listing various control arguments, with their characteristics. */
} control_def;
static control_def controls_list[] = {
- { US"caseful_local_part", CONTROL_CASEFUL_LOCAL_PART, FALSE},
- { US"caselower_local_part", CONTROL_CASELOWER_LOCAL_PART, FALSE},
- { US"enforce_sync", CONTROL_ENFORCE_SYNC, FALSE},
- { US"freeze", CONTROL_FREEZE, FALSE},
- { US"no_enforce_sync", CONTROL_NO_ENFORCE_SYNC, FALSE},
- { US"no_multiline_responses", CONTROL_NO_MULTILINE, FALSE},
- { US"queue_only", CONTROL_QUEUE_ONLY, FALSE},
- { US"submission", CONTROL_SUBMISSION, TRUE}
+ { US"allow_auth_unadvertised", CONTROL_AUTH_UNADVERTISED, FALSE },
+#ifdef EXPERIMENTAL_BRIGHTMAIL
+ { US"bmi_run", CONTROL_BMI_RUN, FALSE },
+#endif
+ { US"debug", CONTROL_DEBUG, TRUE },
+#ifndef DISABLE_DKIM
+ { US"dkim_disable_verify", CONTROL_DKIM_VERIFY, FALSE },
+#endif
+ { US"caseful_local_part", CONTROL_CASEFUL_LOCAL_PART, FALSE },
+ { US"caselower_local_part", CONTROL_CASELOWER_LOCAL_PART, FALSE },
+ { US"enforce_sync", CONTROL_ENFORCE_SYNC, FALSE },
+ { US"freeze", CONTROL_FREEZE, TRUE },
+ { US"no_callout_flush", CONTROL_NO_CALLOUT_FLUSH, FALSE },
+ { US"no_delay_flush", CONTROL_NO_DELAY_FLUSH, FALSE },
+ { US"no_enforce_sync", CONTROL_NO_ENFORCE_SYNC, FALSE },
+ { US"no_multiline_responses", CONTROL_NO_MULTILINE, FALSE },
+ { US"no_pipelining", CONTROL_NO_PIPELINING, FALSE },
+ { US"queue_only", CONTROL_QUEUE_ONLY, FALSE },
+#ifdef WITH_CONTENT_SCAN
+ { US"no_mbox_unspool", CONTROL_NO_MBOX_UNSPOOL, FALSE },
+#endif
+ { US"fakedefer", CONTROL_FAKEDEFER, TRUE },
+ { US"fakereject", CONTROL_FAKEREJECT, TRUE },
+ { US"submission", CONTROL_SUBMISSION, TRUE },
+ { US"suppress_local_fixups", CONTROL_SUPPRESS_LOCAL_FIXUPS, FALSE }
};
+/* Support data structures for Client SMTP Authorization. acl_verify_csa()
+caches its result in a tree to avoid repeated DNS queries. The result is an
+integer code which is used as an index into the following tables of
+explanatory strings and verification return codes. */
+
+static tree_node *csa_cache = NULL;
+
+enum { CSA_UNKNOWN, CSA_OK, CSA_DEFER_SRV, CSA_DEFER_ADDR,
+ CSA_FAIL_EXPLICIT, CSA_FAIL_DOMAIN, CSA_FAIL_NOADDR, CSA_FAIL_MISMATCH };
+
+/* The acl_verify_csa() return code is translated into an acl_verify() return
+code using the following table. It is OK unless the client is definitely not
+authorized. This is because CSA is supposed to be optional for sending sites,
+so recipients should not be too strict about checking it - especially because
+DNS problems are quite likely to occur. It's possible to use $csa_status in
+further ACL conditions to distinguish ok, unknown, and defer if required, but
+the aim is to make the usual configuration simple. */
+
+static int csa_return_code[] = {
+ OK, OK, OK, OK,
+ FAIL, FAIL, FAIL, FAIL
+};
+
+static uschar *csa_status_string[] = {
+ US"unknown", US"ok", US"defer", US"defer",
+ US"fail", US"fail", US"fail", US"fail"
+};
+
+static uschar *csa_reason_string[] = {
+ US"unknown",
+ US"ok",
+ US"deferred (SRV lookup failed)",
+ US"deferred (target address lookup failed)",
+ US"failed (explicit authorization required)",
+ US"failed (host name not authorized)",
+ US"failed (no authorized addresses)",
+ US"failed (client address mismatch)"
+};
+
+/* Options for the ratelimit condition. Note that there are two variants of
+the per_rcpt option, depending on the ACL that is used to measure the rate.
+However any ACL must be able to look up per_rcpt rates in /noupdate mode,
+so the two variants must have the same internal representation as well as
+the same configuration string. */
+
+enum {
+ RATE_PER_WHAT, RATE_PER_CLASH, RATE_PER_ADDR, RATE_PER_BYTE, RATE_PER_CMD,
+ RATE_PER_CONN, RATE_PER_MAIL, RATE_PER_RCPT, RATE_PER_ALLRCPTS
+};
+
+#define RATE_SET(var,new) \
+ (((var) == RATE_PER_WHAT) ? ((var) = RATE_##new) : ((var) = RATE_PER_CLASH))
+
+static uschar *ratelimit_option_string[] = {
+ US"?", US"!", US"per_addr", US"per_byte", US"per_cmd",
+ US"per_conn", US"per_mail", US"per_rcpt", US"per_rcpt"
+};
+
/* Enable recursion between acl_check_internal() and acl_check_condition() */
static int acl_check_internal(int, address_item *, uschar *, int, uschar **,
s++;
}
- /* Read the name of a verb or a condition, or the start of a new ACL */
+ /* Read the name of a verb or a condition, or the start of a new ACL, which
+ can be started by a name, or by a macro definition. */
s = readconf_readname(name, sizeof(name), s);
- if (*s == ':')
- {
- if (negated || name[0] == 0)
- {
- *error = string_sprintf("malformed ACL name in \"%s\"", saveline);
- return NULL;
- }
- break;
- }
+ if (*s == ':' || (isupper(name[0]) && *s == '=')) return yield;
/* If a verb is unrecognized, it may be another condition or modifier that
continues the previous verb. */
{
if (this == NULL)
{
- *error = string_sprintf("unknown ACL verb in \"%s\"", saveline);
+ *error = string_sprintf("unknown ACL verb \"%s\" in \"%s\"", name,
+ saveline);
return NULL;
}
}
/* The "set" modifier is different in that its argument is "name=value"
rather than just a value, and we can check the validity of the name, which
- gives us a variable number to insert into the data block. */
+ gives us a variable name to insert into the data block. The original ACL
+ variable names were acl_c0 ... acl_c9 and acl_m0 ... acl_m9. This was
+ extended to 20 of each type, but after that people successfully argued for
+ arbitrary names. In the new scheme, the names must start with acl_c or acl_m.
+ After that, we allow alphanumerics and underscores, but the first character
+ after c or m must be a digit or an underscore. This retains backwards
+ compatibility. */
if (c == ACLC_SET)
{
- if (Ustrncmp(s, "acl_", 4) != 0 || (s[4] != 'c' && s[4] != 'm') ||
- !isdigit(s[5]) || (!isspace(s[6]) && s[6] != '='))
+ uschar *endptr;
+
+ if (Ustrncmp(s, "acl_c", 5) != 0 &&
+ Ustrncmp(s, "acl_m", 5) != 0)
+ {
+ *error = string_sprintf("invalid variable name after \"set\" in ACL "
+ "modifier \"set %s\" (must start \"acl_c\" or \"acl_m\")", s);
+ return NULL;
+ }
+
+ endptr = s + 5;
+ if (!isdigit(*endptr) && *endptr != '_')
{
- *error = string_sprintf("unrecognized name after \"set\" in ACL "
- "modifier \"set %s\"", s);
+ *error = string_sprintf("invalid variable name after \"set\" in ACL "
+ "modifier \"set %s\" (digit or underscore must follow acl_c or acl_m)",
+ s);
return NULL;
}
- cond->u.varnumber = s[5] - '0';
- if (s[4] == 'm') cond->u.varnumber += ACL_C_MAX;
- s += 6;
+ while (*endptr != 0 && *endptr != '=' && !isspace(*endptr))
+ {
+ if (!isalnum(*endptr) && *endptr != '_')
+ {
+ *error = string_sprintf("invalid character \"%c\" in variable name "
+ "in ACL modifier \"set %s\"", *endptr, s);
+ return NULL;
+ }
+ endptr++;
+ }
+
+ cond->u.varname = string_copyn(s + 4, endptr - s - 4);
+ s = endptr;
while (isspace(*s)) s++;
}
+/*************************************************
+* Set up added header line(s) *
+*************************************************/
+
+/* This function is called by the add_header modifier, and also from acl_warn()
+to implement the now-deprecated way of adding header lines using "message" on a
+"warn" verb. The argument is treated as a sequence of header lines which are
+added to a chain, provided there isn't an identical one already there.
+
+Argument: string of header lines
+Returns: nothing
+*/
+
+static void
+setup_header(uschar *hstring)
+{
+uschar *p, *q;
+int hlen = Ustrlen(hstring);
+
+/* An empty string does nothing; otherwise add a final newline if necessary. */
+
+if (hlen <= 0) return;
+if (hstring[hlen-1] != '\n') hstring = string_sprintf("%s\n", hstring);
+
+/* Loop for multiple header lines, taking care about continuations */
+
+for (p = q = hstring; *p != 0; )
+ {
+ uschar *s;
+ int newtype = htype_add_bot;
+ header_line **hptr = &acl_added_headers;
+
+ /* Find next header line within the string */
+
+ for (;;)
+ {
+ q = Ustrchr(q, '\n');
+ if (*(++q) != ' ' && *q != '\t') break;
+ }
+
+ /* If the line starts with a colon, interpret the instruction for where to
+ add it. This temporarily sets up a new type. */
+
+ if (*p == ':')
+ {
+ if (strncmpic(p, US":after_received:", 16) == 0)
+ {
+ newtype = htype_add_rec;
+ p += 16;
+ }
+ else if (strncmpic(p, US":at_start_rfc:", 14) == 0)
+ {
+ newtype = htype_add_rfc;
+ p += 14;
+ }
+ else if (strncmpic(p, US":at_start:", 10) == 0)
+ {
+ newtype = htype_add_top;
+ p += 10;
+ }
+ else if (strncmpic(p, US":at_end:", 8) == 0)
+ {
+ newtype = htype_add_bot;
+ p += 8;
+ }
+ while (*p == ' ' || *p == '\t') p++;
+ }
+
+ /* See if this line starts with a header name, and if not, add X-ACL-Warn:
+ to the front of it. */
+
+ for (s = p; s < q - 1; s++)
+ {
+ if (*s == ':' || !isgraph(*s)) break;
+ }
+
+ s = string_sprintf("%s%.*s", (*s == ':')? "" : "X-ACL-Warn: ", (int) (q - p), p);
+ hlen = Ustrlen(s);
+
+ /* See if this line has already been added */
+
+ while (*hptr != NULL)
+ {
+ if (Ustrncmp((*hptr)->text, s, hlen) == 0) break;
+ hptr = &((*hptr)->next);
+ }
+
+ /* Add if not previously present */
+
+ if (*hptr == NULL)
+ {
+ header_line *h = store_get(sizeof(header_line));
+ h->text = s;
+ h->next = NULL;
+ h->type = newtype;
+ h->slen = hlen;
+ *hptr = h;
+ hptr = &(h->next);
+ }
+
+ /* Advance for next header line within the string */
+
+ p = q;
+ }
+}
+
+
+
+
/*************************************************
* Handle warnings *
*************************************************/
the message's headers, and/or writes information to the log. In each case, this
only happens once (per message for headers, per connection for log).
+** NOTE: The header adding action using the "message" setting is historic, and
+its use is now deprecated. The new add_header modifier should be used instead.
+
Arguments:
where ACL_WHERE_xxxx indicating which ACL this is
user_message message for adding to headers
static void
acl_warn(int where, uschar *user_message, uschar *log_message)
{
-int hlen;
-
if (log_message != NULL && log_message != user_message)
{
uschar *text;
strcmpic(log_message, US"sender verify failed") == 0)
text = string_sprintf("%s: %s", text, sender_verified_failed->message);
- /* Search previously logged warnings. They are kept in malloc store so they
- can be freed at the start of a new message. */
+ /* Search previously logged warnings. They are kept in malloc
+ store so they can be freed at the start of a new message. */
for (logged = acl_warn_logged; logged != NULL; logged = logged->next)
if (Ustrcmp(logged->text, text) == 0) break;
return;
}
-/* Treat the user message as a sequence of one or more header lines. */
-
-hlen = Ustrlen(user_message);
-if (hlen > 0)
- {
- uschar *text, *p, *q;
-
- /* Add a final newline if not present */
-
- text = ((user_message)[hlen-1] == '\n')? user_message :
- string_sprintf("%s\n", user_message);
-
- /* Loop for multiple header lines, taking care about continuations */
-
- for (p = q = text; *p != 0; )
- {
- uschar *s;
- int newtype = htype_add_bot;
- header_line **hptr = &acl_warn_headers;
-
- /* Find next header line within the string */
-
- for (;;)
- {
- q = Ustrchr(q, '\n');
- if (*(++q) != ' ' && *q != '\t') break;
- }
-
- /* If the line starts with a colon, interpret the instruction for where to
- add it. This temporarily sets up a new type. */
-
- if (*p == ':')
- {
- if (strncmpic(p, US":after_received:", 16) == 0)
- {
- newtype = htype_add_rec;
- p += 16;
- }
- else if (strncmpic(p, US":at_start:", 10) == 0)
- {
- newtype = htype_add_top;
- p += 10;
- }
- else if (strncmpic(p, US":at_end:", 8) == 0)
- {
- newtype = htype_add_bot;
- p += 8;
- }
- while (*p == ' ' || *p == '\t') p++;
- }
-
- /* See if this line starts with a header name, and if not, add X-ACL-Warn:
- to the front of it. */
-
- for (s = p; s < q - 1; s++)
- {
- if (*s == ':' || !isgraph(*s)) break;
- }
-
- s = string_sprintf("%s%.*s", (*s == ':')? "" : "X-ACL-Warn: ", q - p, p);
- hlen = Ustrlen(s);
-
- /* See if this line has already been added */
-
- while (*hptr != NULL)
- {
- if (Ustrncmp((*hptr)->text, s, hlen) == 0) break;
- hptr = &((*hptr)->next);
- }
-
- /* Add if not previously present */
-
- if (*hptr == NULL)
- {
- header_line *h = store_get(sizeof(header_line));
- h->text = s;
- h->next = NULL;
- h->type = newtype;
- h->slen = hlen;
- *hptr = h;
- hptr = &(h->next);
- }
+/* The code for setting up header lines is now abstracted into a separate
+function so that it can be used for the add_header modifier as well. */
- /* Advance for next header line within the string */
-
- p = q;
- }
- }
+setup_header(user_message);
}
/*************************************************
-* Handle verification (address & other) *
+* Check client IP address matches CSA target *
*************************************************/
-/* 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
-lookup for a non-TCP/IP message). Others are restricted to certain ACLs.
+/* Called from acl_verify_csa() below. This routine scans a section of a DNS
+response for address records belonging to the CSA target hostname. The section
+is specified by the reset argument, either RESET_ADDITIONAL or RESET_ANSWERS.
+If one of the addresses matches the client's IP address, then the client is
+authorized by CSA. If there are target IP addresses but none of them match
+then the client is using an unauthorized IP address. If there are no target IP
+addresses then the client cannot be using an authorized IP address. (This is
+an odd configuration - why didn't the SRV record have a weight of 1 instead?)
Arguments:
- where where called from
- addr the recipient address that the ACL is handling, or NULL
- arg the argument of "verify"
- user_msgptr pointer for user message
- log_msgptr pointer for log message
- basic_errno where to put verify errno
-
-Returns: OK verification condition succeeded
- FAIL verification failed
- DEFER there was a problem verifying
- ERROR syntax error
+ dnsa the DNS answer block
+ dnss a DNS scan block for us to use
+ reset option specifing what portion to scan, as described above
+ target the target hostname to use for matching RR names
+
+Returns: CSA_OK successfully authorized
+ CSA_FAIL_MISMATCH addresses found but none matched
+ CSA_FAIL_NOADDR no target addresses found
*/
static int
-acl_verify(int where, address_item *addr, uschar *arg,
- uschar **user_msgptr, uschar **log_msgptr, int *basic_errno)
+acl_verify_csa_address(dns_answer *dnsa, dns_scan *dnss, int reset,
+ uschar *target)
{
-int sep = '/';
-int callout = -1;
-int callout_overall = -1;
-int verify_options = 0;
-int rc;
-BOOL verify_header_sender = FALSE;
-BOOL defer_ok = FALSE;
-BOOL callout_defer_ok = FALSE;
-BOOL no_details = FALSE;
-address_item *sender_vaddr = NULL;
-uschar *verify_sender_address = NULL;
-uschar *pm_mailfrom = NULL;
-uschar *se_mailfrom = NULL;
-uschar *list = arg;
-uschar *ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size);
-
-if (ss == NULL) goto BAD_VERIFY;
+dns_record *rr;
+dns_address *da;
-/* Handle name/address consistency verification in a separate function. */
+BOOL target_found = FALSE;
-if (strcmpic(ss, US"reverse_host_lookup") == 0)
+for (rr = dns_next_rr(dnsa, dnss, reset);
+ rr != NULL;
+ rr = dns_next_rr(dnsa, dnss, RESET_NEXT))
{
- if (sender_host_address == NULL) return OK;
- return acl_verify_reverse(user_msgptr, log_msgptr);
- }
+ /* Check this is an address RR for the target hostname. */
-/* TLS certificate verification is done at STARTTLS time; here we just
-test whether it was successful or not. (This is for optional verification; for
-mandatory verification, the connection doesn't last this long.) */
-
-if (strcmpic(ss, US"certificate") == 0)
- {
- if (tls_certificate_verified) return OK;
- *user_msgptr = US"no verified certificate";
- return FAIL;
- }
+ if (rr->type != T_A
+ #if HAVE_IPV6
+ && rr->type != T_AAAA
+ #ifdef SUPPORT_A6
+ && rr->type != T_A6
+ #endif
+ #endif
+ ) continue;
-/* We can test the result of optional HELO verification */
+ if (strcmpic(target, rr->name) != 0) continue;
-if (strcmpic(ss, US"helo") == 0) return helo_verified? OK : FAIL;
+ target_found = TRUE;
-/* Handle header verification options - permitted only after DATA or a non-SMTP
-message. */
+ /* Turn the target address RR into a list of textual IP addresses and scan
+ the list. There may be more than one if it is an A6 RR. */
-if (strncmpic(ss, US"header_", 7) == 0)
- {
- if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP)
+ for (da = dns_address_from_rr(dnsa, rr); da != NULL; da = da->next)
{
- *log_msgptr = string_sprintf("cannot check header contents in ACL for %s "
- "(only possible in ACL for DATA)", acl_wherenames[where]);
- return ERROR;
- }
+ /* If the client IP address matches the target IP address, it's good! */
- /* Check that all relevant header lines have the correct syntax. If there is
- a syntax error, we return details of the error to the sender if configured to
- send out full details. (But a "message" setting on the ACL can override, as
- always). */
+ DEBUG(D_acl) debug_printf("CSA target address is %s\n", da->address);
- if (strcmpic(ss+7, US"syntax") == 0)
- {
- int rc = verify_check_headers(log_msgptr);
- if (rc != OK && smtp_return_error_details && *log_msgptr != NULL)
- *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr);
- return rc;
+ if (strcmpic(sender_host_address, da->address) == 0) return CSA_OK;
}
+ }
- /* Check that there is at least one verifiable sender address in the relevant
- header lines. This can be followed by callout and defer options, just like
- sender and recipient. */
+/* If we found some target addresses but none of them matched, the client is
+using an unauthorized IP address, otherwise the target has no authorized IP
+addresses. */
- else if (strcmpic(ss+7, US"sender") == 0) verify_header_sender = TRUE;
+if (target_found) return CSA_FAIL_MISMATCH;
+else return CSA_FAIL_NOADDR;
+}
- /* Unknown verify argument starting with "header_" */
- else goto BAD_VERIFY;
- }
-/* Otherwise, first item in verify argument must be "sender" or "recipient".
-In the case of a sender, this can optionally be followed by an address to use
-in place of the actual sender (rare special-case requirement). */
+/*************************************************
+* Verify Client SMTP Authorization *
+*************************************************/
-else if (strncmpic(ss, US"sender", 6) == 0)
- {
- uschar *s = ss + 6;
- if (where > ACL_WHERE_NOTSMTP)
- {
- *log_msgptr = string_sprintf("cannot verify sender in ACL for %s "
- "(only possible for MAIL, RCPT, PREDATA, or DATA)",
- acl_wherenames[where]);
- return ERROR;
- }
- if (*s == 0)
- verify_sender_address = sender_address;
- else
+/* Called from acl_verify() below. This routine calls dns_lookup_special()
+to find the CSA SRV record corresponding to the domain argument, or
+$sender_helo_name if no argument is provided. It then checks that the
+client is authorized, and that its IP address corresponds to the SRV
+target's address by calling acl_verify_csa_address() above. The address
+should have been returned in the DNS response's ADDITIONAL section, but if
+not we perform another DNS lookup to get it.
+
+Arguments:
+ domain pointer to optional parameter following verify = csa
+
+Returns: CSA_UNKNOWN no valid CSA record found
+ CSA_OK successfully authorized
+ CSA_FAIL_* client is definitely not authorized
+ CSA_DEFER_* there was a DNS problem
+*/
+
+static int
+acl_verify_csa(uschar *domain)
+{
+tree_node *t;
+uschar *found, *p;
+int priority, weight, port;
+dns_answer dnsa;
+dns_scan dnss;
+dns_record *rr;
+int rc, type;
+uschar target[256];
+
+/* Work out the domain we are using for the CSA lookup. The default is the
+client's HELO domain. If the client has not said HELO, use its IP address
+instead. If it's a local client (exim -bs), CSA isn't applicable. */
+
+while (isspace(*domain) && *domain != '\0') ++domain;
+if (*domain == '\0') domain = sender_helo_name;
+if (domain == NULL) domain = sender_host_address;
+if (sender_host_address == NULL) return CSA_UNKNOWN;
+
+/* If we have an address literal, strip off the framing ready for turning it
+into a domain. The framing consists of matched square brackets possibly
+containing a keyword and a colon before the actual IP address. */
+
+if (domain[0] == '[')
+ {
+ uschar *start = Ustrchr(domain, ':');
+ if (start == NULL) start = domain;
+ domain = string_copyn(start + 1, Ustrlen(start) - 2);
+ }
+
+/* Turn domains that look like bare IP addresses into domains in the reverse
+DNS. This code also deals with address literals and $sender_host_address. It's
+not quite kosher to treat bare domains such as EHLO 192.0.2.57 the same as
+address literals, but it's probably the most friendly thing to do. This is an
+extension to CSA, so we allow it to be turned off for proper conformance. */
+
+if (string_is_ip_address(domain, NULL) != 0)
+ {
+ if (!dns_csa_use_reverse) return CSA_UNKNOWN;
+ dns_build_reverse(domain, target);
+ domain = target;
+ }
+
+/* Find out if we've already done the CSA check for this domain. If we have,
+return the same result again. Otherwise build a new cached result structure
+for this domain. The name is filled in now, and the value is filled in when
+we return from this function. */
+
+t = tree_search(csa_cache, domain);
+if (t != NULL) return t->data.val;
+
+t = store_get_perm(sizeof(tree_node) + Ustrlen(domain));
+Ustrcpy(t->name, domain);
+(void)tree_insertnode(&csa_cache, t);
+
+/* Now we are ready to do the actual DNS lookup(s). */
+
+found = domain;
+switch (dns_special_lookup(&dnsa, domain, T_CSA, &found))
+ {
+ /* If something bad happened (most commonly DNS_AGAIN), defer. */
+
+ default:
+ return t->data.val = CSA_DEFER_SRV;
+
+ /* If we found nothing, the client's authorization is unknown. */
+
+ case DNS_NOMATCH:
+ case DNS_NODATA:
+ return t->data.val = CSA_UNKNOWN;
+
+ /* We got something! Go on to look at the reply in more detail. */
+
+ case DNS_SUCCEED:
+ break;
+ }
+
+/* Scan the reply for well-formed CSA SRV records. */
+
+for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS);
+ rr != NULL;
+ rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT))
+ {
+ if (rr->type != T_SRV) continue;
+
+ /* Extract the numerical SRV fields (p is incremented) */
+
+ p = rr->data;
+ GETSHORT(priority, p);
+ GETSHORT(weight, p);
+ GETSHORT(port, p);
+
+ DEBUG(D_acl)
+ debug_printf("CSA priority=%d weight=%d port=%d\n", priority, weight, port);
+
+ /* Check the CSA version number */
+
+ if (priority != 1) continue;
+
+ /* If the domain does not have a CSA SRV record of its own (i.e. the domain
+ found by dns_special_lookup() is a parent of the one we asked for), we check
+ the subdomain assertions in the port field. At the moment there's only one
+ assertion: legitimate SMTP clients are all explicitly authorized with CSA
+ SRV records of their own. */
+
+ if (found != domain)
{
- while (isspace(*s)) s++;
- if (*s++ != '=') goto BAD_VERIFY;
- while (isspace(*s)) s++;
- verify_sender_address = string_copy(s);
+ if (port & 1)
+ return t->data.val = CSA_FAIL_EXPLICIT;
+ else
+ return t->data.val = CSA_UNKNOWN;
}
+
+ /* This CSA SRV record refers directly to our domain, so we check the value
+ in the weight field to work out the domain's authorization. 0 and 1 are
+ unauthorized; 3 means the client is authorized but we can't check the IP
+ address in order to authenticate it, so we treat it as unknown; values
+ greater than 3 are undefined. */
+
+ if (weight < 2) return t->data.val = CSA_FAIL_DOMAIN;
+
+ if (weight > 2) continue;
+
+ /* Weight == 2, which means the domain is authorized. We must check that the
+ client's IP address is listed as one of the SRV target addresses. Save the
+ target hostname then break to scan the additional data for its addresses. */
+
+ (void)dn_expand(dnsa.answer, dnsa.answer + dnsa.answerlen, p,
+ (DN_EXPAND_ARG4_TYPE)target, sizeof(target));
+
+ DEBUG(D_acl) debug_printf("CSA target is %s\n", target);
+
+ break;
}
+
+/* If we didn't break the loop then no appropriate records were found. */
+
+if (rr == NULL) return t->data.val = CSA_UNKNOWN;
+
+/* Do not check addresses if the target is ".", in accordance with RFC 2782.
+A target of "." indicates there are no valid addresses, so the client cannot
+be authorized. (This is an odd configuration because weight=2 target=. is
+equivalent to weight=1, but we check for it in order to keep load off the
+root name servers.) Note that dn_expand() turns "." into "". */
+
+if (Ustrcmp(target, "") == 0) return t->data.val = CSA_FAIL_NOADDR;
+
+/* Scan the additional section of the CSA SRV reply for addresses belonging
+to the target. If the name server didn't return any additional data (e.g.
+because it does not fully support SRV records), we need to do another lookup
+to obtain the target addresses; otherwise we have a definitive result. */
+
+rc = acl_verify_csa_address(&dnsa, &dnss, RESET_ADDITIONAL, target);
+if (rc != CSA_FAIL_NOADDR) return t->data.val = rc;
+
+/* The DNS lookup type corresponds to the IP version used by the client. */
+
+#if HAVE_IPV6
+if (Ustrchr(sender_host_address, ':') != NULL)
+ type = T_AAAA;
else
+#endif /* HAVE_IPV6 */
+ type = T_A;
+
+
+#if HAVE_IPV6 && defined(SUPPORT_A6)
+DNS_LOOKUP_AGAIN:
+#endif
+
+switch (dns_lookup(&dnsa, target, type, NULL))
+ {
+ /* If something bad happened (most commonly DNS_AGAIN), defer. */
+
+ default:
+ return t->data.val = CSA_DEFER_ADDR;
+
+ /* If the query succeeded, scan the addresses and return the result. */
+
+ case DNS_SUCCEED:
+ rc = acl_verify_csa_address(&dnsa, &dnss, RESET_ANSWERS, target);
+ if (rc != CSA_FAIL_NOADDR) return t->data.val = rc;
+ /* else fall through */
+
+ /* If the target has no IP addresses, the client cannot have an authorized
+ IP address. However, if the target site uses A6 records (not AAAA records)
+ we have to do yet another lookup in order to check them. */
+
+ case DNS_NOMATCH:
+ case DNS_NODATA:
+
+ #if HAVE_IPV6 && defined(SUPPORT_A6)
+ if (type == T_AAAA) { type = T_A6; goto DNS_LOOKUP_AGAIN; }
+ #endif
+
+ return t->data.val = CSA_FAIL_NOADDR;
+ }
+}
+
+
+
+/*************************************************
+* Handle verification (address & other) *
+*************************************************/
+
+enum { VERIFY_REV_HOST_LKUP, VERIFY_CERT, VERIFY_HELO, VERIFY_CSA, VERIFY_HDR_SYNTAX,
+ VERIFY_NOT_BLIND, VERIFY_HDR_SNDR, VERIFY_SNDR, VERIFY_RCPT
+ };
+typedef struct {
+ uschar * name;
+ int value;
+ unsigned where_allowed; /* bitmap */
+ BOOL no_options; /* Never has /option(s) following */
+ unsigned alt_opt_sep; /* >0 Non-/ option separator (custom parser) */
+ } verify_type_t;
+static verify_type_t verify_type_list[] = {
+ { US"reverse_host_lookup", VERIFY_REV_HOST_LKUP, ~0, TRUE, 0 },
+ { US"certificate", VERIFY_CERT, ~0, TRUE, 0 },
+ { US"helo", VERIFY_HELO, ~0, TRUE, 0 },
+ { US"csa", VERIFY_CSA, ~0, FALSE, 0 },
+ { US"header_syntax", VERIFY_HDR_SYNTAX, (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP), TRUE, 0 },
+ { US"not_blind", VERIFY_NOT_BLIND, (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP), TRUE, 0 },
+ { US"header_sender", VERIFY_HDR_SNDR, (1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP), FALSE, 0 },
+ { US"sender", VERIFY_SNDR, (1<<ACL_WHERE_MAIL)|(1<<ACL_WHERE_RCPT)
+ |(1<<ACL_WHERE_PREDATA)|(1<<ACL_WHERE_DATA)|(1<<ACL_WHERE_NOTSMTP),
+ FALSE, 6 },
+ { US"recipient", VERIFY_RCPT, (1<<ACL_WHERE_RCPT), FALSE, 0 }
+ };
+
+
+enum { CALLOUT_DEFER_OK, CALLOUT_NOCACHE, CALLOUT_RANDOM, CALLOUT_USE_SENDER,
+ CALLOUT_USE_POSTMASTER, CALLOUT_POSTMASTER, CALLOUT_FULLPOSTMASTER,
+ CALLOUT_MAILFROM, CALLOUT_POSTMASTER_MAILFROM, CALLOUT_MAXWAIT, CALLOUT_CONNECT,
+ CALLOUT_TIME
+ };
+typedef struct {
+ uschar * name;
+ int value;
+ int flag;
+ BOOL has_option; /* Has =option(s) following */
+ BOOL timeval; /* Has a time value */
+ } callout_opt_t;
+static callout_opt_t callout_opt_list[] = {
+ { US"defer_ok", CALLOUT_DEFER_OK, 0, FALSE, FALSE },
+ { US"no_cache", CALLOUT_NOCACHE, vopt_callout_no_cache, FALSE, FALSE },
+ { US"random", CALLOUT_RANDOM, vopt_callout_random, FALSE, FALSE },
+ { US"use_sender", CALLOUT_USE_SENDER, vopt_callout_recipsender, FALSE, FALSE },
+ { US"use_postmaster", CALLOUT_USE_POSTMASTER,vopt_callout_recippmaster, FALSE, FALSE },
+ { US"postmaster_mailfrom",CALLOUT_POSTMASTER_MAILFROM,0, TRUE, FALSE },
+ { US"postmaster", CALLOUT_POSTMASTER, 0, FALSE, FALSE },
+ { US"fullpostmaster", CALLOUT_FULLPOSTMASTER,vopt_callout_fullpm, FALSE, FALSE },
+ { US"mailfrom", CALLOUT_MAILFROM, 0, TRUE, FALSE },
+ { US"maxwait", CALLOUT_MAXWAIT, 0, TRUE, TRUE },
+ { US"connect", CALLOUT_CONNECT, 0, TRUE, TRUE },
+ { NULL, CALLOUT_TIME, 0, FALSE, TRUE }
+ };
+
+
+
+/* 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
+lookup for a non-TCP/IP message). Others are restricted to certain ACLs.
+
+Arguments:
+ where where called from
+ addr the recipient address that the ACL is handling, or NULL
+ arg the argument of "verify"
+ user_msgptr pointer for user message
+ log_msgptr pointer for log message
+ basic_errno where to put verify errno
+
+Returns: OK verification condition succeeded
+ FAIL verification failed
+ DEFER there was a problem verifying
+ ERROR syntax error
+*/
+
+static int
+acl_verify(int where, address_item *addr, uschar *arg,
+ uschar **user_msgptr, uschar **log_msgptr, int *basic_errno)
+{
+int sep = '/';
+int callout = -1;
+int callout_overall = -1;
+int callout_connect = -1;
+int verify_options = 0;
+int rc;
+BOOL verify_header_sender = FALSE;
+BOOL defer_ok = FALSE;
+BOOL callout_defer_ok = FALSE;
+BOOL no_details = FALSE;
+BOOL success_on_redirect = FALSE;
+address_item *sender_vaddr = NULL;
+uschar *verify_sender_address = NULL;
+uschar *pm_mailfrom = NULL;
+uschar *se_mailfrom = NULL;
+
+/* Some of the verify items have slash-separated options; some do not. Diagnose
+an error if options are given for items that don't expect them.
+*/
+
+uschar *slash = Ustrchr(arg, '/');
+uschar *list = arg;
+uschar *ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size);
+verify_type_t * vp;
+
+if (ss == NULL) goto BAD_VERIFY;
+
+/* Handle name/address consistency verification in a separate function. */
+
+for (vp= verify_type_list;
+ (char *)vp < (char *)verify_type_list + sizeof(verify_type_list);
+ vp++
+ )
+ if (vp->alt_opt_sep ? strncmpic(ss, vp->name, vp->alt_opt_sep) == 0
+ : strcmpic (ss, vp->name) == 0)
+ break;
+if ((char *)vp >= (char *)verify_type_list + sizeof(verify_type_list))
+ goto BAD_VERIFY;
+
+if (vp->no_options && slash != NULL)
+ {
+ *log_msgptr = string_sprintf("unexpected '/' found in \"%s\" "
+ "(this verify item has no options)", arg);
+ return ERROR;
+ }
+if (!(vp->where_allowed & (1<<where)))
{
- if (strcmpic(ss, US"recipient") != 0) goto BAD_VERIFY;
- if (addr == NULL)
+ *log_msgptr = string_sprintf("cannot verify %s in ACL for %s", vp->name, acl_wherenames[where]);
+ return ERROR;
+ }
+switch(vp->value)
+ {
+ case VERIFY_REV_HOST_LKUP:
+ if (sender_host_address == NULL) return OK;
+ return acl_verify_reverse(user_msgptr, log_msgptr);
+
+ case VERIFY_CERT:
+ /* TLS certificate verification is done at STARTTLS time; here we just
+ test whether it was successful or not. (This is for optional verification; for
+ mandatory verification, the connection doesn't last this long.) */
+
+ if (tls_certificate_verified) return OK;
+ *user_msgptr = US"no verified certificate";
+ return FAIL;
+
+ case VERIFY_HELO:
+ /* We can test the result of optional HELO verification that might have
+ occurred earlier. If not, we can attempt the verification now. */
+
+ if (!helo_verified && !helo_verify_failed) smtp_verify_helo();
+ return helo_verified? OK : FAIL;
+
+ case VERIFY_CSA:
+ /* Do Client SMTP Authorization checks in a separate function, and turn the
+ result code into user-friendly strings. */
+
+ rc = acl_verify_csa(list);
+ *log_msgptr = *user_msgptr = string_sprintf("client SMTP authorization %s",
+ csa_reason_string[rc]);
+ csa_status = csa_status_string[rc];
+ DEBUG(D_acl) debug_printf("CSA result %s\n", csa_status);
+ return csa_return_code[rc];
+
+ case VERIFY_HDR_SYNTAX:
+ /* Check that all relevant header lines have the correct syntax. If there is
+ a syntax error, we return details of the error to the sender if configured to
+ send out full details. (But a "message" setting on the ACL can override, as
+ always). */
+
+ rc = verify_check_headers(log_msgptr);
+ if (rc != OK && smtp_return_error_details && *log_msgptr != NULL)
+ *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr);
+ return rc;
+
+ case VERIFY_NOT_BLIND:
+ /* Check that no recipient of this message is "blind", that is, every envelope
+ recipient must be mentioned in either To: or Cc:. */
+
+ rc = verify_check_notblind();
+ if (rc != OK)
+ {
+ *log_msgptr = string_sprintf("bcc recipient detected");
+ if (smtp_return_error_details)
+ *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr);
+ }
+ return rc;
+
+ /* The remaining verification tests check recipient and sender addresses,
+ either from the envelope or from the header. There are a number of
+ slash-separated options that are common to all of them. */
+
+ case VERIFY_HDR_SNDR:
+ verify_header_sender = TRUE;
+ break;
+
+ case VERIFY_SNDR:
+ /* In the case of a sender, this can optionally be followed by an address to use
+ in place of the actual sender (rare special-case requirement). */
{
- *log_msgptr = string_sprintf("cannot verify recipient in ACL for %s "
- "(only possible for RCPT)", acl_wherenames[where]);
- return ERROR;
+ uschar *s = ss + 6;
+ if (*s == 0)
+ verify_sender_address = sender_address;
+ else
+ {
+ while (isspace(*s)) s++;
+ if (*s++ != '=') goto BAD_VERIFY;
+ while (isspace(*s)) s++;
+ verify_sender_address = string_copy(s);
+ }
}
+ break;
+
+ case VERIFY_RCPT:
+ break;
}
-/* Remaining items are optional */
+
+
+/* Remaining items are optional; they apply to sender and recipient
+verification, including "header sender" verification. */
while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size))
!= NULL)
{
if (strcmpic(ss, US"defer_ok") == 0) defer_ok = TRUE;
else if (strcmpic(ss, US"no_details") == 0) no_details = TRUE;
+ else if (strcmpic(ss, US"success_on_redirect") == 0) success_on_redirect = TRUE;
/* These two old options are left for backwards compatibility */
uschar *opt;
uschar buffer[256];
while (isspace(*ss)) ss++;
+
while ((opt = string_nextinlist(&ss, &optsep, buffer, sizeof(buffer)))
!= NULL)
{
- if (strcmpic(opt, US"defer_ok") == 0) callout_defer_ok = TRUE;
- else if (strcmpic(opt, US"no_cache") == 0)
- verify_options |= vopt_callout_no_cache;
- else if (strcmpic(opt, US"random") == 0)
- verify_options |= vopt_callout_random;
- else if (strcmpic(opt, US"use_sender") == 0)
- verify_options |= vopt_callout_recipsender;
- else if (strcmpic(opt, US"use_postmaster") == 0)
- verify_options |= vopt_callout_recippmaster;
- else if (strcmpic(opt, US"postmaster") == 0) pm_mailfrom = US"";
-
- else if (strncmpic(opt, US"mailfrom", 8) == 0)
- {
- if (!verify_header_sender)
- {
- *log_msgptr = string_sprintf("\"mailfrom\" is allowed as a "
- "callout option only for verify=header_sender (detected in ACL "
- "condition \"%s\")", arg);
- return ERROR;
- }
- opt += 8;
- while (isspace(*opt)) opt++;
- if (*opt++ != '=')
- {
- *log_msgptr = string_sprintf("'=' expected after "
- "\"mailfrom\" in ACL condition \"%s\"", arg);
- return ERROR;
- }
- while (isspace(*opt)) opt++;
- se_mailfrom = string_copy(opt);
- }
+ callout_opt_t * op;
+ double period = 1.0F;
- else if (strncmpic(opt, US"postmaster_mailfrom", 19) == 0)
- {
- opt += 19;
- while (isspace(*opt)) opt++;
- if (*opt++ != '=')
- {
- *log_msgptr = string_sprintf("'=' expected after "
- "\"postmaster_mailfrom\" in ACL condition \"%s\"", arg);
- return ERROR;
- }
- while (isspace(*opt)) opt++;
- pm_mailfrom = string_copy(opt);
- }
+ for (op= callout_opt_list; op->name; op++)
+ if (strncmpic(opt, op->name, Ustrlen(op->name)) == 0)
+ break;
- else if (strncmpic(opt, US"maxwait", 7) == 0)
- {
- opt += 7;
+ verify_options |= op->flag;
+ if (op->has_option)
+ {
+ opt += Ustrlen(op->name);
while (isspace(*opt)) opt++;
if (*opt++ != '=')
{
- *log_msgptr = string_sprintf("'=' expected after \"maxwait\" in "
- "ACL condition \"%s\"", arg);
+ *log_msgptr = string_sprintf("'=' expected after "
+ "\"%s\" in ACL verify condition \"%s\"", op->name, arg);
return ERROR;
}
while (isspace(*opt)) opt++;
- callout_overall = readconf_readtime(opt, 0, FALSE);
- if (callout_overall < 0)
- {
- *log_msgptr = string_sprintf("bad time value in ACL condition "
- "\"verify %s\"", arg);
- return ERROR;
- }
- }
- else /* Plain time is callout connect/command timeout */
- {
- callout = readconf_readtime(opt, 0, FALSE);
- if (callout < 0)
+ }
+ if (op->timeval)
+ {
+ period = readconf_readtime(opt, 0, FALSE);
+ if (period < 0)
{
*log_msgptr = string_sprintf("bad time value in ACL condition "
"\"verify %s\"", arg);
return ERROR;
}
- }
+ }
+
+ switch(op->value)
+ {
+ case CALLOUT_DEFER_OK: callout_defer_ok = TRUE; break;
+ case CALLOUT_POSTMASTER: pm_mailfrom = US""; break;
+ case CALLOUT_FULLPOSTMASTER: pm_mailfrom = US""; break;
+ case CALLOUT_MAILFROM:
+ if (!verify_header_sender)
+ {
+ *log_msgptr = string_sprintf("\"mailfrom\" is allowed as a "
+ "callout option only for verify=header_sender (detected in ACL "
+ "condition \"%s\")", arg);
+ return ERROR;
+ }
+ se_mailfrom = string_copy(opt);
+ break;
+ case CALLOUT_POSTMASTER_MAILFROM: pm_mailfrom = string_copy(opt); break;
+ case CALLOUT_MAXWAIT: callout_overall = period; break;
+ case CALLOUT_CONNECT: callout_connect = period; break;
+ case CALLOUT_TIME: callout = period; break;
+ }
}
}
else
if (verify_header_sender)
{
+ int verrno;
rc = verify_check_header_address(user_msgptr, log_msgptr, callout,
- callout_overall, se_mailfrom, pm_mailfrom, verify_options);
- if (smtp_return_error_details)
+ callout_overall, callout_connect, se_mailfrom, pm_mailfrom, verify_options,
+ &verrno);
+ if (rc != OK)
{
- if (*user_msgptr == NULL && *log_msgptr != NULL)
- *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr);
- if (rc == DEFER) acl_temp_details = TRUE;
+ *basic_errno = verrno;
+ if (smtp_return_error_details)
+ {
+ if (*user_msgptr == NULL && *log_msgptr != NULL)
+ *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr);
+ if (rc == DEFER) acl_temp_details = TRUE;
+ }
}
}
else
{
BOOL routed = TRUE;
+ uschar *save_address_data = deliver_address_data;
+
sender_vaddr = deliver_make_addr(verify_sender_address, TRUE);
if (no_details) setflag(sender_vaddr, af_sverify_told);
if (verify_sender_address[0] != 0)
else
verify_options |= vopt_fake_sender;
+ if (success_on_redirect)
+ verify_options |= vopt_success_on_redirect;
+
/* The recipient, qualify, and expn options are never set in
verify_options. */
rc = verify_address(sender_vaddr, NULL, verify_options, callout,
- callout_overall, se_mailfrom, pm_mailfrom, &routed);
+ callout_overall, callout_connect, se_mailfrom, pm_mailfrom, &routed);
HDEBUG(D_acl) debug_printf("----------- end verify ------------\n");
sender_vaddr->special_action = rc;
sender_vaddr->next = sender_verified_list;
sender_verified_list = sender_vaddr;
+
+ /* Restore the recipient address data, which might have been clobbered by
+ the sender verification. */
+
+ deliver_address_data = save_address_data;
}
+
+ /* Put the sender address_data value into $sender_address_data */
+
+ sender_address_data = sender_vaddr->p.address_data;
}
/* A recipient address just gets a straightforward verify; again we must handle
{
address_item addr2;
+ if (success_on_redirect)
+ verify_options |= vopt_success_on_redirect;
+
/* We must use a copy of the address for verification, because it might
get rewritten. */
addr2 = *addr;
rc = verify_address(&addr2, NULL, verify_options|vopt_is_recipient, callout,
- callout_overall, se_mailfrom, pm_mailfrom, NULL);
+ callout_overall, callout_connect, se_mailfrom, pm_mailfrom, NULL);
HDEBUG(D_acl) debug_printf("----------- end verify ------------\n");
- *log_msgptr = addr2.message;
- *user_msgptr = addr2.user_message;
+
*basic_errno = addr2.basic_errno;
+ *log_msgptr = addr2.message;
+ *user_msgptr = (addr2.user_message != NULL)?
+ addr2.user_message : addr2.message;
+
+ /* Allow details for temporary error if the address is so flagged. */
+ if (testflag((&addr2), af_pass_message)) acl_temp_details = TRUE;
+
+ /* Make $address_data visible */
+ deliver_address_data = addr2.p.address_data;
+ }
+
+/* We have a result from the relevant test. Handle defer overrides first. */
+
+if (rc == DEFER && (defer_ok ||
+ (callout_defer_ok && *basic_errno == ERRNO_CALLOUTDEFER)))
+ {
+ HDEBUG(D_acl) debug_printf("verify defer overridden by %s\n",
+ defer_ok? "defer_ok" : "callout_defer_ok");
+ rc = OK;
+ }
+
+/* If we've failed a sender, set up a recipient message, and point
+sender_verified_failed to the address item that actually failed. */
+
+if (rc != OK && verify_sender_address != NULL)
+ {
+ if (rc != DEFER)
+ {
+ *log_msgptr = *user_msgptr = US"Sender verify failed";
+ }
+ else if (*basic_errno != ERRNO_CALLOUTDEFER)
+ {
+ *log_msgptr = *user_msgptr = US"Could not complete sender verify";
+ }
+ else
+ {
+ *log_msgptr = US"Could not complete sender verify callout";
+ *user_msgptr = smtp_return_error_details? sender_vaddr->user_message :
+ *log_msgptr;
+ }
+
+ sender_verified_failed = sender_vaddr;
+ }
+
+/* Verifying an address messes up the values of $domain and $local_part,
+so reset them before returning if this is a RCPT ACL. */
+
+if (addr != NULL)
+ {
+ deliver_domain = addr->domain;
+ deliver_localpart = addr->local_part;
+ }
+return rc;
+
+/* Syntax errors in the verify argument come here. */
+
+BAD_VERIFY:
+*log_msgptr = string_sprintf("expected \"sender[=address]\", \"recipient\", "
+ "\"helo\", \"header_syntax\", \"header_sender\" or "
+ "\"reverse_host_lookup\" at start of ACL condition "
+ "\"verify %s\"", arg);
+return ERROR;
+}
+
+
+
+
+/*************************************************
+* Check argument for control= modifier *
+*************************************************/
+
+/* Called from acl_check_condition() below
+
+Arguments:
+ arg the argument string for control=
+ pptr set to point to the terminating character
+ where which ACL we are in
+ log_msgptr for error messages
+
+Returns: CONTROL_xxx value
+*/
+
+static int
+decode_control(uschar *arg, uschar **pptr, int where, uschar **log_msgptr)
+{
+int len;
+control_def *d;
+
+for (d = controls_list;
+ d < controls_list + sizeof(controls_list)/sizeof(control_def);
+ d++)
+ {
+ len = Ustrlen(d->name);
+ if (Ustrncmp(d->name, arg, len) == 0) break;
+ }
+
+if (d >= controls_list + sizeof(controls_list)/sizeof(control_def) ||
+ (arg[len] != 0 && (!d->has_option || arg[len] != '/')))
+ {
+ *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
+ return CONTROL_ERROR;
+ }
+
+*pptr = arg + len;
+return d->value;
+}
+
+
+
+
+/*************************************************
+* Return a ratelimit error *
+*************************************************/
+
+/* Called from acl_ratelimit() below
+
+Arguments:
+ log_msgptr for error messages
+ format format string
+ ... supplementary arguments
+ ss ratelimit option name
+ where ACL_WHERE_xxxx indicating which ACL this is
+
+Returns: ERROR
+*/
+
+static int
+ratelimit_error(uschar **log_msgptr, const char *format, ...)
+{
+va_list ap;
+uschar buffer[STRING_SPRINTF_BUFFER_SIZE];
+va_start(ap, format);
+if (!string_vformat(buffer, sizeof(buffer), format, ap))
+ log_write(0, LOG_MAIN|LOG_PANIC_DIE,
+ "string_sprintf expansion was longer than " SIZE_T_FMT, sizeof(buffer));
+va_end(ap);
+*log_msgptr = string_sprintf(
+ "error in arguments to \"ratelimit\" condition: %s", buffer);
+return ERROR;
+}
+
+
+
+
+/*************************************************
+* Handle rate limiting *
+*************************************************/
+
+/* Called by acl_check_condition() below to calculate the result
+of the ACL ratelimit condition.
+
+Note that the return value might be slightly unexpected: if the
+sender's rate is above the limit then the result is OK. This is
+similar to the dnslists condition, and is so that you can write
+ACL clauses like: defer ratelimit = 15 / 1h
+
+Arguments:
+ arg the option string for ratelimit=
+ where ACL_WHERE_xxxx indicating which ACL this is
+ log_msgptr for error messages
+
+Returns: OK - Sender's rate is above limit
+ FAIL - Sender's rate is below limit
+ DEFER - Problem opening ratelimit database
+ ERROR - Syntax error in options.
+*/
+
+static int
+acl_ratelimit(uschar *arg, int where, uschar **log_msgptr)
+{
+double limit, period, count;
+uschar *ss;
+uschar *key = NULL;
+uschar *unique = NULL;
+int sep = '/';
+BOOL leaky = FALSE, strict = FALSE, readonly = FALSE;
+BOOL noupdate = FALSE, badacl = FALSE;
+int mode = RATE_PER_WHAT;
+int old_pool, rc;
+tree_node **anchor, *t;
+open_db dbblock, *dbm;
+int dbdb_size;
+dbdata_ratelimit *dbd;
+dbdata_ratelimit_unique *dbdb;
+struct timeval tv;
+
+/* Parse the first two options and record their values in expansion
+variables. These variables allow the configuration to have informative
+error messages based on rate limits obtained from a table lookup. */
+
+/* First is the maximum number of messages per period / maximum burst
+size, which must be greater than or equal to zero. Zero is useful for
+rate measurement as opposed to rate limiting. */
+
+sender_rate_limit = string_nextinlist(&arg, &sep, NULL, 0);
+if (sender_rate_limit == NULL)
+ limit = -1.0;
+else
+ {
+ limit = Ustrtod(sender_rate_limit, &ss);
+ if (tolower(*ss) == 'k') { limit *= 1024.0; ss++; }
+ else if (tolower(*ss) == 'm') { limit *= 1024.0*1024.0; ss++; }
+ else if (tolower(*ss) == 'g') { limit *= 1024.0*1024.0*1024.0; ss++; }
+ }
+if (limit < 0.0 || *ss != '\0')
+ return ratelimit_error(log_msgptr,
+ "\"%s\" is not a positive number", sender_rate_limit);
+
+/* Second is the rate measurement period / exponential smoothing time
+constant. This must be strictly greater than zero, because zero leads to
+run-time division errors. */
+
+sender_rate_period = string_nextinlist(&arg, &sep, NULL, 0);
+if (sender_rate_period == NULL) period = -1.0;
+else period = readconf_readtime(sender_rate_period, 0, FALSE);
+if (period <= 0.0)
+ return ratelimit_error(log_msgptr,
+ "\"%s\" is not a time value", sender_rate_period);
+
+/* By default we are counting one of something, but the per_rcpt,
+per_byte, and count options can change this. */
+
+count = 1.0;
+
+/* Parse the other options. */
+
+while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size))
+ != NULL)
+ {
+ if (strcmpic(ss, US"leaky") == 0) leaky = TRUE;
+ else if (strcmpic(ss, US"strict") == 0) strict = TRUE;
+ else if (strcmpic(ss, US"noupdate") == 0) noupdate = TRUE;
+ else if (strcmpic(ss, US"readonly") == 0) readonly = TRUE;
+ else if (strcmpic(ss, US"per_cmd") == 0) RATE_SET(mode, PER_CMD);
+ else if (strcmpic(ss, US"per_conn") == 0)
+ {
+ RATE_SET(mode, PER_CONN);
+ if (where == ACL_WHERE_NOTSMTP || where == ACL_WHERE_NOTSMTP_START)
+ badacl = TRUE;
+ }
+ else if (strcmpic(ss, US"per_mail") == 0)
+ {
+ RATE_SET(mode, PER_MAIL);
+ if (where > ACL_WHERE_NOTSMTP) badacl = TRUE;
+ }
+ else if (strcmpic(ss, US"per_rcpt") == 0)
+ {
+ /* If we are running in the RCPT ACL, then we'll count the recipients
+ one by one, but if we are running when we have accumulated the whole
+ list then we'll add them all in one batch. */
+ if (where == ACL_WHERE_RCPT)
+ RATE_SET(mode, PER_RCPT);
+ else if (where >= ACL_WHERE_PREDATA && where <= ACL_WHERE_NOTSMTP)
+ RATE_SET(mode, PER_ALLRCPTS), count = (double)recipients_count;
+ else if (where == ACL_WHERE_MAIL || where > ACL_WHERE_NOTSMTP)
+ RATE_SET(mode, PER_RCPT), badacl = TRUE;
+ }
+ else if (strcmpic(ss, US"per_byte") == 0)
+ {
+ /* If we have not yet received the message data and there was no SIZE
+ declaration on the MAIL comand, then it's safe to just use a value of
+ zero and let the recorded rate decay as if nothing happened. */
+ RATE_SET(mode, PER_MAIL);
+ if (where > ACL_WHERE_NOTSMTP) badacl = TRUE;
+ else count = message_size < 0 ? 0.0 : (double)message_size;
+ }
+ else if (strcmpic(ss, US"per_addr") == 0)
+ {
+ RATE_SET(mode, PER_RCPT);
+ if (where != ACL_WHERE_RCPT) badacl = TRUE, unique = US"*";
+ else unique = string_sprintf("%s@%s", deliver_localpart, deliver_domain);
+ }
+ else if (strncmpic(ss, US"count=", 6) == 0)
+ {
+ uschar *e;
+ count = Ustrtod(ss+6, &e);
+ if (count < 0.0 || *e != '\0')
+ return ratelimit_error(log_msgptr,
+ "\"%s\" is not a positive number", ss);
+ }
+ else if (strncmpic(ss, US"unique=", 7) == 0)
+ unique = string_copy(ss + 7);
+ else if (key == NULL)
+ key = string_copy(ss);
+ else
+ key = string_sprintf("%s/%s", key, ss);
+ }
+
+/* Sanity check. When the badacl flag is set the update mode must either
+be readonly (which is the default if it is omitted) or, for backwards
+compatibility, a combination of noupdate and strict or leaky. */
+
+if (mode == RATE_PER_CLASH)
+ return ratelimit_error(log_msgptr, "conflicting per_* options");
+if (leaky + strict + readonly > 1)
+ return ratelimit_error(log_msgptr, "conflicting update modes");
+if (badacl && (leaky || strict) && !noupdate)
+ return ratelimit_error(log_msgptr,
+ "\"%s\" must not have /leaky or /strict option in %s ACL",
+ ratelimit_option_string[mode], acl_wherenames[where]);
+
+/* Set the default values of any unset options. In readonly mode we
+perform the rate computation without any increment so that its value
+decays to eventually allow over-limit senders through. */
+
+if (noupdate) readonly = TRUE, leaky = strict = FALSE;
+if (badacl) readonly = TRUE;
+if (readonly) count = 0.0;
+if (!strict && !readonly) leaky = TRUE;
+if (mode == RATE_PER_WHAT) mode = RATE_PER_MAIL;
+
+/* Create the lookup key. If there is no explicit key, use sender_host_address.
+If there is no sender_host_address (e.g. -bs or acl_not_smtp) then we simply
+omit it. The smoothing constant (sender_rate_period) and the per_xxx options
+are added to the key because they alter the meaning of the stored data. */
+
+if (key == NULL)
+ key = (sender_host_address == NULL)? US"" : sender_host_address;
+
+key = string_sprintf("%s/%s/%s%s",
+ sender_rate_period,
+ ratelimit_option_string[mode],
+ unique == NULL ? "" : "unique/",
+ key);
+
+HDEBUG(D_acl)
+ debug_printf("ratelimit condition count=%.0f %.1f/%s\n", count, limit, key);
+
+/* See if we have already computed the rate by looking in the relevant tree.
+For per-connection rate limiting, store tree nodes and dbdata in the permanent
+pool so that they survive across resets. In readonly mode we only remember the
+result for the rest of this command in case a later command changes it. After
+this bit of logic the code is independent of the per_* mode. */
+
+old_pool = store_pool;
+
+if (readonly)
+ anchor = &ratelimiters_cmd;
+else switch(mode) {
+case RATE_PER_CONN:
+ anchor = &ratelimiters_conn;
+ store_pool = POOL_PERM;
+ break;
+case RATE_PER_BYTE:
+case RATE_PER_MAIL:
+case RATE_PER_ALLRCPTS:
+ anchor = &ratelimiters_mail;
+ break;
+case RATE_PER_ADDR:
+case RATE_PER_CMD:
+case RATE_PER_RCPT:
+ anchor = &ratelimiters_cmd;
+ break;
+default:
+ anchor = NULL; /* silence an "unused" complaint */
+ log_write(0, LOG_MAIN|LOG_PANIC_DIE,
+ "internal ACL error: unknown ratelimit mode %d", mode);
+ break;
+}
+
+t = tree_search(*anchor, key);
+if (t != NULL)
+ {
+ dbd = t->data.ptr;
+ /* The following few lines duplicate some of the code below. */
+ rc = (dbd->rate < limit)? FAIL : OK;
+ store_pool = old_pool;
+ sender_rate = string_sprintf("%.1f", dbd->rate);
+ HDEBUG(D_acl)
+ debug_printf("ratelimit found pre-computed rate %s\n", sender_rate);
+ return rc;
+ }
+
+/* We aren't using a pre-computed rate, so get a previously recorded rate
+from the database, which will be updated and written back if required. */
+
+dbm = dbfn_open(US"ratelimit", O_RDWR, &dbblock, TRUE);
+if (dbm == NULL)
+ {
+ store_pool = old_pool;
+ sender_rate = NULL;
+ HDEBUG(D_acl) debug_printf("ratelimit database not available\n");
+ *log_msgptr = US"ratelimit database not available";
+ return DEFER;
+ }
+dbdb = dbfn_read_with_length(dbm, key, &dbdb_size);
+dbd = NULL;
+
+gettimeofday(&tv, NULL);
+
+if (dbdb != NULL)
+ {
+ /* Locate the basic ratelimit block inside the DB data. */
+ HDEBUG(D_acl) debug_printf("ratelimit found key in database\n");
+ dbd = &dbdb->dbd;
+
+ /* Forget the old Bloom filter if it is too old, so that we count each
+ repeating event once per period. We don't simply clear and re-use the old
+ filter because we want its size to change if the limit changes. Note that
+ we keep the dbd pointer for copying the rate into the new data block. */
+
+ if(unique != NULL && tv.tv_sec > dbdb->bloom_epoch + period)
+ {
+ HDEBUG(D_acl) debug_printf("ratelimit discarding old Bloom filter\n");
+ dbdb = NULL;
+ }
+
+ /* Sanity check. */
+
+ if(unique != NULL && dbdb_size < sizeof(*dbdb))
+ {
+ HDEBUG(D_acl) debug_printf("ratelimit discarding undersize Bloom filter\n");
+ dbdb = NULL;
+ }
+ }
+
+/* Allocate a new data block if the database lookup failed
+or the Bloom filter passed its age limit. */
+
+if (dbdb == NULL)
+ {
+ if (unique == NULL)
+ {
+ /* No Bloom filter. This basic ratelimit block is initialized below. */
+ HDEBUG(D_acl) debug_printf("ratelimit creating new rate data block\n");
+ dbdb_size = sizeof(*dbd);
+ dbdb = store_get(dbdb_size);
+ }
+ else
+ {
+ int extra;
+ HDEBUG(D_acl) debug_printf("ratelimit creating new Bloom filter\n");
- /* Make $address_data visible */
- deliver_address_data = addr2.p.address_data;
- }
+ /* See the long comment below for an explanation of the magic number 2.
+ The filter has a minimum size in case the rate limit is very small;
+ this is determined by the definition of dbdata_ratelimit_unique. */
-/* We have a result from the relevant test. Handle defer overrides first. */
+ extra = (int)limit * 2 - sizeof(dbdb->bloom);
+ if (extra < 0) extra = 0;
+ dbdb_size = sizeof(*dbdb) + extra;
+ dbdb = store_get(dbdb_size);
+ dbdb->bloom_epoch = tv.tv_sec;
+ dbdb->bloom_size = sizeof(dbdb->bloom) + extra;
+ memset(dbdb->bloom, 0, dbdb->bloom_size);
-if (rc == DEFER && (defer_ok ||
- (callout_defer_ok && *basic_errno == ERRNO_CALLOUTDEFER)))
- {
- HDEBUG(D_acl) debug_printf("verify defer overridden by %s\n",
- defer_ok? "defer_ok" : "callout_defer_ok");
- rc = OK;
+ /* Preserve any basic ratelimit data (which is our longer-term memory)
+ by copying it from the discarded block. */
+
+ if (dbd != NULL)
+ {
+ dbdb->dbd = *dbd;
+ dbd = &dbdb->dbd;
+ }
+ }
}
-/* If we've failed a sender, set up a recipient message, and point
-sender_verified_failed to the address item that actually failed. */
+/* If we are counting unique events, find out if this event is new or not.
+If the client repeats the event during the current period then it should be
+counted. We skip this code in readonly mode for efficiency, because any
+changes to the filter will be discarded and because count is already set to
+zero. */
-if (rc != OK && verify_sender_address != NULL)
+if (unique != NULL && !readonly)
{
- if (rc != DEFER)
+ /* We identify unique events using a Bloom filter. (You can find my
+ notes on Bloom filters at http://fanf.livejournal.com/81696.html)
+ With the per_addr option, an "event" is a recipient address, though the
+ user can use the unique option to define their own events. We only count
+ an event if we have not seen it before.
+
+ We size the filter according to the rate limit, which (in leaky mode)
+ is the limit on the population of the filter. We allow 16 bits of space
+ per entry (see the construction code above) and we set (up to) 8 of them
+ when inserting an element (see the loop below). The probability of a false
+ positive (an event we have not seen before but which we fail to count) is
+
+ size = limit * 16
+ numhash = 8
+ allzero = exp(-numhash * pop / size)
+ = exp(-0.5 * pop / limit)
+ fpr = pow(1 - allzero, numhash)
+
+ For senders at the limit the fpr is 0.06% or 1 in 1700
+ and for senders at half the limit it is 0.0006% or 1 in 170000
+
+ In strict mode the Bloom filter can fill up beyond the normal limit, in
+ which case the false positive rate will rise. This means that the
+ measured rate for very fast senders can bogusly drop off after a while.
+
+ At twice the limit, the fpr is 2.5% or 1 in 40
+ At four times the limit, it is 31% or 1 in 3.2
+
+ It takes ln(pop/limit) periods for an over-limit burst of pop events to
+ decay below the limit, and if this is more than one then the Bloom filter
+ will be discarded before the decay gets that far. The false positive rate
+ at this threshold is 9.3% or 1 in 10.7. */
+
+ BOOL seen;
+ unsigned n, hash, hinc;
+ uschar md5sum[16];
+ md5 md5info;
+
+ /* Instead of using eight independent hash values, we combine two values
+ using the formula h1 + n * h2. This does not harm the Bloom filter's
+ performance, and means the amount of hash we need is independent of the
+ number of bits we set in the filter. */
+
+ md5_start(&md5info);
+ md5_end(&md5info, unique, Ustrlen(unique), md5sum);
+ hash = md5sum[0] | md5sum[1] << 8 | md5sum[2] << 16 | md5sum[3] << 24;
+ hinc = md5sum[4] | md5sum[5] << 8 | md5sum[6] << 16 | md5sum[7] << 24;
+
+ /* Scan the bits corresponding to this event. A zero bit means we have
+ not seen it before. Ensure all bits are set to record this event. */
+
+ HDEBUG(D_acl) debug_printf("ratelimit checking uniqueness of %s\n", unique);
+
+ seen = TRUE;
+ for (n = 0; n < 8; n++, hash += hinc)
{
- *log_msgptr = *user_msgptr = US"Sender verify failed";
+ int bit = 1 << (hash % 8);
+ int byte = (hash / 8) % dbdb->bloom_size;
+ if ((dbdb->bloom[byte] & bit) == 0)
+ {
+ dbdb->bloom[byte] |= bit;
+ seen = FALSE;
+ }
}
- else if (*basic_errno != ERRNO_CALLOUTDEFER)
+
+ /* If this event has occurred before, do not count it. */
+
+ if (seen)
{
- *log_msgptr = *user_msgptr = US"Could not complete sender verify";
+ HDEBUG(D_acl) debug_printf("ratelimit event found in Bloom filter\n");
+ count = 0.0;
}
else
- {
- *log_msgptr = US"Could not complete sender verify callout";
- *user_msgptr = smtp_return_error_details? sender_vaddr->user_message :
- *log_msgptr;
- }
-
- sender_verified_failed = sender_vaddr;
+ HDEBUG(D_acl) debug_printf("ratelimit event added to Bloom filter\n");
}
-/* Verifying an address messes up the values of $domain and $local_part,
-so reset them before returning if this is a RCPT ACL. */
+/* If there was no previous ratelimit data block for this key, initialize
+the new one, otherwise update the block from the database. The initial rate
+is what would be computed by the code below for an infinite interval. */
-if (addr != NULL)
+if (dbd == NULL)
{
- deliver_domain = addr->domain;
- deliver_localpart = addr->local_part;
+ HDEBUG(D_acl) debug_printf("ratelimit initializing new key's rate data\n");
+ dbd = &dbdb->dbd;
+ dbd->time_stamp = tv.tv_sec;
+ dbd->time_usec = tv.tv_usec;
+ dbd->rate = count;
+ }
+else
+ {
+ /* The smoothed rate is computed using an exponentially weighted moving
+ average adjusted for variable sampling intervals. The standard EWMA for
+ a fixed sampling interval is: f'(t) = (1 - a) * f(t) + a * f'(t - 1)
+ where f() is the measured value and f'() is the smoothed value.
+
+ Old data decays out of the smoothed value exponentially, such that data n
+ samples old is multiplied by a^n. The exponential decay time constant p
+ is defined such that data p samples old is multiplied by 1/e, which means
+ that a = exp(-1/p). We can maintain the same time constant for a variable
+ sampling interval i by using a = exp(-i/p).
+
+ The rate we are measuring is messages per period, suitable for directly
+ comparing with the limit. The average rate between now and the previous
+ message is period / interval, which we feed into the EWMA as the sample.
+
+ It turns out that the number of messages required for the smoothed rate
+ to reach the limit when they are sent in a burst is equal to the limit.
+ This can be seen by analysing the value of the smoothed rate after N
+ messages sent at even intervals. Let k = (1 - a) * p/i
+
+ rate_1 = (1 - a) * p/i + a * rate_0
+ = k + a * rate_0
+ rate_2 = k + a * rate_1
+ = k + a * k + a^2 * rate_0
+ rate_3 = k + a * k + a^2 * k + a^3 * rate_0
+ rate_N = rate_0 * a^N + k * SUM(x=0..N-1)(a^x)
+ = rate_0 * a^N + k * (1 - a^N) / (1 - a)
+ = rate_0 * a^N + p/i * (1 - a^N)
+
+ When N is large, a^N -> 0 so rate_N -> p/i as desired.
+
+ rate_N = p/i + (rate_0 - p/i) * a^N
+ a^N = (rate_N - p/i) / (rate_0 - p/i)
+ N * -i/p = log((rate_N - p/i) / (rate_0 - p/i))
+ N = p/i * log((rate_0 - p/i) / (rate_N - p/i))
+
+ Numerical analysis of the above equation, setting the computed rate to
+ increase from rate_0 = 0 to rate_N = limit, shows that for large sending
+ rates, p/i, the number of messages N = limit. So limit serves as both the
+ maximum rate measured in messages per period, and the maximum number of
+ messages that can be sent in a fast burst. */
+
+ double this_time = (double)tv.tv_sec
+ + (double)tv.tv_usec / 1000000.0;
+ double prev_time = (double)dbd->time_stamp
+ + (double)dbd->time_usec / 1000000.0;
+
+ /* We must avoid division by zero, and deal gracefully with the clock going
+ backwards. If we blunder ahead when time is in reverse then the computed
+ rate will be bogus. To be safe we clamp interval to a very small number. */
+
+ double interval = this_time - prev_time <= 0.0 ? 1e-9
+ : this_time - prev_time;
+
+ double i_over_p = interval / period;
+ double a = exp(-i_over_p);
+
+ /* Combine the instantaneous rate (period / interval) with the previous rate
+ using the smoothing factor a. In order to measure sized events, multiply the
+ instantaneous rate by the count of bytes or recipients etc. */
+
+ dbd->time_stamp = tv.tv_sec;
+ dbd->time_usec = tv.tv_usec;
+ dbd->rate = (1 - a) * count / i_over_p + a * dbd->rate;
+
+ /* When events are very widely spaced the computed rate tends towards zero.
+ Although this is accurate it turns out not to be useful for our purposes,
+ especially when the first event after a long silence is the start of a spam
+ run. A more useful model is that the rate for an isolated event should be the
+ size of the event per the period size, ignoring the lack of events outside
+ the current period and regardless of where the event falls in the period. So,
+ if the interval was so long that the calculated rate is unhelpfully small, we
+ re-intialize the rate. In the absence of higher-rate bursts, the condition
+ below is true if the interval is greater than the period. */
+
+ if (dbd->rate < count) dbd->rate = count;
}
-return rc;
-
-/* Syntax errors in the verify argument come here. */
-
-BAD_VERIFY:
-*log_msgptr = string_sprintf("expected \"sender[=address]\", \"recipient\", "
- "\"header_syntax\" or \"header_sender\" at start of ACL condition "
- "\"verify %s\"", arg);
-return ERROR;
-}
+/* Clients sending at the limit are considered to be over the limit.
+This matters for edge cases such as a limit of zero, when the client
+should be completely blocked. */
+rc = (dbd->rate < limit)? FAIL : OK;
+/* Update the state if the rate is low or if we are being strict. If we
+are in leaky mode and the sender's rate is too high, we do not update
+the recorded rate in order to avoid an over-aggressive sender's retry
+rate preventing them from getting any email through. If readonly is set,
+neither leaky nor strict are set, so we do not do any updates. */
-/*************************************************
-* Check argument for control= modifier *
-*************************************************/
+if ((rc == FAIL && leaky) || strict)
+ {
+ dbfn_write(dbm, key, dbdb, dbdb_size);
+ HDEBUG(D_acl) debug_printf("ratelimit db updated\n");
+ }
+else
+ {
+ HDEBUG(D_acl) debug_printf("ratelimit db not updated: %s\n",
+ readonly? "readonly mode" : "over the limit, but leaky");
+ }
-/* Called from acl_check_condition() below
+dbfn_close(dbm);
-Arguments:
- arg the argument string for control=
- pptr set to point to the terminating character
- where which ACL we are in
- log_msgptr for error messages
+/* Store the result in the tree for future reference. */
-Returns: CONTROL_xxx value
-*/
+t = store_get(sizeof(tree_node) + Ustrlen(key));
+t->data.ptr = dbd;
+Ustrcpy(t->name, key);
+(void)tree_insertnode(anchor, t);
-static int
-decode_control(uschar *arg, uschar **pptr, int where, uschar **log_msgptr)
-{
-int len;
-control_def *d;
+/* We create the formatted version of the sender's rate very late in
+order to ensure that it is done using the correct storage pool. */
-for (d = controls_list;
- d < controls_list + sizeof(controls_list)/sizeof(control_def);
- d++)
- {
- len = Ustrlen(d->name);
- if (Ustrncmp(d->name, arg, len) == 0) break;
- }
+store_pool = old_pool;
+sender_rate = string_sprintf("%.1f", dbd->rate);
-if (d >= controls_list + sizeof(controls_list)/sizeof(control_def) ||
- (arg[len] != 0 && (!d->has_option || arg[len] != '/')))
- {
- *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
- return CONTROL_ERROR;
- }
+HDEBUG(D_acl)
+ debug_printf("ratelimit computed rate %s\n", sender_rate);
-*pptr = arg + len;
-return d->value;
+return rc;
}
{
uschar *user_message = NULL;
uschar *log_message = NULL;
-uschar *p;
+uschar *debug_tag = NULL;
+uschar *debug_opts = NULL;
+uschar *p = NULL;
int rc = OK;
+#ifdef WITH_CONTENT_SCAN
+int sep = '/';
+#endif
for (; cb != NULL; cb = cb->next)
{
uschar *arg;
- int control_type;
+ int control_type;
/* The message and log_message items set up messages to be used in
case of rejection. They are expanded later. */
if (cb->type == ACLC_SET)
{
- int n = cb->u.varnumber;
- int t = (n < ACL_C_MAX)? 'c' : 'm';
- if (n >= ACL_C_MAX) n -= ACL_C_MAX;
- debug_printf("acl_%c%d ", t, n);
- lhswidth += 7;
+ debug_printf("acl_%s ", cb->u.varname);
+ lhswidth += 5 + Ustrlen(cb->u.varname);
}
debug_printf("= %s\n", cb->arg);
switch(cb->type)
{
+ case ACLC_ADD_HEADER:
+ setup_header(arg);
+ break;
+
/* A nested ACL that returns "discard" makes sense only for an "accept" or
"discard" verb. */
TRUE, NULL);
break;
+ #ifdef EXPERIMENTAL_BRIGHTMAIL
+ case ACLC_BMI_OPTIN:
+ {
+ int old_pool = store_pool;
+ store_pool = POOL_PERM;
+ bmi_current_optin = string_copy(arg);
+ store_pool = old_pool;
+ }
+ break;
+ #endif
+
case ACLC_CONDITION:
+ /* The true/false parsing here should be kept in sync with that used in
+ expand.c when dealing with ECOND_BOOL so that we don't have too many
+ different definitions of what can be a boolean. */
if (Ustrspn(arg, "0123456789") == Ustrlen(arg)) /* Digits, or empty */
rc = (Uatoi(arg) == 0)? FAIL : OK;
else
*log_msgptr = string_sprintf("invalid \"condition\" value \"%s\"", arg);
break;
+ case ACLC_CONTINUE: /* Always succeeds */
+ break;
+
case ACLC_CONTROL:
control_type = decode_control(arg, &p, where, log_msgptr);
- /* Check this control makes sense at this time */
+ /* Check if this control makes sense at this time */
if ((control_forbids[control_type] & (1 << where)) != 0)
{
*log_msgptr = string_sprintf("cannot use \"control=%s\" in %s ACL",
controls[control_type], acl_wherenames[where]);
return ERROR;
- }
+ }
switch(control_type)
{
+ case CONTROL_AUTH_UNADVERTISED:
+ allow_auth_unadvertised = TRUE;
+ break;
+
+ #ifdef EXPERIMENTAL_BRIGHTMAIL
+ case CONTROL_BMI_RUN:
+ bmi_run = 1;
+ break;
+ #endif
+
+ #ifndef DISABLE_DKIM
+ case CONTROL_DKIM_VERIFY:
+ dkim_disable_verify = TRUE;
+ break;
+ #endif
+
case CONTROL_ERROR:
return ERROR;
smtp_enforce_sync = FALSE;
break;
+ #ifdef WITH_CONTENT_SCAN
+ case CONTROL_NO_MBOX_UNSPOOL:
+ no_mbox_unspool = TRUE;
+ break;
+ #endif
+
case CONTROL_NO_MULTILINE:
no_multiline_responses = TRUE;
break;
+ case CONTROL_NO_PIPELINING:
+ pipelining_enable = FALSE;
+ break;
+
+ case CONTROL_NO_DELAY_FLUSH:
+ disable_delay_flush = TRUE;
+ break;
+
+ case CONTROL_NO_CALLOUT_FLUSH:
+ disable_callout_flush = TRUE;
+ break;
+
+ case CONTROL_FAKEDEFER:
+ case CONTROL_FAKEREJECT:
+ fake_response = (control_type == CONTROL_FAKEDEFER) ? DEFER : FAIL;
+ if (*p == '/')
+ {
+ uschar *pp = p + 1;
+ while (*pp != 0) pp++;
+ fake_response_text = expand_string(string_copyn(p+1, pp-p-1));
+ p = pp;
+ }
+ else
+ {
+ /* Explicitly reset to default string */
+ fake_response_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s).";
+ }
+ break;
+
case CONTROL_FREEZE:
deliver_freeze = TRUE;
deliver_frozen_at = time(NULL);
+ freeze_tell = freeze_tell_config; /* Reset to configured value */
+ if (Ustrncmp(p, "/no_tell", 8) == 0)
+ {
+ p += 8;
+ freeze_tell = NULL;
+ }
+ if (*p != 0)
+ {
+ *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
+ return ERROR;
+ }
break;
case CONTROL_QUEUE_ONLY:
break;
case CONTROL_SUBMISSION:
+ originator_name = US"";
submission_mode = TRUE;
while (*p == '/')
- {
+ {
if (Ustrncmp(p, "/sender_retain", 14) == 0)
{
p += 14;
active_local_sender_retain = TRUE;
- active_local_from_check = FALSE;
- }
+ active_local_from_check = FALSE;
+ }
else if (Ustrncmp(p, "/domain=", 8) == 0)
{
uschar *pp = p + 8;
- while (*pp != 0 && *pp != '/') pp++;
- submission_domain = string_copyn(p+8, pp-p);
- p = pp;
+ while (*pp != 0 && *pp != '/') pp++;
+ submission_domain = string_copyn(p+8, pp-p-8);
+ p = pp;
+ }
+ /* The name= option must be last, because it swallows the rest of
+ the string. */
+ else if (Ustrncmp(p, "/name=", 6) == 0)
+ {
+ uschar *pp = p + 6;
+ while (*pp != 0) pp++;
+ submission_name = string_copy(parse_fix_phrase(p+6, pp-p-6,
+ big_buffer, big_buffer_size));
+ p = pp;
}
- else break;
- }
+ else break;
+ }
if (*p != 0)
{
*log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
return ERROR;
}
break;
+
+ case CONTROL_DEBUG:
+ while (*p == '/')
+ {
+ if (Ustrncmp(p, "/tag=", 5) == 0)
+ {
+ uschar *pp = p + 5;
+ while (*pp != '\0' && *pp != '/') pp++;
+ debug_tag = string_copyn(p+5, pp-p-5);
+ p = pp;
+ }
+ else if (Ustrncmp(p, "/opts=", 6) == 0)
+ {
+ uschar *pp = p + 6;
+ while (*pp != '\0' && *pp != '/') pp++;
+ debug_opts = string_copyn(p+6, pp-p-6);
+ p = pp;
+ }
+ }
+ debug_logging_activate(debug_tag, debug_opts);
+ break;
+
+ case CONTROL_SUPPRESS_LOCAL_FIXUPS:
+ suppress_local_fixups = TRUE;
+ break;
+ }
+ break;
+
+ #ifdef EXPERIMENTAL_DCC
+ case ACLC_DCC:
+ {
+ /* Seperate the regular expression and any optional parameters. */
+ uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size);
+ /* Run the dcc backend. */
+ rc = dcc_process(&ss);
+ /* Modify return code based upon the existance of options. */
+ while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size))
+ != NULL) {
+ if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER)
+ {
+ /* FAIL so that the message is passed to the next ACL */
+ rc = FAIL;
+ }
+ }
}
break;
+ #endif
+
+ #ifdef WITH_CONTENT_SCAN
+ case ACLC_DECODE:
+ rc = mime_decode(&arg);
+ break;
+ #endif
case ACLC_DELAY:
{
HDEBUG(D_acl)
debug_printf("delay skipped in -bh checking mode\n");
}
- else sleep(delay);
+
+ /* It appears to be impossible to detect that a TCP/IP connection has
+ gone away without reading from it. This means that we cannot shorten
+ the delay below if the client goes away, because we cannot discover
+ that the client has closed its end of the connection. (The connection
+ is actually in a half-closed state, waiting for the server to close its
+ end.) It would be nice to be able to detect this state, so that the
+ Exim process is not held up unnecessarily. However, it seems that we
+ can't. The poll() function does not do the right thing, and in any case
+ it is not always available.
+
+ NOTE 1: If ever this state of affairs changes, remember that we may be
+ dealing with stdin/stdout here, in addition to TCP/IP connections.
+ Also, delays may be specified for non-SMTP input, where smtp_out and
+ smtp_in will be NULL. Whatever is done must work in all cases.
+
+ NOTE 2: The added feature of flushing the output before a delay must
+ apply only to SMTP input. Hence the test for smtp_out being non-NULL.
+ */
+
+ else
+ {
+ if (smtp_out != NULL && !disable_delay_flush) mac_smtp_fflush();
+ while (delay > 0) delay = sleep(delay);
+ }
}
}
break;
+ #ifdef WITH_OLD_DEMIME
+ case ACLC_DEMIME:
+ rc = demime(&arg);
+ break;
+ #endif
+
+ #ifndef DISABLE_DKIM
+ case ACLC_DKIM_SIGNER:
+ if (dkim_cur_signer != NULL)
+ rc = match_isinlist(dkim_cur_signer,
+ &arg,0,NULL,NULL,MCL_STRING,TRUE,NULL);
+ else
+ rc = FAIL;
+ break;
+
+ case ACLC_DKIM_STATUS:
+ rc = match_isinlist(dkim_exim_expand_query(DKIM_VERIFY_STATUS),
+ &arg,0,NULL,NULL,MCL_STRING,TRUE,NULL);
+ break;
+ #endif
+
case ACLC_DNSLISTS:
rc = verify_check_dnsbl(&arg);
break;
&deliver_localpart_data);
break;
+ case ACLC_LOG_REJECT_TARGET:
+ {
+ int logbits = 0;
+ int sep = 0;
+ uschar *s = arg;
+ uschar *ss;
+ while ((ss = string_nextinlist(&s, &sep, big_buffer, big_buffer_size))
+ != NULL)
+ {
+ if (Ustrcmp(ss, "main") == 0) logbits |= LOG_MAIN;
+ else if (Ustrcmp(ss, "panic") == 0) logbits |= LOG_PANIC;
+ else if (Ustrcmp(ss, "reject") == 0) logbits |= LOG_REJECT;
+ else
+ {
+ logbits |= LOG_MAIN|LOG_REJECT;
+ log_write(0, LOG_MAIN|LOG_PANIC, "unknown log name \"%s\" in "
+ "\"log_reject_target\" in %s ACL", ss, acl_wherenames[where]);
+ }
+ }
+ log_reject_target = logbits;
+ }
+ break;
+
case ACLC_LOGWRITE:
{
int logbits = 0;
s++;
}
while (isspace(*s)) s++;
+
+
if (logbits == 0) logbits = LOG_MAIN;
log_write(0, logbits, "%s", string_printing(s));
}
break;
+ #ifdef WITH_CONTENT_SCAN
+ case ACLC_MALWARE:
+ {
+ /* Separate the regular expression and any optional parameters. */
+ uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size);
+ /* Run the malware backend. */
+ rc = malware(&ss);
+ /* Modify return code based upon the existance of options. */
+ while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size))
+ != NULL) {
+ if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER)
+ {
+ /* FAIL so that the message is passed to the next ACL */
+ rc = FAIL;
+ }
+ }
+ }
+ break;
+
+ case ACLC_MIME_REGEX:
+ rc = mime_regex(&arg);
+ break;
+ #endif
+
+ case ACLC_RATELIMIT:
+ rc = acl_ratelimit(arg, where, log_msgptr);
+ break;
+
case ACLC_RECIPIENTS:
rc = match_address_list(addr->address, TRUE, TRUE, &arg, NULL, -1, 0,
&recipient_data);
break;
+ #ifdef WITH_CONTENT_SCAN
+ case ACLC_REGEX:
+ rc = regex(&arg);
+ break;
+ #endif
+
case ACLC_SENDER_DOMAINS:
{
uschar *sdomain;
case ACLC_SET:
{
int old_pool = store_pool;
- if (cb->u.varnumber < ACL_C_MAX) store_pool = POOL_PERM;
- acl_var[cb->u.varnumber] = string_copy(arg);
+ if (cb->u.varname[0] == 'c') store_pool = POOL_PERM;
+ acl_var_create(cb->u.varname)->data.ptr = string_copy(arg);
store_pool = old_pool;
}
break;
+ #ifdef WITH_CONTENT_SCAN
+ case ACLC_SPAM:
+ {
+ /* Seperate the regular expression and any optional parameters. */
+ uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size);
+ /* Run the spam backend. */
+ rc = spam(&ss);
+ /* Modify return code based upon the existance of options. */
+ while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size))
+ != NULL) {
+ if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER)
+ {
+ /* FAIL so that the message is passed to the next ACL */
+ rc = FAIL;
+ }
+ }
+ }
+ break;
+ #endif
+
+ #ifdef EXPERIMENTAL_SPF
+ case ACLC_SPF:
+ rc = spf_process(&arg, sender_address, SPF_PROCESS_NORMAL);
+ break;
+ case ACLC_SPF_GUESS:
+ rc = spf_process(&arg, sender_address, SPF_PROCESS_GUESS);
+ break;
+ #endif
+
/* If the verb is WARN, discard any user message from verification, because
such messages are SMTP responses, not header additions. The latter come
- only from explicit "message" modifiers. */
+ only from explicit "message" modifiers. However, put the user message into
+ $acl_verify_message so it can be used in subsequent conditions or modifiers
+ (until something changes it). */
case ACLC_VERIFY:
rc = acl_verify(where, addr, arg, user_msgptr, log_msgptr, basic_errno);
+ acl_verify_message = *user_msgptr;
if (verb == ACL_WARN) *user_msgptr = NULL;
break;
/* If the result is the one for which "message" and/or "log_message" are used,
-handle the values of these options. Most verbs have but a single return for
-which the messages are relevant, but for "discard", it's useful to have the log
-message both when it succeeds and when it fails. Also, for an "accept" that
-appears in a QUIT ACL, we want to handle the user message. Since only "accept"
-and "warn" are permitted in that ACL, we don't need to test the verb.
-
-These modifiers act in different ways:
+handle the values of these modifiers. If there isn't a log message set, we make
+it the same as the user message.
"message" is a user message that will be included in an SMTP response. Unless
it is empty, it overrides any previously set user message.
"log_message" is a non-user message, and it adds to any existing non-user
message that is already set.
-If there isn't a log message set, we make it the same as the user message. */
+Most verbs have but a single return for which the messages are relevant, but
+for "discard", it's useful to have the log message both when it succeeds and
+when it fails. For "accept", the message is used in the OK case if there is no
+"endpass", but (for backwards compatibility) in the FAIL case if "endpass" is
+present. */
-if (((rc == FAIL_DROP)? FAIL : rc) == msgcond[verb] ||
- (verb == ACL_DISCARD && rc == OK) ||
- (where == ACL_WHERE_QUIT))
+if (*epp && rc == OK) user_message = NULL;
+
+if (((1<<rc) & msgcond[verb]) != 0)
{
uschar *expmessage;
+ uschar *old_user_msgptr = *user_msgptr;
+ uschar *old_log_msgptr = (*log_msgptr != NULL)? *log_msgptr : old_user_msgptr;
/* If the verb is "warn", messages generated by conditions (verification or
- nested ACLs) are discarded. Only messages specified at this level are used.
+ nested ACLs) are always discarded. This also happens for acceptance verbs
+ when they actually do accept. Only messages specified at this level are used.
However, the value of an existing message is available in $acl_verify_message
during expansions. */
- uschar *old_user_msgptr = *user_msgptr;
- uschar *old_log_msgptr = (*log_msgptr != NULL)? *log_msgptr : old_user_msgptr;
-
- if (verb == ACL_WARN) *log_msgptr = *user_msgptr = NULL;
+ if (verb == ACL_WARN ||
+ (rc == OK && (verb == ACL_ACCEPT || verb == ACL_DISCARD)))
+ *log_msgptr = *user_msgptr = NULL;
if (user_message != NULL)
{
return ERROR;
}
acl_text[statbuf.st_size] = 0;
- close(fd);
+ (void)close(fd);
acl_name = string_sprintf("ACL \"%s\"", ss);
HDEBUG(D_acl) debug_printf("read ACL from file %s\n", ss);
*log_msgptr = *user_msgptr = NULL;
acl_temp_details = FALSE;
- if (where == ACL_WHERE_QUIT &&
+ if ((where == ACL_WHERE_QUIT || where == ACL_WHERE_NOTQUIT) &&
acl->verb != ACL_ACCEPT &&
acl->verb != ACL_WARN)
{
- *log_msgptr = string_sprintf("\"%s\" is not allowed in a QUIT ACL",
+ *log_msgptr = string_sprintf("\"%s\" is not allowed in a QUIT or not-QUIT ACL",
verbs[acl->verb]);
return ERROR;
}
switch (cond)
{
case DEFER:
- HDEBUG(D_acl) debug_printf("%s: condition test deferred\n", verbs[acl->verb]);
+ HDEBUG(D_acl) debug_printf("%s: condition test deferred in %s\n", verbs[acl->verb], acl_name);
if (basic_errno != ERRNO_CALLOUTDEFER)
{
if (search_error_message != NULL && *search_error_message != 0)
default: /* Paranoia */
case ERROR:
- HDEBUG(D_acl) debug_printf("%s: condition test error\n", verbs[acl->verb]);
+ HDEBUG(D_acl) debug_printf("%s: condition test error in %s\n", verbs[acl->verb], acl_name);
return ERROR;
case OK:
- HDEBUG(D_acl) debug_printf("%s: condition test succeeded\n",
- verbs[acl->verb]);
+ HDEBUG(D_acl) debug_printf("%s: condition test succeeded in %s\n",
+ verbs[acl->verb], acl_name);
break;
case FAIL:
- HDEBUG(D_acl) debug_printf("%s: condition test failed\n", verbs[acl->verb]);
+ HDEBUG(D_acl) debug_printf("%s: condition test failed in %s\n", verbs[acl->verb], acl_name);
break;
/* DISCARD and DROP can happen only from a nested ACL condition, and
DISCARD can happen only for an "accept" or "discard" verb. */
case DISCARD:
- HDEBUG(D_acl) debug_printf("%s: condition test yielded \"discard\"\n",
- verbs[acl->verb]);
+ HDEBUG(D_acl) debug_printf("%s: condition test yielded \"discard\" in %s\n",
+ verbs[acl->verb], acl_name);
break;
case FAIL_DROP:
- HDEBUG(D_acl) debug_printf("%s: condition test yielded \"drop\"\n",
- verbs[acl->verb]);
+ HDEBUG(D_acl) debug_printf("%s: condition test yielded \"drop\" in %s\n",
+ verbs[acl->verb], acl_name);
break;
}
case ACL_WARN:
if (cond == OK)
acl_warn(where, *user_msgptr, *log_msgptr);
- else if (cond == DEFER)
- acl_warn(where, NULL, string_sprintf("ACL \"warn\" statement skipped: "
- "condition test deferred: %s",
- (*log_msgptr == NULL)? US"" : *log_msgptr));
+ else if (cond == DEFER && (log_extra_selector & LX_acl_warn_skipped) != 0)
+ log_write(0, LOG_MAIN, "%s Warning: ACL \"warn\" statement skipped: "
+ "condition test deferred%s%s", host_and_ident(TRUE),
+ (*log_msgptr == NULL)? US"" : US": ",
+ (*log_msgptr == NULL)? US"" : *log_msgptr);
*log_msgptr = *user_msgptr = NULL; /* In case implicit DENY follows */
break;
Arguments:
where ACL_WHERE_xxxx indicating where called from
- data_string RCPT address, or SMTP command argument, or NULL
+ recipient RCPT address for RCPT check, else NULL
s the input string; NULL is the same as an empty ACL => DENY
user_msgptr where to put a user error (for SMTP response)
log_msgptr where to put a logging message (not for SMTP response)
*/
int
-acl_check(int where, uschar *data_string, uschar *s, uschar **user_msgptr,
+acl_check(int where, uschar *recipient, uschar *s, uschar **user_msgptr,
uschar **log_msgptr)
{
int rc;
address_item adb;
-address_item *addr;
+address_item *addr = NULL;
*user_msgptr = *log_msgptr = NULL;
sender_verified_failed = NULL;
+ratelimiters_cmd = NULL;
+log_reject_target = LOG_MAIN|LOG_REJECT;
if (where == ACL_WHERE_RCPT)
{
adb = address_defaults;
addr = &adb;
- addr->address = data_string;
+ addr->address = recipient;
if (deliver_split_address(addr) == DEFER)
{
*log_msgptr = US"defer in percent_hack_domains check";
deliver_domain = addr->domain;
deliver_localpart = addr->local_part;
}
-else
- {
- addr = NULL;
- smtp_command_argument = data_string;
- }
rc = acl_check_internal(where, addr, s, 0, user_msgptr, log_msgptr);
-smtp_command_argument = deliver_domain =
- deliver_localpart = deliver_address_data = NULL;
+deliver_domain = deliver_localpart = deliver_address_data =
+ sender_address_data = NULL;
/* A DISCARD response is permitted only for message ACLs, excluding the PREDATA
ACL, which is really in the middle of an SMTP command. */
return ERROR;
}
-/* Before giving an error response, take a look at the length of any user
-message, and split it up into multiple lines if possible. */
+/* Before giving a response, take a look at the length of any user message, and
+split it up into multiple lines if possible. */
-if (rc != OK && *user_msgptr != NULL && Ustrlen(*user_msgptr) > 75)
- {
- uschar *s = *user_msgptr = string_copy(*user_msgptr);
- uschar *ss = s;
+*user_msgptr = string_split_message(*user_msgptr);
+if (fake_response != OK)
+ fake_response_text = string_split_message(fake_response_text);
- for (;;)
- {
- int i = 0;
- while (i < 75 && *ss != 0 && *ss != '\n') ss++, i++;
- if (*ss == 0) break;
- if (*ss == '\n')
- s = ++ss;
- else
- {
- uschar *t = ss + 1;
- uschar *tt = NULL;
- while (--t > s + 35)
- {
- if (*t == ' ')
- {
- if (t[-1] == ':') { tt = t; break; }
- if (tt == NULL) tt = t;
- }
- }
+return rc;
+}
- if (tt == NULL) /* Can't split behind - try ahead */
- {
- t = ss + 1;
- while (*t != 0)
- {
- if (*t == ' ' || *t == '\n')
- { tt = t; break; }
- t++;
- }
- }
- if (tt == NULL) break; /* Can't find anywhere to split */
- *tt = '\n';
- s = ss = tt+1;
- }
- }
+
+/*************************************************
+* Create ACL variable *
+*************************************************/
+
+/* Create an ACL variable or reuse an existing one. ACL variables are in a
+binary tree (see tree.c) with acl_var_c and acl_var_m as root nodes.
+
+Argument:
+ name pointer to the variable's name, starting with c or m
+
+Returns the pointer to variable's tree node
+*/
+
+tree_node *
+acl_var_create(uschar *name)
+{
+tree_node *node, **root;
+root = (name[0] == 'c')? &acl_var_c : &acl_var_m;
+node = tree_search(*root, name);
+if (node == NULL)
+ {
+ node = store_get(sizeof(tree_node) + Ustrlen(name));
+ Ustrcpy(node->name, name);
+ (void)tree_insertnode(root, node);
}
+node->data.ptr = NULL;
+return node;
+}
-return rc;
+
+
+/*************************************************
+* Write an ACL variable in spool format *
+*************************************************/
+
+/* This function is used as a callback for tree_walk when writing variables to
+the spool file. To retain spool file compatibility, what is written is -aclc or
+-aclm followed by the rest of the name and the data length, space separated,
+then the value itself, starting on a new line, and terminated by an additional
+newline. When we had only numbered ACL variables, the first line might look
+like this: "-aclc 5 20". Now it might be "-aclc foo 20" for the variable called
+acl_cfoo.
+
+Arguments:
+ name of the variable
+ value of the variable
+ ctx FILE pointer (as a void pointer)
+
+Returns: nothing
+*/
+
+void
+acl_var_write(uschar *name, uschar *value, void *ctx)
+{
+FILE *f = (FILE *)ctx;
+fprintf(f, "-acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value);
}
/* End of acl.c */