From fd68bd6baee5d1cacdb5caf231b55b148765fb11 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 11 Oct 2022 08:49:28 -0500 Subject: [PATCH] [9.x] Alternative Mailable Syntax (#44462) * working on cleaner mailables * continue work * work on headers * add file * organize methods * add doc blocks * add address support to mail to, cc, bcc * adding tests * add more testing * formatting * update stubs * update stubs * add html as an option * Update src/Illuminate/Mail/Mailables/Headers.php Co-authored-by: Tim MacDonald * Update src/Illuminate/Mail/Mailables/Envelope.php Co-authored-by: Tim MacDonald * Update src/Illuminate/Mail/Mailables/Content.php Co-authored-by: Tim MacDonald * Apply fixes from StyleCI * add methods to envelope * fixes and callbacks * map attachables * add methods to other classes * add using method Co-authored-by: Tim MacDonald Co-authored-by: StyleCI Bot --- .../Foundation/Console/MailMakeCommand.php | 6 +- .../Foundation/Console/stubs/mail.stub | 34 +- .../Console/stubs/markdown-mail.stub | 34 +- src/Illuminate/Mail/Attachment.php | 17 + src/Illuminate/Mail/Mailable.php | 220 ++++++++++- src/Illuminate/Mail/Mailables/Address.php | 33 ++ src/Illuminate/Mail/Mailables/Attachment.php | 10 + src/Illuminate/Mail/Mailables/Content.php | 135 +++++++ src/Illuminate/Mail/Mailables/Envelope.php | 369 ++++++++++++++++++ src/Illuminate/Mail/Mailables/Headers.php | 100 +++++ tests/Mail/MailableAlternativeSyntaxTest.php | 92 +++++ 11 files changed, 1037 insertions(+), 13 deletions(-) create mode 100644 src/Illuminate/Mail/Mailables/Address.php create mode 100644 src/Illuminate/Mail/Mailables/Attachment.php create mode 100644 src/Illuminate/Mail/Mailables/Content.php create mode 100644 src/Illuminate/Mail/Mailables/Envelope.php create mode 100644 src/Illuminate/Mail/Mailables/Headers.php create mode 100644 tests/Mail/MailableAlternativeSyntaxTest.php diff --git a/src/Illuminate/Foundation/Console/MailMakeCommand.php b/src/Illuminate/Foundation/Console/MailMakeCommand.php index 25c4c211892e..fe0982933854 100644 --- a/src/Illuminate/Foundation/Console/MailMakeCommand.php +++ b/src/Illuminate/Foundation/Console/MailMakeCommand.php @@ -87,7 +87,11 @@ protected function writeMarkdownTemplate() */ protected function buildClass($name) { - $class = parent::buildClass($name); + $class = str_replace( + '{{ subject }}', + Str::headline(str_replace($this->getNamespace($name).'\\', '', $name)), + parent::buildClass($name) + ); if ($this->option('markdown') !== false) { $class = str_replace(['DummyView', '{{ view }}'], $this->getView(), $class); diff --git a/src/Illuminate/Foundation/Console/stubs/mail.stub b/src/Illuminate/Foundation/Console/stubs/mail.stub index f432a815cec6..45967d8ac3fb 100644 --- a/src/Illuminate/Foundation/Console/stubs/mail.stub +++ b/src/Illuminate/Foundation/Console/stubs/mail.stub @@ -5,6 +5,8 @@ namespace {{ namespace }}; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; +use Illuminate\Mail\Mailables\Content; +use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; class {{ class }} extends Mailable @@ -22,12 +24,36 @@ class {{ class }} extends Mailable } /** - * Build the message. + * Get the message envelope. * - * @return $this + * @return \Illuminate\Mail\Mailables\Envelope */ - public function build() + public function envelope() { - return $this->view('view.name'); + return new Envelope( + subject: '{{ subject }}', + ); + } + + /** + * Get the message content definition. + * + * @return \Illuminate\Mail\Mailables\Content + */ + public function content() + { + return new Content( + view: 'view.name', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments() + { + return []; } } diff --git a/src/Illuminate/Foundation/Console/stubs/markdown-mail.stub b/src/Illuminate/Foundation/Console/stubs/markdown-mail.stub index e4c7cd4b93fa..76b6ccc53b25 100644 --- a/src/Illuminate/Foundation/Console/stubs/markdown-mail.stub +++ b/src/Illuminate/Foundation/Console/stubs/markdown-mail.stub @@ -5,6 +5,8 @@ namespace {{ namespace }}; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Mail\Mailable; +use Illuminate\Mail\Mailables\Content; +use Illuminate\Mail\Mailables\Envelope; use Illuminate\Queue\SerializesModels; class {{ class }} extends Mailable @@ -22,12 +24,36 @@ class {{ class }} extends Mailable } /** - * Build the message. + * Get the message envelope. * - * @return $this + * @return \Illuminate\Mail\Mailables\Envelope */ - public function build() + public function envelope() { - return $this->markdown('{{ view }}'); + return new Envelope( + subject: '{{ subject }}', + ); + } + + /** + * Get the message content definition. + * + * @return \Illuminate\Mail\Mailables\Content + */ + public function content() + { + return new Content( + markdown: '{{ view }}', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments() + { + return []; } } diff --git a/src/Illuminate/Mail/Attachment.php b/src/Illuminate/Mail/Attachment.php index 7105090f987b..5c78ddf394dc 100644 --- a/src/Illuminate/Mail/Attachment.php +++ b/src/Illuminate/Mail/Attachment.php @@ -152,4 +152,21 @@ public function attachTo($mail) fn ($data) => $mail->attachData($data(), $this->as, ['mime' => $this->mime]) ); } + + /** + * Determine if the given attachment is equivalent to this attachment. + * + * @param \Illuminate\Mail\Attachment $attachment + * @return bool + */ + public function isEquivalent(Attachment $attachment) + { + return $this->attachWith( + fn ($path) => [$path, ['as' => $this->as, 'mime' => $this->mime]], + fn ($data) => [$data(), ['as' => $this->as, 'mime' => $this->mime]], + ) === $attachment->attachWith( + fn ($path) => [$path, ['as' => $attachment->as, 'mime' => $attachment->mime]], + fn ($data) => [$data(), ['as' => $attachment->as, 'mime' => $attachment->mime]], + ); + } } diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index 5888a4aba7c2..4fffe502d8ed 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -194,7 +194,7 @@ class Mailable implements MailableContract, Renderable public function send($mailer) { return $this->withLocale($this->locale, function () use ($mailer) { - Container::getInstance()->call([$this, 'build']); + $this->prepareMailableForDelivery(); $mailer = $mailer instanceof MailFactory ? $mailer->mailer($this->mailer) @@ -275,7 +275,7 @@ protected function newQueuedJob() public function render() { return $this->withLocale($this->locale, function () { - Container::getInstance()->call([$this, 'build']); + $this->prepareMailableForDelivery(); return Container::getInstance()->make('mailer')->render( $this->buildView(), $this->buildViewData() @@ -728,6 +728,8 @@ protected function normalizeRecipient($recipient) return (object) ['email' => $recipient]; } elseif ($recipient instanceof Address) { return (object) ['email' => $recipient->getAddress(), 'name' => $recipient->getName()]; + } elseif ($recipient instanceof Mailables\Address) { + return (object) ['email' => $recipient->address, 'name' => $recipient->name]; } return $recipient; @@ -756,6 +758,10 @@ protected function hasRecipient($address, $name = null, $property = 'to') 'address' => $expected->email, ]; + if ($this->hasEnvelopeRecipient($expected['address'], $expected['name'], $property)) { + return true; + } + return collect($this->{$property})->contains(function ($actual) use ($expected) { if (! isset($expected['name'])) { return $actual['address'] == $expected['address']; @@ -765,6 +771,25 @@ protected function hasRecipient($address, $name = null, $property = 'to') }); } + /** + * Determine if the mailable "envelope" method defines a recipient. + * + * @param string $address + * @param string|null $name + * @param string $property + * @return bool + */ + private function hasEnvelopeRecipient($address, $name, $property) + { + return method_exists($this, 'envelope') && match ($property) { + 'from' => $this->envelope()->isFrom($address, $name), + 'to' => $this->envelope()->hasTo($address, $name), + 'cc' => $this->envelope()->hasCc($address, $name), + 'bcc' => $this->envelope()->hasBcc($address, $name), + 'replyTo' => $this->envelope()->hasReplyTo($address, $name), + }; + } + /** * Set the subject of the message. * @@ -786,7 +811,8 @@ public function subject($subject) */ public function hasSubject($subject) { - return $this->subject === $subject; + return $this->subject === $subject || + (method_exists($this, 'envelope') && $this->envelope()->hasSubject($subject)); } /** @@ -922,6 +948,10 @@ public function hasAttachment($file, array $options = []) $file = $file->toMailAttachment(); } + if ($file instanceof Attachment && $this->hasEnvelopeAttachment($file)) { + return true; + } + if ($file instanceof Attachment) { $parts = $file->attachWith( fn ($path) => [$path, ['as' => $file->as, 'mime' => $file->mime]], @@ -942,6 +972,25 @@ public function hasAttachment($file, array $options = []) ); } + /** + * Determine if the mailable has the given envelope attachment. + * + * @param \Illuminate\Mail\Attachment $attachment + * @return bool + */ + private function hasEnvelopeAttachment($attachment) + { + if (! method_exists($this, 'envelope')) { + return false; + } + + $attachments = $this->attachments(); + + return Collection::make(is_object($attachments) ? [$attachments] : $attachments) + ->map(fn ($attached) => $attached instanceof Attachable ? $attached->toMailAttachment() : $attached) + ->contains(fn ($attached) => $attached->isEquivalent($attachment)); + } + /** * Attach a file to the message from storage. * @@ -1059,6 +1108,18 @@ public function tag($value) return $this; } + /** + * Determine if the mailable has the given tag. + * + * @param string $value + * @return bool + */ + public function hasTag($value) + { + return in_array($value, $this->tags) || + (method_exists($this, 'envelope') && in_array($value, $this->envelope()->tags)); + } + /** * Add a metadata header to the message when supported by the underlying transport. * @@ -1073,6 +1134,19 @@ public function metadata($key, $value) return $this; } + /** + * Determine if the mailable has the given metadata. + * + * @param string $key + * @param string $value + * @return bool + */ + public function hasMetadata($key, $value) + { + return (isset($this->metadata[$key]) && $this->metadata[$key] === $value) || + (method_exists($this, 'envelope') && $this->envelope()->hasMetadata($key, $value)); + } + /** * Assert that the given text is present in the HTML email body. * @@ -1269,7 +1343,7 @@ protected function renderForAssertions() } return $this->assertionableRenderStrings = $this->withLocale($this->locale, function () { - Container::getInstance()->call([$this, 'build']); + $this->prepareMailableForDelivery(); $html = Container::getInstance()->make('mailer')->render( $view = $this->buildView(), $this->buildViewData() @@ -1291,6 +1365,144 @@ protected function renderForAssertions() }); } + /** + * Prepare the mailable instance for delivery. + * + * @return void + */ + private function prepareMailableForDelivery() + { + if (method_exists($this, 'build')) { + Container::getInstance()->call([$this, 'build']); + } + + $this->ensureHeadersAreHydrated(); + $this->ensureEnvelopeIsHydrated(); + $this->ensureContentIsHydrated(); + $this->ensureAttachmentsAreHydrated(); + } + + /** + * Ensure the mailable's headers are hydrated from the "headers" method. + * + * @return void + */ + private function ensureHeadersAreHydrated() + { + if (! method_exists($this, 'headers')) { + return; + } + + $headers = $this->headers(); + + $this->withSymfonyMessage(function ($message) use ($headers) { + if ($headers->messageId) { + $message->getHeaders()->addIdHeader('Message-Id', $headers->messageId); + } + + if (count($headers->references) > 0) { + $message->getHeaders()->addTextHeader('References', $headers->referencesString()); + } + + foreach ($headers->text as $key => $value) { + $message->getHeaders()->addTextHeader($key, $value); + } + }); + } + + /** + * Ensure the mailable's "envelope" data is hydrated from the "envelope" method. + * + * @return void + */ + private function ensureEnvelopeIsHydrated() + { + if (! method_exists($this, 'envelope')) { + return; + } + + $envelope = $this->envelope(); + + if (isset($envelope->from)) { + $this->from($envelope->from->address, $envelope->from->name); + } + + foreach (['to', 'cc', 'bcc', 'replyTo'] as $type) { + foreach ($envelope->{$type} as $address) { + $this->{$type}($address->address, $address->name); + } + } + + if ($envelope->subject) { + $this->subject($envelope->subject); + } + + foreach ($envelope->tags as $tag) { + $this->tag($tag); + } + + foreach ($envelope->metadata as $key => $value) { + $this->metadata($key, $value); + } + + foreach ($envelope->using as $callback) { + $this->withSymfonyMessage($callback); + } + } + + /** + * Ensure the mailable's content is hydrated from the "content" method. + * + * @return void + */ + private function ensureContentIsHydrated() + { + if (! method_exists($this, 'content')) { + return; + } + + $content = $this->content(); + + if ($content->view) { + $this->view($content->view); + } + + if ($content->html) { + $this->view($content->html); + } + + if ($content->text) { + $this->text($content->text); + } + + if ($content->markdown) { + $this->markdown($content->markdown); + } + + foreach ($content->with as $key => $value) { + $this->with($key, $value); + } + } + + /** + * Ensure the mailable's attachments are hydrated from the "attachments" method. + * + * @return void + */ + private function ensureAttachmentsAreHydrated() + { + if (! method_exists($this, 'attachments')) { + return; + } + + $attachments = $this->attachments(); + + Collection::make(is_object($attachments) ? [$attachments] : $attachments) + ->each(function ($attachment) { + $this->attach($attachment); + }); + } + /** * Set the name of the mailer that should send the message. * diff --git a/src/Illuminate/Mail/Mailables/Address.php b/src/Illuminate/Mail/Mailables/Address.php new file mode 100644 index 000000000000..be54a24a7413 --- /dev/null +++ b/src/Illuminate/Mail/Mailables/Address.php @@ -0,0 +1,33 @@ +address = $address; + $this->name = $name; + } +} diff --git a/src/Illuminate/Mail/Mailables/Attachment.php b/src/Illuminate/Mail/Mailables/Attachment.php new file mode 100644 index 000000000000..e11d2e96e169 --- /dev/null +++ b/src/Illuminate/Mail/Mailables/Attachment.php @@ -0,0 +1,10 @@ +view = $view; + $this->html = $html; + $this->text = $text; + $this->markdown = $markdown; + $this->with = $with; + } + + /** + * Set the view for the message. + * + * @param string $view + * @return $this + */ + public function view(string $view) + { + $this->view = $view; + + return $this; + } + + /** + * Set the view for the message. + * + * @param string $view + * @return $this + */ + public function html(string $view) + { + return $this->view($view); + } + + /** + * Set the plain text view for the message. + * + * @param string $view + * @return $this + */ + public function text(string $view) + { + $this->text = $view; + + return $this; + } + + /** + * Set the Markdown view for the message. + * + * @param string $view + * @return $this + */ + public function markdown(string $view) + { + $this->markdown = $view; + + return $this; + } + + /** + * Add a piece of view data to the message. + * + * @param string $key + * @param mixed|null $value + * @return $this + */ + public function with($key, $value = null) + { + if (is_array($key)) { + $this->with = array_merge($this->with, $key); + } else { + $this->with[$key] = $value; + } + + return $this; + } +} diff --git a/src/Illuminate/Mail/Mailables/Envelope.php b/src/Illuminate/Mail/Mailables/Envelope.php new file mode 100644 index 000000000000..4d3a6211ab2b --- /dev/null +++ b/src/Illuminate/Mail/Mailables/Envelope.php @@ -0,0 +1,369 @@ +from = is_string($from) ? new Address($from) : $from; + $this->to = $this->normalizeAddresses($to); + $this->cc = $this->normalizeAddresses($cc); + $this->bcc = $this->normalizeAddresses($bcc); + $this->replyTo = $this->normalizeAddresses($replyTo); + $this->subject = $subject; + $this->tags = $tags; + $this->metadata = $metadata; + $this->using = Arr::wrap($using); + } + + /** + * Normalize the given array of addresses. + * + * @param array $addresses + * @return array + */ + protected function normalizeAddresses($addresses) + { + return collect($addresses)->map(function ($address) { + return is_string($address) ? new Address($address) : $address; + })->all(); + } + + /** + * Specify who the message will be "from". + * + * @param \Illuminate\Mail\Mailables\Address|string $address + * @param string|null $name + * @return $this + */ + public function from(Address|string $address, $name = null) + { + $this->from = is_string($address) ? new Address($address, $name) : $address; + + return $this; + } + + /** + * Add a "to" recipient to the message envelope. + * + * @param \Illuminate\Mail\Mailables\Address|array|string $address + * @param string|null $name + * @return $this + */ + public function to(Address|array|string $address, $name = null) + { + $this->to = array_merge($this->to, $this->normalizeAddresses( + is_string($name) ? [new Address($address, $name)] : Arr::wrap($address), + )); + + return $this; + } + + /** + * Add a "cc" recipient to the message envelope. + * + * @param \Illuminate\Mail\Mailables\Address|array|string $address + * @param string|null $name + * @return $this + */ + public function cc(Address|array|string $address, $name = null) + { + $this->cc = array_merge($this->cc, $this->normalizeAddresses( + is_string($name) ? [new Address($address, $name)] : Arr::wrap($address), + )); + + return $this; + } + + /** + * Add a "bcc" recipient to the message envelope. + * + * @param \Illuminate\Mail\Mailables\Address|array|string $address + * @param string|null $name + * @return $this + */ + public function bcc(Address|array|string $address, $name = null) + { + $this->bcc = array_merge($this->bcc, $this->normalizeAddresses( + is_string($name) ? [new Address($address, $name)] : Arr::wrap($address), + )); + + return $this; + } + + /** + * Add a "reply to" recipient to the message envelope. + * + * @param \Illuminate\Mail\Mailables\Address|array|string $address + * @param string|null $name + * @return $this + */ + public function replyTo(Address|array|string $address, $name = null) + { + $this->replyTo = array_merge($this->replyTo, $this->normalizeAddresses( + is_string($name) ? [new Address($address, $name)] : Arr::wrap($address), + )); + + return $this; + } + + /** + * Set the subject of the message. + * + * @param string $subject + * @return $this + */ + public function subject(string $subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * Add "tags" to the message. + * + * @param array $tags + * @return $this + */ + public function tags(array $tags) + { + $this->tags = array_merge($this->tags, $tags); + + return $this; + } + + /** + * Add a "tag" to the message. + * + * @param string $tag + * @return $this + */ + public function tag(string $tag) + { + $this->tags[] = $tag; + + return $this; + } + + /** + * Add metadata to the message. + * + * @param string $key + * @param string|int $value + * @return $this + */ + public function metadata(string $key, string|int $value) + { + $this->metadata[$key] = $value; + + return $this; + } + + /** + * Add a Symfony Message customization callback to the message. + * + * @param \Closure $callback + * @return $this + */ + public function using(Closure $callback) + { + $this->using[] = $callback; + + return $this; + } + + /** + * Determine if the message is from the given address. + * + * @param string $address + * @param string|null $name + * @return bool + */ + public function isFrom(string $address, string $name = null) + { + if (is_null($name)) { + return $this->from->address === $address; + } + + return $this->from->address === $address && + $this->from->name === $name; + } + + /** + * Determine if the message has the given address as a recipient. + * + * @param string $address + * @param string|null $name + * @return bool + */ + public function hasTo(string $address, string $name = null) + { + return $this->hasRecipient($this->to, $address, $name); + } + + /** + * Determine if the message has the given address as a "cc" recipient. + * + * @param string $address + * @param string|null $name + * @return bool + */ + public function hasCc(string $address, string $name = null) + { + return $this->hasRecipient($this->cc, $address, $name); + } + + /** + * Determine if the message has the given address as a "bcc" recipient. + * + * @param string $address + * @param string|null $name + * @return bool + */ + public function hasBcc(string $address, string $name = null) + { + return $this->hasRecipient($this->bcc, $address, $name); + } + + /** + * Determine if the message has the given address as a "reply to" recipient. + * + * @param string $address + * @param string|null $name + * @return bool + */ + public function hasReplyTo(string $address, string $name = null) + { + return $this->hasRecipient($this->replyTo, $address, $name); + } + + /** + * Determine if the message has the given recipient. + * + * @param array $recipients + * @param string $address + * @param string|null $name + * @return bool + */ + protected function hasRecipient(array $recipients, string $address, ?string $name = null) + { + return collect($recipients)->contains(function ($recipient) use ($address, $name) { + if (is_null($name)) { + return $recipient->address === $address; + } + + return $recipient->address === $address && + $recipient->name === $name; + }); + } + + /** + * Determine if the message has the given subject. + * + * @param string $subject + * @return bool + */ + public function hasSubject(string $subject) + { + return $this->subject === $subject; + } + + /** + * Determine if the message has the given metadata. + * + * @param string $key + * @param string $value + * @return bool + */ + public function hasMetadata(string $key, string $value) + { + return isset($this->metadata[$key]) && $this->metadata[$key] === $value; + } +} diff --git a/src/Illuminate/Mail/Mailables/Headers.php b/src/Illuminate/Mail/Mailables/Headers.php new file mode 100644 index 000000000000..87cee52b4768 --- /dev/null +++ b/src/Illuminate/Mail/Mailables/Headers.php @@ -0,0 +1,100 @@ +messageId = $messageId; + $this->references = $references; + $this->text = $text; + } + + /** + * Set the message ID. + * + * @param string $messageId + * @return $this + */ + public function messageId(string $messageId) + { + $this->messageId = $messageId; + + return $this; + } + + /** + * Set the message IDs referenced by this message. + * + * @param array $references + * @return $this + */ + public function references(array $references) + { + $this->references = array_merge($this->references, $references); + + return $this; + } + + /** + * Set the headers for this message. + * + * @param array $references + * @return $this + */ + public function text(array $text) + { + $this->text = array_merge($this->text, $text); + + return $this; + } + + /** + * Get the references header as a string. + * + * @return string + */ + public function referencesString(): string + { + return collect($this->references)->map(function ($messageId) { + return Str::finish(Str::start($messageId, '<'), '>'); + })->implode(' '); + } +} diff --git a/tests/Mail/MailableAlternativeSyntaxTest.php b/tests/Mail/MailableAlternativeSyntaxTest.php new file mode 100644 index 000000000000..82794208a10e --- /dev/null +++ b/tests/Mail/MailableAlternativeSyntaxTest.php @@ -0,0 +1,92 @@ +assertTrue($mailable->hasTo('taylor@laravel.com')); + $this->assertTrue($mailable->hasCc('adam@laravel.com')); + $this->assertTrue($mailable->hasBcc('tyler@laravel.com')); + $this->assertTrue($mailable->hasTo('taylor@laravel.com', 'Taylor Otwell')); + $this->assertFalse($mailable->hasTo('taylor@laravel.com', 'Wrong Name')); + + $mailable->to(new Address('abigail@laravel.com', 'Abigail Otwell')); + $this->assertTrue($mailable->hasTo('taylor@laravel.com', 'Taylor Otwell')); + + $this->assertTrue($mailable->hasSubject('Test Subject')); + $this->assertFalse($mailable->hasSubject('Wrong Subject')); + $this->assertTrue($mailable->hasTag('tag-1')); + $this->assertTrue($mailable->hasMetadata('test-meta', 'test-meta-value')); + + $reflection = new ReflectionClass($mailable); + $method = $reflection->getMethod('prepareMailableForDelivery'); + $method->setAccessible(true); + $method->invoke($mailable); + + $this->assertEquals('test-view', $mailable->view); + $this->assertEquals(['test-data-key' => 'test-data-value'], $mailable->viewData); + $this->assertEquals(2, count($mailable->to)); + $this->assertEquals(1, count($mailable->cc)); + $this->assertEquals(1, count($mailable->bcc)); + } + + public function testEnvelopesCanReceiveAdditionalRecipients() + { + $envelope = new Envelope(to: ['taylor@example.com']); + $envelope->to(new Address('taylorotwell@example.com')); + + $this->assertCount(2, $envelope->to); + $this->assertEquals('taylor@example.com', $envelope->to[0]->address); + $this->assertEquals('taylorotwell@example.com', $envelope->to[1]->address); + + $envelope->to('abigailotwell@example.com', 'Abigail Otwell'); + $this->assertEquals('abigailotwell@example.com', $envelope->to[2]->address); + $this->assertEquals('Abigail Otwell', $envelope->to[2]->name); + + $envelope->to('adam@example.com'); + $this->assertEquals('adam@example.com', $envelope->to[3]->address); + $this->assertNull($envelope->to[3]->name); + + $envelope->to(['jeffrey@example.com', 'tyler@example.com']); + $this->assertEquals('jeffrey@example.com', $envelope->to[4]->address); + $this->assertEquals('tyler@example.com', $envelope->to[5]->address); + + $envelope->from('dries@example.com', 'Dries Vints'); + $this->assertEquals('dries@example.com', $envelope->from->address); + $this->assertEquals('Dries Vints', $envelope->from->name); + } +} + +class MailableWithAlternativeSyntax extends Mailable +{ + public function envelope() + { + return new Envelope( + to: [new Address('taylor@laravel.com', 'Taylor Otwell')], + cc: [new Address('adam@laravel.com', 'Adam Wathan')], + bcc: [new Address('tyler@laravel.com', 'Tyler Blair')], + subject: 'Test Subject', + tags: ['tag-1', 'tag-2'], + metadata: ['test-meta' => 'test-meta-value'], + ); + } + + public function content() + { + return new Content( + view: 'test-view', + with: ['test-data-key' => 'test-data-value'], + ); + } +}