/email-bundle

Symfony Bundle to automatically create and send emails (notification about events on your web-app or newsletters or any other tailored content) to your users.

Primary LanguagePHPMIT LicenseMIT

Azine Email Bundle

Symfony2 Bundle provides an infrastructure for the following functionalities:

  • simplify the rendering and sending of nicely styled html-emails from within your application
  • send notifications/update-infos to your recipients, immediately or aggregated and scheduled.
  • send newsletters to the recipients which wish to recieve it.
  • preview the email-rendering in your browser and sent test mails to you, to view in your favorite email client.
  • view sent emails in a web-view in the browser, for the case the email isn't displayed well in the users email-client.
  • link campaign tracking with your analytics tool (google analytics or piwik or ...)
  • track email opens with your analytics tool (google analytics or piwik or ...)
  • works nicely with transactional email services like mailgun.com.

Table of contents

Quick start guide & examples

Requirements

1. Swift-Mailer with working configuration

Mails are sent using the Swiftmailer with it's configuration.

So the symfony/swiftmailer-bundle must be installed and properly configured.

https://github.com/symfony/SwiftmailerBundle

2. Doctrine for notification spooling

For spooling notifications and for the web-view, Notification-/SentEmail-Objects (=>Azine\EmailBundle\Entity\Notification/Azine\EmailBundle\Entity\SentEmail ) are stored in the database. So far only doctrine-storage is implemented.

3. FOSUserBundle

In its current Version it depends on the FOSUserBundle, as it also "beautyfies" the mails sent from the FOSUserBundle and uses the Users to provide recipient information (name/email/notification interval/newsletter subscription) for the mails to be sent.

Installation

To install AzineEmailBundle with Composer just add the following to your composer.json file:

// composer.json
{
    require: {
        "azine/email-bundle": "dev-master"
    }
}

Then, you can install the new dependencies by running Composer’s update command from the directory where your composer.json file is located:

php composer.phar update

Now, Composer will automatically download all required files, and install them for you. All that is left to do is to update your AppKernel.php file, and register the new bundle. AzineEmailBundle has a dependency on KnpPaginatorBundle, so it`s also nessesary to add it to AppKernel.php file after installing.

<?php

// in AppKernel::registerBundles()
$bundles = array(
    // ...
    new Azine\EmailBundle\AzineEmailBundle(),
    new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),
    // ...
);

Register the routes of the AzineEmailBundle:

# in app/config/routing.yml

azine_email_bundle:
    resource: "@AzineEmailBundle/Resources/config/routing.yml"
    

Configuration options

For the bundle to work with the default-settings, no config-options are required, but the swiftmailer must be configured.

This is the complete list of configuration options with their defaults.

# app/config/config.yml
azine_email:

    # the class of your implementation of the RecipientInterface
    recipient_class:      Acme\SomeBundle\Entity\User # Required

    # the fieldname of the boolean field on the recipient class indicating, that a newsletter should be sent or not
    recipient_newsletter_field:  newsletter

    # the service-id of your implementation of the nofitier service to be used
    notifier_service:     azine_email.example.notifier_service

    # the service-id of your implementation of the template provider service to be used
    template_provider:    azine_email.example.template_provider # Required

    # the service-id of the implementation of the RecipientProviderInterface to be used
    recipient_provider:   azine_email.default.recipient_provider

    # the service-id of the mailer service to be used
    template_twig_swift_mailer:  azine_email.default.template_twig_swift_mailer
    no_reply:             # Required

        # the no-reply email-address
        email:                no-reply@example.com # Required

        # the name to appear with the 'no-reply'-address.
        name:                 'notification daemon' # Required

    # absolute path to the image-folder containing the images used in your templates.
    image_dir:            '%kernel.root_dir%/../vendor/azine/email-bundle/Azine/EmailBundle/Resources/htmlTemplateImages/'

    # list of folders from which images are allowed to be embeded into emails
    allowed_images_folders:  []

    # newsletter configuration
    newsletter:

        # number of days between newsletters
        interval:             '14'

        # time of the day, when newsletters should be sent, 24h-format => e.g. 23:59
        send_time:            '10:00'

    # templates configuration
    templates:

        # wrapper template id (without ending) for the newsletter
        newsletter:           'AzineEmailBundle::newsletterEmailLayout'

        # wrapper template id (without ending) for notifications
        notifications:        'AzineEmailBundle::notificationsEmailLayout'

        # template id (without ending) for notification content items
        content_item:         'AzineEmailBundle:contentItem:message'

    # the parameters for link tracking. see https://support.google.com/analytics/answer/1033867 for more infos.
    tracking_params_campaign_name:    utm_campaign #defaluts to utm_name, piwik and google analytics both understand these parameters
    tracking_params_campaign_term:    utm_term     #defaluts to utm_term, piwik and google analytics both understand these parameters
    tracking_params_campaign_content: utm_content  #defaluts to utm_content, piwik and google analytics both understand these parameters
    tracking_params_campaign_medium:  utm_medium   #defaluts to utm_medium, piwik and google analytics both understand these parameters
    tracking_params_campaign_source:  utm_source   #defaluts to utm_source, piwik and google analytics both understand these parameters

    # See the chapter further below for more information
    email_open_tracking_url:  null

    # Defaults to the AzineEmailOpenTrackingCodeBuilder. Depending on the email_open_tracking_url it will create tracking images for piwik or google analytics. 
    email_open_tracking_code_builder:  azine.email.open.tracking.code.builder.ga.or.piwik

    # number of days that emails should be available in web-view
    web_view_retention:   90

    # the service-id of your implementation of the web view service to be used
    web_view_service:     azine_email.example.web_view_service

Some actions in AzineEmailBundle use iframe (e.g. email dashboard), make sure that X-Frame-Options is not set to DENY

    Header set X-Frame-Options DENY

It should be set to SAMEORIGIN to allow iframes from your domain

    Header set X-Frame-Options SAMEORIGIN

For more information about X-Frame-Options please follow:

https://developer.mozilla.org/docs/Web/HTTP/Headers/X-Frame-Options

Customise the content and subjects of your emails

You must implement your version of the notifier service in which you pull the content of you notification- or newsletter-emails together. In you subclass of AzineNotifierService you can/should implement the following functions:

  • getVarsForNotificationsEmail => variables you use in your twig-templates that are the same for all notification recipients
  • getRecipientVarsForNotificationsEmail => variables you use in twig-templates for notifications that are specific for a recipient
  • getRecipientSpecificNotificationsSubject => the notification-email subject
  • getGeneralVarsForNewsletter => variable you use in twig-templates that are the same for all newsletter recipients
  • getNonRecipientSpecificNewsletterContentItems => content items that are the same for all newsletter recipients
  • getRecipientSpecificNewsletterParams => variables you use in twig-templates that are specific for a recipient
  • getRecipientSpecificNewsletterContentItems => content items for a specific recipient
  • getRecipientSpecificNewsletterSubject => newsletter subject for a specific recipient

See ExampleNotifierService.php for an example.

Customise the layout of your emails

You can/must customize the layout of your email in three ways:

  • define your own styles by writing your own implementation of the TemplateProviderInterface
  • use your own images
  • create your own html and txt twig-templates.

A general overview is given here and the classes you should extend contain more inline-documentation on how stuff works.

Your own implementation of TemplateProviderInterface

This bundle includes a default implementation of a TemplateProvider ( => Services/AzineTemplateProvider) and also an example how to customize things (Services/ExampleTemplateProvider).

Remember that css-styles don't work in most email viewers. You need to embed everything into the attributes (e.g. "style") of your html-elements and do not use div-elements as they are not properly displayed in many viewers. Use Tables instead (<table><tr><td> etc.) to layout your emails.

e.g. <table width="100%"><tr><td height="20" style="font-size: 12px; color: blue;">Bla bla</td></tr></table>

You can define styles you would like to use in your emails and and also blocks of html-code. E.g. a drop-shadow implemented with td-elements with different shades of grey as background color. => see "leftShadow" and "cellSeparator" in the AzineTemplateProvider class.

Your own Images

You can use your own images which will be embeded into the emails. To do this, just define the path to your image-folder in your config.yml. => see above.

Some (web-) mail clients (gmail/thunderbird) will show those embeded images properly without aksing the recipient if he would like to show the attached images.

BUT as far as I can tell, this only works if there is only one email recipient visible to the client and the "from"-address matches the account that the mails have been send from. There are many reasons why the images might not get displayed, so make sure your mails look good without the images too.

Your own Twig-Templates

There are two kinds of templates required for this bundle, and both kinds for html- and for txt-content.

1. the wrapper-templates

These templates contain a header-section (logo etc.), header-content-section ("This is our newsletter blablabla"), main content (see "content-item-templates" below) and footer (links etc.).

They contain stuff that is usually exactly the same (except the greeting) for each of your recipients of this email.

This bundle will render email that consist of a html-part and a plain-text part.

The supplied baseEmailLayout-template in this bundle is split into two files baseEmailLayout.txt.twig and baseEmailLayout.html.twig to make them more easy to extend and manage. The *.txt.twig with the required blocks and the *.html.twig which is included in the body_html-block of the *.txt.twig template.

The "*.txt.twig" is the template that will be called for rendering. It must contain the following blocks:

  • subject
  • body_txt
  • body_html

2. the content item-templates

In a newsletter- or notification-email you probably have different kind of items you would like to include.

For example in an update-email to your users you could mention 6 private messages, 3 events and 2 news-articles.

For those three types of content items you can define different "content-item-templates" and also differenct styles.

An example for "Private-Messages" is included => Resources/views/contentItem/message.txt.twig and Resources/views/contentItem/message.html.twig.

For a type of content-item you must allways provide a html and a txt-version. They will be referenced by their full ID withour the format.twig-ending.

=> "AzineEmailBundle:contentItem:message" for Resources/views/contentItem/message.txt.twig and Resources/views/contentItem/message.html.twig

When rendering those templates you have access to the styles and snippets defined for this template in your TemplateProvider.

Make your emails available in the web-view and web-pre-view

In the "web-pre-view" you can take a look at the content and layout of your email before sending them and you can send test-emails to your own email-address to view it in your favorite email-client.

In the "web-view" users reading you email can take a look at the email in their browser, if their favorite email client messes up the layout. The bundle adds a link at the top of you email to direct your users to the web-view : "If this email isn't displayed properly, see the web-version"

Configuring the web-view and web-pre-view

In order to use the web-view you must:

  1. implement your version of WebViewServiceInterface. To minimize your effort, you can subclass the AzineWebViewService.
  2. configure it as service in your services.yml and
  3. set it in your config.yml as azine_email_web_view_service.

You can define how many days the sent mails shall be kept available by setting the value for azine_email_web_view_retention. The default is 90 days.

The web-view uses the following routes:

// ...EmailBundle/Resources/config/routing.yml
# route for users to see an emails
azine_email_webview:
    pattern:  /email/webview/{token}
    defaults: { _controller: "AzineEmailBundle:AzineEmailTemplate:webView" }
    
# route for images that were embeded in emails and now must be shown in web-view
azine_email_serve_template_image:
    pattern:  /email/image/{folderKey}/{filename}
    defaults: { _controller: "AzineEmailBundle:AzineEmailTemplate:serveImage"}

# index with all the email-templates you configured in you implementation of `WebViewServiceInterface`
azine_email_template_index:
    pattern:  /admin/email/
    defaults: { _controller: "AzineEmailBundle:AzineEmailTemplate:index" }
    
# preview of a template filled with dummy-data ... this should probably only be accessible by admins
azine_email_web_preview:
    pattern:  /admin/email/webpreview/{template}/{format}
    defaults: { _controller: "AzineEmailBundle:AzineEmailTemplate:webPreView", format : null }

# route to send test-mails filled with dummy-data ... this should probably only be accessible by admins
azine_email_send_test_email:
    pattern:  /admin/email/send-test-email-for/{template}/to/{email}
    defaults: { _controller: "AzineEmailBundle:AzineEmailTemplate:sendTestEmail", email: null}

To use web-view you must enable these routes by including the routing file in you config.

// app/config/routing.yml
...
azine_email_bundle:
    resource: "@AzineEmailBundle/Resources/config/routing.yml"
    prefix:   /{_locale}/
    requirements:
        culture:  en|de
...

Implement WebViewServiceInterface

The easiest way for you is to extend the AzineWebViewService and implement the three public functions

  • public function getTemplatesForWebPreView()
  • public function getTestMailAccounts()
  • public function getDummyVarsFor($template, $locale) and maybe the
  • public function __constructor(...) if you need any extra services to gather the dummy-data to populate the web-pre-view.

You can take a look at the ExampleWebViewService what to do in those functions.

Update your database

The web-view stores all sent emails in the database. In order to do so, the entity SentEmail must be available.

It is defined in SentEmail.orm.yml and you can update you database either with the command doctrine:schema:update or via migrations.

Define which mails to store for web-view

You can decide which mails you want to make available in web-view by overriding the function saveWebViewFor($template) in your TemplateProvider.

See ExampleTemplateProvider for hints on how to do this.

Deleting old "SentEmails"

Sent emails that are available in the web-view are stored in the database. As you want to get rid of old emails, there is a Symfony command to handle this for you.

The symfony console-command emails:remove-old-web-view-emails will remove all "SentEmail" that are older than the number of days you defined in azine_email_web_view_retention in your config.yml.

You can configure a cron-job to call this command regularly. See Cron-Job examples below.

Operations

Deleting failed mail-files from spool-folder

Some emails you want to send might fail and if you configured the swiftmailer to use file spooling, the spool-files for these mails will stay in you spool-folder named *.message.sending.

Running the swiftmailer:spool:send command will try to send those files again, but if sending fails repeatedly, you might end up with a spool-folder filled with mail-files that will allways fail.

To delete those files, you can use the emails:clear-and-log-failures command from this package.

Add a cron-job to do this for your application once per day, or if you have a lot of failing messages, once per hour. See Cron-Job examples below.

Using the date parameter for the command, you can define the duration during which the mailer should attempt to send those mails, before they are deleted by this command.

Examples of Cron-Jobs we use in operation.

Here are some examples how to configure your cronjobs to send the emails and cleanup periodically.

# Send a newsletter every friday:
0 	10 	* 	* 	5 	/usr/local/bin/php -c /path/to/folder/with/php.ini-file/to/use /path/to/your/application/app/console emails:sendNewsletter -e prod >>/path/to/your/application/app/logs/cron.log 2>&1 

# Send a newsletter every other friday:
0 	10 	* 	* 	5 	[ `expr \`date +\%s\` / 86400 \% 2` -eq 0 ] && /usr/local/bin/php -c /path/to/folder/with/php.ini-file/to/use /path/to/your/application/app/console emails:sendNewsletter -e prod >>/path/to/your/application/app/logs/cron.log 2>&1 

# Send notifications every minute:
# Beware! If processing this command takes longer than 1 minute (e.g. trying to send a lot of notifications in one run), 
# then duplicate emails will be sent, as another run is started every 60s. This issue is fixed in the master branch, but only available for Symfony 2.6
* 	* 	* 	* 	* 	/usr/local/bin/php -c /path/to/folder/with/php.ini-file/to/use /path/to/your/application/app/console emails:sendNotifications -e prod >>/path/to/your/application/app/logs/cron.log 2>&1 

# Delete old web-view-emails every night:
0 	3 	1 	* 	* 	/usr/local/bin/php -c /path/to/folder/with/php.ini-file/to/use /path/to/your/application/app/console emails:remove-old-web-view-emails -e prod >>/path/to/your/application/app/logs/cron.log 2>&1 

# Try to re-send failed messages every night:
1 	4 	* 	* 	* 	/usr/local/bin/php -c /path/to/folder/with/php.ini-file/to/use /path/to/your/application/app/console emails:clear-and-log-failures -e prod >>/path/to/your/application/app/logs/cron.log 2>&1 

# If you are spooling emails, then call the send command of the swiftmailer every minute:
* 	* 	* 	* 	* 	/usr/local/bin/php -c /path/to/folder/with/php.ini-file/to/use /path/to/your/application/app/console swiftmailer:spool:send --env=prod >>/path/to/your/application/app/logs/cron.log 2>&1 

TWIG-Filters

This bundle also adds some twig filters that are useful when writing emails.

textWrap

The textWrap filter allows you to wrap text using the php function wordwrap. It defaults to a line width of 75 chars.

{{ "This text should be wrapped after 75 characters, as it is too long for just one line. But there is not line break in the text so far" | textWrap }}
or
{{ "This text should be wrapped after 30 characters, as it is too long for just one line. But there is not line break in the text so far" | textWrap(30) }}

urlEncodeText

This filter will url encode text. With url encoded text, you can, for example, create mailto-links that will create an email with the subject and body already pre-filled.

{% set subject = "I love your service. Thanx a lot" | trans | urlEncodeText %}
{% set body = "Hi,\n\nI just wanted to say thank you!\n\nBest regards,\n%username%" | trans({'%username%' : user.name}) | urlEncodeText %}
<a href="mailto:support@azine.com?subject={{ subject }}&body={{ body }}">Mail to us</a>

stripAndConvertTags

When writing html emails, you should always supply a reasonably similar text version of your email.

If you do not have a text version of your html content, you can convert the html-code into something acceptable with this filter.

// some.email.txt.twig
{{ htmlContent | stripAndConvertTags }}

This will:

  • replace all "a" html elements with text built as follows: "link text: url" or just "url", depending on the link text.
  • strip all html tags => see strip_tags
  • replace htmlentites with their original character => see html_entity_decode

addCampaignParamsForTemplate

This filter will get the tracking campaign parameters and values for the given twig-template and merge them with the given campaign parameters. Then all links in the template will be complemented with the campaign parameters that are not yet present.

// used for examlpe in Azine/EmailBundle/Resources/views/baseEmailLayout.html.twig
{% filter addCampaignParamsForTemplate(contentItemTemplate, contentItemParams) %}
    {% include contentItemTemplate ~ '.html.twig' with contentItemParams %}
{% endfilter %}

Use two different mailers for "normal" and for "urgent" emails

In most cases you'll probably prefer UI-performance over speed of email-delivery. But for example for the password-reset- or for email-confirmation-emails you want the user to receive the mail a.s.a.p and not after the next spool-mailing.

To achieve this you must do two things:

  1. configure multiple swiftmailers in you config.yml-files (see example below and http://symfony.com/doc/2.6/reference/configuration/swiftmailer.html#using-multiple-mailers)
  2. define which emails should be delivered immediately in your TemplateProvider.

Here are extracts from config.yml-files:

# app/config/config.yml
# Swiftmailer Configuration
swiftmailer:
    default_mailer: defaultMailer // name of the default mailer defined below.
    mailers:
        defaultMailer: // you can choose your name for the default mailer
            host:           "%mailer_host%"
            username:       "%mailer_user%"
            password:       "%mailer_password%"
            transport:      "%mailer_transport%"
            port:           "%mailer_port%"
            encryption:     "%mailer_encryption%"
            antiflood:
                threshold:  10
                sleep:      2
            logging:        "%kernel.debug%"

        immediateMailer: // this name is hard-coded in the bundle!
            host:           "%mailer_host%"
            username:       "%mailer_user%"
            password:       "%mailer_password%"
            transport:      "%mailer_transport%"
            port:           "%mailer_port%"
            encryption:     "%mailer_encryption%"
            antiflood:
                threshold:  10
                sleep:      2
            logging:        "%kernel.debug%"


// app/config/config_prod.yml
// for most mails (defaultMailer) use spooling to improve ui responsiveness
// you must configure a cron-job to execute the "swiftmailer:spool:send"-command
swiftmailer:
    mailers:
        defaultMailer:
            spool:
                type: file
                path: "%kernel.root_dir%/spool.mails/prod"


// app/config/config_dev.yml
// make sure dev-environment mails are sent immediately to a developer-account and not to real email-addresses
swiftmailer:
    mailers:
        defaultMailer:
            delivery_address: mail-to-dev-account-spool@your.domain.com

        immediateMailer:
            delivery_address: mail-to-dev-account-nospool@your.domain.com


// app/config/config_test.yml
// don't send mails during test-runs, only spool them in a dedicated directory
swiftmailer:
    mailers:
        defaultMailer:
            spool:
                type: file
                path: "%kernel.root_dir%/spool.mails/test"

        immediateMailer:
            spool:
                type: file
                path: "%kernel.root_dir%/spool.mails/test"

And in your implementation of the TemplateProviderInterface, you can define emails from which templates should be sent immediately instead of spooled:

// see ExampleTemplateProvider or AzineTemplateProvider for an example
   protected function getParamArrayFor($template){

     ...
        // send some mails immediately instead of spooled
        if($template == self::VIP_INFO_MAIL_TEMPLATE){
            $newVars[self::SEND_IMMEDIATELY_FLAG] = true;
        }
     ...

Use transactional email services e.g.e mailgun.com

To send and track your emails with a transactional email service
like mailgun.com, postmarkapp.com or madrill.com, you can set the swiftmailer configuration to use their smpt server to send emails.

# app/config/config.yml (or imported from your parameters.yml.dist)
swiftmailer:
    host:           "smtp.mailgun.org"
    username:       "postmaster@acme.com"
    password:       "your-secret-mailgun.com-password"
    transport:      "smtp"
    port:           "587"
    encryption:     "tls"

If you are sending mails with mailgun on a free account and would like to be able to check the logs etc. for more than only two days, also check out the AzineMailgunWebhooksBundle. Mailgun deletes logs etc. from free accounts after aprox. two days. With this bundle you can store the events in your own database and keep them as long as you like.

PS: make sure your application is allowed to connect to the other smpt. This might be blocked on shared hosting accounts. => ask your admin to un-block it.

GoogleAnalytics & Piwik: Customize the tracking values in email links

In your implementation of the TemplateProvider, you can implement the function getCampaignParamsFor($templateId, $params) to define the values for the campaign tracking parameters in the links inside your emails.

You can define the values for each tracking parameter on an email template level and for emails that are composed from multiple nested content items, you can define values for these content item templates as well. And finally inside all the templates you can add campaign parameter values manually to individual links.

In this hierarchy (email template > content item template > individual link) values are not overwritten if they are defined on the more granular level. You can check the resulting links in the WebPreView of the email.

Email-open-tracking with a tracking image (e.g. with piwik or google-analytics)

To be able to track with Piwik or GoogleAnalytics (or the like) if an email has been opened, you can specify an image tracking url in your configuration.

#app/config/config.yml
azine_email:
  # for GoogleAnalytics
  email_open_tracking_url: "https://www.google-analytics.com/collect?v=1&..." 

  # for Piwik
  email_open_tracking_url: "https://your.host.com/piwik-directory/piwik.php?idsite={$IDSITE}&bots=1&rec=1"

If you configure the email_open_tracking_url in your config.yml, then the provided url will be complemented with the tracking parameters and values and a html img tag will be inserted into the html code at the end of your email.

If you wan't to change the way the tracking image url is complemented with the tracking parameters and values, then you can create and use your own implementation of the EmailOpenTrackingCodeBuilderInterface and update your config.yml to use that implementation.

// app/config/config.yml
azine_email:
  # Defaults to the AzineEmailOpenTrackingCodeBuilder. See the README.md file for more information
  email_open_tracking_code_builder:  your.email.open.tracking.code.builder

See these links for more details on email tracking with images:

The tracking-image will also be inserted in the WebPreView of your emails, but to avoid false tracking events, the url will be modified to not point to your email-open-tracking system.

Contribute

Contributions are very welcome. Please fork the repository and issue your pull-request against the master branch.

The PR should:

  • contain a description what the PR solves or adds to the bundle (reference existing issues if applicable)
  • contain clean code with some iniline documentation and phpdocs, no "pure whitespace" changes.
  • respect the Symfony best practices and coding style
  • have phpunit tests covering the new feature or fix
  • result in a 'green' build for your branch on travis-ci.org before you issue the PR

Code style

You can check the code style with the php-cs-fixer. Optionally you can set up a pre-commit hook which contains the php-cs-fixer check. Also see https://github.com/FriendsOfPHP/PHP-CS-Fixer

All you have to do is to move pre-commit.sample file from commit-hooks/ to .git/hooks/ folder and rename it to pre-commit.

php-cs-fixer will check the style of your new added code each time you commit and apply fixes to the commit.

To run php-cs-fixer manually, install dependencies (composer install) and execute php vendor/friendsofphp/php-cs-fixer/php-cs-fixer --diff --dry-run -v fix --config=.php_cs.dist .

Build-Status etc.

Build Status Total Downloads Latest Stable Version Scrutinizer Quality Score Code Coverage