Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEW Move to SwiftMailer powered Emails #6466

Merged
merged 8 commits into from
Jan 13, 2017

Conversation

dhensby
Copy link
Contributor

@dhensby dhensby commented Jan 9, 2017

This PR rewrites our Mailer and Email classes to be powered by SwiftMailer.

Email now acts as a proxy for Swift_Message and adds our framework's rendering over the top.

Mailer converts an Email object into a Swift_Message and sends it using Swift_Mailer using the Swift_MailTransport by default (php's mail function) just to keep things the same as they are now.

todo:

  • Docs

}

$message->render();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do devs have to call this manually now? Shouldn’t this just be called by send()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

render is the replacement of populateTemplate which had to be called to "render" the email in v3.

So yes, devs do have to call this themselves if they don't use the callback method for creating emails, but this is not a change in behaviour IMO.

The problem with send calling render is what if a dev has manually set the body of the email? it'll be overridden and it'd be impossible to send a manually constructed email.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting - we could check if the body is populated or not, though I do like the certainty of basically saying "you have to render your email" over the ambiguity of it...

$swiftMessage->setDate(DBDatetime::now()->Format('U'));
if ($defaultFrom = $this->config()->admin_email) {
$swiftMessage->setFrom($defaultFrom);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won’t this mean that if you use this method, you’re forced to use admin_email (if set)? i.e. if I set a default from address, how can I override it on a per-email basis? I think this needs to check if a from address has already been set

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point. I'll update this

use SilverStripe\Core\Object;
use Swift_Mailer;
use Swift_MailTransport;
Copy link
Member

@kinglozzer kinglozzer Jan 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There’s a lot of Swift-specific code in this class, which shouldn’t really be inherited by modules that provide other mailers. Would it be better to make Mailer an interface?

interface Mailer
{
    public function send($message);
}

class SwiftMailer implements Mailer
{
    public function send($message)
    {
        $swiftMessage = $message->getSwiftMessage();
        return $this->sendSwift($swiftMessage);
    }

    // ... other methods
}

// Email.php
Injector::inst()->get('SilverStripe\Email\Mailer')->send($this);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I've taken a bit of a look at how Laravel does it and it's quite closely coupled - mostly because devs can write their own transports (which is basically the bulk of what they'd probably need to do).

There are a lot of out of the box transports, too, which would likely cover most of what people need.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second @kinglozzer's suggestion, however I don't think it's critical to fully-decouple from swiftmailer for the sake of the initial implementation. It's not a merge blocker. :)

@kinglozzer
Copy link
Member

kinglozzer commented Jan 9, 2017

So I’ve just had a look at SwiftMailer in a bit more detail (I’d never used it before), and I’m guessing the idea here is that you’d write or use a transporter for Swift rather than your own Mailer implementation? How easy is that to do?


Edit: doesn’t look hard, most of the big names already exist:

@dhensby
Copy link
Contributor Author

dhensby commented Jan 9, 2017

I’m guessing the idea here is that you’d write or use a transporter for Swift rather than your own Mailer implementation?

Yep - pretty much - SwiftMailer is pretty popular and there are transports for all the major mail services.

@dhensby
Copy link
Contributor Author

dhensby commented Jan 9, 2017

I've addressed the 2 issues and pushed up

@dhensby dhensby force-pushed the pulls/4/mailer branch 6 times, most recently from 04e7ca0 to 7d56db1 Compare January 10, 2017 13:51
like to use a more robust transport to send mail you can swap out the transport used by the `Mailer` via config:

```yml
Mailer:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs the full namespace: SilverStripe\Control\Email\Mailer:

if ($body) {
$result .= "\n$body";
if (!$this->swift) {
$this->setSwiftMailer(new \Swift_Mailer($this->getTransport()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could inject this with dependencies, instead of via PHP.

Injector:
  Swift_Transport: Swift_MailTransport
  Swift_Mailer:
    constructor:
      - '%$Swift_Transport'
  SilverStripe\Control\Email\Mailer:
    properties:
      SwiftMailer: '%$Swift_Mailer'

How about this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. This approach would be in keeping with what we've done for assets and logging.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition the plugins could be provided to Swift_Mailer with the Injector's 'calls' option.

I think that SilverStripe\Control\Email\Mailer could be an interface, with the current impelmentation called SilverStripe\Control\Email\SwiftMaileror something.

The interface would probably be very simple:

namespace SilverStripe\Control\Email;
interface Mailer {
  public function send(Email $message);
  public function getFailedRecipients();
}  

That being the case, rather than Mailer::get_inst() we'd probably want to use Injector::inst()->get(Mailer::class).

Alternatively, we could do away with Mailer altogether and use Injector to provide a Swift_Mailer class? We can use the yaml config to add the transport and plugins.

For test mailing we could make a test transport instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could do away with Mailer altogether and use Injector to provide a Swift_Mailer class? We can use the yaml config to add the transport and plugins.

The problem with this is we can't send Email objects directly to the Mailer for sending as we can now.

We'd basically just be implementing raw SwiftMailer without any convenience methods.

use SilverStripe\Core\Object;
use Swift_Mailer;
use Swift_MailTransport;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I second @kinglozzer's suggestion, however I don't think it's critical to fully-decouple from swiftmailer for the sake of the initial implementation. It's not a merge blocker. :)

Copy link
Contributor

@tractorcow tractorcow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial review completed. :) Good so far overall.

* @param string $fullBody Prepared message
* @param array $headers Prepared headers
* @return mixed Return false if failure, or list of arguments if success
* @return mixed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use @return static or @return Mailer for correct type hinting. :)

*/
protected $ss_template = 'GenericEmail';
private $template = "SilverStripe\\Email\\Email";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please use the full qualified path for the template? The convention is that it should match the same class name.

I.e.

private static $template = 'SilverStripe\Control\Email\Email, or just private static $template = self::class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, private $template not private static $template in this case. :)


// Rewrite relative URLs
$this->body = HTTP::absoluteURLs($fullBody);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very easy to miss, but we need to ensure we still do this somewhere. Perhaps convert absoluteURLs() in Email::render() ? Relative links won't work in email content once sent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

phew - good spot. added

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I've moved it to setBody so we always do this on the body regardless of render. However, maybe the Mailer or even SwiftPlugin should do it to absolutely make sure nothing can get through?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep good job.


// Rewrite relative URLs
$this->body = HTTP::absoluteURLs($fullBody);
public function addAttachmentFromData($data, $name, $mime = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PHPDoc missing. :)

}

/**
* @return string|null
* @return array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be @return string

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it returns an array see my test testGetSetSender

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I look at SimpleMessage::getSender(), it says @return string, so something is wrong here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess SimpleMessage::getSender() is wrong?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so!

* and it won't be plain email :)
*
* @param bool $isPlain
* @param string $path Path to file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please phpdoc $alias and $mime arguments too.

if ($this->bcc) {
$headers['Bcc'] = $this->bcc;
}
/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs a good description. Also please document the method signature for the $callback

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thoughts, see my below comment.

* @param callable|null $callback
* @return static
*/
public static function create_from_callback($template, $data, $callback = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why this is valuable; All it does is wrap Email::create()->setArg()->setArg()? All of these setters return $this, so there's nothing preventing them from being chained. I think we should just drop this method altogether.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm - originally it was a nice convenience method which would call render for you (it had been mandatory to call render by hand otherwise) but after @kinglozzer's suggestion of checking if the body is empty, this may be less desirable...

/**
* Send the message to the recipients
*
* @return array|bool true if successful or array of failed recipients
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be more concise to have a simple boolean response, and document that the user can call $message->getFailedRecipients() to get the list of failed recipients.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is some recipients could be successful and some rejected which means that a true/false return value is misleading. If one recipient is rejected then we should return false, but that doesn't mean a complete failure and so more debugging is needed.

I suppose that just means better documentation is required...

// Send prepared message
return $this->sendPreparedMessage($to, $from, $subject, $attachedFiles, $customHeaders, $fullBody, $headers);
}
private $failedRecipients = array();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that the service should be storing data related to specific invocations of Mailer::sendSwift(); I suggest to pass this back to Email::send() and have it stored on the email instance instead. Otherwise it dilutes the role of Mailer as a service.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - good point.

Copy link
Member

@sminnee sminnee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking really good, but I don't think we need the Mailer class. Let's just get Injector to build us a Swift_Mailer object and have Email.php request that service from Injector.

@@ -953,17 +954,16 @@ public function onBeforeWrite()

// We don't send emails out on dev/tests sites to prevent accidentally spamming users.
// However, if TestMailer is in use this isn't a risk.
if ((Director::isLive() || Email::mailer() instanceof TestMailer)
if ((Director::isLive() || Mailer::get_inst() instanceof TestMailer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to rely on Injector for these global services?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to get rid of these references completely. Are tests not run under "live" mode?

@@ -15,513 +13,124 @@ class Mailer extends Object
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May as well remove 'extends Object' while we're tidying up this class.

if ($body) {
$result .= "\n$body";
if (!$this->swift) {
$this->setSwiftMailer(new \Swift_Mailer($this->getTransport()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. This approach would be in keeping with what we've done for assets and logging.


or in PHP

```php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't encourage the use of PHP-based configuration I think.

$subject .= " [from $from]";
}
$from = $sendAllfrom;
$numFailed = Mailer::send_message($this);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would change this to Injector::inst()->get(Mailer::class)->send($this) if Mailer is to become an interface.

$from = $sendAllfrom;
$numFailed = Mailer::send_message($this);
if ($numFailed) {
return Mailer::get_inst()->getFailedRecipients();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would change this to Injector::inst()->get(Mailer::class)->getFailedRecipients() if Mailer is to become an interface.

if ($body) {
$result .= "\n$body";
if (!$this->swift) {
$this->setSwiftMailer(new \Swift_Mailer($this->getTransport()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition the plugins could be provided to Swift_Mailer with the Injector's 'calls' option.

I think that SilverStripe\Control\Email\Mailer could be an interface, with the current impelmentation called SilverStripe\Control\Email\SwiftMaileror something.

The interface would probably be very simple:

namespace SilverStripe\Control\Email;
interface Mailer {
  public function send(Email $message);
  public function getFailedRecipients();
}  

That being the case, rather than Mailer::get_inst() we'd probably want to use Injector::inst()->get(Mailer::class).

Alternatively, we could do away with Mailer altogether and use Injector to provide a Swift_Mailer class? We can use the yaml config to add the transport and plugins.

For test mailing we could make a test transport instead?

@dhensby dhensby force-pushed the pulls/4/mailer branch 2 times, most recently from 7fe018f to 8b51bb7 Compare January 11, 2017 14:07
@dhensby
Copy link
Contributor Author

dhensby commented Jan 11, 2017

I've implemented pretty much all the feedback.

@sminnee In addition the plugins could be provided to Swift_Mailer with the Injector's 'calls' option.

How could we do this and allow devs to specify many plugins that should be loaded?

This could work for our case, but doesn't provide a nice way for devs to add extra plugins:

SilverStripe\Core\Injector\Injector:
  Swift_Plugin: SilverStripe\Control\Email\SwiftPlugin
Swift_Mailer:
  calls:
    SwiftPlugin:
      - registerPlugin
      - [ '%$Swift_Plugin' ]

todo:

  • More docs highlighting how to do lots of this stuff with Injector

@dhensby dhensby force-pushed the pulls/4/mailer branch 4 times, most recently from f01662c to bab27ba Compare January 11, 2017 14:48
@dhensby
Copy link
Contributor Author

dhensby commented Jan 11, 2017

Looks like https://github.com/silverstripe/silverstripe-behat-extension's TestMailer needs re-writing

Copy link
Member

@sminnee sminnee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK - I noted a couple of method on SwiftMailer that could be removed / protected, and suggested a different yml style recommendation for docs about changing the transport.

In addition, I think we need a mention of this on the upgrade guide. We should also note that $Body in GenericEmail has been renamed to $EmailContent.

/**
* @return static
*/
public static function get_inst()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this.

* @param Email $message
* @return int
*/
public static function send_message($message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this.

* @param Swift_Message $message
* @return int
*/
public function sendSwift($message, &$failedRecipients = null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be tidier if this was protected because it's not part of the interface-provided API. If people start calling it directly it becomes harder for another implementation of the Mailer interface to be dropped in as a replacement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point


```yml
SilverStripe\Control\Email\Mailer:
swift_transport: Swift_SendmailTransport
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to recommend that developers override the Swift_Transport service instead? Otherwise the resulting service definition list will include a service called "Swift_Transport"' that's not actually the swift transport being used...

Swift_Transport:
  class: Swift_SendmailTransport

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'll amend

@sminnee
Copy link
Member

sminnee commented Jan 11, 2017

@dhensby if you can rebase to the latest master and fix up src/Dev/TestMailer.php, behat should Just Work(tm)

@dhensby dhensby force-pushed the pulls/4/mailer branch 3 times, most recently from a12b5ba to d5127af Compare January 12, 2017 00:16
constructor:
- '%$Swift_Transport'
calls:
SwfitPlugin:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: SwiftPlugin

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo still here in your last push :)

'mimetype' => $mimeType,
);
$swiftMessage->setDate(DBDatetime::now()->Format('U'));
if (!$swiftMessage->getFrom() && $defaultFrom = $this->config()->admin_email) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Operator precedence; Should the assignment = be surrounded with braces, since it's lower than the &&?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the assignment is the last condition it won't be messed up an assign a boolean value, however I'll wrap it in parentheses for clarity.

}

/**
* @return string|null
* @return array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I look at SimpleMessage::getSender(), it says @return string, so something is wrong here.

}

/**
* @return string|null
* @return array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess SimpleMessage::getSender() is wrong?

* Mailer objects are responsible for actually sending emails.
* The default Mailer class will use PHP's mail() function.
*/
class SwiftMailer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You forgot implements Mailer interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks


return $emailAddress;
}
public function getFailedRecipients();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this going to be moved to Email? SwiftMailer doesn't implement this, so we can just remove this I guess.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep - removed

* @param Swift_Message $message
* @return int
*/
public function sendSwift($message, &$failedRecipients = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing PHPDoc.

public function setSwiftMailer($swift)
{
// register any required plugins
foreach ($this->config()->get('swift_plugins') as $plugin) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be a simple setter. Use YML injection to set these plugins on the injected $swift object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question is how does that let site developers add 1 more plug-in in addition to what's currently existing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that's good enough I guess.

$this->getSwiftMessage()->setContentType('text/plain');
if (!$this->getBody()) {
$this->render();
$this->setBody(Convert::xml2raw($this->getBody()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code looks super risky, especially if you are sending an email multiple times, you are at risk of double-encoding.

Why not have separate messages for plain / html (with separate setBody / setPlainBody) and allow them to be set independently? You can always convert between the two if attempting to send one which is blank. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use cases:

  • Send a HTML message with a html template, I want the system to automatically pick a plain text version for me (as current implementation)
  • Send a HTML message with a html template, and separate plain text string provided for plain text version
  • Sending a plain text message only with a setPlainBody().
  • Sending a html and plain message, I will provide setBody() / setPlainBody() for each.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed. Perhaps there should be a getPlainBody() / setPlainBody() pair, and this transformation is called is PlainBody isn't set explicitly?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively getPlainTextSwiftMessage() could be a second getter that transforms the email prior to passing it to the mailers. This, however, would mean that the mailer would need to accept a swift message object rather than our email. I don't have a problem with that, but it's another tweak to the interface.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the Convert::xml2plain is inside the if (!$this->getBody()) block then there's no risk of double encoding because we render and then take a copy we don't repeatedly set the plain part.

Adding the plain part automatically is all round risky. I don't like that the send method does anything other than send the email (it tries to guess if it should be rendered and then attempts to render a plain part too).

In Laravel you have to set the plain part explicitly and can even define a template for the plain version - perhaps we should do something similar.

We could have something like.

$email = Email::create()
    ->setHTMLTemplate('MyTemplate')
    ->setPlainTemplate('MyTemplate_plain')
    ->send(); //still performs a render

Or if there's no plain template we create a generatePlainPart method that does what we have at the moment.

I think the biggest challenge is with reusing Email objects because I'm not so sure how easy it is to detach the previously generated plain part... I'll look into it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have code in HTMLText that generates plain text from HTML if you want to re-use that. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
     * Get plain-text version
     *
     * @return string
     */
    public function Plain()
    {
        // Preserve line breaks
        $text = preg_replace('/\<br(\s*)?\/?\>/i', "\n", $this->RAW());

        // Convert paragraph breaks to multi-lines
        $text = preg_replace('/\<\/p\>/i', "\n\n", $text);

        // Strip out HTML tags
        $text = strip_tags($text);

        // Implode >3 consecutive linebreaks into 2
        $text = preg_replace('~(\R){2,}~', "\n\n", $text);

        // Decode HTML entities back to plain text
        return trim(Convert::xml2raw($text));
    }

@dhensby dhensby force-pushed the pulls/4/mailer branch 3 times, most recently from 91f7172 to b1482fb Compare January 12, 2017 13:47
constructor:
- '%$Swift_Transport'
calls:
SwfitPlugin:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo still here in your last push :)

Copy link
Member

@sminnee sminnee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TestMailer::saveEmail() API change isn't a blocker but it will require another patch to behat-extension.

]);
public function send($message)
{
$this->saveEmail($message);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you have to change the API for saveEmail? Now the behat test mailer is broken again :-(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:(

An email isn't really suitably represented by the old parameters and I thought best that we just store the email object instead...

This test suite is passing - where are the failures coming from?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$state->emails[] = array_filter($data); in TestMailer::saveEmail() in behat extension. array_filter will fail on an Email instance.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestSession state needs to be serialized, which is the main reason these were originally array format. I think maybe we should revert this back?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whatever you thinks best :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if they rely on some of the specific data (like Type) then behat tests will need changing because that's not really a thing anymore.... or we'll need to have a "best guess" at type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of my guesses are the best.

@tractorcow
Copy link
Contributor

@sminnee I've pushed a change to revert api changes to TestMailer.

@tractorcow
Copy link
Contributor

Happy to merge once CI passes.

@sminnee
Copy link
Member

sminnee commented Jan 13, 2017

OK I'm happy with this. Say the word and I'll squash & merge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants