diff --git a/composer.json b/composer.json index 998111cd0..6fd1f052e 100755 --- a/composer.json +++ b/composer.json @@ -21,7 +21,6 @@ "pda/pheanstalk": "~4.0", "phpseclib/phpseclib": "~2.0", "predis/predis": "^1.1", - "swiftmailer/swiftmailer": "^6.0", "symfony/browser-kit": "~6.4", "symfony/console": "~6.4", "symfony/css-selector": "~6.4", @@ -31,6 +30,7 @@ "symfony/finder": "~6.4", "symfony/http-foundation": "~6.4", "symfony/http-kernel": "~6.4", + "symfony/mailer": "^6.4", "symfony/mime": "~6.4", "symfony/process": "~6.4", "symfony/routing": "~6.4", diff --git a/src/Illuminate/Auth/Reminders/PasswordBroker.php b/src/Illuminate/Auth/Reminders/PasswordBroker.php index 6b914b464..979163f83 100755 --- a/src/Illuminate/Auth/Reminders/PasswordBroker.php +++ b/src/Illuminate/Auth/Reminders/PasswordBroker.php @@ -46,35 +46,35 @@ class PasswordBroker { * * @var \Illuminate\Auth\Reminders\ReminderRepositoryInterface $reminders */ - protected $reminders; + protected ReminderRepositoryInterface $reminders; /** * The user provider implementation. * * @var \Illuminate\Auth\UserProviderInterface */ - protected $users; + protected UserProviderInterface $users; /** * The mailer instance. * * @var \Illuminate\Mail\Mailer */ - protected $mailer; + protected Mailer $mailer; /** * The view of the password reminder e-mail. * * @var string */ - protected $reminderView; + protected string $reminderView; /** * The custom password validator callback. * * @var \Closure */ - protected $passwordValidator; + protected Closure $passwordValidator; /** * Create a new password broker instance. @@ -82,13 +82,13 @@ class PasswordBroker { * @param \Illuminate\Auth\Reminders\ReminderRepositoryInterface $reminders * @param \Illuminate\Auth\UserProviderInterface $users * @param \Illuminate\Mail\Mailer $mailer - * @param string $reminderView + * @param string $reminderView * @return void */ public function __construct(ReminderRepositoryInterface $reminders, - UserProviderInterface $users, - Mailer $mailer, - $reminderView) + UserProviderInterface $users, + Mailer $mailer, + string $reminderView) { $this->users = $users; $this->mailer = $mailer; @@ -103,14 +103,14 @@ public function __construct(ReminderRepositoryInterface $reminders, * @param \Closure $callback * @return string */ - public function remind(array $credentials, Closure $callback = null) + public function remind(array $credentials, Closure $callback = null): string { // First we will check to see if we found a user at the given credentials and // if we did not we will redirect back to this current URI with a piece of // "flash" data in the session to indicate to the developers the errors. $user = $this->getUser($credentials); - if (is_null($user)) + if ($user === null) { return self::INVALID_USER; } @@ -129,22 +129,24 @@ public function remind(array $credentials, Closure $callback = null) * Send the password reminder e-mail. * * @param \Illuminate\Auth\Reminders\RemindableInterface $user - * @param string $token - * @param \Closure $callback - * @return int + * @param string $token + * @param Closure $callback + * @return void */ - public function sendReminder(RemindableInterface $user, $token, Closure $callback = null) + public function sendReminder(RemindableInterface $user, string $token, Closure $callback = null): void { // We will use the reminder view that was given to the broker to display the // password reminder e-mail. We'll pass a "token" variable into the views // so that it may be displayed for an user to click for password reset. $view = $this->reminderView; - return $this->mailer->send($view, compact('token', 'user'), function($m) use ($user, $token, $callback) + $this->mailer->send($view, compact('token', 'user'), function($m) use ($user, $token, $callback) { $m->to($user->getReminderEmail()); - if ( ! is_null($callback)) call_user_func($callback, $m, $user, $token); + if ($callback !== null) { + call_user_func($callback, $m, $user, $token); + } }); } @@ -153,10 +155,10 @@ public function sendReminder(RemindableInterface $user, $token, Closure $callbac * * @param array $credentials * @param \Closure $callback - * @return mixed - */ - public function reset(array $credentials, Closure $callback) - { + * @return RemindableInterface|string|int + */ + public function reset(array $credentials, Closure $callback): RemindableInterface|string|int + { // If the responses from the validate method is not a user instance, we will // assume that it is a redirect and simply return it from this method and // the user is properly redirected having an error message on the post. @@ -179,25 +181,26 @@ public function reset(array $credentials, Closure $callback) return self::PASSWORD_RESET; } - /** - * Validate a password reset for the given credentials. - * - * @param array $credentials - * @return \Illuminate\Auth\Reminders\RemindableInterface - */ - protected function validateReset(array $credentials) - { - if (is_null($user = $this->getUser($credentials))) + /** + * Validate a password reset for the given credentials. + * + * @param array $credentials + * @return RemindableInterface|int|string + */ + protected function validateReset(array $credentials): RemindableInterface|int|string + { + $user = $this->getUser($credentials); + if ($user === null) { return self::INVALID_USER; } - if ( ! $this->validNewPasswords($credentials)) + if (!$this->validNewPasswords($credentials)) { return self::INVALID_PASSWORD; } - if ( ! $this->reminders->exists($user, $credentials['token'])) + if (!$this->reminders->exists($user, $credentials['token'])) { return self::INVALID_TOKEN; } @@ -211,8 +214,8 @@ protected function validateReset(array $credentials) * @param \Closure $callback * @return void */ - public function validator(Closure $callback) - { + public function validator(Closure $callback): void + { $this->passwordValidator = $callback; } @@ -222,9 +225,9 @@ public function validator(Closure $callback) * @param array $credentials * @return bool */ - protected function validNewPasswords(array $credentials) - { - list($password, $confirm) = array($credentials['password'], $credentials['password_confirmation']); + protected function validNewPasswords(array $credentials): bool + { + list($password, $confirm) = [$credentials['password'], $credentials['password_confirmation']]; if (isset($this->passwordValidator)) { @@ -240,24 +243,23 @@ protected function validNewPasswords(array $credentials) * @param array $credentials * @return bool */ - protected function validatePasswordWithDefaults(array $credentials) - { + protected function validatePasswordWithDefaults(array $credentials): bool + { list($password, $confirm) = [$credentials['password'], $credentials['password_confirmation']]; return $password === $confirm && mb_strlen((string) $password) >= 6; } - /** - * Get the user for the given credentials. - * - * @param array $credentials - * @return \Illuminate\Auth\Reminders\RemindableInterface - * - * @throws \UnexpectedValueException - */ - public function getUser(array $credentials) - { - $credentials = array_except($credentials, array('token')); + /** + * Get the user for the given credentials. + * + * @param array $credentials + * @return RemindableInterface|null + * @throws \UnexpectedValueException + */ + public function getUser(array $credentials): ?RemindableInterface + { + $credentials = array_except($credentials, ['token']); $user = $this->users->retrieveByCredentials($credentials); @@ -274,8 +276,8 @@ public function getUser(array $credentials) * * @return \Illuminate\Auth\Reminders\ReminderRepositoryInterface */ - protected function getRepository() - { + protected function getRepository(): ReminderRepositoryInterface + { return $this->reminders; } diff --git a/src/Illuminate/Mail/MailServiceProvider.php b/src/Illuminate/Mail/MailServiceProvider.php index 2598e3c99..e9be1f943 100755 --- a/src/Illuminate/Mail/MailServiceProvider.php +++ b/src/Illuminate/Mail/MailServiceProvider.php @@ -1,13 +1,13 @@ app->bindShared('mailer', function($app) use ($me) { - $me->registerSwiftMailer(); + $me->registerSymfonyMailer(); // Once we have create 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. $mailer = new Mailer( - $app['view'], $app['swift.mailer'], $app['events'] + $app['view'], $app['symfony.transport'], $app['events'] ); $this->setMailerDependencies($mailer, $app); @@ -68,7 +68,7 @@ public function register() * @param \Illuminate\Foundation\Application $app * @return void */ - protected function setMailerDependencies($mailer, $app) + protected function setMailerDependencies(Mailer $mailer, Application $app): void { $mailer->setContainer($app); @@ -83,179 +83,167 @@ protected function setMailerDependencies($mailer, $app) } } - /** - * Register the Swift Mailer instance. - * - * @return void - */ - public function registerSwiftMailer() - { - $config = $this->app['config']['mail']; - - $this->registerSwiftTransport($config); - - // Once we have the transporter registered, we will register the actual Swift - // mailer instance, passing in the transport instances, which allows us to - // override this transporter instances during app start-up if necessary. - $this->app['swift.mailer'] = $this->app->share(function($app) - { - return new Swift_Mailer($app['swift.transport']); - }); - } - - /** - * Register the Swift Transport instance. - * - * @param array $config - * @return void - * - * @throws \InvalidArgumentException - */ - protected function registerSwiftTransport($config) - { - switch ($config['driver']) - { - case 'smtp': - return $this->registerSmtpTransport($config); - - case 'sendmail': - return $this->registerSendmailTransport($config); - - case 'mail': - return $this->registerMailTransport($config); - - case 'mailgun': - return $this->registerMailgunTransport($config); - - case 'mandrill': - return $this->registerMandrillTransport($config); - - case 'log': - return $this->registerLogTransport($config); - - default: - throw new \InvalidArgumentException('Invalid mail driver.'); - } - } + public function registerSymfonyMailer(): void + { + $config = $this->app['config']['mail']; + + switch ($config['driver']) + { + case 'smtp': + $this->registerSmtpTransport($config); + break; + case 'sendmail': + $this->registerSendmailTransport($config); + break; + case 'mail': + $this->registerMailTransport($config); + break; +// case 'mailgun': +// $this->registerMailgunTransport($config); +// break; +// case 'mandrill': +// $this->registerMandrillTransport($config); +// break; + case 'log': + $this->registerLogTransport($config); + break; + default: + throw new \InvalidArgumentException('Invalid mail driver.'); + } + } /** - * Register the SMTP Swift Transport instance. + * Register the SMTP symfony Transport instance. * * @param array $config * @return void */ - protected function registerSmtpTransport($config) + protected function registerSmtpTransport(array $config): void { - $this->app['swift.transport'] = $this->app->share(function($app) use ($config) + $this->app['symfony.transport'] = $this->app->share(function($app) use ($config) { - extract($config); - - // The Swift SMTP transport instance will allow us to use any SMTP backend - // for delivering mail such as Sendgrid, Amazon SES, or a custom server - // a developer has available. We will just pass this configured host. - $transport = new SmtpTransport($host, $port); - - if (isset($encryption)) - { - $transport->setEncryption($encryption); - } - - // Once we have the transport we will check for the presence of a username - // and password. If we have it we will set the credentials on the Swift - // transporter instance so that we'll properly authenticate delivery. - if (isset($username)) - { - $transport->setUsername($username); - - $transport->setPassword($password); - } + $factory = new EsmtpTransportFactory(); + + $scheme = $config['scheme'] ?? null; + + if (! $scheme) { + $scheme = ! empty($config['encryption']) && $config['encryption'] === 'tls' + ? (($config['port'] == 465) ? 'smtps' : 'smtp') + : ''; + } + + /** @var EsmtpTransport $transport */ + $transport = $factory->create(new Dsn( + $scheme, + $config['host'], + $config['username'] ?? null, + $config['password'] ?? null, + $config['port'] ?? null, + $config + )); + + $stream = $transport->getStream(); + + if ($stream instanceof SocketStream) { + if (isset($config['source_ip'])) { + $stream->setSourceIp($config['source_ip']); + } + + if (isset($config['timeout'])) { + $stream->setTimeout($config['timeout']); + } + } return $transport; }); } /** - * Register the Sendmail Swift Transport instance. + * Register the Sendmail Symfony Transport instance. * * @param array $config * @return void */ - protected function registerSendmailTransport($config) + protected function registerSendmailTransport(array $config): void { - $this->app['swift.transport'] = $this->app->share(function($app) use ($config) - { - return SendmailTransport::newInstance($config['sendmail']); - }); + $this->app['symfony.transport'] = $this->app->share(fn($app) => new SendmailTransport( + $config['path'] ?? $app['config']->get('mail.sendmail') + )); } /** - * Register the Mail Swift Transport instance. + * Register the Mail Symfony Transport instance. * * @param array $config * @return void */ - protected function registerMailTransport($config) + protected function registerMailTransport(array $config): void { - $this->app['swift.transport'] = $this->app->share(function() - { - return MailTransport::newInstance(); - }); + $this->app['symfony.transport'] = $this->app->share(fn() => new SendmailTransport()); } /** - * Register the Mailgun Swift Transport instance. + * Register the Mailgun Symfony Transport instance. * * @param array $config * @return void */ - protected function registerMailgunTransport($config) - { - $mailgun = $this->app['config']->get('services.mailgun', array()); - - $this->app->bindShared('swift.transport', function() use ($mailgun) - { - return new MailgunTransport($mailgun['secret'], $mailgun['domain']); - }); - } +// protected function registerMailgunTransport(array $config): void +// { +// $this->app->bindShared('symfony.transport', function() use ($config) +// { +// $factory = new MailgunTransportFactory(null, $this->getHttpClient($config)); +// +// if (! isset($config['secret'])) { +// $config = $this->app['config']->get('services.mailgun', []); +// } +// +// return $factory->create(new Dsn( +// 'mailgun+'.($config['scheme'] ?? 'https'), +// $config['endpoint'] ?? 'default', +// $config['secret'], +// $config['domain'] +// )); +// }); +// } /** - * Register the Mandrill Swift Transport instance. + * Register the "Log" Symfony Transport instance. * * @param array $config * @return void */ - protected function registerMandrillTransport($config) + protected function registerLogTransport(array $config): void { - $mandrill = $this->app['config']->get('services.mandrill', array()); - - $this->app->bindShared('swift.transport', function() use ($mandrill) - { - return new MandrillTransport($mandrill['secret']); - }); + $this->app->bindShared('symfony.transport', fn($app) => new LogTransport($app->make('Psr\Log\LoggerInterface'))); } - /** - * Register the "Log" Swift Transport instance. - * - * @param array $config - * @return void - */ - protected function registerLogTransport($config) - { - $this->app->bindShared('swift.transport', function($app) - { - return new LogTransport($app->make('Psr\Log\LoggerInterface')); - }); - } +// /** +// * Get a configured Symfony HTTP client instance. +// * +// * @return \Symfony\Contracts\HttpClient\HttpClientInterface|null +// */ +// protected function getHttpClient(array $config): ?HttpClientInterface +// { +// $clientOptions = $config['client'] ?? false; +// if ($clientOptions) { +// $maxHostConnections = Arr::pull($clientOptions, 'max_host_connections', 6); +// $maxPendingPushes = Arr::pull($clientOptions, 'max_pending_pushes', 50); +// +// return HttpClient::create($clientOptions, $maxHostConnections, $maxPendingPushes); +// } +// +// return null; +// } /** * Get the services provided by the provider. * * @return array */ - public function provides() + public function provides(): array { - return array('mailer', 'swift.mailer', 'swift.transport'); + return ['mailer', 'symfony.transport']; } } diff --git a/src/Illuminate/Mail/Mailer.php b/src/Illuminate/Mail/Mailer.php index a83b8de79..c47440968 100755 --- a/src/Illuminate/Mail/Mailer.php +++ b/src/Illuminate/Mail/Mailer.php @@ -1,111 +1,92 @@ views = $views; - $this->swift = $swift; + $this->transport = $transport; $this->events = $events; } - /** - * Set the global from address and name. - * - * @param string $address - * @param string $name - * @return void - */ - public function alwaysFrom($address, $name = null) + /** + * Set the global from address and name. + * @param string $address + * @param ?string $name + * @return void + */ + public function alwaysFrom(string $address, ?string $name = null): void { $this->from = compact('address', 'name'); } @@ -118,20 +99,20 @@ public function alwaysFrom($address, $name = null) * @param mixed $callback * @return int */ - public function plain($view, array $data, $callback) + public function plain(string $view, array $data, mixed $callback): int { - return $this->send(array('text' => $view), $data, $callback); + return $this->send(['text' => $view], $data, $callback); } /** * Send a new message using a view. * - * @param string|array $view + * @param array|string $view * @param array $data - * @param \Closure|string $callback + * @param string|\Closure $callback * @return void */ - public function send($view, array $data, $callback) + public function send(array|string $view, array $data, string|Closure $callback): void { // First we need to parse the view, which could either be a string or an array // containing both an HTML and plain text versions of the view which should @@ -147,22 +128,21 @@ public function send($view, array $data, $callback) // to creating view based emails that are able to receive arrays of data. $this->addContent($message, $view, $plain, $data); - $message = $message->getSwiftMessage(); - - $this->sendSwiftMessage($message); + $message = $message->getSymfonyMessage(); + $this->sendSymfonyMessage($message); } /** * Queue a new e-mail message for sending. * - * @param string|array $view + * @param array|string $view * @param array $data - * @param \Closure|string $callback - * @param string $queue + * @param string|\Closure $callback + * @param string|null $queue * @return mixed */ - public function queue($view, array $data, $callback, $queue = null) - { + public function queue(array|string $view, array $data, string|Closure $callback, ?string $queue = null): mixed + { $callback = $this->buildQueueCallable($callback); return $this->queue->push('mailer@handleQueuedMessage', compact('view', 'data', 'callback'), $queue); @@ -171,29 +151,29 @@ public function queue($view, array $data, $callback, $queue = null) /** * Queue a new e-mail message for sending on the given queue. * - * @param string $queue - * @param string|array $view + * @param string $queue + * @param array|string $view * @param array $data - * @param \Closure|string $callback + * @param string|\Closure $callback * @return mixed */ - public function queueOn($queue, $view, array $data, $callback) - { + public function queueOn(string $queue, array|string $view, array $data, string|Closure $callback): mixed + { return $this->queue($view, $data, $callback, $queue); } /** * Queue a new e-mail message for sending after (n) seconds. * - * @param int $delay + * @param int $delay * @param string|array $view * @param array $data * @param \Closure|string $callback - * @param string $queue + * @param ?string $queue * @return mixed */ - public function later($delay, $view, array $data, $callback, $queue = null) - { + public function later(int $delay, string|array $view, array $data, Closure|string $callback, ?string $queue = null): mixed + { $callback = $this->buildQueueCallable($callback); return $this->queue->later($delay, 'mailer@handleQueuedMessage', compact('view', 'data', 'callback'), $queue); @@ -209,8 +189,8 @@ public function later($delay, $view, array $data, $callback, $queue = null) * @param \Closure|string $callback * @return mixed */ - public function laterOn($queue, $delay, $view, array $data, $callback) - { + public function laterOn(string $queue, int $delay, string|array $view, array $data, Closure|string $callback): mixed + { return $this->later($delay, $view, $data, $callback, $queue); } @@ -220,9 +200,11 @@ public function laterOn($queue, $delay, $view, array $data, $callback) * @param mixed $callback * @return mixed */ - protected function buildQueueCallable($callback) - { - if ( ! $callback instanceof Closure) return $callback; + protected function buildQueueCallable(mixed $callback): mixed + { + if (!$callback instanceof Closure) { + return $callback; + } return serialize(new SerializableClosure($callback)); } @@ -234,10 +216,9 @@ protected function buildQueueCallable($callback) * @param array $data * @return void */ - public function handleQueuedMessage($job, $data) - { + public function handleQueuedMessage(Job $job, array $data): void + { $this->send($data['view'], $data['data'], $this->getQueuedCallable($data)); - $job->delete(); } @@ -247,8 +228,8 @@ public function handleQueuedMessage($job, $data) * @param array $data * @return mixed */ - protected function getQueuedCallable(array $data) - { + protected function getQueuedCallable(array $data): mixed + { if (Str::contains($data['callback'], 'SerializableClosure')) { return with(unserialize($data['callback']))->getClosure(); @@ -266,19 +247,24 @@ protected function getQueuedCallable(array $data) * @param array $data * @return void */ - protected function addContent($message, $view, $plain, $data) - { - if (isset($view)) + protected function addContent(Message $message, ?string $view, ?string $plain, array $data): void + { + if ($view !== null) { - $message->setBody($this->getView($view, $data), 'text/html'); + $message->html($this->renderView($view, $data)); } - if (isset($plain)) + if ($plain !== null) { - $message->addPart($this->getView($plain, $data), 'text/plain'); + $message->text($this->renderView($plain, $data)); } } + protected function renderView(string $view, array $data): string + { + return $this->views->make($view, $data)->render(); + } + /** * Parse the given view name or array. * @@ -287,9 +273,11 @@ protected function addContent($message, $view, $plain, $data) * * @throws \InvalidArgumentException */ - protected function parseView($view) - { - if (is_string($view)) return array($view, null); + protected function parseView(string|array $view): array + { + if (is_string($view)) { + return [$view, null]; + } // If the given view is an array with numeric keys, we will just assume that // both a "pretty" and "plain" view were provided, so we will return this @@ -304,46 +292,41 @@ protected function parseView($view) // named keys instead, allowing the developers to use one or the other. elseif (is_array($view)) { - return array( - array_get($view, 'html'), array_get($view, 'text') - ); + return [ + $view['html'] ?? null, + $view['text'] ?? null + ]; } throw new \InvalidArgumentException("Invalid view."); } - /** - * Send a Swift Message instance. - * - * @param \Swift_Message $message - * @return void - */ - protected function sendSwiftMessage($message) - { - if ($this->events) - { - $this->events->fire('mailer.sending', array($message)); - } + /** + * Send a Symfony Message. + */ + protected function sendSymfonyMessage(Email $message): void + { + $this->events?->fire('mailer.sending', [$message]); - if ( ! $this->pretending) - { - $this->swift->send($message, $this->failedRecipients); - } - elseif (isset($this->logger)) - { - $this->logMessage($message); - } - } + if (!$this->pretending) + { + $this->transport->send($message, Envelope::create($message)); + } + elseif (isset($this->logger)) + { + $this->logMessage($message); + } + } /** * Log that a message was sent. - * - * @param \Swift_Message $message - * @return void */ - protected function logMessage($message) + protected function logMessage(Email $message): void { - $emails = implode(', ', array_keys((array) $message->getTo())); + $emails = implode(', ', array_map( + fn (Address $address) => $address->getAddress(), + $message->getTo() + )); $this->logger->info("Pretending to mail message to: {$emails}"); } @@ -351,24 +334,20 @@ protected function logMessage($message) /** * Call the provided message builder. * - * @param \Closure|string $callback - * @param \Illuminate\Mail\Message $message + * @param string|\Closure $callback + * @param \Illuminate\Mail\Message $message * @return mixed - * - * @throws \InvalidArgumentException */ - protected function callMessageBuilder($callback, $message) - { + protected function callMessageBuilder(string|Closure $callback, Message $message): mixed + { if ($callback instanceof Closure) { return call_user_func($callback, $message); } - elseif (is_string($callback)) + else { return $this->container[$callback]->mail($message); } - - throw new \InvalidArgumentException("Callback is not valid."); } /** @@ -376,9 +355,9 @@ protected function callMessageBuilder($callback, $message) * * @return \Illuminate\Mail\Message */ - protected function createMessage() - { - $message = new Message(new Swift_Message); + protected function createMessage(): Message + { + $message = new Message(new Email()); // If a global from address has been specified we will set it on every message // instances so the developer does not have to repeat themselves every time @@ -391,26 +370,14 @@ protected function createMessage() return $message; } - /** - * Render the given view. - * - * @param string $view - * @param array $data - * @return \Illuminate\View\View - */ - protected function getView($view, $data) - { - return $this->views->make($view, $data)->render(); - } - /** * Tell the mailer to not really send messages. * * @param bool $value * @return void */ - public function pretend($value = true) - { + public function pretend(bool $value = true): void + { $this->pretending = $value; } @@ -419,8 +386,8 @@ public function pretend($value = true) * * @return bool */ - public function isPretending() - { + public function isPretending(): bool + { return $this->pretending; } @@ -429,40 +396,36 @@ public function isPretending() * * @return \Illuminate\View\Factory */ - public function getViewFactory() - { + public function getViewFactory(): Factory + { return $this->views; } /** - * Get the Swift Mailer instance. - * - * @return \Swift_Mailer + * Get the Symfony Transport instance. */ - public function getSwiftMailer() + public function getSymfonyTransport(): TransportInterface { - return $this->swift; + return $this->transport; } /** * Get the array of failed recipients. * * @return array + * @deprecated */ - public function failures() - { + public function failures(): array + { return $this->failedRecipients; } /** - * Set the Swift Mailer instance. - * - * @param \Swift_Mailer $swift - * @return void + * Set the Symfony Transport instance. */ - public function setSwiftMailer($swift) + public function setSymfonyTransport(TransportInterface $transport): void { - $this->swift = $swift; + $this->transport = $transport; } /** @@ -471,8 +434,8 @@ public function setSwiftMailer($swift) * @param \Illuminate\Log\Writer $logger * @return $this */ - public function setLogger(Writer $logger) - { + public function setLogger(Writer $logger): static + { $this->logger = $logger; return $this; @@ -484,8 +447,8 @@ public function setLogger(Writer $logger) * @param \Illuminate\Queue\QueueManager $queue * @return $this */ - public function setQueue(QueueManager $queue) - { + public function setQueue(QueueManager $queue): static + { $this->queue = $queue; return $this; @@ -497,8 +460,8 @@ public function setQueue(QueueManager $queue) * @param \Illuminate\Container\Container $container * @return void */ - public function setContainer(Container $container) - { + public function setContainer(Container $container): void + { $this->container = $container; } diff --git a/src/Illuminate/Mail/Message.php b/src/Illuminate/Mail/Message.php index 408557184..714a6e058 100755 --- a/src/Illuminate/Mail/Message.php +++ b/src/Illuminate/Mail/Message.php @@ -1,39 +1,44 @@ swift = $swift; + $this->message = $message; } /** * Add a "from" address to the message. * * @param string $address - * @param string $name + * @param ?string $name * @return $this */ - public function from($address, $name = null) + public function from(string $address, ?string $name = null): self { - $this->swift->setFrom($address, $name); - + $this->message->from(new Address($address, $name ?? '')); return $this; } @@ -41,12 +46,12 @@ public function from($address, $name = null) * Set the "sender" of the message. * * @param string $address - * @param string $name + * @param ?string $name * @return $this */ - public function sender($address, $name = null) + public function sender(string $address, ?string $name = null): self { - $this->swift->setSender($address, $name); + $this->message->sender(new Address($address, $name ?? '')); return $this; } @@ -57,21 +62,20 @@ public function sender($address, $name = null) * @param string $address * @return $this */ - public function returnPath($address) + public function returnPath(string $address): self { - $this->swift->setReturnPath($address); - + $this->message->returnPath($address); return $this; } /** * Add a recipient to the message. * - * @param string|array $address - * @param string $name + * @param string|Address[] $address + * @param ?string $name * @return $this */ - public function to($address, $name = null) + public function to(string|array $address, ?string $name = null): self { return $this->addAddresses($address, $name, 'To'); } @@ -83,7 +87,7 @@ public function to($address, $name = null) * @param string $name * @return $this */ - public function cc($address, $name = null) + public function cc(string $address, ?string $name = null): self { return $this->addAddresses($address, $name, 'Cc'); } @@ -95,7 +99,7 @@ public function cc($address, $name = null) * @param string $name * @return $this */ - public function bcc($address, $name = null) + public function bcc(string $address, ?string $name = null): self { return $this->addAddresses($address, $name, 'Bcc'); } @@ -107,7 +111,7 @@ public function bcc($address, $name = null) * @param string $name * @return $this */ - public function replyTo($address, $name = null) + public function replyTo(string $address, ?string $name = null): self { return $this->addAddresses($address, $name, 'ReplyTo'); } @@ -115,20 +119,36 @@ public function replyTo($address, $name = null) /** * Add a recipient to the message. * - * @param string|array $address + * @param string|Address[]|array $address * @param string $name * @param string $type * @return $this */ - protected function addAddresses($address, $name, $type) + protected function addAddresses(string|array $address, ?string $name, string $type): self { if (is_array($address)) { - $this->swift->{"set{$type}"}($address, $name); + $type = lcfirst($type); + $addresses = (new Collection($address))->map(function ($address, $key) { + if (is_string($key) && is_string($address)) { + return new Address($key, $address); + } + + if (is_array($address)) { + return new Address($address['email'] ?? $address['address'], $address['name'] ?? null); + } + + if (is_null($address)) { + return new Address($key); + } + + return $address; + })->all(); + $this->message->$type(...$addresses); } else { - $this->swift->{"add{$type}"}($address, $name); + $this->message->{"add{$type}"}(new Address($address, $name ?? '')); } return $this; @@ -140,10 +160,9 @@ protected function addAddresses($address, $name, $type) * @param string $subject * @return $this */ - public function subject($subject) + public function subject(string $subject): self { - $this->swift->setSubject($subject); - + $this->message->subject($subject); return $this; } @@ -153,10 +172,9 @@ public function subject($subject) * @param int $level * @return $this */ - public function priority($level) + public function priority(int $level): self { - $this->swift->setPriority($level); - + $this->message->priority($level); return $this; } @@ -167,22 +185,10 @@ public function priority($level) * @param array $options * @return $this */ - public function attach($file, array $options = array()) + public function attach(string $file, array $options = []): self { - $attachment = $this->createAttachmentFromPath($file); - - return $this->prepAttachment($attachment, $options); - } - - /** - * Create a Swift Attachment instance. - * - * @param string $file - * @return \Swift_Attachment - */ - protected function createAttachmentFromPath($file) - { - return Swift_Attachment::fromPath($file); + $this->message->attachFromPath($file, $options['as'] ?? null, $options['mime'] ?? null); + return $this; } /** @@ -193,23 +199,10 @@ protected function createAttachmentFromPath($file) * @param array $options * @return $this */ - public function attachData($data, $name, array $options = array()) - { - $attachment = $this->createAttachmentFromData($data, $name); - - return $this->prepAttachment($attachment, $options); - } - - /** - * Create a Swift Attachment instance from data. - * - * @param string $data - * @param string $name - * @return \Swift_Attachment - */ - protected function createAttachmentFromData($data, $name) + public function attachData(string $data, string $name, array $options = []): self { - return Swift_Attachment::newInstance($data, $name); + $this->message->attach($data, $name, $options['mime'] ?? null); + return $this; } /** @@ -218,9 +211,11 @@ protected function createAttachmentFromData($data, $name) * @param string $file * @return string */ - public function embed($file) + public function embed(string $file): string { - return $this->swift->embed(Swift_Image::fromPath($file)); + $cid = Str::random(10); + $this->message->embedFromPath($file, $cid); + return "cid:$cid"; } /** @@ -231,63 +226,32 @@ public function embed($file) * @param string $contentType * @return string */ - public function embedData($data, $name, $contentType = null) + public function embedData(string $data, string $name, ?string $contentType = null): string { - $image = Swift_Image::newInstance($data, $name, $contentType); - - return $this->swift->embed($image); - } - - /** - * Prepare and attach the given attachment. - * - * @param \Swift_Attachment $attachment - * @param array $options - * @return $this - */ - protected function prepAttachment($attachment, $options = array()) - { - // First we will check for a MIME type on the message, which instructs the - // mail client on what type of attachment the file is so that it may be - // downloaded correctly by the user. The MIME option is not required. - if (isset($options['mime'])) - { - $attachment->setContentType($options['mime']); - } - - // If an alternative name was given as an option, we will set that on this - // attachment so that it will be downloaded with the desired names from - // the developer, otherwise the default file names will get assigned. - if (isset($options['as'])) - { - $attachment->setFilename($options['as']); - } - - $this->swift->attach($attachment); - - return $this; + $this->message->embed($data, $name, $contentType); + return "cid:$name"; } /** - * Get the underlying Swift Message instance. + * Get the underlying Symfony Message instance. * - * @return \Swift_Message + * @return Email */ - public function getSwiftMessage() + public function getSymfonyMessage(): Email { - return $this->swift; + return $this->message; } /** - * Dynamically pass missing methods to the Swift instance. + * Dynamically pass missing methods to the Symfony Message instance. * * @param string $method * @param array $parameters * @return mixed */ - public function __call($method, $parameters) + public function __call(string $method, array $parameters) { - $callable = array($this->swift, $method); + $callable = [$this->message, $method]; return call_user_func_array($callable, $parameters); } diff --git a/src/Illuminate/Mail/SentMessage.php b/src/Illuminate/Mail/SentMessage.php new file mode 100644 index 000000000..cce42dbd0 --- /dev/null +++ b/src/Illuminate/Mail/SentMessage.php @@ -0,0 +1,54 @@ +sentMessage = $sentMessage; + } + + /** + * Get the underlying Symfony Email instance. + * + * @return \Symfony\Component\Mailer\SentMessage + */ + public function getSymfonySentMessage() + { + return $this->sentMessage; + } + + /** + * Dynamically pass missing methods to the Symfony instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return $this->forwardCallTo($this->sentMessage, $method, $parameters); + } +} diff --git a/src/Illuminate/Mail/Transport/ArrayTransport.php b/src/Illuminate/Mail/Transport/ArrayTransport.php new file mode 100644 index 000000000..9b0ccfcef --- /dev/null +++ b/src/Illuminate/Mail/Transport/ArrayTransport.php @@ -0,0 +1,67 @@ +messages = new Collection(); + } + + /** + * {@inheritdoc} + */ + public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage + { + return $this->messages[] = new SentMessage($message, $envelope ?? Envelope::create($message)); + } + + /** + * Retrieve the collection of messages. + * + * @return \Illuminate\Support\Collection + */ + public function messages(): Collection + { + return $this->messages; + } + + /** + * Clear all of the messages from the local collection. + * + * @return \Illuminate\Support\Collection + */ + public function flush(): Collection + { + return $this->messages = new Collection(); + } + + /** + * Get the string representation of the transport. + * + * @return string + */ + public function __toString(): string + { + return 'array'; + } +} \ No newline at end of file diff --git a/src/Illuminate/Mail/Transport/LogTransport.php b/src/Illuminate/Mail/Transport/LogTransport.php index 1fe81360d..97bd56f99 100644 --- a/src/Illuminate/Mail/Transport/LogTransport.php +++ b/src/Illuminate/Mail/Transport/LogTransport.php @@ -1,87 +1,41 @@ -logger = $logger; - } - - /** - * {@inheritdoc} - */ - public function isStarted() - { - return true; - } - - /** - * {@inheritdoc} - */ - public function start() - { - return true; - } - - /** - * {@inheritdoc} - */ - public function stop() - { - return true; - } +namespace Illuminate\Mail\Transport; - /** - * {@inheritdoc} - */ - public function send(Swift_Mime_Message $message, &$failedRecipients = null) - { - $this->logger->debug($this->getMimeEntityString($message)); - } - - /** - * Get a loggable string out of a Swiftmailer entity. - * - * @param \Swift_Mime_MimeEntity $entity - * @return string - */ - protected function getMimeEntityString(Swift_Mime_MimeEntity $entity) - { - $string = (string) $entity->getHeaders().PHP_EOL.$entity->getBody(); - - foreach ($entity->getChildren() as $children) - { - $string .= PHP_EOL.PHP_EOL.$this->getMimeEntityString($children); - } - - return $string; - } - - /** - * {@inheritdoc} - */ - public function registerPlugin(Swift_Events_EventListener $plugin) - { - // - } - -} +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\RawMessage; + +class LogTransport implements TransportInterface +{ + protected LoggerInterface $logger; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + $this->logger->debug($message->toString()); + return new SentMessage($message, $envelope ?? Envelope::create($message)); + } + + public function logger(): LoggerInterface + { + return $this->logger; + } + + /** + * @inheritDoc + */ + public function __toString(): string + { + return 'log'; + } +} \ No newline at end of file diff --git a/src/Illuminate/Mail/Transport/MailgunTransport.php b/src/Illuminate/Mail/Transport/MailgunTransport.php deleted file mode 100644 index 3b19f88e5..000000000 --- a/src/Illuminate/Mail/Transport/MailgunTransport.php +++ /dev/null @@ -1,168 +0,0 @@ -key = $key; - $this->setDomain($domain); - } - - /** - * {@inheritdoc} - */ - public function isStarted() - { - return true; - } - - /** - * {@inheritdoc} - */ - public function start() - { - return true; - } - - /** - * {@inheritdoc} - */ - public function stop() - { - return true; - } - - /** - * {@inheritdoc} - */ - public function send(Swift_Mime_Message $message, &$failedRecipients = null) - { - $client = $this->getHttpClient(); - - $client->post($this->url, ['auth' => ['api', $this->key], - 'body' => [ - 'to' => $this->getTo($message), - 'message' => new PostFile('message', (string) $message), - ], - ]); - } - - /** - * {@inheritdoc} - */ - public function registerPlugin(Swift_Events_EventListener $plugin) - { - // - } - - /** - * Get the "to" payload field for the API request. - * - * @param \Swift_Mime_Message $message - * @return array - */ - protected function getTo(Swift_Mime_Message $message) - { - $formatted = []; - - $contacts = array_merge( - (array) $message->getTo(), (array) $message->getCc(), (array) $message->getBcc() - ); - - foreach ($contacts as $address => $display) - { - $formatted[] = $display ? $display." <$address>" : $address; - } - - return implode(',', $formatted); - } - - /** - * Get a new HTTP client instance. - * - * @return \GuzzleHttp\Client - */ - protected function getHttpClient() - { - return new Client; - } - - /** - * Get the API key being used by the transport. - * - * @return string - */ - public function getKey() - { - return $this->key; - } - - /** - * Set the API key being used by the transport. - * - * @param string $key - * @return void - */ - public function setKey($key) - { - return $this->key = $key; - } - - /** - * Get the domain being used by the transport. - * - * @return string - */ - public function getDomain() - { - return $this->domain; - } - - /** - * Set the domain being used by the transport. - * - * @param string $domain - * @return void - */ - public function setDomain($domain) - { - $this->url = 'https://api.mailgun.net/v2/'.$domain.'/messages.mime'; - - return $this->domain = $domain; - } - -} diff --git a/src/Illuminate/Mail/Transport/MandrillTransport.php b/src/Illuminate/Mail/Transport/MandrillTransport.php deleted file mode 100644 index daa823b64..000000000 --- a/src/Illuminate/Mail/Transport/MandrillTransport.php +++ /dev/null @@ -1,107 +0,0 @@ -key = $key; - } - - /** - * {@inheritdoc} - */ - public function isStarted() - { - return true; - } - - /** - * {@inheritdoc} - */ - public function start() - { - return true; - } - - /** - * {@inheritdoc} - */ - public function stop() - { - return true; - } - - /** - * {@inheritdoc} - */ - public function send(Swift_Mime_Message $message, &$failedRecipients = null) - { - $client = $this->getHttpClient(); - - $client->post('https://mandrillapp.com/api/1.0/messages/send-raw.json', [ - 'body' => [ - 'key' => $this->key, - 'raw_message' => (string) $message, - 'async' => false, - ], - ]); - } - - /** - * {@inheritdoc} - */ - public function registerPlugin(Swift_Events_EventListener $plugin) - { - // - } - - /** - * Get a new HTTP client instance. - * - * @return \GuzzleHttp\Client - */ - protected function getHttpClient() - { - return new Client; - } - - /** - * Get the API key being used by the transport. - * - * @return string - */ - public function getKey() - { - return $this->key; - } - - /** - * Set the API key being used by the transport. - * - * @param string $key - * @return void - */ - public function setKey($key) - { - return $this->key = $key; - } - -} diff --git a/src/Illuminate/Mail/composer.json b/src/Illuminate/Mail/composer.json index a6038462e..33715802b 100755 --- a/src/Illuminate/Mail/composer.json +++ b/src/Illuminate/Mail/composer.json @@ -13,7 +13,7 @@ "illuminate/log": "4.2.*", "illuminate/support": "4.2.*", "illuminate/view": "4.2.*", - "swiftmailer/swiftmailer": "^6.0" + "symfony/mailer": "^6.4" }, "require-dev": { "illuminate/queue": "4.2.*" diff --git a/src/Illuminate/Support/Traits/ForwardsCalls.php b/src/Illuminate/Support/Traits/ForwardsCalls.php new file mode 100644 index 000000000..e71818098 --- /dev/null +++ b/src/Illuminate/Support/Traits/ForwardsCalls.php @@ -0,0 +1,75 @@ +{$method}(...$parameters); + } catch (Error|BadMethodCallException $e) { + $pattern = '~^Call to undefined method (?P[^:]+)::(?P[^\(]+)\(\)$~'; + + if (! preg_match($pattern, $e->getMessage(), $matches)) { + throw $e; + } + + if ($matches['class'] != get_class($object) || + $matches['method'] != $method) { + throw $e; + } + + static::throwBadMethodCallException($method); + } + } + + /** + * Forward a method call to the given object, returning $this if the forwarded call returned itself. + * + * @param mixed $object + * @param string $method + * @param array $parameters + * @return mixed + * + * @throws \BadMethodCallException + */ + protected function forwardDecoratedCallTo($object, $method, $parameters) + { + $result = $this->forwardCallTo($object, $method, $parameters); + + if ($result === $object) { + return $this; + } + + return $result; + } + + /** + * Throw a bad method call exception for the given method. + * + * @param string $method + * @return void + * + * @throws \BadMethodCallException + */ + protected static function throwBadMethodCallException($method) + { + throw new BadMethodCallException(sprintf( + 'Call to undefined method %s::%s()', static::class, $method + )); + } +} diff --git a/tests/Auth/AuthPasswordBrokerTest.php b/tests/Auth/AuthPasswordBrokerTest.php index 0f69eef70..fd9b1a8c3 100755 --- a/tests/Auth/AuthPasswordBrokerTest.php +++ b/tests/Auth/AuthPasswordBrokerTest.php @@ -5,9 +5,12 @@ use Illuminate\Auth\Reminders\ReminderRepositoryInterface; use Illuminate\Auth\UserProviderInterface; use Illuminate\Mail\Mailer; +use Illuminate\Mail\Transport\ArrayTransport; +use Illuminate\View\Factory; use L4\Tests\BackwardCompatibleTestCase; use Mockery as m; use Prophecy\Prophecy\ObjectProphecy; +use Symfony\Component\Mailer\SentMessage; class AuthPasswordBrokerTest extends BackwardCompatibleTestCase { @@ -95,21 +98,35 @@ public function testBrokerCreatesReminderAndRedirectsWithoutError() public function testMailerIsCalledWithProperViewTokenAndCallback() { - unset($_SERVER['__auth.reminder']); - $broker = $this->getBroker($mocks = $this->getMocks()); - $callback = function($message, $user) { $_SERVER['__auth.reminder'] = true; }; - $user = m::mock(RemindableInterface::class); - $mocks['mailer']->shouldReceive('send')->once()->with('reminderView', ['token' => 'token', 'user' => $user], m::type('Closure'))->andReturnUsing(function($view, $data, $callback) - { - return $callback; - }); - $user->shouldReceive('getReminderEmail')->once()->andReturn('email'); - $message = m::mock('StdClass'); - $message->shouldReceive('to')->once()->with('email'); - $result = $broker->sendReminder($user, 'token', $callback); - call_user_func($result, $message); - - $this->assertTrue($_SERVER['__auth.reminder']); + $factoryView = m::mock(Factory::class); + $factoryView->shouldReceive('make')->once()->andReturnUsing(function ($view, $data) use($factoryView, &$maker) { + $factoryView->shouldReceive('render')->once()->andReturn($view); + return $factoryView; + }); + + $broker = $this->getBroker($mocks = [ + ...$this->getMocks(), + 'mailer' => new Mailer($factoryView, $transport = new ArrayTransport()) + ]); + $mocks['mailer']->alwaysFrom('sender@mail.com'); + + $user = m::mock(RemindableInterface::class); + $user->shouldReceive('getReminderEmail')->once()->andReturn('user@email.com'); + + $receivedCallback = new stdClass(); + $someCallback = function($message, $user, $token) use (&$receivedCallback) { + $receivedCallback->user = $user; + $receivedCallback->token = $token; + }; + + $broker->sendReminder($user, 'token', $someCallback); + + /** @var SentMessage $message */ + $message = $transport->messages()[0]; + self::assertEquals('user@email.com', $message->getEnvelope()->getRecipients()[0]->getAddress()); + self::assertStringContainsString('reminderView', $message->toString()); + self::assertEquals($user, $receivedCallback->user); + self::assertEquals('token', $receivedCallback->token); } @@ -207,22 +224,20 @@ public function testResetRemovesRecordOnReminderTableAndCallsCallback() } - protected function getBroker($mocks) - { + protected function getBroker($mocks): PasswordBroker + { return new PasswordBroker($mocks['reminders'], $mocks['users'], $mocks['mailer'], $mocks['view']); } - protected function getMocks() - { - $mocks = [ - 'reminders' => m::mock(ReminderRepositoryInterface::class), - 'users' => m::mock(UserProviderInterface::class), - 'mailer' => m::mock(Mailer::class), - 'view' => 'reminderView', + protected function getMocks(): array + { + return [ + 'reminders' => m::mock(ReminderRepositoryInterface::class), + 'users' => m::mock(UserProviderInterface::class), + 'mailer' => m::mock(Mailer::class), + 'view' => 'reminderView', ]; - - return $mocks; } } diff --git a/tests/Mail/MailMailerTest.php b/tests/Mail/MailMailerTest.php index 436d2cb02..bb39609d1 100755 --- a/tests/Mail/MailMailerTest.php +++ b/tests/Mail/MailMailerTest.php @@ -2,10 +2,13 @@ use Illuminate\Log\Writer; use Illuminate\Mail\Mailer; +use Illuminate\Mail\Message; +use Illuminate\Mail\Transport\ArrayTransport; use Illuminate\Queue\QueueManager; use Illuminate\View\Factory; use L4\Tests\BackwardCompatibleTestCase; use Mockery as m; +use Symfony\Component\Mailer\SentMessage; class MailMailerTest extends BackwardCompatibleTestCase { @@ -15,72 +18,110 @@ protected function tearDown(): void m::close(); } - public function testMailerSendSendsMessageWithProperViewContent() { - unset($_SERVER['__mailer.test']); - $mailer = $this->getMock(Mailer::class, ['createMessage'], $this->getMocks()); - $message = m::mock('StdClass'); - $mailer->expects($this->once())->method('createMessage')->willReturn($message); - $view = m::mock('StdClass'); - $mailer->getViewFactory()->shouldReceive('make')->once()->with('foo', ['data', 'message' => $message])->andReturn($view); - $view->shouldReceive('render')->once()->andReturn('rendered.view'); - $message->shouldReceive('setBody')->once()->with('rendered.view', 'text/html'); - $message->shouldReceive('setFrom')->never(); - $mailer->setSwiftMailer(m::mock('StdClass')); - $message->shouldReceive('getSwiftMessage')->once()->andReturn($message); - $mailer->getSwiftMailer()->shouldReceive('send')->once()->with($message, []); - $mailer->send('foo', ['data'], function($m) { $_SERVER['__mailer.test'] = $m; }); - unset($_SERVER['__mailer.test']); + $view = m::mock(Factory::class); + $view->shouldReceive('make')->once()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + + $mailer = new Mailer($view, $transport = new ArrayTransport()); + $mailer->send('foo', ['data'], function (Message $message) { + $message->to('taylor@laravel.com')->from('hello@laravel.com'); + }); + + $sentMessages = $transport->messages(); + self::assertCount(1, $sentMessages); + + /** @var SentMessage $sentMessage */ + $sentMessage = $sentMessages[0]; + self::assertStringContainsString('rendered.view', $sentMessage->toString()); + self::assertStringContainsString('Content-Type: text/html;', $sentMessage->toString()); + self::assertEquals('taylor@laravel.com', $sentMessage->getEnvelope()->getRecipients()[0]->getAddress()); + self::assertEquals('hello@laravel.com', $sentMessage->getEnvelope()->getSender()->getAddress()); } public function testMailerSendSendsMessageWithProperPlainViewContent() { - unset($_SERVER['__mailer.test']); - $mailer = $this->getMock(Mailer::class, ['createMessage'], $this->getMocks()); - $message = m::mock('StdClass'); - $mailer->expects($this->once())->method('createMessage')->willReturn($message); - $view = m::mock('StdClass'); - $mailer->getViewFactory()->shouldReceive('make')->once()->with('foo', ['data', 'message' => $message])->andReturn($view); - $mailer->getViewFactory()->shouldReceive('make')->once()->with('bar', ['data', 'message' => $message])->andReturn($view); - $view->shouldReceive('render')->twice()->andReturn('rendered.view'); - $message->shouldReceive('setBody')->once()->with('rendered.view', 'text/html'); - $message->shouldReceive('addPart')->once()->with('rendered.view', 'text/plain'); - $message->shouldReceive('setFrom')->never(); - $mailer->setSwiftMailer(m::mock('StdClass')); - $message->shouldReceive('getSwiftMessage')->once()->andReturn($message); - $mailer->getSwiftMailer()->shouldReceive('send')->once()->with($message, []); - $mailer->send(['foo', 'bar'], ['data'], function($m) { $_SERVER['__mailer.test'] = $m; }); - unset($_SERVER['__mailer.test']); + $view = m::mock(Factory::class); + $view->shouldReceive('make')->twice()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + $view->shouldReceive('render')->once()->andReturn('rendered.plain'); + + $mailer = new Mailer($view, $transport = new ArrayTransport()); + $mailer->send(['foo', 'bar'], ['data'], function (Message $message) { + $message->to('taylor@laravel.com')->from('hello@laravel.com'); + }); + + $sentMessages = $transport->messages(); + self::assertCount(1, $sentMessages); + + /** @var SentMessage $sentMessage */ + $sentMessage = $sentMessages[0]; + $expected = <<toString()); + + $expected = <<toString()); + self::assertStringContainsString('Content-Type: text/html;', $sentMessage->toString()); + self::assertEquals('taylor@laravel.com', $sentMessage->getEnvelope()->getRecipients()[0]->getAddress()); + self::assertEquals('hello@laravel.com', $sentMessage->getEnvelope()->getSender()->getAddress()); } public function testMailerSendSendsMessageWithProperPlainViewContentWhenExplicit() { - unset($_SERVER['__mailer.test']); - $mailer = $this->getMock(Mailer::class, ['createMessage'], $this->getMocks()); - $message = m::mock('StdClass'); - $mailer->expects($this->once())->method('createMessage')->willReturn($message); - $view = m::mock('StdClass'); - $mailer->getViewFactory()->shouldReceive('make')->once()->with('foo', ['data', 'message' => $message])->andReturn($view); - $mailer->getViewFactory()->shouldReceive('make')->once()->with('bar', ['data', 'message' => $message])->andReturn($view); - $view->shouldReceive('render')->twice()->andReturn('rendered.view'); - $message->shouldReceive('setBody')->once()->with('rendered.view', 'text/html'); - $message->shouldReceive('addPart')->once()->with('rendered.view', 'text/plain'); - $message->shouldReceive('setFrom')->never(); - $mailer->setSwiftMailer(m::mock('StdClass')); - $message->shouldReceive('getSwiftMessage')->once()->andReturn($message); - $mailer->getSwiftMailer()->shouldReceive('send')->once()->with($message, []); - $mailer->send(['html' => 'foo', 'text' => 'bar'], ['data'], function($m) { $_SERVER['__mailer.test'] = $m; }); - unset($_SERVER['__mailer.test']); + $view = m::mock(Factory::class); + $view->shouldReceive('make')->twice()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + $view->shouldReceive('render')->once()->andReturn('rendered.plain'); + + $mailer = new Mailer($view, $transport = new ArrayTransport()); + $mailer->send(['html' => 'foo', 'text' => 'bar'], ['data'], function (Message $message) { + $message->to('taylor@laravel.com')->from('hello@laravel.com'); + }); + + $sentMessages = $transport->messages(); + self::assertCount(1, $sentMessages); + + /** @var SentMessage $sentMessage */ + $sentMessage = $sentMessages[0]; + $expected = <<toString()); + + $expected = <<toString()); } public function testMailerCanQueueMessagesToItself() { - list($view, $swift) = $this->getMocks(); - $mailer = new Illuminate\Mail\Mailer($view, $swift); + $view = m::mock(Factory::class); + $mailer = new Mailer($view, new ArrayTransport()); $mailer->setQueue($queue = m::mock(QueueManager::class)); $queue->shouldReceive('push')->once()->with('mailer@handleQueuedMessage', ['view' => 'foo', 'data' => [1], 'callback' => 'callable'], null); @@ -90,8 +131,8 @@ public function testMailerCanQueueMessagesToItself() public function testMailerCanQueueMessagesToItselfOnAnotherQueue() { - list($view, $swift) = $this->getMocks(); - $mailer = new Illuminate\Mail\Mailer($view, $swift); + $view = m::mock(Factory::class); + $mailer = new Mailer($view, new ArrayTransport()); $mailer->setQueue($queue = m::mock(QueueManager::class)); $queue->shouldReceive('push')->once()->with('mailer@handleQueuedMessage', ['view' => 'foo', 'data' => [1], 'callback' => 'callable'], 'queue'); @@ -101,8 +142,8 @@ public function testMailerCanQueueMessagesToItselfOnAnotherQueue() public function testMailerCanQueueMessagesToItselfWithSerializedClosures() { - list($view, $swift) = $this->getMocks(); - $mailer = new Illuminate\Mail\Mailer($view, $swift); + $view = m::mock(Factory::class); + $mailer = new Mailer($view, new ArrayTransport()); $mailer->setQueue($queue = m::mock(QueueManager::class)); $serialized = serialize(new Illuminate\Support\SerializableClosure($closure = function() {})); $queue->shouldReceive('push')->once()->with('mailer@handleQueuedMessage', ['view' => 'foo', 'data' => [1], 'callback' => $serialized], null); @@ -113,8 +154,8 @@ public function testMailerCanQueueMessagesToItselfWithSerializedClosures() public function testMailerCanQueueMessagesToItselfLater() { - list($view, $swift) = $this->getMocks(); - $mailer = new Illuminate\Mail\Mailer($view, $swift); + $view = m::mock(Factory::class); + $mailer = new Mailer($view, new ArrayTransport()); $mailer->setQueue($queue = m::mock(QueueManager::class)); $queue->shouldReceive('later')->once()->with(10, 'mailer@handleQueuedMessage', ['view' => 'foo', 'data' => [1], 'callback' => 'callable'], null); @@ -124,8 +165,8 @@ public function testMailerCanQueueMessagesToItselfLater() public function testMailerCanQueueMessagesToItselfLaterOnAnotherQueue() { - list($view, $swift) = $this->getMocks(); - $mailer = new Illuminate\Mail\Mailer($view, $swift); + $view = m::mock(Factory::class); + $mailer = new Mailer($view, new ArrayTransport()); $mailer->setQueue($queue = m::mock(QueueManager::class)); $queue->shouldReceive('later')->once()->with(10, 'mailer@handleQueuedMessage', ['view' => 'foo', 'data' => [1], 'callback' => 'callable'], 'queue'); @@ -135,103 +176,74 @@ public function testMailerCanQueueMessagesToItselfLaterOnAnotherQueue() public function testMessagesCanBeLoggedInsteadOfSent() { - $mailer = $this->getMock(Mailer::class, ['createMessage'], $this->getMocks()); - $message = m::mock('StdClass'); - $mailer->expects($this->once())->method('createMessage')->willReturn($message); - $view = m::mock('StdClass'); - $mailer->getViewFactory()->shouldReceive('make')->once()->with('foo', ['data', 'message' => $message])->andReturn($view); - $view->shouldReceive('render')->once()->andReturn('rendered.view'); - $message->shouldReceive('setBody')->once()->with('rendered.view', 'text/html'); - $message->shouldReceive('setFrom')->never(); - $mailer->setSwiftMailer(m::mock('StdClass')); - $message->shouldReceive('getTo')->once()->andReturn(['taylor@userscape.com' => 'Taylor']); - $message->shouldReceive('getSwiftMessage')->once()->andReturn($message); - $mailer->getSwiftMailer()->shouldReceive('send')->never(); - $logger = m::mock(Writer::class); - $logger->shouldReceive('info')->once()->with('Pretending to mail message to: taylor@userscape.com'); - $mailer->setLogger($logger); - $mailer->pretend(); - - $mailer->send('foo', ['data'], function($m) {}); + $view = m::mock(Factory::class); + $view->shouldReceive('make')->once()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + + $mailer = new Mailer($view, $transport = new ArrayTransport()); + $logger = m::mock(Writer::class); + $logger->shouldReceive('info')->once()->with('Pretending to mail message to: taylor@userscape.com'); + $mailer->setLogger($logger); + $mailer->pretend(); + + $mailer->send('foo', ['data'], function (Message $message) { + $message->from('hello@laravel.com'); + $message->to('taylor@userscape.com'); + }); + + self::assertEmpty($transport->messages()); } public function testMailerCanResolveMailerClasses() { - $mailer = $this->getMock(Mailer::class, ['createMessage'], $this->getMocks()); - $message = m::mock('StdClass'); - $mailer->expects($this->once())->method('createMessage')->willReturn($message); - $view = m::mock('StdClass'); - $container = new Illuminate\Container\Container; - $mailer->setContainer($container); - $mockMailer = m::mock('StdClass'); - $container['FooMailer'] = $container->share(function() use ($mockMailer) - { - return $mockMailer; - }); - $mockMailer->shouldReceive('mail')->once()->with($message); - $mailer->getViewFactory()->shouldReceive('make')->once()->with('foo', ['data', 'message' => $message])->andReturn($view); - $view->shouldReceive('render')->once()->andReturn('rendered.view'); - $message->shouldReceive('setBody')->once()->with('rendered.view', 'text/html'); - $message->shouldReceive('setFrom')->never(); - $mailer->setSwiftMailer(m::mock('StdClass')); - $message->shouldReceive('getSwiftMessage')->once()->andReturn($message); - $mailer->getSwiftMailer()->shouldReceive('send')->once()->with($message, []); - $mailer->send('foo', ['data'], 'FooMailer'); - } - - - public function testGlobalFromIsRespectedOnAllMessages() - { - unset($_SERVER['__mailer.test']); - $mailer = $this->getMailer(); - $view = m::mock('StdClass'); - $mailer->getViewFactory()->shouldReceive('make')->once()->andReturn($view); - $view->shouldReceive('render')->once()->andReturn('rendered.view'); - $mailer->setSwiftMailer(m::mock('StdClass')); - $mailer->alwaysFrom('taylorotwell@gmail.com', 'Taylor Otwell'); - $me = $this; - $mailer->getSwiftMailer()->shouldReceive('send')->once()->with(m::type('Swift_Message'), [])->andReturnUsing(function($message) use ($me) - { - $me->assertEquals(['taylorotwell@gmail.com' => 'Taylor Otwell'], $message->getFrom()); - }); - $mailer->send('foo', ['data'], function($m) {}); - } - + $view = m::mock(Factory::class); + $view->shouldReceive('make')->once()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + + $mailer = new Mailer($view, $transport = new ArrayTransport()); + $container = new Illuminate\Container\Container(); + $mailer->setContainer($container); + $fooMailer = new class { + public int $calledTimes = 0; + public function mail(Message $message): void + { + $message->from('hello@laravel.com'); + $message->to('taylor@laravel.com'); + $this->calledTimes++; + } + }; + $container['FooMailer'] = $container->share(fn() => $fooMailer); - public function testFailedRecipientsAreAppendedAndCanBeRetrieved() - { - unset($_SERVER['__mailer.test']); - $mailer = $this->getMailer(); - $view = m::mock('StdClass'); - $mailer->getViewFactory()->shouldReceive('make')->once()->andReturn($view); - $view->shouldReceive('render')->once()->andReturn('rendered.view'); - $swift = new FailingSwiftMailerStub; - $mailer->setSwiftMailer($swift); + $mailer->send('foo', ['data'], 'FooMailer'); - $mailer->send('foo', ['data'], function($m) {}); + $sentMessage = $transport->messages()[0]; + self::assertEquals(1, $fooMailer->calledTimes); + self::assertEquals('taylor@laravel.com', $sentMessage->getEnvelope()->getRecipients()[0]->getAddress()); + self::assertEquals('hello@laravel.com', $sentMessage->getEnvelope()->getSender()->getAddress()); - $this->assertEquals(['taylorotwell@gmail.com'], $mailer->failures()); } - protected function getMailer() + public function testGlobalFromIsRespectedOnAllMessages() { - return new Illuminate\Mail\Mailer(m::mock(Factory::class), m::mock('Swift_Mailer')); - } + $view = m::mock(Factory::class); + $view->shouldReceive('make')->once()->andReturn($view); + $view->shouldReceive('render')->once()->andReturn('rendered.view'); + $mailer = new Mailer($view, $transport = new ArrayTransport()); + $mailer->alwaysFrom('hello@laravel.com'); + $mailer->send('foo', ['data'], function (Message $message) { + $message->to('taylor@laravel.com'); + }); - protected function getMocks() - { - return [m::mock(Factory::class), m::mock('Swift_Mailer')]; - } -} + $sentMessages = $transport->messages(); + self::assertCount(1, $sentMessages); -class FailingSwiftMailerStub -{ - public function send($message, &$failed) - { - $failed[] = 'taylorotwell@gmail.com'; + /** @var SentMessage $sentMessage */ + $sentMessage = $sentMessages[0]; + self::assertSame('taylor@laravel.com', $sentMessage->getEnvelope()->getRecipients()[0]->getAddress()); + self::assertSame('hello@laravel.com', $sentMessage->getEnvelope()->getSender()->getAddress()); } } diff --git a/tests/Mail/MailMessageTest.php b/tests/Mail/MailMessageTest.php index bf0474b84..35ef82285 100755 --- a/tests/Mail/MailMessageTest.php +++ b/tests/Mail/MailMessageTest.php @@ -3,42 +3,118 @@ use Illuminate\Mail\Message; use L4\Tests\BackwardCompatibleTestCase; use Mockery as m; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; class MailMessageTest extends BackwardCompatibleTestCase { + private static string $staticFilePath; + + private Message $message; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + file_put_contents(self::$staticFilePath = __DIR__.'/foo.jpg', 'expected attachment body'); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + @unlink(self::$staticFilePath); + } + + protected function setUp(): void + { + parent::setUp(); + $this->message = new Message(new Email()); + } protected function tearDown(): void { m::close(); } + public function testFromMethod() + { + self::assertInstanceOf(Message::class, $message = $this->message->from('foo@bar.baz', 'Foo')); + self::assertEquals(new Address('foo@bar.baz', 'Foo'), $message->getSymfonyMessage()->getFrom()[0]); + } + + public function testSenderMethod() + { + self::assertInstanceOf(Message::class, $message = $this->message->sender('foo@bar.baz', 'Foo')); + self::assertEquals(new Address('foo@bar.baz', 'Foo'), $message->getSymfonyMessage()->getSender()); + } - public function testBasicAttachment() + public function testReturnPathMethod() { - $swift = m::mock('StdClass'); - $message = $this->getMock(Message::class, ['createAttachmentFromPath'], [$swift]); - $attachment = m::mock('StdClass'); - $message->expects($this->once())->method('createAttachmentFromPath')->with($this->equalTo('foo.jpg'))->willReturn( - $attachment - ); - $swift->shouldReceive('attach')->once()->with($attachment); - $attachment->shouldReceive('setContentType')->once()->with('image/jpeg'); - $attachment->shouldReceive('setFilename')->once()->with('bar.jpg'); - $message->attach('foo.jpg', ['mime' => 'image/jpeg', 'as' => 'bar.jpg']); + self::assertInstanceOf(Message::class, $message = $this->message->returnPath('foo@bar.baz')); + self::assertEquals(new Address('foo@bar.baz'), $message->getSymfonyMessage()->getReturnPath()); + } + + public function testToMethod() + { + self::assertInstanceOf(Message::class, $message = $this->message->to('foo@bar.baz', 'Foo')); + self::assertEquals(new Address('foo@bar.baz', 'Foo'), $message->getSymfonyMessage()->getTo()[0]); + + self::assertInstanceOf(Message::class, $message = $this->message->to(['bar@bar.baz' => 'Bar'])); + self::assertEquals(new Address('bar@bar.baz', 'Bar'), $message->getSymfonyMessage()->getTo()[0]); + } + + public function testCcMethod() + { + self::assertInstanceOf(Message::class, $message = $this->message->cc('foo@bar.baz', 'Foo')); + self::assertEquals(new Address('foo@bar.baz', 'Foo'), $message->getSymfonyMessage()->getCc()[0]); + } + + public function testBccMethod() + { + self::assertInstanceOf(Message::class, $message = $this->message->bcc('foo@bar.baz', 'Foo')); + self::assertEquals(new Address('foo@bar.baz', 'Foo'), $message->getSymfonyMessage()->getBcc()[0]); + } + + public function testReplyToMethod() + { + self::assertInstanceOf(Message::class, $message = $this->message->replyTo('foo@bar.baz', 'Foo')); + self::assertEquals(new Address('foo@bar.baz', 'Foo'), $message->getSymfonyMessage()->getReplyTo()[0]); + } + + public function testSubjectMethod() + { + self::assertInstanceOf(Message::class, $message = $this->message->subject('foo')); + self::assertSame('foo', $message->getSymfonyMessage()->getSubject()); + } + + public function testPriorityMethod() + { + self::assertInstanceOf(Message::class, $message = $this->message->priority(1)); + self::assertEquals(1, $message->getSymfonyMessage()->getPriority()); + } + + public function testBasicAttachment(): void + { + $this->message->attach(self::$staticFilePath, ['as' => 'bar.jpg', 'mime' => 'image/jpg']); + + $attachment = $this->message->getSymfonyMessage()->getAttachments()[0]; + $headers = $attachment->getPreparedHeaders()->toArray(); + self::assertSame('expected attachment body', $attachment->getBody()); + self::assertSame('Content-Type: image/jpg; name=bar.jpg', $headers[0]); + self::assertSame('Content-Transfer-Encoding: base64', $headers[1]); + self::assertSame('Content-Disposition: attachment; name=bar.jpg; filename=bar.jpg', $headers[2]); } - public function testDataAttachment() + public function testDataAttachment(): void { - $swift = m::mock('StdClass'); - $message = $this->getMock(Message::class, ['createAttachmentFromData'], [$swift]); - $attachment = m::mock('StdClass'); - $message->expects($this->once())->method('createAttachmentFromData')->with($this->equalTo('foo'), $this->equalTo('name'))->willReturn( - $attachment - ); - $swift->shouldReceive('attach')->once()->with($attachment); - $attachment->shouldReceive('setContentType')->once()->with('image/jpeg'); - $message->attachData('foo', 'name', ['mime' => 'image/jpeg']); + $this->message->attachData('expected attachment body', 'foo.jpg', ['mime' => 'image/jpg']); + + $attachment = $this->message->getSymfonyMessage()->getAttachments()[0]; + $headers = $attachment->getPreparedHeaders()->toArray(); + self::assertSame('expected attachment body', $attachment->getBody()); + self::assertSame('Content-Type: image/jpg; name=foo.jpg', $headers[0]); + self::assertSame('Content-Transfer-Encoding: base64', $headers[1]); + self::assertSame('Content-Disposition: attachment; name=foo.jpg; filename=foo.jpg', $headers[2]); } }