3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
13 * Test that content produced by CiviMail looks the way it's expected.
15 * @package CiviCRM_APIv3
18 * @copyright CiviCRM LLC https://civicrm.org/licensing
22 * Class CRM_Mailing_MailingSystemTest
24 * @see \Civi\FlexMailer\FlexMailerSystemTest
25 * @see CRM_Mailing_MailingSystemTest
27 abstract class CRM_Mailing_BaseMailingSystemTest
extends CiviUnitTestCase
{
28 protected $_apiversion = 3;
30 public $DBResetRequired = FALSE;
31 public $defaultParams = [];
39 public function setUp() {
40 $this->useTransaction();
42 CRM_Mailing_BAO_MailingJob
::$mailsProcessed = 0;
44 $this->_groupID
= $this->groupCreate();
45 $this->createContactsInGroup(2, $this->_groupID
);
47 $this->defaultParams
= [
48 'name' => 'mailing name',
50 'groups' => ['include' => [$this->_groupID
]],
51 'scheduled_date' => 'now',
53 $this->_mut
= new CiviMailUtils($this, TRUE);
54 $this->callAPISuccess('mail_settings', 'get',
55 ['api.mail_settings.create' => ['domain' => 'chaos.org']]);
60 public function tearDown() {
62 CRM_Utils_Hook
::singleton()->reset();
64 CRM_Mailing_BAO_MailingJob
::$mailsProcessed = 0;
69 * Generate a fully-formatted mailing with standard email headers.
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}.',
76 foreach ($allMessages as $k => $message) {
77 /** @var ezcMail $message */
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
);
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]);
95 * Generate a fully-formatted mailing (with body_text content).
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
104 foreach ($allMessages as $message) {
105 /** @var ezcMail $message */
106 /** @var ezcMailText $textPart */
108 $this->assertTrue($message->body
instanceof ezcMailText
);
110 $this->assertEquals('plain', $message->body
->subType
);
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" .
117 "to unsubscribe: http.*civicrm/mailing/optout" .
125 * Generate a fully-formatted mailing (with body_html content).
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,
134 foreach ($allMessages as $message) {
135 /** @var ezcMail $message */
136 /** @var ezcMailText $htmlPart */
137 /** @var ezcMailText $textPart */
139 $this->assertTrue($message->body
instanceof ezcMailMultipartAlternative
);
141 list($textPart, $htmlPart) = $message->body
->getParts();
143 $this->assertEquals('html', $htmlPart->subType
);
147 "Sample Header for HTML formatted content.\n" .
148 // FIXME: CiviMail puts double " after hyperlink!
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" .
152 "Sample Footer for HTML formatted content" .
154 "<img src=\".*extern/open.php.*\"" .
159 $this->assertEquals('plain', $textPart->subType
);
163 "Sample Header for TEXT formatted content.\n" .
164 // body_html, filtered
165 "You can go to Google \\[1\\] or opt out \\[2\\]\\.\n" .
169 "\\[1\\] http://example.net/first\\?cs=[0-9a-f_]+\n" .
170 "\\[2\\] http.*civicrm/mailing/optout.*\n" .
173 "to unsubscribe: http.*civicrm/mailing/optout" .
181 * Generate a fully-formatted mailing (with body_html content).
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,
190 foreach ($allMessages as $message) {
191 /** @var ezcMail $message */
192 /** @var ezcMailText $htmlPart */
193 /** @var ezcMailText $textPart */
195 $this->assertTrue($message->body
instanceof ezcMailMultipartAlternative
);
197 list($textPart, $htmlPart) = $message->body
->getParts();
199 $this->assertEquals('html', $htmlPart->subType
);
203 "<p>You can go to <a href=['\"].*extern/url\.php\?u=\d+&\\;qid=\d+['\"] rel='nofollow'>Google</a>" .
204 " or <a href=\"http.*civicrm/mailing/optout.*\">opt out</a>.</p>\n" .
206 "Sample Footer for HTML formatted content" .
208 // Open-tracking code
209 "<img src=\".*extern/open.php.*\"" .
214 $this->assertEquals('plain', $textPart->subType
);
217 // body_html, filtered
218 "You can go to Google \\[1\\] or opt out \\[2\\]\\.\n" .
222 "\\[1\\] .*extern/url\.php\?u=\d+&qid=\d+\n" .
223 "\\[2\\] http.*civicrm/mailing/optout.*\n" .
226 "to unsubscribe: http.*civicrm/mailing/optout" .
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
242 public function urlTrackingExamples() {
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],
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],
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],
266 '<p>Look at <img src="http://example.net/foo.png">.</p>',
267 ';<p>Look at <img src="http://example\.net/foo\.png">\.</p>;',
269 ['url_tracking' => 0],
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],
282 '<p><a href="http://example.net/">Foo</a></p>',
283 ';<p><a href=[\'"].*extern/url\.php\?u=\d+.*[\'"]>Foo</a></p>;',
284 ';\\[1\\] .*extern/url\.php\?u=\d+.*;',
285 ['url_tracking' => 1],
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],
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],
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>;',
307 ['url_tracking' => 1],
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\?u=\d+&qid=\d+;',
315 ['url_tracking' => 1],
322 * Generate a fully-formatted mailing (with body_html content).
324 * @dataProvider urlTrackingExamples
326 public function testUrlTracking($inputHtml, $htmlUrlRegex, $textUrlRegex, $params) {
327 $caseName = print_r(['inputHtml' => $inputHtml, 'params' => $params], 1);
329 $allMessages = $this->runMailingSuccess($params +
[
330 'subject' => 'Example Subject',
331 'body_html' => $inputHtml,
333 foreach ($allMessages as $message) {
334 /** @var ezcMail $message */
335 /** @var ezcMailText $htmlPart */
336 /** @var ezcMailText $textPart */
338 $this->assertTrue($message->body
instanceof ezcMailMultipartAlternative
);
340 list($textPart, $htmlPart) = $message->body
->getParts();
343 $this->assertEquals('html', $htmlPart->subType
, "Should have HTML part in case: $caseName");
344 $this->assertRegExp($htmlUrlRegex, $htmlPart->text
, "Should have correct HTML in case: $caseName");
348 $this->assertEquals('plain', $textPart->subType
, "Should have text part in case: $caseName");
349 $this->assertRegExp($textUrlRegex, $textPart->text
, "Should have correct text in case: $caseName");
355 * Create contacts in group.
358 * @param int $groupID
359 * @param string $domain
361 protected function createContactsInGroup(
364 $domain = 'nul.example.com'
366 for ($i = 1; $i <= $count; $i++
) {
367 $contactID = $this->individualCreate([
368 'first_name' => "Foo{$i}",
369 'email' => 'mail' . $i . '@' . $domain,
371 $this->callAPISuccess('group_contact', 'create', [
372 'contact_id' => $contactID,
373 'group_id' => $groupID,
380 * Create and execute a mailing. Return the matching messages.
382 * @param array $params
383 * List of parameters to send to Mailing.create API.
384 * @return array<ezcMail>
386 protected function runMailingSuccess($params) {
387 $mailingParams = array_merge($this->defaultParams
, $params);
388 $this->callAPISuccess('mailing', 'create', $mailingParams);
389 $this->_mut
->assertRecipients([]);
390 $this->callAPISuccess('job', 'process_mailing', ['runInNonProductionEnvironment' => TRUE]);
392 $allMessages = $this->_mut
->getAllMessages('ezc');
393 // There are exactly two contacts produced by setUp().
394 $this->assertEquals(2, count($allMessages));