A few months ago I was putting the final touches on a new SaaS site for my employer (TD Mobility for reference). We had decided to build the site on Symfony2 with a MongoDB back-end. The site is able to be white-labeled per domain, has robust settings management, and is highly configurable.
Everything was going great until I tried to deal with the requirement to send emails from a unique email address depending on the settings provided for the particular domain. IE. Joe’s Emporium does not want to send their order confirmations from [email protected], they want to send their emails from [email protected]. (I apologize to Joe’s Emporium in advance for using them in an example.)
This would normally be a simple task. Just change the “from” address to whatever I want and away it goes. Only problem with this solution is that in today’s modern time with Nigeria pumping out 1 billion emails per person per day, an email from an address that is spoofing the sending server will likely go straight to junk.
No problem you say, I can just set the settings for the outgoing SMTP server in the parameters.yml file. Yes you can. However these settings will then be used for EVERY email that leaves the system. Remember, we need to send emails via a different SMTP server depending on the settings provided by the customer.
After much research (Google) on the topic, the closest I came was an article on StackOverflow (http://stackoverflow.com/questions/10723129/changing-smtp-settings-in-swiftmailer-dynamically) which solves the problem but only if the message is being sent immediately. (For the record, this is the route we went until the subject of this article was conjured out of the ether.) This is fine, and works, however it puts an undue amount of lag into the page load times because your web server needs to connect to the foreign SMTP server to send the message.
The problem had been hanging in the back of my mind until I finally had an epiphany. I would add the SMTP connection settings to the Message when it was created, and then when we flush the spool (remember we want to use the file spool) we will read the connection info for each message, change the transport’s settings, send the message, and then revert the transport back so it doesn’t screw up anything else.
Definitely easier said than done. Fabien Potencier (Author of Swiftmailer, Symfony2, and all around PHP Guru) did not make life easy for modifying Swiftmailer. It uses some require_once statements that load configuration and initialize things, lots of singleton class patterns, and a fair number of static methods.
The route I ended up taking was a two-pronged approach.
First I created a bundle that added some Symfony2 event handling to Swiftmailer’s Mailer and Transport Classes. I also added functionality to the Message object so that it now has a key-value store on it called AdditionalData. This was by far the most complicated piece. Luckily I bundled it up and put it on Packagist for everyone to use. You can find it at https://packagist.org/packages/tdm/swiftmailer-events-bundle. If anybody wants to discuss the merits (or bone-headedness) of my solution, toss it in the comments or better yet, Fork it on GitHub ([https://github.com/TDMobility/SwiftmailerEventsBundle])https://github.com/TDMobility/SwiftmailerEventsBundle)) and help me make it better!
The second part of the solution was to write a listener class which had four methods and listened on four separate events. The flow of what happens is below.The system sends an email using Swiftmailer.
- An event () is triggered via a wrapper method for the Swift_Mailer::send() method. This happens immediately prior to calling the original send() method.
- The listener picks up this event
tdm.swiftmailer.mailer.pre_send_processand adds the settings to the message object using the AdditionalData functionality. The settings in our case are derived by a settings handler and the whole settings handler was injected directly into the listener.
- The message is then
sentto the file spool.
On a cron, the
app/console swiftmailer:send:spool command is called which cycles through each message and sends it. For each message the following steps are followed.
- The transport::send() method has the same type of wrapper method that Mailer::send() has. When send() is called, the wrapper method dispatches the
tdm.swiftmailer.transport.pre_send_processevent which is picked up by the same listener as above (doesn’t have to be the same, but it makes it neat and tidy).
- The listener checks the values in the message’s additional data and if all the connection data is present, it updates the transport object. The original values were stored in a class property for later access. It then stops and starts the transport to force a new connection.
- The transport actually sends the message via SMTP.
- The send() wrapper dispatches the
tdm.swiftmailer.transport.post_send_processwhich is picked up by the listener. The original values are pulled from the class property and placed back into the transport object. Again the stop and start methods are called, and everything is back to normal.
Feel free to post any feedback or questions in the comments. If you want the code, you can get it at https://github.com/TDMobility/SwiftmailerEventsBundle. I even added some example code to demonstrate how we used the listeners.