Add SES V2 API support, tenant isolation and custom SMTP headers - #348
dlackovic2 wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive AWS SES V2 API support to FluentSMTP, replacing the legacy V1 API implementation. It introduces tenant isolation capabilities for multi-tenant environments and adds custom SMTP header support for enhanced routing capabilities.
Key Changes:
- Complete migration from SES V1 to V2 API with new
SimpleEmailServiceV2client class - Tenant isolation feature with configuration set and tenant name validation
- Custom SMTP headers support for advanced email routing scenarios
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
app/Services/Mailer/Providers/AmazonSes/SimpleEmailServiceV2.php |
New SES V2 API client with AWS Signature V4 authentication, tenant support, and configuration set handling |
app/Services/Mailer/Providers/AmazonSes/Handler.php |
Updated to use V2 API for sending emails, listing identities, and retrieving account statistics |
app/Services/Mailer/Providers/AmazonSes/ValidatorTrait.php |
Enhanced validation with tenant/configuration set checks and improved AWS error message parsing |
app/Services/Mailer/Providers/Smtp/Handler.php |
Added custom header parsing and injection for SMTP emails |
resources/admin/Modules/Settings/Partials/Providers/AmazonSes.vue |
Added UI controls for tenant mode configuration with conditional field visibility |
resources/admin/Modules/Settings/Partials/Providers/Smtp.vue |
Added custom headers textarea input with help text |
app/Functions/helpers.php |
Updated helper function to instantiate V2 client instead of V1 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // Check if it's a domain (no @ sign) or email address | ||
| if (strpos($identityName, '@') === false) { | ||
| // It's a domain | ||
| if ($sendingEnabled) { | ||
| $domains[] = $identityName; | ||
| } | ||
| } else { | ||
| // It's an email address | ||
| if ($sendingEnabled) { | ||
| $addresses[] = $identityName; | ||
| } |
There was a problem hiding this comment.
The logic to determine if an identity is a domain or email address by checking for '@' sign may not be fully reliable. AWS SES identities could include other types of resources. Additionally, the SendingEnabled field might not exist in all identity responses from the V2 API.
Consider checking the IdentityType field if available in the API response to reliably distinguish between email addresses and domains, rather than relying on the presence of '@' character.
| // Check if it's a domain (no @ sign) or email address | |
| if (strpos($identityName, '@') === false) { | |
| // It's a domain | |
| if ($sendingEnabled) { | |
| $domains[] = $identityName; | |
| } | |
| } else { | |
| // It's an email address | |
| if ($sendingEnabled) { | |
| $addresses[] = $identityName; | |
| } | |
| $identityType = isset($identity['IdentityType']) ? $identity['IdentityType'] : null; | |
| // Use IdentityType to distinguish between domain and email address | |
| if ($identityType === 'DOMAIN' || $identityType === 'MANAGED_DOMAIN') { | |
| if ($sendingEnabled) { | |
| $domains[] = $identityName; | |
| } | |
| } elseif ($identityType === 'EMAIL_ADDRESS') { | |
| if ($sendingEnabled) { | |
| $addresses[] = $identityName; | |
| } | |
| } else { | |
| // Fallback: if IdentityType is missing, use previous logic | |
| if (strpos($identityName, '@') === false) { | |
| if ($sendingEnabled) { | |
| $domains[] = $identityName; | |
| } | |
| } else { | |
| if ($sendingEnabled) { | |
| $addresses[] = $identityName; | |
| } | |
| } |
| if (isset($this->response['MessageId'])) { | ||
| $this->response = [ | ||
| 'MessageId' => $this->response['MessageId'] | ||
| ]; |
There was a problem hiding this comment.
If the SES V2 API returns a successful response without a MessageId field, the $this->response will be the full API response array, which may not match the expected format downstream. This could cause unexpected behavior if the response structure doesn't match what handleResponse() expects.
Consider adding an else clause to handle the case where MessageId is not present:
if (isset($this->response['MessageId'])) {
$this->response = [
'MessageId' => $this->response['MessageId']
];
} else {
// Handle unexpected response format
$this->response = new \WP_Error(422, __('Invalid response from SES API', 'fluent-smtp'), []);
}| ]; | |
| ]; | |
| } else { | |
| $this->response = new \WP_Error(422, __('Invalid response from SES API', 'fluent-smtp'), []); |
| // Only include verified identities | ||
| if (isset($identity['SendingEnabled']) && $identity['SendingEnabled']) { |
There was a problem hiding this comment.
The listVerifiedEmailAddresses() method includes both email addresses and domains in the Identities array. This differs from the SES V1 API which had separate methods for listing verified email addresses and domains.
If existing code expects listVerifiedEmailAddresses() to return only email addresses (not domains), this could cause unexpected behavior. Consider filtering to only include identities that contain '@' character to match the V1 API behavior:
if (isset($identity['SendingEnabled']) && $identity['SendingEnabled'] && strpos($identity['IdentityName'], '@') !== false) {
$verifiedEmails[] = $identity['IdentityName'];
}| // Only include verified identities | |
| if (isset($identity['SendingEnabled']) && $identity['SendingEnabled']) { | |
| // Only include verified email addresses (not domains) | |
| if ( | |
| isset($identity['SendingEnabled']) && $identity['SendingEnabled'] && | |
| strpos($identity['IdentityName'], '@') !== false | |
| ) { |
| if ($colonPos !== false) { | ||
| $headerName = trim(substr($headerLine, 0, $colonPos)); | ||
| $headerValue = trim(substr($headerLine, $colonPos + 1)); | ||
| if (!empty($headerName) && !empty($headerValue)) { |
There was a problem hiding this comment.
Custom headers from user input are added to SMTP messages without validation or sanitization. This could allow header injection attacks where malicious users could inject newline characters (\r\n) in header values to inject additional headers or manipulate the email structure.
Consider validating that:
- Header names contain only valid characters (RFC 5322: printable ASCII, no colons or whitespace)
- Header values do not contain newline characters (
\ror\n) - Consider using a whitelist of allowed header names if possible
Example mitigation:
// Validate header name (RFC 5322)
if (!preg_match('/^[!-9;-~]+$/', $headerName)) {
continue; // Skip invalid header names
}
// Validate header value (no newlines)
if (preg_match('/[\r\n]/', $headerValue)) {
continue; // Skip values with newlines
}| if (!empty($headerName) && !empty($headerValue)) { | |
| if (!empty($headerName) && !empty($headerValue)) { | |
| // Validate header name (RFC 5322: printable ASCII, no colons or whitespace) | |
| if (!preg_match('/^[!-9;-~]+$/', $headerName)) { | |
| continue; // Skip invalid header names | |
| } | |
| // Validate header value (no newlines) | |
| if (preg_match('/[\r\n]/', $headerValue)) { | |
| continue; // Skip values with newlines | |
| } |
- Refactor SES V2 initialization by removing unnecessary parameter since we can use default in construcotr for SSL verfyPeer - enhance header validation to prevent injection attacks
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Only allow alphanumeric, hyphen, and underscore in header names | ||
| if (!empty($headerName) && !empty($headerValue) && preg_match('/^[A-Za-z0-9\-_]+$/', $headerName)) { |
There was a problem hiding this comment.
The regex pattern /^[A-Za-z0-9\-_]+$/ doesn't allow dots (.) in header names, but dots are valid characters in RFC 822 header names. For example, X-SES.Tenant would be rejected even though it's a valid header name according to RFC standards. Consider updating the pattern to /^[A-Za-z0-9\-_.]+$/ to allow dots.
| // Only allow alphanumeric, hyphen, and underscore in header names | |
| if (!empty($headerName) && !empty($headerValue) && preg_match('/^[A-Za-z0-9\-_]+$/', $headerName)) { | |
| // Only allow alphanumeric, hyphen, underscore, and dot in header names | |
| if (!empty($headerName) && !empty($headerValue) && preg_match('/^[A-Za-z0-9\-_.]+$/', $headerName)) { |
| } catch (ValidationException $e) { | ||
| throw $e; |
There was a problem hiding this comment.
The catch blocks that re-throw ValidationException immediately after catching it are redundant. Since ValidationException extends from the base \Exception, it will already be caught by the subsequent catch (\Exception $e) block. Consider removing these redundant catch blocks to simplify the code.
| } catch (ValidationException $e) { | ||
| throw $e; |
There was a problem hiding this comment.
The catch blocks that re-throw ValidationException immediately after catching it are redundant. Since ValidationException extends from the base \Exception, it will already be caught by the subsequent catch (\Exception $e) block. Consider removing these redundant catch blocks to simplify the code.
| if ($httpCode >= 200 && $httpCode < 300) { | ||
| $response = ['success' => true, 'httpCode' => $httpCode]; |
There was a problem hiding this comment.
When the response body is empty and the HTTP code is in the 2xx range, the code creates a success response with ['success' => true, 'httpCode' => $httpCode]. However, for operations like sendEmail(), the caller expects a MessageId in the response. This could lead to issues where a 2xx response without proper JSON would be treated as successful even though it's missing required data. Consider validating that the response contains expected fields for critical operations or logging a warning when receiving an empty successful response.
|
Comment just to see if this will make PR reappear since it is not appearing in PRs at all |
Add SES V2 API Support, Tenant Mode, Config Sets & Custom SMTP Headers
Summary
This PR adds full Amazon SES V2 API support to FluentSMTP, including tenant isolation, configuration set validation, error handling, and updated UI fields.
SMTP provider now supports custom headers (e.g.
X-SES-TENANT) for tenant routing.Key Changes
SimpleEmailServiceV2)/v2/email/outbound-emailsTesting
SES V2
MessageIdTenant Mode
Enable tenant mode
Enter configuration set name + tenant name
Validation should:
Send test email → tenant should apply
SMTP Custom Headers