@@ -56,22 +56,11 @@ function () use ( &$filter_val ) {
5656 }
5757
5858 public function test_send () {
59- // The pre_wp_mail filter was introduced in WordPress 5.7.
60- if ( version_compare ( $ GLOBALS ['wp_version ' ], '5.7 ' , '< ' ) ) {
61- $ this ->markTestSkipped ( 'This test requires WordPress 5.7 or higher for the pre_wp_mail filter. ' );
62- }
59+ $ this ->skip_if_pre_wp_mail_unsupported ();
6360
6461 // Use pre_wp_mail to simulate successful email sending and capture attributes.
6562 $ captured_atts = null ;
66- add_filter (
67- 'pre_wp_mail ' ,
68- function ( $ short_circuit , $ atts ) use ( &$ captured_atts ) {
69- $ captured_atts = $ atts ;
70- return true ;
71- },
72- 10 ,
73- 2
74- );
63+ $ this ->capture_wp_mail_atts ( $ captured_atts );
7564
7665 $ to = 'test@example.com ' ;
7766 $ subject = 'Test Subject ' ;
@@ -88,10 +77,7 @@ function ( $short_circuit, $atts ) use ( &$captured_atts ) {
8877 }
8978
9079 public function test_send_failure () {
91- // The pre_wp_mail filter was introduced in WordPress 5.7.
92- if ( version_compare ( $ GLOBALS ['wp_version ' ], '5.7 ' , '< ' ) ) {
93- $ this ->markTestSkipped ( 'This test requires WordPress 5.7 or higher for the pre_wp_mail filter. ' );
94- }
80+ $ this ->skip_if_pre_wp_mail_unsupported ();
9581
9682 // Force wp_mail failure using pre_wp_mail filter.
9783 $ captured_atts = null ;
@@ -126,6 +112,161 @@ function ( $short_circuit, $atts ) use ( &$captured_atts ) {
126112 $ this ->assertEquals ( $ content , $ captured_atts ['message ' ], 'Content should match. ' );
127113 }
128114
115+ public function test_send_adds_default_content_type_header_when_none_supplied () {
116+ $ this ->skip_if_pre_wp_mail_unsupported ();
117+
118+ $ captured_atts = null ;
119+ $ this ->capture_wp_mail_atts ( $ captured_atts );
120+
121+ $ this ->email ->send ( 'test@example.com ' , 'Test Subject ' , '<p>HTML Content</p> ' );
122+
123+ $ this ->assertNotNull ( $ captured_atts , 'Attributes should be captured. ' );
124+ $ this ->assertIsArray ( $ captured_atts ['headers ' ], 'Headers should be passed as an array. ' );
125+ $ this ->assertContains (
126+ 'Content-Type: text/html; charset=UTF-8 ' ,
127+ $ captured_atts ['headers ' ],
128+ 'Default text/html Content-Type header should be present when no caller Content-Type is supplied. '
129+ );
130+ $ this ->assertSingleContentTypeHeader ( $ captured_atts ['headers ' ] );
131+ }
132+
133+ public function test_send_preserves_caller_supplied_content_type_header () {
134+ $ this ->skip_if_pre_wp_mail_unsupported ();
135+
136+ $ captured_atts = null ;
137+ $ this ->capture_wp_mail_atts ( $ captured_atts );
138+
139+ $ caller_content_type = 'Content-Type: text/plain; charset=ISO-8859-1 ' ;
140+ $ this ->email ->send (
141+ 'test@example.com ' ,
142+ 'Test Subject ' ,
143+ 'Plain content ' ,
144+ array ( $ caller_content_type )
145+ );
146+
147+ $ this ->assertNotNull ( $ captured_atts , 'Attributes should be captured. ' );
148+ $ this ->assertContains (
149+ $ caller_content_type ,
150+ $ captured_atts ['headers ' ],
151+ 'Caller-supplied Content-Type should be preserved verbatim. '
152+ );
153+ $ this ->assertNotContains (
154+ 'Content-Type: text/html; charset=UTF-8 ' ,
155+ $ captured_atts ['headers ' ],
156+ 'Default Content-Type should not be added when a caller Content-Type is already present. '
157+ );
158+ $ this ->assertSingleContentTypeHeader ( $ captured_atts ['headers ' ] );
159+ }
160+
161+ public function test_send_preserves_caller_supplied_content_type_header_case_insensitive () {
162+ $ this ->skip_if_pre_wp_mail_unsupported ();
163+
164+ $ captured_atts = null ;
165+ $ this ->capture_wp_mail_atts ( $ captured_atts );
166+
167+ $ caller_content_type = 'content-type: text/plain; charset=UTF-8 ' ;
168+ $ this ->email ->send (
169+ 'test@example.com ' ,
170+ 'Test Subject ' ,
171+ 'Plain content ' ,
172+ array ( $ caller_content_type )
173+ );
174+
175+ $ this ->assertNotNull ( $ captured_atts , 'Attributes should be captured. ' );
176+ $ this ->assertContains (
177+ $ caller_content_type ,
178+ $ captured_atts ['headers ' ],
179+ 'Lowercase caller Content-Type should be preserved verbatim. '
180+ );
181+ $ this ->assertNotContains (
182+ 'Content-Type: text/html; charset=UTF-8 ' ,
183+ $ captured_atts ['headers ' ],
184+ 'Default Content-Type should not be added when a Content-Type header is present in any case. '
185+ );
186+ $ this ->assertSingleContentTypeHeader ( $ captured_atts ['headers ' ] );
187+ }
188+
189+ public function test_send_preserves_multipart_alternative_when_text_content_provided () {
190+ $ captured_initial_content_type = null ;
191+ $ captured_alt_body = null ;
192+ $ captured_final_content_type = null ;
193+
194+ add_action (
195+ 'phpmailer_init ' ,
196+ function ( $ phpmailer ) use ( &$ captured_initial_content_type , &$ captured_alt_body , &$ captured_final_content_type ) {
197+ $ captured_initial_content_type = $ phpmailer ->ContentType ;
198+ // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- PHPMailer property.
199+ $ captured_alt_body = $ phpmailer ->AltBody ;
200+
201+ // Trigger body construction so PHPMailer applies its multipart/alternative
202+ // upgrade for the attached AltBody.
203+ $ phpmailer ->preSend ();
204+ $ captured_final_content_type = $ phpmailer ->ContentType ;
205+
206+ // Block delivery by clearing recipients.
207+ $ phpmailer ->clearAllRecipients ();
208+ },
209+ 999 // Run after Site Kit's hook (priority 10).
210+ );
211+
212+ // send() fails on the cleared recipients. Only the captured state matters.
213+ $ this ->email ->send (
214+ 'test@example.com ' ,
215+ 'Test Subject ' ,
216+ '<html><body>HTML Content</body></html> ' ,
217+ array (),
218+ 'Plain text content '
219+ );
220+
221+ $ this ->assertEquals ( 'text/html ' , $ captured_initial_content_type , 'PHPMailer ContentType should be text/html from the default Site Kit header before AltBody upgrade. ' );
222+ $ this ->assertEquals ( 'Plain text content ' , $ captured_alt_body , 'AltBody should be set so PHPMailer can produce a multipart/alternative message. ' );
223+ $ this ->assertStringStartsWith ( 'multipart/alternative ' , $ captured_final_content_type , 'PHPMailer should upgrade ContentType to multipart/alternative when AltBody is set alongside the default text/html Content-Type. ' );
224+ }
225+
226+ /**
227+ * Skips the test if the pre_wp_mail filter (WordPress 5.7+) isn't available.
228+ */
229+ private function skip_if_pre_wp_mail_unsupported () {
230+ if ( version_compare ( $ GLOBALS ['wp_version ' ], '5.7 ' , '< ' ) ) {
231+ $ this ->markTestSkipped ( 'This test requires WordPress 5.7 or higher for the pre_wp_mail filter. ' );
232+ }
233+ }
234+
235+ /**
236+ * Captures wp_mail arguments via pre_wp_mail and short-circuits delivery.
237+ *
238+ * @param array|null $captured_atts Reference filled with wp_mail args.
239+ *
240+ * @param-out array $captured_atts
241+ */
242+ private function capture_wp_mail_atts ( &$ captured_atts ) {
243+ add_filter (
244+ 'pre_wp_mail ' ,
245+ function ( $ short_circuit , $ atts ) use ( &$ captured_atts ) {
246+ $ captured_atts = $ atts ;
247+ return true ;
248+ },
249+ 10 ,
250+ 2
251+ );
252+ }
253+
254+ /**
255+ * Asserts that exactly one Content-Type header is present in the headers array.
256+ *
257+ * @param array $headers Headers array captured from wp_mail.
258+ */
259+ private function assertSingleContentTypeHeader ( array $ headers ) {
260+ $ content_type_headers = array_filter (
261+ $ headers ,
262+ static function ( $ header ) {
263+ return is_string ( $ header ) && 0 === stripos ( ltrim ( $ header ), 'Content-Type: ' );
264+ }
265+ );
266+
267+ $ this ->assertCount ( 1 , $ content_type_headers , 'Exactly one Content-Type header should be present. ' );
268+ }
269+
129270 public function test_get_last_error () {
130271 $ this ->assertNull ( $ this ->email ->get_last_error (), 'Last error should be null initially. ' );
131272 }
@@ -159,10 +300,7 @@ function ( $phpmailer ) use ( &$captured_alt_body ) {
159300 }
160301
161302 public function test_send_does_not_set_alt_body_when_text_content_empty () {
162- // The pre_wp_mail filter was introduced in WordPress 5.7.
163- if ( version_compare ( $ GLOBALS ['wp_version ' ], '5.7 ' , '< ' ) ) {
164- $ this ->markTestSkipped ( 'This test requires WordPress 5.7 or higher for the pre_wp_mail filter. ' );
165- }
303+ $ this ->skip_if_pre_wp_mail_unsupported ();
166304
167305 $ alt_body_was_set = false ;
168306
@@ -198,10 +336,7 @@ function () {
198336 }
199337
200338 public function test_send_removes_phpmailer_init_hook_after_send () {
201- // The pre_wp_mail filter was introduced in WordPress 5.7.
202- if ( version_compare ( $ GLOBALS ['wp_version ' ], '5.7 ' , '< ' ) ) {
203- $ this ->markTestSkipped ( 'This test requires WordPress 5.7 or higher for the pre_wp_mail filter. ' );
204- }
339+ $ this ->skip_if_pre_wp_mail_unsupported ();
205340
206341 // Use pre_wp_mail to simulate successful email sending.
207342 add_filter (
0 commit comments