summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/doc-txt/NewStuff6
-rw-r--r--doc/doc-txt/experimental-spec.txt75
-rw-r--r--src/src/EDITME4
-rw-r--r--src/src/config.h.defaults1
-rw-r--r--src/src/exim.c2
-rw-r--r--src/src/expand.c332
-rw-r--r--src/src/globals.c3
-rw-r--r--src/src/globals.h3
-rw-r--r--src/src/macro_predef.c5
-rw-r--r--test/confs/462087
-rw-r--r--test/log/462016
-rw-r--r--test/mail/4620.CALLER56
-rw-r--r--test/scripts/4620-SRS/462016
-rw-r--r--test/scripts/4620-SRS/REQUIRES2
14 files changed, 559 insertions, 49 deletions
diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff
index 4caa897e3..fd1ab8b3d 100644
--- a/doc/doc-txt/NewStuff
+++ b/doc/doc-txt/NewStuff
@@ -6,6 +6,12 @@ Before a formal release, there may be quite a lot of detail so that people can
test from the snapshots or the Git before the documentation is updated. Once
the documentation is updated, this file is reduced to a short list.
+Version 4.next
+--------------
+
+ 1. EXPERIMENTAL_SRS_NATIVE optional build feature. See the experimental.spec
+ file.
+
Version 4.93
------------
diff --git a/doc/doc-txt/experimental-spec.txt b/doc/doc-txt/experimental-spec.txt
index 373d9dc4c..e9a557aec 100644
--- a/doc/doc-txt/experimental-spec.txt
+++ b/doc/doc-txt/experimental-spec.txt
@@ -292,10 +292,11 @@ These four steps are explained in more details below.
-SRS (Sender Rewriting Scheme) Support
+SRS (Sender Rewriting Scheme) Support (using libsrs_alt)
--------------------------------------------------------------
+See also below, for an alternative native support implementation.
-Exiscan currently includes SRS support via Miles Wilton's
+Exim currently includes SRS support via Miles Wilton's
libsrs_alt library. The current version of the supported
library is 0.5, there are reports of 1.0 working.
@@ -343,6 +344,76 @@ For configuration information see https://github.com/Exim/exim/wiki/SRS .
+SRS (Sender Rewriting Scheme) Support (native)
+--------------------------------------------------------------
+This is less full-featured than the libsrs_alt version above.
+
+The Exim build needs to be done with this in Local/Makefile:
+EXPERIMENTAL_SRS_NATIVE=yes
+
+The following are provided:
+- an expansion item "srs_encode"
+ This takes three arguments:
+ - a site SRS secret
+ - the return_path
+ - the pre-forwarding domain
+
+- an expansion condition "inbound_srs"
+ This takes two arguments: the local_part to check, and a site SRS secret.
+ If the secret is zero-length, only the pattern of the local_part is checked.
+ The $srs_recipient variable is set as a side-effect.
+
+- an expansion variable $srs_recipient
+ This gets the original return_path encoded in the SRS'd local_part
+
+- predefined macros _HAVE_SRS and _HAVE_NATIVE_SRS
+
+Sample usage:
+
+ #macro
+ SRS_SECRET = <pick something unique for your site for this>
+
+ #routers
+
+ outbound:
+ driver = dnslookup
+ # if outbound, and forwarding has been done, use an alternate transport
+ domains = ! +my_domains
+ transport = ${if eq {$local_part@$domain} \
+ {$original_local_part@$original_domain} \
+ {remote_smtp} {remote_forwarded_smtp}}
+
+ inbound_srs:
+ driver = redirect
+ senders = :
+ domains = +my_domains
+ # detect inbound bounces which are SRS'd, and decode them
+ condition = ${if inbound_srs {$local_part} {SRS_SECRET}}
+ data = $srs_recipient
+
+ inbound_srs_failure:
+ driver = redirect
+ senders = :
+ domains = +my_domains
+ # detect inbound bounces which look SRS'd but are invalid
+ condition = ${if inbound_srs {$local_part} {}}
+ allow_fail
+ data = :fail: Invalid SRS recipient address
+
+ #... further routers here
+
+
+ # transport; should look like the non-forward outbound
+ # one, plus the max_rcpt and return_path options
+ remote_forwarded_smtp:
+ driver = smtp
+ # modify the envelope from, for mails that we forward
+ max_rcpt = 1
+ return_path = ${srs_encode {SRS_SECRET} {$return_path} {$original_domain}}
+
+
+
+
DCC Support
--------------------------------------------------------------
Distributed Checksum Clearinghouse; http://www.rhyolite.com/dcc/
diff --git a/src/src/EDITME b/src/src/EDITME
index 45af21063..1d916a559 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -590,6 +590,10 @@ DISABLE_MAL_MKS=yes
# CFLAGS += -I/usr/local/include
# LDFLAGS += -lsrs_alt
+# Uncomment the following lines to add SRS (Sender rewriting scheme) support
+# using only native facilities.
+# EXPERIMENTAL_SRS_NATIVE=yes
+
# Uncomment the following line to add DMARC checking capability, implemented
# using libopendmarc libraries. You must have SPF and DKIM support enabled also.
# SUPPORT_DMARC=yes
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index b94b36866..84837d527 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -204,6 +204,7 @@ Do not put spaces between # and the 'define'.
#define EXPERIMENTAL_LMDB
#define EXPERIMENTAL_QUEUEFILE
#define EXPERIMENTAL_SRS
+#define EXPERIMENTAL_SRS_NATIVE
#define EXPERIMENTAL_TLS_RESUME
diff --git a/src/src/exim.c b/src/src/exim.c
index 68734e35c..1bd49a0d4 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -929,7 +929,7 @@ fprintf(fp, "Support for:");
#ifdef EXPERIMENTAL_QUEUEFILE
fprintf(fp, " Experimental_QUEUEFILE");
#endif
-#ifdef EXPERIMENTAL_SRS
+#if defined(EXPERIMENTAL_SRS) || defined(EXPERIMENTAL_SRS_NATIVE)
fprintf(fp, " Experimental_SRS");
#endif
#ifdef EXPERIMENTAL_ARC
diff --git a/src/src/expand.c b/src/src/expand.c
index 8be10c14f..e30756123 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -129,6 +129,9 @@ static uschar *item_table[] = {
US"run",
US"sg",
US"sort",
+#ifdef EXPERIMENTAL_SRS_NATIVE
+ US"srs_encode",
+#endif
US"substr",
US"tr" };
@@ -160,6 +163,9 @@ enum {
EITEM_RUN,
EITEM_SG,
EITEM_SORT,
+#ifdef EXPERIMENTAL_SRS_NATIVE
+ EITEM_SRS_ENCODE,
+#endif
EITEM_SUBSTR,
EITEM_TR };
@@ -323,6 +329,9 @@ static uschar *cond_table[] = {
US"gei",
US"gt",
US"gti",
+#ifdef EXPERIMENTAL_SRS_NATIVE
+ US"inbound_srs",
+#endif
US"inlist",
US"inlisti",
US"isip",
@@ -373,6 +382,9 @@ enum {
ECOND_STR_GEI,
ECOND_STR_GT,
ECOND_STR_GTI,
+#ifdef EXPERIMENTAL_SRS_NATIVE
+ ECOND_INBOUND_SRS,
+#endif
ECOND_INLIST,
ECOND_INLISTI,
ECOND_ISIP,
@@ -736,7 +748,11 @@ static var_entry var_table[] = {
{ "srs_db_key", vtype_stringptr, &srs_db_key },
{ "srs_orig_recipient", vtype_stringptr, &srs_orig_recipient },
{ "srs_orig_sender", vtype_stringptr, &srs_orig_sender },
+#endif
+#if defined(EXPERIMENTAL_SRS) || defined(EXPERIMENTAL_SRS_NATIVE)
{ "srs_recipient", vtype_stringptr, &srs_recipient },
+#endif
+#ifdef EXPERIMENTAL_SRS
{ "srs_status", vtype_stringptr, &srs_status },
#endif
{ "thisaddress", vtype_stringptr, &filter_thisaddress },
@@ -2293,6 +2309,127 @@ return chop_match(name, cond_table, nelem(cond_table));
/*************************************************
+* Handle MD5 or SHA-1 computation for HMAC *
+*************************************************/
+
+/* These are some wrapping functions that enable the HMAC code to be a bit
+cleaner. A good compiler will spot the tail recursion.
+
+Arguments:
+ type HMAC_MD5 or HMAC_SHA1
+ remaining are as for the cryptographic hash functions
+
+Returns: nothing
+*/
+
+static void
+chash_start(int type, void * base)
+{
+if (type == HMAC_MD5)
+ md5_start((md5 *)base);
+else
+ sha1_start((hctx *)base);
+}
+
+static void
+chash_mid(int type, void * base, const uschar * string)
+{
+if (type == HMAC_MD5)
+ md5_mid((md5 *)base, string);
+else
+ sha1_mid((hctx *)base, string);
+}
+
+static void
+chash_end(int type, void * base, const uschar * string, int length,
+ uschar * digest)
+{
+if (type == HMAC_MD5)
+ md5_end((md5 *)base, string, length, digest);
+else
+ sha1_end((hctx *)base, string, length, digest);
+}
+
+
+
+
+/* Do an hmac_md5. The result is _not_ nul-terminated, and is sized as
+the smaller of a full hmac_md5 result (16 bytes) or the supplied output buffer.
+
+Arguments:
+ key encoding key, nul-terminated
+ src data to be hashed, nul-terminated
+ buf output buffer
+ len size of output buffer
+*/
+
+static void
+hmac_md5(const uschar * key, const uschar * src, uschar * buf, unsigned len)
+{
+md5 md5_base;
+const uschar * keyptr;
+uschar * p;
+unsigned int keylen;
+
+#define MD5_HASHLEN 16
+#define MD5_HASHBLOCKLEN 64
+
+uschar keyhash[MD5_HASHLEN];
+uschar innerhash[MD5_HASHLEN];
+uschar finalhash[MD5_HASHLEN];
+uschar innerkey[MD5_HASHBLOCKLEN];
+uschar outerkey[MD5_HASHBLOCKLEN];
+
+keyptr = key;
+keylen = Ustrlen(keyptr);
+
+/* If the key is longer than the hash block length, then hash the key
+first */
+
+if (keylen > MD5_HASHBLOCKLEN)
+ {
+ chash_start(HMAC_MD5, &md5_base);
+ chash_end(HMAC_MD5, &md5_base, keyptr, keylen, keyhash);
+ keyptr = keyhash;
+ keylen = MD5_HASHLEN;
+ }
+
+/* Now make the inner and outer key values */
+
+memset(innerkey, 0x36, MD5_HASHBLOCKLEN);
+memset(outerkey, 0x5c, MD5_HASHBLOCKLEN);
+
+for (int i = 0; i < keylen; i++)
+ {
+ innerkey[i] ^= keyptr[i];
+ outerkey[i] ^= keyptr[i];
+ }
+
+/* Now do the hashes */
+
+chash_start(HMAC_MD5, &md5_base);
+chash_mid(HMAC_MD5, &md5_base, innerkey);
+chash_end(HMAC_MD5, &md5_base, src, Ustrlen(src), innerhash);
+
+chash_start(HMAC_MD5, &md5_base);
+chash_mid(HMAC_MD5, &md5_base, outerkey);
+chash_end(HMAC_MD5, &md5_base, innerhash, MD5_HASHLEN, finalhash);
+
+/* Encode the final hash as a hex string, limited by output buffer size */
+
+p = buf;
+for (int i = 0, j = len; i < MD5_HASHLEN; i++)
+ {
+ if (j-- <= 0) break;
+ *p++ = hex_digits[(finalhash[i] & 0xf0) >> 4];
+ if (j-- <= 0) break;
+ *p++ = hex_digits[finalhash[i] & 0x0f];
+ }
+return;
+}
+
+
+/*************************************************
* Read and evaluate a condition *
*************************************************/
@@ -3229,6 +3366,100 @@ switch(cond_type = identify_operator(&s, &opname))
return s;
}
+#ifdef EXPERIMENTAL_SRS_NATIVE
+ case ECOND_INBOUND_SRS:
+ /* ${if inbound_srs {local_part}{secret} {yes}{no}} */
+ {
+ uschar * sub[2];
+ const pcre * re;
+ int ovec[3*(4+1)];
+ int n;
+ uschar cksum[4];
+ BOOL boolvalue = FALSE;
+
+ switch(read_subs(sub, 2, 2, CUSS &s, yield == NULL, FALSE, US"inbound_srs", resetok))
+ {
+ case 1: expand_string_message = US"too few arguments or bracketing "
+ "error for inbound_srs";
+ case 2:
+ case 3: return NULL;
+ }
+
+ /* Match the given local_part against the SRS-encoded pattern */
+
+ re = regex_must_compile(US"^(?i)SRS0=([^=]+)=([A-Z2-7]+)=([^=]*)=(.*)$",
+ TRUE, FALSE);
+ if (pcre_exec(re, NULL, CS sub[0], Ustrlen(sub[0]), 0, PCRE_EOPT,
+ ovec, nelem(ovec)) < 0)
+ {
+ DEBUG(D_expand) debug_printf("no match for SRS'd local-part pattern\n");
+ goto srs_result;
+ }
+
+ /* Side-effect: record the decoded recipient */
+
+ srs_recipient = string_sprintf("%.*S@%.*S", /* lowercased */
+ ovec[9]-ovec[8], sub[0] + ovec[8], /* substring 4 */
+ ovec[7]-ovec[6], sub[0] + ovec[6]); /* substring 3 */
+
+ /* If a zero-length secret was given, we're done. Otherwise carry on
+ and validate the given SRS local_part againt our secret. */
+
+ if (!*sub[1])
+ {
+ boolvalue = TRUE;
+ goto srs_result;
+ }
+
+ /* check the timestamp */
+ {
+ struct timeval now;
+ uschar * ss = sub[0] + ovec[4]; /* substring 2, the timestamp */
+ long d;
+
+ gettimeofday(&now, NULL);
+ now.tv_sec /= 86400; /* days since epoch */
+
+ /* Decode substring 2 from base32 to a number */
+
+ for (d = 0, n = ovec[5]-ovec[4]; n; n--)
+ {
+ uschar * t = Ustrchr(base32_chars, *ss++);
+ d = d * 32 + (t - base32_chars);
+ }
+
+ if (((now.tv_sec - d) & 0x3ff) > 10) /* days since SRS generated */
+ {
+ DEBUG(D_expand) debug_printf("SRS too old\n");
+ goto srs_result;
+ }
+ }
+
+ /* check length of substring 1, the offered checksum */
+
+ if (ovec[3]-ovec[2] != 4)
+ {
+ DEBUG(D_expand) debug_printf("SRS checksum wrong size\n");
+ goto srs_result;
+ }
+
+ /* Hash the address with our secret, and compare that computed checksum
+ with the one extracted from the arg */
+
+ hmac_md5(sub[1], srs_recipient, cksum, sizeof(cksum));
+ if (Ustrncmp(cksum, sub[0] + ovec[2], 4) != 0)
+ {
+ DEBUG(D_expand) debug_printf("SRS checksum mismatch\n");
+ goto srs_result;
+ }
+ boolvalue = TRUE;
+
+srs_result:
+ if (yield) *yield = (boolvalue == testfor);
+ return s;
+ }
+#endif /*EXPERIMENTAL_SRS_NATIVE*/
+
/* Unknown condition */
default:
@@ -3501,51 +3732,6 @@ FAILED:
-/*************************************************
-* Handle MD5 or SHA-1 computation for HMAC *
-*************************************************/
-
-/* These are some wrapping functions that enable the HMAC code to be a bit
-cleaner. A good compiler will spot the tail recursion.
-
-Arguments:
- type HMAC_MD5 or HMAC_SHA1
- remaining are as for the cryptographic hash functions
-
-Returns: nothing
-*/
-
-static void
-chash_start(int type, void *base)
-{
-if (type == HMAC_MD5)
- md5_start((md5 *)base);
-else
- sha1_start((hctx *)base);
-}
-
-static void
-chash_mid(int type, void *base, uschar *string)
-{
-if (type == HMAC_MD5)
- md5_mid((md5 *)base, string);
-else
- sha1_mid((hctx *)base, string);
-}
-
-static void
-chash_end(int type, void *base, uschar *string, int length, uschar *digest)
-{
-if (type == HMAC_MD5)
- md5_end((md5 *)base, string, length, digest);
-else
- sha1_end((hctx *)base, string, length, digest);
-}
-
-
-
-
-
/********************************************************
* prvs: Get last three digits of days since Jan 1, 1970 *
********************************************************/
@@ -6668,6 +6854,62 @@ while (*s != 0)
}
continue;
}
+
+#ifdef EXPERIMENTAL_SRS_NATIVE
+ case EITEM_SRS_ENCODE:
+ /* ${srs_encode {secret} {return_path} {orig_domain}} */
+ {
+ uschar * sub[3];
+ uschar cksum[4];
+
+ switch (read_subs(sub, 3, 3, CUSS &s, skipping, TRUE, name, &resetok))
+ {
+ case 1: goto EXPAND_FAILED_CURLY;
+ case 2:
+ case 3: goto EXPAND_FAILED;
+ }
+
+ yield = string_catn(yield, US"SRS0=", 5);
+
+ /* ${l_4:${hmac{md5}{SRS_SECRET}{${lc:$return_path}}}}= */
+ hmac_md5(sub[0], string_copylc(sub[1]), cksum, sizeof(cksum));
+ yield = string_catn(yield, cksum, sizeof(cksum));
+ yield = string_catn(yield, US"=", 1);
+
+ /* ${base32:${eval:$tod_epoch/86400&0x3ff}}= */
+ {
+ struct timeval now;
+ unsigned long i;
+ gstring * g = NULL;
+
+ gettimeofday(&now, NULL);
+ for (unsigned long i = (now.tv_sec / 86400) & 0x3ff; i; i >>= 5)
+ g = string_catn(g, &base32_chars[i & 0x1f], 1);
+ if (g) while (g->ptr > 0)
+ yield = string_catn(yield, &g->s[--g->ptr], 1);
+ }
+ yield = string_catn(yield, US"=", 1);
+
+ /* ${domain:$return_path}=${local_part:$return_path} */
+ {
+ int start, end, domain;
+ uschar * t = parse_extract_address(sub[1], &expand_string_message,
+ &start, &end, &domain, FALSE);
+ if (!t)
+ goto EXPAND_FAILED;
+
+ if (domain > 0) yield = string_cat(yield, t + domain);
+ yield = string_catn(yield, US"=", 1);
+ yield = domain > 0
+ ? string_catn(yield, t, domain - 1) : string_cat(yield, t);
+ }
+
+ /* @$original_domain */
+ yield = string_catn(yield, US"@", 1);
+ yield = string_cat(yield, sub[2]);
+ continue;
+ }
+#endif /*EXPERIMENTAL_SRS_NATIVE*/
} /* EITEM_* switch */
/* Control reaches here if the name is not recognized as one of the more
diff --git a/src/src/globals.c b/src/src/globals.c
index 302e18eb3..3540a9eba 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1498,6 +1498,9 @@ uschar *srs_recipient = NULL;
uschar *srs_secrets = NULL;
uschar *srs_status = NULL;
#endif
+#ifdef EXPERIMENTAL_SRS_NATIVE
+uschar *srs_recipient = NULL;
+#endif
int string_datestamp_offset= -1;
int string_datestamp_length= 0;
int string_datestamp_type = -1;
diff --git a/src/src/globals.h b/src/src/globals.h
index 27a4bd9e0..ffc633f60 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -986,6 +986,9 @@ extern uschar *srs_status; /* SRS staus */
extern BOOL srs_usehash; /* SRS use hash flag */
extern BOOL srs_usetimestamp; /* SRS use timestamp flag */
#endif
+#ifdef EXPERIMENTAL_SRS_NATIVE
+extern uschar *srs_recipient; /* SRS recipient */
+#endif
extern BOOL strict_acl_vars; /* ACL variables have to be set before being used */
extern int string_datestamp_offset;/* After insertion by string_format */
extern int string_datestamp_length;/* After insertion by string_format */
diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index e96fef938..e20ae89fe 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -182,9 +182,12 @@ due to conflicts with other common macros. */
#ifdef SUPPORT_SPF
builtin_macro_create(US"_HAVE_SPF");
#endif
-#ifdef EXPERIMENTAL_SRS
+#if defined(EXPERIMENTAL_SRS) || defined(EXPERIMENTAL_SRS_NATIVE)
builtin_macro_create(US"_HAVE_SRS");
#endif
+#if defined(EXPERIMENTAL_SRS_NATIVE)
+ builtin_macro_create(US"_HAVE_NATIVE_SRS"); /* beware clash with _HAVE_SRS */
+#endif
#ifdef EXPERIMENTAL_ARC
builtin_macro_create(US"_HAVE_ARC");
#endif
diff --git a/test/confs/4620 b/test/confs/4620
new file mode 100644
index 000000000..5b1175a53
--- /dev/null
+++ b/test/confs/4620
@@ -0,0 +1,87 @@
+# Exim test configuration 4620
+
+.include DIR/aux-var/std_conf_prefix
+
+SRS_SECRET = mysecret
+
+# ----- Main settings -----
+
+acl_smtp_rcpt = accept
+
+domainlist local_domains = test.ex
+domainlist remotesite_domains = remote.ex
+
+log_selector = +all_parents +received_recipients
+queue_only
+
+# ----- Routers -----
+
+begin routers
+
+remote_bouncer:
+ driver = redirect
+ condition = ${if eq {$sender_host_address}{127.0.0.1}}
+ data = :fail: account disabled
+ allow_fail
+
+external:
+ driver = manualroute
+ domains = !+local_domains
+ route_list = remote.ex 127.0.0.1::PORT_S
+ self = send
+ transport = ${if eq {$local_part@$domain} {$original_local_part@$original_domain} \
+ {to_external} {forwarded_external}}
+
+inbound_srs:
+ driver = redirect
+ senders = :
+ domains = +local_domains
+ # detect inbound bounces which are SRS'd, and decode them
+ condition = ${if inbound_srs {$local_part} {SRS_SECRET}}
+ data = $srs_recipient
+
+inbound_srs_failure:
+ driver = redirect
+ senders = :
+ domains = +local_domains
+ # detect inbound bounces which look SRS'd but are invalid
+ condition = ${if inbound_srs {$local_part} {}}
+ allow_fail
+ data = :fail: Invalid SRS recipient address
+
+
+local_redirect:
+ driver = redirect
+ local_parts = redirect
+ data = remote_user@remote.ex
+
+local:
+ driver = accept
+ transport = appendfile
+
+
+# ----- Transports -----
+
+begin transports
+
+to_external:
+ driver = smtp
+
+forwarded_external:
+ driver = smtp
+ max_rcpt = 1
+ return_path = ${srs_encode {SRS_SECRET} {$return_path} {$original_domain}}
+
+
+appendfile:
+ driver = appendfile
+ file = DIR/test-mail/$local_part
+ user = CALLER
+
+# ----- Retry -----
+
+begin retry
+
+* * F,5d,1d
+
+# End
diff --git a/test/log/4620 b/test/log/4620
new file mode 100644
index 000000000..0a98c2712
--- /dev/null
+++ b/test/log/4620
@@ -0,0 +1,16 @@
+1999-03-02 09:44:33 10HmaX-0005vi-00 <= CALLER@the.local.host.name U=CALLER P=local S=sss for redirect@test.ex
+1999-03-02 09:44:33 10HmaX-0005vi-00 => remote_user@remote.ex <redirect@test.ex> R=external T=forwarded_external H=127.0.0.1 [127.0.0.1] C="250 OK id=10HmaY-0005vi-00"
+1999-03-02 09:44:33 10HmaX-0005vi-00 Completed
+1999-03-02 09:44:33 Start queue run: pid=pppp
+1999-03-02 09:44:33 10HmaY-0005vi-00 ** remote_user@remote.ex R=remote_bouncer: account disabled
+1999-03-02 09:44:33 10HmaZ-0005vi-00 <= <> R=10HmaY-0005vi-00 U=EXIMUSER P=local S=sss for SRS0=12a1=yg=the.local.host.name=CALLER@test.ex
+1999-03-02 09:44:33 10HmaY-0005vi-00 Completed
+1999-03-02 09:44:33 End queue run: pid=pppp
+1999-03-02 09:44:33 Start queue run: pid=pppp
+1999-03-02 09:44:33 10HmaZ-0005vi-00 => CALLER <SRS0=12a1=yg=the.local.host.name=CALLER@test.ex> R=local T=appendfile
+1999-03-02 09:44:33 10HmaZ-0005vi-00 Completed
+1999-03-02 09:44:33 End queue run: pid=pppp
+
+******** SERVER ********
+1999-03-02 09:44:33 exim x.yz daemon started: pid=pppp, no queue runs, listening for SMTP on [127.0.0.1]:PORT_S
+1999-03-02 09:44:33 10HmaY-0005vi-00 <= SRS0=12a1=yg=the.local.host.name=CALLER@test.ex H=localhost (the.local.host.name) [127.0.0.1] P=esmtp S=sss id=E10HmaX-0005vi-00@the.local.host.name for remote_user@remote.ex
diff --git a/test/mail/4620.CALLER b/test/mail/4620.CALLER
new file mode 100644
index 000000000..b0a1372da
--- /dev/null
+++ b/test/mail/4620.CALLER
@@ -0,0 +1,56 @@
+From MAILER-DAEMON Tue Mar 02 09:44:33 1999
+Received: from EXIMUSER by the.local.host.name with local (Exim x.yz)
+ id 10HmaZ-0005vi-00
+ for SRS0=12a1=yg=the.local.host.name=CALLER@test.ex; Tue, 2 Mar 1999 09:44:33 +0000
+X-Failed-Recipients: remote_user@remote.ex
+Auto-Submitted: auto-replied
+From: Mail Delivery System <Mailer-Daemon@the.local.host.name>
+To: SRS0=12a1=yg=the.local.host.name=CALLER@test.ex
+References: <E10HmaX-0005vi-00@the.local.host.name>
+Content-Type: multipart/report; report-type=delivery-status; boundary=NNNNNNNNNN-eximdsn-MMMMMMMMMM
+MIME-Version: 1.0
+Subject: Mail delivery failed: returning message to sender
+Message-Id: <E10HmaZ-0005vi-00@the.local.host.name>
+Date: Tue, 2 Mar 1999 09:44:33 +0000
+
+--NNNNNNNNNN-eximdsn-MMMMMMMMMM
+Content-type: text/plain; charset=us-ascii
+
+This message was created automatically by mail delivery software.
+
+A message that you sent could not be delivered to one or more of its
+recipients. This is a permanent error. The following address(es) failed:
+
+ remote_user@remote.ex
+ account disabled
+
+--NNNNNNNNNN-eximdsn-MMMMMMMMMM
+Content-type: message/delivery-status
+
+Reporting-MTA: dns; the.local.host.name
+
+Action: failed
+Final-Recipient: rfc822;remote_user@remote.ex
+Status: 5.0.0
+
+--NNNNNNNNNN-eximdsn-MMMMMMMMMM
+Content-type: message/rfc822
+
+Return-path: <SRS0=12a1=yg=the.local.host.name=CALLER@test.ex>
+Received: from localhost ([127.0.0.1] helo=the.local.host.name)
+ by the.local.host.name with esmtp (Exim x.yz)
+ (envelope-from <SRS0=12a1=yg=the.local.host.name=CALLER@test.ex>)
+ id 10HmaY-0005vi-00
+ for remote_user@remote.ex; Tue, 2 Mar 1999 09:44:33 +0000
+Received: from CALLER by the.local.host.name with local (Exim x.yz)
+ (envelope-from <CALLER@the.local.host.name>)
+ id 10HmaX-0005vi-00
+ for redirect@test.ex; Tue, 2 Mar 1999 09:44:33 +0000
+Message-Id: <E10HmaX-0005vi-00@the.local.host.name>
+From: CALLER_NAME <CALLER@the.local.host.name>
+Date: Tue, 2 Mar 1999 09:44:33 +0000
+
+Message body
+
+--NNNNNNNNNN-eximdsn-MMMMMMMMMM--
+
diff --git a/test/scripts/4620-SRS/4620 b/test/scripts/4620-SRS/4620
new file mode 100644
index 000000000..4a126b8b9
--- /dev/null
+++ b/test/scripts/4620-SRS/4620
@@ -0,0 +1,16 @@
+# SRS native implementation
+#
+exim -bd -DSERVER=server -oX 127.0.0.1:PORT_S
+****
+# Inject a message; will be passed on to remote and queued there
+exim -odi redirect@test.ex
+Message body
+****
+# Run the queue for the remote, will generate bounce which is queued
+exim -q
+****
+# Run the queue for the remote, will send bounce to origin
+exim -q
+****
+#
+killdaemon
diff --git a/test/scripts/4620-SRS/REQUIRES b/test/scripts/4620-SRS/REQUIRES
new file mode 100644
index 000000000..7286713d6
--- /dev/null
+++ b/test/scripts/4620-SRS/REQUIRES
@@ -0,0 +1,2 @@
+support Experimental_SRS
+feature _HAVE_NATIVE_SRS