Symfony Swiftmailer: Conditionally spool emails or send immediately - symfony

Has anyone ever set a condition on spooling emails with Swiftmailer in Symfony?
I'd like to have the option to either send my emails immediately or spool them in a file, depending on which function I'm running.
I have the email service abstracted in an own Bundle and just call the sendEmail() function in other Bundles when needed. But for some Bundles/Functions, I'd like the emails to be sent out immediately and for others, spooling is fine. I thought about using a spool parameter in my sendEmail() function, so if the parameter is set true when calling the function, the emails get spooled and if it's set to false, they get send immediately.
Or maybe a simple if condition would be sufficient?]
Any ideas, tips, experiences etc. would be awesome!
Update
in my config.yml:
# Swiftmailer Configuration
swiftmailer:
transport: "%mailer_transport%"
host: "%mailer_host%"
username: "%mailer_user%"
password: "%mailer_password%"
spool:
type: file
path: /srv/http/test/spool

By specifying a spool option in the parameters, Swiftmailers will use an instance of Swift_Transport_SpoolTransport which will manage the spool by sending messages to the Queue instead of instead of sending them out to the world straight away. Through the Transport object, you can access the Spool instance (either a Swift_MemorySpool or a Swift_FileSpool) and force Swiftmailer to flush the queue.
// custom function to send an email
// inject \Swift_Mailer like you normally would
public function sendMessage($name, \Swift_Mailer $mailer, $bypassSpool = false)
{
$message = new \Swift_Message('Hello Email')
->setFrom(/* from */)
->setTo(/* to */)
->setBody(/* render view */);
$mailer->send($message); // pushes the message to the spool queue
if($bypassSpool) {
$spool = $mailer->getTransport->getSpool()
$spool->flushQueue(new Swift_SmtpTransport(
/* Get host, username and password from config */
));
}
}

You can configure two mailers. One with spooling and another without. And use one or the other based on mail priority.

Related

Handling messages from different namespaces in Symfony Messenger with RMQ

I'm building an application with micro services approach. For communication between services I use Symfony Messenger with RMQ transport. Basically everything works fine, but all my services has to be in same namespace. Once I tried to separate them into their own namespaces like App\Mail, App\Auth and so on messenger was complaining about lack of Event classes because whole namesapce is provided within header of message sent to RMQ. Is there any way I could map events from two different namespaces?
For instance Auth app dispatches event UserRegistered so message has type of App\Auth\Event\UserRegistered. I want to handle that event in my Mail app but messenger can't consume it because my Event and Handler are under App\Mail namespace, so it can't find App\Auth\Event\UserRegistered class in "Mail" app.
Example error I'm getting:
In Serializer.php line 85:
Could not decode message: Could not denormalize object of type App\Event\UserRequestedPasswordReset, no supporting normalizer found.
In this exact example I'm sending event UserRequestedPasswordReset from app that is under App namespace, and I'm trying to consume it with application under App\Mail namespace.
I wasn't able to find anything helpful in documentation or over the internet. I was trying to alias App\Event\UserRequestedPasswordReset to App\Mail\Event\UserRequestedPasswordReset in container but no luck. I'm guessing that it's something doable with Denormalizers, but also couldn't find anything helpful over internet.
Communication itself is working, messages are sent to RMQ and received in other services. My setup for RMQ is:
I have multiple queues, one for each service. I have fanout exchange with those queues binded. Whenever I'm producing event I'm publishing it to exchange to populate it to all queues, so interested services can handle them.
Example messenger configuration in one of my services. Besides event I'm using messenger to handle CQRS commands and queries, so I'm using three different buses.
messenger:
default_bus: messenger.bus.commands
buses:
messenger.bus.commands:
middleware:
# - validation
# - doctrine_transaction
messenger.bus.queries:
middleware:
# - validation
messenger.bus.events:
default_middleware: allow_no_handlers
middleware:
# - validation
transports:
events:
dsn: "%env(MESSENGER_AMQP_DSN)%"
options:
exchange:
name: ecommerce_events
type: fanout
queue:
name: ecommerce_auth
routing:
'App\Event\UserCreated': events
'App\Event\UserModified': events
'App\Event\UserChangedPassword': events
'App\Event\UserRequestedPasswordReset': events
I would like to keep my applications in different namespaces and still be able to handle events from other services
So after digging into topic I was able to find solution.
I just needed to create custom serializer and then during encoding I was stripping off the namespace and then during decoding I was providing map for type to actual event class. Here is my code
class EventsSerializer extends Serializer
{
public function encode(Envelope $envelope): array
{
$data = parent::encode($envelope);
$data['headers']['type'] = $this->parseType($data['headers']['type']);
return $data;
}
private function parseType(string $type)
{
return end(explode('\\', $type));
}
public function decode(array $encodedEnvelope): Envelope
{
$translatedType = $this->translateType($encodedEnvelope['headers']['type']);
$encodedEnvelope['headers']['type'] = $translatedType;
return parent::decode($encodedEnvelope);
}
private function translateType($type)
{
$map = [
'UserCreated' => UserCreated::class,
'UserRequestedPasswordReset' => UserRequestedPasswordReset::class
];
return $map[$type] ?? $type;
}
}
In messenger configuration:
framework:
messenger:
serializer:
default_serializer: AppBundle\Serializer\EventsSerializer
Please bare in mind that this is more like proof of concept and it probably can be enhanced, but it's working.

Contact form 7 not getting email

I'm using contact form 7 and facing a problem with this email id, I'm not getting any mail what we can do for that. I used SMTP also.
could you please suggest me any other option.
have you checked spam folder ? Perhaps emails are in spam for email not get in spam you can use https://wordpress.org/plugins/wp-mail-smtp/
Here is setup documentation https://wpforms.com/docs/how-to-set-up-smtp-using-the-wp-mail-smtp-plugin/.
Please check may be helpful for you.
The best option is using PhpMailer library in php. You can check if the email was sent successfully using php mailer. All you need to do is to take all the library code and add it to your FTP or install it using composer.
Here is a simple example.
<?php
// Import PHPMailer classes into the global namespace
// These must be at the top of your script, not inside a function
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
// Load Composer's autoloader
require 'vendor/autoload.php';
// Instantiation and passing `true` enables exceptions
$mail = new PHPMailer(true);
try {
//Server settings
$mail->SMTPDebug = 2; // Enable verbose debug output
$mail->isSMTP(); // Set mailer to use SMTP
$mail->Host = 'smtp1.example.com;smtp2.example.com'; // Specify main and backup SMTP servers
$mail->SMTPAuth = true; // Enable SMTP authentication
$mail->Username = 'user#example.com'; // SMTP username
$mail->Password = 'secret'; // SMTP password
$mail->SMTPSecure = 'tls'; // Enable TLS encryption, `ssl` also accepted
$mail->Port = 587; // TCP port to connect to
//Recipients
$mail->setFrom('from#example.com', 'Mailer');
$mail->addAddress('joe#example.net', 'Joe User'); // Add a recipient
$mail->addAddress('ellen#example.com'); // Name is optional
$mail->addReplyTo('info#example.com', 'Information');
$mail->addCC('cc#example.com');
$mail->addBCC('bcc#example.com');
// Attachments
$mail->addAttachment('/var/tmp/file.tar.gz'); // Add attachments
$mail->addAttachment('/tmp/image.jpg', 'new.jpg'); // Optional name
// Content
$mail->isHTML(true); // Set email format to HTML
$mail->Subject = 'Here is the subject';
$mail->Body = 'This is the HTML message body <b>in bold!</b>';
$mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
$mail->send();
echo 'Message has been sent';
} catch (Exception $e) {
echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}
You can get all the information on PhpMailer here Php Mailer On Github
The PhpMailer is well configured so you don't have to worry about your emails ending up in Spam folder. Also be careful on the number of mails you send periodically to single recipients.

Swiftmailer not sending emails to the delivery_addresses in dev mode

I am having trouble sending emails to the email address specified in config_dev.yml:
swiftmailer:
delivery_addresses: ['dev#example.com']
I am trying to send from a console command:
php app/console ecard:task:run send --env=dev
Console command code:
...
foreach ($emailQueue as $item) {
$transport = Swift_SmtpTransport::newInstance($item['smtp_host'])
->setPort($item['smtp_port'])
->setUserName($item['smtp_login'])
->setPassword($item['smtp_password'])
->setEncryption('ssl');
$mailer = Swift_Mailer::newInstance($transport);
$message = Swift_Message::newInstance()
->setSubject($item['email_subject'])
->setFrom($item['email_from'], $item['email_from_name'])
->setTo($item['email_to'])
->addPart($item['email_template'], 'text/html');
$ret = $mailer->send($message, $failedRecipients);
....
}
The emails are sent to the real recipient addresses, and not the one I specified in config_dev.yml.
If I dump $this->getContainer()->getParameter('kernel.environment') in code, its 'dev'.
I don't get the $mailer this way: $this->getContainer()->get('mailer'), because I want specify/change the smtp settings in cycle - the config is stored in the database, and not in the parameters.yml. Maybe this is the problem?
(Symfony version 2.8.16, Swiftmailer 5.4.5)
If you get your mailer with:
$mailer = Swift_Mailer::newInstance($transport);
then it ignores all the parameters specified in config.yml.
In this case you must set the destination address in code, as you do with the other mailer parameters, something like:
$message = Swift_Message::newInstance()
->setSubject($item['email_subject'])
->setFrom($item['email_from'], $item['email_from_name'])
->setTo($this->getContainer()->getParameter('swiftmailer.delivery_addresses')?:$item['email_to'])
->addPart($item['email_template'], 'text/html');

Symfony2, RabbitMQ: I'm lost

I've installed the RabbitMQ Bundle already. Now here is what I want to do:
Controller: Creates Redis-List, pushes message to client, afterwards send a message into queue, so heavier background task can be executed asynchronously.
But I'm lost.
$msg = array('userid' => $someid);
$this->get('old_sound_rabbit_mq.task_example_producer')->publish(serialize($msg));
This will send some data to a produce? And the according consumer will execute the heavy background task (DB queries etc, based on the "userid" from the producer)? Do I need a callback? What's the queue?! The queue forwards the messages from the producer to the consumer one by one? So can I have multiple consumers to handle more messages at the same time?!
Kinda old post but in case someone else comes looking for help:
It seems that you are using the old_sound's rabbitmq bundle. It has a somewhat helpful tutorial-type of documentation here: https://github.com/videlalvaro/RabbitMqBundle
It helped me get going with rabbitmq in symfony.
In a nutshell:
1: You need to have some configration in the config.yml-file. For example:
# RabbitMQ Configuration
old_sound_rabbit_mq:
connections:
default:
host: 'localhost'
port: 5672
user: 'guest'
password: 'guest'
vhost: '/'
lazy: true
connection_timeout: 3
read_write_timeout: 3
# requires php-amqplib v2.4.1+ and PHP5.4+
keepalive: false
# requires php-amqplib v2.4.1+
heartbeat: 0
producers:
task_example:
connection: default
exchange_options: {name: 'task_example', type: direct}
consumers:
task_example:
connection: default
exchange_options: {name: 'task_example', type: direct}
queue_options: {name: 'task_example'}
callback: test_class
Here the connection is defined, and one producer and one consumer. Both use the same "default" connection.
You will also need to define the callback as a service:
# My services
services:
test_class:
class: AppBundle\Testclasses\rabbittest\testclass
arguments: [#logger]
2: Now you need to have the consumer, which is the callback-option here, the "test_class". Simple consumer could look like this:
namespace AppBundle\Testclasses\rabbittest;
use OldSound\RabbitMqBundle\RabbitMq\ConsumerInterface;
use PhpAmqpLib\Message\AMQPMessage;
class testclass implements ConsumerInterface
{
private $logger; // Monolog-logger.
// Init:
public function __construct( $logger )
{
$this->logger = $logger;
echo "testclass is listening...";
}
public function execute(AMQPMessage $msg)
{
$message = unserialize($msg->body);
$userid = $message['userid'];
// Do something with the data. Save to db, write a log, whatever.
}
}
3: And now the producer that you already had:
$msg = array('userid' => $someid);
$this->get('old_sound_rabbit_mq.task_example_producer')->publish(serialize($msg));
4: And final piece of the puzzle is running the consumer. Consumer is started from the console, I was developing in a windows machine, and used Windows PowerShell. You can start up the consumer like this:
php app/console rabbitmq:consumer task_example
And it should give you the text:
testclass is listening...
, if you copied that from this example. That text is not necessary, and without it, the console will output nothing but will work just fine. Unless some error is presented.
But remember that you have to be in the directory where your symfony-application is. For example:
C:\wamp\www\symfony\my_project
A queue is a list of messages you want processed.
An exchange is a router of messages to queues. (you can have multiple queues listing to the same exchange, for example).
A producer pushes messages to an exchange.
A consumer reads messages from the queue.
Normally you have one producer and many consumers to process the messages in parallel.
The code you posted demonstrates a producer publishing to an exchange.
RabbitMQBundle expects you to have in-depth knowledge of the broker internals. That's not always what you want.
There is a solution that hides all those nitty-gritty details, leaving a simple but yet powerful interface for you to use. The doc is short. If you follow it you get working solution with zero knowledge of how RabbitMQ actually works.
P.S. Here's the blog post on how to migrate from RabbitMQBundle to EnqueueBundle.

How to implement FosOAuthServerBundle to secure a REST API?

I would like to provide a RESTful API secured with OAuth2 using FOSOAuthServerBundle and I'm not really sure about what I have to do.
I followed basic steps from the documentation but some things are missing and I can't find a complete example of what I need.
So, I tried to understand the best I could this example of implementation (the only one I found) but there are still things I don't understand.
First, why do we need a login page in an API? Let's suppose my client is a iPhone or Android App, I see the interest of the login page on the app, but I think the client have just to call a webservice from the API to get its token, am I wrong? So how to implement autorisation and token providing via REST endpoint?
Then, documentation tells to write this firewall :
oauth_authorize:
pattern: ^/oauth/v2/auth
# Add your favorite authentication process here
And I don't know how to add an authentication process. Should I write my own one, for example following this tutorial or am I completely wrong?
Globally, can someone take the time to explain the process needed, after the five steps in the docs, to provide an OAuth2 secured RESTful API? It would be very nice...
EDIT after #Sehael answer :
I still have some questions before it is perfect...
What is represented by the "Client" here? For exemaple, should I create a client for a iPhone app, and another for an Android app? and do I have to create a new client for every instance wanting to use the API? what is the best practice in this case?
Unlike you, I don't use the OAuth process for the front website but the "classic" symfony way. Does it seem strange to you, or is it normal?
What is the usefulness of the refresh_token? How to use it?
I tried to test my new OAuth protected services. I used POSTman chrome extension, which support OAuth 1.0, does OAuth2 look like OAuth1 enough to be tested with POSTman? There is a "secret token" field that I don't know how to fill. If I can't, I'd be glad to see your (#Sehael) PHP class, as you proposed. / Edit : OK I think I found the answer for this one. I just added access_token as GET parameter with the token as value. It seems to be working. It is unfortunate that I have to do reverse engenering on bundle code to find that instead of read it in documentation.
Anyway, thanks a lot !
I have also found that the documentation can be a bit confusing. But after many hours of trying, I figured it out with the help of this blog (update - the blog no longer exists, changed to Internet Archive). In your case, you don't need a firewall entry for ^/oauth/v2/auth because this is for the authorization page. You need to remember what oAuth is able to do... it is used for more than just a REST api. But if a REST api is what you want to protect, you don't need it. here is an example firewall config from my app:
firewalls:
oauth_token:
pattern: ^/oauth/v2/token
security: false
api_firewall:
pattern: ^/api/.*
fos_oauth: true
stateless: true
anonymous: false
secure_area:
pattern: ^/
fos_oauth: true
form_login:
provider: user_provider
check_path: /oauth/v2/auth_login_check
login_path: /oauth/v2/auth_login
logout:
path: /logout
target: /
anonymous: ~
access_control:
- { path: ^/oauth/v2/auth_login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
Notice that you need to define a user provider. If you use FOSUserBundle, there is a user provider already created for you. In my case, I made my own and created a service out of it.
And in my config.yml:
fos_oauth_server:
db_driver: orm
client_class: BB\AuthBundle\Entity\Client
access_token_class: BB\AuthBundle\Entity\AccessToken
refresh_token_class: BB\AuthBundle\Entity\RefreshToken
auth_code_class: BB\AuthBundle\Entity\AuthCode
service:
user_provider: platform.user.provider
options:
supported_scopes: user
I should also mention that the tables that you create in the database (access_token, client, auth_code, refresh_token) are required to have more fields than what is shown in the docs...
Access Token Table: id(int), client_id(int), user_id(int), token(string), scope(string), expires_at(int)
Client Table: id(int), random_id(string), secret(string), redirect_urls(string), allowed_grant_types(string)
Auth Code Table: id(int), client_id(int), user_id(int)
Refresh Token Table: id(int), client_id(int), user_id(int), token(string), expires_at(int), scope(string)
These tables will store the info needed for oAuth, so update your Doctrine entities so they match the db tables like above.
And then you need a way to actually generate the secret and client_id, so that's where the "Creating a Client" section of the docs comes in, although it isn't very helpful...
Create a file at /src/My/AuthBundle/Command/CreateClientCommand.php (you will need to create the folder Command) This code is from the article I linked to above, and shows an example of what you can put into this file:
<?php
# src/Acme/DemoBundle/Command/CreateClientCommand.php
namespace Acme\DemoBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CreateClientCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->setName('acme:oauth-server:client:create')
->setDescription('Creates a new client')
->addOption(
'redirect-uri',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs.',
null
)
->addOption(
'grant-type',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'Sets allowed grant type for client. Use this option multiple times to set multiple grant types..',
null
)
->setHelp(
<<<EOT
The <info>%command.name%</info>command creates a new client.
<info>php %command.full_name% [--redirect-uri=...] [--grant-type=...] name</info>
EOT
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$clientManager = $this->getContainer()->get('fos_oauth_server.client_manager.default');
$client = $clientManager->createClient();
$client->setRedirectUris($input->getOption('redirect-uri'));
$client->setAllowedGrantTypes($input->getOption('grant-type'));
$clientManager->updateClient($client);
$output->writeln(
sprintf(
'Added a new client with public id <info>%s</info>, secret <info>%s</info>',
$client->getPublicId(),
$client->getSecret()
)
);
}
}
Then to actually create the client_id and secret, execute this command from the command line (this will insert into the database the necessary ids and stuff):
php app/console acme:oauth-server:client:create --redirect-uri="http://clinet.local/" --grant-type="password" --grant-type="refresh_token" --grant-type="client_credentials"
notice that the acme:oauth-server:client:create can be whatever you actually name your command in the CreateClientCommand.php file with $this->setName('acme:oauth-server:client:create').
Once you have the client_id and secret, you are ready to authenticate. Make a request in your browser that is something like this:
http://example.com/oauth/v2/token?client_id=[CLIENT_ID_YOU GENERATED]&client_secret=[SECRET_YOU_GENERATED]&grant_type=password&username=[USERNAME]&password=[PASSWORD]
Hopefully it works for you. There is definitly alot to configure, just try to take it one step at a time.
I also wrote a simple PHP class to call my Symfony REST api using oAuth, if you think that would be useful, let me know and I can pass it on.
UPDATE
In response to your further questions:
The "client" is described on the same blog, just a different article. Read the Clients and Scopes section here, it should clarify for you what a client is. Like mentioned in the article, you don't need a client for every user. You could have a single client for all your users if you want.
I actually am also using the classic Symfony authentication for my frontend site, but that may change in the future. So it's always good to keep these things in the back of your mind, but I wouldn't say that it is strange to combine the two methods.
The refresh_token is used when the access_token has expired and you want to request a new access_token without resending the user credentials. instead, you send the refresh token and get a new access_token. This isn't really necessary for a REST API because a single request probably won't take long enough for the access_token to expire.
oAuth1 and oAuth2 are very different, so I would assume that the method you use wouldn't work, but I've never tried with that. But just for testing, you can make a normal GET or POST request, as long as you pass the access_token=[ACCESS_TOKEN] in the GET query string (for all types of requests, actually).
But anyways, here is my class. I used a config file to store some variables, and I didn't implement the ability to DELETE, but that isn't too hard.
class RestRequest{
private $token_url;
private $access_token;
private $refresh_token;
private $client_id;
private $client_secret;
public function __construct(){
include 'config.php';
$this->client_id = $conf['client_id'];
$this->client_secret = $conf['client_secret'];
$this->token_url = $conf['token_url'];
$params = array(
'client_id'=>$this->client_id,
'client_secret'=>$this->client_secret,
'username'=>$conf['rest_user'],
'password'=>$conf['rest_pass'],
'grant_type'=>'password'
);
$result = $this->call($this->token_url, 'GET', $params);
$this->access_token = $result->access_token;
$this->refresh_token = $result->refresh_token;
}
public function getToken(){
return $this->access_token;
}
public function refreshToken(){
$params = array(
'client_id'=>$this->client_id,
'client_secret'=>$this->client_secret,
'refresh_token'=>$this->refresh_token,
'grant_type'=>'refresh_token'
);
$result = $this->call($this->token_url, "GET", $params);
$this->access_token = $result->access_token;
$this->refresh_token = $result->refresh_token;
return $this->access_token;
}
public function call($url, $method, $getParams = array(), $postParams = array()){
ob_start();
$curl_request = curl_init();
curl_setopt($curl_request, CURLOPT_HEADER, 0); // don't include the header info in the output
curl_setopt($curl_request, CURLOPT_RETURNTRANSFER, 1); // don't display the output on the screen
$url = $url."?".http_build_query($getParams);
switch(strtoupper($method)){
case "POST": // Set the request options for POST requests (create)
curl_setopt($curl_request, CURLOPT_URL, $url); // request URL
curl_setopt($curl_request, CURLOPT_POST, 1); // set request type to POST
curl_setopt($curl_request, CURLOPT_POSTFIELDS, http_build_query($postParams)); // set request params
break;
case "GET": // Set the request options for GET requests (read)
curl_setopt($curl_request, CURLOPT_URL, $url); // request URL and params
break;
case "PUT": // Set the request options for PUT requests (update)
curl_setopt($curl_request, CURLOPT_URL, $url); // request URL
curl_setopt($curl_request, CURLOPT_CUSTOMREQUEST, "PUT"); // set request type
curl_setopt($curl_request, CURLOPT_POSTFIELDS, http_build_query($postParams)); // set request params
break;
case "DELETE":
break;
default:
curl_setopt($curl_request, CURLOPT_URL, $url);
break;
}
$result = curl_exec($curl_request); // execute the request
if($result === false){
$result = curl_error($curl_request);
}
curl_close($curl_request);
ob_end_flush();
return json_decode($result);
}
}
And then to use the class, just:
$request = new RestRequest();
$insertUrl = "http://example.com/api/users";
$postParams = array(
"username"=>"test",
"is_active"=>'false',
"other"=>"3g12g53g5gg4g246542g542g4"
);
$getParams = array("access_token"=>$request->getToken());
$response = $request->call($insertUrl, "POST", $getParams, $postParams);

Resources