zend-blog-3-backend
284 строки · 8.5 Кб
1<?php
2
3namespace App\Service;
4
5use App\DTO\EmailMessageDTO;
6use App\Entity\Comment;
7use App\Entity\EmailSubscriptionSettings;
8use App\Repository\EmailSubscriptionSettingsRepository;
9use App\Utils\HashId;
10use App\Utils\VerifyEmail;
11use Psr\Log\LoggerInterface;
12use Swift_Mailer;
13use Swift_Message;
14use Twig\Environment as TwigEnvironment;
15use Xelbot\Telegram\Robot;
16
17class Mailer
18{
19/**
20* @var Swift_Mailer
21*/
22private $mailer;
23
24/**
25* @var TwigEnvironment
26*/
27private $twig;
28
29/**
30* @var string
31*/
32private $emailFrom;
33
34/**
35* @var Robot
36*/
37private $bot;
38
39/**
40* @var string
41*/
42private $frontendSite;
43
44private EmailSubscriptionSettingsRepository $subscriptionRepository;
45
46private LoggerInterface $logger;
47
48/**
49* @param Swift_Mailer $mailer
50* @param TwigEnvironment $twig
51* @param Robot $bot
52* @param EmailSubscriptionSettingsRepository $subscriptionRepository
53* @param string $frontendSite
54* @param string $emailFrom
55*/
56public function __construct(
57Swift_Mailer $mailer,
58TwigEnvironment $twig,
59Robot $bot,
60EmailSubscriptionSettingsRepository $subscriptionRepository,
61LoggerInterface $logger,
62string $frontendSite,
63string $emailFrom
64) {
65$this->mailer = $mailer;
66$this->twig = $twig;
67$this->emailFrom = $emailFrom;
68$this->bot = $bot;
69$this->subscriptionRepository = $subscriptionRepository;
70$this->frontendSite = $frontendSite;
71$this->logger = $logger;
72}
73
74public function newComment(Comment $comment, string $emailTo, bool $spool = true)
75{
76$emailTo = VerifyEmail::normalize($emailTo);
77$context = $this->twig->mergeGlobals($this->context($comment));
78
79$template = $this->twig->load('mails/newComment.html.twig');
80$textTemplate = $this->twig->load('mails/newComment.txt.twig');
81
82$message = new EmailMessageDTO();
83
84$message->subject = 'Новый комментарий';
85$message->from = $this->emailFrom;
86$message->to = $emailTo;
87$message->messageHtml = $template->render($context);
88$message->messageText = $textTemplate->render($context);
89
90$spool ? $this->queueMessage($message) : $this->send($message);
91}
92
93public function send(EmailMessageDTO $messageDTO): bool
94{
95if ($this->isBlocked($messageDTO)) {
96// successfully sent to black hole :)
97return true;
98}
99
100$successfullySent = false;
101try {
102$message = (new Swift_Message())
103->setSubject($messageDTO->subject)
104->setFrom($messageDTO->from)
105->setTo($messageDTO->to)
106;
107
108if ($messageDTO->messageHtml) {
109$message->addPart($messageDTO->messageHtml, 'text/html');
110}
111if ($messageDTO->messageText) {
112$message->addPart($messageDTO->messageText, 'text/plain');
113}
114
115if ($messageDTO->unsubscribeLink) {
116$headers = $message->getHeaders();
117$headers->addTextHeader(
118'List-Unsubscribe',
119sprintf('<%s%s>', $this->frontendSite, $messageDTO->unsubscribeLink)
120);
121$headers->addTextHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
122}
123
124$successfullySent = $this->mailer->send($message) > 0;
125} catch (\Throwable $e) {
126$this->logger->error('email sent error', ['exception' => $e]);
127$this->bot->sendMessage(
128'email sent error: ' . $e->getMessage()
129. "\n\nfile: " . $e->getFile()
130. "\nline: " . $e->getLine()
131);
132}
133
134return $successfullySent;
135}
136
137/**
138* Save message to spool
139*
140* @param EmailMessageDTO $messageDTO
141*/
142public function queueMessage(EmailMessageDTO $messageDTO): void
143{
144if ($this->isBlocked($messageDTO)) {
145return;
146}
147
148try {
149$randomBytes = bin2hex(random_bytes(3));
150} catch (\Exception $e) {
151$randomBytes = dechex(mt_rand(0, 255) + 256 * mt_rand(0, 255) + 65536 * mt_rand(0, 255));
152}
153
154$fileName = sprintf(
155'%s/%X%s.message',
156$this->spoolPath(),
157(int)date('U'),
158strtoupper($randomBytes)
159);
160
161$fp = fopen($fileName, 'wb');
162fwrite($fp, serialize($messageDTO));
163fclose($fp);
164}
165
166public function spoolSend($messageLimit = null, $timeLimit = null): int
167{
168$count = 0;
169$time = time();
170foreach (new \DirectoryIterator($this->spoolPath()) as $fileInfo) {
171$file = $fileInfo->getRealPath();
172if (substr($file, -8) !== '.message') {
173continue;
174}
175
176if (rename($file, $file . '.sending')) {
177$message = unserialize(
178file_get_contents($file . '.sending'),
179['allowed_classes' => [EmailMessageDTO::class]]
180);
181if ($this->send($message)) {
182$count++;
183unlink($file . '.sending');
184}
185} else {
186continue;
187}
188
189if ($messageLimit && $count >= $messageLimit) {
190break;
191}
192if ($timeLimit && (time() - $time) >= $timeLimit) {
193break;
194}
195}
196
197return $count;
198}
199
200public function replyComment(Comment $comment)
201{
202$parent = $comment->getParent();
203if ($parent) {
204$emailTo = null;
205$recipient = 'undefined';
206if ($user = $parent->getUser()) {
207$emailTo = $user->getEmail();
208$recipient = $user->getUsername();
209} elseif ($commentator = $parent->getCommentator()) {
210$emailTo = $commentator->isValidEmail() ? $commentator->getEmail() : null;
211$recipient = $commentator->getName();
212}
213
214if ($emailTo) {
215$emailTo = VerifyEmail::normalize($emailTo);
216$unsubscribeLink = $this->unsubscribeLink($emailTo, EmailSubscriptionSettings::TYPE_COMMENT_REPLY);
217
218$context = $this->twig->mergeGlobals(array_merge(
219$this->context($comment),
220[
221'unsubscribeLink' => $unsubscribeLink,
222],
223));
224
225$template = $this->twig->load('mails/replyComment.html.twig');
226$textTemplate = $this->twig->load('mails/replyComment.txt.twig');
227
228$message = new EmailMessageDTO();
229
230$message->subject = 'Ответ на комментарий';
231$message->from = $this->emailFrom;
232$message->to = [$emailTo => $recipient];
233$message->messageHtml = $template->render($context);
234$message->messageText = $textTemplate->render($context);
235$message->unsubscribeLink = $unsubscribeLink;
236
237$this->queueMessage($message);
238}
239}
240}
241
242private function spoolPath(): string
243{
244return APP_VAR_DIR . '/spool';
245}
246
247private function context(Comment $comment): array
248{
249$username = 'undefined';
250if ($user = $comment->getUser()) {
251$username = $user->getUsername();
252} elseif ($commentator = $comment->getCommentator()) {
253$username = $commentator->getName();
254}
255
256return [
257'topicTitle' => $comment->getPost()->getTitle(),
258'topicUrl' => '/article/' . $comment->getPost()->getUrl(),
259'username' => $username,
260'commentText' => $comment->getText(),
261'avatar' => $comment->getAvatarHash() . '.png',
262];
263}
264
265private function isBlocked(EmailMessageDTO $messageDTO): bool
266{
267if ($messageDTO->type === 0) {
268return false;
269}
270
271$email = $messageDTO->getRecipientEmail();
272$settings = $this->subscriptionRepository->findOneBy(['email' => $email, 'type' => $messageDTO->type]);
273
274return $settings && $settings->isBlockSending();
275}
276
277private function unsubscribeLink(string $email, int $type): string
278{
279$settings = $this->subscriptionRepository->findOrCreate($email, $type);
280$hash = HashId::hash($settings->getId(), mt_rand(1, 9999));
281
282return '/email-unsubscribe/' . $hash;
283}
284}
285