[PECL-CVS] [pecl-mail-mailparse] fix/gh20-content-id-parentheses: address reviews

[email protected] (Rasmus Lerdorf) Sun, 5 Apr 2026 11:32:24 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Rasmus Lerdorf (rlerdorf)
Date: 2026-04-05T07:32:20-04:00

Commit: https://github.com/php/pecl-mail-mailparse/commit/5dae201982607ebe28254b39edddfc46e0dd4ed8
Raw diff: https://github.com/php/pecl-mail-mailparse/commit/5dae201982607ebe28254b39edddfc46e0dd4ed8.diff

address reviews

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


Diff:

diff --git a/mailparse.c b/mailparse.c
index 9894a76..c0459f4 100644
--- a/mailparse.c
+++ b/mailparse.c
@@ -1511,7 +1511,7 @@ static int mailparse_get_part_data(php_mimepart *part, zval *return_value)
 
 	if ((tmpval = zend_hash_find(Z_ARRVAL_P(&headers), hash_key)) != NULL) {
 		zval *content_id = tmpval;
-		char *id, *open, *close;
+		char *id, *close;
 		size_t len;
 
 		/* Handle multiple Content-ID headers (stored as array) */
@@ -1522,23 +1522,27 @@ static int mailparse_get_part_data(php_mimepart *part, zval *return_value)
 		if (content_id != NULL && Z_TYPE_P(content_id) == IS_STRING) {
 			/* Extract content-id value directly instead of parsing it as an
 			 * RFC 822 address, which incorrectly strips parenthesized text
-			 * as comments (GH-20). Extract the msg-id between '<' and '>'
-			 * if present, otherwise return the trimmed value as-is. */
+			 * as comments (GH-20). Trim whitespace, then strip angle brackets
+			 * only when '<' is the first character; otherwise return as-is. */
 			id = Z_STRVAL_P(content_id);
 			len = Z_STRLEN_P(content_id);
 
-			open = memchr(id, '<', len);
-			if (open) {
-				close = memchr(open + 1, '>', len - (open - id) - 1);
+			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", open + 1, close - open - 1);
+					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 {
-				/* No angle brackets — trim whitespace and return as-is */
-				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);
 			}
 		}
diff --git a/tests/gh20.phpt b/tests/gh20.phpt
index 3a8647e..ab13616 100644
--- a/tests/gh20.phpt
+++ b/tests/gh20.phpt
@@ -63,6 +63,21 @@ $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--
@@ -70,4 +85,5 @@ 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