[PECL-CVS] [pecl-mail-mailparse] master: Merge pull request #47 from php/fix/gh20-content-id-parentheses

[email protected] (Rasmus Lerdorf via GitHub) Sun, 5 Apr 2026 11:51:27 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Rasmus Lerdorf (rlerdorf)
Committer: GitHub (web-flow)
Pusher: rlerdorf
Date: 2026-04-05T12:51:24+01:00

Commit: https://github.com/php/pecl-mail-mailparse/commit/8788b362587af24d2ca08d180f813bff46598e13
Raw diff: https://github.com/php/pecl-mail-mailparse/commit/8788b362587af24d2ca08d180f813bff46598e13.diff

Merge pull request #47 from php/fix/gh20-content-id-parentheses

fixes #20

Changed paths:
  A  tests/gh20.phpt
  M  mailparse.c


Diff:

diff --git a/mailparse.c b/mailparse.c
index dba3309..4dbdffd 100644
--- a/mailparse.c
+++ b/mailparse.c
@@ -1460,7 +1460,7 @@ static int mailparse_get_part_data(php_mimepart *part, zval *return_value)
 	zval headers, *tmpval;
 	off_t startpos, endpos, bodystart;
 	int nlines, nbodylines;
-	/* extract the address part of the content-id only */
+	/* extract the content-id value */
 	zend_string *hash_key = zend_string_init("content-id", sizeof("content-id") - 1, 0);
 
 	array_init(return_value);
@@ -1509,15 +1509,52 @@ static int mailparse_get_part_data(php_mimepart *part, zval *return_value)
 		add_assoc_string(return_value, "content-boundary", part->boundary);
 
 	if ((tmpval = zend_hash_find(Z_ARRVAL_P(&headers), hash_key)) != NULL) {
-		php_rfc822_tokenized_t *toks;
-		php_rfc822_addresses_t *addrs;
-
-		toks = php_mailparse_rfc822_tokenize(Z_STRVAL_P(tmpval), 1);
-		addrs = php_rfc822_parse_address_tokens(toks);
-		if (addrs->naddrs > 0)
-			add_assoc_string(return_value, "content-id", addrs->addrs[0].address);
-		php_rfc822_free_addresses(addrs);
-		php_rfc822_tokenize_free(toks);
+		zval *content_id = tmpval;
+		char *id, *close;
+		size_t len;
+
+		/* Handle multiple Content-ID headers (stored as array) */
+		if (Z_TYPE_P(content_id) == IS_ARRAY) {
+			content_id = zend_hash_index_find(Z_ARRVAL_P(content_id), 0);
+		}
+
+		if (content_id != NULL && Z_TYPE_P(content_id) == IS_STRING) {
+			/* Extract the Content-ID value directly. Trim surrounding
+			 * horizontal whitespace, and if the first non-whitespace
+			 * character is '<', return the text up to the first '>';
+			 * otherwise return the trimmed value unchanged (GH-20). */
+			id = Z_STRVAL_P(content_id);
+			len = Z_STRLEN_P(content_id);
+
+			while (len > 0 && (id[0] == ' ' || id[0] == '\t')) {
+				id++;
+				len--;
+			}
+			while (len > 0 && (id[len-1] == ' ' || id[len-1] == '\t')) {
+				len--;
+			}
+
+			if (len >= 2 && id[0] == '<') {
+				close = memchr(id + 1, '>', len - 1);
+				if (close) {
+					add_assoc_stringl(return_value, "content-id", id + 1, close - id - 1);
+				} else {
+					/* Malformed: '<' without '>' — skip the '<' and trim */
+					id++;
+					len--;
+					while (len > 0 && (id[0] == ' ' || id[0] == '\t')) {
+						id++;
+						len--;
+					}
+					while (len > 0 && (id[len-1] == ' ' || id[len-1] == '\t')) {
+						len--;
+					}
+					add_assoc_stringl(return_value, "content-id", id, len);
+				}
+			} else {
+				add_assoc_stringl(return_value, "content-id", id, len);
+			}
+		}
 	}
 	zend_string_release(hash_key);
 
diff --git a/tests/gh20.phpt b/tests/gh20.phpt
new file mode 100644
index 0000000..ab13616
--- /dev/null
+++ b/tests/gh20.phpt
@@ -0,0 +1,89 @@
+--TEST--
+GH issue #20 (Unexpected parsed value of content-id with parentheses)
+--SKIPIF--
+<?php
+if (!extension_loaded("mailparse")) die("skip mailparse extension not available");
+?>
+--FILE--
+<?php
+$mime = "Content-Type: image/png\r\n" .
+    "Content-Transfer-Encoding: base64\r\n" .
+    "Content-ID: <Facebook_32x32(1)_aa284ba9-f148-4698-9c1f-c8e92bdb842e.png>\r\n" .
+    "\r\n" .
+    "iVBOR\r\n";
+
+$resource = mailparse_msg_create();
+mailparse_msg_parse($resource, $mime);
+
+$part = mailparse_msg_get_part($resource, 1);
+$data = mailparse_msg_get_part_data($part);
+
+echo "content-id: " . $data['content-id'] . "\n";
+
+/* Also test bare content-id without angle brackets */
+$mime2 = "Content-Type: image/png\r\n" .
+    "Content-Transfer-Encoding: base64\r\n" .
+    "Content-ID: Facebook_32x32(1)_test.png\r\n" .
+    "\r\n" .
+    "iVBOR\r\n";
+
+$resource2 = mailparse_msg_create();
+mailparse_msg_parse($resource2, $mime2);
+
+$part2 = mailparse_msg_get_part($resource2, 1);
+$data2 = mailparse_msg_get_part_data($part2);
+
+echo "content-id bare: " . $data2['content-id'] . "\n";
+
+/* Standard RFC-compliant content-id */
+$mime3 = "Content-Type: image/png\r\n" .
+    "Content-ID: <[email protected]>\r\n" .
+    "\r\n" .
+    "iVBOR\r\n";
+
+$resource3 = mailparse_msg_create();
+mailparse_msg_parse($resource3, $mime3);
+
+$part3 = mailparse_msg_get_part($resource3, 1);
+$data3 = mailparse_msg_get_part_data($part3);
+
+echo "content-id rfc: " . $data3['content-id'] . "\n";
+
+/* RFC-compliant content-id with trailing comment */
+$mime4 = "Content-Type: image/png\r\n" .
+    "Content-ID: <[email protected]> (comment)\r\n" .
+    "\r\n" .
+    "iVBOR\r\n";
+
+$resource4 = mailparse_msg_create();
+mailparse_msg_parse($resource4, $mime4);
+
+$part4 = mailparse_msg_get_part($resource4, 1);
+$data4 = mailparse_msg_get_part_data($part4);
+
+echo "content-id trailing comment: " . $data4['content-id'] . "\n";
+
+/* Duplicate Content-ID headers (stored as array internally) */
+$mime5 = "Content-Type: image/png\r\n" .
+    "Content-ID: <[email protected]>\r\n" .
+    "Content-ID: <[email protected]>\r\n" .
+    "\r\n" .
+    "iVBOR\r\n";
+
+$resource5 = mailparse_msg_create();
+mailparse_msg_parse($resource5, $mime5);
+
+$part5 = mailparse_msg_get_part($resource5, 1);
+$data5 = mailparse_msg_get_part_data($part5);
+
+echo "content-id duplicate: " . $data5['content-id'] . "\n";
+
+echo "ok\n";
+?>
+--EXPECT--
+content-id: Facebook_32x32(1)_aa284ba9-f148-4698-9c1f-c8e92bdb842e.png
+content-id bare: Facebook_32x32(1)_test.png
+content-id rfc: [email protected]
+content-id trailing comment: [email protected]
+content-id duplicate: [email protected]
+ok