Skip to content

Commit 8482ac1

Browse files
committed
[Mailer] introduce send command
Aim is to allow to test if your mailer configuration or supplier is operational in production Usage - console mailer:send-email --from=a@symfony.com --to=b@symfony.com --subject=Test --body=body - console mailer:send-email - console mailer:send-email --body-source=file --body=/path/to/file
1 parent 44e98db commit 8482ac1

File tree

6 files changed

+343
-1
lines changed

6 files changed

+343
-1
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
8888
use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory;
8989
use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory;
90+
use Symfony\Component\Mailer\Command\MailerSendEmailCommand;
9091
use Symfony\Component\Mailer\Mailer;
9192
use Symfony\Component\Messenger\Bridge\AmazonSqs\Transport\AmazonSqsTransportFactory;
9293
use Symfony\Component\Messenger\Bridge\Amqp\Transport\AmqpTransportFactory;
@@ -388,6 +389,10 @@ public function load(array $configs, ContainerBuilder $container)
388389
$this->registerMailerConfiguration($config['mailer'], $container, $loader);
389390
}
390391

392+
if (false === $this->mailerConfigEnabled || false === class_exists(MailerSendEmailCommand::class)) {
393+
$container->removeDefinition('console.command.mailer_send_email');
394+
}
395+
391396
if ($this->notifierConfigEnabled = $this->isConfigEnabled($container, $config['notifier'])) {
392397
$this->registerNotifierConfiguration($config['notifier'], $container, $loader);
393398
}

src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand;
4040
use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber;
4141
use Symfony\Component\Console\EventListener\ErrorListener;
42+
use Symfony\Component\Mailer\Command\MailerSendEmailCommand;
4243
use Symfony\Component\Messenger\Command\ConsumeMessagesCommand;
4344
use Symfony\Component\Messenger\Command\DebugCommand;
4445
use Symfony\Component\Messenger\Command\FailedMessagesRemoveCommand;
@@ -133,6 +134,12 @@
133134
])
134135
->tag('console.command', ['command' => 'debug:event-dispatcher'])
135136

137+
->set('console.command.mailer_send_email', MailerSendEmailCommand::class)
138+
->args([
139+
service('mailer.mailer'),
140+
])
141+
->tag('console.command', ['command' => 'mailer:send-email'])
142+
136143
->set('console.command.messenger_consume_messages', ConsumeMessagesCommand::class)
137144
->args([
138145
abstract_arg('Routable message bus'),

src/Symfony/Component/Mailer/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CHANGELOG
55
-----
66

77
* added the `mailer` monolog channel and set it on all transport definitions
8+
* added `console mailer:send-email` command to check if your mailer configuration or supplier is (still) operational
9+
810

911
5.2.0
1012
-----
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Command;
13+
14+
use Symfony\Component\Console\Command\Command;
15+
use Symfony\Component\Console\Input\InputInterface;
16+
use Symfony\Component\Console\Input\InputOption;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Console\Style\SymfonyStyle;
19+
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
20+
use Symfony\Component\Mailer\MailerInterface;
21+
use Symfony\Component\Mime\Email;
22+
23+
/**
24+
* Helps making sure your mailer provider is operational.
25+
*
26+
* @author Guillaume MOREL <me@gmorel.io>
27+
*/
28+
final class MailerSendEmailCommand extends Command
29+
{
30+
/** {@inheritdoc} */
31+
protected static $defaultName = 'mailer:send-email';
32+
33+
/** @var MailerInterface */
34+
private $mailer;
35+
36+
/** @var SymfonyStyle */
37+
private $io;
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function __construct(MailerInterface $mailer)
43+
{
44+
parent::__construct();
45+
46+
$this->mailer = $mailer;
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
protected function configure()
53+
{
54+
$this->setDescription('Send simple email message')
55+
->addArgument('from', null, 'The from address of the message')
56+
->addArgument('to', null, 'The to address of the message')
57+
->addOption('subject', null, InputOption::VALUE_REQUIRED, 'The subject of the message', 'Testing Mailer Component')
58+
->addOption('body', null, InputOption::VALUE_REQUIRED, 'The body of the message', 'This is a test email.')
59+
->addOption('body-source', null, InputOption::VALUE_REQUIRED, 'The source where body come from [stdin|file]', 'stdin')
60+
->setHelp(
61+
<<<EOF
62+
The <info>%command.name%</info> command creates and sends a simple email message.
63+
Usage:
64+
- <info>php %command.full_name%</info> will trigger the interactive mode
65+
- <info>php %command.full_name% from=a@symfony.com to=b@symfony.com</info>
66+
- <info>php %command.full_name% from=a@symfony.com to=b@symfony.com --subject=Test --body=body</info>
67+
68+
You can get body of message from a file:
69+
<info>php %command.full_name% --body-source=file --body=/path/to/file</info>
70+
EOF
71+
);
72+
}
73+
74+
/**
75+
* {@inheritdoc}
76+
*/
77+
protected function initialize(InputInterface $input, OutputInterface $output)
78+
{
79+
$this->io = new SymfonyStyle($input, $output);
80+
}
81+
82+
/**
83+
* {@inheritdoc}
84+
*
85+
* @throws TransportExceptionInterface
86+
*/
87+
protected function execute(InputInterface $input, OutputInterface $output): int
88+
{
89+
switch ($input->getOption('body-source')) {
90+
case 'file':
91+
$content = $this->loadFileContent(
92+
$input->getOption('body')
93+
);
94+
$input->setOption('body', $content);
95+
break;
96+
case 'stdin':
97+
break;
98+
default:
99+
throw new \InvalidArgumentException('Body-input option should be "stdin" or "file".');
100+
}
101+
102+
if ('file' === $input->getOption('body-source')) {
103+
$email = $this->createEmailFromFile($input);
104+
} else {
105+
$email = $this->createEmailFromString($input);
106+
}
107+
108+
$this->mailer->send($email);
109+
110+
$this->io->success(
111+
sprintf(
112+
'Email was successfully sent to "%s".',
113+
(string) $input->getArgument('to')
114+
)
115+
);
116+
117+
return Command::SUCCESS;
118+
}
119+
120+
private function createEmailFromString(InputInterface $input): Email
121+
{
122+
$subject = $input->getOption('subject');
123+
$body = $input->getOption('body');
124+
125+
return $this->createEmailWithoutBody($input)
126+
->text($body)
127+
->html(
128+
<<<HTML
129+
<!doctype html>
130+
<html>
131+
<head>
132+
<meta name="viewport" content="width=device-width" />
133+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
134+
<title>$subject</title>
135+
</head>
136+
<body>
137+
<p>$body</p>
138+
</body>
139+
</html>
140+
HTML
141+
);
142+
}
143+
144+
private function createEmailFromFile(InputInterface $input): Email
145+
{
146+
$body = $input->getOption('body');
147+
148+
return $this->createEmailWithoutBody($input)
149+
->text($body)
150+
->html($body);
151+
}
152+
153+
private function createEmailWithoutBody(InputInterface $input): Email
154+
{
155+
return (new Email())
156+
->from($input->getArgument('from'))
157+
->to($input->getArgument('to'))
158+
->priority(Email::PRIORITY_HIGH)
159+
->subject($input->getOption('subject'));
160+
}
161+
162+
/**
163+
* @throws \InvalidArgumentException
164+
* @throws \LogicException
165+
*/
166+
private function loadFileContent(string $fileUri): string
167+
{
168+
if (false === file_exists($fileUri)) {
169+
throw new \InvalidArgumentException("Could not find file \"$fileUri\".");
170+
}
171+
172+
$content = file_get_contents($fileUri);
173+
if (false === $content) {
174+
throw new \LogicException("Could not get contents from file \"$fileUri\".");
175+
}
176+
177+
return $content;
178+
}
179+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Tests\Command;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\Application;
16+
use Symfony\Component\Console\Tester\CommandTester;
17+
use Symfony\Component\Mailer\Command\MailerSendEmailCommand;
18+
use Symfony\Component\Mailer\MailerInterface;
19+
use Symfony\Component\Mime\Email;
20+
21+
class MailerSendEmailCommandTest extends TestCase
22+
{
23+
public function testSendMail()
24+
{
25+
$expectedMail = $this->createExpectedMailFromString(
26+
'a@symfony.com',
27+
'b@symfony.com',
28+
'Test',
29+
'body'
30+
);
31+
32+
$mailer = $this->createMock(MailerInterface::class);
33+
$mailer->expects($this->once())->method('send')->with($expectedMail);
34+
35+
$command = new MailerSendEmailCommand($mailer);
36+
37+
$application = new Application();
38+
$application->add($command);
39+
$tester = new CommandTester($application->get('mailer:send-email'));
40+
$tester->execute([
41+
'from' => 'a@symfony.com',
42+
'to' => 'b@symfony.com',
43+
'--subject' => 'Test',
44+
'--body' => 'body',
45+
]);
46+
47+
$this->assertSame(0, $tester->getStatusCode());
48+
$this->assertStringContainsString('[OK] Email was successfully sent to "b@symfony.com"', $tester->getDisplay());
49+
}
50+
51+
public function testSendMailNoSubject()
52+
{
53+
$expectedMail = $this->createExpectedMailFromString(
54+
'a@symfony.com',
55+
'b@symfony.com',
56+
'Testing Mailer Component',
57+
'This is a test email.'
58+
);
59+
60+
$mailer = $this->createMock(MailerInterface::class);
61+
$mailer->expects($this->once())->method('send')->with($expectedMail);
62+
63+
$command = new MailerSendEmailCommand($mailer);
64+
65+
$application = new Application();
66+
$application->add($command);
67+
$tester = new CommandTester($application->get('mailer:send-email'));
68+
$tester->execute([
69+
'from' => 'a@symfony.com',
70+
'to' => 'b@symfony.com',
71+
]);
72+
73+
$this->assertSame(0, $tester->getStatusCode());
74+
$this->assertStringContainsString('[OK] Email was successfully sent to "b@symfony.com"', $tester->getDisplay());
75+
}
76+
77+
public function testSendMailBodyFromFile()
78+
{
79+
$temporaryPath = $this->createTemporaryPath();
80+
file_put_contents($temporaryPath, 'Body from file');
81+
82+
$expectedMail = $this->createExpectedMailFromFile(
83+
'a@symfony.com',
84+
'b@symfony.com',
85+
'Testing Mailer Component',
86+
'Body from file'
87+
);
88+
89+
$mailer = $this->createMock(MailerInterface::class);
90+
$mailer->expects($this->once())->method('send')->with($expectedMail);
91+
92+
$command = new MailerSendEmailCommand($mailer);
93+
94+
$application = new Application();
95+
$application->add($command);
96+
$tester = new CommandTester($application->get('mailer:send-email'));
97+
$tester->execute([
98+
'from' => 'a@symfony.com',
99+
'to' => 'b@symfony.com',
100+
'--body' => $temporaryPath,
101+
'--body-source' => 'file',
102+
]);
103+
104+
$this->assertSame(0, $tester->getStatusCode());
105+
$this->assertStringContainsString('[OK] Email was successfully sent to "b@symfony.com"', $tester->getDisplay());
106+
}
107+
108+
private function createExpectedMailFromString(string $from, string $to, string $subject, string $body): Email
109+
{
110+
return (new Email())
111+
->from($from)
112+
->to($to)
113+
->priority(Email::PRIORITY_HIGH)
114+
->subject($subject)
115+
->text($body)
116+
->html(
117+
<<<HTML
118+
<!doctype html>
119+
<html>
120+
<head>
121+
<meta name="viewport" content="width=device-width" />
122+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
123+
<title>$subject</title>
124+
</head>
125+
<body>
126+
<p>$body</p>
127+
</body>
128+
</html>
129+
HTML
130+
);
131+
}
132+
133+
private function createExpectedMailFromFile(string $from, string $to, string $subject, string $body): Email
134+
{
135+
return (new Email())
136+
->from($from)
137+
->to($to)
138+
->priority(Email::PRIORITY_HIGH)
139+
->subject($subject)
140+
->text($body)
141+
->html($body);
142+
}
143+
144+
private function createTemporaryPath(): string
145+
{
146+
return stream_get_meta_data(tmpfile())['uri'];
147+
}
148+
}

src/Symfony/Component/Mailer/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"symfony/mailchimp-mailer": "^4.4|^5.0",
3434
"symfony/messenger": "^4.4|^5.0",
3535
"symfony/postmark-mailer": "^4.4|^5.0",
36-
"symfony/sendgrid-mailer": "^4.4|^5.0"
36+
"symfony/sendgrid-mailer": "^4.4|^5.0",
37+
"symfony/console": "^4.4|^5.0"
3738
},
3839
"conflict": {
3940
"symfony/http-kernel": "<4.4"

0 commit comments

Comments
 (0)