Skip to content

Commit ae6b1b8

Browse files
authored
Fixes bug where emails are sent to blocked addresses (#43)
1 parent 540a4ff commit ae6b1b8

File tree

3 files changed

+232
-17
lines changed

3 files changed

+232
-17
lines changed

models/MailBlocker.php

+31-17
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,11 @@ public static function checkAllForUser($user)
211211
/**
212212
* Checks if an email address has blocked a given template,
213213
* returns an array of blocked emails.
214-
* @param string $template
215-
* @param string $email
214+
* @param ?string $template
215+
* @param string|string[] $email
216216
* @return array
217217
*/
218-
public static function checkForEmail($template, $email)
218+
public static function checkForEmail(?string $template, $email): array
219219
{
220220
if (in_array($template, static::$safeTemplates)) {
221221
return [];
@@ -225,11 +225,7 @@ public static function checkForEmail($template, $email)
225225
return [];
226226
}
227227

228-
if (!is_array($email)) {
229-
$email = [$email => null];
230-
}
231-
232-
$emails = array_keys($email);
228+
$emails = is_array($email) ? $email : [$email];
233229

234230
return static::where(
235231
function ($query) use ($template) {
@@ -248,22 +244,40 @@ function ($query) use ($template) {
248244
* @param \Illuminate\Mail\Message $message
249245
* @return bool|null
250246
*/
251-
public static function filterMessage($template, $message)
247+
public static function filterMessage($template, $message): ?bool
252248
{
253-
$recipients = $message->getTo();
254-
$blockedAddresses = static::checkForEmail($template, $recipients);
249+
$recipients = array_map(function ($address) {
250+
return $address->getAddress();
251+
}, $message->getTo());
252+
$ccRecipients = array_map(function ($address) {
253+
return $address->getAddress();
254+
}, $message->getCc());
255+
$bccRecipients = array_map(function ($address) {
256+
return $address->getAddress();
257+
}, $message->getBcc());
258+
259+
$allRecipients = array_merge($recipients, $ccRecipients, $bccRecipients);
260+
$allRecipientsNoDuplicates = array_combine($allRecipients, $allRecipients);
261+
262+
$blockedAddresses = static::checkForEmail($template, $allRecipientsNoDuplicates);
255263

256264
if (!count($blockedAddresses)) {
257265
return null;
258266
}
259267

260-
foreach (array_keys($recipients) as $address) {
261-
if (in_array($address, $blockedAddresses)) {
262-
unset($recipients[$address]);
263-
}
264-
}
268+
$recipients = array_filter($recipients, function ($address) use (&$blockedAddresses) {
269+
return !in_array($address, $blockedAddresses);
270+
});
271+
$ccRecipients = array_filter($ccRecipients, function ($address) use (&$blockedAddresses) {
272+
return !in_array($address, $blockedAddresses);
273+
});
274+
$bccRecipients = array_filter($bccRecipients, function ($address) use (&$blockedAddresses) {
275+
return !in_array($address, $blockedAddresses);
276+
});
265277

266278
$message->to($recipients, null, true);
267-
return count($recipients) ? null : false;
279+
$message->cc($ccRecipients, null, true);
280+
$message->bcc($bccRecipients, null, true);
281+
return (count($recipients) + count($ccRecipients) + count($bccRecipients)) ? null : false;
268282
}
269283
}
+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
<?php
2+
3+
namespace Winter\User\Tests\Unit\Models;
4+
5+
use Illuminate\Mail\Message;
6+
use Mockery;
7+
use Symfony\Component\Mime\Email;
8+
use Winter\User\Models\MailBlocker;
9+
use Winter\User\Models\User;
10+
use Winter\User\Tests\UserPluginTestCase;
11+
12+
class MailBlockerModelTest extends UserPluginTestCase
13+
{
14+
public function testFilterMessage_to()
15+
{
16+
list(
17+
$userMock1,
18+
$userMock2,
19+
$userMock3
20+
) = $this->getMockedUsers();
21+
22+
$this->mockBlockers($userMock1, $userMock2);
23+
24+
$message1 = $this->getMessage('test1', [$userMock1->email]);
25+
$message2 = $this->getMessage('test2', [$userMock2->email]);
26+
$message3 = $this->getMessage('test3', [$userMock3->email]);
27+
28+
$message12 = $this->getMessage('test12', [$userMock1->email, $userMock2->email]);
29+
$message13 = $this->getMessage('test13', [$userMock1->email, $userMock3->email]);
30+
31+
$filterMessage1 = MailBlocker::filterMessage(null, $message1);
32+
$filterMessage2 = MailBlocker::filterMessage(null, $message2);
33+
$filterMessage3 = MailBlocker::filterMessage(null, $message3);
34+
$filterMessage12 = MailBlocker::filterMessage(null, $message12);
35+
$filterMessage13 = MailBlocker::filterMessage(null, $message13);
36+
37+
$this->assertFalse($filterMessage1);
38+
$this->assertEmpty($message1->getTo());
39+
$this->assertFalse($filterMessage2);
40+
$this->assertEmpty($message2->getTo());
41+
$this->assertNull($filterMessage3);
42+
$this->assertEquals($userMock3->email, $message3->getTo()[0]->getAddress());
43+
$this->assertFalse($filterMessage12);
44+
$this->assertEmpty($message12->getTo());
45+
$this->assertNull($filterMessage13);
46+
$this->assertEquals(1, count($message13->getTo()));
47+
$this->assertEquals($userMock3->email, $message13->getTo()[0]->getAddress());
48+
}
49+
50+
public function testFilterMessage_ccAndBcc()
51+
{
52+
list(
53+
$userMock1,
54+
$userMock2,
55+
$userMock3,
56+
$userMock4
57+
) = $this->getMockedUsers();
58+
59+
$this->mockBlockers($userMock1, $userMock2);
60+
61+
$messageTo0Cc12 = $this->getMessage(
62+
'test_to0_cc12',
63+
[],
64+
[$userMock1->email,$userMock2->email]
65+
);
66+
$messageTo3Cc14 = $this->getMessage(
67+
'test_to3_cc14',
68+
[$userMock3->email],
69+
[$userMock1->email,$userMock4->email]
70+
);
71+
$messageTo3Bcc14 = $this->getMessage(
72+
'test_to3_bcc14',
73+
[$userMock3->email],
74+
[],
75+
[$userMock1->email,$userMock4->email]
76+
);
77+
78+
$filterMessageTo0Cc12 = MailBlocker::filterMessage(null, $messageTo0Cc12);
79+
$filterMessageTo3Cc14 = MailBlocker::filterMessage(null, $messageTo3Cc14);
80+
$filterMessageTo3Bcc14 = MailBlocker::filterMessage(null, $messageTo3Bcc14);
81+
82+
$this->assertFalse($filterMessageTo0Cc12);
83+
$this->assertNull($filterMessageTo3Cc14);
84+
$this->assertNull($filterMessageTo3Bcc14);
85+
$this->assertEquals($userMock4->email, $messageTo3Cc14->getCc()[0]->getAddress());
86+
$this->assertEmpty($messageTo0Cc12->getCc());
87+
$this->assertEquals($userMock4->email, $messageTo3Bcc14->getBcc()[0]->getAddress());
88+
}
89+
90+
public function testCheckForEmail()
91+
{
92+
list($userMock1, $userMock2, $userMock3) = $this->getMockedUsers();
93+
94+
$this->mockBlockers($userMock1, $userMock2);
95+
96+
$checkForEmail1 = MailBlocker::checkForEmail(null, $userMock1->email);
97+
$checkForEmail2 = MailBlocker::checkForEmail(null, $userMock2->email);
98+
$checkForEmail3 = MailBlocker::checkForEmail(null, $userMock3->email);
99+
$checkForEmail12 = MailBlocker::checkForEmail(null, [$userMock1->email, $userMock2->email]);
100+
$checkForEmail13 = MailBlocker::checkForEmail(null, [$userMock1->email, $userMock3->email]);
101+
102+
$this->assertEquals([$userMock1->email], $checkForEmail1);
103+
$this->assertEquals([$userMock2->email], $checkForEmail2);
104+
$this->assertEmpty($checkForEmail3);
105+
$this->assertEquals([$userMock1->email, $userMock2->email], $checkForEmail12);
106+
$this->assertEquals([$userMock1->email], $checkForEmail13);
107+
}
108+
109+
/**
110+
* Helper method that mocks 4 users, saves them and returns them
111+
* @return array
112+
*/
113+
private static function getMockedUsers(): array
114+
{
115+
$userMock1 = Mockery::mock(User::class)->makePartial();
116+
$userMock2 = Mockery::mock(User::class)->makePartial();
117+
$userMock3 = Mockery::mock(User::class)->makePartial();
118+
$userMock4 = Mockery::mock(User::class)->makePartial();
119+
120+
$userMock1->shouldReceive('isActivatedByUser')->andReturn(true);
121+
$userMock2->shouldReceive('isActivatedByUser')->andReturn(true);
122+
$userMock3->shouldReceive('isActivatedByUser')->andReturn(true);
123+
$userMock4->shouldReceive('isActivatedByUser')->andReturn(true);
124+
$userMock1->shouldReceive('flushEventListeners')->andReturnNull();
125+
$userMock2->shouldReceive('flushEventListeners')->andReturnNull();
126+
$userMock3->shouldReceive('flushEventListeners')->andReturnNull();
127+
$userMock4->shouldReceive('flushEventListeners')->andReturnNull();
128+
129+
$userMock1->fill([
130+
'name' => 'test1',
131+
'email' => '[email protected]',
132+
'password' => 'password',
133+
]);
134+
$userMock2->fill([
135+
'name' => 'test2',
136+
'email' => '[email protected]',
137+
'password' => 'password',
138+
]);
139+
$userMock3->fill([
140+
'name' => 'test3',
141+
'email' => '[email protected]',
142+
'password' => 'password',
143+
]);
144+
$userMock4->fill([
145+
'name' => 'test3',
146+
'email' => '[email protected]',
147+
'password' => 'password',
148+
]);
149+
$userMock1->save();
150+
$userMock2->save();
151+
$userMock3->save();
152+
$userMock4->save();
153+
return array($userMock1, $userMock2, $userMock3, $userMock4);
154+
}
155+
156+
/**
157+
* Helper method that mocks and saves 2 mail blockers for the two given users
158+
* @param mixed $userMock1
159+
* @param mixed $userMock2
160+
* @return void
161+
*/
162+
private static function mockBlockers(mixed $userMock1, mixed $userMock2): void
163+
{
164+
$mailBlockerMock1 = Mockery::mock(MailBlocker::class)->makePartial();
165+
$mailBlockerMock2 = Mockery::mock(MailBlocker::class)->makePartial();
166+
$mailBlockerMock1->shouldReceive('flushEventListeners')->andReturnNull();
167+
$mailBlockerMock2->shouldReceive('flushEventListeners')->andReturnNull();
168+
169+
$mailBlockerMock1->email = $userMock1->email;
170+
$mailBlockerMock1->template = '*';
171+
$mailBlockerMock1->user_id = $userMock1->id;
172+
173+
$mailBlockerMock2->email = $userMock2->email;
174+
$mailBlockerMock2->template = '*';
175+
$mailBlockerMock2->user_id = $userMock2->id;
176+
177+
$mailBlockerMock1->save();
178+
$mailBlockerMock2->save();
179+
}
180+
181+
/**
182+
* Helper to create and return a new Mail Message
183+
* @param string $subject
184+
* @param string[] $to
185+
* @param string[] $cc
186+
* @param string[] $bcc
187+
* @return Message
188+
*/
189+
private static function getMessage(string $subject, array $to = [], array $cc = [], array $bcc = []): Message
190+
{
191+
$message = new Message(new Email());
192+
$message
193+
->subject($subject)
194+
->to($to)
195+
->cc($cc)
196+
->bcc($bcc)
197+
;
198+
return $message;
199+
}
200+
}

updates/version.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,4 @@
8383
- v2.0.1/rename_indexes.php
8484
"2.1.0": "Enforce password length rules on sign in. Compatibility fixes."
8585
"2.2.0": "Add avatar removal. Password resets will activate users if User activation mode is enabled."
86+
"2.2.1": "Fixes a bug introduced by the adoption of symfony/mime required since Laravel 7.x where sending an email to a blocked email address would not be prevented."

0 commit comments

Comments
 (0)