SwiftMailer silently ignores errors when messages

2019-05-25 03:48发布

问题:

When run from inside controller and when in-memory spooling is configured via spool: { type: memory } swiftmailer seems to work like this:

  • whenever from within controller mailer->send($message) is called -> save message in memory
  • when controller has finished work and symfony Kernel is about to shutdown (somwehere in event kernel.terminate or smth) - check messages saved in memory and _actually submit them to SMTP-server

However this last step seems to silently ignore any errors which may be thrown when peforming submitting of the message to the SMTP server.

I discovered that errors are silently swallowed when I was setting up SMTP from Amazon SES, and have made wrong configuration:

mailer_transport: smtp
# For illustration I put WRONG port 9999, which means that this should trigger
# error (correct port would be 587)
mailer_port: 9999
mailer_encryption: tls
mailer_host: email-smtp.us-east-1.amazonaws.com
mailer_user: SES_USER_KEY
mailer_password: SES_USER_SECRET 

Now, if I attempt to send email using wrong configuration from a symfony Command, just as expected I get Swift_TransportException and error is NOT silently ignored. (From my observations it seems that symfony commands do NOT use memory-spooling and attempt to send messages immediately)

Below is sample of the command (so you're sure I am doing it right).

protected function execute(InputInterface $input, OutputInterface $output) {
     $email = $input->getArgument('email');
     $content = $this->getHelper('dialog')->ask($output, 'Please input content:');

     $emsg = \Swift_Message::newInstance();
     $emsg->setTo($email);
     $emsg->setFrom('d@my-ses-verified-domain.com');
     $emsg->setSubject('This is subject');
     $emsg->setBody($content);

     $this->getContainer()->get('mailer')->send($emsg);
}

And here's command output when the exception Swift_TransportException is thrown:

ubuntu@localhost:~/my-app$ console acme:email:send existing@email.com
We are going to send email to :existing@email.com

Please input content:asdf adf


[Swift_TransportException]                                                                                    
Connection could not be established with host email-smtp.us-east-1.amazonaws.com [Connection timed out #110]  

Hovewer if I attempt to send email from controller, then I see no error messages. Basically this means that in case there's an error (misconfiguration or network error or SMTP server down), all the emails I sent will silently disappear without any trace (no exception thrown, no error loggged in dev.log neither in prod.log).

How can I force Swiftmailer to leave trace of failed delivery attempt?

回答1:

You might be able to force the delivery of the emails by flushing the spooler manually, which should then allow you to catch the exception and log as needed. I found an example in the docs here (modified for the context of a controller)...

http://symfony.com/doc/current/cookbook/console/sending_emails.html

$message = new \Swift_Message();

// ... prepare the message

$mailer = $this->get('mailer');

$mailer->send($message);

// now manually flush the queue
$spool = $mailer->getTransport()->getSpool();
$transport = $this->get('swiftmailer.transport.real');

$spool->flushQueue($transport);

While the example was originally for use in the console environment, I see no reason why it wouldn't be valid in a controller.

Edit:

Another way to log the exception would be to utilize the Swiftmailer event system. This involves creating a custom plugin (as a service would be best) that implements \Swift_Events_TransportExceptionListener, then registering it with the mailer.

For example, the custom plugin class:

namespace Acme\DemoBundle\SwiftPlugin;

use Symfony\Bridge\Monolog\Logger;

class SwiftExceptionLoggerPlugin implements \Swift_Events_TransportExceptionListener
{
    private $logger;

    public function __construct(Logger $logger)
    {
        $this->logger = $logger;
    }

    /**
     * Invoked as a TransportException is thrown in the Transport system.
     *
     * @param Swift_Events_TransportExceptionEvent $evt
     */
    public function exceptionThrown(\Swift_Events_TransportExceptionEvent $evt)
    {
        $e = $evt->getException();
        $message = $e->getMessage();
        $this->logger->err(sprintf("Swiftmailer Exception: %s", $message));
    }
}

Then add this as a service along with the swiftmailer.default.plugin tag. That will automatically register it as a Swiftmailer plugin...

<service id="acme.demo.swift_plugin.swift_exception_logger_plugin" class="Acme\DemoBundle\SwiftPlugin\SwiftExceptionLoggerPlugin">
    <tag name="swiftmailer.default.plugin" />
    <argument type="service" id="logger" />
</service>

This will log the exception message in the standard log area for dev/prod. However, if the timeout takes very long to occur I think it still may not log correctly if the user clicks away or closes their browser/tab, etc. Perhaps the above along with setting a lower timeout value for Swiftmailer via the mailer_timeout parameter.