Failed Conditions
Push — master ( 6a8bd6...5def7d )
by Adrien
08:40
created

Mailer::sendMessage()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.0466

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 8
nop 1
dl 0
loc 21
rs 9.8333
c 0
b 0
f 0
ccs 12
cts 14
cp 0.8571
crap 4.0466
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
        echo 'email sent to: ' . $message->getEmail() . "\t" . $overriddenBy . "\t" . $message->getSubject() . PHP_EOL;
114 1
    }
115
116
    /**
117
     * Convert our model message to a mail message
118
     *
119
     * @param Message $modelMessage
120
     *
121
     * @return Mail\Message
122
     */
123 1
    private function modelMessageToMailMessage(Message $modelMessage): Mail\Message
124
    {
125
        // set Mime type html
126 1
        $htmlPart = new MimePart($modelMessage->getBody());
127 1
        $htmlPart->type = Mime::TYPE_HTML;
128 1
        $htmlPart->charset = 'UTF-8';
129 1
        $htmlPart->encoding = Mime::ENCODING_BASE64;
130
131 1
        $body = new MimeMessage();
132 1
        $body->setParts([$htmlPart]);
133
134 1
        $mailMessage = new Mail\Message();
135 1
        $mailMessage->setEncoding('UTF-8');
136 1
        $mailMessage->setSubject($modelMessage->getSubject());
137 1
        $mailMessage->setBody($body);
138 1
        $mailMessage->setFrom($this->fromEmail, 'Ichtus');
139
140 1
        return $mailMessage;
141
    }
142
143
    /**
144
     * Send all messages that are not sent yet
145
     */
146
    public function sendAllMessages(): void
147
    {
148
        $this->acquireLock();
149
150
        /** @var MessageRepository $messageRepository */
151
        $messageRepository = $this->entityManager->getRepository(Message::class);
152
        $messages = $messageRepository->getAllMessageToSend();
153
        foreach ($messages as $message) {
154
            $this->sendMessage($message);
155
        }
156
    }
157
158
    /**
159
     * Acquire an exclusive lock
160
     *
161
     * This is to ensure only one mailer can run at any given time. This is to prevent sending the same email twice.
162
     */
163
    private function acquireLock(): void
164
    {
165
        $lockFile = 'data/tmp/mailer.lock';
166
        touch($lockFile);
167
        $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...
168
        if ($this->lock === false) {
169
            throw new Exception('Could not read lock file. This is not normal and might be a permission issue');
170
        }
171
172
        if (!flock($this->lock, LOCK_EX | LOCK_NB)) {
173
            $message = LogRepository::MAILER_LOCKED;
174
            _log()->info($message);
175
176
            echo $message . PHP_EOL;
177
            echo 'If the problem persist and another mailing is not in progress, try deleting ' . $lockFile . PHP_EOL;
178
179
            // Not getting the lock is not considered as error to avoid being spammed
180
            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...
181
        }
182
    }
183
}
184