MailSendController.php 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. <?php
  2. namespace Controllers;
  3. use Libs\ExecLib;
  4. use Libs\ResponseLib;
  5. use Psr\Http\Message\ServerRequestInterface;
  6. use function React\Async\await;
  7. class MailSendController
  8. {
  9. public function __invoke(ServerRequestInterface $request)
  10. {
  11. $body = json_decode((string) $request->getBody(), true);
  12. if (!is_array($body)) {
  13. return ResponseLib::sendFail('Invalid JSON body', [], 'E_VALIDATE')->withStatus(400);
  14. }
  15. $templateName = trim((string)($body['mail_template'] ?? ''));
  16. $config = $body['mail_config'] ?? null;
  17. if ($templateName === '' || !is_array($config)) {
  18. return ResponseLib::sendFail('Missing mail_template or mail_config', [], 'E_VALIDATE')->withStatus(400);
  19. }
  20. // Required fields for sending
  21. $toEmail = trim((string)($config['to_email'] ?? ''));
  22. $toName = trim((string)($config['to_name'] ?? ''));
  23. $subject = trim((string)($config['subject'] ?? ''));
  24. if ($toEmail === '' || $subject === '') {
  25. return ResponseLib::sendFail('Missing to_email or subject', [], 'E_VALIDATE')->withStatus(400);
  26. }
  27. $fromEmail = trim((string)($_ENV['MAIL_FROM'] ?? 'no-reply@example.com'));
  28. $fromName = trim((string)($_ENV['MAIL_FROM_NAME'] ?? 'No Reply'));
  29. // Load template file from templates/{name}.tpl
  30. $tplPath = dirname(__DIR__) . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . $templateName . '.tpl';
  31. if (!is_file($tplPath)) {
  32. return ResponseLib::sendFail('Template not found', ['template' => $templateName], 'E_NOT_FOUND')->withStatus(404);
  33. }
  34. $tpl = file_get_contents($tplPath);
  35. if ($tpl === false) {
  36. return ResponseLib::sendFail('Failed to load template', ['template' => $templateName], 'E_IO')->withStatus(500);
  37. }
  38. // Simple placeholder substitution: {{key}}
  39. $replacements = [];
  40. foreach ($config as $k => $v) {
  41. if (is_scalar($v)) {
  42. $replacements['{{' . $k . '}}'] = (string)$v;
  43. }
  44. }
  45. $bodyText = strtr($tpl, $replacements);
  46. // Build RFC 5322 message
  47. $headers = [];
  48. $headers[] = sprintf('From: %s <%s>', $fromName !== '' ? $fromName : $fromEmail, $fromEmail);
  49. $headers[] = sprintf('To: %s <%s>', $toName !== '' ? $toName : $toEmail, $toEmail);
  50. $headers[] = sprintf('Subject: %s', $subject);
  51. $headers[] = 'MIME-Version: 1.0';
  52. $headers[] = 'Content-Type: text/plain; charset=UTF-8';
  53. $headers[] = 'Content-Transfer-Encoding: 8bit';
  54. $message = implode("\r\n", $headers) . "\r\n\r\n" . $bodyText . "\r\n";
  55. // Write to temp file
  56. $tmpFile = tempnam(sys_get_temp_dir(), 'mail_');
  57. if ($tmpFile === false) {
  58. return ResponseLib::sendFail('Failed to create temp file', [], 'E_IO')->withStatus(500);
  59. }
  60. file_put_contents($tmpFile, $message);
  61. // Execute sendmail (postfix)
  62. $sendmail = $_ENV['SENDMAIL_PATH'] ?? '/usr/sbin/sendmail';
  63. $cmd = sprintf("%s -t -i < %s", escapeshellcmd($sendmail), escapeshellarg($tmpFile));
  64. try {
  65. [$code, $output] = await(ExecLib::exec($cmd, (float)($_ENV['SENDMAIL_TIMEOUT'] ?? 10)));
  66. } catch (\Throwable $e) {
  67. @unlink($tmpFile);
  68. return ResponseLib::sendFail('Sendmail failed', ['error' => $e->getMessage()], 'E_SENDMAIL')->withStatus(500);
  69. } finally {
  70. @unlink($tmpFile);
  71. }
  72. if ($code !== 0) {
  73. return ResponseLib::sendFail('Sendmail exited with non-zero', ['code' => $code, 'output' => $output], 'E_SENDMAIL')->withStatus(500);
  74. }
  75. return ResponseLib::sendOk([
  76. 'status' => 'sent',
  77. 'to' => $toEmail,
  78. 'template' => $templateName,
  79. 'code' => $code,
  80. ]);
  81. }
  82. }