diff -r -U3 exim-4.30/doc/spec.txt exim-4.30.helo.cache/doc/spec.txt
--- exim-4.30/doc/spec.txt	Mon Dec  1 10:15:41 2003
+++ exim-4.30.helo.cache/doc/spec.txt	Wed Mar  3 10:26:18 2004
@@ -8282,6 +8282,12 @@
   general expansion strings, and provokes an 'unknown variable' error if
   encountered.
 
+$previous_sender_helo_name: When a message is received from a remote host that
+  has issued a HELO or EHLO command, the previously cached value of the
+  argument is placed in this variable. If there was no previously cached value,
+  or "helo_cache" is not set in the configuration then the variable is not
+  defined. The behaviour of this variable applies only to remote SMTP input.
+
 $primary_hostname: The value set in the configuration file, or read by the
   "uname()" function. If "uname()" returns a single-component name, Exim calls
   "gethostbyname()" (or "getipnodebyname()" where available) in an attempt to
@@ -9851,6 +9857,19 @@
       helo_allow_chars = _
 
     Note that the value is one string, not a list.
+
+helo_cache                    Type: boolean                     Default: false
+
+    If this option is set, the arguments received from remote hosts to HELO
+    and EHLO commands are cached. Previously cached values of the arguments
+    are available in the "$previous_sender_helo_name". The IP address of the
+    sending host is used as the key.
+
+helo_cache_expire             Type: time                        Default: 1h
+
+    When "helo_cache" is used to cache arguments to HELO/EHLO commands, this
+    value defines for how long Exim should remember the values after the most
+    recent HELO/EHLO command from a particular host.
 
 helo_lookup_domains           Type: domain list*                Default: @:@[]
 
Only in exim-4.30.helo.cache/src: clarauser-exim-4.30.diff
diff -r -U3 exim-4.30/src/dbstuff.h exim-4.30.helo.cache/src/dbstuff.h
--- exim-4.30/src/dbstuff.h	Mon Dec  1 10:15:41 2003
+++ exim-4.30.helo.cache/src/dbstuff.h	Tue Mar  2 20:30:31 2004
@@ -628,5 +628,16 @@
   int    count;           /* Reserved for possible connection count */
 } dbdata_serialize;
 
+/* This structure records how hosts identify themselves by HELO/EHLO. The key
+is the IP address of the connecting host. */
+
+typedef struct {
+  time_t time_stamp;
+  /*************/
+  BOOL   expired;         /* Item has expired */
+  uschar helo[1];         /* Host identifier  */
+} dbdata_helo_cache;
+
+
 
 /* End of dbstuff.h */
diff -r -U3 exim-4.30/src/exim_dbutil.c exim-4.30.helo.cache/src/exim_dbutil.c
--- exim-4.30/src/exim_dbutil.c	Mon Dec  1 10:15:41 2003
+++ exim-4.30.helo.cache/src/exim_dbutil.c	Wed Mar  3 10:15:42 2004
@@ -65,6 +65,7 @@
 #define type_wait    2
 #define type_misc    3
 #define type_callout 4
+#define type_helo    5
 
 
 
@@ -111,7 +112,7 @@
 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\n");
+printf("       <database-name> = retry | misc | wait-<transport-name> | callout | helo\n");
 exit(1);
 }
 
@@ -133,6 +134,7 @@
   if (Ustrcmp(argv[2], "misc") == 0) return type_misc;
   if (Ustrncmp(argv[2], "wait-", 5) == 0) return type_wait;
   if (Ustrcmp(argv[2], "callout") == 0) return type_callout;
+  if (Ustrcmp(argv[2], "helo") == 0) return type_helo;
   }
 usage(name, options);
 return -1;              /* Never obeyed */
@@ -534,6 +536,7 @@
   dbdata_retry *retry;
   dbdata_wait *wait;
   dbdata_callout_cache *callout;
+  dbdata_helo_cache *helo;
   int count_bad = 0;
   int i, length;
   uschar *t;
@@ -659,6 +662,11 @@
           print_cache(callout->random_result));
         }
 
+      break;
+
+      case type_helo:
+      helo = (dbdata_helo_cache *)value;
+      printf("  %s %s\n", keybuffer, helo->helo);
       break;
       }
     store_reset(value);
diff -r -U3 exim-4.30/src/expand.c exim-4.30.helo.cache/src/expand.c
--- exim-4.30/src/expand.c	Mon Dec  1 10:15:41 2003
+++ exim-4.30.helo.cache/src/expand.c	Wed Mar  3 10:28:39 2004
@@ -276,6 +276,7 @@
   { "parent_domain",       vtype_stringptr,   &deliver_domain_parent },
   { "parent_local_part",   vtype_stringptr,   &deliver_localpart_parent },
   { "pid",                 vtype_pid,         NULL },
+  { "previous_sender_helo_name",vtype_stringptr, &previous_sender_helo_name },
   { "primary_hostname",    vtype_stringptr,   &primary_hostname },
   { "qualify_domain",      vtype_stringptr,   &qualify_domain_sender },
   { "qualify_recipient",   vtype_stringptr,   &qualify_domain_recipient },
diff -r -U3 exim-4.30/src/globals.c exim-4.30.helo.cache/src/globals.c
--- exim-4.30/src/globals.c	Mon Dec  1 10:15:41 2003
+++ exim-4.30.helo.cache/src/globals.c	Wed Mar  3 10:19:20 2004
@@ -490,6 +490,8 @@
 BOOL    header_rewritten       = FALSE;
 uschar *helo_accept_junk_hosts = NULL;
 uschar *helo_allow_chars       = US"";
+BOOL    helo_cache             = FALSE;
+int     helo_cache_expire      = 60*60;         /* 1 hour */
 uschar *helo_lookup_domains    = US"@ : @[]";
 uschar *helo_try_verify_hosts  = NULL;
 BOOL    helo_verified          = FALSE;
@@ -629,6 +631,7 @@
 			   "\0<--------------Space to patch pid_file_path->";
 uschar *pipelining_advertise_hosts = US"*";
 BOOL    preserve_message_logs  = FALSE;
+uschar *previous_sender_helo_name = NULL;
 uschar *primary_hostname       = NULL;
 uschar *primary_hostname_lc    = NULL;
 BOOL    print_topbitchars      = FALSE;
diff -r -U3 exim-4.30/src/globals.h exim-4.30.helo.cache/src/globals.h
--- exim-4.30/src/globals.h	Mon Dec  1 10:15:41 2003
+++ exim-4.30.helo.cache/src/globals.h	Wed Mar  3 10:07:57 2004
@@ -277,6 +277,8 @@
 extern BOOL    header_rewritten;       /* TRUE if header changed by router */
 extern uschar *helo_accept_junk_hosts; /* Allowed to use junk arg */
 extern uschar *helo_allow_chars;       /* Rogue chars to allow in HELO/EHLO */
+extern BOOL    helo_cache;             /* Whether to cache arguments */
+extern int     helo_cache_expire;      /* When to expire cached helo data */
 extern uschar *helo_lookup_domains;    /* If these given, lookup host name */
 extern uschar *helo_try_verify_hosts;  /* Soft check HELO argument for these */
 extern BOOL    helo_verified;          /* True if HELO verified */
@@ -381,6 +383,7 @@
 extern uschar *percent_hack_domains;   /* Local domains for which '% operates */
 extern uschar *pid_file_path;          /* For writing daemon pids */
 extern uschar *pipelining_advertise_hosts; /* As it says */
+extern uschar *previous_sender_helo_name; /* Value of previous EHLO/HELO arg */
 extern BOOL    preserve_message_logs;  /* Save msglog files */
 extern uschar *primary_hostname;       /* Primary name of this computer */
 extern uschar *primary_hostname_lc;    /* ... lower cased */
diff -r -U3 exim-4.30/src/readconf.c exim-4.30.helo.cache/src/readconf.c
--- exim-4.30/src/readconf.c	Mon Dec  1 10:15:41 2003
+++ exim-4.30.helo.cache/src/readconf.c	Wed Mar  3 10:08:12 2004
@@ -200,6 +200,8 @@
   { "headers_charset",          opt_stringptr,   &headers_charset },
   { "helo_accept_junk_hosts",   opt_stringptr,   &helo_accept_junk_hosts },
   { "helo_allow_chars",         opt_stringptr,   &helo_allow_chars },
+  { "helo_cache",               opt_bool,        &helo_cache },
+  { "helo_cache_expire",        opt_time,        &helo_cache_expire },
   { "helo_lookup_domains",      opt_stringptr,   &helo_lookup_domains },
   { "helo_try_verify_hosts",    opt_stringptr,   &helo_try_verify_hosts },
   { "helo_verify_hosts",        opt_stringptr,   &helo_verify_hosts },
Only in exim-4.30/src: sieve.c.orig
diff -r -U3 exim-4.30/src/smtp_in.c exim-4.30.helo.cache/src/smtp_in.c
--- exim-4.30/src/smtp_in.c	Mon Dec  1 10:15:41 2003
+++ exim-4.30.helo.cache/src/smtp_in.c	Wed Mar  3 10:17:38 2004
@@ -644,7 +644,7 @@
 
 /* Check the format of a HELO line. The data for HELO/EHLO is supposed to be
 the domain name of the sending host, or an ip literal in square brackets. The
-arrgument is placed in sender_helo_name, which is in malloc store, because it
+argument is placed in sender_helo_name, which is in malloc store, because it
 must persist over multiple incoming messages. If helo_accept_junk is set, this
 host is permitted to send any old junk (needed for some broken hosts).
 Otherwise, helo_allow_chars can be used for rogue characters in general
@@ -723,6 +723,111 @@
 
 
 
+/******************************
+*  Store a helo cache record  *
+******************************/
+
+/* This function stores a helo cache record using the IP address as a string
+as the key and the argument to HELO/EHLO as the value.
+
+Arguments:
+   dbmfile        pointer to open database handler
+   address        IP address of sender
+   arg            argument to HELO/EHLO
+
+Returns:          nothing
+*/
+
+static void
+write_helo_cache(open_db *dbm_file, uschar *address, uschar *arg)
+{
+int message_length;
+dbdata_helo_cache *helo_record;
+
+/* Set up the helo cache record */
+message_length = strlen(arg);
+helo_record = store_get(sizeof(dbdata_helo_cache) + message_length);
+
+Ustrncpy(helo_record->helo, arg, message_length);
+helo_record->helo[message_length] = 0;
+
+DEBUG(D_receive)
+  {
+  debug_printf("Writing helo data cache item for %s\n", arg);
+  debug_printf("  helo argument: %s\n", helo_record->helo);
+  }
+
+(void)dbfn_write(dbm_file, address, helo_record,
+  sizeof(dbdata_helo_cache) + message_length);
+}
+
+
+
+
+
+/***************************************************
+*   Compare HELO line with previously cached value *
+****************************************************/
+
+/* Check the value of the HELO line against previously cached value.
+
+sender_helo_name is not modified by this function.
+previous_sender_helo_name contains the last cached HELO argument, if any.
+
+Mismatches between the two may be checked in ACLs.
+
+The cache file is updated with every call to this function.
+
+Argument:
+  s       the data portion of the line (already past any white space)
+
+Returns:  TRUE or FALSE
+*/
+
+static BOOL same_helo(uschar *address, char *smtp_data)
+{
+time_t now = time(NULL);
+open_db dbblock;
+open_db *dbm_file;
+dbdata_helo_cache *helo_record;
+BOOL yield;
+
+if ((dbm_file = dbfn_open(US"helo", O_RDWR, &dbblock, FALSE)) == NULL)
+  {
+  DEBUG(D_receive)
+    debug_printf("no HELO/EHLO cache data available\n");
+  return TRUE;
+  }
+helo_record = dbfn_read(dbm_file, address);
+
+/* If no matching record was found, or the record returned was too old
+then we yield as if the current argument and the previous argument do match. */
+if (helo_record == NULL || now - helo_record->time_stamp > helo_cache_expire)
+  {
+  previous_sender_helo_name = NULL;
+  yield = TRUE;
+  }
+
+/* The cached data is valid, so compare it to what was passed as the
+argument to HELO/EHLO. If the value does not match update the cache with
+the new value. */
+else
+  {
+  previous_sender_helo_name = string_copy_malloc(helo_record->helo);
+  if (Ustrlen(smtp_data) != Ustrlen(helo_record->helo) ||
+      Ustrcmp(smtp_data, helo_record->helo) != 0)
+    yield = FALSE;
+  else
+    yield = TRUE;
+  }
+
+/* Update the cache file with the (possibly new) HELO argument, but more
+importantly, the new timestamp. */
+write_helo_cache(dbm_file, address, smtp_data);
+dbfn_close(dbm_file);
+return yield;
+}
+
 /*************************************************
 *         Extract SMTP command option            *
 *************************************************/
@@ -2135,6 +2240,14 @@
 
       break;
       }
+
+    /* Compare the current HELO/EHLO value from this host to what the host
+     * identified itself as last time. */
+    if (sender_host_address && helo_cache &&
+        !same_helo(sender_host_address, smtp_data))
+      log_write(0, LOG_MAIN, "detected change in %s argument from %s "
+        "(%s -> %s)", hello, host_and_ident(FALSE), previous_sender_helo_name,
+        sender_helo_name);
 
     /* If sender_host_unknown is true, we have got here via the -bs interface,
     not called from inetd. Otherwise, we are running an IP connection and the
