Passed
Push — master ( ac1336...fb5ae8 )
by Sylvain
08:49
created

Mailer::sendMessageAsync()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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