Passed
Push — master ( 062b8f...6eb324 )
by Adrien
07:53
created

Mailer::sendMessage()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4.0312

Importance

Changes 0
Metric Value
cc 4
dl 0
loc 23
rs 9.7666
c 0
b 0
f 0
eloc 15
nc 8
nop 1
ccs 14
cts 16
cp 0.875
crap 4.0312
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Service;
6
7
use Application\Model\Message;
8
use Application\Repository\LogRepository;
9
use Application\Repository\MessageRepository;
10
use Cake\Chronos\Chronos;
11
use Doctrine\ORM\EntityManager;
12
use Exception;
13
use Laminas\Mail;
14
use Laminas\Mail\Transport\TransportInterface;
15
use Laminas\Mime\Message as MimeMessage;
16
use Laminas\Mime\Mime;
17
use Laminas\Mime\Part as MimePart;
18
19
/**
20
 * Service to send a message as an email
21
 */
22
class Mailer
23
{
24
    /**
25
     * @var resource
26
     */
27
    private $lock;
28
29
    /**
30
     * @var EntityManager
31
     */
32
    private $entityManager;
33
34
    /**
35
     * @var TransportInterface
36
     */
37
    private $transport;
38
39
    /**
40
     * @var null|string
41
     */
42
    private $emailOverride;
43
44
    /**
45
     * @var string
46
     */
47
    private $fromEmail;
48
49
    /**
50
     * @var string
51
     */
52
    private $phpPath;
53
54 2
    public function __construct(EntityManager $entityManager, TransportInterface $transport, ?string $emailOverride, string $fromEmail, string $phpPath)
55
    {
56 2
        $this->entityManager = $entityManager;
57 2
        $this->transport = $transport;
58 2
        $this->emailOverride = $emailOverride;
59 2
        $this->fromEmail = $fromEmail;
60 2
        $this->phpPath = $phpPath;
61 2
    }
62
63
    /**
64
     * Send a message asynchronously in a separate process.
65
     *
66
     * This should be the preferred way to send a message, unless if we are the cron.
67
     *
68
     * @param Message $message
69
     */
70 4
    public function sendMessageAsync(Message $message): void
71
    {
72
        // Be sure we have an ID before "forking" process
73 4
        if ($message->getId() === null) {
0 ignored issues
show
introduced by
The condition $message->getId() === null is always false.
Loading history...
74 4
            $this->entityManager->flush();
75
        }
76
77
        $args = [
78 4
            realpath('bin/send-message.php'),
79 4
            $message->getId(),
80
        ];
81
82 4
        $escapedArgs = array_map('escapeshellarg', $args);
83
84 4
        $cmd = escapeshellcmd($this->phpPath) . ' ' . implode(' ', $escapedArgs) . ' > /dev/null 2>&1 &';
85 4
        exec($cmd);
86 4
    }
87
88
    /**
89
     * Send a message
90
     *
91
     * @param Message $message
92
     */
93 1
    public function sendMessage(Message $message): void
94
    {
95 1
        $mailMessage = $this->modelMessageToMailMessage($message);
96
97 1
        $email = $message->getEmail();
98 1
        $overriddenBy = '';
99 1
        if ($this->emailOverride) {
100
            $email = $this->emailOverride;
101
            $overriddenBy = ' overridden by ' . $email;
102
        }
103
104 1
        $recipientName = $message->getRecipient() ? $message->getRecipient()->getName() : null;
105 1
        if ($email) {
106 1
            $mailMessage->addTo($email, $recipientName);
107 1
            $this->transport->send($mailMessage);
108
        }
109
110 1
        $message->setDateSent(new Chronos());
111 1
        $this->entityManager->flush();
112
113 1
        $addressList = $mailMessage->getFrom();
114 1
        $addressList->rewind();
115 1
        echo 'email from ' . $addressList->current()->getEmail() . ' sent to: ' . $message->getEmail() . "\t" . $overriddenBy . "\t" . $message->getSubject() . PHP_EOL;
116 1
    }
117
118
    /**
119
     * Convert our model message to a mail message
120
     *
121
     * @param Message $modelMessage
122
     *
123
     * @return Mail\Message
124
     */
125 1
    private function modelMessageToMailMessage(Message $modelMessage): Mail\Message
126
    {
127
        // set Mime type html
128 1
        $htmlPart = new MimePart($modelMessage->getBody());
129 1
        $htmlPart->type = Mime::TYPE_HTML;
130 1
        $htmlPart->charset = 'UTF-8';
131 1
        $htmlPart->encoding = Mime::ENCODING_BASE64;
132
133 1
        $body = new MimeMessage();
134 1
        $body->setParts([$htmlPart]);
135
136 1
        $mailMessage = new Mail\Message();
137 1
        $mailMessage->setEncoding('UTF-8');
138 1
        $mailMessage->setSubject($modelMessage->getSubject());
139 1
        $mailMessage->setBody($body);
140 1
        $mailMessage->setFrom($this->fromEmail, 'Ichtus');
141
142 1
        return $mailMessage;
143
    }
144
145
    /**
146
     * Send all messages that are not sent yet
147
     */
148
    public function sendAllMessages(): void
149
    {
150
        $this->acquireLock();
151
152
        /** @var MessageRepository $messageRepository */
153
        $messageRepository = $this->entityManager->getRepository(Message::class);
154
        $messages = $messageRepository->getAllMessageToSend();
155
        foreach ($messages as $message) {
156
            $this->sendMessage($message);
157
        }
158
    }
159
160
    /**
161
     * Acquire an exclusive lock
162
     *
163
     * This is to ensure only one mailer can run at any given time. This is to prevent sending the same email twice.
164
     */
165
    private function acquireLock(): void
166
    {
167
        $lockFile = 'data/tmp/mailer.lock';
168
        touch($lockFile);
169
        $this->lock = fopen($lockFile, 'r+');
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen($lockFile, 'r+') can also be of type false. However, the property $lock is declared as type resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
170
        if ($this->lock === false) {
171
            throw new Exception('Could not read lock file. This is not normal and might be a permission issue');
172
        }
173
174
        if (!flock($this->lock, LOCK_EX | LOCK_NB)) {
175
            $message = LogRepository::MAILER_LOCKED;
176
            _log()->info($message);
177
178
            echo $message . PHP_EOL;
179
            echo 'If the problem persist and another mailing is not in progress, try deleting ' . $lockFile . PHP_EOL;
180
181
            // Not getting the lock is not considered as error to avoid being spammed
182
            die();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
183
        }
184
    }
185
}
186