PSHTML icon indicating copy to clipboard operation
PSHTML copied to clipboard

Consider adding support to send HTML rendered code embeded in Email

Open Stephanevg opened this issue 7 years ago • 10 comments

Rendering HTML to be send directly in an Email can be a bit different, as some of the styles seem to work differently. This issue is to identify

  1. in which condition are the email output different (Depending on which underlying technology we use to send the email? strucutre of the code, unsuported tags or CSS styles?
  2. How can we ensure that the output in a Email, will be identical to one generated locally.

The solution (might / could) be as simple as just create a specific CSS style that will apply 'only' to HTML code that is intended to be used in an Email.

This excludes adding charts (Is tracked here

Stephanevg avatar Mar 27 '19 07:03 Stephanevg

Have a look at: How to Code HTML Email Newsletters for some of the issues around HTML emails. Having generated these myself from scratch in scripts, the rule seems to be 'simple is best'. Not sure if that helps very much.

wightsci avatar Jul 11 '19 23:07 wightsci

Ho great, that is exactly something I have been looking for actually. thanks @wightsci

In the end, there a few things that report generator won't be be able / allowed to do, because it release on additional framework / external files which we cannot attach to our email

  • Point to any CDN (Bootstrap/ Jquery / ChartJs)
  • Have Use BootStrap class
  • Have charts added via PSHTML (At least up until this one is done -> https://github.com/Stephanevg/PSHTML/issues/220)

We ce can/must do for sure is the following:

  • Use inline CSS
  • Use Tables to structure the Email content (yes.. apprently that is how it works)

Design

Thinking out loud

On the design part of things I am unsure what the best approach would be.

As we don't want to have any dependencies on any of the PSHTML Assets (Bootstrap / Jquery / ChartJS) we need to be sure that the HTML strucutre we are shipping doesn't contains:

  1. A CDN / Script / Link reference to any of the above mentionned framework.
  2. No Bootstrap class are assigned to any of the HTML tags (We could potentially override any bootstrap styling that the user add using.)

How could this look like

I want to have an open discussion on how this could look like. I see several possible scenarios which I would like to discuss.

  1. We write a new function called ConvertTo-PSHTMLEmailContent (or something around these lines) where there would be a parameter -HtmlDocument which would get an HTML document generated by PHSTML. there, we will parse the HTMLStructure, and search for the things that are not supported in email (such as references to CDN / frameworks) and remove any bootstrap class references (This step might not be needed). The function would also add the necessary inline CSS styling. (this could be done by adding a new CSS file, which can contain our desired classes, and we simply add it on top of the HTML document using a Get-Content -Raw

  2. We add switches on all of the html tags, like ForEmail which will add the correct CSS styling. (writing this one seemed already cumbersome / difficult to implement).

  3. Document what is allowed and not in an About_PSHTMLEmail.md file.

for the moment, I cannot think of other solutions, but please share your ideas, thoughts on how this could:

  1. have the best end user experience (Easy and straightforward to use)
  2. The easiest for us to implement.

Looking forward to your feedback ideas critics and design proposals.

Stephanevg avatar Jul 12 '19 11:07 Stephanevg

As I said in my comment – I’ve had to deal with this situation before, both for messages I’ve generated myself and trying to help people who were creating messages by hand. It’s not fun. I think that the success of using PSHTML for email depends to a great extent on the email client of the recipient, which is out of our hands.

Just to try to understand the scope of what you are trying to achieve, what is ‘report generator’ ?

Regards,

Stuart.

wightsci avatar Jul 12 '19 11:07 wightsci

https://evotec.xyz/sending-html-emails-with-powershell-and-zero-html-knowledge-required/?fbclid=IwAR3jvGOMoU5gnTDWJc8ut4NOk0f-8chq3W9eH0NfoMZl5dNoteqJ7E285Mo#utm_source=rss&utm_medium=rss&utm_campaign=sending-html-emails-with-powershell-and-zero-html-knowledge-required

For information

LaurentLienhard avatar Jul 13 '19 09:07 LaurentLienhard

Good idea ! If I’m not wrong the rendering of the email depends of the email client, I mean outlook do not render mail like Thunderbird, and even in outlook, it depend of the version.. you have to pay attention especially about images.. there is different way to add them to an email. And some way are working on one client, others no.. I’ll look at what I’m doing actually for outlook, I don’t remember (did it last year..)

christophekumor avatar Jul 13 '19 18:07 christophekumor

@christophekumor have a look at the link from @wightsci above. Everything is described in there already apperently

Stephanevg avatar Jul 13 '19 18:07 Stephanevg

Ok, i just wanted to share my own personal experience about generating email with powershell. Sorry

christophekumor avatar Jul 14 '19 08:07 christophekumor

hi, I took the bar chart example, and added a javascript function to transformt the chart into a base64 source. It replace the chart canvas into an img tag with the base64 render of the the chart. Then it delete all the script tags

<html ><head ><title >Bar Chart</title></head><Body ><h1 >PSHTML Graph</h1><div ><p >This is a bar graph</p>
    <canvas Width="400px" Id="barcanvas" Height="400px"  ></canvas></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js" type="text/javascript"  ></script>
<script id='chartscript'>
    var ctx = document.getElementById("barcanvas").getContext('2d');
    var myChart = new Chart(ctx, {
        "type":"bar",
        "data":{
            "labels":["January","February","Mars","April","Mai","June","July","August","September","October","November","december"],
            "datasets":[{
                "borderWidth":1,
                "xAxisID":null,
                "yAxisID":null,
                "backgroundColor":"rgb(30,144,255)",
                "borderColor":null,
                "borderSkipped":null,
                "hoverBackgroundColor":null,
                "hoverBorderColor":null,
                "hoverBorderWidth":0,
                "data":[4,1,6,12,17,25,18,17,22,30,35,44],
                "label":"2018"
            }]
        },
            "options":{
                "barPercentage":1,
                "categoryPercentage":1,
                "responsive":false,
                "barThickness":null,
                "maxBarThickness":0,
                "offsetGridLines":true,
                "scales":{
                    "yAxes":[{"ticks":{"beginAtZero":true}}],"xAxes":[""]
                },
                "title":{
                    "display":true,
                    "text":"Bar Chart Example"
                },
                "animation": {
                    "onComplete" : RemoveCanvasAndCreateBase64Image
                }
            }
        } );

        function RemoveCanvasAndCreateBase64Image (){
            var base64 = this.toBase64Image();
            var element = document.getElementById('barcanvas');
            var parent = element.parentNode;
            var img = document.createElement('img');
            img.src = base64;
            parent.appendChild(img);
            parent.removeChild(element);
			var scripttags = document.getElementsByTagName('script');
			var scripttags = document.getElementsByTagName('script');
			for (i=0;i<scripttags.length;){
				var parent = scripttags[i].parentNode;
				parent.removeChild(scripttags[i]);
			}
        };
        </script></Body></html>

important part: this is part of chartjs. It will call the RemoveCanvasAndCreateBase64Image js function when the chart animation is completed. We can not do this before! or the base64image will represent a white image.

"animation": {
                    "onComplete" : RemoveCanvasAndCreateBase64Image
                }

second important part, the js function that do to work for you :)

all of this JS code must be added into the GetDefinition method from the Chart base Class. Best we overload getdifnition with a new parameter, a [bool]ToBase64

Then we must change the New-PSHTMLChart function and add a [switch]$ToBase64 = $False

this is the easy part.

I recommand using https://github.com/adamdriscoll/selenium-powershell to be able to fetch the code of the generated page (the one after the javascript is executed ... ! ) in adam example, we could just do a $drivers.pagesource and voila !

Might be a better way to do this !

Edit: Copy/Paste the html code into a file on your disk then, after importing the selenium project:

$drivers = Start-SeChrome
Enter-SeUrl -Driver $drivers -Url C:\temp\pshtml\BarChart1.html
sleep 10
$drivers.pagesource | out-fille c:\temp\pshtml\barchart_withoutJS.html

Edit2: Here is the overload i started to write, it needs to be completed with the javascript available in the html code:

[String] GetDefinition([String]$CanvasID,[Bool]$ToBase64){
        
        $FullDefintion = [System.Text.StringBuilder]::new()
        $FullDefintion.Append($this.GetDefinitionStart([String]$CanvasID))
        $FullDefintion.AppendLine($this.GetDefinitionBody())
        $FullDefintion.AppendLine($this.GetDefinitionEnd())
        $FullDefintion.AppendLine("function RemoveCanvasAndCreateBase64Image (){")
        $FullDefintion.AppendLine("var base64 = this.toBase64Image();")
        $FullDefintion.AppendLine("var element = this.canvas;")
        $FullDefintion.AppendLine("var parent = element.parentNode;")
        $FullDefintion.AppendLine("var img = document.createElement('img');")
        $FullDefintion.AppendLine("img.src = base64;")
        $FullDefintion.AppendLine("img.name = element.id;")
        $FullDefintion.AppendLine("element.before(img);")
        $FullDefintion.AppendLine("parent.removeChild(element);")
        $FullDefintion.AppendLine("};")
        $FullDefintion.replace('"RemoveCanvasAndCreateBase64Image"','RemoveCanvasAndCreateBase64Image')
        $FullDefintionCleaned = Clear-WhiteSpace $FullDefintion
        return $FullDefintionCleaned
    }

Somewhere we also need to add the animation : for the javascript function to execute properly. I'll try to complete the code later.

LxLeChat avatar Jul 21 '19 12:07 LxLeChat

As discussed on Slack with @LxLeChat for this to be function, we will need have one script tag per PSHTML chart. (currently, several ones could be located in one ScriptChart). This enhancement is tracked here -> https://github.com/Stephanevg/PSHTML/issues/255

Stephanevg avatar Jul 22 '19 13:07 Stephanevg

function RemoveCanvasAndCreateBase64Image (){
    var base64 = this.toBase64Image();
    var element = this.canvas;
    var parent = element.parentNode;
    var img = document.createElement('img');
    img.src = base64;
    img.name = element.id;
    element.before(img);
    parent.removeChild(element);
    //var scripttags = document.getElementsByTagName('script');
    //var scripttags = document.getElementsByTagName('script');
    //for (i=0;i<scripttags.length;){
    //    var parent = scripttags[i].parentNode;
    //    parent.removeChild(scripttags[i]);
    //}
};

this is an updated version of the function to create the base64 img. This will work even if their are multiple charts on the same page. It'll create a img tag, with the name of the charttype like this: <img src=... name="barcanvas"> For the moment i commented the part that delete script tags because all the code for the chart are in one huge script block.. and i want to make sure that the script tag is not deleted after the completion of the first chart ... !

LxLeChat avatar Jul 22 '19 14:07 LxLeChat