Symfony: Logging all Mails created by Mailer - symfony

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.

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.

Related

Api-Platform: using PUT for creating resources

I would like to use the PUT method for creating resources. They are identified by an UUID, and since it is possible to create UUIDs on the client side, I would like to enable the following behaviour:
on PUT /api/myresource/4dc6efae-1edd-4f46-b2fe-f00c968fd881 if this resource exists, update it
on PUT /api/myresource/4dc6efae-1edd-4f46-b2fe-f00c968fd881 if this resource does not exist, create it
It's possible to achieve this by implementing an ItemDataProviderInterface / RestrictedDataProviderInterface.
However, my resource is actually a subresource, so let's say I want to create a new Book which references an existing Author.
My constructor looks like this:
/**
* Book constructor
*/
public function __construct(Author $author, string $uuid) {
$this->author = $author;
$this->id = $uuid;
}
But I don't know how to access the Author entity (provided in the request body) from my BookItemProvider.
Any ideas?
In API Platform many things that should occur on item creation is based on the kind of request it is. It would be complicated to change.
Here are 2 possibilities to make what you want.
First, you may consider to do a custom route and use your own logic. If you do it you will probably be happy to know that using the option _api_resource_class on your custom route will enable some listeners of APIPlaform and avoid you some work.
The second solution, if you need global behavior for example, is to override API Platform. Your main problem for this is the ReadListener of ApiPlatform that will throw an exception if it can't found your resource. This code may not work but here is the idea of how to override this behavior:
class CustomReadListener
{
private $decoratedListener;
public function __construct($decoratedListener)
{
$this->decoratedListener = $decoratedListener;
}
public function onKernelRequest(GetResponseEvent $event)
{
try {
$this->decoratedListener->onKernelRequest($event);
} catch (NotFoundHttpException $e) {
// Don't forget to throw the exception if the http method isn't PUT
// else you're gonna break the 404 errors
$request = $event->getRequest();
if (Request::METHOD_PUT !== $request->getMethod()) {
throw $e;
}
// 2 solutions here:
// 1st is doing nothing except add the id inside request data
// so the deserializer listener will be able to build your object
// 2nd is to build the object, here is a possible implementation
// The resource class is stored in this property
$resourceClass = $request->attributes->get('_api_resource_class');
// You may want to use a factory? Do your magic.
$request->attributes->set('data', new $resourceClass());
}
}
}
And you need to specify a configuration to declare your class as service decorator:
services:
CustomReadListener:
decorate: api_platform.listener.request.read
arguments:
- "#CustomReadListener.inner"
Hope it helps. :)
More information:
Information about event dispatcher and kernel events: http://symfony.com/doc/current/components/event_dispatcher.html
ApiPlatform custom operation: https://api-platform.com/docs/core/operations#creating-custom-operations-and-controllers
Symfony service decoration: https://symfony.com/doc/current/service_container/service_decoration.html

Implementing asynchronous publish subscribe topic in tibco ems

my scenario is that me as a movie distributor, need to update my clients on new movies, I publish this information on a topic with durable subscribers and clients who want to buy the movie will express their interest.
However, this is where things go south, my implementation of the publisher stops listening as soon as it receives the first reply. Any help would be greatly appreciated. Thank you.
request(Message message)
Sends a request and waits for a reply.
The temporary topic is used for the JMSReplyTo destination; the first reply is returned, and any following replies are discarded.
https://docs.oracle.com/javaee/6/api/javax/jms/TopicRequestor.html
First thing first... I have questions regarding the scenario. Is this some kind of test/exercice, or are we talking about a real world scenario ?
Are all client interested in the movie SEPARATE topic subscribers ? How does that scale ? I the plan to have a topic for every movie, and possible interested parties declaring durable subscribers (one each, for every movie) ? This seems to be abuse of durable subcribers... I would suggest using ONLY one subscriber (in system B) to a "Movie Released" event/topic (from system A), and have some code (in system B) reading all the clients from a DB to send emails/messages/whatever. (If system A and B are the same, it may or not be a good idea to use EMS at all... depends.)
If it is not an exercise, I must comment : Don't use a MOM (EMS, ActiveMQ) to do a DBMS' (Oracle, PostGreSQL) work !
With the disclaimer section done, I suggest an asynchronous subscription approach (These two clips are taken for the EMS sample directory. File tibjmsAsyncMsgConsumer.java).
Extract from the constructor (The main class must implements ExceptionListener, MessageListener):
ConnectionFactory factory = new com.tibco.tibjms.TibjmsConnectionFactory(serverUrl);
/* create the connection */
connection = factory.createConnection(userName,password);
/* create the session */
session = connection.createSession();
/* set the exception listener */
connection.setExceptionListener(this);
/* create the destination */
if (useTopic)
destination = session.createTopic(name);
else
destination = session.createQueue(name);
System.err.println("Subscribing to destination: "+name);
/* create the consumer */
msgConsumer = session.createConsumer(destination);
/* set the message listener */
msgConsumer.setMessageListener(this);
/* start the connection */
connection.start();
The method is then called every time a message arrives.
public void onMessage(Message msg)
{
try
{
System.err.println("Received message: " + msg);
}
catch (Exception e)
{
System.err.println("Unexpected exception in the message callback!");
e.printStackTrace();
System.exit(-1);
}
}
You want to continue reading messages in a loop. Here is an example:
/* read messages */
while (true)
{
/* receive the message */
msg = msgConsumer.receive();
if (msg == null)
break;
if (ackMode == Session.CLIENT_ACKNOWLEDGE ||
ackMode == Tibjms.EXPLICIT_CLIENT_ACKNOWLEDGE ||
ackMode == Tibjms.EXPLICIT_CLIENT_DUPS_OK_ACKNOWLEDGE)
{
msg.acknowledge();
}
System.err.println("Received message: "+ msg);
}
You may want to also consider a possible issue with durable consumers. If your consumers never pick up their messages, storage will continue to grow at the server side. For this reason you may want to send your messages with an a expiration time, and/or limit maximum number of messages (or size in KB/MB/GB) of the JMS topics you are using.

Silverstripe 3 Extending Upload Field with nice error messages

I've made a custom upload field to extract zip files automatically. I can't figure out how to throw 'nice' error messages that are readable to a end-user.
I want the errors to come from the saveTemporaryFile method.
eg (something like this would be great):
return $this->setError("My custom error");
Error messages are usually thrown in the validate function where you have access to the form field's Validator object. For instance:
<?php
class MyUploadField extends UploadField {
// ...
private $temporaryFileSaveSuccessful = false;
private function saveTemporaryFile() {
// Do extract logic
// set $this->temporaryFileSaveSuccessful appropriately
}
public function validate($validator) {
if(!$this->temporaryFileSaveSuccessful) {
$validator->validationError('ExtractionUnsuccessful',
'Unable to extract your zip file',
'validation'
);
return false;
}
return parent::validate($validator);
}
// ...
}
However this has the one issue that the validate function is called on form submission, so depending on your code saveTemporaryFile may not have been run by that time. You could possibly run the extraction earlier by creating a Form subclass and overriding the httpSubmission function.

Trouble with dispatching events from external AS class in Flex

I've been trying this for a day now and I can't work it out.
I have a main application Planner.mxml. This view has a couple of custom components, one of which is LoginView.mxml. In LoginView.mxml I do the following:
protected function btnLoginClick(event:MouseEvent):void
{
try
{
var login:Login = new Login(txtEmail.text, txtPassword.text);
}
catch(error:Error)
{
Alert.show(error.message, "Oops!");
}
}
I create a new instance of my Login class and send some parameters to the constructor. My constructor looks like this:
public function Login(email:String, password:String)
{
if(email == "" || password == "")
{
throw new Error("Please complete all fields.");
}
else
{
var loginRequest:HTTPService = new HTTPService();
var parameters:Object = new Object();
parameters.email = email;
parameters.password = password;
loginRequest.url = Globals.LOGIN_URL;
loginRequest.resultFormat = "object";
loginRequest.method = "POST";
loginRequest.addEventListener("result", loginHandleResult);
loginRequest.addEventListener("fault", loginHandleFault);
loginRequest.send(parameters);
}
}
Here I check if all fields are complete, and if so, I put the constructor parameters in a parameters object which I then send to the HTTPService, which is a simple PHP file that handles the request, checks with the db and returns some xml. (This might not be the best way, but this really isn't too important at this point).
If the user is logged in successfully, the xml will contain a status property which is set to true. I check for this in the result event handler of the HTTP service. This is where everything goes wrong though.
protected function loginHandleResult(event:ResultEvent):void
{
if(event.result.status == true)
{
trace("logged in");
// here stuff goes wrong
var e:LoggedInEvent = new LoggedInEvent("loggedIn");
dispatchEvent(e);
}
else
{
trace("not logged in");
Alert.show("Wrong credentials.", "Oops!");
}
}
As you can see, when the user is successfully logged in, I want to dispatch a custom event; if not, I show an alert box. However, this event doesn't dispatch (or at least, I don't know how to listen for it).
I would like to listen for it in my main application where I can then change my viewstate to the logged-in state. However, the event never seems to get there. I listen for it by having loggedIn="loggedInHandler(event)" on my loginComponent.
Any idea how to do this? Thanks in advance. I would really appreciate any help.
First, your Login needs to extend event dispatcher or implement IEventDispatcher. I'm not sure why you're getting compiler errors trying to dispatch events from it.
Next, you need to listen to the new Login instance for that event.
However, you have an architectural problem here that your View should NOT be handling business logic and it should DEFINITELY not be creating new objects that are not its own children on the Display List.
Instead, you should dispatch an event from the View that REQUESTS that a login occur, and then that request should be handled further up. Depending on the scale of your application, this can be the main mxml file or separate controller or Command logic. It is ok for the View to do a minimal amount of validation prior to dispatching the Event, but ideally you would want to encapsulate this stuff into a PresentationModel (because it is easier to test).
If you dispatch event then somebody who interested in this event must to subscribe to this event.
If you dispatch event from LoginView instance then in object who wait this event you need such lines:
loginViewInstance.addEventListemer("loggedIn", loggedInHandler);
and in handler:
private function loggedInHandler(event:LoggedInEvent):void
{
//do something
}
do what you need.

Unable to broadcast to single connection using Atmosphere runtime

I am using Atmosphere runtime 0.6 Snapshot. Tomcat 7 is logging correctly that I am using the Http11 Nio connector and there is no warning that BlockingIO will be used.
I am trying to send messages to three kinds of channels.
Global Broadcaster - broadcast to all suspended resources. (All)
Broadcast to a particular resource (say, Partner)
Broadcast to current resource (Self)
When a login action occurs, what all do I have to store in session in order to achieve this kind of broadcasting?
Some details of my code are as follows:
My Handler implements AtmosphereHandler
In the constructor, I instantiate the globalBroadcaster as follows:
globalBroadcaster = new DefaultBroadcaster();
On login,
resource.getAtmosphereConfig().getServletContext().setAttribute(name, selfBroadcaster);
where name is the user name from request parameter and selfBroadcaster is a new instance of DefaultBroadcaster.
Here is the code for sendMessageToPartner,
private synchronized void sendMessageToPartner(Broadcaster selfBroadcaster,
AtmosphereResource<HttpServletRequest, HttpServletResponse> resource,String name, String message) {
// this gives the partner's name
String partner= (String) resource.getAtmosphereConfig().getServletContext().getAttribute(name + PARTNER_NAME_TOKEN);
// get partner's broadcaster
Broadcaster outsiderBroadcaster = (Broadcaster) resource
.getAtmosphereConfig().getServletContext()
.getAttribute(partner);
if (outsiderBroadcaster == null) {
sendMessage(selfBroadcaster, "Invalid user " + partner);
return;
}
// broadcast to partner
outsiderBroadcaster.broadcast(" **" + message);
I hope I have given all the required information. I can provide more information if required.
The problem is, the global message gets sent. When message to partner is sent, sometimes it gets blocked, the message is not received in the client at all. This happens consistently after 3-4 messages.
Is there some threading problem? What am I doing wrong?
I hope somebody helps me out with this.
Ok, I figured out how this can be achieved with Atmosphere runtime.
First, I upgraded to 0.7 SNAPSHOT, but I think the same logic would work with 0.6 as well.
So, to create a broadcaster for a single user:
In GET request,
// Use one Broadcaster per AtmosphereResource
try {
atmoResource.setBroadcaster(BroadcasterFactory.getDefault().get());
} catch (Throwable t) {
throw new IOException(t);
}
// Create a Broadcaster based on this session id.
selfBroadcaster = atmoResource.getBroadcaster();
// add to the selfBroadcaster
selfBroadcaster.addAtmosphereResource(atmoResource);
atmoResource.suspend();
When login action is invoked,
//Get this broadcaster from session and add it to BroadcasterFactory.
Broadcaster selfBroadcaster = (Broadcaster) session.getAttribute(sessionId);
BroadcasterFactory.getDefault().add(selfBroadcaster, name);
Now the global broadcaster. The logic here is, you create a broadcaster from the first resource and then add each resource as they log in.
Broadcaster globalBroadcaster;
globalBroadcaster = BroadcasterFactory.getDefault().lookup(DefaultBroadcaster.class, GLOBAL_TOKEN, false);
if (globalBroadcaster == null) {
globalBroadcaster = selfBroadcaster;
} else {
BroadcasterFactory.getDefault().remove(
globalBroadcaster, GLOBAL_TOKEN);
AtmosphereResource r = (AtmosphereResource) session
.getAttribute("atmoResource");
globalBroadcaster.addAtmosphereResource(r);
}
BroadcasterFactory.getDefault().add(globalBroadcaster,
GLOBAL_TOKEN);
Finally, you can broadcast to Single connection or Globally to all connections as follows:
// Single Connection/Session
Broadcaster singleBroadcaster= BroadcasterFactory.getDefault().lookup(
DefaultBroadcaster.class, name);
singleBroadcaster.broadcast("Only for you");
// Global
Broadcaster globalBroadcaster = BroadcasterFactory.getDefault().lookup(DefaultBroadcaster.class,GLOBAL_TOKEN, false);
globalBroadcaster.broadcast("Global message to all");
To send message to partner, just lookup the broadcaster for the partner and do the same as above for single connection.
Hope this helps someone who tries to achieve the same.
There may be better ways of doing this.
I think I will have to use this approach until someone suggests a better solution.

Resources