smtp.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. <?php
  2. /*
  3. Copyright (c) 2009-2014 F3::Factory/Bong Cosca, All rights reserved.
  4. This file is part of the Fat-Free Framework (http://fatfree.sf.net).
  5. THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF
  6. ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
  7. IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
  8. PURPOSE.
  9. Please see the license.txt file for more information.
  10. */
  11. //! SMTP plug-in
  12. class SMTP extends Magic {
  13. //@{ Locale-specific error/exception messages
  14. const
  15. E_Header='%s: header is required',
  16. E_Blank='Message must not be blank',
  17. E_Attach='Attachment %s not found';
  18. //@}
  19. protected
  20. //! Message properties
  21. $headers,
  22. //! E-mail attachments
  23. $attachments,
  24. //! SMTP host
  25. $host,
  26. //! SMTP port
  27. $port,
  28. //! TLS/SSL
  29. $scheme,
  30. //! User ID
  31. $user,
  32. //! Password
  33. $pw,
  34. //! TCP/IP socket
  35. $socket,
  36. //! Server-client conversation
  37. $log;
  38. /**
  39. * Fix header
  40. * @return string
  41. * @param $key string
  42. **/
  43. protected function fixheader($key) {
  44. return str_replace(' ','-',
  45. ucwords(preg_replace('/[_-]/',' ',strtolower($key))));
  46. }
  47. /**
  48. * Return TRUE if header exists
  49. * @return bool
  50. * @param $key
  51. **/
  52. function exists($key) {
  53. $key=$this->fixheader($key);
  54. return isset($this->headers[$key]);
  55. }
  56. /**
  57. * Bind value to e-mail header
  58. * @return string
  59. * @param $key string
  60. * @param $val string
  61. **/
  62. function set($key,$val) {
  63. $key=$this->fixheader($key);
  64. return $this->headers[$key]=$val;
  65. }
  66. /**
  67. * Return value of e-mail header
  68. * @return string|NULL
  69. * @param $key string
  70. **/
  71. function get($key) {
  72. $key=$this->fixheader($key);
  73. return isset($this->headers[$key])?$this->headers[$key]:NULL;
  74. }
  75. /**
  76. * Remove header
  77. * @return NULL
  78. * @param $key string
  79. **/
  80. function clear($key) {
  81. $key=$this->fixheader($key);
  82. unset($this->headers[$key]);
  83. }
  84. /**
  85. * Return client-server conversation history
  86. * @return string
  87. **/
  88. function log() {
  89. return str_replace("\n",PHP_EOL,$this->log);
  90. }
  91. /**
  92. * Send SMTP command and record server response
  93. * @return string
  94. * @param $cmd string
  95. * @param $log bool
  96. **/
  97. protected function dialog($cmd=NULL,$log=TRUE) {
  98. $socket=&$this->socket;
  99. if (!is_null($cmd))
  100. fputs($socket,$cmd."\r\n");
  101. $reply='';
  102. while (!feof($socket) && ($info=stream_get_meta_data($socket)) &&
  103. !$info['timed_out'] && $str=fgets($socket,4096)) {
  104. $reply.=$str;
  105. if (preg_match('/(?:^|\n)\d{3} .+?\r\n/s',$reply))
  106. break;
  107. }
  108. if ($log) {
  109. $this->log.=$cmd."\n";
  110. $this->log.=str_replace("\r",'',$reply);
  111. }
  112. return $reply;
  113. }
  114. /**
  115. * Add e-mail attachment
  116. * @return NULL
  117. * @param $file
  118. **/
  119. function attach($file) {
  120. if (!is_file($file))
  121. user_error(sprintf(self::E_Attach,$file));
  122. $this->attachments[]=$file;
  123. }
  124. /**
  125. * Transmit message
  126. * @return bool
  127. * @param $message string
  128. * @param $log bool
  129. **/
  130. function send($message,$log=TRUE) {
  131. if ($this->scheme=='ssl' && !extension_loaded('openssl'))
  132. return FALSE;
  133. // Message should not be blank
  134. if (!$message)
  135. user_error(self::E_Blank);
  136. $fw=Base::instance();
  137. // Retrieve headers
  138. $headers=$this->headers;
  139. // Connect to the server
  140. $socket=&$this->socket;
  141. $socket=@fsockopen($this->host,$this->port);
  142. if (!$socket)
  143. return FALSE;
  144. stream_set_blocking($socket,TRUE);
  145. // Get server's initial response
  146. $this->dialog(NULL,FALSE);
  147. // Announce presence
  148. $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log);
  149. if (strtolower($this->scheme)=='tls') {
  150. $this->dialog('STARTTLS',$log);
  151. stream_socket_enable_crypto(
  152. $socket,TRUE,STREAM_CRYPTO_METHOD_TLS_CLIENT);
  153. $reply=$this->dialog('EHLO '.$fw->get('HOST'),$log);
  154. if (preg_match('/8BITMIME/',$reply))
  155. $headers['Content-Transfer-Encoding']='8bit';
  156. else {
  157. $headers['Content-Transfer-Encoding']='quoted-printable';
  158. $message=quoted_printable_encode($message);
  159. }
  160. }
  161. if ($this->user && $this->pw && preg_match('/AUTH/',$reply)) {
  162. // Authenticate
  163. $this->dialog('AUTH LOGIN',$log);
  164. $this->dialog(base64_encode($this->user),$log);
  165. $this->dialog(base64_encode($this->pw),$log);
  166. }
  167. // Required headers
  168. $reqd=array('From','To','Subject');
  169. foreach ($reqd as $id)
  170. if (empty($headers[$id]))
  171. user_error(sprintf(self::E_Header,$id));
  172. $eol="\r\n";
  173. $str='';
  174. // Stringify headers
  175. foreach ($headers as $key=>$val)
  176. if (!in_array($key,$reqd))
  177. $str.=$key.': '.$val.$eol;
  178. // Start message dialog
  179. $this->dialog('MAIL FROM: '.strstr($headers['From'],'<'),$log);
  180. foreach ($fw->split($headers['To'].
  181. (isset($headers['Cc'])?(';'.$headers['Cc']):'').
  182. (isset($headers['Bcc'])?(';'.$headers['Bcc']):'')) as $dst)
  183. $this->dialog('RCPT TO: '.strstr($dst,'<'),$log);
  184. $this->dialog('DATA',$log);
  185. if ($this->attachments) {
  186. // Replace Content-Type
  187. $hash=uniqid(NULL,TRUE);
  188. $type=$headers['Content-Type'];
  189. $headers['Content-Type']='multipart/mixed; '.
  190. 'boundary="'.$hash.'"';
  191. // Send mail headers
  192. $out='';
  193. foreach ($headers as $key=>$val)
  194. if ($key!='Bcc')
  195. $out.=$key.': '.$val.$eol;
  196. $out.=$eol;
  197. $out.='This is a multi-part message in MIME format'.$eol;
  198. $out.=$eol;
  199. $out.='--'.$hash.$eol;
  200. $out.='Content-Type: '.$type.$eol;
  201. $out.=$eol;
  202. $out.=$message.$eol;
  203. foreach ($this->attachments as $attachment) {
  204. $out.='--'.$hash.$eol;
  205. $out.='Content-Type: application/octet-stream'.$eol;
  206. $out.='Content-Transfer-Encoding: base64'.$eol;
  207. $out.='Content-Disposition: attachment; '.
  208. 'filename="'.basename($attachment).'"'.$eol;
  209. $out.=$eol;
  210. $out.=chunk_split(
  211. base64_encode(file_get_contents($attachment))).$eol;
  212. }
  213. $out.=$eol;
  214. $out.='--'.$hash.'--'.$eol;
  215. $out.='.';
  216. $this->dialog($out,FALSE);
  217. }
  218. else {
  219. // Send mail headers
  220. $out='';
  221. foreach ($headers as $key=>$val)
  222. if ($key!='Bcc')
  223. $out.=$key.': '.$val.$eol;
  224. $out.=$eol;
  225. $out.=$message.$eol;
  226. $out.='.';
  227. // Send message
  228. $this->dialog($out);
  229. }
  230. $this->dialog('QUIT',$log);
  231. if ($socket)
  232. fclose($socket);
  233. return TRUE;
  234. }
  235. /**
  236. * Instantiate class
  237. * @param $host string
  238. * @param $port int
  239. * @param $scheme string
  240. * @param $user string
  241. * @param $pw string
  242. **/
  243. function __construct($host,$port,$scheme,$user,$pw) {
  244. $this->headers=array(
  245. 'MIME-Version'=>'1.0',
  246. 'Content-Type'=>'text/plain; '.
  247. 'charset='.Base::instance()->get('ENCODING')
  248. );
  249. $this->host=$host;
  250. if (strtolower($this->scheme=strtolower($scheme))=='ssl')
  251. $this->host='ssl://'.$host;
  252. $this->port=$port;
  253. $this->user=$user;
  254. $this->pw=$pw;
  255. }
  256. }