Sending from multiple Mailgun domains using Laravel Mail facade - email

I'm using the Laravel 4's Mail::queue() to send emails, using the built in Mailgun driver. The problem is that there are multiple Mailgun domains I would like to be able to send emails from, but the domain must be set in app/config/services.php. Since I'm using Mail::queue(), I can't see how to dynamically set that configuration variable.
Is there any way to do what I'm asking? Ideally, I'd like to be able to pass in the domain when I call Mail::queue() (the Mailgun api key is the same for all the domains I want to send from).

I used Macros to add dynamic configuration. I don't remember if this can be done in Laravel 4 but works on 5.
Register macro in service provider (AppServiceProvider)
public function boot()
{
Mail::macro('setConfig', function (string $key, string $domain) {
$transport = $this->getSwiftMailer()->getTransport();
$transport->setKey($key);
$transport->setDomain($domain);
return $this;
});
}
Then I can use like this:
\Mail::setConfig($mailgunKey, $mailgunDomain)->to(...)->send(...)
In your case
\Mail::setConfig($mailgunKey, $mailgunDomain)->to(...)->queue(...)

Switching the configuration details of the Laravel Mailer at runtime is not that hard, however I don't know of any way it can be done using the Mail::queue facade. It can be done by using a combination of Queue::push and Mail::send (which is what Mail::queue does anyway).
The problem with the Mail::queue facade is that the $message parameter passed to the closure, is of type Illuminate\Mail\Message and we need to modify the mailer transport, which is only accessible through the Swift_Mailer instance (and that is readonly within the Message class).
You need to create a class responsible for sending the email, using a Mailgun transport instance that uses the domain you want:
use Illuminate\Mail\Transport\MailgunTransport;
use Illuminate\Support\SerializableClosure;
class SendQueuedMail {
public function fire($job, $params)
{
// Get the needed parameters
list($domain, $view, $data, $callback) = $params;
// Backup your default mailer
$backup = Mail::getSwiftMailer();
// Setup your mailgun transport
$transport = new MailgunTransport(Config::get('services.mailgun.secret'), $domain);
$mailer = new Swift_Mailer($transport);
// Set the new mailer with the domain
Mail::setSwiftMailer($mailer);
// Send your message
Mail::send($view, $data, unserialize($callback)->getClosure());
// Restore the default mailer instance
Mail::setSwiftMailer($backup);
}
}
And now you can queue emails like this:
use Illuminate\Support\SerializableClosure;
...
Queue::push('SendQueuedMail', ['domain.com', 'view', $data, serialize(new SerializableClosure(function ($message)
{
// do your email sending stuff here
}))]);
While it's not using Mail::queue, this alternative is just as compact and easy to read. This code is not tested but should work.

This works in Laravel 5.4:
// Get the existing SwiftMailer
$swiftMailer = Mail::getSwiftMailer();
// Update the domain in the transporter (Mailgun)
$transport = $swiftMailer->getTransport();
$transport->setDomain('YOUR-DOMAIN.HERE');
// Use the updated version
$mailer = Swift_Mailer::newInstance($transport);
Mail::setSwiftMailer($mailer);

My use case was similar to this, in short I just wanted to automatically configure the mailgun sending domain at runtime, by looking at the domain set in the from address field of the message (which I set on the fly before sending using Mail::from(...)->send(...)). This would solve the OP's use case if they are setting the from address in the message to match the mailgun sending domain, which likely should be done.
My solution registers an alternate MailgunTransport which overrides the built in MailgunTransport and sets the domain before sending. This way I only need to register the new driver in my mail.php, and call Mail::send or Mail::queue.
config\mail.php:
'driver' => env('MAIL_DRIVER', 'mailgun-magic-domain')
providers\MailgunMagicDomainProvider:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Mail\Transport\MailgunTransport;
use Swift_Mime_Message;
use Illuminate\Support\Arr;
use GuzzleHttp\Client as HttpClient;
class MailgunMagicDomainProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
$swiftTransport = $this->app['swift.transport'];
$swiftTransport->extend('mailgun-magic-domain', function($app) {
$config = $app['config']->get('services.mailgun', []);
$client = new HttpClient(Arr::add(
Arr::get($config, 'guzzle', []), 'connect_timeout', 60
));
return new MailgunTransportWithDomainFromMessage(
$client,
$config['secret'],
$config['domain'] // <- we have to pass this in to avoid re-writing the whole transport, but we'll be dynamically setting this before each send anyway
);
});
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
}
}
/**
* Overrides the built in Illuminate\Mail\Transport\MailgunTransport but doesnt pull the
* mailgun sending domain from the config, instead it uses the domain in the from address
* to dynamically set the mailgun sending domain
*/
class MailgunTransportWithDomainFromMessage extends MailgunTransport
{
/**
* {#inheritdoc}
*/
public function send(Swift_Mime_Message $message, &$failedRecipients = null)
{
$this->setDomain($this->getDomainFromMessage($message));
return parent::send($message, $failedRecipients);
}
protected function getDomainFromMessage(Swift_Mime_Message $message)
{
$fromArray = $message->getFrom();
if (count($fromArray) !== 1) {
throw new \Exception('Cannot use the mailgun-magic-domain driver when there isn\'t exactly one from address');
}
return explode('#', array_keys($fromArray)[0])[1];
}
}
config/app.php:
'providers' => [
...
\App\Providers\MailgunMagicDomainProvider::class
],

Maybe it's useful to somebody, I solved it as follows;
In a ServiceProvider under the boot function/method;
public function boot()
{
Mail::macro('setConfig', function (string $key, string $domain) {
config()->set('services', array_merge(config('services'), [
'mailgun' => [
'domain' => $domain,
'secret' => $key
]
]));
});
}
Call to queue
Mail::setConfig($key, $domain)->to(...)->queue(...)

Related

Symfony 5: The server couldn't send a response: Ensure that the backend is working properly

I am trying to send a modification through Json to my project in Symfony 5, but I only get error responses, as if there is no Url, I have not inserted any API key or any header, I have searched for a guide but I cannot find it:
UserController.php
<?php
namespace App\Controller;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class UserController
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* #Route("/user/", name="add_user", methods={"POST"})
* #param Request $request
* #return JsonResponse
*/
public function add(Request $request): JsonResponse
{
$data = json_decode($request->getContent(), true);
$firstName = $data['firstName'];
$lastName = $data['lastName'];
$email = $data['email'];
$phoneNumber = $data['phoneNumber'];
if (empty($firstName) || empty($lastName) || empty($email) || empty($phoneNumber)) {
throw new NotFoundHttpException('Expecting mandatory parameters!');
}
$this->userRepository->saveUser($firstName, $lastName, $email, $phoneNumber);
return new JsonResponse(['status' => 'User created!'], Response::HTTP_CREATED);
}
UserRepository.php:
public function saveUser($firstName, $lastName, $email, $phoneNumber)
{
$newUser = new User();
$newUser
->setFirstName($firstName)
->setLastName($lastName)
->setEmail($email)
->setPhoneNumber($phoneNumber);
$this->manager->persist($newUser);
$this->manager->flush();
}
As for the GET, I have done it correctly and I get an answer with the data.
Your path looks wrong. On the controller action you define the route as:
/**
* #Route("/user/", name="add_user", methods={"POST"})
* #param Request $request
* #return JsonResponse
*/
The add_user is just the internal route name. The url in postman should probably be:
https://127.0.0.1:8000/user/
Additionally, since you did not get any response, so not even a 404 not found, I assume you do not have a web server running right now. If you use the Symfony CLI-tool you can call the command: symfony serve in your project directory to get the web server running. Closing the terminal window or restarting your computer will stop the web server.

zfcuser add user role after registration

I'm using Zend Framework 2 with ZfcUser, BjyAuthorize and Doctrine for the database. Registration etc. works very well so far. My problem is, that registered users have no role assigned, so i want to add the role "user" to the user during registration.
I think i could attach this to the "register" event, but i don't know how to do that.
I hope someone can help me ...
(i used this tutorial for setting up zfcuser etc. http://samminds.com/2013/03/zfcuser-bjyauthorize-and-doctrine-working-together/)
public function onBootstrap(MvcEvent $e)
{
$zfcServiceEvents = $e->getApplication()->getServiceManager()->get('zfcuser_user_service')->getEventManager();
$zfcServiceEvents->attach('register', function($e) {
$user = $e->getParam('user');
// probably the role must be added here, with $user->addRole();
// but how do i get the user Role Entity to add from DB?
});
Building on DangelZM's answer, and using another reference (see link at end of my post) about the Event Manager, I came up with this solution which organizes the potential ZfcUser event listeners out into a user listener object.
Note: I created my own user module called NvUser, so depending on the name of your module you'll have to replace all references of NvUser to your user module name.
Summary
I created an NvUserListener object that can itself attach event listeners to the shared event manager, and house the event listener callbacks.
Inside NvUser/Module.php:
<?php
namespace NvUser;
use Zend\Mvc\MvcEvent;
use NvUser\Listener\NvUserListener;
class Module
{
public function onBootstrap(MvcEvent $mvcEvent)
{
$em = $mvcEvent->getApplication()->getEventManager();
$em->attach(new NvUserListener());
}
}
Inside NvUser/src/NvUser/Listener/NvUserListener.php:
<?php
namespace NvUser\Listener;
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\Event;
class NvUserListener extends AbstractListenerAggregate
{
public function attach(EventManagerInterface $events)
{
$sharedManager = $events->getSharedManager();
$this->listeners[] = $sharedManager->attach('ZfcUser\Service\User', 'register', array($this, 'onRegister'));
$this->listeners[] = $sharedManager->attach('ZfcUser\Service\User', 'register.post', array($this, 'onRegisterPost'));
}
public function onRegister(Event $e)
{
$sm = $e->getTarget()->getServiceManager();
$em = $sm->get('doctrine.entitymanager.orm_default');
$user = $e->getParam('user');
$config = $sm->get('config');
$criteria = array('roleId' => $config['zfcuser']['new_user_default_role']);
$defaultUserRole = $em->getRepository('NvUser\Entity\Role')->findOneBy($criteria);
if ($defaultUserRole !== null)
{
$user->addRole($defaultUserRole);
}
}
public function onRegisterPost(Event $e)
{
$user = $e->getParam('user');
$form = $e->getParam('form');
// Do something after user has registered
}
}
Inside NvUser/config/module.config.php:
<?php
namespace NvUser;
return array(
'zfcuser' => array(
'new_user_default_role' => 'user',
),
);
References:
Understanding the Zend Framework 2 Event Manager
Maybe it's not the best solution, but it works for me.
Add user_role_id option in config scope.
public function onBootstrap(MvcEvent $mvcEvent)
{
$zfcServiceEvents = $mvcEvent->getApplication()->getServiceManager()->get('zfcuser_user_service')->getEventManager();
$zfcServiceEvents->attach('register', function($e) use($mvcEvent) {
$user = $e->getParam('user');
$em = $mvcEvent->getApplication()->getServiceManager()->get('doctrine.entitymanager.orm_default');
$config = $mvcEvent->getApplication()->getServiceManager()->get('config');
$defaultUserRole = $em->getRepository('SamUser\Entity\Role')->find($config['user_role_id']);
$user->addRole($defaultUserRole);
});
}
Maybe someone will offer better solution.
This work too.
public function onBootstrap(MvcEvent $mvcEvent)
{
$zfcServiceEvents = $mvcEvent->getApplication()-getServiceManager()->get('zfcuser_user_service')->getEventManager();
$zfcServiceEvents->attach('register', function($e) use($mvcEvent) {
$user = $e->getParam('user');
$em = $mvcEvent->getApplication()->getServiceManager()-get('doctrine.entitymanager.orm_default');
$defaultUserRole = $em->getRepository('SamUser\Entity\Role')-find('id_of_your_role_on_table_role_for_example: '2'');
$user->addRole($defaultUserRole);
});
}
I just used a MySQL trigger
DROP TRIGGER IF EXISTS `user_role_after_insert_trig`;
DELIMITER //
CREATE TRIGGER `user_role_after_insert_trig` AFTER INSERT ON `user`
FOR EACH ROW begin
insert into user_role_linker (user_id,role_id) values (new.user_id, 5);
end
//
DELIMITER ;

ZF2 - Show just one error on forms

I can't seem to get ZF2 to show just one error message for failed form validation messages.
For example, an EmailAddress validator can pass back up to 7 messages and typically shows the following if the user has made a typo:
oli.meffff' is not a valid hostname for the email address
The input appears to be a DNS hostname but cannot match TLD against known list
The input appears to be a local network name but local network names are not allowed
How can I override the error to show something a little more friendly, such as "Please enter a valid email address" instead of specifics like the above?
OK, managed to come up with a solution for this. Instead of using the same string as the error for all validator failures as Sam suggested above, I have overridden the error messages in the InputFilter for the elements and then used a custom form error view helper to show only the first message.
Here is the helper:
<?php
namespace Application\Form\View\Helper;
use Traversable;
use \Zend\Form\ElementInterface;
use \Zend\Form\Exception;
class FormElementSingleErrors extends \Zend\Form\View\Helper\FormElementErrors
{
/**
* Render validation errors for the provided $element
*
* #param ElementInterface $element
* #param array $attributes
* #throws Exception\DomainException
* #return string
*/
public function render(ElementInterface $element, array $attributes = array())
{
$messages = $element->getMessages();
if (empty($messages)) {
return '';
}
if (!is_array($messages) && !$messages instanceof Traversable) {
throw new Exception\DomainException(sprintf(
'%s expects that $element->getMessages() will return an array or Traversable; received "%s"',
__METHOD__,
(is_object($messages) ? get_class($messages) : gettype($messages))
));
}
// We only want a single message
$messages = array(current($messages));
// Prepare attributes for opening tag
$attributes = array_merge($this->attributes, $attributes);
$attributes = $this->createAttributesString($attributes);
if (!empty($attributes)) {
$attributes = ' ' . $attributes;
}
// Flatten message array
$escapeHtml = $this->getEscapeHtmlHelper();
$messagesToPrint = array();
array_walk_recursive($messages, function ($item) use (&$messagesToPrint, $escapeHtml) {
$messagesToPrint[] = $escapeHtml($item);
});
if (empty($messagesToPrint)) {
return '';
}
// Generate markup
$markup = sprintf($this->getMessageOpenFormat(), $attributes);
$markup .= implode($this->getMessageSeparatorString(), $messagesToPrint);
$markup .= $this->getMessageCloseString();
return $markup;
}
}
It's just an extension of FormElementErrors with the render function overridden to include this:
// We only want a single message
$messages = array(current($messages));
I then insert the helper into my application using the solution I posted to my issue here.

Zend redirector not working properly

I have just uploaded my app into a shared hosting environment and it does not seem to be working properly.
I have 2 plugins registered. One checks for session timeout and the other check for session is created after logged in.
the pproblem is that after the second plugin(security.php) kicks in it suppose to redirect the user to the login screen because session has not been created yet. Upon redirection the page displays :The page isn't redirecting properly.
I am not sure what is happenning since everything works fine locally.Below are my two files i mentioned here.
Security.php(here you can see that i have tried couple options, but nothing worked).
class Plugins_security extends Zend_Controller_Plugin_Abstract
{
public function preDispatch (Zend_Controller_Request_Abstract $request)
{
$auth = Zend_Auth::getInstance();
$moduleName = $request->getModuleName();
//$vc = new Zend_Application_Resource_View();
if ($request->getModuleName() != "auth")
{
$auth = Zend_Auth::getInstance();
if (! $auth->hasIdentity())
{
//$redirector = Zend_Controller_Action_HelperBroker::getStaticHelper(
//'redirector');
$flashMessenger = Zend_Controller_Action_HelperBroker::getStaticHelper('FlashMessenger');
$flashMessenger->addMessage(array('message' => 'Sua sessão expirou. Favor logar novamente', 'status' => 'info'));
//$this->_redirect('/auth/login/',array(‘code’ => 301));
$r = Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
$r->gotoSimple("index", "login", "auth");
//header('Location: /auth/login/');
//return;
}
}
}
}
timeout.php
class Plugins_timeout extends Zend_Controller_Plugin_Abstract
{
protected $_auth = null;
protected $_acl = null;
protected $_flashMessenger = null;
protected static $_ZEND_SESSION_NAMESPACE_EXPIRATION_SECONDS= 900;
public function preDispatch(Zend_Controller_Request_Abstract $request)
{
Zend_Session::start();
$moduleName = parent::getRequest()->getModuleName();
if($moduleName !='auth'){
if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > self::$_ZEND_SESSION_NAMESPACE_EXPIRATION_SECONDS)) {
// last request was more than 30 minates ago
session_destroy(); // destroy session data in storage
session_unset(); // unset $_SESSION variable for the runtime
$front = Zend_Controller_Front::getInstance();
$_baseUrl=$front->getBaseUrl();
Zend_Debug::dump(time() - $_SESSION['LAST_ACTIVITY']);
header("Location:$_baseUrl/auth/login/index/timeout/1" );
}else{
$_SESSION['LAST_ACTIVITY']= time();
}
}
}
}
Any help is appreciated. I need to deploy this app ASAP.
thank you.
I think you want:
$r = Zend_Controller_Action_HelperBroker::getStaticHelper('redirector');
$r->gotoSimpleAndExit("index", "login", "auth"); // Note the 'AndExit' suffix.
$r->gotoXXX() just sets the correct header and codes in the $response object, but allows the rest of the dispatch to continue. In contrast, the AndExit part immediately sends the response to the client and exits.
[Not clear why AndExit would not be required in your local environment, though...]

Zend_Auth - Be Able To Login With Both Email And Username

So I am using Zend_Auth to authenticate users of my website. Currently they are only able to log in with their email as login but I would like to enable them to log in also with their username.
Here is some code:
// prepare adapter for Zend_Auth
$adapter = new Zend_Auth_Adapter_DbTable($this->_getDb());
$adapter->setTableName('users');
$adapter->setIdentityColumn('email');
$adapter->setCredentialColumn('password_hash');
$adapter->setCredentialTreatment('CONCAT(SUBSTRING(password_hash, 1, 40), SHA1(CONCAT(SUBSTRING(password_hash, 1, 40), ?)))');
$adapter->setIdentity($request->getParam('email'));
$adapter->setCredential($request->getParam('password'));
Notice the line:
$adapter->setIdentityColumn('email');
How can I add also username there (column in the database called username, too)?
UPDATE:
This is how I solved this:
$login = $request->getParam('email');
$validator = new Zend_Validate_EmailAddress();
if (false === $validator->isValid($login)) {
$u = $this->_getTable('Users')->getSingleWithUsername($login);
if (null === $u) {
throw new Exception ('Invalid login and/or password');
}
$login = $u->email;
}
// prepare adapter for Zend_Auth
$adapter = new Zend_Auth_Adapter_DbTable($this->_getDb());
$adapter->setTableName('users');
$adapter->setIdentityColumn('email');
$adapter->setCredentialColumn('password_hash');
$adapter->setCredentialTreatment('CONCAT(SUBSTRING(password_hash, 1, 40), SHA1(CONCAT(SUBSTRING(password_hash, 1, 40), ?)))');
$adapter->setIdentity($login);
$adapter->setCredential($request->getParam('password'));
I deal with the same thing, and I handle it before Zend_Auth. I use a single user sign-in field and first check whether it's an email address -- if so, it's converted to the appropriate username. Then, let Zend_Auth do its thing.
This works well for me, although you'll need to kinda switch it around, since you're going the other way.
i. Add a filter to your user sign-in field, like this:
$user_field->addFilter('EmailToUsername');
ii. The filter:
<?php
/**
* Converts an email address to the username.
*/
class Prontiso_Filter_EmailToUsername implements Zend_Filter_Interface
{
public function filter( $value )
{
if ( Zend_Validate::is($value, 'EmailAddress') ) {
$user_table = new Users();
$user = $user_table->findByEmail($value);
if ( $user ) {
return $user->username;
}
}
/**
* Nothing happened, so don't filter.
*/
return $value;
}
}
As for just changing a Zend_Auth setting instead, Zend_Auth doesn't like either/or identity columns, so you'd have to write your own auth adapter.
My own solution:
$adapter->setIdentityColumn(stripos($indentity, '#') ? 'email' : 'username');
Fast and simple!