lib/smtp.php
<?php

/*

	Copyright (c) 2009-2015 F3::Factory/Bong Cosca, All rights reserved.

	This file is part of the Fat-Free Framework (http://fatfreeframework.com).

	This is free software: you can redistribute it and/or modify it under the
	terms of the GNU General Public License as published by the Free Software
	Foundation, either version 3 of the License, or later.

	Fat-Free Framework is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
	General Public License for more details.

	You should have received a copy of the GNU General Public License along
	with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.

*/

//! SMTP plug-in
class SMTP extends Magic {

	//@{ Locale-specific error/exception messages
	const
		E_Header='%s: header is required',
		E_Blank='Message must not be blank',
		E_Attach='Attachment %s not found';
	//@}

	protected
		//! Message properties
		$headers,
		//! E-mail attachments
		$attachments,
		//! SMTP host
		$host,
		//! SMTP port
		$port,
		//! TLS/SSL
		$scheme,
		//! User ID
		$user,
		//! Password
		$pw,
		//! TCP/IP socket
		$socket,
		//! Server-client conversation
		$log;

	/**
	*	Fix header
	*	@return string
	*	@param $key string
	**/
	protected function fixheader($key) {
		return str_replace(' ','-',
			ucwords(preg_replace('/[_-]/',' ',strtolower($key))));
	}

	/**
	*	Return TRUE if header exists
	*	@return bool
	*	@param $key
	**/
	function exists($key) {
		$key=$this->fixheader($key);
		return isset($this->headers[$key]);
	}

	/**
	*	Bind value to e-mail header
	*	@return string
	*	@param $key string
	*	@param $val string
	**/
	function set($key,$val) {
		$key=$this->fixheader($key);
		return $this->headers[$key]=$val;
	}

	/**
	*	Return value of e-mail header
	*	@return string|NULL
	*	@param $key string
	**/
	function &get($key) {
		$key=$this->fixheader($key);
		if (isset($this->headers[$key]))
			$val=&$this->headers[$key];
		else
			$val=NULL;
		return $val;
	}

	/**
	*	Remove header
	*	@return NULL
	*	@param $key string
	**/
	function clear($key) {
		$key=$this->fixheader($key);
		unset($this->headers[$key]);
	}

	/**
	*	Return client-server conversation history
	*	@return string
	**/
	function log() {
		return str_replace("\n",PHP_EOL,$this->log);
	}

	/**
	*	Send SMTP command and record server response
	*	@return string
	*	@param $cmd string
	*	@param $log bool
	**/
	protected function dialog($cmd=NULL,$log=TRUE) {
		$socket=&$this->socket;
		if (!is_null($cmd))
			fputs($socket,$cmd."\r\n");
		$reply='';
		while (!feof($socket) && ($info=stream_get_meta_data($socket)) &&
			!$info['timed_out'] && $str=fgets($socket,4096)) {
			$reply.=$str;
			if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply))
				break;
		}
		if ($log) {
			$this->log.=$cmd."\n";
			$this->log.=str_replace("\r",'',$reply);
		}
		return $reply;
	}

	/**
	*	Add e-mail attachment
	*	@return NULL
	*	@param $file string
	*	@param $alias string
	*	@param $cid string
	**/
	function attach($file,$alias=NULL,$cid=NULL) {
		if (!is_file($file))
			user_error(sprintf(self::E_Attach,$file),E_USER_ERROR);
		if (is_string($alias))
			$file=array($alias=>$file);
		$this->attachments[]=array('filename'=>$file,'cid'=>$cid);
	}

	/**
	*	Transmit message
	*	@return bool
	*	@param $message string
	*	@param $log bool
	**/
	function send($message,$log=TRUE) {
		if ($this->scheme=='ssl' && !extension_loaded('openssl'))
			return FALSE;
		// Message should not be blank
		if (!$message)
			user_error(self::E_Blank,E_USER_ERROR);
		$fw=Base::instance();
		// Retrieve headers
		$headers=$this->headers;
		// Connect to the server
		$socket=&$this->socket;
		$socket=@fsockopen($this->host,$this->port);
		if (!$socket)
			return FALSE;
		stream_set_blocking($socket,TRUE);
		// Get server's initial response
		$this->dialog(NULL,FALSE);
		// Announce presence
		$reply=$this->dialog('EHLO '.$fw->get('HOST'),$log);
		if (strtolower($this->scheme)=='tls') {
			$this->dialog('STARTTLS',$log);
			stream_socket_enable_crypto(
				$socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT);
			$reply=$this->dialog('EHLO '.$fw->get('HOST'),$log);
			if (preg_match('/8BITMIME/',$reply))
				$headers['Content-Transfer-Encoding']='8bit';
			else {
				$headers['Content-Transfer-Encoding']='quoted-printable';
				$message=quoted_printable_encode($message);
			}
		}
		if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) {
			// Authenticate
			$this->dialog('AUTH LOGIN',$log);
			$this->dialog(base64_encode($this->user),$log);
			$this->dialog(base64_encode($this->pw),$log);
		}
		// Required headers
		$reqd=array('From','To','Subject');
		foreach ($reqd as $id)
			if (empty($headers[$id]))
				user_error(sprintf(self::E_Header,$id),E_USER_ERROR);
		$eol="\r\n";
		$str='';
		// Stringify headers
		foreach ($headers as $key=>&$val) {
			if (!in_array($key,$reqd)) {
				$str.=$key.': '.$val.$eol;
			}
			if (in_array($key,array('From','To','Cc','Bcc')) &&
				!preg_match('/[<>]/',$val))
				$val='<'.$val.'>';
			unset($val);
		}
		// Start message dialog
		$this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log);
		foreach ($fw->split($headers['To'].
			(isset($headers['Cc'])?(';'.$headers['Cc']):'').
			(isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst)
			$this->dialog('RCPT TO: '.strstr($dst,'<'),$log);
		$this->dialog('DATA',$log);
		if ($this->attachments) {
			// Replace Content-Type
			$hash=uniqid(NULL,TRUE);
			$type=$headers['Content-Type'];
			$headers['Content-Type']='multipart/mixed; '.
				'boundary="'.$hash.'"';
			// Send mail headers
			$out='';
			foreach ($headers as $key=>$val)
				if ($key!='Bcc')
					$out.=$key.': '.$val.$eol;
			$out.=$eol;
			$out.='This is a multi-part message in MIME format'.$eol;
			$out.=$eol;
			$out.='--'.$hash.$eol;
			$out.='Content-Type: '.$type.$eol;
			$out.=$eol;
			$out.=$message.$eol;
			foreach ($this->attachments as $attachment) {
				if (is_array($attachment['filename'])) {
					list($alias,$file)=each($attachment);
					$filename=$alias;
					$attachment['filename']=$file;
				}
				else
					$filename=basename($attachment);
				$out.='--'.$hash.$eol;
				$out.='Content-Type: application/octet-stream'.$eol;
				$out.='Content-Transfer-Encoding: base64'.$eol;
				if ($attachment['cid'])
					$out.='Content-ID: '.$attachment['cid'].$eol;
				$out.='Content-Disposition: attachment; '.
					'filename="'.$filename.'"'.$eol;
				$out.=$eol;
				$out.=chunk_split(
					base64_encode(file_get_contents($attachment))).$eol;
			}
			$out.=$eol;
			$out.='--'.$hash.'--'.$eol;
			$out.='.';
			$this->dialog($out,FALSE);
		}
		else {
			// Send mail headers
			$out='';
			foreach ($headers as $key=>$val)
				if ($key!='Bcc')
					$out.=$key.': '.$val.$eol;
			$out.=$eol;
			$out.=$message.$eol;
			$out.='.';
			// Send message
			$this->dialog($out);
		}
		$this->dialog('QUIT',$log);
		if ($socket)
			fclose($socket);
		return TRUE;
	}

	/**
	*	Instantiate class
	*	@param $host string
	*	@param $port int
	*	@param $scheme string
	*	@param $user string
	*	@param $pw string
	**/
	function __construct($host,$port,$scheme,$user,$pw) {
		$this->headers=array(
			'MIME-Version'=>'1.0',
			'Content-Type'=>'text/plain; '.
				'charset='.Base::instance()->get('ENCODING')
		);
		$this->host=$host;
		if (strtolower($this->scheme=strtolower($scheme))=='ssl')
			$this->host='ssl://'.$host;
		$this->port=$port;
		$this->user=$user;
		$this->pw=$pw;
	}

}
