1 | <?php |
||||
2 | |||||
3 | namespace LeKoala\Mandrill; |
||||
4 | |||||
5 | use Mandrill; |
||||
6 | use Exception; |
||||
7 | use Psr\Log\LoggerInterface; |
||||
8 | use Symfony\Component\Mime\Email; |
||||
9 | use SilverStripe\Control\Director; |
||||
10 | use Symfony\Component\Mailer\Envelope; |
||||
11 | use SilverStripe\Assets\FileNameFilter; |
||||
12 | use Symfony\Component\HttpClient\Response\MockResponse; |
||||
13 | use Symfony\Component\Mailer\SentMessage; |
||||
14 | use Symfony\Component\Mailer\Header\TagHeader; |
||||
15 | use Symfony\Component\Mailer\Header\MetadataHeader; |
||||
16 | use Symfony\Contracts\HttpClient\ResponseInterface; |
||||
17 | use Symfony\Component\Mime\Header\UnstructuredHeader; |
||||
18 | use Symfony\Contracts\HttpClient\HttpClientInterface; |
||||
19 | use Symfony\Component\Mailer\Transport\AbstractApiTransport; |
||||
20 | use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; |
||||
21 | |||||
22 | /** |
||||
23 | * We create our own class |
||||
24 | * We cannot extend easily due to private methods |
||||
25 | * |
||||
26 | * @link https://www.Mandrill.com/api#/reference/introduction |
||||
27 | * @link https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Mailer/Bridge/Mailchimp/Transport/MandrillApiTransport.php |
||||
28 | * @author LeKoala <[email protected]> |
||||
29 | */ |
||||
30 | class MandrillApiTransport extends AbstractApiTransport |
||||
31 | { |
||||
32 | private const HOST = 'mandrillapp.com'; |
||||
33 | |||||
34 | /** |
||||
35 | * @var Mandrill |
||||
36 | */ |
||||
37 | private $apiClient; |
||||
38 | |||||
39 | private $apiResult; |
||||
40 | |||||
41 | public function __construct(Mandrill $apiClient, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) |
||||
42 | { |
||||
43 | $this->apiClient = $apiClient; |
||||
44 | |||||
45 | parent::__construct($client, $dispatcher, $logger); |
||||
46 | } |
||||
47 | |||||
48 | public function __toString(): string |
||||
49 | { |
||||
50 | return sprintf('mandrill+api://%s', $this->getEndpoint()); |
||||
51 | } |
||||
52 | |||||
53 | protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface |
||||
54 | { |
||||
55 | $disableSending = $email->getHeaders()->has('X-SendingDisabled') || !MandrillHelper::getSendingEnabled(); |
||||
56 | |||||
57 | // We don't really care about the actual response |
||||
58 | $response = new MockResponse(); |
||||
59 | if ($disableSending) { |
||||
60 | $result = []; |
||||
61 | foreach ($email->getTo() as $recipient) { |
||||
62 | $result[] = [ |
||||
63 | 'email' => $recipient->toString(), |
||||
64 | 'status' => 'sent', |
||||
65 | 'reject_reason' => '', |
||||
66 | '_id' => uniqid(), |
||||
67 | 'disabled' => true, |
||||
68 | ]; |
||||
69 | } |
||||
70 | } else { |
||||
71 | $payload = $this->getPayload($email, $envelope); |
||||
72 | $result = $this->apiClient->messages->send($payload); |
||||
73 | |||||
74 | $sendCount = 0; |
||||
75 | foreach ($result as $item) { |
||||
76 | if ($item['status'] === 'sent' || $item['status'] === 'queued') { |
||||
77 | $sendCount++; |
||||
78 | } |
||||
79 | } |
||||
80 | } |
||||
81 | |||||
82 | $this->apiResult = $result; |
||||
83 | |||||
84 | $firstRecipient = reset($result); |
||||
85 | if ($firstRecipient) { |
||||
86 | $sentMessage->setMessageId($firstRecipient['_id']); |
||||
87 | } |
||||
88 | |||||
89 | if (MandrillHelper::getLoggingEnabled()) { |
||||
90 | $this->logMessageContent($email, $result); |
||||
91 | } |
||||
92 | |||||
93 | return $response; |
||||
94 | } |
||||
95 | |||||
96 | public function getApiResult() |
||||
97 | { |
||||
98 | return $this->apiResult; |
||||
99 | } |
||||
100 | |||||
101 | private function getEndpoint(): ?string |
||||
102 | { |
||||
103 | return ($this->host ?: self::HOST) . ($this->port ? ':' . $this->port : ''); |
||||
104 | } |
||||
105 | |||||
106 | private function getPayload(Email $email, Envelope $envelope): array |
||||
107 | { |
||||
108 | $message = array_merge(MandrillHelper::config()->default_params, [ |
||||
109 | 'html' => $email->getHtmlBody(), |
||||
110 | 'text' => $email->getTextBody(), |
||||
111 | 'subject' => $email->getSubject(), |
||||
112 | 'from_email' => $envelope->getSender()->getAddress(), |
||||
113 | 'to' => $this->getRecipients($email, $envelope), |
||||
114 | ]); |
||||
115 | |||||
116 | if ('' !== $envelope->getSender()->getName()) { |
||||
117 | $message['from_name'] = $envelope->getSender()->getName(); |
||||
118 | } |
||||
119 | |||||
120 | foreach ($email->getAttachments() as $attachment) { |
||||
121 | $headers = $attachment->getPreparedHeaders(); |
||||
122 | $disposition = $headers->getHeaderBody('Content-Disposition'); |
||||
123 | $att = [ |
||||
124 | 'content' => $attachment->bodyToString(), |
||||
125 | 'type' => $headers->get('Content-Type')->getBody(), |
||||
126 | ]; |
||||
127 | if ($name = $headers->getHeaderParameter('Content-Disposition', 'name')) { |
||||
128 | $att['name'] = $name; |
||||
129 | } |
||||
130 | if ('inline' === $disposition) { |
||||
131 | $message['images'][] = $att; |
||||
132 | } else { |
||||
133 | $message['attachments'][] = $att; |
||||
134 | } |
||||
135 | } |
||||
136 | |||||
137 | $headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type']; |
||||
138 | foreach ($email->getHeaders()->all() as $name => $header) { |
||||
139 | if (\in_array($name, $headersToBypass, true)) { |
||||
140 | continue; |
||||
141 | } |
||||
142 | if ($header instanceof TagHeader) { |
||||
143 | $message['tags'] = array_merge( |
||||
144 | $message['tags'] ?? [], |
||||
145 | explode(',', $header->getValue()) |
||||
146 | ); |
||||
147 | continue; |
||||
148 | } |
||||
149 | if ($header instanceof MetadataHeader) { |
||||
150 | $message['metadata'][$header->getKey()] = $header->getValue(); |
||||
151 | continue; |
||||
152 | } |
||||
153 | $message['headers'][$header->getName()] = $header->getBodyAsString(); |
||||
154 | } |
||||
155 | |||||
156 | foreach ($email->getHeaders()->all() as $name => $header) { |
||||
157 | if (!($header instanceof UnstructuredHeader)) { |
||||
158 | continue; |
||||
159 | } |
||||
160 | $headerValue = $header->getValue(); |
||||
161 | switch ($name) { |
||||
162 | case 'List-Unsubscribe': |
||||
163 | $message['headers']['List-Unsubscribe'] = $headerValue; |
||||
164 | break; |
||||
165 | case 'X-MC-InlineCSS': |
||||
166 | $message['inline_css'] = $headerValue; |
||||
167 | break; |
||||
168 | case 'X-MC-Tags': |
||||
169 | $tags = $headerValue; |
||||
170 | if (!is_array($tags)) { |
||||
171 | $tags = explode(',', $tags); |
||||
172 | } |
||||
173 | $message['tags'] = $tags; |
||||
174 | break; |
||||
175 | case 'X-MC-Autotext': |
||||
176 | $autoText = $headerValue; |
||||
177 | if (in_array($autoText, array('true', 'on', 'yes', 'y', true), true)) { |
||||
178 | $message['auto_text'] = true; |
||||
179 | } |
||||
180 | if (in_array($autoText, array('false', 'off', 'no', 'n', false), true)) { |
||||
181 | $message['auto_text'] = false; |
||||
182 | } |
||||
183 | break; |
||||
184 | case 'X-MC-GoogleAnalytics': |
||||
185 | $analyticsDomains = explode(',', $headerValue); |
||||
186 | if (is_array($analyticsDomains)) { |
||||
187 | $message['google_analytics_domains'] = $analyticsDomains; |
||||
188 | } |
||||
189 | break; |
||||
190 | case 'X-MC-GoogleAnalyticsCampaign': |
||||
191 | $message['google_analytics_campaign'] = $headerValue; |
||||
192 | break; |
||||
193 | default: |
||||
194 | if (strncmp($header->getName(), 'X-', 2) === 0) { |
||||
195 | $message['headers'][$header->getName()] = $headerValue; |
||||
196 | } |
||||
197 | break; |
||||
198 | } |
||||
199 | } |
||||
200 | |||||
201 | return $message; |
||||
202 | } |
||||
203 | |||||
204 | protected function getRecipients(Email $email, Envelope $envelope): array |
||||
205 | { |
||||
206 | $recipients = []; |
||||
207 | foreach ($envelope->getRecipients() as $recipient) { |
||||
208 | $type = 'to'; |
||||
209 | if (\in_array($recipient, $email->getBcc(), true)) { |
||||
210 | $type = 'bcc'; |
||||
211 | } elseif (\in_array($recipient, $email->getCc(), true)) { |
||||
212 | $type = 'cc'; |
||||
213 | } |
||||
214 | |||||
215 | $recipientPayload = [ |
||||
216 | 'email' => $recipient->getAddress(), |
||||
217 | 'type' => $type, |
||||
218 | ]; |
||||
219 | |||||
220 | if ('' !== $recipient->getName()) { |
||||
221 | $recipientPayload['name'] = $recipient->getName(); |
||||
222 | } |
||||
223 | |||||
224 | $recipients[] = $recipientPayload; |
||||
225 | } |
||||
226 | |||||
227 | return $recipients; |
||||
228 | } |
||||
229 | |||||
230 | /** |
||||
231 | * Log message content |
||||
232 | * |
||||
233 | * @param Email $message |
||||
234 | * @param array $results Results from the api |
||||
235 | * @throws Exception |
||||
236 | */ |
||||
237 | protected function logMessageContent(Email $message, $results = []) |
||||
238 | { |
||||
239 | // Folder not set |
||||
240 | $logFolder = MandrillHelper::getLogFolder(); |
||||
241 | if (!$logFolder) { |
||||
242 | return; |
||||
243 | } |
||||
244 | // Logging disabled |
||||
245 | if (!MandrillHelper::getLoggingEnabled()) { |
||||
246 | return; |
||||
247 | } |
||||
248 | |||||
249 | $subject = $message->getSubject(); |
||||
250 | $body = $message->getBody(); |
||||
251 | $contentType = $message->getHtmlBody() !== null ? "text/html" : "text"; |
||||
252 | |||||
253 | $logContent = $body; |
||||
254 | if (is_object($logContent)) { |
||||
255 | $logContent = $logContent->toString(); |
||||
256 | } |
||||
257 | |||||
258 | // Append some extra information at the end |
||||
259 | $logContent .= '<hr><pre>Debug infos:' . "\n\n"; |
||||
260 | $logContent .= 'To : ' . print_r($message->getTo(), true) . "\n"; |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
261 | $logContent .= 'Subject : ' . $subject . "\n"; |
||||
262 | $logContent .= 'From : ' . print_r($message->getFrom(), true) . "\n"; |
||||
0 ignored issues
–
show
Are you sure
print_r($message->getFrom(), true) of type string|true can be used in concatenation ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
263 | $logContent .= 'Headers:' . "\n" . $message->getHeaders()->toString() . "\n"; |
||||
264 | if (!empty($params['recipients'])) { |
||||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||||
265 | $logContent .= 'Recipients : ' . print_r($message->getTo(), true) . "\n"; |
||||
266 | } |
||||
267 | $logContent .= 'Results:' . "\n"; |
||||
268 | $logContent .= print_r($results, true) . "\n"; |
||||
0 ignored issues
–
show
Are you sure
print_r($results, true) of type string|true can be used in concatenation ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
269 | $logContent .= '</pre>'; |
||||
270 | |||||
271 | // Generate filename |
||||
272 | $filter = new FileNameFilter(); |
||||
273 | $title = substr($filter->filter($subject), 0, 35); |
||||
274 | $logName = date('Ymd_His') . '_' . $title; |
||||
275 | |||||
276 | // Store attachments if any |
||||
277 | $attachments = $message->getAttachments(); |
||||
278 | if (!empty($attachments)) { |
||||
279 | $logContent .= '<hr />'; |
||||
280 | foreach ($attachments as $attachment) { |
||||
281 | $attachmentDestination = $logFolder . '/' . $logName . '_' . $attachment->getFilename(); |
||||
282 | file_put_contents($attachmentDestination, $attachment->getBody()); |
||||
283 | $logContent .= 'File : <a href="' . $attachmentDestination . '">' . $attachment->getFilename() . '</a><br/>'; |
||||
284 | } |
||||
285 | } |
||||
286 | |||||
287 | // Store it |
||||
288 | $ext = ($contentType == 'text/html') ? 'html' : 'txt'; |
||||
289 | $r = file_put_contents($logFolder . '/' . $logName . '.' . $ext, $logContent); |
||||
290 | |||||
291 | if (!$r && Director::isDev()) { |
||||
292 | throw new Exception('Failed to store email in ' . $logFolder); |
||||
293 | } |
||||
294 | } |
||||
295 | } |
||||
296 |