Merge pull request #17953 from civicrm/5.28
[civicrm-core.git] / tests / phpunit / CRM / Mailing / BaseMailingSystemTest.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
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 |
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 *
18 * @copyright CiviCRM LLC https://civicrm.org/licensing
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;
31 public $defaultParams = [];
32 private $_groupID;
33
34 /**
35 * @var CiviMailUtils
36 */
37 private $_mut;
38
39 public function setUp() {
40 $this->useTransaction();
41 parent::setUp();
42 CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0;
43
44 $this->_groupID = $this->groupCreate();
45 $this->createContactsInGroup(2, $this->_groupID);
46
47 $this->defaultParams = [
48 'name' => 'mailing name',
49 'created_id' => 1,
50 'groups' => ['include' => [$this->_groupID]],
51 'scheduled_date' => 'now',
52 ];
53 $this->_mut = new CiviMailUtils($this, TRUE);
54 $this->callAPISuccess('mail_settings', 'get',
55 ['api.mail_settings.create' => ['domain' => 'chaos.org']]);
56 }
57
58 /**
59 */
60 public function tearDown() {
61 $this->_mut->stop();
62 CRM_Utils_Hook::singleton()->reset();
63 // DGW
64 CRM_Mailing_BAO_MailingJob::$mailsProcessed = 0;
65 parent::tearDown();
66 }
67
68 /**
69 * Generate a fully-formatted mailing with standard email headers.
70 */
71 public function testBasicHeaders() {
72 $allMessages = $this->runMailingSuccess([
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}.',
75 ]);
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() {
98 $allMessages = $this->runMailingSuccess([
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
103 ]);
104 foreach ($allMessages as $message) {
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 ";" .
113 // Default header
114 "Sample Header for TEXT formatted content.\n" .
115 "BEWARE children need regular infusions of toys. Santa knows your .*\\. There is no http.*civicrm/mailing/optout.*\\.\n" .
116 // Default footer
117 "to unsubscribe: http.*civicrm/mailing/optout" .
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() {
128 $allMessages = $this->runMailingSuccess([
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,
133 ]);
134 foreach ($allMessages as $message) {
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 ";" .
146 // Default header
147 "Sample Header for HTML formatted content.\n" .
148 // FIXME: CiviMail puts double " after hyperlink!
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" .
153 ".*\n" .
154 "<img src=\".*(extern/open.php|civicrm/mailing/open).*\"" .
155 ";",
156 $htmlPart->text
157 );
158
159 $this->assertEquals('plain', $textPart->subType);
160 $this->assertRegExp(
161 ";" .
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" .
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" .
172 // Default footer
173 "to unsubscribe: http.*civicrm/mailing/optout" .
174 ";",
175 $textPart->text
176 );
177 }
178 }
179
180 /**
181 * Generate a fully-formatted mailing (with body_html content).
182 */
183 public function testHtmlWithOpenAndUrlTracking() {
184 $allMessages = $this->runMailingSuccess([
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,
189 ]);
190 foreach ($allMessages as $message) {
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
203 "<p>You can go to <a href=['\"].*(extern/url.php|civicrm/mailing/url)(\?|&amp\\;)u=\d+&amp\\;qid=\d+['\"] rel='nofollow'>Google</a>" .
204 " or <a href=\"http.*civicrm/mailing/optout.*\">opt out</a>.</p>\n" .
205 // Default footer
206 "Sample Footer for HTML formatted content" .
207 ".*\n" .
208 // Open-tracking code
209 "<img src=\".*(extern/open.php|civicrm/mailing/open).*\"" .
210 ";",
211 $htmlPart->text
212 );
213
214 $this->assertEquals('plain', $textPart->subType);
215 $this->assertRegExp(
216 ";" .
217 // body_html, filtered
218 "You can go to Google \\[1\\] or opt out \\[2\\]\\.\n" .
219 "\n" .
220 "Links:\n" .
221 "------\n" .
222 "\\[1\\] http.*(extern/url.php|civicrm/mailing/url)(\?|&)u=\d+&qid=\d+\n" .
223 "\\[2\\] http.*civicrm/mailing/optout.*\n" .
224 "\n" .
225 // Default footer
226 "to unsubscribe: http.*civicrm/mailing/optout" .
227 ";",
228 $textPart->text
229 );
230 }
231 }
232
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 */
242 public function urlTrackingExamples() {
243 $cases = [];
244
245 // Tracking disabled
246 $cases[0] = [
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/;',
250 ['url_tracking' => 0],
251 ];
252 $cases[1] = [
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+;',
257 ['url_tracking' => 0],
258 ];
259 $cases[2] = [
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.*;',
263 ['url_tracking' => 0],
264 ];
265 $cases[3] = [
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 \.;',
269 ['url_tracking' => 0],
270 ];
271 $cases[4] = [
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/;',
277 ['url_tracking' => 0],
278 ];
279
280 // Tracking enabled
281 $cases[5] = [
282 '<p><a href="http://example.net/">Foo</a></p>',
283 ';<p><a href=[\'"].*(extern/url.php|civicrm/mailing/url)(\?|&amp\\;)u=\d+.*[\'"]>Foo</a></p>;',
284 ';\\[1\\] .*(extern/url.php|civicrm/mailing/url)[\?&]u=\d+.*;',
285 ['url_tracking' => 1],
286 ];
287 $cases[6] = [
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+;',
293 ['url_tracking' => 1],
294 ];
295 $cases[7] = [
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.*;',
300 ['url_tracking' => 1],
301 ];
302 $cases[8] = [
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 \.;',
307 ['url_tracking' => 1],
308 ];
309 $cases[9] = [
310 // Plain-text URL's are tracked in plain-text emails...
311 // but not in HTML emails.
312 "<p>Please go to: http://example.net/</p>",
313 ";<p>Please go to: http://example\.net/</p>;",
314 ';Please go to: .*(extern/url.php|civicrm/mailing/url)[\?&]u=\d+&qid=\d+;',
315 ['url_tracking' => 1],
316 ];
317
318 return $cases;
319 }
320
321 /**
322 * Generate a fully-formatted mailing (with body_html content).
323 *
324 * @dataProvider urlTrackingExamples
325 * @throws \CRM_Core_Exception
326 */
327 public function testUrlTracking($inputHtml, $htmlUrlRegex, $textUrlRegex, $params) {
328 $caseName = print_r(['inputHtml' => $inputHtml, 'params' => $params], 1);
329
330 $allMessages = $this->runMailingSuccess($params + [
331 'subject' => 'Example Subject',
332 'body_html' => $inputHtml,
333 ]);
334 foreach ($allMessages as $message) {
335 /** @var ezcMail $message */
336 /** @var ezcMailText $htmlPart */
337 /** @var ezcMailText $textPart */
338
339 $this->assertTrue($message->body instanceof ezcMailMultipartAlternative);
340
341 list($textPart, $htmlPart) = $message->body->getParts();
342
343 if ($htmlUrlRegex) {
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) {
349 $this->assertEquals('plain', $textPart->subType, "Should have text part in case: $caseName");
350 $this->assertRegExp($textUrlRegex, $textPart->text, "Should have correct text in case: $caseName");
351 }
352 }
353 }
354
355 /**
356 * Create contacts in group.
357 *
358 * @param int $count
359 * @param int $groupID
360 * @param string $domain
361 */
362 protected function createContactsInGroup(
363 $count,
364 $groupID,
365 $domain = 'nul.example.com'
366 ) {
367 for ($i = 1; $i <= $count; $i++) {
368 $contactID = $this->individualCreate([
369 'first_name' => "Foo{$i}",
370 'email' => 'mail' . $i . '@' . $domain,
371 ]);
372 $this->callAPISuccess('group_contact', 'create', [
373 'contact_id' => $contactID,
374 'group_id' => $groupID,
375 'status' => 'Added',
376 ]);
377 }
378 }
379
380 /**
381 * Create and execute a mailing. Return the matching messages.
382 *
383 * @param array $params
384 * List of parameters to send to Mailing.create API.
385 *
386 * @return array<ezcMail>
387 * @throws \CRM_Core_Exception
388 */
389 protected function runMailingSuccess($params) {
390 $mailingParams = array_merge($this->defaultParams, $params);
391 $this->callAPISuccess('mailing', 'create', $mailingParams);
392 $this->_mut->assertRecipients([]);
393 $this->callAPISuccess('job', 'process_mailing', ['runInNonProductionEnvironment' => TRUE]);
394
395 $allMessages = $this->_mut->getAllMessages('ezc');
396 // There are exactly two contacts produced by setUp().
397 $this->assertEquals(2, count($allMessages));
398
399 return $allMessages;
400 }
401
402 }