1: <?php
2: /**
3: * SMTP implementation.
4: * Requires the Net_SMTP class.
5: *
6: * LICENSE:
7: *
8: * Copyright (c) 2010, Chuck Hagenbuch
9: * All rights reserved.
10: *
11: * Redistribution and use in source and binary forms, with or without
12: * modification, are permitted provided that the following conditions
13: * are met:
14: *
15: * o Redistributions of source code must retain the above copyright
16: * notice, this list of conditions and the following disclaimer.
17: * o Redistributions in binary form must reproduce the above copyright
18: * notice, this list of conditions and the following disclaimer in the
19: * documentation and/or other materials provided with the distribution.
20: * o The names of the authors may not be used to endorse or promote
21: * products derived from this software without specific prior written
22: * permission.
23: *
24: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
25: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
26: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
27: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
28: * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
29: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
30: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
31: * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
32: * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
33: * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
34: * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35: *
36: * @category Horde
37: * @package Mail
38: * @author Jon Parise <jon@php.net>
39: * @author Chuck Hagenbuch <chuck@horde.org>
40: * @copyright 2010 Chuck Hagenbuch
41: * @license http://www.horde.org/licenses/bsd New BSD License
42: */
43:
44: /**
45: * SMTP implementation.
46: *
47: * @category Horde
48: * @package Mail
49: */
50: class Horde_Mail_Transport_Smtp extends Horde_Mail_Transport
51: {
52: /* Error: Failed to create a Net_SMTP object */
53: const ERROR_CREATE = 10000;
54:
55: /* Error: Failed to connect to SMTP server */
56: const ERROR_CONNECT = 10001;
57:
58: /* Error: SMTP authentication failure */
59: const ERROR_AUTH = 10002;
60:
61: /* Error: No From: address has been provided */
62: const ERROR_FROM = 10003;
63:
64: /* Error: Failed to set sender */
65: const ERROR_SENDER = 10004;
66:
67: /* Error: Failed to add recipient */
68: const ERROR_RECIPIENT = 10005;
69:
70: /* Error: Failed to send data */
71: const ERROR_DATA = 10006;
72:
73: /**
74: * The SMTP greeting.
75: *
76: * @var string
77: */
78: public $greeting = null;
79:
80: /**
81: * The SMTP queued response.
82: *
83: * @var string
84: */
85: public $queuedAs = null;
86:
87: /**
88: * SMTP connection object.
89: *
90: * @var Net_SMTP
91: */
92: protected $_smtp = null;
93:
94: /**
95: * The list of service extension parameters to pass to the Net_SMTP
96: * mailFrom() command.
97: *
98: * @var array
99: */
100: protected $_extparams = array();
101:
102: /**
103: * Constructor.
104: *
105: * @param array $params Additional parameters:
106: * - auth: (mixed) SMTP authentication.
107: * This value may be set to true, false or the name of a
108: * specific authentication method. If the value is set to true,
109: * the Net_SMTP package will attempt to use the best
110: * authentication method advertised by the remote SMTP server.
111: * DEFAULT: false.
112: * - debug: (boolean) Activate SMTP debug mode?
113: * DEFAULT: false
114: * - host: (string) The server to connect to.
115: * DEFAULT: localhost
116: * - localhost: (string) Hostname or domain that will be sent to the
117: * remote SMTP server in the HELO / EHLO message.
118: * DEFAULT: localhost
119: * - password: (string) The password to use for SMTP auth.
120: * DEFAULT: NONE
121: * - persist: (boolean) Should the SMTP connection persist?
122: * DEFAULT: false
123: * - pipelining: (boolean) Use SMTP command pipelining.
124: * Use SMTP command pipelining (specified in RFC 2920) if
125: * the SMTP server supports it. This speeds up delivery
126: * over high-latency connections.
127: * DEFAULT: false (use default value from Net_SMTP)
128: * - port: (integer) The port to connect to.
129: * DEFAULT: 25
130: * - timeout: (integer) The SMTP connection timeout.
131: * DEFAULT: NONE
132: * - username: (string) The username to use for SMTP auth.
133: * DEFAULT: NONE
134: */
135: public function __construct(array $params = array())
136: {
137: $this->_params = array_merge(array(
138: 'auth' => false,
139: 'debug' => false,
140: 'host' => 'localhost',
141: 'localhost' => 'localhost',
142: 'password' => '',
143: 'persist' => false,
144: 'pipelining' => false,
145: 'port' => 25,
146: 'timeout' => null,
147: 'username' => ''
148: ), $params);
149:
150: /* Destructor implementation to ensure that we disconnect from any
151: * potentially-alive persistent SMTP connections. */
152: register_shutdown_function(array($this, 'disconnect'));
153:
154: /* SMTP requires CRLF line endings. */
155: $this->sep = "\r\n";
156: }
157:
158: /**
159: * Send a message.
160: *
161: * @param mixed $recipients Either a comma-seperated list of recipients
162: * (RFC822 compliant), or an array of
163: * recipients, each RFC822 valid. This may
164: * contain recipients not specified in the
165: * headers, for Bcc:, resending messages, etc.
166: * @param array $headers The headers to send with the mail, in an
167: * associative array, where the array key is the
168: * header name (ie, 'Subject'), and the array
169: * value is the header value (ie, 'test'). The
170: * header produced from those values would be
171: * 'Subject: test'.
172: * If the '_raw' key exists, the value of this
173: * key will be used as the exact text for
174: * sending the message.
175: * @param mixed $body The full text of the message body, including
176: * any Mime parts, etc. Either a string or a
177: * stream resource.
178: *
179: * @throws Horde_Mail_Exception
180: */
181: public function send($recipients, array $headers, $body)
182: {
183: /* If we don't already have an SMTP object, create one. */
184: $this->getSMTPObject();
185:
186: $headers = $this->_sanitizeHeaders($headers);
187:
188: try {
189: list($from, $textHeaders) = $this->prepareHeaders($headers);
190: } catch (Horde_Mail_Exception $e) {
191: $this->_smtp->rset();
192: throw $e;
193: }
194:
195: /* Since few MTAs are going to allow this header to be forged unless
196: * it's in the MAIL FROM: exchange, we'll use Return-Path instead of
197: * From: if it's set. */
198: foreach (array_keys($headers) as $hdr) {
199: if (strcasecmp($hdr, 'Return-Path') === 0) {
200: $from = $headers[$hdr];
201: break;
202: }
203: }
204:
205: if (!strlen($from)) {
206: $this->_smtp->rset();
207: throw new Horde_Mail_Exception('No From: address has been provided', self::ERROR_FROM);
208: }
209:
210: $params = '';
211: foreach ($this->_extparams as $key => $val) {
212: $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val);
213: }
214:
215: $res = $this->_smtp->mailFrom($from, ltrim($params));
216: if ($res instanceof PEAR_Error) {
217: $this->_error("Failed to set sender: $from", $res, self::ERROR_SENDER);
218: }
219:
220: try {
221: $recipients = $this->parseRecipients($recipients);
222: } catch (Horde_Mail_Exception $e) {
223: $this->_smtp->rset();
224: throw $e;
225: }
226:
227: foreach ($recipients as $recipient) {
228: $res = $this->_smtp->rcptTo($recipient);
229: if ($res instanceof PEAR_Error) {
230: $this->_error("Failed to add recipient: $recipient", $res, self::ERROR_RECIPIENT);
231: }
232: }
233:
234: /* Send the message's headers and the body as SMTP data. Net_SMTP does
235: * the necessary EOL conversions. */
236: $res = $this->_smtp->data($body, $textHeaders);
237: list(,$args) = $this->_smtp->getResponse();
238:
239: if (preg_match("/Ok: queued as (.*)/", $args, $queued)) {
240: $this->queuedAs = $queued[1];
241: }
242:
243: /* We need the greeting; from it we can extract the authorative name
244: * of the mail server we've really connected to. Ideal if we're
245: * connecting to a round-robin of relay servers and need to track
246: * which exact one took the email */
247: $this->greeting = $this->_smtp->getGreeting();
248:
249: if ($res instanceof PEAR_Error) {
250: $this->_error('Failed to send data', $res, self::ERROR_DATA);
251: }
252:
253: /* If persistent connections are disabled, destroy our SMTP object. */
254: if ($this->_params['persist']) {
255: $this->disconnect();
256: }
257: }
258:
259: /**
260: * Connect to the SMTP server by instantiating a Net_SMTP object.
261: *
262: * @return Net_SMTP The SMTP object.
263: * @throws Horde_Mail_Exception
264: */
265: public function getSMTPObject()
266: {
267: if ($this->_smtp) {
268: return $this->_smtp;
269: }
270:
271: $this->_smtp = new Net_SMTP(
272: $this->_params['host'],
273: $this->_params['port'],
274: $this->_params['localhost']
275: );
276:
277: /* If we still don't have an SMTP object at this point, fail. */
278: if (!($this->_smtp instanceof Net_SMTP)) {
279: throw new Horde_Mail_Exception('Failed to create a Net_SMTP object', self::ERROR_CREATE);
280: }
281:
282: /* Configure the SMTP connection. */
283: if ($this->_params['debug']) {
284: $this->_smtp->setDebug(true);
285: }
286:
287: /* Attempt to connect to the configured SMTP server. */
288: $res = $this->_smtp->connect($this->_params['timeout']);
289: if ($res instanceof PEAR_Error) {
290: $this->_error('Failed to connect to ' . $this->_params['host'] . ':' . $this->_params['port'], $res, self::ERROR_CONNECT);
291: }
292:
293: /* Attempt to authenticate if authentication has been enabled. */
294: if ($this->_params['auth']) {
295: $method = is_string($this->_params['auth'])
296: ? $this->_params['auth']
297: : '';
298:
299: $res = $this->_smtp->auth($this->_params['username'], $this->_params['password'], $method);
300: if ($res instanceof PEAR_Error) {
301: $this->_error("$method authentication failure", $res, self::ERROR_AUTH);
302: }
303: }
304:
305: return $this->_smtp;
306: }
307:
308: /**
309: * Add parameter associated with a SMTP service extension.
310: *
311: * @param string $keyword Extension keyword.
312: * @param string $value Any value the keyword needs.
313: */
314: public function addServiceExtensionParameter($keyword, $value = null)
315: {
316: $this->_extparams[$keyword] = $value;
317: }
318:
319: /**
320: * Disconnect and destroy the current SMTP connection.
321: *
322: * @return boolean True if the SMTP connection no longer exists.
323: */
324: public function disconnect()
325: {
326: /* If we have an SMTP object, disconnect and destroy it. */
327: if (is_object($this->_smtp) && $this->_smtp->disconnect()) {
328: $this->_smtp = null;
329: }
330:
331: /* We are disconnected if we no longer have an SMTP object. */
332: return ($this->_smtp === null);
333: }
334:
335: /**
336: * Build a standardized string describing the current SMTP error.
337: *
338: * @param string $text Custom string describing the error context.
339: * @param PEAR_Error $error PEAR_Error object.
340: * @param integer $e_code Error code.
341: *
342: * @throws Horde_Mail_Exception
343: */
344: protected function _error($text, $error, $e_code)
345: {
346: /* Split the SMTP response into a code and a response string. */
347: list($code, $response) = $this->_smtp->getResponse();
348:
349: /* Abort current SMTP transaction. */
350: $this->_smtp->rset();
351:
352: /* Build our standardized error string. */
353: throw new Horde_Mail_Exception($text . ' [SMTP: ' . $error->getMessage() . " (code: $code, response: $response)]", $e_code);
354: }
355: }
356: