From 4323731d1d52c8bc7196c3fbed7323c4bac7d1dc Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Fri, 25 Jul 2025 17:42:44 +0200 Subject: [PATCH] Fix GH-19188: Add support for new INI `mail.cr_lf_mode` --- NEWS | 1 + ext/standard/mail.c | 60 ++++++++++++++++++- .../tests/mail/gh19188_cr_lf_mode.phpt | 23 +++++++ .../tests/mail/gh19188_invalid_mode.phpt | 26 ++++++++ ext/standard/tests/mail/gh19188_lf_mode.phpt | 23 +++++++ .../tests/mail/gh19188_mixed_mode.phpt | 20 +++++++ .../tests/mail/gh19188_os_mode_unix.phpt | 30 ++++++++++ .../tests/mail/gh19188_os_mode_windows.phpt | 31 ++++++++++ main/main.c | 31 ++++++++++ main/php_globals.h | 1 + php.ini-development | 8 +++ php.ini-production | 8 +++ 12 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 ext/standard/tests/mail/gh19188_cr_lf_mode.phpt create mode 100644 ext/standard/tests/mail/gh19188_invalid_mode.phpt create mode 100644 ext/standard/tests/mail/gh19188_lf_mode.phpt create mode 100644 ext/standard/tests/mail/gh19188_mixed_mode.phpt create mode 100644 ext/standard/tests/mail/gh19188_os_mode_unix.phpt create mode 100644 ext/standard/tests/mail/gh19188_os_mode_windows.phpt diff --git a/NEWS b/NEWS index 961870b6f3053..6b2f0a06cad06 100644 --- a/NEWS +++ b/NEWS @@ -37,6 +37,7 @@ PHP NEWS (nielsdos) . Optimized PHP html_entity_decode function. (Artem Ukrainskiy) . Minor optimization to array_chunk(). (nielsdos) + . Implement GH-19188: Add support for new INI mail.cr_lf_mode. (alexandre-daubois) - URI: . Empty host handling is fixed. (Máté Kocsis) diff --git a/ext/standard/mail.c b/ext/standard/mail.c index 41e2a02078e78..702f8ce5bb5a3 100644 --- a/ext/standard/mail.c +++ b/ext/standard/mail.c @@ -494,7 +494,27 @@ PHPAPI bool php_mail(const char *to, const char *subject, const char *message, c MAIL_RET(false); } - char *line_sep = PG(mail_mixed_lf_and_crlf) ? "\n" : "\r\n"; + char *line_sep; + const char *cr_lf_mode = PG(mail_cr_lf_mode); + + if (cr_lf_mode && strcmp(cr_lf_mode, "crlf") != 0) { + if (strcmp(cr_lf_mode, "lf") == 0) { + line_sep = "\n"; + } else if (strcmp(cr_lf_mode, "mixed") == 0) { + line_sep = "\n"; + } else if (strcmp(cr_lf_mode, "os") == 0) { +#ifdef PHP_WIN32 + line_sep = "\r\n"; +#else + line_sep = "\n"; +#endif + } else { + ZEND_ASSERT(0 && "Unexpected cr_lf_mode value"); + } + } else { + /* CRLF is default mode, but respect mail.mixed_lf_and_crlf for backward compatibility */ + line_sep = PG(mail_mixed_lf_and_crlf) ? "\n" : "\r\n"; + } if (PG(mail_x_header)) { const char *tmp = zend_get_executed_filename(); @@ -586,7 +606,43 @@ PHPAPI bool php_mail(const char *to, const char *subject, const char *message, c if (hdr != NULL) { fprintf(sendmail, "%s%s", hdr, line_sep); } - fprintf(sendmail, "%s%s%s", line_sep, message, line_sep); + + fprintf(sendmail, "%s", line_sep); + + if (cr_lf_mode && strcmp(cr_lf_mode, "lf") == 0) { + char *converted_message = NULL; + size_t msg_len = strlen(message); + size_t new_len = 0; + + for (size_t i = 0; i < msg_len - 1; ++i) { + if (message[i] == '\r' && message[i + 1] == '\n') { + ++new_len; + } + } + + if (new_len == 0) { + fprintf(sendmail, "%s", message); + } else { + converted_message = emalloc(msg_len - new_len + 1); + size_t j = 0; + for (size_t i = 0; i < msg_len; ++i) { + if (i < msg_len - 1 && message[i] == '\r' && message[i + 1] == '\n') { + converted_message[j++] = '\n'; + ++i; /* skip LF part */ + } else { + converted_message[j++] = message[i]; + } + } + + converted_message[j] = '\0'; + fprintf(sendmail, "%s", converted_message); + efree(converted_message); + } + } else { + fprintf(sendmail, "%s", message); + } + + fprintf(sendmail, "%s", line_sep); #ifdef PHP_WIN32 ret = pclose(sendmail); diff --git a/ext/standard/tests/mail/gh19188_cr_lf_mode.phpt b/ext/standard/tests/mail/gh19188_cr_lf_mode.phpt new file mode 100644 index 0000000000000..15c9aa2aa28ff --- /dev/null +++ b/ext/standard/tests/mail/gh19188_cr_lf_mode.phpt @@ -0,0 +1,23 @@ +--TEST-- +GH-19188: new INI mail.cr_lf_mode +--INI-- +sendmail_path={MAIL:gh19188_cr_lf_mode.out} +mail.cr_lf_mode=crlf +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +CRLF mode: +int(5) +int(0) diff --git a/ext/standard/tests/mail/gh19188_invalid_mode.phpt b/ext/standard/tests/mail/gh19188_invalid_mode.phpt new file mode 100644 index 0000000000000..fb1baa3d4702d --- /dev/null +++ b/ext/standard/tests/mail/gh19188_invalid_mode.phpt @@ -0,0 +1,26 @@ +--TEST-- +GH-19188: mail.cr_lf_mode runtime changes should fail +--FILE-- + +--EXPECT-- +bool(false) +string(4) "crlf" +bool(false) +string(4) "crlf" +bool(false) +string(4) "crlf" +bool(false) +string(4) "crlf" diff --git a/ext/standard/tests/mail/gh19188_lf_mode.phpt b/ext/standard/tests/mail/gh19188_lf_mode.phpt new file mode 100644 index 0000000000000..bd69cc42b62df --- /dev/null +++ b/ext/standard/tests/mail/gh19188_lf_mode.phpt @@ -0,0 +1,23 @@ +--TEST-- +GH-19188: mail.cr_lf_mode=lf +--INI-- +sendmail_path={MAIL:gh19188_lf_mode.out} +mail.cr_lf_mode=lf +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +LF mode: +int(0) +int(6) diff --git a/ext/standard/tests/mail/gh19188_mixed_mode.phpt b/ext/standard/tests/mail/gh19188_mixed_mode.phpt new file mode 100644 index 0000000000000..fa5eb63bd4fe7 --- /dev/null +++ b/ext/standard/tests/mail/gh19188_mixed_mode.phpt @@ -0,0 +1,20 @@ +--TEST-- +GH-19188: mail.cr_lf_mode=mixed +--INI-- +sendmail_path={MAIL:gh19188_mixed_mode.out} +mail.cr_lf_mode=mixed +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +Mixed mode: +int(5) diff --git a/ext/standard/tests/mail/gh19188_os_mode_unix.phpt b/ext/standard/tests/mail/gh19188_os_mode_unix.phpt new file mode 100644 index 0000000000000..b365e3a671b03 --- /dev/null +++ b/ext/standard/tests/mail/gh19188_os_mode_unix.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-19188: mail.cr_lf_mode=os (Unix) +--SKIPIF-- + +--INI-- +sendmail_path={MAIL:gh19188_os_mode.out} +mail.cr_lf_mode=os +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +OS mode: +CRLF count: int(0) +LF-only count: int(5) diff --git a/ext/standard/tests/mail/gh19188_os_mode_windows.phpt b/ext/standard/tests/mail/gh19188_os_mode_windows.phpt new file mode 100644 index 0000000000000..c3c3e7c08ee9d --- /dev/null +++ b/ext/standard/tests/mail/gh19188_os_mode_windows.phpt @@ -0,0 +1,31 @@ +--TEST-- +GH-19188: mail.cr_lf_mode=os (Windows) +--SKIPIF-- + +--INI-- +sendmail_path={MAIL:gh19188_os_mode.out} +mail.cr_lf_mode=os +--FILE-- + +--CLEAN-- + +--EXPECT-- +bool(true) +OS mode: +CRLF count: int(5) +LF-only count: int(0) diff --git a/main/main.c b/main/main.c index 3518e4137ecef..fe47873b83753 100644 --- a/main/main.c +++ b/main/main.c @@ -703,6 +703,36 @@ static PHP_INI_MH(OnUpdateMailLog) } /* }}} */ +/* {{{ PHP_INI_MH */ +static PHP_INI_MH(OnUpdateMailCrLfMode) +{ + if (new_value) { + const char *val = ZSTR_VAL(new_value); + if (ZSTR_LEN(new_value) > 0 && + strcmp(val, "crlf") != 0 && + strcmp(val, "lf") != 0 && + strcmp(val, "mixed") != 0 && + strcmp(val, "os") != 0) { + int err_type; + + if (stage == ZEND_INI_STAGE_RUNTIME) { + err_type = E_WARNING; + } else { + err_type = E_ERROR; + } + + if (stage != ZEND_INI_STAGE_DEACTIVATE) { + php_error_docref(NULL, err_type, "Invalid value \"%s\" for mail.cr_lf_mode. Must be one of: \"crlf\", \"lf\", \"mixed\", \"os\"", val); + } + + return FAILURE; + } + } + OnUpdateString(entry, new_value, mh_arg1, mh_arg2, mh_arg3, stage); + return SUCCESS; +} +/* }}} */ + /* {{{ PHP_INI_MH */ static PHP_INI_MH(OnChangeMailForceExtra) { @@ -808,6 +838,7 @@ PHP_INI_BEGIN() PHP_INI_ENTRY("smtp_port", "25", PHP_INI_ALL, NULL) STD_PHP_INI_BOOLEAN("mail.add_x_header", "0", PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateBool, mail_x_header, php_core_globals, core_globals) STD_PHP_INI_BOOLEAN("mail.mixed_lf_and_crlf", "0", PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateBool, mail_mixed_lf_and_crlf, php_core_globals, core_globals) + STD_PHP_INI_ENTRY("mail.cr_lf_mode", "crlf", PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateMailCrLfMode, mail_cr_lf_mode, php_core_globals, core_globals) STD_PHP_INI_ENTRY("mail.log", NULL, PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateMailLog, mail_log, php_core_globals, core_globals) PHP_INI_ENTRY("browscap", NULL, PHP_INI_SYSTEM, OnChangeBrowscap) PHP_INI_ENTRY("memory_limit", "128M", PHP_INI_ALL, OnChangeMemoryLimit) diff --git a/main/php_globals.h b/main/php_globals.h index ab7a9a00b2f1d..988ff4f0bdc34 100644 --- a/main/php_globals.h +++ b/main/php_globals.h @@ -154,6 +154,7 @@ struct _php_core_globals { char *mail_log; bool mail_x_header; bool mail_mixed_lf_and_crlf; + char *mail_cr_lf_mode; bool in_error_log; diff --git a/php.ini-development b/php.ini-development index 6ef1f940b2f7e..e97affab50bc5 100644 --- a/php.ini-development +++ b/php.ini-development @@ -1101,6 +1101,14 @@ mail.add_x_header = Off ; RFC 2822 non conformant MTA. mail.mixed_lf_and_crlf = Off +; Control line ending mode for mail messages and headers. +; Possible values: "crlf" (default), "lf", "mixed", "os" +; - crlf: Use CRLF line endings +; - lf: Use LF line endings only (converts CRLF in message to LF) +; - mixed: Same as mail.mixed_lf_and_crlf = On +; - os: Use CRLF on Windows, LF on other systems +mail.cr_lf_mode = crlf + ; The path to a log file that will log all mail() calls. Log entries include ; the full path of the script, line number, To address and headers. ;mail.log = diff --git a/php.ini-production b/php.ini-production index d1b25a34a487f..2ae640c8a3418 100644 --- a/php.ini-production +++ b/php.ini-production @@ -1103,6 +1103,14 @@ mail.add_x_header = Off ; RFC 2822 non conformant MTA. mail.mixed_lf_and_crlf = Off +; Control line ending mode for mail messages and headers. +; Possible values: "crlf" (default), "lf", "mixed", "os" +; - crlf: Use CRLF line endings +; - lf: Use LF line endings only (converts CRLF in message to LF) +; - mixed: Same as mail.mixed_lf_and_crlf = On +; - os: Use CRLF on Windows, LF on other systems +mail.cr_lf_mode = crlf + ; The path to a log file that will log all mail() calls. Log entries include ; the full path of the script, line number, To address and headers. ;mail.log =