The Mail service is an Email microservice that sends emails using mustache-based templates. It was built to allow our development teams at 2amigos to avoid having to configure mail over and over on projects involving a microservices infrastructure. It's a combination of two separate applications, one being Symfony's CI application and the other being an API built with Slim3 as it uses a technique called spooling.
The project uses Monolog for logging, Fractal as a serializer, Tactitian as a command bus, gettext for translations, Basic access authentication and Json Web Tokens for authentication (this is optional), and Zend filter for data filtering and validation.
Docker compose and Postman collection
files are included for easy development, even though docker
is not strictly necessary for development as you could easily
use PHP built-in server.
The project tries to follow DDD principles.
Install the latest version using composer.
$ composer create-project --no-interaction --stability=dev 2amigos/mail-service app
If you are using it from a private repository (using a github url here as an example).
$ composer create-project --no-interaction --stability=dev 2amigos/mail-service app --repository-url=https://github.com/2amigos/mail-service
The project uses environment files to configure secrets, for that reason, you must create a file named .env
at the root
directory of the project. An .env.example
file has been provided with all required environment values. Modify that file
and save it as .env
in the root directory.
By default, the API application is configured to work under basic authentication processes. It uses an array of users for
that purpose but you could easily change that behavior by configuring the authenticator
option of the HttpBasicAuthentication middleware
by creating your own or using one provided by the library. Check the PdoAuthenticator.
If authentication is successful, the action will return a Json Web Token to be used for subsequent calls.
Authentication, or the usage of scopes are optional. If you don't wish to work with this kind of setup, simply remove
the middleware configurations of HttpBasicAuthentication
, JwtAuthentication
and ScopeMiddleware
middlewares.
The most important part of the application is its views
, which need to be in the views/txt
and views/html
subdirectories.
Their names already explain what type of templates should be in each one.
It also has multi-language support by using gettext
. An example translation file and template have been provided to help
you understand how it works. We have also added a command to work with the excellent localization management platform
called POEditor. That command will import for you the translations and transform the files to .php
files. To import the required translations to your project from POEditor
use this command:
$ ./bin/console import-translations:run --api-token={POEDITOR_TOKEN} --project={PROJECT_ID} --languages=es,de --delay=3
Where {POEDITOR_TOKEN} and {PROJECT_ID} are your token and project id respectively.
The translations will be automatically imported into the ./i18n/
folder.
In order to work with translations, we have also included a helper class to work with Mustache
that will parse the
content and attempt to get the translated content of text within {{#i18n}}{{/i18n}}
tags. See the example view provided on
this project.
For the sake of the example, go to the public
folder of the app and start the built-in PHP server like this:
php -S localhost:8080
Now we can access the api at http://127.0.0.1:8080
.
To get a token, use the following:
$ curl "https://127.0.0.1:8080/token" \
--request POST \
--include \
--insecure \
--header "Content-Type: application/json" \
--data '["mail.all"]' \
--user test:test
HTTP/1.1 201 Created
Content-Type: application/json
{
"data": {
"token": "XXXXXXXXXX",
"expires": 1550271641
}
}
Using the token
, you can now post a request using application/form-data
to send an email.
curl -X POST \
http://127.0.0.1:8080/mail/send \
-H 'Authorization: Bearer YOUR_TOKEN_HERE' \
-H 'Cache-Control: no-cache' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Postman-Token: 22bf2715-35e4-41ee-a04b-fd8beddcdd62' \
-H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
-F from=noreply@example.com \
-F to=user@example.com \
-F 'subject=Testing micro-services' \
-F template=hello-world \
-F 'data[name]=World' \
-F language=es \
-F 'attachments[]=@/path/to/image/to/attach/41835188_10217308479844850_6484466208170049536_o.jpg'
The above command will create an email message on the spool directory, configured by default at the runtime
folder.
from
: Optional. Who the message is from. If not specified, the mail message will be configured with the environment variable namedMAIL_NO_REPLY_EMAIL
(see.env.example
file).to
: Required. To whom the message was addressed.subject
: Required. This is what the sender set as the topic of the email content.template
: Required. The name of the template. For example, if we set this parameter with the valueregistration
, the system will validate whether a mustache template with the nameregistration.mustache
can be found in either theviews/txt
orviews/html
folders.language
: Optional. If the name is translated, set this parameter with the language code you wish to have the message translated to. For example, if you set it toes
, it will try to load the translations from thei18n/es/messages.php
file. Seesrc\Infrastructure\Mustache\Helpers\TranslatorHelper
for further information on loading translations.
We use Symfony's SwiftMailer bundle to ease the task of sending emails
from the spool as it comes with some handy commands. From the project root type $ ./bin/console
on the terminal and
the following commands will be shown:
Available commands:
about Displays information about the current project
help Displays help for a command
list Lists commands
assets
assets:install Installs bundles web assets under a public directory
cache
cache:clear Clears the cache
cache:pool:clear Clears cache pools
cache:pool:delete Deletes an item from a cache pool
cache:pool:prune Prunes cache pools
cache:warmup Warms up an empty cache
config
config:dump-reference Dumps the default configuration for an extension
debug
debug:autowiring Lists classes/interfaces you can use for autowiring
debug:config Dumps the current configuration for an extension
debug:container Displays current services for an application
debug:event-dispatcher Displays configured listeners for an application
debug:router Displays current routes for an application
debug:swiftmailer Displays current mailers for an application
enqueue
enqueue:consume [enq:c] A client's worker that processes messages. By default it connects to default queue. It select an appropriate message processor based on a message headers
enqueue:produce Sends an event to the topic
enqueue:routes [debug:enqueue:routes] A command lists all registered routes.
enqueue:setup-broker [enq:sb] Setup broker. Configure the broker, creates queues, topics and so on.
enqueue:transport:consume A worker that consumes message from a broker. To use this broker you have to explicitly set a queue to consume from and a message processor service
import-translations
import-translations:run Import POEditor translations command
lint
lint:yaml Lints a file and outputs encountered errors
router
router:match Helps debug routes by simulating a path info match
swiftmailer
swiftmailer:email:send Send simple email message
swiftmailer:spool:send Sends emails from the spool
To send emails from the spool, simply configure a cron job on your server to run the following command at whatever time interval you think is best for your application:
$ ./bin/console swiftmailer:spool:send --message-limit=10
Now, we have to say that whilst spooling
is a great feature from SwiftMailer
, this technique is not suitable for
applications that require sending a high volume of emails. In that case, we highly recommend the usage of a good queue
library such as php-enqueue with the broker that best suits your knowledge and requirements.
We recommend RabbitMQ
as it comes with a manager interface where you will be able to see things such as how many emails
are being sent, how many have failed, and so on, with the ease of adding/removing as many workers as you need under a possible heavy load.
It is also open source
.
We have provided a working example using php-enqueue/enqueue-bundle which comes with a set of very handy commands so you don't need to replicate that work, in conjunction with its Filesystem transport. The following sections explain how to work with that queue system provided.
First what we need to do is to modify the commandBus
locator map and use the mail.send.queue.handler
instead of
mail.send.spool.handler
:
$map = [
CreateTokenCommand::class => 'token.create.handler',
SendMessageCommand::class => 'mail.send.queue.handler', // must be set like this
];
And that's it. Using the same previous call, this time the message will be sent to the configured queue on the runtime folder.
As we said previously, the php-enqueue/enqueue-bundle comes with a set of pretty handy commands. For the full reference of those commands, please go to its documentation.
The one to consume all the messages that go to the queue is enqueue:consume
:
$ ./bin/console enqueue:consume mail --no-interaction -vvv --receive-timeout=60000
The library also comes with a custom spool so you can use php-enqueue with Swiftmailer.
In order to use it you will have to make some changes on the code:
We need to use the already configured SendMesssageSpoolHandler
to use our custom EnqueueSpool
component on the
./config/api/dependencies.php
file:
$container['mail.send.spool.handler'] = function ($container) {
return new SendMessageSpoolHandler(
$container['enqueue.mailer'], // <--- here is the modification
$container['mustache'],
$container['mustache.i18n.helper'],
$container['fs']
);
};
Then on ./config/console/services.yaml
, we should refactor the file and make it look like this:
swiftmailer.mailer.spool_mailer.spool.custom:
class: App\Infrastructure\SwiftMailer\EnqueueSpool
arguments:
$context: @enqueue.fs.context
$queue: 'enqueue.app.mail'
# class: App\Infrastructure\SwitftMailer\FileSpool # commented setting!
# arguments:
# $path: '%kernel.project_dir%/runtime/spool/default'
enqueue.mail.processor:
class: App\Application\Console\Processor\SendMailProcessor
public: true
arguments:
$mailer: '@swiftmailer.mailer.enqueue_mailer'
$mustache: '@mustache.engine.mail'
$translatorHelper: '@mustache.i18n.helper'
tags:
- { name: 'enqueue.processor', command: '__command__', processorName: 'mail' }
enqueue.fs.context:
class: Enqueue\Fs\FsContext
arguments:
$storeDir: '%kernel.project_dir%/runtime/queue'
$preFetchCount: 1
$chmod: 600
$pollingInterval: 100
That's it, the way to use it simply follow the guidelines of sending an email from/to the spool
above.
To contribute, please read our CONTRIBUTION guidelines.
- Tuupola slim api skeleton Thanks for the boilerplate inspiration!
- 2amigos
- All Contributors
The BSD License (BSD). Please see License File for more information.
Beyond Software
www.2amigos.us