1112 lines
33 KiB
PHP
1112 lines
33 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2013-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (LGPL). If you
|
|
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
|
|
*
|
|
* @category Horde
|
|
* @copyright 2013-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Smtp
|
|
*/
|
|
|
|
/**
|
|
* An interface to an SMTP server (RFC 5321).
|
|
*
|
|
* Implements the following SMTP-related RFCs:
|
|
* <pre>
|
|
* - RFC 1870/STD 10: Message Size Declaration
|
|
* - RFC 1985: SMTP Service Extension for Remote Message Queue Starting
|
|
* - RFC 2034: Enhanced-Status-Codes
|
|
* - RFC 2195: CRAM-MD5 (SASL Authentication)
|
|
* - RFC 2595/4616: TLS & PLAIN (SASL Authentication)
|
|
* - RFC 2831: DIGEST-MD5 authentication mechanism (obsoleted by RFC 6331)
|
|
* - RFC 2920/STD 60: Pipelining
|
|
* - RFC 3030: SMTP Service Extensions for Transmission of Large and Binary
|
|
* MIME Messages
|
|
* - RFC 3207: Secure SMTP over TLS
|
|
* - RFC 3463: Enhanced Mail System Status Codes
|
|
* - RFC 4422: SASL Authentication (for DIGEST-MD5)
|
|
* - RFC 4954: Authentication
|
|
* - RFC 5321: Simple Mail Transfer Protocol
|
|
* - RFC 6152/STD 71: 8bit-MIMEtransport
|
|
* - RFC 6409/STD 72: Message Submission for Mail
|
|
* - RFC 6531: Internationalized Email
|
|
*
|
|
* - XOAUTH2: https://developers.google.com/gmail/xoauth2_protocol
|
|
* </pre>
|
|
*
|
|
* TODO:
|
|
* <pre>
|
|
* - RFC 1845: CHECKPOINT
|
|
* - RFC 2852: DELIVERYBY
|
|
* - RFC 3461: DSN
|
|
* - RFC 3865: NO-SOLICITING
|
|
* - RFC 3885: MTRK
|
|
* - RFC 4141: CONPERM/CONNEG
|
|
* - RFC 4405: SUBMITTER
|
|
* - RFC 4468: BURL
|
|
* - RFC 4865: FUTURERELEASE
|
|
* - RFC 6710: MT-PRIORITY
|
|
* - RFC 7293: RRVS
|
|
* </pre>
|
|
*
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @category Horde
|
|
* @copyright 2013-2017 Horde LLC
|
|
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
|
|
* @package Smtp
|
|
*
|
|
* @property-read boolean $data_8bit Does server support sending 8-bit MIME
|
|
* data?
|
|
* @property-read boolean $data_binary Does server support sending binary
|
|
* MIME data? (@since 1.7.0)
|
|
* @property-read boolean $data_intl Does server support sending
|
|
* internationalized (UTF-8) header data?
|
|
* (@since 1.6.0)
|
|
* @property-read integer $size The maximum message size supported (in
|
|
* bytes) or null if this cannot be determined.
|
|
*/
|
|
class Horde_Smtp implements Serializable
|
|
{
|
|
const CHUNK_DEFAULT = 1048576;
|
|
|
|
/**
|
|
* Connection to the SMTP server.
|
|
*
|
|
* @var Horde_Smtp_Connection
|
|
*/
|
|
protected $_connection;
|
|
|
|
/**
|
|
* The debug object.
|
|
*
|
|
* @var Horde_Smtp_Debug
|
|
*/
|
|
protected $_debug;
|
|
|
|
/**
|
|
* The hello command to use for extended SMTP support.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_ehlo = 'EHLO';
|
|
|
|
/**
|
|
* The list of ESMTP extensions.
|
|
* If this value is null, we have not connected to server yet.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_extensions = null;
|
|
|
|
/**
|
|
* Hash containing connection parameters.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_params = array();
|
|
|
|
/**
|
|
* List of required ESMTP extensions.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_requiredExts = array();
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param array $params Configuration parameters:
|
|
* - chunk_size: (integer) If CHUNKING is supported on the server, the
|
|
* chunk size (in octets) to send. 0 will disable chunking.
|
|
* @since 1.7.0
|
|
* - context: (array) Any context parameters passed to
|
|
* stream_create_context(). @since 1.9.0
|
|
* - debug: (string) If set, will output debug information to the stream
|
|
* provided. The value can be any PHP supported wrapper that
|
|
* can be opened via fopen().
|
|
* DEFAULT: No debug output
|
|
* - host: (string) The SMTP server.
|
|
* DEFAULT: localhost
|
|
* - localhost: (string) The hostname of the localhost. (since 1.9.0)
|
|
* DEFAULT: Auto-determined.
|
|
* - password: (mixed) The SMTP password or a Horde_Smtp_Password object
|
|
* (since 1.1.0).
|
|
* DEFAULT: NONE
|
|
* - port: (string) The SMTP port.
|
|
* DEFAULT: 587 (See RFC 6409/STD 72)
|
|
* - secure: (string) Use SSL or TLS to connect.
|
|
* DEFAULT: true (use 'tls' option, if available)
|
|
* - false (No encryption)
|
|
* - 'ssl' (Auto-detect SSL version)
|
|
* - 'sslv2' (Force SSL version 2)
|
|
* - 'sslv3' (Force SSL version 3)
|
|
* - 'tls' (TLS; started via protocol-level negotation over
|
|
* unencrypted channel; RECOMMENDED way of initiating secure
|
|
* connection)
|
|
* - 'tlsv1' (TLS direct version 1.x connection to server) [@since
|
|
* 1.3.0]
|
|
* - true (Use TLS, if available) [@since 1.2.0]
|
|
* - timeout: (integer) Connection timeout, in seconds.
|
|
* DEFAULT: 30 seconds
|
|
* - username: (string) The SMTP username.
|
|
* DEFAULT: NONE
|
|
* - xoauth2_token: (string) If set, will authenticate via the XOAUTH2
|
|
* mechanism (if available) with this token. Either a
|
|
* string or a Horde_Smtp_Password object (since 1.1.0).
|
|
*/
|
|
public function __construct(array $params = array())
|
|
{
|
|
// Default values.
|
|
$params = array_merge(array(
|
|
'chunk_size' => self::CHUNK_DEFAULT,
|
|
'context' => array(),
|
|
'host' => 'localhost',
|
|
'port' => 587,
|
|
'secure' => true,
|
|
'timeout' => 30
|
|
), $params);
|
|
|
|
foreach ($params as $key => $val) {
|
|
$this->setParam($key, $val);
|
|
}
|
|
|
|
$this->_initOb();
|
|
}
|
|
|
|
/**
|
|
* Get encryption key.
|
|
*
|
|
* @deprecated
|
|
*
|
|
* @return string The encryption key.
|
|
*/
|
|
protected function _getEncryptKey()
|
|
{
|
|
if (is_callable($ekey = $this->getParam('password_encrypt'))) {
|
|
return call_user_func($ekey);
|
|
}
|
|
|
|
throw new InvalidArgumentException('password_encrypt parameter is not a valid callback.');
|
|
}
|
|
|
|
/**
|
|
* Do initialization tasks.
|
|
*/
|
|
protected function _initOb()
|
|
{
|
|
register_shutdown_function(array($this, 'shutdown'));
|
|
$this->_debug = ($debug = $this->getParam('debug'))
|
|
? new Horde_Smtp_Debug($debug)
|
|
: new Horde_Support_Stub();
|
|
}
|
|
|
|
/**
|
|
* Shutdown actions.
|
|
*/
|
|
public function shutdown()
|
|
{
|
|
$this->logout();
|
|
}
|
|
|
|
/**
|
|
* This object can not be cloned.
|
|
*/
|
|
public function __clone()
|
|
{
|
|
throw new LogicException('Object cannot be cloned.');
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public function serialize()
|
|
{
|
|
return serialize($this->_params);
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public function unserialize($data)
|
|
{
|
|
$this->_params = @unserialize($data);
|
|
$this->_initOb();
|
|
}
|
|
|
|
/**
|
|
*/
|
|
public function __get($name)
|
|
{
|
|
switch ($name) {
|
|
case 'data_8bit':
|
|
// RFC 6152
|
|
return $this->queryExtension('8BITMIME');
|
|
|
|
case 'data_binary':
|
|
// RFC 3030
|
|
return $this->queryExtension('BINARYMIME');
|
|
|
|
case 'data_intl':
|
|
// RFC 6531
|
|
return $this->queryExtension('SMTPUTF8');
|
|
|
|
case 'size':
|
|
// RFC 1870
|
|
return $this->queryExtension('SIZE') ?: null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a value from the internal params array.
|
|
*
|
|
* @param string $key The param key.
|
|
*
|
|
* @return mixed The param value, or null if not found.
|
|
*/
|
|
public function getParam($key)
|
|
{
|
|
/* Passwords may be stored encrypted. */
|
|
switch ($key) {
|
|
case 'password':
|
|
case 'xoauth2_token':
|
|
if (isset($this->_params[$key]) &&
|
|
($this->_params[$key] instanceof Horde_Smtp_Password)) {
|
|
return $this->_params[$key]->getPassword();
|
|
}
|
|
|
|
// DEPRECATED
|
|
if (($key == 'password') &&
|
|
!empty($this->_params['_passencrypt'])) {
|
|
try {
|
|
$secret = new Horde_Secret();
|
|
return $secret->read($this->_getEncryptKey(), $this->_params['password']);
|
|
} catch (Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return isset($this->_params[$key])
|
|
? $this->_params[$key]
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* Sets a configuration parameter value.
|
|
*
|
|
* @param string $key The param key.
|
|
* @param mixed $val The param value.
|
|
*/
|
|
public function setParam($key, $val)
|
|
{
|
|
switch ($key) {
|
|
case 'password':
|
|
if ($val instanceof Horde_Smtp_Password) {
|
|
break;
|
|
}
|
|
|
|
// Encrypt password. DEPRECATED
|
|
try {
|
|
$encrypt_key = $this->_getEncryptKey();
|
|
if (strlen($encrypt_key)) {
|
|
$secret = new Horde_Secret();
|
|
$val = $secret->write($encrypt_key, $val);
|
|
$this->_params['_passencrypt'] = true;
|
|
}
|
|
} catch (Exception $e) {}
|
|
break;
|
|
}
|
|
|
|
$this->_params[$key] = $val;
|
|
}
|
|
|
|
/**
|
|
* Returns whether the SMTP server supports the given extension.
|
|
*
|
|
* @param string $ext The extension to query.
|
|
*
|
|
* @return mixed False if the server doesn't support the extension;
|
|
* otherwise, the extension value (returns true if the
|
|
* extension only supports existence).
|
|
*/
|
|
public function queryExtension($ext)
|
|
{
|
|
try {
|
|
$this->login();
|
|
} catch (Horde_Smtp_Exception $e) {
|
|
return false;
|
|
}
|
|
|
|
$ext = Horde_String::upper($ext);
|
|
|
|
return isset($this->_extensions[$ext])
|
|
? $this->_extensions[$ext]
|
|
: false;
|
|
}
|
|
|
|
/**
|
|
* Display if connection to the server has been secured via TLS or SSL.
|
|
*
|
|
* @return boolean True if the SMTP connection is secured.
|
|
*/
|
|
public function isSecureConnection()
|
|
{
|
|
return ($this->_connection && $this->_connection->secure);
|
|
}
|
|
|
|
/**
|
|
* Connect/login to the SMTP server.
|
|
*
|
|
* @throws Horde_Smtp_Exception
|
|
*/
|
|
public function login()
|
|
{
|
|
if (!is_null($this->_extensions)) {
|
|
return;
|
|
}
|
|
|
|
if (!$this->_connection) {
|
|
try {
|
|
$this->_connection = new Horde_Smtp_Connection(
|
|
$this->getParam('host'),
|
|
$this->getParam('port'),
|
|
$this->getParam('timeout'),
|
|
$this->getParam('secure'),
|
|
$this->getParam('context'),
|
|
array(
|
|
'debug' => $this->_debug
|
|
)
|
|
);
|
|
} catch (Horde\Socket\Client\Exception $e) {
|
|
$e2 = new Horde_Smtp_Exception(
|
|
Horde_Smtp_Translation::r("Error connecting to SMTP server."),
|
|
Horde_Smtp_Exception::SERVER_CONNECT
|
|
);
|
|
$e2->details = $e->details;
|
|
throw $e2;
|
|
}
|
|
|
|
$this->_debug->info(sprintf(
|
|
'Connection to: smtp://%s:%s',
|
|
$this->getParam('host'),
|
|
$this->getParam('port')
|
|
));
|
|
|
|
// Get initial line (RFC 5321 [3.1]).
|
|
$this->_getResponse(220, array(
|
|
'error' => 'logout'
|
|
));
|
|
}
|
|
|
|
$this->_hello();
|
|
|
|
if ($this->_startTls()) {
|
|
$this->_extensions = null;
|
|
$this->login();
|
|
return;
|
|
}
|
|
|
|
/* Check for required ESMTP extensions. */
|
|
foreach ($this->_requiredExts as $val) {
|
|
if (!$this->queryExtension($val)) {
|
|
throw new Horde_Smtp_Exception(
|
|
sprintf(
|
|
Horde_Smtp_Translation::r("Server does not support a necessary server extension: %s."),
|
|
$val
|
|
),
|
|
Horde_Smtp_Exception::LOGIN_MISSINGEXTENSION
|
|
);
|
|
}
|
|
}
|
|
|
|
/* If we reached this point and don't have a secure connection, then
|
|
* a secure connections is not available. */
|
|
if (!$this->isSecureConnection() &&
|
|
($this->getParam('secure') === true)) {
|
|
$this->setParam('secure', false);
|
|
}
|
|
|
|
if (!strlen($this->getParam('username')) ||
|
|
!($auth = $this->queryExtension('AUTH'))) {
|
|
return;
|
|
}
|
|
|
|
$auth = array_flip(array_map('trim', explode(' ', $auth)));
|
|
|
|
// XOAUTH2
|
|
if (isset($auth['XOAUTH2'])) {
|
|
unset($auth['XOAUTH2']);
|
|
if ($this->getParam('xoauth2_token')) {
|
|
$auth = array('XOAUTH2' => true) + $auth;
|
|
}
|
|
}
|
|
|
|
foreach (array_keys($auth) as $method) {
|
|
try {
|
|
$this->_auth($method);
|
|
return;
|
|
} catch (Horde_Smtp_Exception $e) {}
|
|
}
|
|
|
|
$this->logout();
|
|
throw new Horde_Smtp_Exception(
|
|
Horde_Smtp_Translation::r("Server denied authentication."),
|
|
Horde_Smtp_Exception::LOGIN_AUTHENTICATIONFAILED
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Logout from the SMTP server.
|
|
*/
|
|
public function logout()
|
|
{
|
|
if (!is_null($this->_extensions) && $this->_connection->connected) {
|
|
try {
|
|
// See RFC 5321 [4.1.1.10]
|
|
$this->_connection->write('QUIT');
|
|
$this->_getResponse(221);
|
|
} catch (Exception $e) {}
|
|
|
|
$this->_connection->close();
|
|
}
|
|
|
|
unset($this->_connection);
|
|
$this->_extensions = null;
|
|
}
|
|
|
|
/**
|
|
* Send a message.
|
|
*
|
|
* @param mixed $from The from address. Either a
|
|
* Horde_Mail_Rfc822_Address object or a string.
|
|
* @param mixed $to The to (recipient) addresses. Either a
|
|
* Horde_Mail_Rfc822_List object, a string, or an
|
|
* array of addresses.
|
|
* @param mixed $data The data to send. Either a stream or a string.
|
|
*
|
|
* @return array If no receipients were successful, a
|
|
* Horde_Smtp_Exception will be thrown. If at least one
|
|
* recipient was successful, an array with the following
|
|
* format is returned: (@since 1.5.0)
|
|
* - KEYS: Recipient addresses ($to addresses).
|
|
* - VALUES: Boolean true (if message was accepted for this recpieint)
|
|
* or a Horde_Smtp_Exception (if messages was not accepted).
|
|
*
|
|
* @throws Horde_Smtp_Exception
|
|
* @throws Horde_Smtp_Exception_Recipients
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
public function send($from, $to, $data, array $opts = array())
|
|
{
|
|
$this->login();
|
|
|
|
if (!($from instanceof Horde_Mail_Rfc822_Address)) {
|
|
$from = new Horde_Mail_Rfc822_Address($from);
|
|
}
|
|
|
|
/* Are EAI addresses present? */
|
|
$eai = $from->eai;
|
|
|
|
if (!($to instanceof Horde_Mail_Rfc822_List)) {
|
|
$to = new Horde_Mail_Rfc822_List($to);
|
|
}
|
|
|
|
if (!$eai) {
|
|
foreach ($to->raw_addresses as $val) {
|
|
if ($eai = $val->eai) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* RFC 6531: EAI */
|
|
if ($eai && !$this->data_intl) {
|
|
throw new InvalidArgumentException(
|
|
'Server does not support sending internationalized header data.'
|
|
);
|
|
}
|
|
|
|
$stream = fopen('php://temp', 'r+');
|
|
|
|
stream_filter_register('horde_smtp_data', 'Horde_Smtp_Filter_Data');
|
|
$filter_data = stream_filter_append(
|
|
$stream,
|
|
'horde_smtp_data',
|
|
STREAM_FILTER_WRITE
|
|
);
|
|
|
|
stream_filter_register('horde_smtp_body', 'Horde_Smtp_Filter_Body');
|
|
$filter_body_params = new stdClass;
|
|
$filter_body = stream_filter_append(
|
|
$stream,
|
|
'horde_smtp_body',
|
|
STREAM_FILTER_WRITE,
|
|
$filter_body_params
|
|
);
|
|
|
|
if (is_resource($data)) {
|
|
while (!feof($data)) {
|
|
fwrite($stream, fread($data, 8192));
|
|
}
|
|
} else {
|
|
fwrite($stream, $data);
|
|
}
|
|
|
|
stream_filter_remove($filter_body);
|
|
stream_filter_remove($filter_data);
|
|
|
|
$size = ftell($stream);
|
|
rewind($stream);
|
|
|
|
$chunking = $this->queryExtension('CHUNKING');
|
|
$chunk_force = false;
|
|
$chunk_size = $this->getParam('chunk_size');
|
|
|
|
/* Do body checks now, since if they fail it doesn't make sense to
|
|
* do anything else. */
|
|
$body = null;
|
|
if ($eai || ($filter_body_params->body === '8bit')) {
|
|
/* RFC 6152[3]. RFC 6531[1.2] also requires at least 8BITMIME to
|
|
* be available. */
|
|
$body = $this->data_8bit
|
|
? ' BODY=8BITMIME'
|
|
: false;
|
|
}
|
|
|
|
/* Fallback to binarymime if 8bitmime is not available. */
|
|
if (($body === false) || ($filter_body_params->body === 'binary')) {
|
|
// RFC 3030[3]
|
|
if (!$this->data_binary) {
|
|
switch ($filter_body_params->body) {
|
|
case 'binary':
|
|
throw new InvalidArgumentException(
|
|
'Server does not support binary message data.'
|
|
);
|
|
|
|
default:
|
|
throw new InvalidArgumentException(
|
|
'Server does not support 8-bit message data.'
|
|
);
|
|
}
|
|
}
|
|
|
|
$body = ' BODY=BINARYMIME';
|
|
|
|
/* BINARYMIME requires CHUNKING. */
|
|
$chunking = $chunk_force = true;
|
|
if (!$chunk_size) {
|
|
$chunk_size = self::CHUNK_DEFAULT;
|
|
}
|
|
}
|
|
|
|
if (is_null($body) && $this->_debug->active && $this->data_8bit) {
|
|
/* Only output extended 7bit command if debug is active (it is
|
|
* default and does not need to be explicitly declared). */
|
|
$body = ' BODY=7BIT';
|
|
}
|
|
|
|
$mailcmd = 'MAIL FROM:<' . ($eai ? $from->bare_address : $from->bare_address_idn) . '>';
|
|
|
|
// RFC 1870[6]
|
|
if ($this->queryExtension('SIZE')) {
|
|
$mailcmd .= ' SIZE=' . intval($size);
|
|
}
|
|
|
|
// RFC 6531[3.4]
|
|
if ($eai) {
|
|
$mailcmd .= ' SMTPUTF8';
|
|
}
|
|
|
|
if (!is_null($body)) {
|
|
$mailcmd .= $body;
|
|
}
|
|
|
|
$recipients = $eai
|
|
? $to->bare_addresses
|
|
: $to->bare_addresses_idn;
|
|
|
|
$recip_cmds = array();
|
|
foreach ($recipients as $val) {
|
|
$recip_cmds[$val] = 'RCPT TO:<' . $val . '>';
|
|
}
|
|
|
|
if ($this->queryExtension('PIPELINING')) {
|
|
$this->_connection->write(
|
|
array_merge(array($mailcmd), array_values($recip_cmds))
|
|
);
|
|
|
|
try {
|
|
$this->_getResponse(250);
|
|
$error = null;
|
|
} catch (Horde_Smtp_Exception $e) {
|
|
$error = $e;
|
|
}
|
|
|
|
foreach ($recip_cmds as $key => $val) {
|
|
try {
|
|
$this->_getResponse(array(250, 251), array(
|
|
'exception' => 'Horde_Smtp_Exception_Recipients'
|
|
));
|
|
} catch (Horde_Smtp_Exception_Recipients $e) {
|
|
if (is_null($error)) {
|
|
$error = $e;
|
|
}
|
|
|
|
if ($error instanceof Horde_Smtp_Exception_Recipients) {
|
|
$error->recipients[] = $key;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Can't pipeline DATA since we want to throw an exception if
|
|
* ANY of the recipients are bad. */
|
|
if (!is_null($error)) {
|
|
$this->resetCmd();
|
|
throw $error;
|
|
}
|
|
} else {
|
|
$this->_connection->write($mailcmd);
|
|
$this->_getResponse(250, array(
|
|
'error' => 'reset'
|
|
));
|
|
|
|
foreach ($recip_cmds as $key => $val) {
|
|
$this->_connection->write($val);
|
|
try {
|
|
$this->_getResponse(array(250, 251), array(
|
|
'error' => 'reset',
|
|
'exception' => 'Horde_Smtp_Exception_Recipients'
|
|
));
|
|
} catch (Horde_Smtp_Exception_Recipients $e) {
|
|
$e->recipients[] = $key;
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* CHUNKING support. RFC 3030[2] */
|
|
if ($chunking &&
|
|
$chunk_size &&
|
|
($chunk_force || ($size > $chunk_size))) {
|
|
while ($size) {
|
|
$c = min($chunk_size, $size);
|
|
$size -= $c;
|
|
|
|
$this->_connection->write(
|
|
'BDAT ' . $c . ($size ? '' : ' LAST')
|
|
);
|
|
$this->_connection->write($stream, $c);
|
|
if ($size) {
|
|
$this->_getResponse(250, array(
|
|
'error' => 'reset'
|
|
));
|
|
}
|
|
}
|
|
} else {
|
|
$this->_connection->write('DATA');
|
|
|
|
try {
|
|
$this->_getResponse(354, array(
|
|
'error' => 'reset'
|
|
));
|
|
} catch (Horde_Smtp_Exception $e) {
|
|
fclose($stream);
|
|
|
|
/* This is the place where a STARTTLS 530 error would occur.
|
|
* If so, explicitly use STARTTLS and try again. */
|
|
switch ($e->getSmtpCode()) {
|
|
case 530:
|
|
if (!$this->isSecureConnection()) {
|
|
$this->logout();
|
|
$this->setParam('secure', 'tls');
|
|
$this->send($from, $to, $data, $opts);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
|
|
$this->_connection->write($stream);
|
|
$this->_connection->write('.');
|
|
}
|
|
|
|
fclose($stream);
|
|
|
|
return $this->_processData($recipients);
|
|
}
|
|
|
|
/**
|
|
* Send a reset command.
|
|
*
|
|
* @throws Horde_Smtp_Exception
|
|
*/
|
|
public function resetCmd()
|
|
{
|
|
$this->login();
|
|
|
|
// See RFC 5321 [4.1.1.5].
|
|
// RSET only useful if we are already authenticated.
|
|
if (!is_null($this->_extensions)) {
|
|
$this->_connection->write('RSET');
|
|
$this->_getResponse(250);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a NOOP command.
|
|
*
|
|
* @throws Horde_Smtp_Exception
|
|
*/
|
|
public function noop()
|
|
{
|
|
$this->login();
|
|
|
|
// See RFC 5321 [4.1.1.9].
|
|
// NOOP only useful if we are already authenticated.
|
|
if (!is_null($this->_extensions)) {
|
|
$this->_connection->write('NOOP');
|
|
$this->_getResponse(250);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send request to process the remote queue.
|
|
*
|
|
* @param string $host The specific host to request queue processing for.
|
|
*
|
|
* @throws Horde_Smtp_Exception
|
|
*/
|
|
public function processQueue($host = null)
|
|
{
|
|
$this->login();
|
|
|
|
if (!$this->queryExtension('ETRN')) {
|
|
return;
|
|
}
|
|
|
|
if (is_null($host)) {
|
|
$host = $this->_getHostname();
|
|
if ($host === false) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->_connection->write('ETRN ' . $host);
|
|
$this->_getResponse(array(250, 251, 252, 253));
|
|
}
|
|
|
|
/* Internal methods. */
|
|
|
|
/**
|
|
* Send "Hello" command to the server.
|
|
*
|
|
* @throws Horde_Smtp_Exception
|
|
*/
|
|
protected function _hello()
|
|
{
|
|
$ehlo = $host = $this->_getHostname();
|
|
if ($host === false) {
|
|
$ehlo = $_SERVER['SERVER_ADDR'];
|
|
$host = 'localhost';
|
|
}
|
|
|
|
$this->_connection->write($this->_ehlo . ' ' . $ehlo);
|
|
try {
|
|
$resp = $this->_getResponse(250);
|
|
|
|
foreach ($resp as $val) {
|
|
$tmp = explode(' ', $val, 2);
|
|
$this->_extensions[$tmp[0]] = empty($tmp[1])
|
|
? true
|
|
: $tmp[1];
|
|
}
|
|
} catch (Horde_Smtp_Exception $e) {
|
|
switch ($e->getSmtpCode()) {
|
|
case 502:
|
|
// Old server - doesn't support EHLO
|
|
$this->_connection->write('HELO ' . $host);
|
|
try {
|
|
$this->_getResponse(250);
|
|
} catch (Horde_Smtp_Exception $e2) {
|
|
$this->logout();
|
|
throw $e;
|
|
}
|
|
$this->_extensions = array();
|
|
break;
|
|
|
|
default:
|
|
$this->logout();
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the TLS connection to the server, if necessary. See RFC 3207.
|
|
*
|
|
* @return boolean True if TLS was started.
|
|
*
|
|
* @throws Horde_Smtp_Exception
|
|
*/
|
|
protected function _startTls()
|
|
{
|
|
$secure = $this->getParam('secure');
|
|
|
|
if ($this->isSecureConnection() ||
|
|
(($secure !== true) && ($secure !== 'tls'))) {
|
|
return false;
|
|
}
|
|
|
|
if (!$this->queryExtension('STARTTLS')) {
|
|
if ($secure === true) {
|
|
return false;
|
|
}
|
|
|
|
throw new Horde_Smtp_Exception(
|
|
Horde_Smtp_Translation::r("Server does not support TLS connections."),
|
|
Horde_Smtp_Exception::LOGIN_TLSFAILURE
|
|
);
|
|
}
|
|
|
|
$this->_connection->write('STARTTLS');
|
|
$this->_getResponse(220, array(
|
|
'error' => 'logout'
|
|
));
|
|
|
|
if (!$this->_connection->startTls()) {
|
|
$this->logout();
|
|
$e = new Horde_Smtp_Exception();
|
|
$e->setSmtpCode(454);
|
|
throw $e;
|
|
}
|
|
|
|
$this->_debug->info('Successfully completed TLS negotiation.');
|
|
|
|
$this->setParam('secure', 'tls');
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Authenticate user to server for a given method.
|
|
*
|
|
* @param string $method Authentication method.
|
|
*
|
|
* @throws Horde_Smtp_Exception
|
|
*/
|
|
protected function _auth($method)
|
|
{
|
|
$user = $this->getParam('username');
|
|
$pass = $this->getParam('password');
|
|
|
|
$debug = sprintf("[AUTH Command - method: %s; username: %s]\n", $method, $user);
|
|
|
|
switch ($method) {
|
|
case 'CRAM-MD5':
|
|
case 'CRAM-SHA1':
|
|
case 'CRAM-SHA256':
|
|
// RFC 2195: CRAM-MD5
|
|
// CRAM-SHA1 & CRAM-SHA256 supported by Courier SASL library
|
|
$this->_connection->write('AUTH ' . $method);
|
|
$resp = $this->_getResponse(334);
|
|
|
|
$this->_debug->active = false;
|
|
$this->_connection->write(
|
|
base64_encode($user . ' ' . hash_hmac(Horde_String::lower(substr($method, 5)), base64_decode(reset($resp)), $pass, false))
|
|
);
|
|
$this->_debug->active = true;
|
|
|
|
$this->_debug->raw($debug);
|
|
break;
|
|
|
|
case 'DIGEST-MD5':
|
|
// RFC 2831/4422; obsoleted by RFC 6331
|
|
// Since this is obsolete, will only attempt if
|
|
// Horde_Imap_Client is also present on the system.
|
|
if (!class_exists('Horde_Imap_Client_Auth_DigestMD5')) {
|
|
throw new Horde_Smtp_Exception('DIGEST-MD5 not supported');
|
|
}
|
|
|
|
$this->_connection->write('AUTH ' . $method);
|
|
$resp = $this->_getResponse(334);
|
|
|
|
$this->_debug->active = false;
|
|
$this->_connection->write(
|
|
base64_encode(new Horde_Imap_Client_Auth_DigestMD5(
|
|
$user,
|
|
$pass,
|
|
base64_decode(reset($resp)),
|
|
$this->getParam('hostspec'),
|
|
'smtp'
|
|
))
|
|
);
|
|
$this->_debug->active = true;
|
|
$this->_debug->raw($debug);
|
|
|
|
$this->_getResponse(334);
|
|
$this->_connection->write('');
|
|
break;
|
|
|
|
case 'LOGIN':
|
|
$this->_connection->write('AUTH ' . $method);
|
|
$this->_getResponse(334);
|
|
$this->_connection->write(base64_encode($user));
|
|
$this->_getResponse(334);
|
|
$this->_debug->active = false;
|
|
$this->_connection->write(base64_encode($pass));
|
|
$this->_debug->active = true;
|
|
$this->_debug->raw($debug);
|
|
break;
|
|
|
|
case 'PLAIN':
|
|
// RFC 2595/4616 - PLAIN SASL mechanism
|
|
$auth = base64_encode(implode("\0", array(
|
|
$user,
|
|
$user,
|
|
$pass
|
|
)));
|
|
$this->_debug->active = false;
|
|
$this->_connection->write('AUTH ' . $method . ' ' . $auth);
|
|
$this->_debug->active = true;
|
|
$this->_debug->raw($debug);
|
|
break;
|
|
|
|
case 'XOAUTH2':
|
|
// Google XOAUTH2
|
|
$this->_debug->active = false;
|
|
$this->_connection->write(
|
|
'AUTH ' . $method . ' ' . $this->getParam('xoauth2_token')
|
|
);
|
|
$this->_debug->active = true;
|
|
$this->_debug->raw($debug);
|
|
|
|
try {
|
|
$this->_getResponse(235);
|
|
return;
|
|
} catch (Horde_Smtp_Exception $e) {
|
|
switch ($e->getSmtpCode()) {
|
|
case 334:
|
|
$this->_connection->write('');
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw new Horde_Smtp_Exception(sprintf('Authentication method %s not supported', $method));
|
|
}
|
|
|
|
$this->_getResponse(235);
|
|
}
|
|
|
|
/**
|
|
* Gets a line from the incoming stream and parses it.
|
|
*
|
|
* @param mixed $code Expected reply code(s) (integer or array).
|
|
* @param array $opts Additional options:
|
|
* <pre>
|
|
* - error: (string) On error, 'logout' or 'reset'?
|
|
* - exception: (string) Throw an exception of this class on error.
|
|
* </pre>
|
|
*
|
|
* @return array An array with the response text.
|
|
* @throws Horde_Smtp_Exception
|
|
*/
|
|
protected function _getResponse($code, array $opts = array())
|
|
{
|
|
$opts = array_merge(array(
|
|
'error' => null,
|
|
'exception' => 'Horde_Smtp_Exception'
|
|
), $opts);
|
|
|
|
$text = array();
|
|
|
|
while ($read = $this->_connection->read()) {
|
|
$read = trim(rtrim($read, "\r\n"));
|
|
$replycode = intval(substr($read, 0, 3));
|
|
$text[] = ltrim(substr($read, 4));
|
|
if ($read[3] != '-') {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!is_array($code)) {
|
|
$code = array($code);
|
|
}
|
|
|
|
if (in_array($replycode, $code)) {
|
|
return $text;
|
|
}
|
|
|
|
/* Check for enhanced status codes (RFC 2034). */
|
|
$details = reset($text);
|
|
if (!is_null($this->_extensions) &&
|
|
$this->queryExtension('ENHANCEDSTATUSCODES')) {
|
|
list($enhanced, $details) = explode(' ', $details, 2);
|
|
if (!strpos($enhanced, '.')) {
|
|
$details = $enhanced . ' ' . $details;
|
|
$enhanced = null;
|
|
}
|
|
} else {
|
|
$enhanced = null;
|
|
}
|
|
|
|
$e = new $opts['exception']($details);
|
|
$e->details = $details;
|
|
$e->setSmtpCode($replycode);
|
|
if ($enhanced) {
|
|
$e->setEnhancedSmtpCode($enhanced);
|
|
}
|
|
|
|
switch ($opts['error']) {
|
|
case 'logout':
|
|
$this->logout();
|
|
break;
|
|
|
|
case 'reset':
|
|
/* RFC 3207: If we see 530, no need to send reset command. */
|
|
if ($code != 530) {
|
|
$this->resetCmd();
|
|
}
|
|
break;
|
|
}
|
|
|
|
throw $e;
|
|
}
|
|
|
|
/**
|
|
* Process the return from the DATA command.
|
|
*
|
|
* @see _send()
|
|
*
|
|
* @param array $recipients The list of message recipients.
|
|
*
|
|
* @return array See _send().
|
|
* @throws Horde_Smtp_Exception
|
|
*/
|
|
protected function _processData($recipients)
|
|
{
|
|
$this->_getResponse(250, array(
|
|
'error' => 'reset'
|
|
));
|
|
return array_fill_keys($recipients, true);
|
|
}
|
|
|
|
/**
|
|
* Return the local hostname.
|
|
*
|
|
* @return string Local hostname (null if it cannot be determined).
|
|
*/
|
|
protected function _getHostname()
|
|
{
|
|
return ($localhost = $this->getParam('localhost'))
|
|
? $localhost
|
|
: gethostname();
|
|
}
|
|
|
|
}
|