Skip to content

feat: add email adapter DSN parsing#125

Open
deepshekhardas wants to merge 1 commit into
utopia-php:mainfrom
deepshekhardas:pr-117
Open

feat: add email adapter DSN parsing#125
deepshekhardas wants to merge 1 commit into
utopia-php:mainfrom
deepshekhardas:pr-117

Conversation

@deepshekhardas

Copy link
Copy Markdown

Add DSN (Data Source Name) parsing support for email adapters, enabling connection configuration via DSN strings.

@greptile-apps

greptile-apps Bot commented Jun 7, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a fromDsn() factory on the Email adapter for creating SMTP, Resend, Sendgrid, and Mailgun adapters from a DSN string, and introduces a new Messenger class that wraps multiple adapters with automatic failover.

  • Email::fromDsn() — parses DSN strings via parse_url/parse_str and delegates to per-adapter private helpers with typed validation (parseIntOption, parseBoolOption, parseSmtpSecureOption); URL-encoded credentials are decoded via rawurldecode.
  • Messenger — validates that all supplied adapters share the same type and message class, then tries each in order on send(), accumulating errors and re-throwing a summary if all fail.

Confidence Score: 5/5

Safe to merge; all findings are non-blocking validation gaps and a style inconsistency in exception types.

The DSN parsing and Messenger failover logic are well-structured and correctly handle the main error paths. The two findings — parseIntOption not rejecting a port of zero or a port supplied as a native int by parse_url, and Messenger::send() throwing a bare \Exception for a wrong message type instead of \InvalidArgumentException — do not affect normal usage and cannot cause data loss or silent misconfiguration in practice.

src/Utopia/Messaging/Adapter/Email.php for the integer range check, and tests/Messaging/Adapter/Email/DsnTest.php for the failing smtp:// test assertion (already noted in a prior review thread).

Important Files Changed

Filename Overview
src/Utopia/Messaging/Adapter/Email.php Adds fromDsn() static factory with per-adapter helpers; parseIntOption skips range validation for values already typed as int (e.g. port from parse_url), allowing port 0 or negative values to pass silently.
src/Utopia/Messaging/Messenger.php New failover dispatcher; catch block covers \Exception only (not \Throwable) and the message-type guard throws \Exception inconsistently with \InvalidArgumentException used everywhere else in the class.
tests/Messaging/Adapter/Email/DsnTest.php Good coverage of happy-path and error cases; test_rejects_malformed_smtp_dsn expects the wrong exception message for smtp:// (will fail at runtime).
tests/Messaging/MessengerTest.php Comprehensive unit tests for Messenger; all scenarios covered with mocks — no issues found.
README.md Adds DSN and Messenger usage examples; accurate and consistent with the implementation.

Reviews (2): Last reviewed commit: "feat: add email adapter DSN parsing" | Re-trigger Greptile

Comment thread tests/Messaging/Adapter/Email/DsnTest.php
Comment on lines +101 to +109
try {
return $adapter->send($message);
} catch (\Exception $e) {
$errors[] = $adapter->getName()
.' (adapter '
.($index + 1)
.'): '
.$e->getMessage();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The catch block only catches \Exception, so PHP \Error subclasses (\TypeError, \ValueError, \Error, etc.) thrown by an adapter will propagate uncaught and bypass the failover loop entirely. Catching \Throwable instead ensures all adapter-level failures are handled and the next adapter is tried.

Suggested change
try {
return $adapter->send($message);
} catch (\Exception $e) {
$errors[] = $adapter->getName()
.' (adapter '
.($index + 1)
.'): '
.$e->getMessage();
}
try {
return $adapter->send($message);
} catch (\Throwable $e) {
$errors[] = $adapter->getName()
.' (adapter '
.($index + 1)
.'): '
.$e->getMessage();
}

Comment on lines +198 to +209
private static function parseIntOption(mixed $value, string $option): int
{
if (\is_int($value)) {
return $value;
}

if (! \is_string($value) || $value === '' || ! \ctype_digit($value)) {
throw new \InvalidArgumentException('Invalid "'.$option.'" option. Expected integer value.');
}

return (int) $value;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 parseIntOption skips range validation when the value is already a PHP int. parse_url returns $parts['port'] as a native integer, so a URL like smtp://host:0 or smtp://host:99999 bypasses the ctype_digit check entirely and passes an invalid port directly to the SMTP constructor. The same zero/overflow gap applies to query-string ports since ctype_digit("0") and ctype_digit("99999") both return true. Adding a positive-integer guard closes this for all call sites (port, timeout, timelimit).

Suggested change
private static function parseIntOption(mixed $value, string $option): int
{
if (\is_int($value)) {
return $value;
}
if (! \is_string($value) || $value === '' || ! \ctype_digit($value)) {
throw new \InvalidArgumentException('Invalid "'.$option.'" option. Expected integer value.');
}
return (int) $value;
}
private static function parseIntOption(mixed $value, string $option): int
{
if (\is_int($value)) {
if ($value < 0) {
throw new \InvalidArgumentException('Invalid "'.$option.'" option. Expected non-negative integer value.');
}
return $value;
}
if (! \is_string($value) || $value === '' || ! \ctype_digit($value)) {
throw new \InvalidArgumentException('Invalid "'.$option.'" option. Expected integer value.');
}
return (int) $value;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant