Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Symfony\Component\Mailer\Transport\Smtp;
13
 
14
use Psr\EventDispatcher\EventDispatcherInterface;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\Mailer\Envelope;
17
use Symfony\Component\Mailer\Exception\LogicException;
18
use Symfony\Component\Mailer\Exception\TransportException;
19
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
20
use Symfony\Component\Mailer\SentMessage;
21
use Symfony\Component\Mailer\Transport\AbstractTransport;
22
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
23
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
24
use Symfony\Component\Mime\RawMessage;
25
 
26
/**
27
 * Sends emails over SMTP.
28
 *
29
 * @author Fabien Potencier <fabien@symfony.com>
30
 * @author Chris Corbyn
31
 */
32
class SmtpTransport extends AbstractTransport
33
{
34
    private bool $started = false;
35
    private int $restartThreshold = 100;
36
    private int $restartThresholdSleep = 0;
37
    private int $restartCounter = 0;
38
    private int $pingThreshold = 100;
39
    private float $lastMessageTime = 0;
40
    private AbstractStream $stream;
41
    private string $mtaResult = '';
42
    private string $domain = '[127.0.0.1]';
43
 
44
    public function __construct(AbstractStream $stream = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
45
    {
46
        parent::__construct($dispatcher, $logger);
47
 
48
        $this->stream = $stream ?? new SocketStream();
49
    }
50
 
51
    public function getStream(): AbstractStream
52
    {
53
        return $this->stream;
54
    }
55
 
56
    /**
57
     * Sets the maximum number of messages to send before re-starting the transport.
58
     *
59
     * By default, the threshold is set to 100 (and no sleep at restart).
60
     *
61
     * @param int $threshold The maximum number of messages (0 to disable)
62
     * @param int $sleep     The number of seconds to sleep between stopping and re-starting the transport
63
     *
64
     * @return $this
65
     */
66
    public function setRestartThreshold(int $threshold, int $sleep = 0): static
67
    {
68
        $this->restartThreshold = $threshold;
69
        $this->restartThresholdSleep = $sleep;
70
 
71
        return $this;
72
    }
73
 
74
    /**
75
     * Sets the minimum number of seconds required between two messages, before the server is pinged.
76
     * If the transport wants to send a message and the time since the last message exceeds the specified threshold,
77
     * the transport will ping the server first (NOOP command) to check if the connection is still alive.
78
     * Otherwise the message will be sent without pinging the server first.
79
     *
80
     * Do not set the threshold too low, as the SMTP server may drop the connection if there are too many
81
     * non-mail commands (like pinging the server with NOOP).
82
     *
83
     * By default, the threshold is set to 100 seconds.
84
     *
85
     * @param int $seconds The minimum number of seconds between two messages required to ping the server
86
     *
87
     * @return $this
88
     */
89
    public function setPingThreshold(int $seconds): static
90
    {
91
        $this->pingThreshold = $seconds;
92
 
93
        return $this;
94
    }
95
 
96
    /**
97
     * Sets the name of the local domain that will be used in HELO.
98
     *
99
     * This should be a fully-qualified domain name and should be truly the domain
100
     * you're using.
101
     *
102
     * If your server does not have a domain name, use the IP address. This will
103
     * automatically be wrapped in square brackets as described in RFC 5321,
104
     * section 4.1.3.
105
     *
106
     * @return $this
107
     */
108
    public function setLocalDomain(string $domain): static
109
    {
110
        if ('' !== $domain && '[' !== $domain[0]) {
111
            if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) {
112
                $domain = '['.$domain.']';
113
            } elseif (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) {
114
                $domain = '[IPv6:'.$domain.']';
115
            }
116
        }
117
 
118
        $this->domain = $domain;
119
 
120
        return $this;
121
    }
122
 
123
    /**
124
     * Gets the name of the domain that will be used in HELO.
125
     *
126
     * If an IP address was specified, this will be returned wrapped in square
127
     * brackets as described in RFC 5321, section 4.1.3.
128
     */
129
    public function getLocalDomain(): string
130
    {
131
        return $this->domain;
132
    }
133
 
134
    public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage
135
    {
136
        try {
137
            $message = parent::send($message, $envelope);
138
        } catch (TransportExceptionInterface $e) {
139
            if ($this->started) {
140
                try {
141
                    $this->executeCommand("RSET\r\n", [250]);
142
                } catch (TransportExceptionInterface) {
143
                    // ignore this exception as it probably means that the server error was final
144
                }
145
            }
146
 
147
            throw $e;
148
        }
149
 
150
        if ($this->mtaResult && $messageId = $this->parseMessageId($this->mtaResult)) {
151
            $message->setMessageId($messageId);
152
        }
153
 
154
        $this->checkRestartThreshold();
155
 
156
        return $message;
157
    }
158
 
159
    protected function parseMessageId(string $mtaResult): string
160
    {
161
        $regexps = [
162
            '/250 Ok (?P<id>[0-9a-f-]+)\r?$/mis',
163
            '/250 Ok:? queued as (?P<id>[A-Z0-9]+)\r?$/mis',
164
        ];
165
        $matches = [];
166
        foreach ($regexps as $regexp) {
167
            if (preg_match($regexp, $mtaResult, $matches)) {
168
                return $matches['id'];
169
            }
170
        }
171
 
172
        return '';
173
    }
174
 
175
    public function __toString(): string
176
    {
177
        if ($this->stream instanceof SocketStream) {
178
            $name = sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost());
179
            $port = $this->stream->getPort();
180
            if (!(25 === $port || ($tls && 465 === $port))) {
181
                $name .= ':'.$port;
182
            }
183
 
184
            return $name;
185
        }
186
 
187
        return 'smtp://sendmail';
188
    }
189
 
190
    /**
191
     * Runs a command against the stream, expecting the given response codes.
192
     *
193
     * @param int[] $codes
194
     *
195
     * @throws TransportException when an invalid response if received
196
     */
197
    public function executeCommand(string $command, array $codes): string
198
    {
199
        $this->stream->write($command);
200
        $response = $this->getFullResponse();
201
        $this->assertResponseCode($response, $codes);
202
 
203
        return $response;
204
    }
205
 
206
    protected function doSend(SentMessage $message): void
207
    {
208
        if (microtime(true) - $this->lastMessageTime > $this->pingThreshold) {
209
            $this->ping();
210
        }
211
 
212
        if (!$this->started) {
213
            $this->start();
214
        }
215
 
216
        try {
217
            $envelope = $message->getEnvelope();
218
            $this->doMailFromCommand($envelope->getSender()->getEncodedAddress());
219
            foreach ($envelope->getRecipients() as $recipient) {
220
                $this->doRcptToCommand($recipient->getEncodedAddress());
221
            }
222
 
223
            $this->executeCommand("DATA\r\n", [354]);
224
            try {
225
                foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) {
226
                    $this->stream->write($chunk, false);
227
                }
228
                $this->stream->flush();
229
            } catch (TransportExceptionInterface $e) {
230
                throw $e;
231
            } catch (\Exception $e) {
232
                $this->stream->terminate();
233
                $this->started = false;
234
                $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
235
                throw $e;
236
            }
237
            $this->mtaResult = $this->executeCommand("\r\n.\r\n", [250]);
238
            $message->appendDebug($this->stream->getDebug());
239
            $this->lastMessageTime = microtime(true);
240
        } catch (TransportExceptionInterface $e) {
241
            $e->appendDebug($this->stream->getDebug());
242
            $this->lastMessageTime = 0;
243
            throw $e;
244
        }
245
    }
246
 
247
    /**
248
     * @internal since version 6.1, to be made private in 7.0
249
     * @final since version 6.1, to be made private in 7.0
250
     */
251
    protected function doHeloCommand(): void
252
    {
253
        $this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]);
254
    }
255
 
256
    private function doMailFromCommand(string $address): void
257
    {
258
        $this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]);
259
    }
260
 
261
    private function doRcptToCommand(string $address): void
262
    {
263
        $this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]);
264
    }
265
 
266
    public function start(): void
267
    {
268
        if ($this->started) {
269
            return;
270
        }
271
 
272
        $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__));
273
 
274
        $this->stream->initialize();
275
        $this->assertResponseCode($this->getFullResponse(), [220]);
276
        $this->doHeloCommand();
277
        $this->started = true;
278
        $this->lastMessageTime = 0;
279
 
280
        $this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__));
281
    }
282
 
283
    /**
284
     * Manually disconnect from the SMTP server.
285
     *
286
     * In most cases this is not necessary since the disconnect happens automatically on termination.
287
     * In cases of long-running scripts, this might however make sense to avoid keeping an open
288
     * connection to the SMTP server in between sending emails.
289
     */
290
    public function stop(): void
291
    {
292
        if (!$this->started) {
293
            return;
294
        }
295
 
296
        $this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__));
297
 
298
        try {
299
            $this->executeCommand("QUIT\r\n", [221]);
300
        } catch (TransportExceptionInterface) {
301
        } finally {
302
            $this->stream->terminate();
303
            $this->started = false;
304
            $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__));
305
        }
306
    }
307
 
308
    private function ping(): void
309
    {
310
        if (!$this->started) {
311
            return;
312
        }
313
 
314
        try {
315
            $this->executeCommand("NOOP\r\n", [250]);
316
        } catch (TransportExceptionInterface) {
317
            $this->stop();
318
        }
319
    }
320
 
321
    /**
322
     * @throws TransportException if a response code is incorrect
323
     */
324
    private function assertResponseCode(string $response, array $codes): void
325
    {
326
        if (!$codes) {
327
            throw new LogicException('You must set the expected response code.');
328
        }
329
 
330
        [$code] = sscanf($response, '%3d');
331
        $valid = \in_array($code, $codes);
332
 
333
        if (!$valid || !$response) {
334
            $codeStr = $code ? sprintf('code "%s"', $code) : 'empty code';
335
            $responseStr = $response ? sprintf(', with message "%s"', trim($response)) : '';
336
 
337
            throw new TransportException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0);
338
        }
339
    }
340
 
341
    private function getFullResponse(): string
342
    {
343
        $response = '';
344
        do {
345
            $line = $this->stream->readLine();
346
            $response .= $line;
347
        } while ($line && isset($line[3]) && ' ' !== $line[3]);
348
 
349
        return $response;
350
    }
351
 
352
    private function checkRestartThreshold(): void
353
    {
354
        // when using sendmail via non-interactive mode, the transport is never "started"
355
        if (!$this->started) {
356
            return;
357
        }
358
 
359
        ++$this->restartCounter;
360
        if ($this->restartCounter < $this->restartThreshold) {
361
            return;
362
        }
363
 
364
        $this->stop();
365
        if (0 < $sleep = $this->restartThresholdSleep) {
366
            $this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep));
367
 
368
            sleep($sleep);
369
        }
370
        $this->start();
371
        $this->restartCounter = 0;
372
    }
373
 
374
    public function __sleep(): array
375
    {
376
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
377
    }
378
 
379
    public function __wakeup()
380
    {
381
        throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
382
    }
383
 
384
    public function __destruct()
385
    {
386
        $this->stop();
387
    }
388
}