diff options
author | Jeremy Harris <jgh146exb@wizmail.org> | 2021-08-01 18:15:39 +0100 |
---|---|---|
committer | Jeremy Harris <jgh146exb@wizmail.org> | 2021-08-01 18:15:39 +0100 |
commit | 2357aa78ccd7182cad14307eb89cb1065f078356 (patch) | |
tree | 480b8913a6d153400e8f85b418263f18b25e6f86 /src | |
parent | 38089ca5c8f4c06092324099fc38494f8491b53c (diff) |
ACL: "seen" condition
Diffstat (limited to 'src')
-rw-r--r-- | src/src/acl.c | 143 | ||||
-rw-r--r-- | src/src/dbstuff.h | 6 | ||||
-rw-r--r-- | src/src/exim_dbutil.c | 12 |
3 files changed, 160 insertions, 1 deletions
diff --git a/src/src/acl.c b/src/src/acl.c index f47259ca0..be17b5768 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -103,6 +103,7 @@ enum { ACLC_ACL, ACLC_REGEX, #endif ACLC_REMOVE_HEADER, + ACLC_SEEN, ACLC_SENDER_DOMAINS, ACLC_SENDERS, ACLC_SET, @@ -288,6 +289,7 @@ static condition_def conditions[] = { ACL_BIT_MIME | ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START), }, + [ACLC_SEEN] = { US"seen", TRUE, FALSE, 0 }, [ACLC_SENDER_DOMAINS] = { US"sender_domains", FALSE, FALSE, ACL_BIT_AUTH | ACL_BIT_CONNECT | ACL_BIT_HELO | @@ -2816,6 +2818,143 @@ return rc; /************************************************* +* Handle a check for previously-seen * +*************************************************/ + +/* +ACL clauses like: seen = -5m / key=$foo / readonly + +Return is true for condition-true - but the semantics +depend heavily on the actual use-case. + +Negative times test for seen-before, positive for seen-more-recently-than +(the given interval before current time). + +All are subject to history not having been cleaned from the DB. + +Default for seen-before is to create if not present, and to +update if older than 10d (with the seen-test time). +Default for seen-since is to always create or update. + +Options: + key=value. Default key is $sender_host_address + readonly + write + refresh=<interval>: update an existing DB entry older than given + amount. Default refresh lacking this option is 10d. + The update sets the record timestamp to the seen-test time. + +XXX do we need separate nocreate, noupdate controls? + +Arguments: + arg the option string for seen= + where ACL_WHERE_xxxx indicating which ACL this is + log_msgptr for error messages + +Returns: OK - Condition is true + FAIL - Condition is false + DEFER - Problem opening history database + ERROR - Syntax error in options +*/ + +static int +acl_seen(const uschar * arg, int where, uschar ** log_msgptr) +{ +enum { SEEN_DEFAULT, SEEN_READONLY, SEEN_WRITE }; + +const uschar * list = arg; +int slash = '/', equal = '=', interval, mode = SEEN_DEFAULT, yield = FAIL; +BOOL before; +int refresh = 10 * 24 * 60 * 60; /* 10 days */ +const uschar * ele, * key = sender_host_address; +open_db dbblock, * dbm; +dbdata_seen * dbd; +time_t now; + +/* Parse the first element, the time-relation. */ + +if (!(ele = string_nextinlist(&list, &slash, NULL, 0))) + goto badparse; +if ((before = *ele == '-')) + ele++; +if ((interval = readconf_readtime(ele, 0, FALSE)) < 0) + goto badparse; + +/* Remaining elements are options */ + +while ((ele = string_nextinlist(&list, &slash, NULL, 0))) + if (Ustrncmp(ele, "key=", 4) == 0) + key = ele + 4; + else if (Ustrcmp(ele, "readonly") == 0) + mode = SEEN_READONLY; + else if (Ustrcmp(ele, "write") == 0) + mode = SEEN_WRITE; + else if (Ustrncmp(ele, "refresh=", 8) == 0) + { + if ((refresh = readconf_readtime(ele + 8, 0, FALSE)) < 0) + goto badparse; + } + else + goto badopt; + +if (!(dbm = dbfn_open(US"seen", O_RDWR, &dbblock, TRUE, TRUE))) + { + HDEBUG(D_acl) debug_printf_indent("database for 'seen' not available\n"); + *log_msgptr = US"database for 'seen' not available"; + return DEFER; + } + +dbd = dbfn_read_with_length(dbm, key, NULL); +now = time(NULL); +if (dbd) /* an existing record */ + { + time_t diff = now - dbd->time_stamp; /* time since the record was written */ + + if (before ? diff >= interval : diff < interval) + yield = OK; + + if (mode == SEEN_READONLY) + { HDEBUG(D_acl) debug_printf_indent("seen db not written (readonly)\n"); } + else if (mode == SEEN_WRITE || !before) + { + dbd->time_stamp = now; + dbfn_write(dbm, key, dbd, sizeof(*dbd)); + HDEBUG(D_acl) debug_printf_indent("seen db written (update)\n"); + } + else if (diff >= refresh) + { + dbd->time_stamp = now - interval; + dbfn_write(dbm, key, dbd, sizeof(*dbd)); + HDEBUG(D_acl) debug_printf_indent("seen db written (refresh)\n"); + } + } +else + { /* No record found, yield always FAIL */ + if (mode != SEEN_READONLY) + { + dbdata_seen d = {.time_stamp = now}; + dbfn_write(dbm, key, &d, sizeof(*dbd)); + HDEBUG(D_acl) debug_printf_indent("seen db written (create)\n"); + } + else + HDEBUG(D_acl) debug_printf_indent("seen db not written (readonly)\n"); + } + +dbfn_close(dbm); +return yield; + + +badparse: + *log_msgptr = string_sprintf("failed to parse '%s'", arg); + return ERROR; +badopt: + *log_msgptr = string_sprintf("unrecognised option '%s' in '%s'", ele, arg); + return ERROR; +} + + + +/************************************************* * The udpsend ACL modifier * *************************************************/ @@ -3740,6 +3879,10 @@ for (; cb; cb = cb->next) setup_remove_header(arg); break; + case ACLC_SEEN: + rc = acl_seen(arg, where, log_msgptr); + break; + case ACLC_SENDER_DOMAINS: { uschar *sdomain; diff --git a/src/src/dbstuff.h b/src/src/dbstuff.h index 2f00dffb4..94db7f7fd 100644 --- a/src/src/dbstuff.h +++ b/src/src/dbstuff.h @@ -788,6 +788,12 @@ typedef struct { uschar bloom[40]; /* Bloom filter which may be larger than this */ } dbdata_ratelimit_unique; + +/* For "seen" ACL condition */ +typedef struct { + time_t time_stamp; +} dbdata_seen; + #ifndef DISABLE_PIPE_CONNECT /* This structure records the EHLO responses, cleartext and crypted, for an IP, as bitmasks (cf. OPTION_TLS). For LIMITS, also values diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c index 13f74540e..45b778fc0 100644 --- a/src/src/exim_dbutil.c +++ b/src/src/exim_dbutil.c @@ -21,7 +21,9 @@ argument is the name of the database file. The available names are: misc: miscellaneous hints data wait-<t>: message waiting information; <t> is a transport name callout: callout verification cache + ratelimit: ACL 'ratelimit' condition tls: TLS session resumption cache + seen: ACL 'seen' condition There are a number of common subroutines, followed by three main programs, whose inclusion is controlled by -D on the compilation command. */ @@ -38,6 +40,7 @@ whose inclusion is controlled by -D on the compilation command. */ #define type_callout 4 #define type_ratelimit 5 #define type_tls 6 +#define type_seen 7 /* This is used by our cut-down dbfn_open(). */ @@ -126,7 +129,7 @@ static void usage(uschar *name, uschar *options) { printf("Usage: exim_%s%s <spool-directory> <database-name>\n", name, options); -printf(" <database-name> = retry | misc | wait-<transport-name> | callout | ratelimit | tls\n"); +printf(" <database-name> = retry | misc | wait-<transport-name> | callout | ratelimit | tls | seen\n"); exit(1); } @@ -150,6 +153,7 @@ if (argc == 3) if (Ustrcmp(argv[2], "callout") == 0) return type_callout; if (Ustrcmp(argv[2], "ratelimit") == 0) return type_ratelimit; if (Ustrcmp(argv[2], "tls") == 0) return type_tls; + if (Ustrcmp(argv[2], "seen") == 0) return type_seen; } usage(name, options); return -1; /* Never obeyed */ @@ -581,6 +585,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor); dbdata_ratelimit *ratelimit; dbdata_ratelimit_unique *rate_unique; dbdata_tls_session *session; + dbdata_seen *seen; int count_bad = 0; int length; uschar *t; @@ -720,6 +725,11 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor); session = (dbdata_tls_session *)value; printf(" %s %.*s\n", keybuffer, length, session->session); break; + + case type_seen: + seen = (dbdata_seen *)value; + printf("%s\t%s\n", keybuffer, print_time(seen->time_stamp)); + break; } } store_reset(reset_point); |