Issues (3627)

EmailBundle/Swiftmailer/Amazon/AmazonCallback.php (1 issue)

1
<?php
2
3
/*
4
 * @copyright   2020 Mautic Contributors. All rights reserved
5
 * @author      Mautic
6
 *
7
 * @link        http://mautic.org
8
 *
9
 * @license     GNU/GPLv3 http://www.gnu.org/licenses/gpl-3.0.html
10
 */
11
12
namespace Mautic\EmailBundle\Swiftmailer\Amazon;
13
14
use Joomla\Http\Exception\UnexpectedResponseException;
15
use Joomla\Http\Http;
16
use Mautic\EmailBundle\Model\TransportCallback;
17
use Mautic\EmailBundle\MonitoredEmail\Exception\BounceNotFound;
18
use Mautic\EmailBundle\MonitoredEmail\Exception\UnsubscriptionNotFound;
19
use Mautic\EmailBundle\MonitoredEmail\Message;
20
use Mautic\EmailBundle\MonitoredEmail\Processor\Bounce\BouncedEmail;
21
use Mautic\EmailBundle\MonitoredEmail\Processor\Bounce\Definition\Category;
22
use Mautic\EmailBundle\MonitoredEmail\Processor\Bounce\Definition\Type;
23
use Mautic\EmailBundle\MonitoredEmail\Processor\Unsubscription\UnsubscribedEmail;
24
use Mautic\LeadBundle\Entity\DoNotContact;
25
use Psr\Log\LoggerInterface;
26
use Symfony\Component\HttpFoundation\Request;
27
use Symfony\Component\HttpKernel\Exception\HttpException;
28
use Symfony\Component\Translation\TranslatorInterface;
29
30
class AmazonCallback
31
{
32
    /**
33
     * From address for SNS email.
34
     */
35
    const SNS_ADDRESS = '[email protected]';
36
37
    /**
38
     * @var TranslatorInterface
39
     */
40
    private $translator;
41
42
    /**
43
     * @var LoggerInterface
44
     */
45
    private $logger;
46
47
    /**
48
     * @var Http
49
     */
50
    private $httpClient;
51
52
    /**
53
     * @var TransportCallback
54
     */
55
    private $transportCallback;
56
57
    public function __construct(TranslatorInterface $translator, LoggerInterface $logger, Http $httpClient, TransportCallback $transportCallback)
58
    {
59
        $this->translator        = $translator;
60
        $this->logger            = $logger;
61
        $this->transportCallback = $transportCallback;
62
        $this->httpClient        = $httpClient;
63
    }
64
65
    /**
66
     * Handle bounces & complaints from Amazon.
67
     *
68
     * @return array
69
     */
70
    public function processCallbackRequest(Request $request)
71
    {
72
        $this->logger->debug('Receiving webhook from Amazon');
73
74
        $payload = json_decode($request->getContent(), true);
75
76
        if (0 !== json_last_error()) {
77
            throw new HttpException(400, 'AmazonCallback: Invalid JSON Payload');
78
        }
79
80
        if (!isset($payload['Type']) && !isset($payload['eventType'])) {
81
            throw new HttpException(400, "Key 'Type' not found in payload ");
82
        }
83
84
        // determine correct key for message type (global or via ConfigurationSet)
85
        $type = (array_key_exists('Type', $payload) ? $payload['Type'] : $payload['eventType']);
86
87
        return $this->processJsonPayload($payload, $type);
88
    }
89
90
    /**
91
     * Process json request from Amazon SES.
92
     *
93
     * http://docs.aws.amazon.com/ses/latest/DeveloperGuide/best-practices-bounces-complaints.html
94
     *
95
     * @param array $payload from Amazon SES
96
     */
97
    public function processJsonPayload(array $payload, $type)
98
    {
99
        switch ($type) {
100
            case 'SubscriptionConfirmation':
101
102
                    // Confirm Amazon SNS subscription by calling back the SubscribeURL from the playload
103
                    try {
104
                        $response = $this->httpClient->get($payload['SubscribeURL']);
105
                        if (200 == $response->code) {
106
                            $this->logger->info('Callback to SubscribeURL from Amazon SNS successfully');
107
                            break;
108
                        }
109
110
                        $reason = 'HTTP Code '.$response->code.', '.$response->body;
111
                    } catch (UnexpectedResponseException $e) {
112
                        $reason = $e->getMessage();
113
                    }
114
115
                    $this->logger->error('Callback to SubscribeURL from Amazon SNS failed, reason: '.$reason);
116
            break;
117
            case 'Notification':
118
                $message = json_decode($payload['Message'], true);
119
120
                $this->processJsonPayload($message, $message['notificationType']);
121
            break;
122
            case 'Complaint':
123
                foreach ($payload['complaint']['complainedRecipients'] as $complainedRecipient) {
124
                    $reason = null;
125
                    if (isset($payload['complaint']['complaintFeedbackType'])) {
126
                        // http://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html#complaint-object
127
                        switch ($payload['complaint']['complaintFeedbackType']) {
128
                            case 'abuse':
129
                                $reason = $this->translator->trans('mautic.email.complaint.reason.abuse');
130
                                break;
131
                            case 'fraud':
132
                                $reason = $this->translator->trans('mautic.email.complaint.reason.fraud');
133
                                break;
134
                            case 'virus':
135
                                $reason = $this->translator->trans('mautic.email.complaint.reason.virus');
136
                                break;
137
                        }
138
                    }
139
140
                    if (null == $reason) {
0 ignored issues
show
It seems like you are loosely comparing $reason of type null|string against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
141
                        $reason = $this->translator->trans('mautic.email.complaint.reason.unknown');
142
                    }
143
144
                    $this->transportCallback->addFailureByAddress($complainedRecipient['emailAddress'], $reason, DoNotContact::UNSUBSCRIBED);
145
146
                    $this->logger->debug("Unsubscribe email '".$complainedRecipient['emailAddress']."'");
147
                }
148
149
            break;
150
            case 'Bounce':
151
152
                if ('Permanent' == $payload['bounce']['bounceType']) {
153
                    $emailId = null;
154
155
                    if (isset($payload['mail']['headers'])) {
156
                        foreach ($payload['mail']['headers'] as $header) {
157
                            if ('X-EMAIL-ID' === $header['name']) {
158
                                $emailId = $header['value'];
159
                            }
160
                        }
161
                    }
162
163
                    // Get bounced recipients in an array
164
                    $bouncedRecipients = $payload['bounce']['bouncedRecipients'];
165
                    foreach ($bouncedRecipients as $bouncedRecipient) {
166
                        $bounceCode = array_key_exists('diagnosticCode', $bouncedRecipient) ? $bouncedRecipient['diagnosticCode'] : 'unknown';
167
                        $this->transportCallback->addFailureByAddress($bouncedRecipient['emailAddress'], $bounceCode, DoNotContact::BOUNCED, $emailId);
168
                        $this->logger->debug("Mark email '".$bouncedRecipient['emailAddress']."' as bounced, reason: ".$bounceCode);
169
                    }
170
                }
171
            break;
172
            default:
173
                $this->logger->warn("Received SES webhook of type '$payload[Type]' but couldn't understand payload");
174
                $this->logger->debug('SES webhook payload: '.json_encode($payload));
175
            break;
176
        }
177
    }
178
179
    /**
180
     * @throws BounceNotFound
181
     */
182
    public function processBounce(Message $message)
183
    {
184
        if (self::SNS_ADDRESS !== $message->fromAddress) {
185
            throw new BounceNotFound();
186
        }
187
188
        $message = $this->getSnsPayload($message->textPlain);
189
        $typeKey = (array_key_exists('eventType', $message) ? 'eventType' : 'notificationType');
190
        if ('Bounce' !== $message[$typeKey]) {
191
            throw new BounceNotFound();
192
        }
193
194
        $bounce = new BouncedEmail();
195
        $bounce->setContactEmail($message['bounce']['bouncedRecipients'][0]['emailAddress'])
196
            ->setBounceAddress($message['mail']['source'])
197
            ->setType(Type::UNKNOWN)
198
            ->setRuleCategory(Category::UNKNOWN)
199
            ->setRuleNumber('0013')
200
            ->setIsFinal(true);
201
202
        return $bounce;
203
    }
204
205
    /**
206
     * @return UnsubscribedEmail
207
     *
208
     * @throws UnsubscriptionNotFound
209
     */
210
    public function processUnsubscription(Message $message)
211
    {
212
        if (self::SNS_ADDRESS !== $message->fromAddress) {
213
            throw new UnsubscriptionNotFound();
214
        }
215
216
        $message = $this->getSnsPayload($message->textPlain);
217
        $typeKey = (array_key_exists('eventType', $message) ? 'eventType' : 'notificationType');
218
        if ('Complaint' !== $message[$typeKey]) {
219
            throw new UnsubscriptionNotFound();
220
        }
221
222
        return new UnsubscribedEmail($message['complaint']['complainedRecipients'][0]['emailAddress'], $message['mail']['source']);
223
    }
224
225
    /**
226
     * @param string $body
227
     *
228
     * @return array
229
     */
230
    public function getSnsPayload($body)
231
    {
232
        return json_decode(strtok($body, "\n"), true);
233
    }
234
}
235