11<?php
22
33/**
4- * Secure Contact Form using PHPMailer & reCAPTCHA v3 with autoreply
4+ * This script processes AJAX form submissions, validates input, applies
5+ * anti-spam measures (honeypot, rate limiting, DNS & reCAPTCHA checks), and
6+ * sends both an admin notification and an optional autoreply to the user
57 *
6- * @author Raspgot <contact@raspgot.fr>
7- * @link https://github.com/raspgot/AjaxForm-PHPMailer-reCAPTCHA
8- * @version 1.7.3
9- * @see https://github.com/PHPMailer/PHPMailer
10- * @see https://developers.google.com/recaptcha/docs/v3
8+ * @author Raspgot <contact@raspgot.fr>
9+ * @link https://github.com/raspgot/AjaxForm-PHPMailer-reCAPTCHA
10+ * @version 1.7.4
11+ * @see https://github.com/PHPMailer/PHPMailer
12+ * @see https://developers.google.com/recaptcha/docs/v3
1113 */
1214
1315declare (strict_types=1 );
101103// Verify reCAPTCHA token authenticity and score
102104validateRecaptcha ($ token );
103105
104- // Build email body from template
106+ // Build email body (HTML) from template
105107$ emailBody = renderEmail ([
106108 'subject ' => $ subject ,
107109 'date ' => $ date ->format ('Y-m-d H:i:s ' ),
112114]);
113115
114116try {
117+ // Build minimal plain text alternative part from HTML
118+ $ altText = buildAltBody ($ emailBody );
119+
115120 // Send notification email to site owner
116- $ mail = new PHPMailer (true );
117- configureMailer ($ mail );
121+ $ mail = configureMailer (new PHPMailer (true ));
118122 $ mail ->addAddress (SMTP_USERNAME , 'Admin ' );
119123 $ mail ->addReplyTo ($ email , $ name );
120124 $ mail ->Subject = $ subject ?: EMAIL_SUBJECT_DEFAULT ;
121125 $ mail ->Body = $ emailBody ;
122- $ alt = preg_replace ('/<br\s*\/?>/i ' , "\n" , $ emailBody ); // First convert <br> tags into \n (strip_tags removes <br> without adding line breaks)
123- $ alt = trim (strip_tags ($ alt ));
124- $ mail ->AltBody = $ alt ;
126+ $ mail ->AltBody = $ altText ;
125127 $ mail ->send ();
126128
127129 // Send autoreply confirmation to user
128- $ autoReply = new PHPMailer (true );
129- configureMailer ($ autoReply );
130+ $ autoReply = configureMailer (new PHPMailer (true ));
130131 $ autoReply ->addAddress ($ email , $ name );
131132 $ autoReply ->Subject = EMAIL_SUBJECT_AUTOREPLY . ' — ' . $ subject ;
132- $ autoReply ->Body = '
133- <p>Hello ' . htmlspecialchars ($ name ) . ',</p>
134- <p>Thank you for reaching out, here is a copy of your message :</p>
135- <hr> ' . $ emailBody ;
136- $ autoReply ->AltBody = $ alt ;
133+ $ autoReply ->Body = '<p>Hello ' . htmlspecialchars ($ name ) . ',</p> ' .
134+ '<p>Thank you for reaching out. Here is a copy of your message:</p> ' .
135+ '<hr> ' . $ emailBody ;
136+ $ autoReply ->AltBody = $ altText ;
137137 $ autoReply ->send ();
138138
139139 respond (true , RESPONSES ['success ' ]);
140140} catch (Exception $ e ) {
141141 respond (false , '❌ Mail error: ' . $ e ->getMessage (), 'email ' );
142142}
143143
144+ /**
145+ * Create a plain-text alternative body from an HTML email fragment
146+ *
147+ * @param string $html HTML email body
148+ * @return string Plain text version
149+ */
150+ function buildAltBody (string $ html ): string
151+ {
152+ $ text = preg_replace ('/<br\s*\/??>/i ' , "\n" , $ html ) ?? $ html ;
153+ $ text = strip_tags ($ text );
154+ return html_entity_decode ($ text , ENT_QUOTES | ENT_SUBSTITUTE , 'UTF-8 ' );
155+ }
156+
144157/**
145158 * Verify reCAPTCHA token with Google API and validate score, action, and hostname
146159 *
147- * @param string $token reCAPTCHA token submitted by the form
160+ * @param string $token The reCAPTCHA token received from the frontend
148161 * @return void
149162 */
150163function validateRecaptcha (string $ token ): void
@@ -215,24 +228,27 @@ function validateRecaptcha(string $token): void
215228/**
216229 * Sanitize user input to prevent XSS and header injection
217230 *
218- * @param string $data Raw input string
219- * @return string Cleaned string
231+ * @param string $data Raw user-supplied input
232+ * @return string Sanitized value
220233 */
221234function sanitize (string $ data ): string
222235{
223236 // Remove control characters and null bytes
224- $ data = preg_replace ('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u ' , '' , $ data );
237+ $ filtered = preg_replace ('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/u ' , '' , $ data );
238+ if ($ filtered === null ) {
239+ $ filtered = $ data ; // Fallback to original if regex engine fails
240+ }
225241
226242 // Escape HTML entities (UTF-8 safe)
227- return trim (htmlspecialchars ($ data , ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 , 'UTF-8 ' , true ));
243+ return trim (htmlspecialchars ($ filtered , ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 , 'UTF-8 ' , true ));
228244}
229245
230246/**
231247 * Send JSON response and terminate execution
232248 *
233249 * @param bool $success Success flag
234250 * @param string $message Message to display
235- * @param string|null $field Optional field to highlight as invalid
251+ * @param string|null $field Optional field to highlight as invalid
236252 * @return never
237253 */
238254function respond (bool $ success , string $ message , ?string $ field = null ): never
@@ -246,10 +262,22 @@ function respond(bool $success, string $message, ?string $field = null): never
246262}
247263
248264/**
249- * Render the email body from template file
265+ * Render the HTML email body using the external template file
266+ *
267+ * @param array{
268+ * subject: string,
269+ * date: string,
270+ * name: string,
271+ * email: string,
272+ * message: string,
273+ * ip: string
274+ * } $data Strictly typed template variables
250275 *
251- * @param array $data Template variables
252- * @return string HTML email content
276+ * Extraction uses EXTR_SKIP to avoid overwriting existing variables inside the closure scope
277+ * Output buffering captures the template output as a string
278+ *
279+ * @return string Fully rendered HTML fragment
280+ * @throws RuntimeException If the template file cannot be found
253281 */
254282function renderEmail (array $ data ): string
255283{
@@ -269,12 +297,12 @@ function renderEmail(array $data): string
269297}
270298
271299/**
272- * Configures a PHPMailer instance with SMTP settings
300+ * Configure a PHPMailer instance with project SMTP defaults
273301 *
274- * @param PHPMailer $mailer Instance to configure
275- * @return void
302+ * @param PHPMailer $mailer The PHPMailer instance to configure
303+ * @return PHPMailer Configured PHPMailer instance
276304 */
277- function configureMailer (PHPMailer $ mailer ): void
305+ function configureMailer (PHPMailer $ mailer ): PHPMailer
278306{
279307 $ mailer ->isSMTP ();
280308 $ mailer ->Host = SMTP_HOST ;
@@ -287,12 +315,13 @@ function configureMailer(PHPMailer $mailer): void
287315 $ mailer ->Sender = SMTP_USERNAME ;
288316 $ mailer ->isHTML (true );
289317 $ mailer ->CharSet = 'UTF-8 ' ;
318+ return $ mailer ;
290319}
291320
292321/**
293- * Enforce session-based rate limiting
322+ * Enforce a simple session-based rate limit
294323 *
295- * @param int $max Max submissions allowed
324+ * @param int $max Max submissions allowed
296325 * @param int $window Time window in seconds
297326 * @return void
298327 */
0 commit comments