Great start. I can't await giving that a try in a project. Also i you want i can help maintaining.

What i like, it's a lean wrapper around Symfony Email object, with some Drupal-ish functionality on top.

I like the design goals (copied from #1803948-48: [META] Adopt the symfony mailer component):

Off the top of my head, here are some requirements that a replacement system should provide:

Plain text or HTML mail with optional plain text alternative
Automatic conversion between plain text and HTML as required, using modern supported library code, using the existing MarkupInterface to distinguishing markup from plain text
Multiple supported transports
Flexible configuration of sending options, allowing different settings for different mail "channels" (module/key in current terminology)
Attachments, including "inline images", with proper security to control attaching of protected files
Theming of HTML mails, with automatic conversion to inline CSS
Error reporting covering transport failure, temporary and permanent errors
Tracking/blocking of permanently failed addresses
?Bounce handling (or API to support it)

Now let me think aloud here on some topics where i doubt.

Email lifecycle

Currently we have, as i grok it, in Mailer::doSend():
- invoke hook_email_alter(Email $message)
- render $message->content IF it's a render array
- replace tokens
- set subject and body
- set plain text alternative body IF not already set

Some rather gut-feeling style comments on that:

- The Email object currently inherits from Symfony Mail. I'm still sorting out why, but my sentiment is, we should rather decorate Symfony Mail. Or have something like a Builder and Alterer, which maybe is thrown away when its job is done and the final Symfony Email given to the transport.
- The intermediate $message->content feels like an odd child to me
- It's good that caller can set an alternative plain text body, but how that is done is non-intuitive to me
(Also, alter hook implementations must care for that alternative plain text body if set.)
- That $message->content may or may not be a render array feels like a confusing polymorphism to me. All implementations of hook_email_alter have to deal with that.
- I doubt that the token system should be hard-wired in. Note that we hook implementations can not alter token-replaced mail.
- Maybe better: Collect strtr() replacement pairs, and one of the hook_email_alter implementations might add token replacements.
- Or even better: Remove token handling and let the caller handle it.
- Should the alterers be hook invocations? Or should we allow hook invocations to introspect and alter an alterer queue, that is then executed?
- We should have some $message->context property that the alterers can introspect.

(If not everything is self-explaining, please excuse, i'm still sorting. Hope i find time to do some code when the picture is clearer.)

Nits

- Mailer::setBody(): $is_html may be better named sth like "create_html_mail"
- We might want to have email-html.html.twig and email-plain.html.twig separate
- We might want to rename $message->key to sth like $message->emailType

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

geek-merlin created an issue. See original summary.

geek-merlin’s picture

Issue summary: View changes
geek-merlin’s picture

Assigned: Unassigned » geek-merlin

geek-merlin’s picture

Title: Sort out some architectural thoughts » Architectural proposal to discuss
Assigned: geek-merlin » Unassigned
Status: Active » Needs review

OK, boldly dumped all thoughts in code of the MR. Reviews welcome.

geek-merlin’s picture

Note: There are a lot of more commits in here, but they apparently are not listed when pushed before creating MR.

adamps’s picture

Great start. I can't await giving that a try in a project. Also i you want i can help maintaining.

Excellent - you should now have access

geek-merlin’s picture

Thanks! Happy to push this forward together.
And i'm really courious on your review of the ideas in this MR.

adamps’s picture

Thank you very much for the comments and MR. Sorry, I think that you might have been looking at an old version of the code due to the mix-up of #3229699: Fix dangling alpha1 tag. Please can you check and update if necessary? I'll wait for your confirmation on that before I review the MR.

Some specific responses:

The content property is now always a render array, and hopefully it works better now. My idea is to bring key Drupal ideas into mail, including templates/theming, MarkupInterface and render arrays. As we already know, render arrays allow hooks to modify content before it is flattened down to a string, and the array structure is still visible even in twig for example to exclude/alter a particular field/variable.

Yes I understand your doubts about token replacement being in here - I was unsure myself. I found that it is used in 2 places already that I know of (user module and simplenews) so there could be benefit in centralising it. We could let the caller do it, but it's slightly tricky - it should be done in a post_render callback.

I'm not really very familiar with the jargon for programming patterns like "decorate" (maybe because I started programming before they existed😃). Anyway I just read an article, and it seems to me that if we decorate then we would have to reproduce each if the methods of Symfony Mail as a pass-through. That seems a shame as there are lots of them and my idea is to keep the wrapper as lightweight and transparent as possible.

Beg pardon, your comments about alterers are rather baffling to me.

berdir’s picture

> Anyway I just read an article, and it seems to me that if we decorate then we would have to reproduce each if the methods of Symfony Mail as a pass-through. That seems a shame as there are lots of them and my idea is to keep the wrapper as lightweight and transparent as possible.

Yes, that is exactly the problem with decorators if the inner system does not have the concept of decorating built-in, in form of a base class/trait for a decorator. You need to re-implement a lot of code and it will break when a new method is added. You can't use _call() with methods on an interface, they need to exist.

Decorators are useful when it is likely that you want to combine multiple extensions on top of each other, I don't think that's really the case here.

joachim’s picture

Could we implement Symfony's interface but not extend it?

It would be nice if mail messages were config entities and/or plugins (see #1346036: Add an alternative to hook_mail() and deprecate it and https://www.drupal.org/project/mail)

adamps’s picture

@joachim interesting ideas thanks for bringing them into the discussion. I haven't fully grasped your architecture yet so apologies if my answers contain mistakes.

It would be nice if mail messages were config entities and/or plugins

Although it will be quite common for a mail to be based on an entity, it seems to me that it won't always be the case. So perhaps we could create a framework like you describe as an extra library that sits above and calls into the new mailsystem rather than being the entire mailsystem?

In many cases there would need to be a non-BC migration of config to make it work. For example, the user mails are individual keys in user.settings rather than being separate config entities. This seems like an obstacle for introducing an experimental new mailsystem in parallel with keeping the existing one working.

I feel that this module could support your use-cases pretty well. For example see UserMailBuilder for simple code based on config that's not a separate config entity.

ContactMailBuilder is messier and I was wondering about converting to use templates.

Could we implement Symfony's interface but not extend it?

I don't understand how this question relates to the other part of your comment.

adamps’s picture

@geek-merlin many thanks there's definitely some cool stuff in your MR although I don't understand it all. I've continued developing so I'm afraid the MR won't apply cleanly but it should still be easy to take some ideas in.

The language part I already had started adding but yours is much more thorough and complete.

I guess the function in setBody() (tokens, absolute URLs, inline CSS, HTML2Text) is mostly a series of conversions or postRender steps so we could generalise that to ConverterInterface - even more general than your HtmlToTextConverterInterface

You suggested two email templates and I had a similar idea - use the is_html variable in email.html.twig (like swiftmailer). As a module/theme developer when I create a template for a specific module/key I don't want to have to create two very similar files. I wonder if many people use HTML mails nowadays and if not then plain text versions can be made more of a background thing.

It's great that you added an example. In the latest version, there is support for @MailBuilder plug-ins which aren't just examples, they are usable code that can run on a real Drupal site right now.

I think Email::params is much like your idea of contexts - I kept the name to match the one familiar from hook_mail().

I think I tend more towards simplicity compared with you - your MR has new services for example for defaulting whereas I am more used to hooks (we could add one in newMail()). The module currently is pretty small and easy to understand which could be an advantage - it just exposes the symfony mailer with some simple Drupal integration such as themes/templates/render arrays/translation. The @MailBuilder plug-ins I wrote are leaner with less boiler plate than your example which I think is important as much as we can.

It would be great to discuss many details but I'd better not get too sucked in as also I need to balance my life and spend time on other matters😃.

joachim’s picture

> Although it will be quite common for a mail to be based on an entity, it seems to me that it won't always be the case. So perhaps we could create a framework like you describe as an extra library that sits above and calls into the new mailsystem rather than being the entire mailsystem?
> In many cases there would need to be a non-BC migration of config to make it work. For example, the user mails are individual keys in user.settings rather than being separate config entities. This seems like an obstacle for introducing an experimental new mailsystem in parallel with keeping the existing one working.

My thinking was to keep as 2 separate system A) the system that defines the content of emails, allows their customisation, alteration, and translation, and B) the system for sending them.

User mails are currently keys in user.settings, yes. With https://www.drupal.org/project/mail, each of those becomes a config entity. That allows:

- mail subject and body can be translated
- mail subject and body can be changed by site admins
- there's a framework for a UI for allowing site admins to change the text of emails. Any module that defines emails can use this in its own part of the admin UI

Meanwhile, mails which are created on the fly like contact messages, are content entities, and these would implement the same interface as the config entities which are for system mails.

@dawehner pointed out on #1346036: Add an alternative to hook_mail() and deprecate it that it should be a plugin + a config entity. The advantage of a plugin would be that if an admin has not customised a mail, changes from newer versions of a module are automatically present rather than needing to update entities. I need to look more into that.

My thinking for all mails being a plugin/entity is that just about every mail that the system sends needs to be customisable by an admin. Those that do not are things like contact messages, which are already entities.

> Could we implement Symfony's interface but not extend it?

What I mean is that plugins and entities in Drupal inherit from base classes. We can't make a plugin class or an entity class inherit from Symfony's mail class. We could instead make a plugin or an entity class which implements Symfony's mail interface.

adamps’s picture

@joachim, thanks for the further explanation

My thinking was to keep as 2 separate system A) the system that defines the content of emails, allows their customisation, alteration, and translation, and B) the system for sending them.

Interesting idea. However it seems that it would allow code to send emails bypassing the alteration hooks, which is perhaps not desirable. So perhaps yes, if we move the alteration into B?

I still feel that your ideas could come as an extra service that sits on top of the fundamental core, mandatory email interface - they are one of the available options for part A however there are other options available. I accept the advantages you describe, but I don't see that it's something to force people to agree with. Some code will not really be building mails that could relate entities. Some code will have the config that could be moved to entity, but the maintainer won't necessarily wish to make a non-BC config model change immediately - that would make a much bigger barrier to migration to the new mail system.

We could instead make a plugin or an entity class which implements Symfony's mail interface.

Yes except I don't think symfony defines an interface - I only found the class.

adamps’s picture

I pushed a new version which takes a lot from @joachim and @geek-merlin. Comments welcome.

geek-merlin’s picture

@AdamPS #13: Thanks for the flowers!

Some heads-up from me:
- Time-wise i'm heavily sucked into other projects, so i still have lots of motivation for this, but very low bandwidth.
- Code-wise, my proposal has some nice ideas, but i'm far from happy with the approach.
Will look into your new commits sonn-ish.
I have half-finished code that explores a totally different approach, and will work on and publish it as soon as i get to it (sorry, no ETA). Some notes on it:
- everything happens in a builder service that wraps the symfony email object and thus can have a drupal-y interface, and in the end we can send the unwrapped email.
- i wish we can get something like "email components" alongside the ideas of ye olde #2702061: Unify & simplify render & theme system: component-based rendering (enables pattern library, style guides, interface previews, client-side re-rendering)
- i want to allow both convert-html-to-text and have-separate-html-and-text-templates workflows
Stay tuned.

@joachim #14: Interesting! Will think about that.

geek-merlin’s picture

Title: Architectural proposal to discuss » Find the best Drupal-y Symfony Mailer API
adamps’s picture

I have created a documentation page. I will describe the existing architecture there and invite anyone to comment.

@geek-merlin I feel that it would likely be more useful for us to discuss there rather than to start a new codebase for a "totally different approach". I feel that we likely mostly agree on many key concepts such as a configuration system, a mailer service . Then there will be some key topics for debate, such as the class/interface to represent a mail.

adamps’s picture

I have updated the code an architecture documentation. I've addresses all the comments here as best I can, and it feels like we've reached something that can work for a 1.x version. I plan to create a beta release in 2 weeks and then avoid major interface changes until 2.x (which I would expect to be not until there are 100s or 1000s of sites using with some significant feedback).

Please take a look and add any further specific comments.

adamps’s picture

I took a look at the mail module. I feel this module could be a valid successor to it, as it seems to address all the original key goals (plus it brings many substantial new features which should encourage people to make the effort to adopt a new interface).

  1. Improved API for sending mails replacing hook_mail()
  2. Flexible plug-in architecture
  3. Support creating emails based on entities

Comments welcome from @joachim regarding this.

adamps’s picture

Status: Needs review » Fixed

I feel it's time to move on from this issue now - many of the ideas are done, or if not are outdated.

New issues to discuss specific points about the API are very welcome.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.