1: <?PHP
2: /**
3: * SMTP MX implementation.
4: * Requires the Net_SMTP class.
5: *
6: * LICENSE:
7: *
8: * Copyright (c) 2010, Gerd Schaufelberger
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 gERD Schaufelberger <gerd@php-tools.net>
39: * @copyright 2010 gERD Schaufelberger
40: * @license http://www.horde.org/licenses/bsd New BSD License
41: */
42:
43: /**
44: * SMTP MX implementation.
45: *
46: * @author gERD Schaufelberger <gerd@php-tools.net>
47: * @category Horde
48: * @package Mail
49: */
50: class Horde_Mail_Transport_Smtpmx extends Horde_Mail_Transport
51: {
52: /**
53: * SMTP connection object.
54: *
55: * @var Net_SMTP
56: */
57: protected $_smtp = null;
58:
59: /**
60: * Net_DNS2_Resolver object.
61: *
62: * @var Net_DNS2_Resolver
63: */
64: protected $_resolver;
65:
66: /**
67: * Internal error codes.
68: * Translate internal error identifier to human readable messages.
69: *
70: * @var array
71: */
72: protected $_errorCode = array(
73: 'not_connected' => array(
74: 'code' => 1,
75: 'msg' => 'Could not connect to any mail server ({HOST}) at port {PORT} to send mail to {RCPT}.'
76: ),
77: 'failed_vrfy_rcpt' => array(
78: 'code' => 2,
79: 'msg' => 'Recipient "{RCPT}" could not be veryfied.'
80: ),
81: 'failed_set_from' => array(
82: 'code' => 3,
83: 'msg' => 'Failed to set sender: {FROM}.'
84: ),
85: 'failed_set_rcpt' => array(
86: 'code' => 4,
87: 'msg' => 'Failed to set recipient: {RCPT}.'
88: ),
89: 'failed_send_data' => array(
90: 'code' => 5,
91: 'msg' => 'Failed to send mail to: {RCPT}.'
92: ),
93: 'no_from' => array(
94: 'code' => 5,
95: 'msg' => 'No from address has be provided.'
96: ),
97: 'send_data' => array(
98: 'code' => 7,
99: 'msg' => 'Failed to create Net_SMTP object.'
100: ),
101: 'no_mx' => array(
102: 'code' => 8,
103: 'msg' => 'No MX-record for {RCPT} found.'
104: ),
105: 'no_resolver' => array(
106: 'code' => 9,
107: 'msg' => 'Could not start resolver! Install PEAR:Net_DNS2 or switch off "netdns"'
108: ),
109: 'failed_rset' => array(
110: 'code' => 10,
111: 'msg' => 'RSET command failed, SMTP-connection corrupt.'
112: )
113: );
114:
115: /**
116: * Constructor.
117: *
118: * @param array $params Additional options:
119: * - debug: (boolean) Activate SMTP debug mode?
120: * DEFAULT: false
121: * - mailname: (string) The name of the local mail system (a valid
122: * hostname which matches the reverse lookup)
123: * DEFAULT: Auto-determined
124: * - netdns: (boolean) Use PEAR:Net_DNS2 (true) or the PHP builtin
125: * getmxrr().
126: * DEFAULT: true
127: * - port: (integer) Port.
128: * DEFAULT: Auto-determined
129: * - test: (boolean) Activate test mode?
130: * DEFAULT: false
131: * - timeout: (integer) The SMTP connection timeout (in seconds).
132: * DEFAULT: 10
133: * - verp: (boolean) Whether to use VERP.
134: * If not a boolean, the string value will be used as the VERP
135: * separators.
136: * DEFAULT: false
137: * - vrfy: (boolean) Whether to use VRFY.
138: * DEFAULT: false
139: */
140: public function __construct(array $params = array())
141: {
142: /* Try to find a valid mailname. */
143: if (!isset($params['mailname']) && function_exists('posix_uname')) {
144: $uname = posix_uname();
145: $params['mailname'] = $uname['nodename'];
146: }
147:
148: if (!isset($params['port'])) {
149: $params['port'] = getservbyname('smtp', 'tcp');
150: }
151:
152: $this->_params = array_merge(array(
153: 'debug' => false,
154: 'mailname' => 'localhost',
155: 'netdns' => true,
156: 'port' => 25,
157: 'test' => false,
158: 'timeout' => 10,
159: 'verp' => false,
160: 'vrfy' => false
161: ), $params);
162:
163: /* SMTP requires CRLF line endings. */
164: $this->sep = "\r\n";
165: }
166:
167: /**
168: * Destructor implementation to ensure that we disconnect from any
169: * potentially-alive persistent SMTP connections.
170: */
171: public function __destruct()
172: {
173: if (is_object($this->_smtp)) {
174: $this->_smtp->disconnect();
175: $this->_smtp = null;
176: }
177: }
178:
179: /**
180: * Send a message.
181: *
182: * @param mixed $recipients Either a comma-seperated list of recipients
183: * (RFC822 compliant), or an array of
184: * recipients, each RFC822 valid. This may
185: * contain recipients not specified in the
186: * headers, for Bcc:, resending messages, etc.
187: * @param array $headers The headers to send with the mail, in an
188: * associative array, where the array key is the
189: * header name (ie, 'Subject'), and the array
190: * value is the header value (ie, 'test'). The
191: * header produced from those values would be
192: * 'Subject: test'.
193: * If the '_raw' key exists, the value of this
194: * key will be used as the exact text for
195: * sending the message.
196: * @param mixed $body The full text of the message body, including
197: * any Mime parts, etc. Either a string or a
198: * stream resource.
199: *
200: * @throws Horde_Mail_Exception
201: */
202: public function send($recipients, array $headers, $body)
203: {
204: $headers = $this->_sanitizeHeaders($headers);
205:
206: // Prepare headers
207: list($from, $textHeaders) = $this->prepareHeaders($headers);
208:
209: // Use 'Return-Path' if possible
210: foreach (array_keys($headers) as $hdr) {
211: if (strcasecmp($hdr, 'Return-Path') === 0) {
212: $from = $headers['Return-Path'];
213: break;
214: }
215: }
216:
217: if (!strlen($from)) {
218: $this->_error('no_from');
219: }
220:
221: // Prepare recipients
222: foreach ($this->parseRecipients($recipients) as $rcpt) {
223: list($user, $host) = explode('@', $rcpt);
224:
225: $mx = $this->_getMx($host);
226: if (!$mx) {
227: $this->_error('no_mx', array('rcpt' => $rcpt));
228: }
229:
230: $connected = false;
231: foreach ($mx as $mserver => $mpriority) {
232: $this->_smtp = new Net_SMTP($mserver, $this->_params['port'], $this->_params['mailname']);
233:
234: // configure the SMTP connection.
235: if ($this->_params['debug']) {
236: $this->_smtp->setDebug(true);
237: }
238:
239: // attempt to connect to the configured SMTP server.
240: $res = $this->_smtp->connect($this->_params['timeout']);
241: if ($res instanceof PEAR_Error) {
242: $this->_smtp = null;
243: continue;
244: }
245:
246: // connection established
247: if ($res) {
248: $connected = true;
249: break;
250: }
251: }
252:
253: if (!$connected) {
254: $this->_error('not_connected', array(
255: 'host' => implode(', ', array_keys($mx)),
256: 'port' => $this->_params['port'],
257: 'rcpt' => $rcpt
258: ));
259: }
260:
261: // Verify recipient
262: if ($this->_params['vrfy']) {
263: $res = $this->_smtp->vrfy($rcpt);
264: if ($res instanceof PEAR_Error) {
265: $this->_error('failed_vrfy_rcpt', array('rcpt' => $rcpt));
266: }
267: }
268:
269: // mail from:
270: $args['verp'] = $this->_params['verp'];
271: $res = $this->_smtp->mailFrom($from, $args);
272: if ($res instanceof PEAR_Error) {
273: $this->_error('failed_set_from', array('from' => $from));
274: }
275:
276: // rcpt to:
277: $res = $this->_smtp->rcptTo($rcpt);
278: if ($res instanceof PEAR_Error) {
279: $this->_error('failed_set_rcpt', array('rcpt' => $rcpt));
280: }
281:
282: // Don't send anything in test mode
283: if ($this->_params['test']) {
284: $res = $this->_smtp->rset();
285: if ($res instanceof PEAR_Error) {
286: $this->_error('failed_rset');
287: }
288:
289: $this->_smtp->disconnect();
290: $this->_smtp = null;
291: return;
292: }
293:
294: // Send data. Net_SMTP does necessary EOL conversions.
295: $res = $this->_smtp->data($body, $textHeaders);
296: if ($res instanceof PEAR_Error) {
297: $this->_error('failed_send_data', array('rcpt' => $rcpt));
298: }
299:
300: $this->_smtp->disconnect();
301: $this->_smtp = null;
302: }
303: }
304:
305: /**
306: * Recieve MX records for a host.
307: *
308: * @param string $host Mail host.
309: *
310: * @return mixed Sorted MX list or false on error.
311: */
312: protected function _getMx($host)
313: {
314: $mx = array();
315:
316: if ($this->params['netdns']) {
317: $this->_loadNetDns();
318:
319: try {
320: $response = $this->_resolver->query($host, 'MX');
321: if (!$response) {
322: return false;
323: }
324: } catch (Exception $e) {
325: throw new Horde_Mail_Exception($e);
326: }
327:
328: foreach ($response->answer as $rr) {
329: if ($rr->type == 'MX') {
330: $mx[$rr->exchange] = $rr->preference;
331: }
332: }
333: } else {
334: $mxHost = $mxWeight = array();
335:
336: if (!getmxrr($host, $mxHost, $mxWeight)) {
337: return false;
338: }
339:
340: for ($i = 0; $i < count($mxHost); ++$i) {
341: $mx[$mxHost[$i]] = $mxWeight[$i];
342: }
343: }
344:
345: asort($mx);
346:
347: return $mx;
348: }
349:
350: /**
351: * Initialize Net_DNS2_Resolver.
352: */
353: protected function _loadNetDns()
354: {
355: if (!$this->_resolver) {
356: if (!class_exists('Net_DNS2_Resolver')) {
357: $this->_error('no_resolver');
358: }
359: $this->_resolver = new Net_DNS2_Resolver();
360: }
361: }
362:
363: /**
364: * Format error message.
365: *
366: * @param string $id Maps error ids to codes and message.
367: * @param array $info Optional information in associative array.
368: *
369: * @throws Horde_Mail_Exception
370: */
371: protected function _error($id, $info = array())
372: {
373: $msg = $this->_errorCode[$id]['msg'];
374:
375: // include info to messages
376: if (!empty($info)) {
377: $replace = $search = array();
378:
379: foreach ($info as $key => $value) {
380: $search[] = '{' . strtoupper($key) . '}';
381: $replace[] = $value;
382: }
383:
384: $msg = str_replace($search, $replace, $msg);
385: }
386:
387: throw new Horde_Mail_Exception($msg, $this->_errorCode[$id]['code']);
388: }
389: }
390: