1<?php
2
3/*
4 * This file is part of SwiftMailer.
5 * (c) 2004-2009 Chris Corbyn
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * Sends Messages over SMTP.
13 *
14 * @author Chris Corbyn
15 */
16abstract class Swift_Transport_AbstractSmtpTransport implements Swift_Transport
17{
18    /** Input-Output buffer for sending/receiving SMTP commands and responses */
19    protected $_buffer;
20
21    /** Connection status */
22    protected $_started = false;
23
24    /** The domain name to use in HELO command */
25    protected $_domain = '[127.0.0.1]';
26
27    /** The event dispatching layer */
28    protected $_eventDispatcher;
29
30    /** Source Ip */
31    protected $_sourceIp;
32
33    /** Return an array of params for the Buffer */
34    abstract protected function _getBufferParams();
35
36    /**
37     * Creates a new EsmtpTransport using the given I/O buffer.
38     *
39     * @param Swift_Transport_IoBuffer     $buf
40     * @param Swift_Events_EventDispatcher $dispatcher
41     */
42    public function __construct(Swift_Transport_IoBuffer $buf, Swift_Events_EventDispatcher $dispatcher)
43    {
44        $this->_eventDispatcher = $dispatcher;
45        $this->_buffer = $buf;
46        $this->_lookupHostname();
47    }
48
49    /**
50     * Set the name of the local domain which Swift will identify itself as.
51     *
52     * This should be a fully-qualified domain name and should be truly the domain
53     * you're using.
54     *
55     * If your server doesn't have a domain name, use the IP in square
56     * brackets (i.e. [127.0.0.1]).
57     *
58     * @param string $domain
59     *
60     * @return $this
61     */
62    public function setLocalDomain($domain)
63    {
64        $this->_domain = $domain;
65
66        return $this;
67    }
68
69    /**
70     * Get the name of the domain Swift will identify as.
71     *
72     * @return string
73     */
74    public function getLocalDomain()
75    {
76        return $this->_domain;
77    }
78
79    /**
80     * Sets the source IP.
81     *
82     * @param string $source
83     */
84    public function setSourceIp($source)
85    {
86        $this->_sourceIp = $source;
87    }
88
89    /**
90     * Returns the IP used to connect to the destination.
91     *
92     * @return string
93     */
94    public function getSourceIp()
95    {
96        return $this->_sourceIp;
97    }
98
99    /**
100     * Start the SMTP connection.
101     */
102    public function start()
103    {
104        if (!$this->_started) {
105            if ($evt = $this->_eventDispatcher->createTransportChangeEvent($this)) {
106                $this->_eventDispatcher->dispatchEvent($evt, 'beforeTransportStarted');
107                if ($evt->bubbleCancelled()) {
108                    return;
109                }
110            }
111
112            try {
113                $this->_buffer->initialize($this->_getBufferParams());
114            } catch (Swift_TransportException $e) {
115                $this->_throwException($e);
116            }
117            $this->_readGreeting();
118            $this->_doHeloCommand();
119
120            if ($evt) {
121                $this->_eventDispatcher->dispatchEvent($evt, 'transportStarted');
122            }
123
124            $this->_started = true;
125        }
126    }
127
128    /**
129     * Test if an SMTP connection has been established.
130     *
131     * @return bool
132     */
133    public function isStarted()
134    {
135        return $this->_started;
136    }
137
138    /**
139     * Send the given Message.
140     *
141     * Recipient/sender data will be retrieved from the Message API.
142     * The return value is the number of recipients who were accepted for delivery.
143     *
144     * @param Swift_Mime_Message $message
145     * @param string[]           $failedRecipients An array of failures by-reference
146     *
147     * @return int
148     */
149    public function send(Swift_Mime_Message $message, &$failedRecipients = null)
150    {
151        $sent = 0;
152        $failedRecipients = (array) $failedRecipients;
153
154        if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) {
155            $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed');
156            if ($evt->bubbleCancelled()) {
157                return 0;
158            }
159        }
160
161        if (!$reversePath = $this->_getReversePath($message)) {
162            $this->_throwException(new Swift_TransportException(
163                'Cannot send message without a sender address'
164                )
165            );
166        }
167
168        $to = (array) $message->getTo();
169        $cc = (array) $message->getCc();
170        $tos = array_merge($to, $cc);
171        $bcc = (array) $message->getBcc();
172
173        $message->setBcc(array());
174
175        try {
176            $sent += $this->_sendTo($message, $reversePath, $tos, $failedRecipients);
177            $sent += $this->_sendBcc($message, $reversePath, $bcc, $failedRecipients);
178        } catch (Exception $e) {
179            $message->setBcc($bcc);
180            throw $e;
181        }
182
183        $message->setBcc($bcc);
184
185        if ($evt) {
186            if ($sent == count($to) + count($cc) + count($bcc)) {
187                $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS);
188            } elseif ($sent > 0) {
189                $evt->setResult(Swift_Events_SendEvent::RESULT_TENTATIVE);
190            } else {
191                $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED);
192            }
193            $evt->setFailedRecipients($failedRecipients);
194            $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed');
195        }
196
197        $message->generateId(); //Make sure a new Message ID is used
198
199        return $sent;
200    }
201
202    /**
203     * Stop the SMTP connection.
204     */
205    public function stop()
206    {
207        if ($this->_started) {
208            if ($evt = $this->_eventDispatcher->createTransportChangeEvent($this)) {
209                $this->_eventDispatcher->dispatchEvent($evt, 'beforeTransportStopped');
210                if ($evt->bubbleCancelled()) {
211                    return;
212                }
213            }
214
215            try {
216                $this->executeCommand("QUIT\r\n", array(221));
217            } catch (Swift_TransportException $e) {
218            }
219
220            try {
221                $this->_buffer->terminate();
222
223                if ($evt) {
224                    $this->_eventDispatcher->dispatchEvent($evt, 'transportStopped');
225                }
226            } catch (Swift_TransportException $e) {
227                $this->_throwException($e);
228            }
229        }
230        $this->_started = false;
231    }
232
233    /**
234     * Register a plugin.
235     *
236     * @param Swift_Events_EventListener $plugin
237     */
238    public function registerPlugin(Swift_Events_EventListener $plugin)
239    {
240        $this->_eventDispatcher->bindEventListener($plugin);
241    }
242
243    /**
244     * Reset the current mail transaction.
245     */
246    public function reset()
247    {
248        $this->executeCommand("RSET\r\n", array(250));
249    }
250
251    /**
252     * Get the IoBuffer where read/writes are occurring.
253     *
254     * @return Swift_Transport_IoBuffer
255     */
256    public function getBuffer()
257    {
258        return $this->_buffer;
259    }
260
261    /**
262     * Run a command against the buffer, expecting the given response codes.
263     *
264     * If no response codes are given, the response will not be validated.
265     * If codes are given, an exception will be thrown on an invalid response.
266     *
267     * @param string   $command
268     * @param int[]    $codes
269     * @param string[] $failures An array of failures by-reference
270     *
271     * @return string
272     */
273    public function executeCommand($command, $codes = array(), &$failures = null)
274    {
275        $failures = (array) $failures;
276        $seq = $this->_buffer->write($command);
277        $response = $this->_getFullResponse($seq);
278        if ($evt = $this->_eventDispatcher->createCommandEvent($this, $command, $codes)) {
279            $this->_eventDispatcher->dispatchEvent($evt, 'commandSent');
280        }
281        $this->_assertResponseCode($response, $codes);
282
283        return $response;
284    }
285
286    /** Read the opening SMTP greeting */
287    protected function _readGreeting()
288    {
289        $this->_assertResponseCode($this->_getFullResponse(0), array(220));
290    }
291
292    /** Send the HELO welcome */
293    protected function _doHeloCommand()
294    {
295        $this->executeCommand(
296            sprintf("HELO %s\r\n", $this->_domain), array(250)
297            );
298    }
299
300    /** Send the MAIL FROM command */
301    protected function _doMailFromCommand($address)
302    {
303        $this->executeCommand(
304            sprintf("MAIL FROM:<%s>\r\n", $address), array(250)
305            );
306    }
307
308    /** Send the RCPT TO command */
309    protected function _doRcptToCommand($address)
310    {
311        $this->executeCommand(
312            sprintf("RCPT TO:<%s>\r\n", $address), array(250, 251, 252)
313            );
314    }
315
316    /** Send the DATA command */
317    protected function _doDataCommand()
318    {
319        $this->executeCommand("DATA\r\n", array(354));
320    }
321
322    /** Stream the contents of the message over the buffer */
323    protected function _streamMessage(Swift_Mime_Message $message)
324    {
325        $this->_buffer->setWriteTranslations(array("\r\n." => "\r\n.."));
326        try {
327            $message->toByteStream($this->_buffer);
328            $this->_buffer->flushBuffers();
329        } catch (Swift_TransportException $e) {
330            $this->_throwException($e);
331        }
332        $this->_buffer->setWriteTranslations(array());
333        $this->executeCommand("\r\n.\r\n", array(250));
334    }
335
336    /** Determine the best-use reverse path for this message */
337    protected function _getReversePath(Swift_Mime_Message $message)
338    {
339        $return = $message->getReturnPath();
340        $sender = $message->getSender();
341        $from = $message->getFrom();
342        $path = null;
343        if (!empty($return)) {
344            $path = $return;
345        } elseif (!empty($sender)) {
346            // Don't use array_keys
347            reset($sender); // Reset Pointer to first pos
348            $path = key($sender); // Get key
349        } elseif (!empty($from)) {
350            reset($from); // Reset Pointer to first pos
351            $path = key($from); // Get key
352        }
353
354        return $path;
355    }
356
357    /** Throw a TransportException, first sending it to any listeners */
358    protected function _throwException(Swift_TransportException $e)
359    {
360        if ($evt = $this->_eventDispatcher->createTransportExceptionEvent($this, $e)) {
361            $this->_eventDispatcher->dispatchEvent($evt, 'exceptionThrown');
362            if (!$evt->bubbleCancelled()) {
363                throw $e;
364            }
365        } else {
366            throw $e;
367        }
368    }
369
370    /** Throws an Exception if a response code is incorrect */
371    protected function _assertResponseCode($response, $wanted)
372    {
373        list($code) = sscanf($response, '%3d');
374        $valid = (empty($wanted) || in_array($code, $wanted));
375
376        if ($evt = $this->_eventDispatcher->createResponseEvent($this, $response,
377            $valid)) {
378            $this->_eventDispatcher->dispatchEvent($evt, 'responseReceived');
379        }
380
381        if (!$valid) {
382            $this->_throwException(
383                new Swift_TransportException(
384                    'Expected response code '.implode('/', $wanted).' but got code '.
385                    '"'.$code.'", with message "'.$response.'"',
386                    $code)
387                );
388        }
389    }
390
391    /** Get an entire multi-line response using its sequence number */
392    protected function _getFullResponse($seq)
393    {
394        $response = '';
395        try {
396            do {
397                $line = $this->_buffer->readLine($seq);
398                $response .= $line;
399            } while (null !== $line && false !== $line && ' ' != $line[3]);
400        } catch (Swift_TransportException $e) {
401            $this->_throwException($e);
402        } catch (Swift_IoException $e) {
403            $this->_throwException(
404                new Swift_TransportException(
405                    $e->getMessage())
406                );
407        }
408
409        return $response;
410    }
411
412    /** Send an email to the given recipients from the given reverse path */
413    private function _doMailTransaction($message, $reversePath, array $recipients, array &$failedRecipients)
414    {
415        $sent = 0;
416        $this->_doMailFromCommand($reversePath);
417        foreach ($recipients as $forwardPath) {
418            try {
419                $this->_doRcptToCommand($forwardPath);
420                ++$sent;
421            } catch (Swift_TransportException $e) {
422                $failedRecipients[] = $forwardPath;
423            }
424        }
425
426        if ($sent != 0) {
427            $this->_doDataCommand();
428            $this->_streamMessage($message);
429        } else {
430            $this->reset();
431        }
432
433        return $sent;
434    }
435
436    /** Send a message to the given To: recipients */
437    private function _sendTo(Swift_Mime_Message $message, $reversePath, array $to, array &$failedRecipients)
438    {
439        if (empty($to)) {
440            return 0;
441        }
442
443        return $this->_doMailTransaction($message, $reversePath, array_keys($to),
444            $failedRecipients);
445    }
446
447    /** Send a message to all Bcc: recipients */
448    private function _sendBcc(Swift_Mime_Message $message, $reversePath, array $bcc, array &$failedRecipients)
449    {
450        $sent = 0;
451        foreach ($bcc as $forwardPath => $name) {
452            $message->setBcc(array($forwardPath => $name));
453            $sent += $this->_doMailTransaction(
454                $message, $reversePath, array($forwardPath), $failedRecipients
455                );
456        }
457
458        return $sent;
459    }
460
461    /** Try to determine the hostname of the server this is run on */
462    private function _lookupHostname()
463    {
464        if (!empty($_SERVER['SERVER_NAME']) && $this->_isFqdn($_SERVER['SERVER_NAME'])) {
465            $this->_domain = $_SERVER['SERVER_NAME'];
466        } elseif (!empty($_SERVER['SERVER_ADDR'])) {
467            // Set the address literal tag (See RFC 5321, section: 4.1.3)
468            if (false === strpos($_SERVER['SERVER_ADDR'], ':')) {
469                $prefix = ''; // IPv4 addresses are not tagged.
470            } else {
471                $prefix = 'IPv6:'; // Adding prefix in case of IPv6.
472            }
473
474            $this->_domain = sprintf('[%s%s]', $prefix, $_SERVER['SERVER_ADDR']);
475        }
476    }
477
478    /** Determine is the $hostname is a fully-qualified name */
479    private function _isFqdn($hostname)
480    {
481        // We could do a really thorough check, but there's really no point
482        if (false !== $dotPos = strpos($hostname, '.')) {
483            return ($dotPos > 0) && ($dotPos != strlen($hostname) - 1);
484        }
485
486        return false;
487    }
488
489    /**
490     * Destructor.
491     */
492    public function __destruct()
493    {
494        try {
495            $this->stop();
496        } catch (Exception $e) {
497        }
498    }
499}
500