Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,12 @@ Add one row only for a shared finding or changed lower-level assumption that ano
| `database-21` | `database` | `database` and `scout` (revalidation complete) | `Complete Scout current parity, queue, and search lifecycles`; finding `database-21` |
| `database-22` | `database` | `database` (revalidation complete) | `Complete Scout current parity, queue, and search lifecycles`; finding `database-22` |
| `database-23` | `database` | `database` and `scout` (revalidation complete) | `Complete Scout current parity, queue, and search lifecycles`; finding `database-23` |
| `mail-17` | `mail` | `mail` and `support` (revalidation complete) | `Complete Mail correctness, current parity, and package boundaries`; finding `mail-17` |
| `support-28` | `support` | `support` and `mail` (revalidation complete) | `Complete Mail correctness, current parity, and package boundaries`; finding `support-28` |
| `support-29` | `support` | `support`, `mail`, and `validation` (targeted revalidation complete); later full `validation` audit | `Complete Mail correctness, current parity, and package boundaries`; finding `support-29` |
| `contracts-10` | `contracts` | `contracts` and `mail` (revalidation complete) | `Complete Mail correctness, current parity, and package boundaries`; finding `contracts-10` |
| `contracts-11` | `contracts` | `contracts`, `mail`, and `console` (revalidation complete) | `Complete Mail correctness, current parity, and package boundaries`; finding `contracts-11` |
| `filesystem-14` | `filesystem` | `filesystem` and `mail` (revalidation complete) | `Complete Mail correctness, current parity, and package boundaries`; finding `filesystem-14` |

## Package checklist

Expand Down Expand Up @@ -1237,7 +1243,7 @@ The order is lower-level first where practical. Hypervel has cross-cutting depen
- [ ] `api-client`
- [x] `grpc`
- [x] `broadcasting`
- [ ] `mail`
- [x] `mail`
- [ ] `notifications`

### Application and domain packages
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

## Testing

- Port current Laravel's complete `tests/Support/SupportTestingEventFakeTest.php`, preserving Hypervel-specific EventFake coverage and coroutine-safe test behavior.
- Complete Testing assertion coverage: port the remaining current Laravel `TestResponseTest` cases through the incremental upstream-update workflow, and add focused coverage for `TestView`'s public assertion and string surface where Laravel has no equivalent suite.
- Add the repository-required `: void` return type to the remaining untyped HTTP test methods: 176 in `tests/Http/HttpClientTest.php`, 30 in `tests/Http/HttpRequestTrustedStateTest.php`, and 4 in `tests/Http/HttpRequestTrustedStateCoroutineTest.php`. Verify each file after the mechanical conversion.

Expand Down
10 changes: 10 additions & 0 deletions src/boost/docs/mail.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ public function headers(): Headers
}
```

To send through an SES tenant, add an `X-SES-TENANT-NAME` text header containing the tenant name.

If you would like to define [additional options](https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sesv2-2019-09-27.html#sendemail) that Hypervel should pass to the AWS SDK's `SendEmail` method when sending an email, you may define an `options` array within your `ses` mailer configuration:

```php
Expand Down Expand Up @@ -313,6 +315,8 @@ Named mailers use pooling by default when their transport is poolable. On-demand

Any other value is rejected. Explicitly requesting pooling for a transport that is not poolable is also rejected instead of being silently ignored.

For a pooled mailer, `getSymfonyTransport()` returns a `TransportPoolProxy` rather than one concrete transport. Configure the transport through `config/mail.php`, or set `pool` to `false` if stable concrete transport inspection is required.

Pool identity is derived from the transport's resolved construction input, including fallback credentials from `config/services.php`. Equivalent named mailers and explicitly pooled equivalent mailers created through `Mail::build()` converge on one pool. Credential changes create a different identity. Failover and round-robin identities include each resolved child transport in order, so changing a child or its credentials also changes the composite pool.

The default pool settings are suitable for most applications. If your application sends a high volume of concurrent mail, or if you encounter pool exhaustion errors, you may tune the pool for any mailer by adding a `pool` configuration option to the mailer's configuration array:
Expand Down Expand Up @@ -604,6 +608,8 @@ public function attachments(): array
}
```

`Attachment::fromUrl()` accepts HTTP and HTTPS URLs. Use `fromPath()` for local filesystem paths.

When attaching files to a message, you may also specify the display name and / or MIME type for the attachment using the `as` and `withMime` methods:

```php
Expand Down Expand Up @@ -1093,6 +1099,8 @@ Mail::to($request->user())
->queue($message);
```

The `queue()`, `later()`, `onQueue()`, `queueOn()`, and `laterOn()` methods accept queue names as strings or PHP enum cases.

<a name="queueing-by-default"></a>
#### Queueing by Default

Expand Down Expand Up @@ -1323,6 +1331,8 @@ public function test_mailable_content(): void

As you might expect, the "HTML" assertions assert that the HTML version of your mailable contains a given string, while the "text" assertions assert that the plain-text version of your mailable contains a given string.

Use `$mailable->assertHasNoAttachments()` when a message should not contain any attachments.

<a name="testing-mailable-sending"></a>
### Testing Mailable Sending

Expand Down
5 changes: 3 additions & 2 deletions src/contracts/src/Mail/MailQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@

use DateInterval;
use DateTimeInterface;
use UnitEnum;

interface MailQueue
{
/**
* Queue a new e-mail message for sending.
*/
public function queue(array|Mailable|string $view, ?string $queue = null): mixed;
public function queue(array|Mailable|string $view, UnitEnum|string|null $queue = null): mixed;

/**
* Queue a new e-mail message for sending after (n) seconds.
*/
public function later(DateInterval|DateTimeInterface|int $delay, array|Mailable|string $view, ?string $queue = null): mixed;
public function later(DateInterval|DateTimeInterface|int $delay, array|Mailable|string $view, UnitEnum|string|null $queue = null): mixed;
}
2 changes: 1 addition & 1 deletion src/contracts/src/Mail/Mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function bcc(mixed $users): PendingMail;
/**
* Send a new message with only a raw text part.
*/
public function raw(string $text, mixed $callback): ?SentMessage;
public function raw(string $text, Closure|string $callback): ?SentMessage;

/**
* Send a new message using a view.
Expand Down
3 changes: 2 additions & 1 deletion src/filesystem/src/ServeFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public function __invoke(Request $request, string $path): Response
$this->isProduction ? 404 : 403
);
try {
/** @var FilesystemAdapter $disk */
$disk = Storage::disk($this->disk);

abort_unless($disk->exists($path), 404);
Expand All @@ -42,6 +41,8 @@ public function __invoke(Request $request, string $path): Response
];

return tap(
// The contract omits adapter response methods, which every shipped disk provides.
// @phpstan-ignore method.notFound
$disk->serve($request, $path, headers: $headers),
function ($response) use ($headers) {
if (! $response->headers->has('Content-Security-Policy')) {
Expand Down
4 changes: 3 additions & 1 deletion src/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Http for Hypervel

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/http)

Ported from: https://github.com/laravel/framework
Documentation: https://hypervel.org/docs/requests

## Differences From Laravel

Expand All @@ -16,3 +16,5 @@ Laravel's `Http::pool()` and `Http::batch()` APIs are intentionally not ported.
`TrustHosts` fails closed when no trusted host patterns resolve. If the middleware is enabled and no resolver, `at()` list, or valid `app.url` host provides a trusted pattern, Hypervel rejects all hosts using a never-matching sentinel. Laravel and Symfony leave the trusted host list empty in this case, which accepts every host. Configure a valid `app.url`, `TrustHosts::at()`, or `TrustHosts::resolveHostsUsing()` when enabling the middleware.

Configure trusted proxies through `$middleware->trustProxies(...)` in `bootstrap/app.php`. Hypervel does not read the legacy `trustedproxy.proxies` configuration key or include Laravel's Cloud, Forge, and Vapor host-specific proxy behavior.

Ported from: https://github.com/laravel/framework
6 changes: 6 additions & 0 deletions src/mail/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Mail for Hypervel

[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/hypervel/mail)

Documentation: https://hypervel.org/docs/mail

## Differences From Laravel

Hypervel omits Laravel's legacy `Attachment::fromCloudStorage()` helper. Use `Attachment::fromStorageDisk(...)` with a named disk instead.

Hypervel supports Amazon SES through SES v2 only. The `ses` mailer uses the `ses-v2` transport.

Ported from: https://github.com/laravel/framework
14 changes: 9 additions & 5 deletions src/mail/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"php": "^8.4",
"league/commonmark": "^2.7",
"psr/log": "^3.0",
"symfony/http-foundation": "^8.1",
"symfony/mailer": "^8.1",
"symfony/mime": "^8.1",
"tijsverkoyen/css-to-inline-styles": "^2.2.5",
"hypervel/bus": "^0.4",
"hypervel/collections": "^0.4",
Expand All @@ -42,11 +44,9 @@
"hypervel/contracts": "^0.4",
"hypervel/log": "^0.4",
"hypervel/macroable": "^0.4",
"hypervel/notifications": "^0.4",
"hypervel/object-pool": "^0.4",
"hypervel/queue": "^0.4",
"hypervel/support": "^0.4",
"hypervel/testing": "^0.4",
"hypervel/view": "^0.4"
},
"config": {
Expand All @@ -64,9 +64,13 @@
},
"suggest": {
"aws/aws-sdk-php": "Required to use the SES mail driver (^3.235.5).",
"hypervel/filesystem": "Required to attach files from storage (^0.4).",
"hypervel/http": "Required to attach uploaded files (^0.4).",
"hypervel/testing": "Required to use ordered Mailable assertions (^0.4).",
"phpunit/phpunit": "Required to use Mailable assertion methods (^13.0).",
"resend/resend-php": "Required to enable support for the Resend mail transport (^1.0).",
"symfony/http-client": "Required to use the Symfony API mail transports (^8.1).",
"symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^8.1).",
"symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^8.1).",
"resend/resend-php": "Required to enable support for the Resend mail transport (^1.0)."
"symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^8.1)."
}
}
}
36 changes: 27 additions & 9 deletions src/mail/src/Attachment.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
use Hypervel\Contracts\Filesystem\Filesystem;
use Hypervel\Http\UploadedFile;
use Hypervel\Notifications\Messages\MailMessage;
use Hypervel\Support\Str;
use Hypervel\Support\Traits\Macroable;
use InvalidArgumentException;
use RuntimeException;

use function with;
Expand Down Expand Up @@ -52,6 +54,10 @@ public static function fromPath(string $path): static
*/
public static function fromUrl(string $url): static
{
if (! Str::isUrl($url, ['http', 'https'])) {
throw new InvalidArgumentException('Attachment URLs must use the http or https scheme.');
}

return static::fromPath($url);
}

Expand Down Expand Up @@ -93,15 +99,26 @@ public static function fromStorage(string $path): static
public static function fromStorageDisk(?string $disk, string $path): static
{
return new static(function ($attachment, $pathStrategy, $dataStrategy) use ($disk, $path) {
$attachment
->as($attachment->as ?? basename($path))
->withMime($attachment->mime ?? static::getStorageDisk($disk)->mimeType($path)); // @phpstan-ignore-line
$storage = static::getStorageDisk($disk);
$mime = $attachment->mime;

if ($mime === null) {
// The contract omits adapter metadata methods, which every shipped disk provides.
// @phpstan-ignore method.notFound
$mime = $storage->mimeType($path);
}

return $dataStrategy(fn () => static::getStorageDisk($disk)->get($path), $attachment);
$attachment->as($attachment->as ?? basename($path));

if ($mime !== false) {
$attachment->withMime($mime);
}

return $dataStrategy(fn () => $storage->get($path), $attachment);
});
}

// Laravel's fromCloudStorage() helper is intentionally not ported.
// REMOVED: Laravel's fromCloudStorage() helper is intentionally not ported.
// Use fromStorageDisk('s3', $path) or another named disk instead.

/**
Expand Down Expand Up @@ -145,13 +162,14 @@ public function attachWith(Closure $pathStrategy, Closure $dataStrategy): mixed
/**
* Attach the attachment to a built-in mail type.
*
* @phpstan-ignore-next-line
* @param Mailable|MailMessage|Message $mail
*
* @throws RuntimeException
*/
public function attachTo(Mailable|MailMessage|Message $mail, array $options = []): mixed
public function attachTo(object $mail, array $options = []): mixed
{
/** @var Mailable $mail */
return $this->attachWith(
fn ($path) => $mail->attach($path, [
fn (string $path) => $mail->attach($path, [
'as' => $options['as'] ?? $this->as,
'mime' => $options['mime'] ?? $this->mime,
]),
Expand Down
40 changes: 28 additions & 12 deletions src/mail/src/MailManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
use Closure;
use Hypervel\Config\Repository;
use Hypervel\Contracts\Container\Container;
use Hypervel\Contracts\Events\Dispatcher;
use Hypervel\Contracts\Mail\Factory as FactoryContract;
use Hypervel\Contracts\Mail\Mailer as MailerContract;
use Hypervel\Contracts\Queue\Factory as QueueFactory;
use Hypervel\Contracts\View\Factory as ViewFactory;
use Hypervel\Log\LogManager;
use Hypervel\Mail\Transport\ArrayTransport;
use Hypervel\Mail\Transport\CloudflareTransport;
Expand Down Expand Up @@ -42,7 +45,7 @@
use function Hypervel\Support\enum_value;

/**
* @mixin \Hypervel\Contracts\Mail\Mailer
* @mixin \Hypervel\Mail\Mailer
*/
class MailManager implements FactoryContract
{
Expand Down Expand Up @@ -134,18 +137,22 @@ protected function resolve(string $name): MailerContract
throw new InvalidArgumentException("Mailer [{$name}] is not defined.");
}

// Once we have created the mailer instance we will set a container instance
// on the mailer. This allows us to resolve mailer classes via containers
// for maximum testability on said classes instead of passing Closures.
/** @var ViewFactory $views */
$views = $this->app->make('view');
/** @var Dispatcher $events */
$events = $this->app->make('events');

$mailer = new Mailer(
$name,
$this->app['view'],
$views,
$this->createMailerTransport($config, [$name], poolByDefault: true),
$this->app['events']
$events
);

if ($this->app->bound('queue')) {
$mailer->setQueue($this->app['queue']);
/** @var QueueFactory $queue */
$queue = $this->app->make('queue');
$mailer->setQueue($queue);
}

// Next we will set all of the global addresses on this mailer, which allows
Expand All @@ -163,15 +170,22 @@ protected function resolve(string $name): MailerContract
*/
public function build(array $config): Mailer
{
/** @var ViewFactory $views */
$views = $this->app->make('view');
/** @var Dispatcher $events */
$events = $this->app->make('events');

$mailer = new Mailer(
$config['name'] ?? 'ondemand',
$this->app['view'],
$views,
$this->createMailerTransport($config),
$this->app['events']
$events
);

if ($this->app->bound('queue')) {
$mailer->setQueue($this->app['queue']);
/** @var QueueFactory $queue */
$queue = $this->app->make('queue');
$mailer->setQueue($queue);
}

return $mailer;
Expand Down Expand Up @@ -288,7 +302,7 @@ protected function transportConstructionConfig(array $config, array $mailerStack
return match ($transport) {
'sendmail' => [
'transport' => $transport,
'path' => $config['path'] ?? $this->config->get('mail.sendmail'),
'path' => $config['path'] ?? null,
],
'ses-v2' => array_merge(
$this->config->array('services.ses', []),
Expand Down Expand Up @@ -328,7 +342,7 @@ protected function transportConstructionConfig(array $config, array $mailerStack
),
'log' => [
'transport' => $transport,
'channel' => $config['channel'] ?? $this->config->get('mail.log_channel'),
'channel' => $config['channel'] ?? null,
],
'mail', 'array' => ['transport' => $transport],
default => $config,
Expand Down Expand Up @@ -463,6 +477,8 @@ protected function createSendmailTransport(array $config): SendmailTransport
return new SendmailTransport($config['path']);
}

// REMOVED: Hypervel supports Amazon SES through the SES v2 API only.

/**
* Create an instance of the Symfony Amazon SES V2 Transport driver.
*/
Expand Down
4 changes: 3 additions & 1 deletion src/mail/src/MailServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public function register(): void
}

/**
* Register the Illuminate mailer instance.
* Register the mailer instance.
*
* The method name is retained for compatibility with Laravel's protected extension point.
*/
protected function registerIlluminateMailer(): void
{
Expand Down
Loading