2

For various reasons I want/need to log all emails sent through my website which runs on Symfony 5.

What I have so far is a subscriber that creates an Entity of type EmailLogEntry when a MessageEvent class is created (at least that's what I understand from (MessageEvent::class) - correct me if I'm wrong). I also use this subscriber to fill in missing emailadresses with the default system address.

Now, after sending the email, I'd like to adjust my entity and call $email->setSent(true);, but I can't figure out how to subscribe to the event that tries to send the email. And for the reusability of the code I don't want to do that in the Services (yes, it's multiple since there's multiple sources that generate mails) where I actually call $this->mailer->send($email);.

My questions now are:

  • Can someone tell me how I can subscribe to the Mailers send event?
  • How, in general, do I figure out what events I can subscribe to? The kernel events are listed in the documentation, but what about all the other events that are fired?

Btw, my subscriber code at the moment:

class SendMailSubscriber implements EventSubscriberInterface
    {
        public static function getSubscribedEvents()
        {
            return [
                MessageEvent::class => [
                    ['onMessage', 255],
                    ['logMessage', 0],
                ],
            ];
        }

        public function logMessage(MessageEvent $event) {
            $email = new EmailLogEntry();
            [...]
        }
    }

Thanks.

fun2life
  • 139
  • 1
  • 11
  • the idea in https://stackoverflow.com/questions/18033210/logging-swiftmailer-send-activity-in-symfony2 very well resembles your use case. you should be able to adapt it. you should also refrain from asking too many questions at once. for events, have a look at the `bin/console debug:event*` commands – Jakumi Feb 06 '20 at 15:25
  • Unfortunately, the Link is for SwiftMailer and I‘m working with Symfony‘s native new Mailer. SwiftMailer has a lot more options that would help me with my problem... – fun2life Feb 06 '20 at 16:43
  • 1
    and that's exactly why I said "adapt it", because even though the Symfony Mailer is not identical to SwiftMailer, I believe it's mostly identical in that you can use the decoration pattern to wrap it. Especially if you always use `MailerInterface` instead of the `Mailer` type hints. It's also very forgiving to implement, because the Mailer only has one function to decorate (send). Also, decoration is the right approach. for reference see https://github.com/symfony/mailer/blob/master/Mailer.php (maybe read up on decoration if necessary) – Jakumi Feb 06 '20 at 17:18
  • do you have the correct use clause for the MessageEvent? that could be a reason for it to not work. – Jakumi Feb 06 '20 at 17:42
  • @Jakumi: ok, I will have to look the thing about decoration pattern up - that's the first time I hear about that. This is my first Symfony based project and so far I'm learning by doing and working thrugh tutorials or similar problems. About the use clause: No, I don't. I've found about `use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mailer\Event\MessageEvents;` but that's all I could find by reading thrugh the Mailer code files. All classes in the EventListener folder also subscribe to `MessageEvent::class` and use `MessageEvent` – fun2life Feb 07 '20 at 14:07
  • have you checked `bin/console debug:event-dispatcher` there should be the MessageEvent and your listener. – Jakumi Feb 07 '20 at 17:47
  • @Jakumi: That's what it sais if I run that command on Windows:#1 App\EventSubscriber\SendMailSubscriber::onMessage() 255 #2 App\EventSubscriber\SendMailSubscriber::logMessage() 0 - But that doesn't help me figure out what events I could subscribe to, or does it? – fun2life Feb 08 '20 at 09:36
  • as far as I can tell, it shows all Events some Listener has subscribed to. as far as I can tell, the only event the Mailer dispatches is the MessageEvent. Transports theoretically could implement more events ... but the default ones don't. and as far as I can tell, you have already subscribed to the one event, that is fired. so ... any questions left? – Jakumi Feb 08 '20 at 13:45
  • Thanks. I now have a Subscriber and a decoration pattern for the MailerInterface, but both of them don‘t do what I want. The next problem is that at the point of the decorated MailerInterface, the whole message has so much payload that it‘s not worth saving. I also never know if the mail was sent. There‘s a test folder with a MessageCounter in the project folder that looks like there might be a way, but I have no idea where I would have to implement the logic in that. Since I‘m using a service for all my emails anyway, I just manually add the logging there and hope for improvement in Mailer. – fun2life Feb 08 '20 at 21:00
  • by default, `send` returns the message if it was "sent", otherwise it returns null (for example on empty recipient list) or it throws an Exception, if the mail transport produces one (maybe it will forward the error from the mail server). please note, that "sent" means, you have transfered data to some server (if applicable), not that it actually was transported to your destination or was processed at all, let alone delivered or read) – Jakumi Feb 08 '20 at 22:15
  • unfortunately this used to be the case, but not anymore. `public function send(RawMessage $message, Envelope $envelope = null): void`. The docs are lacking behind: https://github.com/symfony/symfony-docs/issues/13091 – fun2life Feb 10 '20 at 15:48
  • @Jakumi: I don't know whether that's smart or not, but instead of using the MailerInterface, I could use the TransportInterface and this class does exactly what you've described. – fun2life Feb 10 '20 at 16:27
  • sounds good to me, in theory. try and find out ;o) – Jakumi Feb 10 '20 at 21:25

1 Answers1

1

The answer to my question is: at the moment you can not subscribe to the send() event of Mailer.

As a workaround, that's my code for now: It's an extract from my Mailer service.

// send email and log the whole message
private function logSendEmail($email) {
    $log = new EmailLog();

    // sender
    $log->setSender(($email->getFrom()[0]->getName() ?:"")." <".$email->getFrom()[0]->getAddress().">");

    // get recipient list
    foreach($email->getTo() AS $to) {
        $recipient[] = "To: ".$to->getName()." <".$to->getAddress().">";
    }
    foreach($email->getCc() AS $cc) {
        $recipient[] = "CC: ".$cc->getName()." <".$cc->getAddress().">";
    }
    foreach($email->getBcc() AS $bcc) {
        $recipient[] = "Bcc: ".$bcc->getName()." <".$bcc->getAddress().">";
    }
    $log->setRecipient(implode(";",$recipient));

    // other message data
    $log->setSubject($email->getSubject());
    $log->setMessage(serialize($email->__serialize()));
    $log->setMessageTime(new \DateTime("now"));
    $log->setSent(0); // set Sent to 0 since mail has not been sent yet

    // try to send email
    try {
        $this->mailer->send($email);
        $log->setSent(1);   // set sent to 1 if mail was sent successfully
    }

//      catch(Exception $e) {
//          to be determined
//      }

    // and finally persist entity to database
    finally {
        $this->em->persist($log);
        $this->em->flush();
    }
}

EmailLog is an Entity I created. There's a slight overhang as I save the sender, recipients and subject seperatly. However, in compliance with consumer data protection, I plan to clear the message field automatically after 30 days while holding on to the other fields for 6 months.

I'm also not using a subscriber as of now, mostly because website visitors can request a message copy and I'm not interested in logging that as well. Also, since I want to produce reusable code, I might at some point face the problem that mails could contain personal messages and I certainly will not want to log this mails.

fun2life
  • 139
  • 1
  • 11