Commit | Line | Data |
---|---|---|
b4a332a9 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
7d61e75f | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
b4a332a9 | 5 | | | |
7d61e75f TO |
6 | | This work is published under the GNU AGPLv3 license with some | |
7 | | permitted exceptions and without any warranty. For full license | | |
8 | | and copyright information, see https://civicrm.org/licensing | | |
b4a332a9 TO |
9 | +--------------------------------------------------------------------+ |
10 | */ | |
11 | ||
12 | /** | |
13 | * Test that content produced by CiviMail looks the way it's expected. | |
14 | * | |
15 | * @package CiviCRM_APIv3 | |
16 | * @subpackage API_Job | |
17 | * | |
ca5cec67 | 18 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
b4a332a9 TO |
19 | */ |
20 | ||
21 | /** | |
22 | * Class CRM_Mailing_MailingSystemTest | |
23 | * @group headless | |
24 | * @see \Civi\FlexMailer\FlexMailerSystemTest | |
25 | * @see CRM_Mailing_MailingSystemTest | |
26 | */ | |
27 | abstract class CRM_Mailing_BaseMailingSystemTest extends CiviUnitTestCase { | |
28 | protected $_apiversion = 3; | |
29 | ||
30 | public $DBResetRequired = FALSE; | |
9099cab3 | 31 | public $defaultParams = []; |
b4a332a9 TO |
32 | private $_groupID; |
33 | ||
34 | /** | |
35 | * @var CiviMailUtils | |
36 | */ | |
37 | private $_mut; | |
38 | ||
0b49aa04 | 39 | public function setUp(): void { |
b4a332a9 TO |
40 | $this->useTransaction(); |
41 | parent::setUp(); | |
6f616e5c | 42 | CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; |
b4a332a9 TO |
43 | |
44 | $this->_groupID = $this->groupCreate(); | |
45 | $this->createContactsInGroup(2, $this->_groupID); | |
46 | ||
9099cab3 | 47 | $this->defaultParams = [ |
b4a332a9 TO |
48 | 'name' => 'mailing name', |
49 | 'created_id' => 1, | |
9099cab3 | 50 | 'groups' => ['include' => [$this->_groupID]], |
b4a332a9 | 51 | 'scheduled_date' => 'now', |
9099cab3 | 52 | ]; |
b4a332a9 TO |
53 | $this->_mut = new CiviMailUtils($this, TRUE); |
54 | $this->callAPISuccess('mail_settings', 'get', | |
9099cab3 | 55 | ['api.mail_settings.create' => ['domain' => 'chaos.org']]); |
b4a332a9 TO |
56 | } |
57 | ||
58 | /** | |
59 | */ | |
dd09ee0c | 60 | public function tearDown(): void { |
b4a332a9 TO |
61 | $this->_mut->stop(); |
62 | CRM_Utils_Hook::singleton()->reset(); | |
39b959db SL |
63 | // DGW |
64 | CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0; | |
b4a332a9 TO |
65 | parent::tearDown(); |
66 | } | |
67 | ||
68 | /** | |
69 | * Generate a fully-formatted mailing with standard email headers. | |
70 | */ | |
71 | public function testBasicHeaders() { | |
9099cab3 | 72 | $allMessages = $this->runMailingSuccess([ |
b4a332a9 TO |
73 | 'subject' => 'Accidents in cars cause children for {contact.display_name}!', |
74 | 'body_text' => 'BEWARE children need regular infusions of toys. Santa knows your {domain.address}. There is no {action.optOutUrl}.', | |
9099cab3 | 75 | ]); |
b4a332a9 TO |
76 | foreach ($allMessages as $k => $message) { |
77 | /** @var ezcMail $message */ | |
78 | ||
79 | $offset = $k + 1; | |
80 | ||
81 | $this->assertEquals("FIXME", $message->from->name); | |
82 | $this->assertEquals("info@EXAMPLE.ORG", $message->from->email); | |
83 | $this->assertEquals("Mr. Foo{$offset} Anderson II", $message->to[0]->name); | |
84 | $this->assertEquals("mail{$offset}@nul.example.com", $message->to[0]->email); | |
85 | ||
86 | $this->assertRegExp('#^text/plain; charset=utf-8#', $message->headers['Content-Type']); | |
87 | $this->assertRegExp(';^b\.[\d\.a-f]+@chaos.org$;', $message->headers['Return-Path']); | |
88 | $this->assertRegExp(';^b\.[\d\.a-f]+@chaos.org$;', $message->headers['X-CiviMail-Bounce'][0]); | |
89 | $this->assertRegExp(';^\<mailto:u\.[\d\.a-f]+@chaos.org\>$;', $message->headers['List-Unsubscribe'][0]); | |
90 | $this->assertEquals('bulk', $message->headers['Precedence'][0]); | |
91 | } | |
92 | } | |
93 | ||
94 | /** | |
95 | * Generate a fully-formatted mailing (with body_text content). | |
96 | */ | |
97 | public function testText() { | |
9099cab3 | 98 | $allMessages = $this->runMailingSuccess([ |
b4a332a9 TO |
99 | 'subject' => 'Accidents in cars cause children for {contact.display_name}!', |
100 | 'body_text' => 'BEWARE children need regular infusions of toys. Santa knows your {domain.address}. There is no {action.optOutUrl}.', | |
101 | 'open_tracking' => 1, | |
102 | // Note: open_tracking does nothing with text, but we'll just verify that it does nothing | |
9099cab3 | 103 | ]); |
6f616e5c | 104 | foreach ($allMessages as $message) { |
b4a332a9 TO |
105 | /** @var ezcMail $message */ |
106 | /** @var ezcMailText $textPart */ | |
107 | ||
108 | $this->assertTrue($message->body instanceof ezcMailText); | |
109 | ||
110 | $this->assertEquals('plain', $message->body->subType); | |
111 | $this->assertRegExp( | |
112 | ";" . | |
39b959db SL |
113 | // Default header |
114 | "Sample Header for TEXT formatted content.\n" . | |
b4a332a9 | 115 | "BEWARE children need regular infusions of toys. Santa knows your .*\\. There is no http.*civicrm/mailing/optout.*\\.\n" . |
39b959db SL |
116 | // Default footer |
117 | "to unsubscribe: http.*civicrm/mailing/optout" . | |
b4a332a9 TO |
118 | ";", |
119 | $message->body->text | |
120 | ); | |
121 | } | |
122 | } | |
123 | ||
124 | /** | |
125 | * Generate a fully-formatted mailing (with body_html content). | |
126 | */ | |
127 | public function testHtmlWithOpenTracking() { | |
9099cab3 | 128 | $allMessages = $this->runMailingSuccess([ |
b4a332a9 TO |
129 | 'subject' => 'Example Subject', |
130 | 'body_html' => '<p>You can go to <a href="http://example.net/first?{contact.checksum}">Google</a> or <a href="{action.optOutUrl}">opt out</a>.</p>', | |
131 | 'open_tracking' => 1, | |
132 | 'url_tracking' => 0, | |
9099cab3 | 133 | ]); |
6f616e5c | 134 | foreach ($allMessages as $message) { |
b4a332a9 TO |
135 | /** @var ezcMail $message */ |
136 | /** @var ezcMailText $htmlPart */ | |
137 | /** @var ezcMailText $textPart */ | |
138 | ||
139 | $this->assertTrue($message->body instanceof ezcMailMultipartAlternative); | |
140 | ||
141 | list($textPart, $htmlPart) = $message->body->getParts(); | |
142 | ||
143 | $this->assertEquals('html', $htmlPart->subType); | |
144 | $this->assertRegExp( | |
145 | ";" . | |
39b959db SL |
146 | // Default header |
147 | "Sample Header for HTML formatted content.\n" . | |
b4a332a9 | 148 | // FIXME: CiviMail puts double " after hyperlink! |
39b959db SL |
149 | // body_html |
150 | "<p>You can go to <a href=\"http://example.net/first\\?cs=[0-9a-f_]+\"\"?>Google</a> or <a href=\"http.*civicrm/mailing/optout.*\">opt out</a>.</p>\n" . | |
151 | // Default footer | |
152 | "Sample Footer for HTML formatted content" . | |
76c970f8 | 153 | ".*\n" . |
dc30f800 | 154 | "<img src=\".*(extern/open.php|civicrm/mailing/open).*\"" . |
76c970f8 | 155 | ";", |
b4a332a9 TO |
156 | $htmlPart->text |
157 | ); | |
76c970f8 | 158 | |
b4a332a9 TO |
159 | $this->assertEquals('plain', $textPart->subType); |
160 | $this->assertRegExp( | |
161 | ";" . | |
39b959db SL |
162 | // Default header |
163 | "Sample Header for TEXT formatted content.\n" . | |
164 | // body_html, filtered | |
165 | "You can go to Google \\[1\\] or opt out \\[2\\]\\.\n" . | |
76c970f8 TO |
166 | "\n" . |
167 | "Links:\n" . | |
168 | "------\n" . | |
169 | "\\[1\\] http://example.net/first\\?cs=[0-9a-f_]+\n" . | |
170 | "\\[2\\] http.*civicrm/mailing/optout.*\n" . | |
171 | "\n" . | |
39b959db SL |
172 | // Default footer |
173 | "to unsubscribe: http.*civicrm/mailing/optout" . | |
76c970f8 | 174 | ";", |
b4a332a9 TO |
175 | $textPart->text |
176 | ); | |
177 | } | |
178 | } | |
179 | ||
180 | /** | |
181 | * Generate a fully-formatted mailing (with body_html content). | |
182 | */ | |
183 | public function testHtmlWithOpenAndUrlTracking() { | |
9099cab3 | 184 | $allMessages = $this->runMailingSuccess([ |
b4a332a9 TO |
185 | 'subject' => 'Example Subject', |
186 | 'body_html' => '<p>You can go to <a href="http://example.net">Google</a> or <a href="{action.optOutUrl}">opt out</a>.</p>', | |
187 | 'open_tracking' => 1, | |
188 | 'url_tracking' => 1, | |
9099cab3 | 189 | ]); |
6f616e5c | 190 | foreach ($allMessages as $message) { |
b4a332a9 TO |
191 | /** @var ezcMail $message */ |
192 | /** @var ezcMailText $htmlPart */ | |
193 | /** @var ezcMailText $textPart */ | |
194 | ||
195 | $this->assertTrue($message->body instanceof ezcMailMultipartAlternative); | |
196 | ||
197 | list($textPart, $htmlPart) = $message->body->getParts(); | |
198 | ||
199 | $this->assertEquals('html', $htmlPart->subType); | |
200 | $this->assertRegExp( | |
201 | ";" . | |
202 | // body_html | |
d04905cc | 203 | "<p>You can go to <a href=['\"].*(extern/url.php|civicrm/mailing/url)(\?|&\\;)u=\d+&\\;qid=\d+['\"] rel='nofollow'>Google</a>" . |
b4a332a9 TO |
204 | " or <a href=\"http.*civicrm/mailing/optout.*\">opt out</a>.</p>\n" . |
205 | // Default footer | |
76c970f8 | 206 | "Sample Footer for HTML formatted content" . |
b4a332a9 TO |
207 | ".*\n" . |
208 | // Open-tracking code | |
dc30f800 | 209 | "<img src=\".*(extern/open.php|civicrm/mailing/open).*\"" . |
b4a332a9 TO |
210 | ";", |
211 | $htmlPart->text | |
212 | ); | |
76c970f8 | 213 | |
b4a332a9 TO |
214 | $this->assertEquals('plain', $textPart->subType); |
215 | $this->assertRegExp( | |
216 | ";" . | |
76c970f8 TO |
217 | // body_html, filtered |
218 | "You can go to Google \\[1\\] or opt out \\[2\\]\\.\n" . | |
219 | "\n" . | |
b4a332a9 TO |
220 | "Links:\n" . |
221 | "------\n" . | |
d04905cc | 222 | "\\[1\\] http.*(extern/url.php|civicrm/mailing/url)(\?|&)u=\d+&qid=\d+\n" . |
76c970f8 TO |
223 | "\\[2\\] http.*civicrm/mailing/optout.*\n" . |
224 | "\n" . | |
225 | // Default footer | |
226 | "to unsubscribe: http.*civicrm/mailing/optout" . | |
227 | ";", | |
b4a332a9 TO |
228 | $textPart->text |
229 | ); | |
230 | } | |
231 | } | |
232 | ||
6f616e5c | 233 | /** |
234 | * Each case comes in four parts: | |
235 | * 1. Mailing HTML (body_html) | |
236 | * 2. Regex to run against final HTML | |
237 | * 3. Regex to run against final text | |
238 | * 4. Additional mailing options | |
239 | * | |
240 | * @return array | |
241 | */ | |
b4a332a9 | 242 | public function urlTrackingExamples() { |
9099cab3 | 243 | $cases = []; |
b4a332a9 | 244 | |
b4a332a9 | 245 | // Tracking disabled |
d04905cc | 246 | $cases[0] = [ |
b4a332a9 TO |
247 | '<p><a href="http://example.net/">Foo</a></p>', |
248 | ';<p><a href="http://example\.net/">Foo</a></p>;', | |
249 | ';\\[1\\] http://example\.net/;', | |
9099cab3 CW |
250 | ['url_tracking' => 0], |
251 | ]; | |
d04905cc | 252 | $cases[1] = [ |
b4a332a9 TO |
253 | '<p><a href="http://example.net/?id={contact.contact_id}">Foo</a></p>', |
254 | // FIXME: Legacy tracker adds extra quote after URL | |
255 | ';<p><a href="http://example\.net/\?id=\d+""?>Foo</a></p>;', | |
256 | ';\\[1\\] http://example\.net/\?id=\d+;', | |
9099cab3 CW |
257 | ['url_tracking' => 0], |
258 | ]; | |
d04905cc | 259 | $cases[2] = [ |
b4a332a9 TO |
260 | '<p><a href="{action.optOutUrl}">Foo</a></p>', |
261 | ';<p><a href="http.*civicrm/mailing/optout.*">Foo</a></p>;', | |
262 | ';\\[1\\] http.*civicrm/mailing/optout.*;', | |
9099cab3 CW |
263 | ['url_tracking' => 0], |
264 | ]; | |
d04905cc | 265 | $cases[3] = [ |
b4a332a9 TO |
266 | '<p>Look at <img src="http://example.net/foo.png">.</p>', |
267 | ';<p>Look at <img src="http://example\.net/foo\.png">\.</p>;', | |
268 | ';Look at \.;', | |
9099cab3 CW |
269 | ['url_tracking' => 0], |
270 | ]; | |
d04905cc | 271 | $cases[4] = [ |
b4a332a9 TO |
272 | // Plain-text URL's are tracked in plain-text emails... |
273 | // but not in HTML emails. | |
274 | "<p>Please go to: http://example.net/</p>", | |
275 | ";<p>Please go to: http://example\.net/</p>;", | |
276 | ';Please go to: http://example\.net/;', | |
9099cab3 CW |
277 | ['url_tracking' => 0], |
278 | ]; | |
b4a332a9 TO |
279 | |
280 | // Tracking enabled | |
d04905cc | 281 | $cases[5] = [ |
b4a332a9 | 282 | '<p><a href="http://example.net/">Foo</a></p>', |
d04905cc TO |
283 | ';<p><a href=[\'"].*(extern/url.php|civicrm/mailing/url)(\?|&\\;)u=\d+.*[\'"]>Foo</a></p>;', |
284 | ';\\[1\\] .*(extern/url.php|civicrm/mailing/url)[\?&]u=\d+.*;', | |
9099cab3 CW |
285 | ['url_tracking' => 1], |
286 | ]; | |
d04905cc | 287 | $cases[6] = [ |
b4a332a9 TO |
288 | // FIXME: CiviMail URL tracking doesn't track tokenized links. |
289 | '<p><a href="http://example.net/?id={contact.contact_id}">Foo</a></p>', | |
290 | // FIXME: Legacy tracker adds extra quote after URL | |
291 | ';<p><a href="http://example\.net/\?id=\d+""?>Foo</a></p>;', | |
292 | ';\\[1\\] http://example\.net/\?id=\d+;', | |
9099cab3 CW |
293 | ['url_tracking' => 1], |
294 | ]; | |
d04905cc | 295 | $cases[7] = [ |
b4a332a9 TO |
296 | // It would be redundant/slow to track the action URLs? |
297 | '<p><a href="{action.optOutUrl}">Foo</a></p>', | |
298 | ';<p><a href="http.*civicrm/mailing/optout.*">Foo</a></p>;', | |
299 | ';\\[1\\] http.*civicrm/mailing/optout.*;', | |
9099cab3 CW |
300 | ['url_tracking' => 1], |
301 | ]; | |
d04905cc | 302 | $cases[8] = [ |
b4a332a9 TO |
303 | // It would be excessive/slow to track every embedded image. |
304 | '<p>Look at <img src="http://example.net/foo.png">.</p>', | |
305 | ';<p>Look at <img src="http://example\.net/foo\.png">\.</p>;', | |
306 | ';Look at \.;', | |
9099cab3 CW |
307 | ['url_tracking' => 1], |
308 | ]; | |
7d3ec0ad | 309 | $cases[9] = [ |
76c970f8 TO |
310 | // Plain-text URL's are tracked in plain-text emails... |
311 | // but not in HTML emails. | |
b4a332a9 TO |
312 | "<p>Please go to: http://example.net/</p>", |
313 | ";<p>Please go to: http://example\.net/</p>;", | |
d04905cc | 314 | ';Please go to: .*(extern/url.php|civicrm/mailing/url)[\?&]u=\d+&qid=\d+;', |
9099cab3 CW |
315 | ['url_tracking' => 1], |
316 | ]; | |
b4a332a9 TO |
317 | |
318 | return $cases; | |
319 | } | |
320 | ||
321 | /** | |
322 | * Generate a fully-formatted mailing (with body_html content). | |
323 | * | |
324 | * @dataProvider urlTrackingExamples | |
6296d794 | 325 | * @throws \CRM_Core_Exception |
b4a332a9 TO |
326 | */ |
327 | public function testUrlTracking($inputHtml, $htmlUrlRegex, $textUrlRegex, $params) { | |
b4a332a9 | 328 | |
9099cab3 | 329 | $allMessages = $this->runMailingSuccess($params + [ |
b4a332a9 TO |
330 | 'subject' => 'Example Subject', |
331 | 'body_html' => $inputHtml, | |
9099cab3 | 332 | ]); |
6f616e5c | 333 | foreach ($allMessages as $message) { |
b4a332a9 TO |
334 | /** @var ezcMail $message */ |
335 | /** @var ezcMailText $htmlPart */ | |
336 | /** @var ezcMailText $textPart */ | |
337 | ||
338 | $this->assertTrue($message->body instanceof ezcMailMultipartAlternative); | |
339 | ||
340 | list($textPart, $htmlPart) = $message->body->getParts(); | |
341 | ||
342 | if ($htmlUrlRegex) { | |
4b6bbccf | 343 | $caseName = print_r(['inputHtml' => $inputHtml, 'params' => $params, 'htmlUrlRegex' => $htmlUrlRegex, 'htmlPart' => $htmlPart->text], 1); |
b4a332a9 TO |
344 | $this->assertEquals('html', $htmlPart->subType, "Should have HTML part in case: $caseName"); |
345 | $this->assertRegExp($htmlUrlRegex, $htmlPart->text, "Should have correct HTML in case: $caseName"); | |
346 | } | |
347 | ||
348 | if ($textUrlRegex) { | |
4b6bbccf | 349 | $caseName = print_r(['inputHtml' => $inputHtml, 'params' => $params, 'textUrlRegex' => $textUrlRegex, 'textPart' => $textPart->text], 1); |
b4a332a9 TO |
350 | $this->assertEquals('plain', $textPart->subType, "Should have text part in case: $caseName"); |
351 | $this->assertRegExp($textUrlRegex, $textPart->text, "Should have correct text in case: $caseName"); | |
352 | } | |
353 | } | |
354 | } | |
355 | ||
356 | /** | |
357 | * Create contacts in group. | |
358 | * | |
359 | * @param int $count | |
360 | * @param int $groupID | |
361 | * @param string $domain | |
362 | */ | |
363 | protected function createContactsInGroup( | |
364 | $count, | |
365 | $groupID, | |
366 | $domain = 'nul.example.com' | |
367 | ) { | |
368 | for ($i = 1; $i <= $count; $i++) { | |
9099cab3 | 369 | $contactID = $this->individualCreate([ |
b4a332a9 TO |
370 | 'first_name' => "Foo{$i}", |
371 | 'email' => 'mail' . $i . '@' . $domain, | |
9099cab3 CW |
372 | ]); |
373 | $this->callAPISuccess('group_contact', 'create', [ | |
b4a332a9 TO |
374 | 'contact_id' => $contactID, |
375 | 'group_id' => $groupID, | |
376 | 'status' => 'Added', | |
9099cab3 | 377 | ]); |
b4a332a9 TO |
378 | } |
379 | } | |
380 | ||
381 | /** | |
382 | * Create and execute a mailing. Return the matching messages. | |
383 | * | |
384 | * @param array $params | |
385 | * List of parameters to send to Mailing.create API. | |
9724097e | 386 | * |
b4a332a9 | 387 | * @return array<ezcMail> |
9724097e | 388 | * @throws \CRM_Core_Exception |
b4a332a9 TO |
389 | */ |
390 | protected function runMailingSuccess($params) { | |
391 | $mailingParams = array_merge($this->defaultParams, $params); | |
392 | $this->callAPISuccess('mailing', 'create', $mailingParams); | |
9099cab3 CW |
393 | $this->_mut->assertRecipients([]); |
394 | $this->callAPISuccess('job', 'process_mailing', ['runInNonProductionEnvironment' => TRUE]); | |
b4a332a9 TO |
395 | |
396 | $allMessages = $this->_mut->getAllMessages('ezc'); | |
397 | // There are exactly two contacts produced by setUp(). | |
398 | $this->assertEquals(2, count($allMessages)); | |
399 | ||
400 | return $allMessages; | |
401 | } | |
402 | ||
403 | } |