smtp.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. <?php
  2. /*
  3. Copyright (c) 2009-2019 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,$log,$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. $method=STREAM_CRYPTO_METHOD_TLS_CLIENT;
  195. if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
  196. $method|=STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
  197. $method|=STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
  198. }
  199. stream_socket_enable_crypto($socket,TRUE,$method);
  200. }
  201. $reply=$this->dialog('EHLO '.$fw->HOST,$log,$mock);
  202. }
  203. $message=wordwrap($message,998);
  204. if (preg_match('/8BITMIME/',$reply))
  205. $headers['Content-Transfer-Encoding']='8bit';
  206. else {
  207. $headers['Content-Transfer-Encoding']='quoted-printable';
  208. $message=preg_replace('/^\.(.+)/m',
  209. '..$1',quoted_printable_encode($message));
  210. }
  211. if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) {
  212. // Authenticate
  213. $this->dialog('AUTH LOGIN',$log,$mock);
  214. $this->dialog(base64_encode($this->user),$log,$mock);
  215. $reply=$this->dialog(base64_encode($this->pw),$log,$mock);
  216. if (!preg_match('/^235\s.*/',$reply)) {
  217. $this->dialog('QUIT',$log,$mock);
  218. if (!$mock && $socket)
  219. fclose($socket);
  220. return FALSE;
  221. }
  222. }
  223. if (empty($headers['Message-Id']))
  224. $headers['Message-Id']='<'.uniqid('',TRUE).'@'.$this->host.'>';
  225. if (empty($headers['Date']))
  226. $headers['Date']=date('r');
  227. // Required headers
  228. $reqd=['From','To','Subject'];
  229. foreach ($reqd as $id)
  230. if (empty($headers[$id]))
  231. user_error(sprintf(self::E_Header,$id),E_USER_ERROR);
  232. $eol="\r\n";
  233. // Stringify headers
  234. foreach ($headers as $key=>&$val) {
  235. if (in_array($key,['From','To','Cc','Bcc'])) {
  236. $email='';
  237. preg_match_all('/(?:".+?" )?(?:<.+?>|[^ ,]+)/',
  238. $val,$matches,PREG_SET_ORDER);
  239. foreach ($matches as $raw)
  240. $email.=($email?', ':'').
  241. (preg_match('/<.+?>/',$raw[0])?
  242. $raw[0]:
  243. ('<'.$raw[0].'>'));
  244. $val=$email;
  245. }
  246. unset($val);
  247. }
  248. $from=isset($headers['Sender'])?$headers['Sender']:strstr($headers['From'],'<');
  249. unset($headers['Sender']);
  250. // Start message dialog
  251. $this->dialog('MAIL FROM: '.$from,$log,$mock);
  252. foreach ($fw->split($headers['To'].
  253. (isset($headers['Cc'])?(';'.$headers['Cc']):'').
  254. (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst) {
  255. $this->dialog('RCPT TO: '.strstr($dst,'<'),$log,$mock);
  256. }
  257. $this->dialog('DATA',$log,$mock);
  258. if ($this->attachments) {
  259. // Replace Content-Type
  260. $type=$headers['Content-Type'];
  261. unset($headers['Content-Type']);
  262. $enc=$headers['Content-Transfer-Encoding'];
  263. unset($headers['Content-Transfer-Encoding']);
  264. $hash=uniqid(NULL,TRUE);
  265. // Send mail headers
  266. $out='Content-Type: multipart/mixed; boundary="'.$hash.'"'.$eol;
  267. foreach ($headers as $key=>$val)
  268. if ($key!='Bcc')
  269. $out.=$key.': '.$val.$eol;
  270. $out.=$eol;
  271. $out.='This is a multi-part message in MIME format'.$eol;
  272. $out.=$eol;
  273. $out.='--'.$hash.$eol;
  274. $out.='Content-Type: '.$type.$eol;
  275. $out.='Content-Transfer-Encoding: '.$enc.$eol;
  276. $out.=$eol;
  277. $out.=$message.$eol;
  278. foreach ($this->attachments as $attachment) {
  279. if (is_array($attachment['filename']))
  280. list($alias,$file)=$attachment['filename'];
  281. else
  282. $alias=basename($file=$attachment['filename']);
  283. $out.='--'.$hash.$eol;
  284. $out.='Content-Type: application/octet-stream'.$eol;
  285. $out.='Content-Transfer-Encoding: base64'.$eol;
  286. if ($attachment['cid'])
  287. $out.='Content-Id: '.$attachment['cid'].$eol;
  288. $out.='Content-Disposition: attachment; '.
  289. 'filename="'.$alias.'"'.$eol;
  290. $out.=$eol;
  291. $out.=chunk_split(base64_encode(
  292. file_get_contents($file))).$eol;
  293. }
  294. $out.=$eol;
  295. $out.='--'.$hash.'--'.$eol;
  296. $out.='.';
  297. $this->dialog($out,preg_match('/verbose/i',$log),$mock);
  298. }
  299. else {
  300. // Send mail headers
  301. $out='';
  302. foreach ($headers as $key=>$val)
  303. if ($key!='Bcc')
  304. $out.=$key.': '.$val.$eol;
  305. $out.=$eol;
  306. $out.=$message.$eol;
  307. $out.='.';
  308. // Send message
  309. $this->dialog($out,preg_match('/verbose/i',$log),$mock);
  310. }
  311. $this->dialog('QUIT',$log,$mock);
  312. if (!$mock && $socket)
  313. fclose($socket);
  314. return TRUE;
  315. }
  316. /**
  317. * Instantiate class
  318. * @param $host string
  319. * @param $port int
  320. * @param $scheme string
  321. * @param $user string
  322. * @param $pw string
  323. * @param $ctx resource
  324. **/
  325. function __construct(
  326. $host='localhost',$port=25,$scheme=NULL,$user=NULL,$pw=NULL,$ctx=NULL) {
  327. $this->headers=[
  328. 'MIME-Version'=>'1.0',
  329. 'Content-Type'=>'text/plain; '.
  330. 'charset='.Base::instance()->ENCODING
  331. ];
  332. $this->host=strtolower((($this->scheme=strtolower($scheme))=='ssl'?
  333. 'ssl':'tcp').'://'.$host);
  334. $this->port=$port;
  335. $this->user=$user;
  336. $this->pw=$pw;
  337. $this->context=stream_context_create($ctx);
  338. }
  339. }