smtp.php 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. <?php
  2. /*
  3. Copyright (c) 2009-2017 F3::Factory/Bong Cosca, All rights reserved.
  4. This file is part of the Fat-Free Framework (http://fatfreeframework.com).
  5. This is free software: you can redistribute it and/or modify it under the
  6. terms of the GNU General Public License as published by the Free Software
  7. Foundation, either version 3 of the License, or later.
  8. Fat-Free Framework is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. General Public License for more details.
  12. You should have received a copy of the GNU General Public License along
  13. with Fat-Free Framework. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. //! SMTP plug-in
  16. class SMTP extends Magic {
  17. //@{ Locale-specific error/exception messages
  18. const
  19. E_Header='%s: header is required',
  20. E_Blank='Message must not be blank',
  21. E_Attach='Attachment %s not found';
  22. //@}
  23. protected
  24. //! Message properties
  25. $headers,
  26. //! E-mail attachments
  27. $attachments,
  28. //! SMTP host
  29. $host,
  30. //! SMTP port
  31. $port,
  32. //! TLS/SSL
  33. $scheme,
  34. //! User ID
  35. $user,
  36. //! Password
  37. $pw,
  38. //! TLS/SSL stream context
  39. $context,
  40. //! TCP/IP socket
  41. $socket,
  42. //! Server-client conversation
  43. $log;
  44. /**
  45. * Fix header
  46. * @return string
  47. * @param $key string
  48. **/
  49. protected function fixheader($key) {
  50. return str_replace(' ','-',
  51. ucwords(preg_replace('/[_-]/',' ',strtolower($key))));
  52. }
  53. /**
  54. * Return TRUE if header exists
  55. * @return bool
  56. * @param $key
  57. **/
  58. function exists($key) {
  59. $key=$this->fixheader($key);
  60. return isset($this->headers[$key]);
  61. }
  62. /**
  63. * Bind value to e-mail header
  64. * @return string
  65. * @param $key string
  66. * @param $val string
  67. **/
  68. function set($key,$val) {
  69. $key=$this->fixheader($key);
  70. return $this->headers[$key]=$val;
  71. }
  72. /**
  73. * Return value of e-mail header
  74. * @return string|NULL
  75. * @param $key string
  76. **/
  77. function &get($key) {
  78. $key=$this->fixheader($key);
  79. if (isset($this->headers[$key]))
  80. $val=&$this->headers[$key];
  81. else
  82. $val=NULL;
  83. return $val;
  84. }
  85. /**
  86. * Remove header
  87. * @return NULL
  88. * @param $key string
  89. **/
  90. function clear($key) {
  91. $key=$this->fixheader($key);
  92. unset($this->headers[$key]);
  93. }
  94. /**
  95. * Return client-server conversation history
  96. * @return string
  97. **/
  98. function log() {
  99. return str_replace("\n",PHP_EOL,$this->log);
  100. }
  101. /**
  102. * Send SMTP command and record server response
  103. * @return string
  104. * @param $cmd string
  105. * @param $log bool|string
  106. * @param $mock bool
  107. **/
  108. protected function dialog($cmd=NULL,$log=TRUE,$mock=FALSE) {
  109. $reply='';
  110. if ($mock) {
  111. $host=str_replace('ssl://','',$this->host);
  112. switch ($cmd) {
  113. case NULL:
  114. $reply='220 '.$host.' ESMTP ready'."\n";
  115. break;
  116. case 'DATA':
  117. $reply='354 Go ahead'."\n";
  118. break;
  119. case 'QUIT':
  120. $reply='221 '.$host.' closing connection'."\n";
  121. break;
  122. default:
  123. $reply='250 OK'."\n";
  124. break;
  125. }
  126. }
  127. else {
  128. $socket=&$this->socket;
  129. if ($cmd)
  130. fputs($socket,$cmd."\r\n");
  131. while (!feof($socket) && ($info=stream_get_meta_data($socket)) &&
  132. !$info['timed_out'] && $str=fgets($socket,4096)) {
  133. $reply.=$str;
  134. if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply))
  135. break;
  136. }
  137. }
  138. if ($log) {
  139. if ($cmd)
  140. $this->log.=$cmd."\n";
  141. $this->log.=str_replace("\r",'',$reply);
  142. }
  143. return $reply;
  144. }
  145. /**
  146. * Add e-mail attachment
  147. * @return NULL
  148. * @param $file string
  149. * @param $alias string
  150. * @param $cid string
  151. **/
  152. function attach($file,$alias=NULL,$cid=NULL) {
  153. if (!is_file($file))
  154. user_error(sprintf(self::E_Attach,$file),E_USER_ERROR);
  155. if ($alias)
  156. $file=[$alias,$file];
  157. $this->attachments[]=['filename'=>$file,'cid'=>$cid];
  158. }
  159. /**
  160. * Transmit message
  161. * @return bool
  162. * @param $message string
  163. * @param $log bool|string
  164. * @param $mock bool
  165. **/
  166. function send($message,$log=TRUE,$mock=FALSE) {
  167. if ($this->scheme=='ssl' && !extension_loaded('openssl'))
  168. return FALSE;
  169. // Message should not be blank
  170. if (!$message)
  171. user_error(self::E_Blank,E_USER_ERROR);
  172. $fw=Base::instance();
  173. // Retrieve headers
  174. $headers=$this->headers;
  175. // Connect to the server
  176. if (!$mock) {
  177. $socket=&$this->socket;
  178. $socket=@stream_socket_client($this->host.':'.$this->port,
  179. $errno,$errstr,ini_get('default_socket_timeout'),
  180. STREAM_CLIENT_CONNECT,$this->context);
  181. if (!$socket) {
  182. $fw->error(500,$errstr);
  183. return FALSE;
  184. }
  185. stream_set_blocking($socket,TRUE);
  186. }
  187. // Get server's initial response
  188. $this->dialog(NULL,TRUE,$mock);
  189. // Announce presence
  190. $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock);
  191. if (strtolower($this->scheme)=='tls') {
  192. $this->dialog('STARTTLS',$log,$mock);
  193. if (!$mock)
  194. stream_socket_enable_crypto(
  195. $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT);
  196. $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock);
  197. }
  198. $message=wordwrap($message,998);
  199. if (preg_match('/8BITMIME/',$reply))
  200. $headers['Content-Transfer-Encoding']='8bit';
  201. else {
  202. $headers['Content-Transfer-Encoding']='quoted-printable';
  203. $message=preg_replace('/^\.(.+)/m',
  204. '..$1',quoted_printable_encode($message));
  205. }
  206. if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) {
  207. // Authenticate
  208. $this->dialog('AUTH LOGIN',$log,$mock);
  209. $this->dialog(base64_encode($this->user),$log,$mock);
  210. $reply=$this->dialog(base64_encode($this->pw),$log,$mock);
  211. if (!preg_match('/^235\s.*/',$reply)) {
  212. $this->dialog('QUIT',$log,$mock);
  213. if (!$mock && $socket)
  214. fclose($socket);
  215. return FALSE;
  216. }
  217. }
  218. if (empty($headers['Message-Id']))
  219. $headers['Message-Id']='<'.uniqid('',TRUE).'@'.$this->host.'>';
  220. if (empty($headers['Date']))
  221. $headers['Date']=date('r');
  222. // Required headers
  223. $reqd=['From','To','Subject'];
  224. foreach ($reqd as $id)
  225. if (empty($headers[$id]))
  226. user_error(sprintf(self::E_Header,$id),E_USER_ERROR);
  227. $eol="\r\n";
  228. // Stringify headers
  229. foreach ($headers as $key=>&$val) {
  230. if (in_array($key,['From','To','Cc','Bcc'])) {
  231. $email='';
  232. preg_match_all('/(?:".+?" )?(?:<.+?>|[^ ,]+)/',
  233. $val,$matches,PREG_SET_ORDER);
  234. foreach ($matches as $raw)
  235. $email.=($email?', ':'').
  236. (preg_match('/<.+?>/',$raw[0])?
  237. $raw[0]:
  238. ('<'.$raw[0].'>'));
  239. $val=$email;
  240. }
  241. unset($val);
  242. }
  243. // Start message dialog
  244. $this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log,$mock);
  245. foreach ($fw->split($headers['To'].
  246. (isset($headers['Cc'])?(';'.$headers['Cc']):'').
  247. (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) {
  248. $this->dialog('RCPT TO: '.strstr($dst,'<'),$log,$mock);
  249. }
  250. $this->dialog('DATA',$log,$mock);
  251. if ($this->attachments) {
  252. // Replace Content-Type
  253. $type=$headers['Content-Type'];
  254. unset($headers['Content-Type']);
  255. $enc=$headers['Content-Transfer-Encoding'];
  256. unset($headers['Content-Transfer-Encoding']);
  257. $hash=uniqid(NULL,TRUE);
  258. // Send mail headers
  259. $out='Content-Type: multipart/mixed; boundary="'.$hash.'"'.$eol;
  260. foreach ($headers as $key=>$val)
  261. if ($key!='Bcc')
  262. $out.=$key.': '.$val.$eol;
  263. $out.=$eol;
  264. $out.='This is a multi-part message in MIME format'.$eol;
  265. $out.=$eol;
  266. $out.='--'.$hash.$eol;
  267. $out.='Content-Type: '.$type.$eol;
  268. $out.='Content-Transfer-Encoding: '.$enc.$eol;
  269. $out.=$eol;
  270. $out.=$message.$eol;
  271. foreach ($this->attachments as $attachment) {
  272. if (is_array($attachment['filename']))
  273. list($alias,$file)=$attachment['filename'];
  274. else
  275. $alias=basename($file=$attachment['filename']);
  276. $out.='--'.$hash.$eol;
  277. $out.='Content-Type: application/octet-stream'.$eol;
  278. $out.='Content-Transfer-Encoding: base64'.$eol;
  279. if ($attachment['cid'])
  280. $out.='Content-Id: '.$attachment['cid'].$eol;
  281. $out.='Content-Disposition: attachment; '.
  282. 'filename="'.$alias.'"'.$eol;
  283. $out.=$eol;
  284. $out.=chunk_split(base64_encode(
  285. file_get_contents($file))).$eol;
  286. }
  287. $out.=$eol;
  288. $out.='--'.$hash.'--'.$eol;
  289. $out.='.';
  290. $this->dialog($out,preg_match('/verbose/i',$log),$mock);
  291. }
  292. else {
  293. // Send mail headers
  294. $out='';
  295. foreach ($headers as $key=>$val)
  296. if ($key!='Bcc')
  297. $out.=$key.': '.$val.$eol;
  298. $out.=$eol;
  299. $out.=$message.$eol;
  300. $out.='.';
  301. // Send message
  302. $this->dialog($out,preg_match('/verbose/i',$log),$mock);
  303. }
  304. $this->dialog('QUIT',$log,$mock);
  305. if (!$mock && $socket)
  306. fclose($socket);
  307. return TRUE;
  308. }
  309. /**
  310. * Instantiate class
  311. * @param $host string
  312. * @param $port int
  313. * @param $scheme string
  314. * @param $user string
  315. * @param $pw string
  316. * @param $ctx resource
  317. **/
  318. function __construct(
  319. $host='localhost',$port=25,$scheme=NULL,$user=NULL,$pw=NULL,$ctx=NULL) {
  320. $this->headers=[
  321. 'MIME-Version'=>'1.0',
  322. 'Content-Type'=>'text/plain; '.
  323. 'charset='.Base::instance()->ENCODING
  324. ];
  325. $this->host=strtolower((($this->scheme=strtolower($scheme))=='ssl'?
  326. 'ssl':'tcp').'://'.$host);
  327. $this->port=$port;
  328. $this->user=$user;
  329. $this->pw=$pw;
  330. $this->context=stream_context_create($ctx);
  331. }
  332. }