PageRenderTime 51ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/includes/qcodo/_core/framework/QEmailServer.class.php

https://github.com/tronics/qcodo
PHP | 549 lines | 332 code | 87 blank | 130 comment | 93 complexity | 44ab7a047eb7e0bf50a053fbf69263ba MD5 | raw file
  1. <?php
  2. /**
  3. * This EmailServer (and its dependent EmailMessage class) allows the application to send
  4. * messages via any accessible SMTP server.
  5. *
  6. * The QEmailServer class, specifically, is an abstract class and is NOT meant to be instantiated.
  7. * It has one public static method, Send, which takes in a QEmailMessage object.
  8. */
  9. abstract class QEmailServer extends QBaseClass {
  10. /**
  11. * Server Hostname or IP Address of the server running the SMTP service.
  12. * Using an IP address is slightly faster, but using a Hostname is easier to manage.
  13. * Defaults to "localhost".
  14. *
  15. * @var string SmtpServer
  16. */
  17. public static $SmtpServer = 'localhost';
  18. /**
  19. * Port of the SMTP Service on the SmtpServer, usually 25
  20. *
  21. * @var integer SmtpPort
  22. */
  23. public static $SmtpPort = 25;
  24. /**
  25. * IP Address of the Originating Server (e.g. the IP address of this server)
  26. * used for the EHLO command in the SMTP protocol. Defaults to the
  27. * QApplication::$ServerAddress variable, which uses the PHP $_SERVER
  28. * constants to determine the correct IP address.
  29. *
  30. * @var string OriginatingServerIp
  31. */
  32. public static $OriginatingServerIp;
  33. /**
  34. * Whether or not we are running in Test Mode. Test Mode allows you
  35. * to develop e-mail-based applications without actually having access to
  36. * an SMTP server or the Internet. Instead of messages being sent out,
  37. * the messages and corresponding SMTP communication will be saved to disk.
  38. *
  39. * @var boolean $TestMode
  40. */
  41. public static $TestMode = false;
  42. /**
  43. * The directory where TestMode e-mail files will be saved to. The process
  44. * running the webserver *must* have write access to this directory. Default
  45. * is "/tmp", which makes sense in unix/linux/mac environments. Windows users
  46. * will likely need to set up their own temp directories.
  47. *
  48. * @var string $TestModeDirectory
  49. */
  50. public static $TestModeDirectory = '/tmp';
  51. /**
  52. * Boolean flag signifying whether SMTP's AUTH PLAIN should be used
  53. *
  54. * @var bool $AuthPlain
  55. */
  56. public static $AuthPlain = false;
  57. /**
  58. * Boolean flag signifying whether SMTP's AUTH LOGIN should be used
  59. *
  60. * @var bool $AuthLogin
  61. */
  62. public static $AuthLogin = false;
  63. /**
  64. * SMTP Username to use for AUTH PLAIN or LOGIN
  65. *
  66. * @var string $SmtpUsername
  67. */
  68. public static $SmtpUsername = '';
  69. /**
  70. * SMTP Password to use for AUTH PLAIN or LOGIN
  71. *
  72. * @var string $SmtpPassword
  73. */
  74. public static $SmtpPassword = '';
  75. /**
  76. * Encoding Type (if null, will default to the QApplication::$EncodingType)
  77. *
  78. * @var string $EncodingType
  79. */
  80. public static $EncodingType = null;
  81. /**
  82. * Uses regular expression matching to return an array of valid e-mail addresses
  83. *
  84. * @param string $strAddresses Single string containing e-mail addresses and anything else
  85. * @return string[] An array of e-mail addresses only, or NULL if none
  86. */
  87. public static function GetEmailAddresses($strAddresses) {
  88. $strAddressArray = null;
  89. // Address Lines cannot have any linebreaks
  90. if ((strpos($strAddresses, "\r") !== false) ||
  91. (strpos($strAddresses, "\n") !== false))
  92. return null;
  93. preg_match_all ("/[a-zA-Z0-9_.+-]+[@][\-a-zA-Z0-9_.]+/", $strAddresses, $strAddressArray);
  94. if ((is_array($strAddressArray)) &&
  95. (array_key_exists(0, $strAddressArray)) &&
  96. (is_array($strAddressArray[0])) &&
  97. (array_key_exists(0, $strAddressArray[0]))) {
  98. return $strAddressArray[0];
  99. }
  100. // If we're here, then no addresses were found in $strAddress
  101. // so return null
  102. return null;
  103. }
  104. /**
  105. * Sends a message out via SMTP according to the server, ip, etc. preferences
  106. * as set up on the class. Takes in a QEmailMessage object.
  107. *
  108. * Will throw a QEmailException exception on any error.
  109. *
  110. * @param QEmailMessage $objMessage Message to Send
  111. * @return void
  112. */
  113. public static function Send(QEmailMessage $objMessage) {
  114. $objResource = null;
  115. if (QEmailServer::$TestMode) {
  116. // Open up a File Resource to the TestModeDirectory
  117. $strArray = split(' ', microtime());
  118. $strFileName = sprintf('%s/email_%s%s.txt', QEmailServer::$TestModeDirectory, $strArray[1], substr($strArray[0], 1));
  119. $objResource = fopen($strFileName, 'w');
  120. if (!$objResource)
  121. throw new QEmailException(sprintf('Unable to open Test SMTP connection to: %s', $strFileName));
  122. // Clear the Read Buffer
  123. if (!feof($objResource))
  124. fgets($objResource, 4096);
  125. // Write the Connection Command
  126. fwrite($objResource, sprintf("telnet %s %s\r\n", QEmailServer::$SmtpServer, QEmailServer::$SmtpPort));
  127. } else {
  128. $objResource = fsockopen(QEmailServer::$SmtpServer, QEmailServer::$SmtpPort);
  129. if (!$objResource)
  130. throw new QEmailException(sprintf('Unable to open SMTP connection to: %s %s', QEmailServer::$SmtpServer, QEmailServer::$SmtpPort));
  131. }
  132. // Connect
  133. $strResponse = null;
  134. if (!feof($objResource)) {
  135. $strResponse = fgets($objResource, 4096);
  136. // Iterate through all "220-" responses (stop at "220 ")
  137. while ((substr($strResponse, 0, 3) == "220") && (substr($strResponse, 0, 4) != "220 "))
  138. if (!feof($objResource))
  139. $strResponse = fgets($objResource, 4096);
  140. // Check for a "220" response
  141. if (!QEmailServer::$TestMode)
  142. if ((strpos($strResponse, "220") === false) || (strpos($strResponse, "220") != 0))
  143. throw new QEmailException(sprintf('Error Response on Connect: %s', $strResponse));
  144. }
  145. // Send: EHLO
  146. fwrite($objResource, sprintf("EHLO %s\r\n", QEmailServer::$OriginatingServerIp));
  147. if (!feof($objResource)) {
  148. $strResponse = fgets($objResource, 4096);
  149. // Iterate through all "250-" responses (stop at "250 ")
  150. while ((substr($strResponse, 0, 3) == "250") && (substr($strResponse, 0, 4) != "250 "))
  151. if (!feof($objResource))
  152. $strResponse = fgets($objResource, 4096);
  153. // Check for a "250" response
  154. if (!QEmailServer::$TestMode)
  155. if ((strpos($strResponse, "250") === false) || (strpos($strResponse, "250") != 0))
  156. throw new QEmailException(sprintf('Error Response on EHLO: %s', $strResponse));
  157. }
  158. // Send Authentication
  159. if (QEmailServer::$AuthPlain) {
  160. fwrite($objResource, "AUTH PLAIN " . base64_encode(QEmailServer::$SmtpUsername . "\0" . QEmailServer::$SmtpUsername . "\0" . QEmailServer::$SmtpPassword) . "\r\n");
  161. if (!feof($objResource)) {
  162. $strResponse = fgets($objResource, 4096);
  163. if ((strpos($strResponse, "235") === false) || (strpos($strResponse, "235") != 0))
  164. throw new QEmailException(sprintf('Error in response from AUTH PLAIN: %s', $strResponse));
  165. }
  166. }
  167. if (QEmailServer::$AuthLogin) {
  168. fwrite($objResource,"AUTH LOGIN\r\n");
  169. if (!feof($objResource)) {
  170. $strResponse = fgets($objResource, 4096);
  171. if (!QEmailServer::$TestMode)
  172. if ((strpos($strResponse, "334") === false) || (strpos($strResponse, "334") != 0))
  173. throw new QEmailException(sprintf('Error in response from AUTH LOGIN: %s', $strResponse));
  174. }
  175. fwrite($objResource, base64_encode(QEmailServer::$SmtpUsername) . "\r\n");
  176. if (!feof($objResource)) {
  177. $strResponse = fgets($objResource, 4096);
  178. if (!QEmailServer::$TestMode)
  179. if ((strpos($strResponse, "334") === false) || (strpos($strResponse, "334") != 0))
  180. throw new QEmailException(sprintf('Error in response from AUTH LOGIN: %s', $strResponse));
  181. }
  182. fwrite($objResource, base64_encode(QEmailServer::$SmtpPassword) . "\r\n");
  183. if (!feof($objResource)) {
  184. $strResponse = fgets($objResource, 4096);
  185. if (!QEmailServer::$TestMode)
  186. if ((strpos($strResponse, "235") === false) || (strpos($strResponse, "235") != 0))
  187. throw new QEmailException(sprintf('Error in response from AUTH LOGIN: %s', $strResponse));
  188. }
  189. }
  190. // Setup MAIL FROM line
  191. $strAddressArray = QEmailServer::GetEmailAddresses($objMessage->From);
  192. if (count($strAddressArray) != 1)
  193. throw new QEmailException(sprintf('Not a valid From address: %s', $objMessage->From));
  194. // Send: MAIL FROM line
  195. fwrite($objResource, sprintf("MAIL FROM: <%s>\r\n", $strAddressArray[0]));
  196. if (!feof($objResource)) {
  197. $strResponse = fgets($objResource, 4096);
  198. // Check for a "250" response
  199. if (!QEmailServer::$TestMode)
  200. if ((strpos($strResponse, "250") === false) || (strpos($strResponse, "250") != 0))
  201. throw new QEmailException(sprintf('Error Response on MAIL FROM: %s', $strResponse));
  202. }
  203. // Setup RCPT TO line(s)
  204. $strAddressToArray = QEmailServer::GetEmailAddresses($objMessage->To);
  205. if (!$strAddressToArray)
  206. throw new QEmailException(sprintf('Not a valid To address: %s', $objMessage->To));
  207. $strAddressCcArray = QEmailServer::GetEmailAddresses($objMessage->Cc);
  208. if (!$strAddressCcArray)
  209. $strAddressCcArray = array();
  210. $strAddressBccArray = QEmailServer::GetEmailAddresses($objMessage->Bcc);
  211. if (!$strAddressBccArray)
  212. $strAddressBccArray = array();
  213. $strAddressCcBccArray = array_merge($strAddressCcArray, $strAddressBccArray);
  214. $strAddressArray = array_merge($strAddressToArray, $strAddressCcBccArray);
  215. // Send: RCPT TO line(s)
  216. foreach ($strAddressArray as $strAddress) {
  217. fwrite($objResource, sprintf("RCPT TO: <%s>\r\n", $strAddress));
  218. if (!feof($objResource)) {
  219. $strResponse = fgets($objResource, 4096);
  220. // Check for a "250" response
  221. if (!QEmailServer::$TestMode)
  222. if ((strpos($strResponse, "250") === false) || (strpos($strResponse, "250") != 0))
  223. throw new QEmailException(sprintf('Error Response on RCPT TO: %s', $strResponse));
  224. }
  225. }
  226. // Send: DATA
  227. fwrite($objResource, "DATA\r\n");
  228. if (!feof($objResource)) {
  229. $strResponse = fgets($objResource, 4096);
  230. // Check for a "354" response
  231. if (!QEmailServer::$TestMode)
  232. if ((strpos($strResponse, "354") === false) || (strpos($strResponse, "354") != 0))
  233. throw new QEmailException(sprintf('Error Response on DATA: %s', $strResponse));
  234. }
  235. // Send: Required Headers
  236. fwrite($objResource, sprintf("Date: %s\r\n", QDateTime::NowToString(QDateTime::FormatRfc822)));
  237. fwrite($objResource, sprintf("To: %s\r\n", $objMessage->To));
  238. fwrite($objResource, sprintf("From: %s\r\n", $objMessage->From));
  239. // Send: Optional Headers
  240. if ($objMessage->Subject)
  241. fwrite($objResource, sprintf("Subject: %s\r\n", $objMessage->Subject));
  242. if ($objMessage->Cc)
  243. fwrite($objResource, sprintf("Cc: %s\r\n", $objMessage->Cc));
  244. // Send: Content-Type Header (if applicable)
  245. $semi_random = md5(time());
  246. $strBoundary = sprintf('==qcodo_qemailserver_multipart_boundary____x%sx', $semi_random);
  247. // Send: Other Headers (if any)
  248. foreach ($objArray = $objMessage->HeaderArray as $strKey => $strValue)
  249. fwrite($objResource, sprintf("%s: %s\r\n", $strKey, $strValue));
  250. // if we are adding an html or files to the message we need these headers.
  251. if ($objMessage->HasFiles || $objMessage->HtmlBody) {
  252. fwrite($objResource, "MIME-Version: 1.0\r\n");
  253. fwrite($objResource, sprintf("Content-Type: multipart/mixed;\r\n boundary=\"%s\"\r\n", $strBoundary));
  254. fwrite($objResource, sprintf("This is a multipart message in MIME format.\r\n\r\n", $strBoundary));
  255. fwrite($objResource, sprintf("--%s\r\n", $strBoundary));
  256. }
  257. $strAltBoundary = sprintf('==qcodo_qemailserver_alternative_boundary____x%sx', $semi_random);
  258. // Send: Body
  259. // Setup Encoding Type (use QEmailServer if specified, otherwise default to QApplication's)
  260. if (!($strEncodingType = QEmailServer::$EncodingType))
  261. $strEncodingType = QApplication::$EncodingType;
  262. if ($objMessage->HtmlBody) {
  263. fwrite($objResource, sprintf("Content-Type: multipart/alternative;\r\n boundary=\"%s\"\r\n\r\n", $strAltBoundary));
  264. fwrite($objResource, sprintf("--%s\r\n", $strAltBoundary));
  265. fwrite($objResource, sprintf("Content-Type: text/plain; charset=\"%s\"\r\n", $strEncodingType));
  266. fwrite($objResource, sprintf("Content-Transfer-Encoding: 7bit\r\n\r\n"));
  267. fwrite($objResource, $objMessage->Body);
  268. fwrite($objResource, "\r\n\r\n");
  269. fwrite($objResource, sprintf("--%s\r\n", $strAltBoundary));
  270. fwrite($objResource, sprintf("Content-Type: text/html; charset=\"%s\"\r\n", $strEncodingType));
  271. fwrite($objResource, sprintf("Content-Transfer-Encoding: quoted-printable\r\n\r\n"));
  272. fwrite($objResource, $objMessage->HtmlBody);
  273. fwrite($objResource, "\r\n\r\n");
  274. fwrite($objResource, sprintf("--%s--\r\n", $strAltBoundary));
  275. } elseif($objMessage->HasFiles) {
  276. fwrite($objResource, sprintf("Content-Type: multipart/alternative;\r\n boundary=\"%s\"\r\n\r\n", $strAltBoundary));
  277. fwrite($objResource, sprintf("--%s\r\n", $strAltBoundary));
  278. fwrite($objResource, sprintf("Content-Type: text/plain; charset=\"%s\"\r\n", $strEncodingType));
  279. fwrite($objResource, sprintf("Content-Transfer-Encoding: 7bit\r\n\r\n"));
  280. fwrite($objResource, $objMessage->Body);
  281. fwrite($objResource, "\r\n\r\n");
  282. fwrite($objResource, sprintf("--%s--\r\n", $strAltBoundary));
  283. } else
  284. fwrite($objResource, "\r\n" . $objMessage->Body);
  285. // Send: File Attachments
  286. if($objMessage->HasFiles) {
  287. foreach ($objArray = $objMessage->FileArray as $objFile) {
  288. fwrite($objResource, sprintf("--%s\r\n", $strBoundary));
  289. fwrite($objResource, sprintf("Content-Type: %s;\r\n", $objFile->MimeType ));
  290. fwrite($objResource, sprintf(" name=\"%s\"\r\n", $objFile->FileName ));
  291. fwrite($objResource, "Content-Transfer-Encoding: base64\r\n");
  292. fwrite($objResource, "Content-Length: %s\r\n", strlen($objFile->EncodedFileData));
  293. fwrite($objResource, "Content-Disposition: attachment;\r\n");
  294. fwrite($objResource, sprintf(" filename=\"%s\"\r\n\r\n", $objFile->FileName));
  295. fwrite($objResource, $objFile->EncodedFileData);
  296. // foreach (explode("\n", $objFile->EncodedFileData) as $strLine) {
  297. // $strLine = trim($strLine);
  298. // fwrite($objResource, $strLine . "\r\n");
  299. // }
  300. }
  301. }
  302. // close a message with these boundaries if the message had files or had html
  303. if($objMessage->HasFiles || $objMessage->HtmlBody)
  304. fwrite($objResource, sprintf("\r\n\r\n--%s--\r\n", $strBoundary)); // send end of file attachments...
  305. // Send: Message End
  306. fwrite($objResource, "\r\n.\r\n");
  307. if (!feof($objResource)) {
  308. $strResponse = fgets($objResource, 4096);
  309. // Check for a "250" response
  310. if (!QEmailServer::$TestMode)
  311. if ((strpos($strResponse, "250") === false) || (strpos($strResponse, "250") != 0))
  312. throw new QEmailException(sprintf('Error Response on DATA finish: %s', $strResponse));
  313. }
  314. // Send: QUIT
  315. fwrite($objResource, "QUIT\r\n");
  316. if (!feof($objResource))
  317. $strResponse = fgets($objResource, 4096);
  318. // Close the Resource
  319. fclose($objResource);
  320. if (QEmailServer::$TestMode)
  321. chmod($strFileName, 0777);
  322. }
  323. }
  324. // PHP does not allow Static Class Variables to be set to non-constants.
  325. // So we set QEmailServer's OriginatingServerIp to QApplication's ServerAddress here.
  326. QEmailServer::$OriginatingServerIp = QApplication::$ServerAddress;
  327. class QEmailException extends QCallerException {}
  328. class QEmailAttachment extends QBaseClass {
  329. protected $strFilePath;
  330. protected $strMimeType;
  331. protected $strFileName;
  332. protected $strEncodedFileData;
  333. public function __construct($strFilePath, $strSpecifiedMimeType = null, $strSpecifiedFileName = null) {
  334. // Set File Path
  335. if (!is_file(realpath($strFilePath)))
  336. throw new QCallerException('File Not Found: ' . $strFilePath);
  337. $this->strFilePath = realpath($strFilePath);
  338. // Set the File MIME Type -- if Explicitly Set, use it
  339. if ($strSpecifiedMimeType)
  340. $this->strMimeType = $strSpecifiedMimeType;
  341. // otherwise, use QMimeType to determine
  342. else
  343. $this->strMimeType = QMimeType::GetMimeTypeForFile($this->strFilePath);
  344. // Set the File Name -- if explicitly set, use it
  345. if ($strSpecifiedFileName)
  346. $this->strFileName = $strSpecifiedFileName;
  347. // Otherwise, use basename() to determine
  348. else
  349. $this->strFileName = basename($this->strFilePath);
  350. // Read file into a Base64 Encoded Data Stream
  351. $strFileContents = file_get_contents($this->strFilePath, false);
  352. $this->strEncodedFileData = chunk_split(base64_encode($strFileContents));
  353. }
  354. public function __get($strName) {
  355. switch ($strName) {
  356. case 'FilePath': return $this->strFilePath;
  357. case 'MimeType': return $this->strMimeType;
  358. case 'FileName': return $this->strFileName;
  359. case 'EncodedFileData': return $this->strEncodedFileData;
  360. default:
  361. try {
  362. return parent::__get($strName);
  363. } catch (QCallerException $objExc) {
  364. $objExc->IncrementOffset();
  365. throw $objExc;
  366. }
  367. }
  368. }
  369. }
  370. class QEmailMessage extends QBaseClass {
  371. protected $strFrom;
  372. protected $strTo;
  373. protected $strSubject;
  374. protected $strBody;
  375. protected $strHtmlBody;
  376. protected $strCc;
  377. protected $strBcc;
  378. protected $strHeaderArray = array();
  379. protected $objFileArray = array();
  380. public function AddAttachment(QEmailAttachment $objFile) {
  381. $this->objFileArray[$objFile->FileName] = $objFile;
  382. }
  383. public function Attach($strFilePath, $strSpecifiedMimeType = null, $strSpecifiedFileName = null) {
  384. $this->AddAttachment(new QEmailAttachment($strFilePath, $strSpecifiedMimeType, $strSpecifiedFileName));
  385. }
  386. public function RemoveAttachment($strFileName) {
  387. if (array_key_exists($strName, $this->objFileArray))
  388. unset($this->objFileArray[$strName]);
  389. }
  390. public function SetHeader($strName, $strValue) {
  391. $this->strHeaderArray[$strName] = $strValue;
  392. }
  393. public function GetHeader($strName) {
  394. if (array_key_exists($strName, $this->strHeaderArray))
  395. return $this->strHeaderArray[$strName];
  396. return null;
  397. }
  398. public function RemoveHeader($strName, $strValue) {
  399. if (array_key_exists($strName, $this->strHeaderArray))
  400. unset($this->strHeaderArray[$strName]);
  401. }
  402. public function __construct($strFrom = null, $strTo = null, $strSubject = null, $strBody = null) {
  403. $this->strFrom = $strFrom;
  404. $this->strTo = $strTo;
  405. // We must cleanup the Subject and Body -- use the Property to set
  406. $this->Subject = $strSubject;
  407. $this->Body = $strBody;
  408. }
  409. public function __get($strName) {
  410. switch ($strName) {
  411. case 'From': return $this->strFrom;
  412. case 'To': return $this->strTo;
  413. case 'Subject': return $this->strSubject;
  414. case 'Body': return $this->strBody;
  415. case 'HtmlBody': return $this->strHtmlBody;
  416. case 'Cc': return $this->strCc;
  417. case 'Bcc': return $this->strBcc;
  418. case 'HeaderArray': return $this->strHeaderArray;
  419. case 'FileArray': return $this->objFileArray;
  420. case 'HasFiles': return (count($this->objFileArray) > 0) ? true : false;
  421. default:
  422. try {
  423. return parent::__get($strName);
  424. } catch (QCallerException $objExc) {
  425. $objExc->IncrementOffset();
  426. throw $objExc;
  427. }
  428. }
  429. }
  430. public function __set($strName, $mixValue) {
  431. try {
  432. switch ($strName) {
  433. case 'From': return ($this->strFrom = QType::Cast($mixValue, QType::String));
  434. case 'To': return ($this->strTo = QType::Cast($mixValue, QType::String));
  435. case 'Subject':
  436. $strSubject = trim(QType::Cast($mixValue, QType::String));
  437. $strSubject = str_replace("\r", "", $strSubject);
  438. $strSubject = str_replace("\n", " ", $strSubject);
  439. return ($this->strSubject = $strSubject);
  440. case 'Body':
  441. $strBody = QType::Cast($mixValue, QType::String);
  442. $strBody = str_replace("\r", "", $strBody);
  443. $strBody = str_replace("\n", "\r\n", $strBody);
  444. $strBody = str_replace("\r\n.", "\r\n..", $strBody);
  445. return ($this->strBody = $strBody);
  446. case 'HtmlBody':
  447. $strHtmlBody = QType::Cast($mixValue, QType::String);
  448. $strHtmlBody = str_replace("\r", "", $strHtmlBody);
  449. $strHtmlBody = str_replace("\n", "\r\n", $strHtmlBody);
  450. $strHtmlBody = str_replace("\r\n.", "\r\n..", $strHtmlBody);
  451. return ($this->strHtmlBody = $strHtmlBody);
  452. case 'Cc': return ($this->strCc = QType::Cast($mixValue, QType::String));
  453. case 'Bcc': return ($this->strBcc = QType::Cast($mixValue, QType::String));
  454. default: return (parent::__set($strName, $mixValue));
  455. }
  456. } catch (QInvalidCastException $objExc) {
  457. $objExc->IncrementOffset();
  458. throw $objExc;
  459. }
  460. }
  461. }
  462. ?>