threads / patch / 66592

patch, 2 partsFix imap-send with reordered message headers

Subject: [PATCH 0/2] Fix imap-send with reordered message headers

## tl;dr

git imap-send could not take patches fetched with b4 because it expected Date before Subject, while git send-email writes them the other way round. Read the story.

replies: 38people: 3as markdown or json

Harald Nordgren via GitGitGadget· Oct 19, 2026, 19:19 UTC · lore

git imap-send rejected patches sent with git send-email, such as ones fetched from lore with b4, because it expected Date before Subject.

Harald Nordgren (2):
  imap-send: avoid leaking the tunnel command
  imap-send: accept messages with Subject before Date
 imap-send.c           | 38 ++++++++++++++++++--------------------
 t/t9001-send-email.sh | 35 +++++++++++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 20 deletions(-)
base-commit: c51408889c3b372814bc20b9431c3b74d0d268eb
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2456%2FHaraldNordgren%2Ffix%2Fimap-send-header-order-v1
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2456/HaraldNordgren/fix/imap-send-header-order-v1
Pull-Request: https://github.com/git/git/pull/2456
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Oct 19, 2026, 19:19 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH 1/2] imap-send: avoid leaking the tunnel command

From: Harald Nordgren <haraldnordgren@gmail.com>

When imap.tunnel is set, imap_open_store() starts the tunnel but never releases the child_process used for it, which leaks its argument list. No test sets imap.tunnel, so the leak checker has not caught this.

Only the pipes to the tunnel are needed once it is running, so release the rest right after starting it.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c | 1 +
 1 file changed, 1 insertion(+)
Show changes to imap-send.c +1 −0
diff --git a/imap-send.c b/imap-send.c
index bf1d2cf74d..6f76bade35 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1176,6 +1176,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
 
 		imap->buf.sock.fd[0] = tunnel.out;
 		imap->buf.sock.fd[1] = tunnel.in;
+		child_process_clear(&tunnel);
 
 		imap_info("OK\n");
 	} else {
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Oct 19, 2026, 19:19 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH 2/2] imap-send: accept messages with Subject before Date

From: Harald Nordgren <haraldnordgren@gmail.com>

Patches sent with git send-email cannot be fed back into git imap-send after fetching them from the mailing list archive, for example with

    b4 mbox --single-message -o - <url> | git imap-send

which fails with "no messages found to send". imap-send only counts a message when its From, Date and Subject headers come in that order, and git send-email writes Subject before Date.

Accept the three headers in any order within the header block of each message. The copy that git send-email --imap-sent-folder pipes to imap-send then passes the same check, so the special case added for it in 04133f5bc4 (send-email: add ability to send a copy of sent emails to an IMAP folder, 2025-08-12) is no longer needed.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           | 37 +++++++++++++++++--------------------
 t/t9001-send-email.sh | 35 +++++++++++++++++++++++++++++++++++
 2 files changed, 52 insertions(+), 20 deletions(-)
Show changes to 2 files +52 −20

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index 6f76bade35..6563c8f693 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1453,32 +1453,29 @@ static void wrap_in_html(struct strbuf *msg)
 	*msg = buf;
 }
 
+static bool has_required_headers(const char *line)
+{
+	bool from = false, date = false, subject = false;
+
+	while ((line = strchr(line, '\n')) && *++line != '\n') {
+		if (starts_with(line, "From: "))
+			from = true;
+		else if (starts_with(line, "Date: "))
+			date = true;
+		else if (starts_with(line, "Subject: "))
+			subject = true;
+	}
+	return from && date && subject;
+}
+
 static int count_messages(struct strbuf *all_msgs)
 {
 	int count = 0;
 	char *p = all_msgs->buf;
 
 	while (1) {
-		if (starts_with(p, "From ")) {
-			if (starts_with(p, "From git-send-email")) {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p += 7;
-				p = strstr(p, "\nTo: ");
-				if (!p) break;
-				p += 5;
-				count++;
-			} else {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p = strstr(p+7, "\nDate: ");
-				if (!p) break;
-				p = strstr(p+7, "\nSubject: ");
-				if (!p) break;
-				p += 10;
-				count++;
-			}
-		}
+		if (starts_with(p, "From ") && has_required_headers(p))
+			count++;
 		p = strstr(p+5, "\nFrom ");
 		if (!p)
 			break;
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index d1393ef197..1f77c14315 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2863,4 +2863,39 @@ test_expect_success $PREREQ '--compose handles to headers' '
 	test_cmp expect msgtxt2.to
 '
 
+test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archive' '
+	write_script fake-imap-server <<-\EOF &&
+	printf "* PREAUTH [CAPABILITY IMAP4rev1 LITERAL+] ready\r\n"
+	printf "1 OK\r\n2 OK\r\n3 OK\r\n"
+	cat >/dev/null
+	EOF
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		HEAD^ &&
+	{
+		echo "From mboxrd@z Thu Jan  1 00:00:00 1970" &&
+		cat msgtxt1
+	} >archive.mbox &&
+	git imap-send --folder=INBOX <archive.mbox 2>err &&
+	test_grep "^Sending 1 message to INBOX folder" err &&
+	test_grep "(1/1) done" err
+'
+
+test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch' '
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		--imap-sent-folder=Sent \
+		HEAD^ 2>err &&
+	test_grep "^Sending 1 message to Sent folder" err &&
+	test_grep "(1/1) done" err
+'
+
 test_done
-- 
gitgitgadget
Junio C Hamano· Oct 19, 2026, 22:00 UTC · re: Harald Nordgren via GitGitGadget · lore

Re: [PATCH 2/2] imap-send: accept messages with Subject before Date

"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:
> which fails with "no messages found to send". imap-send only counts a
> message when its From, Date and Subject headers come in that order,
> and git send-email writes Subject before Date.
Nicely analysed.
Show 14 quoted lines
> +static bool has_required_headers(const char *line)
> +{
> +	bool from = false, date = false, subject = false;
> +
> +	while ((line = strchr(line, '\n')) && *++line != '\n') {
> +		if (starts_with(line, "From: "))
> +			from = true;
> +		else if (starts_with(line, "Date: "))
> +			date = true;
> +		else if (starts_with(line, "Subject: "))
> +			subject = true;
> +	}
> +	return from && date && subject;
> +}

So the caller is responsible for placing the cursor at the beginning of a message (e.g., a "From " line) and this one inspects the lines starting at the point looking for the headers we want to see. The cursor is local so our incrementing will not break the caller.

RFC 822/2822/5322 specifies these headers as case insensitive, so you'd want to use istarts_with() instead, though. I can see that the original did not get it right, but we are making the wrongs in the original right, so ...

Show 75 quoted lines
>  static int count_messages(struct strbuf *all_msgs)
>  {
>  	int count = 0;
>  	char *p = all_msgs->buf;
>  
>  	while (1) {
> -		if (starts_with(p, "From ")) {
> -			if (starts_with(p, "From git-send-email")) {
> -				p = strstr(p+5, "\nFrom: ");
> -				if (!p) break;
> -				p += 7;
> -				p = strstr(p, "\nTo: ");
> -				if (!p) break;
> -				p += 5;
> -				count++;
> -			} else {
> -				p = strstr(p+5, "\nFrom: ");
> -				if (!p) break;
> -				p = strstr(p+7, "\nDate: ");
> -				if (!p) break;
> -				p = strstr(p+7, "\nSubject: ");
> -				if (!p) break;
> -				p += 10;
> -				count++;
> -			}
> -		}
> +		if (starts_with(p, "From ") && has_required_headers(p))
> +			count++;
>  		p = strstr(p+5, "\nFrom ");
>  		if (!p)
>  			break;
> diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
> index d1393ef197..1f77c14315 100755
> --- a/t/t9001-send-email.sh
> +++ b/t/t9001-send-email.sh
> @@ -2863,4 +2863,39 @@ test_expect_success $PREREQ '--compose handles to headers' '
>  	test_cmp expect msgtxt2.to
>  '
>  
> +test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archive' '
> +	write_script fake-imap-server <<-\EOF &&
> +	printf "* PREAUTH [CAPABILITY IMAP4rev1 LITERAL+] ready\r\n"
> +	printf "1 OK\r\n2 OK\r\n3 OK\r\n"
> +	cat >/dev/null
> +	EOF
> +	test_config imap.tunnel ./fake-imap-server &&
> +	clean_fake_sendmail &&
> +	git send-email \
> +		--from="Example <from@example.com>" \
> +		--to=nobody@example.com \
> +		--smtp-server="$(pwd)/fake.sendmail" \
> +		HEAD^ &&
> +	{
> +		echo "From mboxrd@z Thu Jan  1 00:00:00 1970" &&
> +		cat msgtxt1
> +	} >archive.mbox &&
> +	git imap-send --folder=INBOX <archive.mbox 2>err &&
> +	test_grep "^Sending 1 message to INBOX folder" err &&
> +	test_grep "(1/1) done" err
> +'
> +
> +test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch' '
> +	test_config imap.tunnel ./fake-imap-server &&
> +	clean_fake_sendmail &&
> +	git send-email \
> +		--from="Example <from@example.com>" \
> +		--to=nobody@example.com \
> +		--smtp-server="$(pwd)/fake.sendmail" \
> +		--imap-sent-folder=Sent \
> +		HEAD^ 2>err &&
> +	test_grep "^Sending 1 message to Sent folder" err &&
> +	test_grep "(1/1) done" err
> +'
> +
>  test_done
Harald Nordgren· Oct 20, 2026, 07:28 UTC · re: Junio C Hamano · lore

Re: [PATCH 2/2] imap-send: accept messages with Subject before Date

> RFC 822/2822/5322 specifies these headers as case insensitive, so
> you'd want to use istarts_with() instead, though.  I can see that
> the original did not get it right, but we are making the wrongs in
> the original right, so ...
Thanks!

Would you like to see a test for case insensitive? Personally, I would skip it since I trust 'istarts_with'.

Harald
Harald Nordgren via GitGitGadget· Oct 21, 2026, 07:07 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v2 0/2] Fix imap-send with reordered message headers

git imap-send rejected patches sent with git send-email, such as ones fetched from lore with b4, because it expected Date before Subject.

Changes in v2:
 * Case insensitive header match according to RFC 822/2822/5322.
Harald Nordgren (2):
  imap-send: avoid leaking the tunnel command
  imap-send: accept messages with Subject before Date
 imap-send.c           | 38 ++++++++++++++++++--------------------
 t/t9001-send-email.sh | 35 +++++++++++++++++++++++++++++++++++
 2 files changed, 53 insertions(+), 20 deletions(-)
base-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2456%2FHaraldNordgren%2Ffix%2Fimap-send-header-order-v2
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2456/HaraldNordgren/fix/imap-send-header-order-v2
Pull-Request: https://github.com/git/git/pull/2456
Range-diff vs v1:
 1:  0784c16134 = 1:  b170b1f6d3 imap-send: avoid leaking the tunnel command
 2:  94129fb718 ! 2:  c42827a122 imap-send: accept messages with Subject before Date
     @@ Commit message
          and git send-email writes Subject before Date.
      
          Accept the three headers in any order within the header block of each
     -    message. The copy that git send-email --imap-sent-folder pipes to
     +    message, and match their names case insensitively as RFC 5322
     +    specifies. The copy that git send-email --imap-sent-folder pipes to
          imap-send then passes the same check, so the special case added for it
          in 04133f5bc4 (send-email: add ability to send a copy of sent emails to
          an IMAP folder, 2025-08-12) is no longer needed.
     @@ imap-send.c: static void wrap_in_html(struct strbuf *msg)
      +	bool from = false, date = false, subject = false;
      +
      +	while ((line = strchr(line, '\n')) && *++line != '\n') {
     -+		if (starts_with(line, "From: "))
     ++		if (istarts_with(line, "From: "))
      +			from = true;
     -+		else if (starts_with(line, "Date: "))
     ++		else if (istarts_with(line, "Date: "))
      +			date = true;
     -+		else if (starts_with(line, "Subject: "))
     ++		else if (istarts_with(line, "Subject: "))
      +			subject = true;
      +	}
      +	return from && date && subject;
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Oct 21, 2026, 07:07 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v2 1/2] imap-send: avoid leaking the tunnel command

From: Harald Nordgren <haraldnordgren@gmail.com>

When imap.tunnel is set, imap_open_store() starts the tunnel but never releases the child_process used for it, which leaks its argument list. No test sets imap.tunnel, so the leak checker has not caught this.

Only the pipes to the tunnel are needed once it is running, so release the rest right after starting it.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c | 1 +
 1 file changed, 1 insertion(+)
Show changes to imap-send.c +1 −0
diff --git a/imap-send.c b/imap-send.c
index bf1d2cf74d..6f76bade35 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1176,6 +1176,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
 
 		imap->buf.sock.fd[0] = tunnel.out;
 		imap->buf.sock.fd[1] = tunnel.in;
+		child_process_clear(&tunnel);
 
 		imap_info("OK\n");
 	} else {
-- 
gitgitgadget
Patrick Steinhardt· Oct 21, 2026, 08:24 UTC · re: Harald Nordgren via GitGitGadget · lore

Re: [PATCH v2 1/2] imap-send: avoid leaking the tunnel command

On Wed, Oct 21, 2026 at 07:07:48AM +0000, Harald Nordgren via GitGitGadget wrote:
Show 9 quoted lines
> diff --git a/imap-send.c b/imap-send.c
> index bf1d2cf74d..6f76bade35 100644
> --- a/imap-send.c
> +++ b/imap-send.c
> @@ -1176,6 +1176,7 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
>  
>  		imap->buf.sock.fd[0] = tunnel.out;
>  		imap->buf.sock.fd[1] = tunnel.in;
> +		child_process_clear(&tunnel);

This change plugs the memory leak, but the bigger question to me is why we never call `finish_command()` on the child process at all. Is there a reason for that?

Patrick
Harald Nordgren· Oct 21, 2026, 08:53 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v2 1/2] imap-send: avoid leaking the tunnel command

Show 7 quoted lines
> >               imap->buf.sock.fd[0] = tunnel.out;
> >               imap->buf.sock.fd[1] = tunnel.in;
> > +             child_process_clear(&tunnel);
>
> This change plugs the memory leak, but the bigger question to me is why
> we never call `finish_command()` on the child process at all. Is there a
> reason for that?
Yeah, that's curious. I'll take a look at handling this better.
Harald
Harald Nordgren via GitGitGadget· Oct 21, 2026, 07:07 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v2 2/2] imap-send: accept messages with Subject before Date

From: Harald Nordgren <haraldnordgren@gmail.com>

Patches sent with git send-email cannot be fed back into git imap-send after fetching them from the mailing list archive, for example with

    b4 mbox --single-message -o - <url> | git imap-send

which fails with "no messages found to send". imap-send only counts a message when its From, Date and Subject headers come in that order, and git send-email writes Subject before Date.

Accept the three headers in any order within the header block of each message, and match their names case insensitively as RFC 5322 specifies. The copy that git send-email --imap-sent-folder pipes to imap-send then passes the same check, so the special case added for it in 04133f5bc4 (send-email: add ability to send a copy of sent emails to an IMAP folder, 2025-08-12) is no longer needed.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           | 37 +++++++++++++++++--------------------
 t/t9001-send-email.sh | 35 +++++++++++++++++++++++++++++++++++
 2 files changed, 52 insertions(+), 20 deletions(-)
Show changes to 2 files +52 −20

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index 6f76bade35..7ae2eeddc9 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1453,32 +1453,29 @@ static void wrap_in_html(struct strbuf *msg)
 	*msg = buf;
 }
 
+static bool has_required_headers(const char *line)
+{
+	bool from = false, date = false, subject = false;
+
+	while ((line = strchr(line, '\n')) && *++line != '\n') {
+		if (istarts_with(line, "From: "))
+			from = true;
+		else if (istarts_with(line, "Date: "))
+			date = true;
+		else if (istarts_with(line, "Subject: "))
+			subject = true;
+	}
+	return from && date && subject;
+}
+
 static int count_messages(struct strbuf *all_msgs)
 {
 	int count = 0;
 	char *p = all_msgs->buf;
 
 	while (1) {
-		if (starts_with(p, "From ")) {
-			if (starts_with(p, "From git-send-email")) {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p += 7;
-				p = strstr(p, "\nTo: ");
-				if (!p) break;
-				p += 5;
-				count++;
-			} else {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p = strstr(p+7, "\nDate: ");
-				if (!p) break;
-				p = strstr(p+7, "\nSubject: ");
-				if (!p) break;
-				p += 10;
-				count++;
-			}
-		}
+		if (starts_with(p, "From ") && has_required_headers(p))
+			count++;
 		p = strstr(p+5, "\nFrom ");
 		if (!p)
 			break;
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index d1393ef197..1f77c14315 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2863,4 +2863,39 @@ test_expect_success $PREREQ '--compose handles to headers' '
 	test_cmp expect msgtxt2.to
 '
 
+test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archive' '
+	write_script fake-imap-server <<-\EOF &&
+	printf "* PREAUTH [CAPABILITY IMAP4rev1 LITERAL+] ready\r\n"
+	printf "1 OK\r\n2 OK\r\n3 OK\r\n"
+	cat >/dev/null
+	EOF
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		HEAD^ &&
+	{
+		echo "From mboxrd@z Thu Jan  1 00:00:00 1970" &&
+		cat msgtxt1
+	} >archive.mbox &&
+	git imap-send --folder=INBOX <archive.mbox 2>err &&
+	test_grep "^Sending 1 message to INBOX folder" err &&
+	test_grep "(1/1) done" err
+'
+
+test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch' '
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		--imap-sent-folder=Sent \
+		HEAD^ 2>err &&
+	test_grep "^Sending 1 message to Sent folder" err &&
+	test_grep "(1/1) done" err
+'
+
 test_done
-- 
gitgitgadget
Patrick Steinhardt· Oct 21, 2026, 08:25 UTC · re: Harald Nordgren via GitGitGadget · lore

Re: [PATCH v2 2/2] imap-send: accept messages with Subject before Date

On Wed, Oct 21, 2026 at 07:07:49AM +0000, Harald Nordgren via GitGitGadget wrote:
Show 17 quoted lines
> From: Harald Nordgren <haraldnordgren@gmail.com>
> 
> Patches sent with git send-email cannot be fed back into git imap-send
> after fetching them from the mailing list archive, for example with
> 
>     b4 mbox --single-message -o - <url> | git imap-send
> 
> which fails with "no messages found to send". imap-send only counts a
> message when its From, Date and Subject headers come in that order,
> and git send-email writes Subject before Date.
> 
> Accept the three headers in any order within the header block of each
> message, and match their names case insensitively as RFC 5322
> specifies. The copy that git send-email --imap-sent-folder pipes to
> imap-send then passes the same check, so the special case added for it
> in 04133f5bc4 (send-email: add ability to send a copy of sent emails to
> an IMAP folder, 2025-08-12) is no longer needed.

Aren't you fixing two different issues at once? I'd argue that this should be done in separate commits.

Show 52 quoted lines
> diff --git a/imap-send.c b/imap-send.c
> index 6f76bade35..7ae2eeddc9 100644
> --- a/imap-send.c
> +++ b/imap-send.c
> @@ -1453,32 +1453,29 @@ static void wrap_in_html(struct strbuf *msg)
>  	*msg = buf;
>  }
>  
> +static bool has_required_headers(const char *line)
> +{
> +	bool from = false, date = false, subject = false;
> +
> +	while ((line = strchr(line, '\n')) && *++line != '\n') {
> +		if (istarts_with(line, "From: "))
> +			from = true;
> +		else if (istarts_with(line, "Date: "))
> +			date = true;
> +		else if (istarts_with(line, "Subject: "))
> +			subject = true;
> +	}
> +
> +	return from && date && subject;
> +}
> +
>  static int count_messages(struct strbuf *all_msgs)
>  {
>  	int count = 0;
>  	char *p = all_msgs->buf;
>  
>  	while (1) {
> -		if (starts_with(p, "From ")) {
> -			if (starts_with(p, "From git-send-email")) {
> -				p = strstr(p+5, "\nFrom: ");
> -				if (!p) break;
> -				p += 7;
> -				p = strstr(p, "\nTo: ");
> -				if (!p) break;
> -				p += 5;
> -				count++;
> -			} else {
> -				p = strstr(p+5, "\nFrom: ");
> -				if (!p) break;
> -				p = strstr(p+7, "\nDate: ");
> -				if (!p) break;
> -				p = strstr(p+7, "\nSubject: ");
> -				if (!p) break;
> -				p += 10;
> -				count++;
> -			}
> -		}
> +		if (starts_with(p, "From ") && has_required_headers(p))
> +			count++;

In the preimage we special-cased when the line starts with "From git-send-email", and in that case we didn't recognize "Subject:" lines. So you're not only changing that we now accept the headers in any arbitrary order, but you also make us accept "Subject:" lines when we saw "From git-send-email". You don't really explain why that change is okay to do though.

Show 9 quoted lines
> diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
> index d1393ef197..1f77c14315 100755
> --- a/t/t9001-send-email.sh
> +++ b/t/t9001-send-email.sh
> @@ -2863,4 +2863,39 @@ test_expect_success $PREREQ '--compose handles to headers' '
>  	test_cmp expect msgtxt2.to
>  '
>  
> +test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archive' '

Quite weird that every single test carries this prerequisite. Makes me wonder why we don't just skip all tests at once.

Patrick
Harald Nordgren· Oct 21, 2026, 08:45 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v2 2/2] imap-send: accept messages with Subject before Date

Show 18 quoted lines
> > Patches sent with git send-email cannot be fed back into git imap-send
> > after fetching them from the mailing list archive, for example with
> >
> >     b4 mbox --single-message -o - <url> | git imap-send
> >
> > which fails with "no messages found to send". imap-send only counts a
> > message when its From, Date and Subject headers come in that order,
> > and git send-email writes Subject before Date.
> >
> > Accept the three headers in any order within the header block of each
> > message, and match their names case insensitively as RFC 5322
> > specifies. The copy that git send-email --imap-sent-folder pipes to
> > imap-send then passes the same check, so the special case added for it
> > in 04133f5bc4 (send-email: add ability to send a copy of sent emails to
> > an IMAP folder, 2025-08-12) is no longer needed.
>
> Aren't you fixing two different issues at once? I'd argue that this
> should be done in separate commits.
Good point.
Show 8 quoted lines
> > @@ -2863,4 +2863,39 @@ test_expect_success $PREREQ '--compose handles to headers' '
> >       test_cmp expect msgtxt2.to
> >  '
> >
> > +test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archive' '
>
> Quite weird that every single test carries this prerequisite. Makes me
> wonder why we don't just skip all tests at once.
What do you mean here, do you want to remove the tests?
Harald
Patrick Steinhardt· Oct 21, 2026, 09:36 UTC · re: Harald Nordgren · lore

Re: [PATCH v2 2/2] imap-send: accept messages with Subject before Date

On Wed, Oct 21, 2026 at 10:45:42AM +0200, Harald Nordgren wrote:
Show 10 quoted lines
> > > @@ -2863,4 +2863,39 @@ test_expect_success $PREREQ '--compose handles to headers' '
> > >       test_cmp expect msgtxt2.to
> > >  '
> > >
> > > +test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archive' '
> >
> > Quite weird that every single test carries this prerequisite. Makes me
> > wonder why we don't just skip all tests at once.
> 
> What do you mean here, do you want to remove the tests?

No, I rather think that we should have this at the beginning of the test file:

        if ! test_have_prereq PERL
        then
        	skip_all='skipping send-email tests; Perl not available'
        	test_done
        fi

Then we can drop the $PREREQ from all tests. But I just spotted that we do some more gymanstics with another prereq that we optionally add later, so this doesn't quite work as-is.

Overall though this is fine, we don't have to fix it as part of this patch series.

Patrick
Harald Nordgren· Oct 21, 2026, 09:59 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v2 2/2] imap-send: accept messages with Subject before Date

Show 17 quoted lines
> > What do you mean here, do you want to remove the tests?
>
> No, I rather think that we should have this at the beginning of the test
> file:
>
>         if ! test_have_prereq PERL
>         then
>                 skip_all='skipping send-email tests; Perl not available'
>                 test_done
>         fi
>
> Then we can drop the $PREREQ from all tests. But I just spotted that we
> do some more gymanstics with another prereq that we optionally add
> later, so this doesn't quite work as-is.
>
> Overall though this is fine, we don't have to fix it as part of this
> patch series.
Alright, maybe better to have this as #leftoverbits ?
Harald
Harald Nordgren via GitGitGadget· Oct 22, 2026, 06:56 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v3 0/4] Fix imap-send with reordered message headers

git imap-send rejected patches sent with git send-email, such as ones fetched from lore with b4, because it expected Date before Subject.

Changes in v3:
 * Split concerns into different commits, commit message explains what they
   do.
 * Handle tunnel leak in the proper place.
Changes in v2:
 * Case insensitive header match according to RFC 822/2822/5322.
Harald Nordgren (4):
  imap-send: wait for the tunnel command to finish
  imap-send: accept messages with Subject before Date
  imap-send: check git send-email copies like other messages
  imap-send: match header names case insensitively
 imap-send.c           | 57 ++++++++++++++++++++++---------------------
 t/t9001-send-email.sh | 35 ++++++++++++++++++++++++++
 2 files changed, 64 insertions(+), 28 deletions(-)
base-commit: 47fdf2208c5b94c8cc01c106b8f7c85efb91c926
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2456%2FHaraldNordgren%2Ffix%2Fimap-send-header-order-v3
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2456/HaraldNordgren/fix/imap-send-header-order-v3
Pull-Request: https://github.com/git/git/pull/2456
Range-diff vs v2:
 1:  b170b1f6d3 ! 1:  0f446c3c12 imap-send: avoid leaking the tunnel command
     @@ Metadata
      Author: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## Commit message ##
     -    imap-send: avoid leaking the tunnel command
     +    imap-send: wait for the tunnel command to finish
      
     -    When imap.tunnel is set, imap_open_store() starts the tunnel but never
     -    releases the child_process used for it, which leaks its argument list.
     -    No test sets imap.tunnel, so the leak checker has not caught this.
     +    When imap.tunnel is set, git imap-send starts the tunnel command but
     +    never waits for it to exit, and leaks the argument list it was started
     +    with. The isync code that imap-send was derived from forked the tunnel
     +    without ever reaping it, and c94d2dd080 (imap-send: use run-command
     +    API for tunneling, 2009-10-19) carried that over.
      
     -    Only the pipes to the tunnel are needed once it is running, so release
     -    the rest right after starting it.
     +    Wait for the tunnel when closing the connection. By then imap-send has
     +    closed its end of the pipes, so the tunnel sees the end of its input
     +    and exits. No test sets imap.tunnel, so the leak checker has not
     +    caught this.
      
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## imap-send.c ##
     +@@ imap-send.c: struct imap {
     + 	/* command queue */
     + 	int nexttag, num_in_progress, literal_pending;
     + 	struct imap_cmd *in_progress, **in_progress_append;
     ++	struct child_process tunnel;
     + 	struct imap_buffer buf; /* this is BIG, so put it last */
     + };
     + 
     +@@ imap-send.c: static void imap_close_server(struct imap_store *ictx)
     + 		imap_exec(ictx, NULL, "LOGOUT");
     + 		socket_shutdown(&imap->buf.sock);
     + 	}
     ++	if (ictx->cfg->tunnel)
     ++		finish_command(&imap->tunnel);
     + 	free(imap);
     + }
     + 
      @@ imap-send.c: static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
     + 	/* open connection to IMAP server */
     + 
     + 	if (srvc->tunnel) {
     +-		struct child_process tunnel = CHILD_PROCESS_INIT;
     ++		struct child_process *tunnel = &imap->tunnel;
     + 
     + 		imap_info("Starting tunnel '%s'... ", srvc->tunnel);
     + 
     +-		strvec_push(&tunnel.args, srvc->tunnel);
     +-		tunnel.use_shell = 1;
     +-		tunnel.in = -1;
     +-		tunnel.out = -1;
     +-		if (start_command(&tunnel))
     ++		child_process_init(tunnel);
     ++		strvec_push(&tunnel->args, srvc->tunnel);
     ++		tunnel->use_shell = 1;
     ++		tunnel->in = -1;
     ++		tunnel->out = -1;
     ++		if (start_command(tunnel))
     + 			die("cannot start proxy %s", srvc->tunnel);
       
     - 		imap->buf.sock.fd[0] = tunnel.out;
     - 		imap->buf.sock.fd[1] = tunnel.in;
     -+		child_process_clear(&tunnel);
     +-		imap->buf.sock.fd[0] = tunnel.out;
     +-		imap->buf.sock.fd[1] = tunnel.in;
     ++		imap->buf.sock.fd[0] = tunnel->out;
     ++		imap->buf.sock.fd[1] = tunnel->in;
       
       		imap_info("OK\n");
       	} else {
 2:  c42827a122 ! 2:  38214d9abf imap-send: accept messages with Subject before Date
     @@ Commit message
          and git send-email writes Subject before Date.
      
          Accept the three headers in any order within the header block of each
     -    message, and match their names case insensitively as RFC 5322
     -    specifies. The copy that git send-email --imap-sent-folder pipes to
     -    imap-send then passes the same check, so the special case added for it
     -    in 04133f5bc4 (send-email: add ability to send a copy of sent emails to
     -    an IMAP folder, 2025-08-12) is no longer needed.
     +    message.
      
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
     @@ imap-send.c: static void wrap_in_html(struct strbuf *msg)
      +	bool from = false, date = false, subject = false;
      +
      +	while ((line = strchr(line, '\n')) && *++line != '\n') {
     -+		if (istarts_with(line, "From: "))
     ++		if (starts_with(line, "From: "))
      +			from = true;
     -+		else if (istarts_with(line, "Date: "))
     ++		else if (starts_with(line, "Date: "))
      +			date = true;
     -+		else if (istarts_with(line, "Subject: "))
     ++		else if (starts_with(line, "Subject: "))
      +			subject = true;
      +	}
      +	return from && date && subject;
     @@ imap-send.c: static void wrap_in_html(struct strbuf *msg)
       static int count_messages(struct strbuf *all_msgs)
       {
       	int count = 0;
     - 	char *p = all_msgs->buf;
     - 
     - 	while (1) {
     --		if (starts_with(p, "From ")) {
     --			if (starts_with(p, "From git-send-email")) {
     --				p = strstr(p+5, "\nFrom: ");
     --				if (!p) break;
     --				p += 7;
     --				p = strstr(p, "\nTo: ");
     --				if (!p) break;
     --				p += 5;
     --				count++;
     +@@ imap-send.c: static int count_messages(struct strbuf *all_msgs)
     + 				if (!p) break;
     + 				p += 5;
     + 				count++;
      -			} else {
      -				p = strstr(p+5, "\nFrom: ");
      -				if (!p) break;
     @@ imap-send.c: static void wrap_in_html(struct strbuf *msg)
      -				p = strstr(p+7, "\nSubject: ");
      -				if (!p) break;
      -				p += 10;
     --				count++;
     --			}
     --		}
     -+		if (starts_with(p, "From ") && has_required_headers(p))
     -+			count++;
     - 		p = strstr(p+5, "\nFrom ");
     - 		if (!p)
     - 			break;
     ++			} else if (has_required_headers(p)) {
     + 				count++;
     + 			}
     + 		}
      
       ## t/t9001-send-email.sh ##
      @@ t/t9001-send-email.sh: test_expect_success $PREREQ '--compose handles to headers' '
     @@ t/t9001-send-email.sh: test_expect_success $PREREQ '--compose handles to headers
      +	test_grep "^Sending 1 message to INBOX folder" err &&
      +	test_grep "(1/1) done" err
      +'
     -+
     -+test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch' '
     -+	test_config imap.tunnel ./fake-imap-server &&
     -+	clean_fake_sendmail &&
     -+	git send-email \
     -+		--from="Example <from@example.com>" \
     -+		--to=nobody@example.com \
     -+		--smtp-server="$(pwd)/fake.sendmail" \
     -+		--imap-sent-folder=Sent \
     -+		HEAD^ 2>err &&
     -+	test_grep "^Sending 1 message to Sent folder" err &&
     -+	test_grep "(1/1) done" err
     -+'
      +
       test_done
 -:  ---------- > 3:  213c4950e0 imap-send: check git send-email copies like other messages
 -:  ---------- > 4:  01add80af2 imap-send: match header names case insensitively
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Oct 22, 2026, 06:56 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v3 1/4] imap-send: wait for the tunnel command to finish

From: Harald Nordgren <haraldnordgren@gmail.com>

When imap.tunnel is set, git imap-send starts the tunnel command but never waits for it to exit, and leaks the argument list it was started with. The isync code that imap-send was derived from forked the tunnel without ever reaping it, and c94d2dd080 (imap-send: use run-command API for tunneling, 2009-10-19) carried that over.

Wait for the tunnel when closing the connection. By then imap-send has closed its end of the pipes, so the tunnel sees the end of its input and exits. No test sets imap.tunnel, so the leak checker has not caught this.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c | 20 ++++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)
Show changes to imap-send.c +12 −8
diff --git a/imap-send.c b/imap-send.c
index bf1d2cf74d..fa326c4ae2 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -118,6 +118,7 @@ struct imap {
 	/* command queue */
 	int nexttag, num_in_progress, literal_pending;
 	struct imap_cmd *in_progress, **in_progress_append;
+	struct child_process tunnel;
 	struct imap_buffer buf; /* this is BIG, so put it last */
 };
 
@@ -858,6 +859,8 @@ static void imap_close_server(struct imap_store *ictx)
 		imap_exec(ictx, NULL, "LOGOUT");
 		socket_shutdown(&imap->buf.sock);
 	}
+	if (ictx->cfg->tunnel)
+		finish_command(&imap->tunnel);
 	free(imap);
 }
 
@@ -1163,19 +1166,20 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
 	/* open connection to IMAP server */
 
 	if (srvc->tunnel) {
-		struct child_process tunnel = CHILD_PROCESS_INIT;
+		struct child_process *tunnel = &imap->tunnel;
 
 		imap_info("Starting tunnel '%s'... ", srvc->tunnel);
 
-		strvec_push(&tunnel.args, srvc->tunnel);
-		tunnel.use_shell = 1;
-		tunnel.in = -1;
-		tunnel.out = -1;
-		if (start_command(&tunnel))
+		child_process_init(tunnel);
+		strvec_push(&tunnel->args, srvc->tunnel);
+		tunnel->use_shell = 1;
+		tunnel->in = -1;
+		tunnel->out = -1;
+		if (start_command(tunnel))
 			die("cannot start proxy %s", srvc->tunnel);
 
-		imap->buf.sock.fd[0] = tunnel.out;
-		imap->buf.sock.fd[1] = tunnel.in;
+		imap->buf.sock.fd[0] = tunnel->out;
+		imap->buf.sock.fd[1] = tunnel->in;
 
 		imap_info("OK\n");
 	} else {
-- 
gitgitgadget
Patrick Steinhardt· Oct 26, 2026, 09:42 UTC · re: Harald Nordgren via GitGitGadget · lore

Re: [PATCH v3 1/4] imap-send: wait for the tunnel command to finish

On Thu, Oct 22, 2026 at 06:56:22AM +0000, Harald Nordgren via GitGitGadget wrote:
Show 18 quoted lines
> diff --git a/imap-send.c b/imap-send.c
> index bf1d2cf74d..fa326c4ae2 100644
> --- a/imap-send.c
> +++ b/imap-send.c
> @@ -118,6 +118,7 @@ struct imap {
>  	/* command queue */
>  	int nexttag, num_in_progress, literal_pending;
>  	struct imap_cmd *in_progress, **in_progress_append;
> +	struct child_process tunnel;
>  	struct imap_buffer buf; /* this is BIG, so put it last */
>  };
>  
> @@ -858,6 +859,8 @@ static void imap_close_server(struct imap_store *ictx)
>  		imap_exec(ictx, NULL, "LOGOUT");
>  		socket_shutdown(&imap->buf.sock);
>  	}
> +	if (ictx->cfg->tunnel)
> +		finish_command(&imap->tunnel);

Hmm, okay. Do we _know_ that `ictx->cfg->tunnel` is always properly initialized in case we call `imap_close_server()`? Or are there error paths where it might not be initialized yet? If the latter I think we might see weird behaviour here as the function does not seem to be safe when called on an uninitialized command.

Maybe we should make `imap->tunnel` an allocated pointer instead so that we know to only finish it when allocated and initialized?

Show 6 quoted lines
> @@ -1163,19 +1166,20 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
>  	/* open connection to IMAP server */
>  
>  	if (srvc->tunnel) {
> -		struct child_process tunnel = CHILD_PROCESS_INIT;
> +		struct child_process *tunnel = &imap->tunnel;
Nit: I would have avoided this local variable, it doesn't help us anyway
to shrink the diff.
Patrick
Harald Nordgren· Oct 26, 2026, 09:59 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v3 1/4] imap-send: wait for the tunnel command to finish

Show 11 quoted lines
> > +     if (ictx->cfg->tunnel)
> > +             finish_command(&imap->tunnel);
>
> Hmm, okay. Do we _know_ that `ictx->cfg->tunnel` is always properly
> initialized in case we call `imap_close_server()`? Or are there error
> paths where it might not be initialized yet? If the latter I think we
> might see weird behaviour here as the function does not seem to be safe
> when called on an uninitialized command.
>
> Maybe we should make `imap->tunnel` an allocated pointer instead so that
> we know to only finish it when allocated and initialized?
Good idea.
Show 9 quoted lines
> > @@ -1163,19 +1166,20 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
> >       /* open connection to IMAP server */
> >
> >       if (srvc->tunnel) {
> > -             struct child_process tunnel = CHILD_PROCESS_INIT;
> > +             struct child_process *tunnel = &imap->tunnel;
>
> Nit: I would have avoided this local variable, it doesn't help us anyway
> to shrink the diff.
Ok.
Harald
Harald Nordgren via GitGitGadget· Oct 22, 2026, 06:56 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v3 2/4] imap-send: accept messages with Subject before Date

From: Harald Nordgren <haraldnordgren@gmail.com>

Patches sent with git send-email cannot be fed back into git imap-send after fetching them from the mailing list archive, for example with

    b4 mbox --single-message -o - <url> | git imap-send

which fails with "no messages found to send". imap-send only counts a message when its From, Date and Subject headers come in that order, and git send-email writes Subject before Date.

Accept the three headers in any order within the header block of each message.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           | 24 ++++++++++++++++--------
 t/t9001-send-email.sh | 22 ++++++++++++++++++++++
 2 files changed, 38 insertions(+), 8 deletions(-)
Show changes to 2 files +38 −8

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index fa326c4ae2..ce2321d42e 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1456,6 +1456,21 @@ static void wrap_in_html(struct strbuf *msg)
 	*msg = buf;
 }
 
+static bool has_required_headers(const char *line)
+{
+	bool from = false, date = false, subject = false;
+
+	while ((line = strchr(line, '\n')) && *++line != '\n') {
+		if (starts_with(line, "From: "))
+			from = true;
+		else if (starts_with(line, "Date: "))
+			date = true;
+		else if (starts_with(line, "Subject: "))
+			subject = true;
+	}
+	return from && date && subject;
+}
+
 static int count_messages(struct strbuf *all_msgs)
 {
 	int count = 0;
@@ -1471,14 +1486,7 @@ static int count_messages(struct strbuf *all_msgs)
 				if (!p) break;
 				p += 5;
 				count++;
-			} else {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p = strstr(p+7, "\nDate: ");
-				if (!p) break;
-				p = strstr(p+7, "\nSubject: ");
-				if (!p) break;
-				p += 10;
+			} else if (has_required_headers(p)) {
 				count++;
 			}
 		}
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index d1393ef197..d105fcb41c 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2863,4 +2863,26 @@ test_expect_success $PREREQ '--compose handles to headers' '
 	test_cmp expect msgtxt2.to
 '
 
+test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archive' '
+	write_script fake-imap-server <<-\EOF &&
+	printf "* PREAUTH [CAPABILITY IMAP4rev1 LITERAL+] ready\r\n"
+	printf "1 OK\r\n2 OK\r\n3 OK\r\n"
+	cat >/dev/null
+	EOF
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		HEAD^ &&
+	{
+		echo "From mboxrd@z Thu Jan  1 00:00:00 1970" &&
+		cat msgtxt1
+	} >archive.mbox &&
+	git imap-send --folder=INBOX <archive.mbox 2>err &&
+	test_grep "^Sending 1 message to INBOX folder" err &&
+	test_grep "(1/1) done" err
+'
+
 test_done
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Oct 22, 2026, 06:56 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v3 3/4] imap-send: check git send-email copies like other messages

From: Harald Nordgren <haraldnordgren@gmail.com>

The copy that git send-email --imap-sent-folder pipes to imap-send starts with a "From git-send-email" line. 04133f5bc4 (send-email: add ability to send a copy of sent emails to an IMAP folder, 2025-08-12) made imap-send count such a message when it has From and To headers, without looking for Date and Subject. Without that special case the copy would not be counted at all, because git send-email writes Subject before Date.

Now that the headers are accepted in any order, the special case is no longer needed. The copy carries the same headers git send-email sends over SMTP, which always include From, Subject and Date, so it passes the check for other messages. A To header is no longer required, the same as for any other message.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           | 15 ++-------------
 t/t9001-send-email.sh | 13 +++++++++++++
 2 files changed, 15 insertions(+), 13 deletions(-)
Show changes to 2 files +15 −13

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index ce2321d42e..1091825dfb 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1477,19 +1477,8 @@ static int count_messages(struct strbuf *all_msgs)
 	char *p = all_msgs->buf;
 
 	while (1) {
-		if (starts_with(p, "From ")) {
-			if (starts_with(p, "From git-send-email")) {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p += 7;
-				p = strstr(p, "\nTo: ");
-				if (!p) break;
-				p += 5;
-				count++;
-			} else if (has_required_headers(p)) {
-				count++;
-			}
-		}
+		if (starts_with(p, "From ") && has_required_headers(p))
+			count++;
 		p = strstr(p+5, "\nFrom ");
 		if (!p)
 			break;
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index d105fcb41c..1f77c14315 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2885,4 +2885,17 @@ test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archiv
 	test_grep "(1/1) done" err
 '
 
+test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch' '
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		--imap-sent-folder=Sent \
+		HEAD^ 2>err &&
+	test_grep "^Sending 1 message to Sent folder" err &&
+	test_grep "(1/1) done" err
+'
+
 test_done
-- 
gitgitgadget
Patrick Steinhardt· Oct 26, 2026, 09:42 UTC · re: Harald Nordgren via GitGitGadget · lore

Re: [PATCH v3 3/4] imap-send: check git send-email copies like other messages

On Thu, Oct 22, 2026 at 06:56:24AM +0000, Harald Nordgren via GitGitGadget wrote:
Show 15 quoted lines
> From: Harald Nordgren <haraldnordgren@gmail.com>
> 
> The copy that git send-email --imap-sent-folder pipes to imap-send
> starts with a "From git-send-email" line. 04133f5bc4 (send-email: add
> ability to send a copy of sent emails to an IMAP folder, 2025-08-12)
> made imap-send count such a message when it has From and To headers,
> without looking for Date and Subject. Without that special case the
> copy would not be counted at all, because git send-email writes
> Subject before Date.
> 
> Now that the headers are accepted in any order, the special case is no
> longer needed. The copy carries the same headers git send-email sends
> over SMTP, which always include From, Subject and Date, so it passes
> the check for other messages. A To header is no longer required, the
> same as for any other message.

You argue from the perspective of git-send-email(1) generating the mbox. But what if somebody writes an mbox via a different script that doesn't write a Date or Subject line? Based on RFC 5322, the Date header is mandatory, so I think it's safe to assume it always exists. But the Subject header is only specified as "SHOULD exist".

Now we probably don't care about such edge cases, so tightening the rules is likely going to be fine. But I don't think that's sufficiently explained.

Patrick
Harald Nordgren· Oct 26, 2026, 10:02 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v3 3/4] imap-send: check git send-email copies like other messages

Show 23 quoted lines
> > The copy that git send-email --imap-sent-folder pipes to imap-send
> > starts with a "From git-send-email" line. 04133f5bc4 (send-email: add
> > ability to send a copy of sent emails to an IMAP folder, 2025-08-12)
> > made imap-send count such a message when it has From and To headers,
> > without looking for Date and Subject. Without that special case the
> > copy would not be counted at all, because git send-email writes
> > Subject before Date.
> >
> > Now that the headers are accepted in any order, the special case is no
> > longer needed. The copy carries the same headers git send-email sends
> > over SMTP, which always include From, Subject and Date, so it passes
> > the check for other messages. A To header is no longer required, the
> > same as for any other message.
>
> You argue from the perspective of git-send-email(1) generating the mbox.
> But what if somebody writes an mbox via a different script that doesn't
> write a Date or Subject line? Based on RFC 5322, the Date header is
> mandatory, so I think it's safe to assume it always exists. But the
> Subject header is only specified as "SHOULD exist".
>
> Now we probably don't care about such edge cases, so tightening the
> rules is likely going to be fine. But I don't think that's sufficiently
> explained.
Thanks, very valuable to have another set of eyes here!
Harald
Harald Nordgren via GitGitGadget· Oct 22, 2026, 06:56 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v3 4/4] imap-send: match header names case insensitively

From: Harald Nordgren <haraldnordgren@gmail.com>

imap-send says "no messages found to send" for a message whose headers are spelled "from:", "date:" or "subject:", although RFC 5322 treats header field names as case insensitive. Match the three names in any case.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)
Show changes to imap-send.c +3 −3
diff --git a/imap-send.c b/imap-send.c
index 1091825dfb..21e20d7720 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1461,11 +1461,11 @@ static bool has_required_headers(const char *line)
 	bool from = false, date = false, subject = false;
 
 	while ((line = strchr(line, '\n')) && *++line != '\n') {
-		if (starts_with(line, "From: "))
+		if (istarts_with(line, "From: "))
 			from = true;
-		else if (starts_with(line, "Date: "))
+		else if (istarts_with(line, "Date: "))
 			date = true;
-		else if (starts_with(line, "Subject: "))
+		else if (istarts_with(line, "Subject: "))
 			subject = true;
 	}
 	return from && date && subject;
-- 
gitgitgadget
Patrick Steinhardt· Oct 26, 2026, 09:42 UTC · re: Harald Nordgren via GitGitGadget · lore

Re: [PATCH v3 4/4] imap-send: match header names case insensitively

On Thu, Oct 22, 2026 at 06:56:25AM +0000, Harald Nordgren via GitGitGadget wrote:
Show 6 quoted lines
> From: Harald Nordgren <haraldnordgren@gmail.com>
> 
> imap-send says "no messages found to send" for a message whose headers
> are spelled "from:", "date:" or "subject:", although RFC 5322 treats
> header field names as case insensitive. Match the three names in any
> case.
Should we have a test for this, as well?
Patrick
Harald Nordgren via GitGitGadget· Oct 27, 2026, 08:31 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v4 0/4] Fix imap-send with reordered message headers

git imap-send rejected patches sent with git send-email, such as ones fetched from lore with b4, because it expected Date before Subject.

Changes in v4:
 * Store the tunnel as an allocated pointer, so it is only finished when it
   was started, and drop the local tunnel variable
 * Add a test for lowercase from:, date: and subject: headers
 * Explain in the commit message why dropping the From git-send-email
   special case is fine for mboxes written by other scripts
Changes in v3:
 * Split concerns into different commits, commit message explains what they
   do.
 * Handle tunnel leak in the proper place.
Changes in v2:
 * Case insensitive header match according to RFC 822/2822/5322.
Harald Nordgren (4):
  imap-send: wait for the tunnel command to finish
  imap-send: accept messages with Subject before Date
  imap-send: check git send-email copies like other messages
  imap-send: match header names case insensitively
 imap-send.c           | 60 ++++++++++++++++++++++---------------------
 t/t9001-send-email.sh | 50 ++++++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+), 29 deletions(-)
base-commit: 7e34d8174dcda3435d1404c9a30bef46afe6c13e
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2456%2FHaraldNordgren%2Ffix%2Fimap-send-header-order-v4
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2456/HaraldNordgren/fix/imap-send-header-order-v4
Pull-Request: https://github.com/git/git/pull/2456
Range-diff vs v3:
 1:  0f446c3c12 ! 1:  8941ae12bb imap-send: wait for the tunnel command to finish
     @@ imap-send.c: struct imap {
       	/* command queue */
       	int nexttag, num_in_progress, literal_pending;
       	struct imap_cmd *in_progress, **in_progress_append;
     -+	struct child_process tunnel;
     ++	struct child_process *tunnel;
       	struct imap_buffer buf; /* this is BIG, so put it last */
       };
       
     @@ imap-send.c: static void imap_close_server(struct imap_store *ictx)
       		imap_exec(ictx, NULL, "LOGOUT");
       		socket_shutdown(&imap->buf.sock);
       	}
     -+	if (ictx->cfg->tunnel)
     -+		finish_command(&imap->tunnel);
     ++	if (imap->tunnel) {
     ++		finish_command(imap->tunnel);
     ++		free(imap->tunnel);
     ++	}
       	free(imap);
       }
       
     @@ imap-send.c: static struct imap_store *imap_open_store(struct imap_server_conf *
       
       	if (srvc->tunnel) {
      -		struct child_process tunnel = CHILD_PROCESS_INIT;
     -+		struct child_process *tunnel = &imap->tunnel;
     - 
     +-
       		imap_info("Starting tunnel '%s'... ", srvc->tunnel);
       
      -		strvec_push(&tunnel.args, srvc->tunnel);
     @@ imap-send.c: static struct imap_store *imap_open_store(struct imap_server_conf *
      -		tunnel.in = -1;
      -		tunnel.out = -1;
      -		if (start_command(&tunnel))
     -+		child_process_init(tunnel);
     -+		strvec_push(&tunnel->args, srvc->tunnel);
     -+		tunnel->use_shell = 1;
     -+		tunnel->in = -1;
     -+		tunnel->out = -1;
     -+		if (start_command(tunnel))
     ++		imap->tunnel = xmalloc(sizeof(*imap->tunnel));
     ++		child_process_init(imap->tunnel);
     ++		strvec_push(&imap->tunnel->args, srvc->tunnel);
     ++		imap->tunnel->use_shell = 1;
     ++		imap->tunnel->in = -1;
     ++		imap->tunnel->out = -1;
     ++		if (start_command(imap->tunnel))
       			die("cannot start proxy %s", srvc->tunnel);
       
      -		imap->buf.sock.fd[0] = tunnel.out;
      -		imap->buf.sock.fd[1] = tunnel.in;
     -+		imap->buf.sock.fd[0] = tunnel->out;
     -+		imap->buf.sock.fd[1] = tunnel->in;
     ++		imap->buf.sock.fd[0] = imap->tunnel->out;
     ++		imap->buf.sock.fd[1] = imap->tunnel->in;
       
       		imap_info("OK\n");
       	} else {
 2:  38214d9abf = 2:  ad3158b336 imap-send: accept messages with Subject before Date
 3:  213c4950e0 ! 3:  919e38c31d imap-send: check git send-email copies like other messages
     @@ Commit message
          the check for other messages. A To header is no longer required, the
          same as for any other message.
      
     +    This makes the check stricter when some other script writes an mbox
     +    with a "From git-send-email" line. RFC 5322 requires a Date header but
     +    only recommends Subject, so a valid message from such a script may
     +    lack a Subject and would no longer be counted. imap-send has always
     +    required a Subject under any other "From " line, though, and only
     +    refuses an mbox when none of its messages are counted, so treating
     +    these copies like the rest should not affect anyone in practice.
     +
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
       ## imap-send.c ##
 4:  01add80af2 ! 4:  5b4a08bbda imap-send: match header names case insensitively
     @@ imap-send.c: static bool has_required_headers(const char *line)
       			subject = true;
       	}
       	return from && date && subject;
     +
     + ## t/t9001-send-email.sh ##
     +@@ t/t9001-send-email.sh: test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch'
     + 	test_grep "(1/1) done" err
     + '
     + 
     ++test_expect_success $PREREQ 'imap-send matches header names in any case' '
     ++	test_config imap.tunnel ./fake-imap-server &&
     ++	cat >lowercase.mbox <<-\EOF &&
     ++	From mboxrd@z Thu Jan  1 00:00:00 1970
     ++	from: Example <from@example.com>
     ++	date: Thu, 1 Jan 1970 00:00:00 +0000
     ++	subject: [PATCH] lowercase headers
     ++
     ++	body
     ++	EOF
     ++	git imap-send --folder=INBOX <lowercase.mbox 2>err &&
     ++	test_grep "^Sending 1 message to INBOX folder" err &&
     ++	test_grep "(1/1) done" err
     ++'
     ++
     + test_done
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Oct 27, 2026, 08:31 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v4 1/4] imap-send: wait for the tunnel command to finish

From: Harald Nordgren <haraldnordgren@gmail.com>

When imap.tunnel is set, git imap-send starts the tunnel command but never waits for it to exit, and leaks the argument list it was started with. The isync code that imap-send was derived from forked the tunnel without ever reaping it, and c94d2dd080 (imap-send: use run-command API for tunneling, 2009-10-19) carried that over.

Wait for the tunnel when closing the connection. By then imap-send has closed its end of the pipes, so the tunnel sees the end of its input and exits. No test sets imap.tunnel, so the leak checker has not caught this.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)
Show changes to imap-send.c +14 −9
diff --git a/imap-send.c b/imap-send.c
index bf1d2cf74d..7e74c37a72 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -118,6 +118,7 @@ struct imap {
 	/* command queue */
 	int nexttag, num_in_progress, literal_pending;
 	struct imap_cmd *in_progress, **in_progress_append;
+	struct child_process *tunnel;
 	struct imap_buffer buf; /* this is BIG, so put it last */
 };
 
@@ -858,6 +859,10 @@ static void imap_close_server(struct imap_store *ictx)
 		imap_exec(ictx, NULL, "LOGOUT");
 		socket_shutdown(&imap->buf.sock);
 	}
+	if (imap->tunnel) {
+		finish_command(imap->tunnel);
+		free(imap->tunnel);
+	}
 	free(imap);
 }
 
@@ -1163,19 +1168,19 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
 	/* open connection to IMAP server */
 
 	if (srvc->tunnel) {
-		struct child_process tunnel = CHILD_PROCESS_INIT;
-
 		imap_info("Starting tunnel '%s'... ", srvc->tunnel);
 
-		strvec_push(&tunnel.args, srvc->tunnel);
-		tunnel.use_shell = 1;
-		tunnel.in = -1;
-		tunnel.out = -1;
-		if (start_command(&tunnel))
+		imap->tunnel = xmalloc(sizeof(*imap->tunnel));
+		child_process_init(imap->tunnel);
+		strvec_push(&imap->tunnel->args, srvc->tunnel);
+		imap->tunnel->use_shell = 1;
+		imap->tunnel->in = -1;
+		imap->tunnel->out = -1;
+		if (start_command(imap->tunnel))
 			die("cannot start proxy %s", srvc->tunnel);
 
-		imap->buf.sock.fd[0] = tunnel.out;
-		imap->buf.sock.fd[1] = tunnel.in;
+		imap->buf.sock.fd[0] = imap->tunnel->out;
+		imap->buf.sock.fd[1] = imap->tunnel->in;
 
 		imap_info("OK\n");
 	} else {
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Oct 27, 2026, 08:31 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v4 2/4] imap-send: accept messages with Subject before Date

From: Harald Nordgren <haraldnordgren@gmail.com>

Patches sent with git send-email cannot be fed back into git imap-send after fetching them from the mailing list archive, for example with

    b4 mbox --single-message -o - <url> | git imap-send

which fails with "no messages found to send". imap-send only counts a message when its From, Date and Subject headers come in that order, and git send-email writes Subject before Date.

Accept the three headers in any order within the header block of each message.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           | 24 ++++++++++++++++--------
 t/t9001-send-email.sh | 22 ++++++++++++++++++++++
 2 files changed, 38 insertions(+), 8 deletions(-)
Show changes to 2 files +38 −8

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index 7e74c37a72..8977a934f8 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1457,6 +1457,21 @@ static void wrap_in_html(struct strbuf *msg)
 	*msg = buf;
 }
 
+static bool has_required_headers(const char *line)
+{
+	bool from = false, date = false, subject = false;
+
+	while ((line = strchr(line, '\n')) && *++line != '\n') {
+		if (starts_with(line, "From: "))
+			from = true;
+		else if (starts_with(line, "Date: "))
+			date = true;
+		else if (starts_with(line, "Subject: "))
+			subject = true;
+	}
+	return from && date && subject;
+}
+
 static int count_messages(struct strbuf *all_msgs)
 {
 	int count = 0;
@@ -1472,14 +1487,7 @@ static int count_messages(struct strbuf *all_msgs)
 				if (!p) break;
 				p += 5;
 				count++;
-			} else {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p = strstr(p+7, "\nDate: ");
-				if (!p) break;
-				p = strstr(p+7, "\nSubject: ");
-				if (!p) break;
-				p += 10;
+			} else if (has_required_headers(p)) {
 				count++;
 			}
 		}
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index d1393ef197..d105fcb41c 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2863,4 +2863,26 @@ test_expect_success $PREREQ '--compose handles to headers' '
 	test_cmp expect msgtxt2.to
 '
 
+test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archive' '
+	write_script fake-imap-server <<-\EOF &&
+	printf "* PREAUTH [CAPABILITY IMAP4rev1 LITERAL+] ready\r\n"
+	printf "1 OK\r\n2 OK\r\n3 OK\r\n"
+	cat >/dev/null
+	EOF
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		HEAD^ &&
+	{
+		echo "From mboxrd@z Thu Jan  1 00:00:00 1970" &&
+		cat msgtxt1
+	} >archive.mbox &&
+	git imap-send --folder=INBOX <archive.mbox 2>err &&
+	test_grep "^Sending 1 message to INBOX folder" err &&
+	test_grep "(1/1) done" err
+'
+
 test_done
-- 
gitgitgadget
Junio C Hamano· Oct 30, 2026, 22:51 UTC · re: Harald Nordgren via GitGitGadget · lore

Re: [PATCH v4 2/4] imap-send: accept messages with Subject before Date

"Harald Nordgren via GitGitGadget" <gitgitgadget@gmail.com> writes:
Show 14 quoted lines
> +static bool has_required_headers(const char *line)
> +{
> +	bool from = false, date = false, subject = false;
> +
> +	while ((line = strchr(line, '\n')) && *++line != '\n') {
> +		if (starts_with(line, "From: "))
> +			from = true;
> +		else if (starts_with(line, "Date: "))
> +			date = true;
> +		else if (starts_with(line, "Subject: "))
> +			subject = true;
> +	}
> +	return from && date && subject;
> +}

The above assumes that an empty line that denotes the end of the header can be detected by looking for '\n\n' in the line[]. So this may be scanning thru to the end of the message including the body, because each and every line ends with CRLF and there is no two consecutive LF appearing in the line.

I admit that it was eons ago I read RFCs on IMAP (like RFC 2060), but I recall that its definition explicitly called for CRLF line endings, and existing code in imap-send.c before these patches seems to expect that it is reading CRLF delimited lines.

I didn't look at other parts of the series, but this one stood out.
Harald Nordgren· Oct 31, 2026, 07:58 UTC · re: Junio C Hamano · lore

Re: [PATCH v4 2/4] imap-send: accept messages with Subject before Date

Show 25 quoted lines
> > +static bool has_required_headers(const char *line)
> > +{
> > +     bool from = false, date = false, subject = false;
> > +
> > +     while ((line = strchr(line, '\n')) && *++line != '\n') {
> > +             if (starts_with(line, "From: "))
> > +                     from = true;
> > +             else if (starts_with(line, "Date: "))
> > +                     date = true;
> > +             else if (starts_with(line, "Subject: "))
> > +                     subject = true;
> > +     }
> > +     return from && date && subject;
> > +}
>
> The above assumes that an empty line that denotes the end of the
> header can be detected by looking for '\n\n' in the line[].  So this
> may be scanning thru to the end of the message including the body,
> because each and every line ends with CRLF and there is no two
> consecutive LF appearing in the line.
>
> I admit that it was eons ago I read RFCs on IMAP (like RFC 2060),
> but I recall that its definition explicitly called for CRLF line
> endings, and existing code in imap-send.c before these patches seems
> to expect that it is reading CRLF delimited lines.
Thanks for pointing this out, I'll take a look.
Harald
Harald Nordgren via GitGitGadget· Oct 27, 2026, 08:31 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v4 3/4] imap-send: check git send-email copies like other messages

From: Harald Nordgren <haraldnordgren@gmail.com>

The copy that git send-email --imap-sent-folder pipes to imap-send starts with a "From git-send-email" line. 04133f5bc4 (send-email: add ability to send a copy of sent emails to an IMAP folder, 2025-08-12) made imap-send count such a message when it has From and To headers, without looking for Date and Subject. Without that special case the copy would not be counted at all, because git send-email writes Subject before Date.

Now that the headers are accepted in any order, the special case is no longer needed. The copy carries the same headers git send-email sends over SMTP, which always include From, Subject and Date, so it passes the check for other messages. A To header is no longer required, the same as for any other message.

This makes the check stricter when some other script writes an mbox with a "From git-send-email" line. RFC 5322 requires a Date header but only recommends Subject, so a valid message from such a script may lack a Subject and would no longer be counted. imap-send has always required a Subject under any other "From " line, though, and only refuses an mbox when none of its messages are counted, so treating these copies like the rest should not affect anyone in practice.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           | 15 ++-------------
 t/t9001-send-email.sh | 13 +++++++++++++
 2 files changed, 15 insertions(+), 13 deletions(-)
Show changes to 2 files +15 −13

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index 8977a934f8..706c517e51 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1478,19 +1478,8 @@ static int count_messages(struct strbuf *all_msgs)
 	char *p = all_msgs->buf;
 
 	while (1) {
-		if (starts_with(p, "From ")) {
-			if (starts_with(p, "From git-send-email")) {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p += 7;
-				p = strstr(p, "\nTo: ");
-				if (!p) break;
-				p += 5;
-				count++;
-			} else if (has_required_headers(p)) {
-				count++;
-			}
-		}
+		if (starts_with(p, "From ") && has_required_headers(p))
+			count++;
 		p = strstr(p+5, "\nFrom ");
 		if (!p)
 			break;
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index d105fcb41c..1f77c14315 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2885,4 +2885,17 @@ test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archiv
 	test_grep "(1/1) done" err
 '
 
+test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch' '
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		--imap-sent-folder=Sent \
+		HEAD^ 2>err &&
+	test_grep "^Sending 1 message to Sent folder" err &&
+	test_grep "(1/1) done" err
+'
+
 test_done
-- 
gitgitgadget
Patrick Steinhardt· Oct 27, 2026, 10:59 UTC · re: Harald Nordgren via GitGitGadget · lore

Re: [PATCH v4 3/4] imap-send: check git send-email copies like other messages

On Tue, Oct 27, 2026 at 08:31:18AM +0000, Harald Nordgren via GitGitGadget wrote:
Show 7 quoted lines
> This makes the check stricter when some other script writes an mbox
> with a "From git-send-email" line. RFC 5322 requires a Date header but
> only recommends Subject, so a valid message from such a script may
> lack a Subject and would no longer be counted. imap-send has always
> required a Subject under any other "From " line, though, and only
> refuses an mbox when none of its messages are counted, so treating
> these copies like the rest should not affect anyone in practice.

Makes sense. I think it's highly unlikely that anyone ever formats patch series without a Subject and then expects everything to "just work". So even though we are tightening checks, I don't see that this will have any negative consequences.

Patrick
Harald Nordgren via GitGitGadget· Oct 27, 2026, 08:31 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v4 4/4] imap-send: match header names case insensitively

From: Harald Nordgren <haraldnordgren@gmail.com>

imap-send says "no messages found to send" for a message whose headers are spelled "from:", "date:" or "subject:", although RFC 5322 treats header field names as case insensitive. Match the three names in any case.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           |  6 +++---
 t/t9001-send-email.sh | 15 +++++++++++++++
 2 files changed, 18 insertions(+), 3 deletions(-)
Show changes to 2 files +18 −3

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index 706c517e51..b8dac92c70 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1462,11 +1462,11 @@ static bool has_required_headers(const char *line)
 	bool from = false, date = false, subject = false;
 
 	while ((line = strchr(line, '\n')) && *++line != '\n') {
-		if (starts_with(line, "From: "))
+		if (istarts_with(line, "From: "))
 			from = true;
-		else if (starts_with(line, "Date: "))
+		else if (istarts_with(line, "Date: "))
 			date = true;
-		else if (starts_with(line, "Subject: "))
+		else if (istarts_with(line, "Subject: "))
 			subject = true;
 	}
 	return from && date && subject;
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index 1f77c14315..897473e786 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2898,4 +2898,19 @@ test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch'
 	test_grep "(1/1) done" err
 '
 
+test_expect_success $PREREQ 'imap-send matches header names in any case' '
+	test_config imap.tunnel ./fake-imap-server &&
+	cat >lowercase.mbox <<-\EOF &&
+	From mboxrd@z Thu Jan  1 00:00:00 1970
+	from: Example <from@example.com>
+	date: Thu, 1 Jan 1970 00:00:00 +0000
+	subject: [PATCH] lowercase headers
+
+	body
+	EOF
+	git imap-send --folder=INBOX <lowercase.mbox 2>err &&
+	test_grep "^Sending 1 message to INBOX folder" err &&
+	test_grep "(1/1) done" err
+'
+
 test_done
-- 
gitgitgadget
Patrick Steinhardt· Oct 27, 2026, 10:59 UTC · re: Harald Nordgren via GitGitGadget · lore

Re: [PATCH v4 0/4] Fix imap-send with reordered message headers

On Tue, Oct 27, 2026 at 08:31:15AM +0000, Harald Nordgren via GitGitGadget wrote:
Show 10 quoted lines
> git imap-send rejected patches sent with git send-email, such as ones
> fetched from lore with b4, because it expected Date before Subject.
> 
> Changes in v4:
> 
>  * Store the tunnel as an allocated pointer, so it is only finished when it
>    was started, and drop the local tunnel variable
>  * Add a test for lowercase from:, date: and subject: headers
>  * Explain in the commit message why dropping the From git-send-email
>    special case is fine for mboxes written by other scripts
Thanks, I'm happy with this version.
Patrick
Junio C Hamano· Oct 29, 2026, 14:29 UTC · re: Patrick Steinhardt · lore

Re: [PATCH v4 0/4] Fix imap-send with reordered message headers

Patrick Steinhardt <ps@pks.im> writes:
Show 15 quoted lines
> On Tue, Oct 27, 2026 at 08:31:15AM +0000, Harald Nordgren via GitGitGadget wrote:
>> git imap-send rejected patches sent with git send-email, such as ones
>> fetched from lore with b4, because it expected Date before Subject.
>> 
>> Changes in v4:
>> 
>>  * Store the tunnel as an allocated pointer, so it is only finished when it
>>    was started, and drop the local tunnel variable
>>  * Add a test for lowercase from:, date: and subject: headers
>>  * Explain in the commit message why dropping the From git-send-email
>>    special case is fine for mboxes written by other scripts
>
> Thanks, I'm happy with this version.
>
> Patrick
Thanks.
Harald Nordgren via GitGitGadget· Nov 1, 2026, 08:21 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v5 0/4] imap-send: fix with reordered message headers

git imap-send rejected patches sent with git send-email, such as ones fetched from lore with b4, because it expected Date before Subject.

Changes in v5:
 * End the header block at an empty line in a CRLF mbox as well, so headers
   in the body or in later messages no longer count. Add a test.
Changes in v4:
 * Store the tunnel as an allocated pointer, so it is only finished when it
   was started, and drop the local tunnel variable
 * Add a test for lowercase from:, date: and subject: headers
 * Explain in the commit message why dropping the From git-send-email
   special case is fine for mboxes written by other scripts
Changes in v3:
 * Split concerns into different commits, commit message explains what they
   do.
 * Handle tunnel leak in the proper place.
Changes in v2:
 * Case insensitive header match according to RFC 822/2822/5322.
Harald Nordgren (4):
  imap-send: wait for the tunnel command to finish
  imap-send: accept messages with Subject before Date
  imap-send: check git send-email copies like other messages
  imap-send: match header names case insensitively
 imap-send.c           | 61 ++++++++++++++++++++-------------------
 t/t9001-send-email.sh | 66 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 98 insertions(+), 29 deletions(-)
base-commit: 50fe5cc63f9cd6e89b87a2c9ccf96a444f4cfdb7
Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-git-2456%2FHaraldNordgren%2Ffix%2Fimap-send-header-order-v5
Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-git-2456/HaraldNordgren/fix/imap-send-header-order-v5
Pull-Request: https://github.com/git/git/pull/2456
Range-diff vs v4:
 1:  8941ae12bb = 1:  cb27ac3a27 imap-send: wait for the tunnel command to finish
 2:  ad3158b336 ! 2:  fa90c64a5c imap-send: accept messages with Subject before Date
     @@ Commit message
          and git send-email writes Subject before Date.
      
          Accept the three headers in any order within the header block of each
     -    message.
     +    message, which ends at the first empty line whether the mbox uses LF or
     +    CRLF line endings.
      
          Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
      
     @@ imap-send.c: static void wrap_in_html(struct strbuf *msg)
      +{
      +	bool from = false, date = false, subject = false;
      +
     -+	while ((line = strchr(line, '\n')) && *++line != '\n') {
     ++	while ((line = strchr(line, '\n')) &&
     ++	       *++line != '\n' && !starts_with(line, "\r\n")) {
      +		if (starts_with(line, "From: "))
      +			from = true;
      +		else if (starts_with(line, "Date: "))
     @@ t/t9001-send-email.sh: test_expect_success $PREREQ '--compose handles to headers
      +	test_grep "^Sending 1 message to INBOX folder" err &&
      +	test_grep "(1/1) done" err
      +'
     ++
     ++test_expect_success $PREREQ 'imap-send ignores headers in the body of a CRLF mbox' '
     ++	test_config imap.tunnel ./fake-imap-server &&
     ++	q_to_cr >crlf.mbox <<-\EOF &&
     ++	From mboxrd@z Thu Jan  1 00:00:00 1970Q
     ++	From: Example <from@example.com>Q
     ++	Subject: Fwd: originalQ
     ++	Q
     ++	---------- Forwarded message ---------Q
     ++	From: Someone <someone@example.com>Q
     ++	Date: Thu, 1 Jan 1970 00:00:00 +0000Q
     ++	Subject: originalQ
     ++	EOF
     ++	test_must_fail git imap-send --folder=INBOX <crlf.mbox 2>err &&
     ++	test_grep "no messages found to send" err
     ++'
      +
       test_done
 3:  919e38c31d ! 3:  8646185486 imap-send: check git send-email copies like other messages
     @@ imap-send.c: static int count_messages(struct strbuf *all_msgs)
       			break;
      
       ## t/t9001-send-email.sh ##
     -@@ t/t9001-send-email.sh: test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archiv
     - 	test_grep "(1/1) done" err
     +@@ t/t9001-send-email.sh: test_expect_success $PREREQ 'imap-send ignores headers in the body of a CRLF mbo
     + 	test_grep "no messages found to send" err
       '
       
      +test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch' '
 4:  5b4a08bbda ! 4:  e7d1d7cd72 imap-send: match header names case insensitively
     @@ Commit message
      
       ## imap-send.c ##
      @@ imap-send.c: static bool has_required_headers(const char *line)
     - 	bool from = false, date = false, subject = false;
       
     - 	while ((line = strchr(line, '\n')) && *++line != '\n') {
     + 	while ((line = strchr(line, '\n')) &&
     + 	       *++line != '\n' && !starts_with(line, "\r\n")) {
      -		if (starts_with(line, "From: "))
      +		if (istarts_with(line, "From: "))
       			from = true;
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Nov 1, 2026, 08:21 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v5 1/4] imap-send: wait for the tunnel command to finish

From: Harald Nordgren <haraldnordgren@gmail.com>

When imap.tunnel is set, git imap-send starts the tunnel command but never waits for it to exit, and leaks the argument list it was started with. The isync code that imap-send was derived from forked the tunnel without ever reaping it, and c94d2dd080 (imap-send: use run-command API for tunneling, 2009-10-19) carried that over.

Wait for the tunnel when closing the connection. By then imap-send has closed its end of the pipes, so the tunnel sees the end of its input and exits. No test sets imap.tunnel, so the leak checker has not caught this.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c | 23 ++++++++++++++---------
 1 file changed, 14 insertions(+), 9 deletions(-)
Show changes to imap-send.c +14 −9
diff --git a/imap-send.c b/imap-send.c
index bf1d2cf74d..7e74c37a72 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -118,6 +118,7 @@ struct imap {
 	/* command queue */
 	int nexttag, num_in_progress, literal_pending;
 	struct imap_cmd *in_progress, **in_progress_append;
+	struct child_process *tunnel;
 	struct imap_buffer buf; /* this is BIG, so put it last */
 };
 
@@ -858,6 +859,10 @@ static void imap_close_server(struct imap_store *ictx)
 		imap_exec(ictx, NULL, "LOGOUT");
 		socket_shutdown(&imap->buf.sock);
 	}
+	if (imap->tunnel) {
+		finish_command(imap->tunnel);
+		free(imap->tunnel);
+	}
 	free(imap);
 }
 
@@ -1163,19 +1168,19 @@ static struct imap_store *imap_open_store(struct imap_server_conf *srvc, const c
 	/* open connection to IMAP server */
 
 	if (srvc->tunnel) {
-		struct child_process tunnel = CHILD_PROCESS_INIT;
-
 		imap_info("Starting tunnel '%s'... ", srvc->tunnel);
 
-		strvec_push(&tunnel.args, srvc->tunnel);
-		tunnel.use_shell = 1;
-		tunnel.in = -1;
-		tunnel.out = -1;
-		if (start_command(&tunnel))
+		imap->tunnel = xmalloc(sizeof(*imap->tunnel));
+		child_process_init(imap->tunnel);
+		strvec_push(&imap->tunnel->args, srvc->tunnel);
+		imap->tunnel->use_shell = 1;
+		imap->tunnel->in = -1;
+		imap->tunnel->out = -1;
+		if (start_command(imap->tunnel))
 			die("cannot start proxy %s", srvc->tunnel);
 
-		imap->buf.sock.fd[0] = tunnel.out;
-		imap->buf.sock.fd[1] = tunnel.in;
+		imap->buf.sock.fd[0] = imap->tunnel->out;
+		imap->buf.sock.fd[1] = imap->tunnel->in;
 
 		imap_info("OK\n");
 	} else {
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Nov 1, 2026, 08:21 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v5 2/4] imap-send: accept messages with Subject before Date

From: Harald Nordgren <haraldnordgren@gmail.com>

Patches sent with git send-email cannot be fed back into git imap-send after fetching them from the mailing list archive, for example with

    b4 mbox --single-message -o - <url> | git imap-send

which fails with "no messages found to send". imap-send only counts a message when its From, Date and Subject headers come in that order, and git send-email writes Subject before Date.

Accept the three headers in any order within the header block of each message, which ends at the first empty line whether the mbox uses LF or CRLF line endings.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           | 25 +++++++++++++++++--------
 t/t9001-send-email.sh | 38 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 55 insertions(+), 8 deletions(-)
Show changes to 2 files +55 −8

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index 7e74c37a72..391bd8b96a 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1457,6 +1457,22 @@ static void wrap_in_html(struct strbuf *msg)
 	*msg = buf;
 }
 
+static bool has_required_headers(const char *line)
+{
+	bool from = false, date = false, subject = false;
+
+	while ((line = strchr(line, '\n')) &&
+	       *++line != '\n' && !starts_with(line, "\r\n")) {
+		if (starts_with(line, "From: "))
+			from = true;
+		else if (starts_with(line, "Date: "))
+			date = true;
+		else if (starts_with(line, "Subject: "))
+			subject = true;
+	}
+	return from && date && subject;
+}
+
 static int count_messages(struct strbuf *all_msgs)
 {
 	int count = 0;
@@ -1472,14 +1488,7 @@ static int count_messages(struct strbuf *all_msgs)
 				if (!p) break;
 				p += 5;
 				count++;
-			} else {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p = strstr(p+7, "\nDate: ");
-				if (!p) break;
-				p = strstr(p+7, "\nSubject: ");
-				if (!p) break;
-				p += 10;
+			} else if (has_required_headers(p)) {
 				count++;
 			}
 		}
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index d1393ef197..4611e6d4a6 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2863,4 +2863,42 @@ test_expect_success $PREREQ '--compose handles to headers' '
 	test_cmp expect msgtxt2.to
 '
 
+test_expect_success $PREREQ 'imap-send accepts a sent patch from the list archive' '
+	write_script fake-imap-server <<-\EOF &&
+	printf "* PREAUTH [CAPABILITY IMAP4rev1 LITERAL+] ready\r\n"
+	printf "1 OK\r\n2 OK\r\n3 OK\r\n"
+	cat >/dev/null
+	EOF
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		HEAD^ &&
+	{
+		echo "From mboxrd@z Thu Jan  1 00:00:00 1970" &&
+		cat msgtxt1
+	} >archive.mbox &&
+	git imap-send --folder=INBOX <archive.mbox 2>err &&
+	test_grep "^Sending 1 message to INBOX folder" err &&
+	test_grep "(1/1) done" err
+'
+
+test_expect_success $PREREQ 'imap-send ignores headers in the body of a CRLF mbox' '
+	test_config imap.tunnel ./fake-imap-server &&
+	q_to_cr >crlf.mbox <<-\EOF &&
+	From mboxrd@z Thu Jan  1 00:00:00 1970Q
+	From: Example <from@example.com>Q
+	Subject: Fwd: originalQ
+	Q
+	---------- Forwarded message ---------Q
+	From: Someone <someone@example.com>Q
+	Date: Thu, 1 Jan 1970 00:00:00 +0000Q
+	Subject: originalQ
+	EOF
+	test_must_fail git imap-send --folder=INBOX <crlf.mbox 2>err &&
+	test_grep "no messages found to send" err
+'
+
 test_done
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Nov 1, 2026, 08:21 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v5 3/4] imap-send: check git send-email copies like other messages

From: Harald Nordgren <haraldnordgren@gmail.com>

The copy that git send-email --imap-sent-folder pipes to imap-send starts with a "From git-send-email" line. 04133f5bc4 (send-email: add ability to send a copy of sent emails to an IMAP folder, 2025-08-12) made imap-send count such a message when it has From and To headers, without looking for Date and Subject. Without that special case the copy would not be counted at all, because git send-email writes Subject before Date.

Now that the headers are accepted in any order, the special case is no longer needed. The copy carries the same headers git send-email sends over SMTP, which always include From, Subject and Date, so it passes the check for other messages. A To header is no longer required, the same as for any other message.

This makes the check stricter when some other script writes an mbox with a "From git-send-email" line. RFC 5322 requires a Date header but only recommends Subject, so a valid message from such a script may lack a Subject and would no longer be counted. imap-send has always required a Subject under any other "From " line, though, and only refuses an mbox when none of its messages are counted, so treating these copies like the rest should not affect anyone in practice.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           | 15 ++-------------
 t/t9001-send-email.sh | 13 +++++++++++++
 2 files changed, 15 insertions(+), 13 deletions(-)
Show changes to 2 files +15 −13

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index 391bd8b96a..d7172078c3 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1479,19 +1479,8 @@ static int count_messages(struct strbuf *all_msgs)
 	char *p = all_msgs->buf;
 
 	while (1) {
-		if (starts_with(p, "From ")) {
-			if (starts_with(p, "From git-send-email")) {
-				p = strstr(p+5, "\nFrom: ");
-				if (!p) break;
-				p += 7;
-				p = strstr(p, "\nTo: ");
-				if (!p) break;
-				p += 5;
-				count++;
-			} else if (has_required_headers(p)) {
-				count++;
-			}
-		}
+		if (starts_with(p, "From ") && has_required_headers(p))
+			count++;
 		p = strstr(p+5, "\nFrom ");
 		if (!p)
 			break;
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index 4611e6d4a6..5e03b350c1 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2901,4 +2901,17 @@ test_expect_success $PREREQ 'imap-send ignores headers in the body of a CRLF mbo
 	test_grep "no messages found to send" err
 '
 
+test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch' '
+	test_config imap.tunnel ./fake-imap-server &&
+	clean_fake_sendmail &&
+	git send-email \
+		--from="Example <from@example.com>" \
+		--to=nobody@example.com \
+		--smtp-server="$(pwd)/fake.sendmail" \
+		--imap-sent-folder=Sent \
+		HEAD^ 2>err &&
+	test_grep "^Sending 1 message to Sent folder" err &&
+	test_grep "(1/1) done" err
+'
+
 test_done
-- 
gitgitgadget
Harald Nordgren via GitGitGadget· Nov 1, 2026, 08:21 UTC · re: Harald Nordgren via GitGitGadget · lore

[PATCH v5 4/4] imap-send: match header names case insensitively

From: Harald Nordgren <haraldnordgren@gmail.com>

imap-send says "no messages found to send" for a message whose headers are spelled "from:", "date:" or "subject:", although RFC 5322 treats header field names as case insensitive. Match the three names in any case.

Signed-off-by: Harald Nordgren <haraldnordgren@gmail.com>
---
 imap-send.c           |  6 +++---
 t/t9001-send-email.sh | 15 +++++++++++++++
 2 files changed, 18 insertions(+), 3 deletions(-)
Show changes to 2 files +18 −3

imap-send.c, t/t9001-send-email.sh

diff --git a/imap-send.c b/imap-send.c
index d7172078c3..1b5850a80e 100644
--- a/imap-send.c
+++ b/imap-send.c
@@ -1463,11 +1463,11 @@ static bool has_required_headers(const char *line)
 
 	while ((line = strchr(line, '\n')) &&
 	       *++line != '\n' && !starts_with(line, "\r\n")) {
-		if (starts_with(line, "From: "))
+		if (istarts_with(line, "From: "))
 			from = true;
-		else if (starts_with(line, "Date: "))
+		else if (istarts_with(line, "Date: "))
 			date = true;
-		else if (starts_with(line, "Subject: "))
+		else if (istarts_with(line, "Subject: "))
 			subject = true;
 	}
 	return from && date && subject;
diff --git a/t/t9001-send-email.sh b/t/t9001-send-email.sh
index 5e03b350c1..37d21424c9 100755
--- a/t/t9001-send-email.sh
+++ b/t/t9001-send-email.sh
@@ -2914,4 +2914,19 @@ test_expect_success $PREREQ '--imap-sent-folder stores a copy of the sent patch'
 	test_grep "(1/1) done" err
 '
 
+test_expect_success $PREREQ 'imap-send matches header names in any case' '
+	test_config imap.tunnel ./fake-imap-server &&
+	cat >lowercase.mbox <<-\EOF &&
+	From mboxrd@z Thu Jan  1 00:00:00 1970
+	from: Example <from@example.com>
+	date: Thu, 1 Jan 1970 00:00:00 +0000
+	subject: [PATCH] lowercase headers
+
+	body
+	EOF
+	git imap-send --folder=INBOX <lowercase.mbox 2>err &&
+	test_grep "^Sending 1 message to INBOX folder" err &&
+	test_grep "(1/1) done" err
+'
+
 test_done
-- 
gitgitgadget

← back to recent threads