PluginProbe
The Innovative Form Builder – IvyForms / 1.4
The Innovative Form Builder – IvyForms v1.4
1.5.1 1.5 1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 All 27 releases
ivyforms / backend / src / Services / Mailer / MailerService.php

MailerService.php in The Innovative Form Builder – IvyForms 1.4, at backend/src/Services/Mailer/MailerService.php

225 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © Melograno Venture Studio. All rights reserved.
5 * @licence See COPYING.md for license details.
6 */
7
8 namespace IvyForms\Services\Mailer;
9
10 use IvyForms\Common\Sanitizer\Sanitizer;
11 use IvyForms\Entity\Notification\Notification;
12
13 class MailerService
14 {
15 /**
16 * Send an email using WordPress wp_mail function
17 *
18 * @param Notification $notification The notification object with email data
19 * @param mixed[] $formData The submitted form data for template variables
20 *
21 * @return bool Whether the email was sent successfully
22 */
23 public function sendEmail(Notification $notification, array $formData): bool
24 {
25 // Extract notification data
26 $contact = $notification->getNotificationContact();
27 $toAddresses = $this->validatedAddresses($contact->getReceiver());
28 $ccAddresses = $this->validatedAddresses($contact->getCc());
29 $bccAddresses = $this->validatedAddresses($contact->getBcc());
30 $to = implode(', ', $toAddresses);
31 $cc = implode(', ', $ccAddresses);
32 $bcc = implode(', ', $bccAddresses);
33 $from = $contact->getSender();
34 $replyTo = $contact->getReplyTo();
35
36 if ($to === '') {
37 return false;
38 }
39
40 if ($this->hasUnsafeHeaderValues($from, $replyTo, $cc, $bcc)) {
41 return false;
42 }
43
44 $subject = $notification->getSubject();
45 $message = $this->replacePlaceholders($notification->getMessage(), $formData);
46
47 $headers = $this->buildHeaders($from, $replyTo);
48 $visibleHeaders = $headers;
49 if ($cc !== '') {
50 $visibleHeaders[] = 'Cc: ' . $cc;
51 }
52
53 // Send email using WordPress function
54 $sent = wp_mail($to, $subject, $message, $visibleHeaders);
55
56 if ($sent && $bccAddresses !== []) {
57 $sent = $this->sendBccCopies($bccAddresses, $subject, $message, $headers);
58 }
59
60 return $sent;
61 }
62
63 /**
64 * Build the headers shared by every copy of the notification.
65 *
66 * @return string[]
67 */
68 private function buildHeaders(string $from, string $replyTo): array
69 {
70 $headers = [
71 'Content-Type: text/html; charset=UTF-8',
72 ];
73
74 if (!empty($from)) {
75 $headers[] = 'From: ' . $from;
76 }
77
78 // If replyTo is set and not identical to From, add Reply-To header
79 if (!empty($replyTo) && $replyTo !== $from) {
80 $headers[] = 'Reply-To: ' . $replyTo;
81 }
82
83 return $headers;
84 }
85
86 /**
87 * PHPMailer keeps the Bcc header in the message on the mail()/sendmail/qmail
88 * transports, so a shared send would expose Bcc addresses to the To and Cc
89 * recipients. Each Bcc recipient therefore gets its own copy.
90 *
91 * @param string[] $bccAddresses
92 * @param string[] $headers
93 */
94 private function sendBccCopies(array $bccAddresses, string $subject, string $message, array $headers): bool
95 {
96 $sent = true;
97
98 foreach ($bccAddresses as $bccAddress) {
99 $sent = wp_mail($bccAddress, $subject, $message, $headers) && $sent;
100 }
101
102 return $sent;
103 }
104
105 /**
106 * Replace template variables in content with actual form data
107 * Supports format like {{field_name}}
108 *
109 * @param string $content Content with template variables
110 * @param mixed[] $formData Form submission data
111 *
112 * @return string Content with replaced variables
113 */
114 private function replacePlaceholders(string $content, array $formData): string
115 {
116 // Replace field variables like {{field_name}}
117 preg_match_all('/{{(.+?)}}/s', $content, $matches);
118
119 if (!empty($matches[1])) {
120 foreach ($matches[1] as $key => $fieldName) {
121 $fieldValue = $formData[$fieldName] ?? '';
122 if (is_array($fieldValue)) {
123 $fieldValue = implode(', ', $fieldValue);
124 }
125
126 $content = str_replace($matches[0][$key], $fieldValue, $content);
127 }
128 }
129
130 // Replace special variables
131 $content = str_replace('{{all_fields}}', $this->getAllFieldsHtml($formData), $content);
132
133 return $content;
134 }
135
136 /**
137 * Generate HTML table with all form fields for email template
138 *
139 * @param mixed[] $formData Form submission data
140 *
141 * @return string HTML representation of all fields
142 */
143 private function getAllFieldsHtml(array $formData): string
144 {
145 // Generate HTML table for all fields
146 $html = '<h2>Form Submission Details</h2>';
147 $html .= '<table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">';
148
149 foreach ($formData as $fieldName => $fieldValue) {
150 // Skip system fields
151 if (in_array($fieldName, ['formId', 'nonce'])) {
152 continue;
153 }
154
155 // Format array values
156 if (is_array($fieldValue)) {
157 $fieldValue = implode(', ', $fieldValue);
158 }
159
160 // Format label by converting snake_case or camelCase to Title Case
161 $label = ucwords(str_replace(['_', '-'], ' ', $fieldName));
162
163 $html .= '<tr>';
164 $html .= '<th style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">';
165 $html .= esc_html($label) . '</th>';
166 $html .= '</th>';
167 $html .= '<td style="text-align: left; padding: 8px; border-bottom: 1px solid #ddd;">';
168 $html .= esc_html($fieldValue) . '</td>';
169 $html .= '</td>';
170 $html .= '</tr>';
171 }
172
173 $html .= '</table>';
174
175 return $html;
176 }
177
178 /**
179 * @param string ...$values
180 */
181 private function hasUnsafeHeaderValues(string ...$values): bool
182 {
183 foreach ($values as $value) {
184 if (Sanitizer::containsEmailHeaderInjection($value)) {
185 return true;
186 }
187 }
188
189 return false;
190 }
191
192 /**
193 * Keep only validated email addresses for To:/Cc:/Bcc: recipients.
194 *
195 * @param string $addresses Comma-separated addresses
196 * @return string[]
197 */
198 private function validatedAddresses(string $addresses): array
199 {
200 $addresses = trim($addresses);
201 if ($addresses === '') {
202 return [];
203 }
204
205 $validated = [];
206
207 foreach (array_map('trim', explode(',', $addresses)) as $address) {
208 if ($address === '' || Sanitizer::containsEmailHeaderInjection($address)) {
209 continue;
210 }
211
212 $emailCandidate = $address;
213 if (preg_match('/<([^>]+)>/', $address, $match)) {
214 $emailCandidate = trim($match[1]);
215 }
216
217 if (Sanitizer::isValidEmail($emailCandidate)) {
218 $validated[] = $address;
219 }
220 }
221
222 return $validated;
223 }
224 }
225