PageRenderTime 64ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/code/classes/Daemon/PMaild/SMTP_Client.class.php

https://github.com/blekkzor/pinetd2
PHP | 315 lines | 266 code | 37 blank | 12 comment | 65 complexity | bdce1937025431568a3d163a1dd2ad21 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. namespace Daemon\PMaild;
  3. class SMTP_Client extends \pinetd\TCP\Client {
  4. protected $helo = null;
  5. protected $dataMode = false;
  6. protected $txn;
  7. protected $dataTxn; // used when sending an email
  8. function __construct($fd, $peer, $parent, $protocol) {
  9. parent::__construct($fd, $peer, $parent, $protocol);
  10. $this->setMsgEnd("\r\n");
  11. }
  12. function welcomeUser() { // nothing to do
  13. return true;
  14. }
  15. function sendBanner() {
  16. $this->sendMsg('220 '.$this->IPC->getName().' ESMTP (pmaild v2.0.0; pinetd v'.PINETD_VERSION.')');
  17. $class = relativeclass($this, 'MTA\\Mail');
  18. $this->txn = new $class($this->peer, $this->IPC);
  19. return true;
  20. }
  21. function shutdown() {
  22. $this->sendMsg('421 4.3.2 SMTP server is shutting down, please try again later');
  23. }
  24. function _cmd_default() {
  25. $this->sendMsg('500 5.5.1 Unknown command');
  26. }
  27. function _cmd_quit() {
  28. $this->sendMsg('221 2.0.0 '.$this->IPC->getName().' closing control connexion.');
  29. $this->close();
  30. }
  31. function _cmd_expn($argv) {
  32. return $this->_cmd_vrfy($argv);
  33. }
  34. function _cmd_vrfy($argv) {
  35. $this->sendMsg('502 5.5.1 I won\'t let you check if this email exists, however RFC said I should reply with a 502 message.');
  36. }
  37. function _cmd_rset() {
  38. $this->txn->reset();
  39. $this->sendMsg('250 RSET done');
  40. }
  41. function _cmd_noop($argv) {
  42. $this->sendMsg('250 2.0.0 Ok');
  43. }
  44. function _cmd_ehlo($argv) {
  45. $argv[0] = 'EHLO';
  46. return $this->_cmd_helo($argv);
  47. }
  48. function _cmd_helo($argv) {
  49. if (!is_null($this->helo)) {
  50. $this->sendMsg('503 5.5.2 You already told me hello, remember?');
  51. return;
  52. }
  53. if (!$this->txn->setHelo($argv[1])) {
  54. $this->sendMsg('500 5.5.2 Bad HELO, please retry...');
  55. return;
  56. }
  57. $this->helo = $argv[1];
  58. if ($argv[0] != 'EHLO') {
  59. $this->sendMsg('250 '.$this->IPC->getName().' pleased to meet you, '.$this->helo);
  60. return;
  61. }
  62. $config = $this->IPC->getSmtpConfig();
  63. $this->sendMsg('250-'.$this->IPC->getName().' pleased to meet you, '.$this->helo);
  64. $this->sendMsg('250-PIPELINING');
  65. $this->sendMsg('250-ENHANCEDSTATUSCODES');
  66. $this->sendMsg('250-ETRN');
  67. $this->sendMsg('250-TXLG');
  68. $this->sendMsg('250-SIZE '.(($config['MaxMailSize']?:100)*1024*1024));
  69. if (($this->IPC->hasTLS()) && ($this->protocol != 'tls')) {
  70. $this->sendMsg('250-STARTTLS');
  71. }
  72. if ( ($this->protocol == 'tls') || (!$this->IPC->hasTLS())) {
  73. $this->sendMsg('250-AUTH PLAIN LOGIN');
  74. $this->sendMsg('250-AUTH=PLAIN LOGIN'); // for deaf clients
  75. }
  76. $this->sendMsg('250 8BITMIME');
  77. }
  78. // AUTH: http://www.technoids.org/saslmech.html
  79. function _cmd_auth($argv) {
  80. if (($this->protocol == 'tcp') && ($this->IPC->hasTLS())) {
  81. $this->sendMsg('550 5.7.0 SSL required before using AUTH');
  82. return;
  83. }
  84. switch(strtoupper($argv[1])) {
  85. case 'LOGIN':
  86. $this->sendMsg('334 '.base64_encode('Username:'));
  87. $login = base64_decode($this->readLine());
  88. $this->sendMsg('334 '.base64_encode('Password:'));
  89. $pass = base64_decode($this->readLine());
  90. break;
  91. case 'PLAIN':
  92. if ($argv[2]) {
  93. list($check, $login, $pass) = explode("\x00", base64_decode($argv[2]));
  94. } else {
  95. $this->sendMsg('334');
  96. list($check, $login, $pass) = explode("\x00", base64_decode($this->readLine()));
  97. }
  98. if ($check != '') {
  99. $this->sendMsg('550 5.5.2 Syntax error in AUTH PLAIN');
  100. return;
  101. }
  102. break;
  103. default:
  104. $this->sendMsg('550 5.5.2 Unsupported auth method');
  105. return;
  106. }
  107. // we got $login & $pass
  108. if (!$this->txn->setLogin($login, $pass)) {
  109. sleep(4); // TODO: watch for DoS here if Fork is disabled! Do not sleep if no fork?
  110. $this->sendMsg('535 5.7.0 Authentification failed');
  111. return;
  112. }
  113. $this->SendMsg('235 2.0.0 OK Authenticated');
  114. }
  115. function _cmd_help() {
  116. $this->sendMsg('214 http://wiki.ooKoo.org/wiki/pinetd');
  117. }
  118. function _cmd_starttls() {
  119. if (!$this->IPC->hasTLS()) {
  120. $this->sendMsg('454 TLS not available');
  121. return;
  122. }
  123. if ($this->protocol != 'tcp') {
  124. $this->sendMsg('500 STARTTLS only available in PLAIN mode. An encryption mode is already enabled');
  125. return;
  126. }
  127. $this->sendMsg('220 Ready to start TLS');
  128. // TODO: this call will lock, need a way to avoid from doing it without Fork
  129. $method = STREAM_CRYPTO_METHOD_SSLv23_SERVER;
  130. // check if this client has broken TLS support that requires SSLv23 instead of TLS
  131. if ($this->IPC->isTlsBroken($this->peer[0]))
  132. $method = STREAM_CRYPTO_METHOD_TLS_SERVER;
  133. if (!stream_socket_enable_crypto($this->fd, true, $method)) {
  134. $this->sendMsg('500 TLS negociation failed!');
  135. $this->IPC->reportTlsFailure($this->peer[0]);
  136. $this->close();
  137. }
  138. $this->helo = NULL; // reset as per RFC-2487 5.2
  139. $this->protocol = 'tls';
  140. $this->txn->reset();
  141. }
  142. function _cmd_mail($argv) {
  143. if (is_null($this->helo)) {
  144. $this->sendMsg('503 Sorry man, can\'t let you send mails without EHLO/HELO first');
  145. return;
  146. }
  147. $nargv = 2;
  148. if (strtoupper($argv[1]) != 'FROM:') {
  149. if (strtoupper(substr($argv[1], 0, 5)) == 'FROM:') {
  150. $from = substr($argv[1], 5);
  151. } else {
  152. $this->sendMsg('550 Invalid syntax. Expected MAIL FROM: <user@domain.tld>');
  153. return;
  154. }
  155. } else {
  156. $from = (string)$argv[$nargv++];
  157. }
  158. $meta = array();
  159. while($nargv < count($argv)) {
  160. $arg = (string)$argv[$nargv++];
  161. $pos = strpos($arg, '=');
  162. if ($pos === false) continue;
  163. $meta[strtolower(substr($arg, 0, $pos))] = substr($arg, $pos+1);
  164. }
  165. // in theory, addr should be in < > (still, we won't require it)
  166. if (($from[0] == '<') && (substr($from, -1) == '>')) {
  167. $from = substr($from, 1, -1);
  168. }
  169. // TODO: we might have BODY=8BITMIME in the meta, what should we do with that?
  170. $config = $this->IPC->getSmtpConfig();
  171. if (isset($meta['size'])) {
  172. $maxsize = ($config['MaxMailSize']?:100)*1024*1024;
  173. if ($meta['size'] > $maxsize) {
  174. $this->sendMsg('550 5.5.0 Mail too large for this system');
  175. return;
  176. }
  177. }
  178. if (!$this->txn->setFrom($from)) {
  179. $this->sendMsg($this->txn->errorMsg());
  180. return;
  181. }
  182. $this->sendMsg('250 2.1.0 Originator <'.$from.'> OK');
  183. }
  184. function _cmd_rcpt($argv) {
  185. if (is_null($this->helo)) {
  186. $this->sendMsg('503 Sorry man, can\'t let you send mails without EHLO/HELO first');
  187. return;
  188. }
  189. $nargv = 2;
  190. if (strtoupper($argv[1]) != 'TO:') {
  191. if (strtoupper(substr($argv[1], 0, 3)) == 'TO:') {
  192. $from = substr($argv[1], 3);
  193. } else {
  194. $this->sendMsg('550 Invalid syntax. Expected RCPT TO: <user@domain.tld>');
  195. return;
  196. }
  197. } else {
  198. $from = $argv[$nargv++];
  199. }
  200. // in theory, addr should be in < > (still, we won't require it)
  201. if (($from[0] == '<') && (substr($from, -1) == '>')) {
  202. $from = substr($from, 1, -1);
  203. }
  204. // TODO: we might have stuff in next argv
  205. if (!$this->txn->addTarget($from)) {
  206. $this->sendMsg($this->txn->errorMsg());
  207. return;
  208. }
  209. $this->sendMsg('250 2.5.0 Target <'.$from.'> OK');
  210. }
  211. function _cmd_data() {
  212. // pipelining -> check that buffer is EMPTY (if not, this may be a HTTP-proxy-attack)
  213. if ($this->buf !== false) {
  214. var_dump($this->buf);
  215. $this->sendMsg('550 Invalid use of pipelining, you can\'t pipeline a mail content');
  216. return;
  217. }
  218. $this->dataTxn = $this->txn->sendMail();
  219. if (!is_array($this->dataTxn)) {
  220. $this->sendMsg('450 Internal error, please try again later');
  221. return;
  222. }
  223. $this->sendMsg('354 Enter message, ending with "." on a line by itself (CR/LF)');
  224. $this->dataMode = true;
  225. }
  226. function parseDataLine($lin) {
  227. // we got one line of data
  228. // check line ending
  229. if (substr($lin, -2) != "\r\n") {
  230. $this->txn->cancelMail();
  231. $this->sendMsg('550 Sorry, you are supposed to end lines with <CR><LF>, which seems to not be the case right now');
  232. $this->dataMode = false;
  233. return;
  234. }
  235. if (rtrim($lin) != '.') { // only one dot in one line ?
  236. if (($lin[0] == '.') && ($lin[1] == '.')) $lin = substr($lin, 1); // strip trailing dots
  237. fputs($this->dataTxn['fd'], $lin); // still have its linebreak
  238. return;
  239. }
  240. // got whole mail, it's time for checks!
  241. $this->dataMode = false;
  242. $config = $this->IPC->getSmtpConfig();
  243. $maxsize = ($config['MaxMailSize']?:100)*1024*1024;
  244. if (ftell($this->dataTxn['fd']) > $maxsize) {
  245. $this->txn->reset();
  246. $this->dataTxn = false;
  247. $this->sendMsg('550 5.5.0 Mail too large for this system');
  248. return;
  249. }
  250. if (!$this->txn->finishMail($this->IPC)) { // failed at sending the mail? :(
  251. $this->sendMsg($this->txn->errorMsg());
  252. $this->txn->reset();
  253. return;
  254. }
  255. $this->txn->reset();
  256. $this->sendMsg('250 2.0.0 OK have a nice day');
  257. }
  258. function _cmd_txlg() {
  259. $list = $this->txn->transmitLog();
  260. foreach($list as $mail => $err) {
  261. if (is_null($err)) $err = '250 2.0.0 OK have a nice day';
  262. $this->sendMsg('250-<'.$mail.'>: '.$err);
  263. }
  264. $this->sendMsg('250 End of list');
  265. }
  266. protected function parseLine($lin) {
  267. if ($this->dataMode)
  268. return $this->parseDataLine($lin);
  269. return parent::parseLine($lin);
  270. }
  271. public function close() {
  272. if ($this->dataMode) {
  273. $this->txn->cancelMail();
  274. $this->dataMode = false;
  275. }
  276. return parent::close();
  277. }
  278. }