├── AjaxForm.js
├── AjaxForm.php
├── PHPMailer
├── Exception.php
├── PHPMailer.php
└── SMTP.php
├── README.md
├── email_template.php
└── index.html
/AjaxForm.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Form Validation: https://getbootstrap.com/docs/5.3/forms/validation
3 | * FormData API: https://developer.mozilla.org/en-US/docs/Web/API/FormData
4 | * Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
5 | * reCaptcha v3: https://developers.google.com/recaptcha/docs/v3
6 | *
7 | * Author: Raspgot
8 | */
9 |
10 | const RECAPTCHA_SITE_KEY = 'YOUR_RECAPTCHA_SITE_KEY'; // Replace with your reCAPTCHA public key
11 |
12 | document.addEventListener('DOMContentLoaded', function () {
13 | 'use strict';
14 |
15 | // Select the form element with Bootstrap validation class
16 | const form = document.querySelector('.needs-validation');
17 | if (!form) return; // Exit if no form found
18 |
19 | // Select DOM elements: spinner, submit button and alert container
20 | const spinner = document.getElementById('loading-spinner');
21 | const submitButton = form.querySelector('button[type="submit"]');
22 | const alertContainer = document.getElementById('alert-status');
23 |
24 | // Add custom submit event listener to the form
25 | form.addEventListener('submit', function (event) {
26 | event.preventDefault(); // Prevent default form submission
27 | event.stopPropagation(); // Stop event from bubbling up
28 |
29 | // Add Bootstrap class to show validation feedback
30 | form.classList.add('was-validated');
31 |
32 | // If the form is invalid, focus the first invalid field and stop
33 | if (!form.checkValidity()) {
34 | const firstInvalidField = form.querySelector(':invalid');
35 | if (firstInvalidField) firstInvalidField.focus();
36 | return;
37 | }
38 |
39 | // Show loading spinner and disable submit button
40 | spinner.classList.remove('d-none');
41 | submitButton.disabled = true;
42 |
43 | // Wait for reCaptcha to be ready
44 | grecaptcha.ready(function () {
45 | // Execute reCaptcha v3 to get the token
46 | grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: 'submit' }).then(function (token) {
47 | // Create FormData object from form fields
48 | const formData = new FormData(form);
49 | // Append the reCaptcha token to form data
50 | formData.append('recaptcha_token', token);
51 |
52 | // Send the form data to the server using Fetch API
53 | fetch('AjaxForm.php', {
54 | method: 'POST',
55 | body: formData,
56 | })
57 | .then(function (response) {
58 | // Handle HTTP-level errors
59 | if (!response.ok) {
60 | throw new Error('Network error: ' + response.status);
61 | }
62 | return response.json(); // Parse the response as JSON
63 | })
64 | .then(function (result) {
65 | // Compose the alert message from result
66 | const message = result.detail ? result.message + ' ' + result.detail : result.message;
67 | const alertType = result.success ? 'success' : 'danger'; // Set alert class based on status
68 |
69 | // Show alert with response message
70 | alertContainer.className = 'alert alert-' + alertType;
71 | alertContainer.textContent = message;
72 | alertContainer.classList.remove('d-none');
73 | alertContainer.scrollIntoView({ behavior: 'smooth' });
74 |
75 | // If the form submission was successful, reset the form
76 | if (result.success) {
77 | form.reset();
78 | form.classList.remove('was-validated');
79 | }
80 | })
81 | .catch(function (error) {
82 | // Log any network or parsing errors
83 | console.error('An error occurred:', error);
84 | })
85 | .finally(function () {
86 | // Hide spinner and re-enable submit button
87 | spinner.classList.add('d-none');
88 | submitButton.disabled = false;
89 | });
90 | });
91 | });
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/AjaxForm.php:
--------------------------------------------------------------------------------
1 | '✉️ Your message has been sent !',
40 | 'enter_name' => '⚠️ Please enter your name.',
41 | 'enter_email' => '⚠️ Please enter a valid email.',
42 | 'enter_message' => '⚠️ Please enter your message.',
43 | 'token_error' => '⚠️ No reCAPTCHA token received.',
44 | 'domain_error' => '⚠️ The email domain is invalid.',
45 | 'method_error' => '⚠️ Method not allowed.',
46 | 'constant_error' => '⚠️ Missing configuration constants.',
47 | 'honeypot_error' => '🚫 Spam detected.',
48 | ];
49 |
50 | // Ensure all necessary constants are set
51 | if (empty(SECRET_KEY) || empty(SMTP_HOST) || empty(SMTP_USERNAME) || empty(SMTP_PASSWORD)) {
52 | respond(false, EMAIL_MESSAGES['constant_error']);
53 | }
54 |
55 | // Allow only POST requests (reject GET or others)
56 | if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
57 | respond(false, EMAIL_MESSAGES['method_error']);
58 | }
59 |
60 | // Simple bot detection based on the user-agent string
61 | if (empty($_SERVER['HTTP_USER_AGENT']) || preg_match('/\b(curl|wget|httpie|python-requests|httpclient|bot|spider|crawler|scrapy)\b/i', $_SERVER['HTTP_USER_AGENT'])) {
62 | respond(false, EMAIL_MESSAGES['honeypot_error']);
63 | }
64 |
65 | // Gather and validate user input from POST data
66 | $date = new DateTime();
67 | $ip = $_SERVER['REMOTE_ADDR'] ?? 'Unknown';
68 | $email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL) ?: respond(false, EMAIL_MESSAGES['enter_email']);
69 | $name = isset($_POST['name']) ? sanitize($_POST['name']) : respond(false, EMAIL_MESSAGES['enter_name']);
70 | $message = isset($_POST['message']) ? sanitize($_POST['message']) : respond(false, EMAIL_MESSAGES['enter_message']);
71 | $token = isset($_POST['recaptcha_token']) ? sanitize($_POST['recaptcha_token']) : respond(false, EMAIL_MESSAGES['token_error']);
72 | $honeypot = isset($_POST['website']) ? trim($_POST['website']) : '';
73 | if (!empty($honeypot)) {
74 | respond(false, EMAIL_MESSAGES['honeypot_error']);
75 | }
76 |
77 | // Check if the email domain is valid (DNS records)
78 | $domain = substr(strrchr($email, "@"), 1);
79 | if (!checkdnsrr($domain, "MX") && !checkdnsrr($domain, "A")) {
80 | respond(false, EMAIL_MESSAGES['domain_error']);
81 | }
82 |
83 | // Validate the reCAPTCHA token with Google
84 | validateRecaptcha($token);
85 |
86 | // Construct the HTML email content
87 | $email_body = render_email([
88 | 'subject' => EMAIL_SUBJECT,
89 | 'date' => $date->format('m/d/Y H:i:s'),
90 | 'name' => $name,
91 | 'email' => $email,
92 | 'message' => nl2br($message),
93 | 'ip' => $ip,
94 | ]);
95 |
96 | // Send the email using PHPMailer
97 | $mail = new PHPMailer(true);
98 |
99 | try {
100 | // SMTP configuration
101 | $mail->isSMTP();
102 | $mail->Host = SMTP_HOST;
103 | $mail->SMTPAuth = SMTP_AUTH;
104 | $mail->Username = SMTP_USERNAME;
105 | $mail->Password = SMTP_PASSWORD;
106 | $mail->SMTPSecure = SMTP_SECURE;
107 | $mail->Port = SMTP_PORT;
108 |
109 | // Set sender and recipient
110 | $mail->setFrom(SMTP_USERNAME, FROM_NAME);
111 | $mail->Sender = SMTP_USERNAME;
112 | $mail->addAddress($email, $name);
113 | $mail->addCC(SMTP_USERNAME, 'Admin');
114 | $mail->addReplyTo($email, $name);
115 |
116 | // Email content
117 | $mail->isHTML(true);
118 | $mail->CharSet = 'UTF-8';
119 | $mail->Subject = EMAIL_SUBJECT;
120 | $mail->Body = $email_body;
121 | $mail->AltBody = strip_tags($email_body);
122 |
123 | // Attempt to send email
124 | $mail->send();
125 | respond(true, EMAIL_MESSAGES['success']);
126 | } catch (Exception $e) {
127 | // Catch errors and return the message
128 | respond(false, '❌ ' . $e->getMessage());
129 | }
130 |
131 | /**
132 | * Validate the reCAPTCHA token via Google's API
133 | *
134 | * @param string $token reCAPTCHA token received from frontend
135 | */
136 | function validateRecaptcha(string $token): void
137 | {
138 | $ch = curl_init('https://www.google.com/recaptcha/api/siteverify');
139 | curl_setopt_array($ch, [
140 | CURLOPT_POST => true,
141 | CURLOPT_POSTFIELDS => http_build_query([
142 | 'secret' => SECRET_KEY,
143 | 'response' => $token
144 | ]),
145 | CURLOPT_RETURNTRANSFER => true,
146 | CURLOPT_TIMEOUT => 10,
147 | ]);
148 |
149 | $response = curl_exec($ch);
150 | $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
151 | $curl_error = curl_error($ch);
152 | curl_close($ch);
153 |
154 | if ($response === false || $http_code !== 200) {
155 | respond(false, '❌ Error during the Google reCAPTCHA request : ' . ($curl_error ?: "HTTP $http_code"));
156 | }
157 |
158 | $data = json_decode($response, true);
159 |
160 | if (empty($data['success'])) {
161 | respond(false, '❌ reCAPTCHA validation failed : ', $data['error-codes'] ?? []);
162 | }
163 |
164 | // Reject if score is too low (likely a bot)
165 | if (isset($data['score']) && $data['score'] < 0.5) {
166 | respond(false, '❌ reCAPTCHA score too low. You might be a robot 🤖');
167 | }
168 | }
169 |
170 | /**
171 | * Sanitize input to prevent header injection, XSS, and control characters
172 | *
173 | * @param string $data Raw input string
174 | * @return string Sanitized and safe string
175 | */
176 | function sanitize(string $data): string
177 | {
178 | // Remove null bytes and other control characters (except \t)
179 | $data = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u', '', $data);
180 |
181 | // Escape HTML entities (with strict quote handling and UTF-8 safety)
182 | return trim(htmlspecialchars($data, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8', true));
183 | }
184 |
185 | /**
186 | * Send a JSON response and stop script execution
187 | *
188 | * @param bool $success Success status
189 | * @param string $message Message to send
190 | * @param mixed $detail Optional additional data
191 | */
192 | function respond(bool $success, string $message, mixed $detail = null): void
193 | {
194 | echo json_encode([
195 | 'success' => $success,
196 | 'message' => $message,
197 | 'detail' => $detail,
198 | ]);
199 | exit;
200 | }
201 |
202 | /**
203 | * Generates the HTML email content using a PHP template and provided data.
204 | *
205 | * @param array $data Array containing the required variables for the template
206 | * @return string The complete rendered HTML content
207 | */
208 | function render_email(array $data): string
209 | {
210 | // Path to the email template (adjustable if needed)
211 | $templateFile = __DIR__ . '/email_template.php';
212 |
213 | if (!is_file($templateFile)) {
214 | throw new RuntimeException("Template file not found: $templateFile");
215 | }
216 |
217 | // Encapsulate in a local scope to avoid variable pollution
218 | return (function () use ($data, $templateFile): string {
219 | extract($data, EXTR_SKIP); // convert array keys into local variables
220 | ob_start();
221 | require $templateFile;
222 | return ob_get_clean();
223 | })();
224 | }
225 |
--------------------------------------------------------------------------------
/PHPMailer/Exception.php:
--------------------------------------------------------------------------------
1 |
10 | * @author Jim Jagielski (jimjag)
\n";
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/PHPMailer/SMTP.php:
--------------------------------------------------------------------------------
1 |
10 | * @author Jim Jagielski (jimjag)
`, appropriate for browser output
134 | * * `error_log` Output to error log as configured in php.ini
135 | * Alternatively, you can provide a callable expecting two params: a message string and the debug level:
136 | *
137 | * ```php
138 | * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";};
139 | * ```
140 | *
141 | * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug`
142 | * level output is used:
143 | *
144 | * ```php
145 | * $mail->Debugoutput = new myPsr3Logger;
146 | * ```
147 | *
148 | * @var string|callable|\Psr\Log\LoggerInterface
149 | */
150 | public $Debugoutput = 'echo';
151 |
152 | /**
153 | * Whether to use VERP.
154 | *
155 | * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path
156 | * @see https://www.postfix.org/VERP_README.html Info on VERP
157 | *
158 | * @var bool
159 | */
160 | public $do_verp = false;
161 |
162 | /**
163 | * Whether to use SMTPUTF8.
164 | *
165 | * @see https://www.rfc-editor.org/rfc/rfc6531
166 | *
167 | * @var bool
168 | */
169 | public $do_smtputf8 = false;
170 |
171 | /**
172 | * The timeout value for connection, in seconds.
173 | * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
174 | * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure.
175 | *
176 | * @see https://www.rfc-editor.org/rfc/rfc2821#section-4.5.3.2
177 | *
178 | * @var int
179 | */
180 | public $Timeout = 300;
181 |
182 | /**
183 | * How long to wait for commands to complete, in seconds.
184 | * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2.
185 | *
186 | * @var int
187 | */
188 | public $Timelimit = 300;
189 |
190 | /**
191 | * Patterns to extract an SMTP transaction id from reply to a DATA command.
192 | * The first capture group in each regex will be used as the ID.
193 | * MS ESMTP returns the message ID, which may not be correct for internal tracking.
194 | *
195 | * @var string[]
196 | */
197 | protected $smtp_transaction_id_patterns = [
198 | 'exim' => '/[\d]{3} OK id=(.*)/',
199 | 'sendmail' => '/[\d]{3} 2\.0\.0 (.*) Message/',
200 | 'postfix' => '/[\d]{3} 2\.0\.0 Ok: queued as (.*)/',
201 | 'Microsoft_ESMTP' => '/[0-9]{3} 2\.[\d]\.0 (.*)@(?:.*) Queued mail for delivery/',
202 | 'Amazon_SES' => '/[\d]{3} Ok (.*)/',
203 | 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/',
204 | 'CampaignMonitor' => '/[\d]{3} 2\.0\.0 OK:([a-zA-Z\d]{48})/',
205 | 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/',
206 | 'ZoneMTA' => '/[\d]{3} Message queued as (.*)/',
207 | 'Mailjet' => '/[\d]{3} OK queued as (.*)/',
208 | ];
209 |
210 | /**
211 | * Allowed SMTP XCLIENT attributes.
212 | * Must be allowed by the SMTP server. EHLO response is not checked.
213 | *
214 | * @see https://www.postfix.org/XCLIENT_README.html
215 | *
216 | * @var array
217 | */
218 | public static $xclient_allowed_attributes = [
219 | 'NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'LOGIN', 'DESTADDR', 'DESTPORT'
220 | ];
221 |
222 | /**
223 | * The last transaction ID issued in response to a DATA command,
224 | * if one was detected.
225 | *
226 | * @var string|bool|null
227 | */
228 | protected $last_smtp_transaction_id;
229 |
230 | /**
231 | * The socket for the server connection.
232 | *
233 | * @var ?resource
234 | */
235 | protected $smtp_conn;
236 |
237 | /**
238 | * Error information, if any, for the last SMTP command.
239 | *
240 | * @var array
241 | */
242 | protected $error = [
243 | 'error' => '',
244 | 'detail' => '',
245 | 'smtp_code' => '',
246 | 'smtp_code_ex' => '',
247 | ];
248 |
249 | /**
250 | * The reply the server sent to us for HELO.
251 | * If null, no HELO string has yet been received.
252 | *
253 | * @var string|null
254 | */
255 | protected $helo_rply;
256 |
257 | /**
258 | * The set of SMTP extensions sent in reply to EHLO command.
259 | * Indexes of the array are extension names.
260 | * Value at index 'HELO' or 'EHLO' (according to command that was sent)
261 | * represents the server name. In case of HELO it is the only element of the array.
262 | * Other values can be boolean TRUE or an array containing extension options.
263 | * If null, no HELO/EHLO string has yet been received.
264 | *
265 | * @var array|null
266 | */
267 | protected $server_caps;
268 |
269 | /**
270 | * The most recent reply received from the server.
271 | *
272 | * @var string
273 | */
274 | protected $last_reply = '';
275 |
276 | /**
277 | * Output debugging info via a user-selected method.
278 | *
279 | * @param string $str Debug string to output
280 | * @param int $level The debug level of this message; see DEBUG_* constants
281 | *
282 | * @see SMTP::$Debugoutput
283 | * @see SMTP::$do_debug
284 | */
285 | protected function edebug($str, $level = 0)
286 | {
287 | if ($level > $this->do_debug) {
288 | return;
289 | }
290 | //Is this a PSR-3 logger?
291 | if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) {
292 | //Remove trailing line breaks potentially added by calls to SMTP::client_send()
293 | $this->Debugoutput->debug(rtrim($str, "\r\n"));
294 |
295 | return;
296 | }
297 | //Avoid clash with built-in function names
298 | if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) {
299 | call_user_func($this->Debugoutput, $str, $level);
300 |
301 | return;
302 | }
303 | switch ($this->Debugoutput) {
304 | case 'error_log':
305 | //Don't output, just log
306 | /** @noinspection ForgottenDebugOutputInspection */
307 | error_log($str);
308 | break;
309 | case 'html':
310 | //Cleans up output a bit for a better looking, HTML-safe output
311 | echo gmdate('Y-m-d H:i:s'), ' ', htmlentities(
312 | preg_replace('/[\r\n]+/', '', $str),
313 | ENT_QUOTES,
314 | 'UTF-8'
315 | ), "
\n";
316 | break;
317 | case 'echo':
318 | default:
319 | //Normalize line breaks
320 | $str = preg_replace('/\r\n|\r/m', "\n", $str);
321 | echo gmdate('Y-m-d H:i:s'),
322 | "\t",
323 | //Trim trailing space
324 | trim(
325 | //Indent for readability, except for trailing break
326 | str_replace(
327 | "\n",
328 | "\n \t ",
329 | trim($str)
330 | )
331 | ),
332 | "\n";
333 | }
334 | }
335 |
336 | /**
337 | * Connect to an SMTP server.
338 | *
339 | * @param string $host SMTP server IP or host name
340 | * @param int $port The port number to connect to
341 | * @param int $timeout How long to wait for the connection to open
342 | * @param array $options An array of options for stream_context_create()
343 | *
344 | * @return bool
345 | */
346 | public function connect($host, $port = null, $timeout = 30, $options = [])
347 | {
348 | //Clear errors to avoid confusion
349 | $this->setError('');
350 | //Make sure we are __not__ connected
351 | if ($this->connected()) {
352 | //Already connected, generate error
353 | $this->setError('Already connected to a server');
354 |
355 | return false;
356 | }
357 | if (empty($port)) {
358 | $port = self::DEFAULT_PORT;
359 | }
360 | //Connect to the SMTP server
361 | $this->edebug(
362 | "Connection: opening to $host:$port, timeout=$timeout, options=" .
363 | (count($options) > 0 ? var_export($options, true) : 'array()'),
364 | self::DEBUG_CONNECTION
365 | );
366 |
367 | $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options);
368 |
369 | if ($this->smtp_conn === false) {
370 | //Error info already set inside `getSMTPConnection()`
371 | return false;
372 | }
373 |
374 | $this->edebug('Connection: opened', self::DEBUG_CONNECTION);
375 |
376 | //Get any announcement
377 | $this->last_reply = $this->get_lines();
378 | $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER);
379 | $responseCode = (int)substr($this->last_reply, 0, 3);
380 | if ($responseCode === 220) {
381 | return true;
382 | }
383 | //Anything other than a 220 response means something went wrong
384 | //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error
385 | //https://www.rfc-editor.org/rfc/rfc5321#section-3.1
386 | if ($responseCode === 554) {
387 | $this->quit();
388 | }
389 | //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down)
390 | $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION);
391 | $this->close();
392 | return false;
393 | }
394 |
395 | /**
396 | * Create connection to the SMTP server.
397 | *
398 | * @param string $host SMTP server IP or host name
399 | * @param int $port The port number to connect to
400 | * @param int $timeout How long to wait for the connection to open
401 | * @param array $options An array of options for stream_context_create()
402 | *
403 | * @return false|resource
404 | */
405 | protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = [])
406 | {
407 | static $streamok;
408 | //This is enabled by default since 5.0.0 but some providers disable it
409 | //Check this once and cache the result
410 | if (null === $streamok) {
411 | $streamok = function_exists('stream_socket_client');
412 | }
413 |
414 | $errno = 0;
415 | $errstr = '';
416 | if ($streamok) {
417 | $socket_context = stream_context_create($options);
418 | set_error_handler(function () {
419 | call_user_func_array([$this, 'errorHandler'], func_get_args());
420 | });
421 | $connection = stream_socket_client(
422 | $host . ':' . $port,
423 | $errno,
424 | $errstr,
425 | $timeout,
426 | STREAM_CLIENT_CONNECT,
427 | $socket_context
428 | );
429 | } else {
430 | //Fall back to fsockopen which should work in more places, but is missing some features
431 | $this->edebug(
432 | 'Connection: stream_socket_client not available, falling back to fsockopen',
433 | self::DEBUG_CONNECTION
434 | );
435 | set_error_handler(function () {
436 | call_user_func_array([$this, 'errorHandler'], func_get_args());
437 | });
438 | $connection = fsockopen(
439 | $host,
440 | $port,
441 | $errno,
442 | $errstr,
443 | $timeout
444 | );
445 | }
446 | restore_error_handler();
447 |
448 | //Verify we connected properly
449 | if (!is_resource($connection)) {
450 | $this->setError(
451 | 'Failed to connect to server',
452 | '',
453 | (string) $errno,
454 | $errstr
455 | );
456 | $this->edebug(
457 | 'SMTP ERROR: ' . $this->error['error']
458 | . ": $errstr ($errno)",
459 | self::DEBUG_CLIENT
460 | );
461 |
462 | return false;
463 | }
464 |
465 | //SMTP server can take longer to respond, give longer timeout for first read
466 | //Windows does not have support for this timeout function
467 | if (strpos(PHP_OS, 'WIN') !== 0) {
468 | $max = (int)ini_get('max_execution_time');
469 | //Don't bother if unlimited, or if set_time_limit is disabled
470 | if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) {
471 | @set_time_limit($timeout);
472 | }
473 | stream_set_timeout($connection, $timeout, 0);
474 | }
475 |
476 | return $connection;
477 | }
478 |
479 | /**
480 | * Initiate a TLS (encrypted) session.
481 | *
482 | * @return bool
483 | */
484 | public function startTLS()
485 | {
486 | if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) {
487 | return false;
488 | }
489 |
490 | //Allow the best TLS version(s) we can
491 | $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
492 |
493 | //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT
494 | //so add them back in manually if we can
495 | if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
496 | $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
497 | $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
498 | }
499 |
500 | //Begin encrypted connection
501 | set_error_handler(function () {
502 | call_user_func_array([$this, 'errorHandler'], func_get_args());
503 | });
504 | $crypto_ok = stream_socket_enable_crypto(
505 | $this->smtp_conn,
506 | true,
507 | $crypto_method
508 | );
509 | restore_error_handler();
510 |
511 | return (bool) $crypto_ok;
512 | }
513 |
514 | /**
515 | * Perform SMTP authentication.
516 | * Must be run after hello().
517 | *
518 | * @see hello()
519 | *
520 | * @param string $username The user name
521 | * @param string $password The password
522 | * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2)
523 | * @param OAuthTokenProvider $OAuth An optional OAuthTokenProvider instance for XOAUTH2 authentication
524 | *
525 | * @return bool True if successfully authenticated
526 | */
527 | public function authenticate(
528 | $username,
529 | $password,
530 | $authtype = null,
531 | $OAuth = null
532 | ) {
533 | if (!$this->server_caps) {
534 | $this->setError('Authentication is not allowed before HELO/EHLO');
535 |
536 | return false;
537 | }
538 |
539 | if (array_key_exists('EHLO', $this->server_caps)) {
540 | //SMTP extensions are available; try to find a proper authentication method
541 | if (!array_key_exists('AUTH', $this->server_caps)) {
542 | $this->setError('Authentication is not allowed at this stage');
543 | //'at this stage' means that auth may be allowed after the stage changes
544 | //e.g. after STARTTLS
545 |
546 | return false;
547 | }
548 |
549 | $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL);
550 | $this->edebug(
551 | 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']),
552 | self::DEBUG_LOWLEVEL
553 | );
554 |
555 | //If we have requested a specific auth type, check the server supports it before trying others
556 | if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) {
557 | $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL);
558 | $authtype = null;
559 | }
560 |
561 | if (empty($authtype)) {
562 | //If no auth mechanism is specified, attempt to use these, in this order
563 | //Try CRAM-MD5 first as it's more secure than the others
564 | foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) {
565 | if (in_array($method, $this->server_caps['AUTH'], true)) {
566 | $authtype = $method;
567 | break;
568 | }
569 | }
570 | if (empty($authtype)) {
571 | $this->setError('No supported authentication methods found');
572 |
573 | return false;
574 | }
575 | $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL);
576 | }
577 |
578 | if (!in_array($authtype, $this->server_caps['AUTH'], true)) {
579 | $this->setError("The requested authentication method \"$authtype\" is not supported by the server");
580 |
581 | return false;
582 | }
583 | } elseif (empty($authtype)) {
584 | $authtype = 'LOGIN';
585 | }
586 | switch ($authtype) {
587 | case 'PLAIN':
588 | //Start authentication
589 | if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) {
590 | return false;
591 | }
592 | //Send encoded username and password
593 | if (
594 | //Format from https://www.rfc-editor.org/rfc/rfc4616#section-2
595 | //We skip the first field (it's forgery), so the string starts with a null byte
596 | !$this->sendCommand(
597 | 'User & Password',
598 | base64_encode("\0" . $username . "\0" . $password),
599 | 235
600 | )
601 | ) {
602 | return false;
603 | }
604 | break;
605 | case 'LOGIN':
606 | //Start authentication
607 | if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) {
608 | return false;
609 | }
610 | if (!$this->sendCommand('Username', base64_encode($username), 334)) {
611 | return false;
612 | }
613 | if (!$this->sendCommand('Password', base64_encode($password), 235)) {
614 | return false;
615 | }
616 | break;
617 | case 'CRAM-MD5':
618 | //Start authentication
619 | if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) {
620 | return false;
621 | }
622 | //Get the challenge
623 | $challenge = base64_decode(substr($this->last_reply, 4));
624 |
625 | //Build the response
626 | $response = $username . ' ' . $this->hmac($challenge, $password);
627 |
628 | //send encoded credentials
629 | return $this->sendCommand('Username', base64_encode($response), 235);
630 | case 'XOAUTH2':
631 | //The OAuth instance must be set up prior to requesting auth.
632 | if (null === $OAuth) {
633 | return false;
634 | }
635 | $oauth = $OAuth->getOauth64();
636 |
637 | //Start authentication
638 | if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) {
639 | return false;
640 | }
641 | break;
642 | default:
643 | $this->setError("Authentication method \"$authtype\" is not supported");
644 |
645 | return false;
646 | }
647 |
648 | return true;
649 | }
650 |
651 | /**
652 | * Calculate an MD5 HMAC hash.
653 | * Works like hash_hmac('md5', $data, $key)
654 | * in case that function is not available.
655 | *
656 | * @param string $data The data to hash
657 | * @param string $key The key to hash with
658 | *
659 | * @return string
660 | */
661 | protected function hmac($data, $key)
662 | {
663 | if (function_exists('hash_hmac')) {
664 | return hash_hmac('md5', $data, $key);
665 | }
666 |
667 | //The following borrowed from
668 | //https://www.php.net/manual/en/function.mhash.php#27225
669 |
670 | //RFC 2104 HMAC implementation for php.
671 | //Creates an md5 HMAC.
672 | //Eliminates the need to install mhash to compute a HMAC
673 | //by Lance Rushing
674 |
675 | $bytelen = 64; //byte length for md5
676 | if (strlen($key) > $bytelen) {
677 | $key = pack('H*', md5($key));
678 | }
679 | $key = str_pad($key, $bytelen, chr(0x00));
680 | $ipad = str_pad('', $bytelen, chr(0x36));
681 | $opad = str_pad('', $bytelen, chr(0x5c));
682 | $k_ipad = $key ^ $ipad;
683 | $k_opad = $key ^ $opad;
684 |
685 | return md5($k_opad . pack('H*', md5($k_ipad . $data)));
686 | }
687 |
688 | /**
689 | * Check connection state.
690 | *
691 | * @return bool True if connected
692 | */
693 | public function connected()
694 | {
695 | if (is_resource($this->smtp_conn)) {
696 | $sock_status = stream_get_meta_data($this->smtp_conn);
697 | if ($sock_status['eof']) {
698 | //The socket is valid but we are not connected
699 | $this->edebug(
700 | 'SMTP NOTICE: EOF caught while checking if connected',
701 | self::DEBUG_CLIENT
702 | );
703 | $this->close();
704 |
705 | return false;
706 | }
707 |
708 | return true; //everything looks good
709 | }
710 |
711 | return false;
712 | }
713 |
714 | /**
715 | * Close the socket and clean up the state of the class.
716 | * Don't use this function without first trying to use QUIT.
717 | *
718 | * @see quit()
719 | */
720 | public function close()
721 | {
722 | $this->server_caps = null;
723 | $this->helo_rply = null;
724 | if (is_resource($this->smtp_conn)) {
725 | //Close the connection and cleanup
726 | fclose($this->smtp_conn);
727 | $this->smtp_conn = null; //Makes for cleaner serialization
728 | $this->edebug('Connection: closed', self::DEBUG_CONNECTION);
729 | }
730 | }
731 |
732 | /**
733 | * Send an SMTP DATA command.
734 | * Issues a data command and sends the msg_data to the server,
735 | * finalizing the mail transaction. $msg_data is the message
736 | * that is to be sent with the headers. Each header needs to be
737 | * on a single line followed by a
17 |
|
57 |
`: 92 | 93 | ```html 94 | 95 | ``` 96 | 97 | --- 98 | 99 | ## 🛠️ Customization 100 | 101 | ### ✏️ Change validation messages 102 | 103 | Edit them directly in the HTML: 104 | 105 | ```html 106 |
107 |
108 | ``` 109 | 110 | ### ➕ Add new fields 111 | 112 | To add fields (e.g. subject or phone): 113 | 114 | **1.** Add the field in `index.html`: 115 | 116 | ```html 117 | 118 | ``` 119 | 120 | **2.** Handle it in `AjaxForm.php`: 121 | 122 | ```php 123 | $subject = sanitize($_POST['subject']) ?? ''; 124 | ``` 125 | 126 | **3.** Include it in `email_template.php`: 127 | 128 | --- 129 | 130 | ## 🙌 Contributing 131 | 132 | Found a bug ? Have a suggestion ? Pull requests and feedback are welcome ! 133 | 134 | --- 135 | 136 | ## Author 137 | 138 |  139 | 140 | Developed with ❤️ by [**Raspgot**](https://raspgot.fr) 141 | 142 | If you find this project helpful, don't forget to ⭐ star the repo ! 143 | 144 | --- 145 | 146 | ## Dependencies 147 | 148 | - [PHPMailer](https://github.com/PHPMailer/PHPMailer) 149 | - [Bootstrap](https://github.com/twbs/bootstrap) 150 | -------------------------------------------------------------------------------- /email_template.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 | 8 | 9 | 10 |
60 | 61 |