Skip to content

Commit f52d960

Browse files
committed
Validate mailer configuration
1 parent cf33b74 commit f52d960

4 files changed

Lines changed: 304 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ release notes.
131131
database preflight failures, with direct coverage for the shared formatter.
132132
- Added a public independence disclaimer clarifying that Phalcon Kit is not
133133
affiliated with, endorsed by, or sponsored by the official Phalcon project.
134+
- Normalized mailer driver and SMTP encryption config case-insensitively,
135+
validated mailer option shapes, and failed fast on unsupported values before
136+
send-time network behavior.
134137

135138
## 3.0.3 - 2026-06-10
136139

resources/skills/phalconkit-app-developer/references/integrations.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,11 @@ Rules:
115115

116116
## Mailer And IMAP
117117

118-
Mailer config supports `mail`, `sendmail`, and `smtp`. IMAP config contains the
119-
mailbox path, login, password, attachments directory, server encoding, and
120-
filename mode.
118+
Mailer config supports `sendmail` and `smtp`. The selected driver is normalized
119+
case-insensitively and invalid driver/options shapes fail before the mailer is
120+
created. SMTP encryption accepts `ssl`, `tls`, or an empty string; values are
121+
also normalized case-insensitively. IMAP config contains the mailbox path, login,
122+
password, attachments directory, server encoding, and filename mode.
121123

122124
Rules:
123125

src/Provider/Mailer/ServiceProvider.php

Lines changed: 204 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,56 +13,244 @@
1313

1414
namespace PhalconKit\Provider\Mailer;
1515

16-
use PhalconKit\Di\DiInterface;
1716
use Phalcon\Events\ManagerInterface;
1817
use Phalcon\Incubator\Mailer\Manager;
18+
use PhalconKit\Di\DiInterface;
19+
use PhalconKit\Exception\ConfigurationException;
1920
use PhalconKit\Provider\AbstractServiceProvider;
21+
use PHPMailer\PHPMailer\PHPMailer;
2022

2123
/**
2224
* Registers the mailer manager service.
2325
*
24-
* Mailer configuration is resolved from `mailer.driver`, `mailer.defaults`,
26+
* Mailer configuration is resolved from `mailer.driver`, `mailer.default`,
2527
* and `mailer.drivers.<driver>`. Driver options are merged over defaults before
2628
* constructing Phalcon Incubator's mailer manager, then the DI container and
2729
* shared events manager are attached when available.
2830
*/
2931
class ServiceProvider extends AbstractServiceProvider
3032
{
33+
/**
34+
* @var string[]
35+
*/
36+
private const array SUPPORTED_DRIVERS = [
37+
'sendmail',
38+
'smtp',
39+
];
40+
41+
/**
42+
* @var string[]
43+
*/
44+
private const array SMTP_ENCRYPTIONS = [
45+
'',
46+
PHPMailer::ENCRYPTION_SMTPS,
47+
PHPMailer::ENCRYPTION_STARTTLS,
48+
];
49+
3150
protected string $serviceName = 'mailer';
32-
51+
3352
/**
3453
* Register the shared `mailer` service.
3554
*
36-
* The SMTP driver enables PHPMailer authentication explicitly because SMTP
37-
* credentials in the merged options imply authenticated transport.
55+
* SMTP encryption is normalized case-insensitively and validated before the
56+
* mailer is created so bad config fails before network I/O. The SMTP driver
57+
* also enables PHPMailer authentication explicitly because SMTP credentials
58+
* in the merged options imply authenticated transport.
59+
*
60+
* @throws ConfigurationException When the selected driver, option shape, or
61+
* SMTP encryption value is invalid.
3862
*/
3963
#[\Override]
4064
public function register(DiInterface $di): void
4165
{
4266
$di->setShared($this->getName(), function () use ($di) {
43-
4467
$config = $di->getConfig();
45-
68+
4669
$mailerConfig = $config->pathToArray('mailer', []);
47-
48-
$driver = $mailerConfig['driver'] ?? '';
49-
$defaultOptions = $mailerConfig['defaults'] ?? [];
50-
$driverOptions = $mailerConfig['drivers'][$driver] ?? [];
51-
$options = array_merge($defaultOptions, $driverOptions);
52-
70+
71+
$driver = self::normalizeMailerToken($mailerConfig['driver'] ?? '', 'mailer.driver');
72+
self::assertSupportedDriver($driver);
73+
74+
$defaultOptions = self::resolveDefaultOptions($mailerConfig);
75+
$drivers = self::normalizeDrivers($mailerConfig['drivers'] ?? null);
76+
$driverOptions = $drivers[$driver] ?? [];
77+
$options = self::normalizeOptions(
78+
array_merge($defaultOptions, $driverOptions),
79+
$driver
80+
);
81+
5382
$manager = new Manager($options);
5483
$manager->setDI($di);
55-
84+
5685
$eventsManager = $di->get('eventsManager');
5786
if ($eventsManager instanceof ManagerInterface) {
5887
$manager->setEventsManager($eventsManager);
5988
}
60-
89+
6190
if ($driver === 'smtp') {
6291
$manager->getMailer()->SMTPAuth = true;
6392
}
64-
93+
6594
return $manager;
6695
});
6796
}
97+
98+
/**
99+
* Normalize and validate the options passed to the incubator mailer.
100+
*
101+
* @param array<mixed> $options
102+
*
103+
* @return array<mixed>
104+
*
105+
* @throws ConfigurationException When SMTP encryption is unsupported.
106+
*/
107+
private static function normalizeOptions(array $options, string $driver): array
108+
{
109+
$configuredDriver = self::normalizeMailerToken(
110+
$options['driver'] ?? $driver,
111+
'mailer driver option'
112+
);
113+
if ($configuredDriver !== $driver) {
114+
throw new ConfigurationException(sprintf(
115+
'Mailer driver option "%s" does not match selected driver "%s".',
116+
$configuredDriver,
117+
$driver
118+
));
119+
}
120+
121+
$options['driver'] = $driver;
122+
123+
if ($driver !== 'smtp' || !array_key_exists('encryption', $options)) {
124+
return $options;
125+
}
126+
127+
$encryption = $options['encryption'];
128+
if ($encryption === null || $encryption === false) {
129+
$encryption = '';
130+
} elseif (!is_string($encryption)) {
131+
throw new ConfigurationException(
132+
'Mailer SMTP encryption must be a string, false, or null.'
133+
);
134+
}
135+
136+
$encryption = self::normalizeOptionalMailerToken($encryption);
137+
if (!in_array($encryption, self::SMTP_ENCRYPTIONS, true)) {
138+
throw new ConfigurationException(sprintf(
139+
'Unsupported mailer SMTP encryption "%s"; use "ssl", "tls", or an empty string.',
140+
$options['encryption']
141+
));
142+
}
143+
144+
$options['encryption'] = $encryption;
145+
146+
return $options;
147+
}
148+
149+
/**
150+
* Return validated defaults from the canonical key or legacy alias.
151+
*
152+
* @param array<mixed> $mailerConfig
153+
*
154+
* @return array<mixed>
155+
*/
156+
private static function resolveDefaultOptions(array $mailerConfig): array
157+
{
158+
$defaults = [];
159+
160+
if (array_key_exists('defaults', $mailerConfig)) {
161+
$defaults = self::normalizeOptionArray($mailerConfig['defaults'], 'mailer.defaults');
162+
}
163+
164+
if (array_key_exists('default', $mailerConfig)) {
165+
$defaults = array_merge(
166+
$defaults,
167+
self::normalizeOptionArray($mailerConfig['default'], 'mailer.default')
168+
);
169+
}
170+
171+
return $defaults;
172+
}
173+
174+
/**
175+
* Normalize and validate driver option groups keyed by driver name.
176+
*
177+
* @return array<string, array<mixed>>
178+
*/
179+
private static function normalizeDrivers(mixed $drivers): array
180+
{
181+
if ($drivers === null) {
182+
return [];
183+
}
184+
185+
if (!is_array($drivers)) {
186+
throw new ConfigurationException('Mailer drivers config must be an array.');
187+
}
188+
189+
$normalized = [];
190+
foreach ($drivers as $driver => $options) {
191+
$driver = self::normalizeMailerToken($driver, 'mailer.drivers key');
192+
$normalized[$driver] = self::normalizeOptionArray(
193+
$options,
194+
sprintf('mailer.drivers.%s', $driver)
195+
);
196+
}
197+
198+
return $normalized;
199+
}
200+
201+
/**
202+
* Validate a mailer option group.
203+
*
204+
* @return array<mixed>
205+
*/
206+
private static function normalizeOptionArray(mixed $options, string $path): array
207+
{
208+
if ($options === null) {
209+
return [];
210+
}
211+
212+
if (!is_array($options)) {
213+
throw new ConfigurationException(sprintf('%s must be an array.', $path));
214+
}
215+
216+
return $options;
217+
}
218+
219+
/**
220+
* Validate the selected driver against the providers this class can create.
221+
*/
222+
private static function assertSupportedDriver(string $driver): void
223+
{
224+
if (!in_array($driver, self::SUPPORTED_DRIVERS, true)) {
225+
throw new ConfigurationException(sprintf(
226+
'Unsupported mailer driver "%s"; use "sendmail" or "smtp".',
227+
$driver
228+
));
229+
}
230+
}
231+
232+
/**
233+
* Normalize ASCII mailer config tokens such as driver and encryption names.
234+
*/
235+
private static function normalizeMailerToken(mixed $value, string $path): string
236+
{
237+
if (!is_string($value)) {
238+
throw new ConfigurationException(sprintf('%s must be a string.', $path));
239+
}
240+
241+
$value = strtolower(trim($value));
242+
if ($value === '') {
243+
throw new ConfigurationException(sprintf('%s must not be empty.', $path));
244+
}
245+
246+
return $value;
247+
}
248+
249+
/**
250+
* Normalize optional mailer config tokens that may intentionally be empty.
251+
*/
252+
private static function normalizeOptionalMailerToken(string $value): string
253+
{
254+
return strtolower(trim($value));
255+
}
68256
}

0 commit comments

Comments
 (0)