W3CGHtmail / html-email-spec

Home for the HTML for Email Community Group Specification Proposal
5 stars 1 forks source link

CSS Prefixing #1

Open hteumeuleu opened 8 years ago

hteumeuleu commented 8 years ago

In order to prevent HTML and CSS to impact a webmail's UI, webmails need to include styles safely and avoid conflicts. This is important in two ways:

Here's a comparison of how webmails procede nowadays. For these tests, I will use the following code example.

<!doctype html>
<html>
    <head>
        <title>Email Spec Test 001</title>
        <style>
            html, body { background:red; font:1em sans-serif; }
            html ~ * { background:red; }
            .foo, .bar { background:green; color:#fff; }
            .foo:hover { background:#000 !important; }
            @media only screen and (min-width:30em) {
                .foo { background:green; }
                .bar { background:green; }
            }
        </style>
    </head>
    <body>
        <div class="foo bar">Hello World.</div>
    </body>
</html>

Gmail

Gmail has various support of CSS, but for the versions that support <style> tags (for example the public desktop webmail), here's what happens.

The <style> is moved inside the <head> of the webmail. The HTML is wrapped by a <div> with a dynamic class (ex: m123ab45c6d7ef89g). Every CSS rule is prefixed by div.m123ab45c6d7ef89g.

<style>
    div.m123ab45c6d7ef89g html, div.m123ab45c6d7ef89g body { background:red; font:1em sans-serif }
    div.m123ab45c6d7ef89g html ~ * { background:red }
    div.m123ab45c6d7ef89g .foo, div.m123ab45c6d7ef89g .bar { background:green; color:#fff }
    @media only screen and (min-width:30em) {
        div.m123ab45c6d7ef89g .foo { background:green }
        div.m123ab45c6d7ef89g .bar { background:green }
    }
</style>
<div id=":va" class="a3s aXjCH m123ab45c6d7ef89g">
    <u></u>
    <div>
        <div>Hello World.</div>
    </div>
</div>

Known bugs

Recently, I realized Gmail didn't prefix @keyframes declarations names. So you could override Gmail's own UI animations from an email. This was reported to Google and fixed in July.

Outlook.com (old version)

The old version of Outlook.com (still available in France for some accounts) wraps the HTML with a <div class="ExternalClass">. Every class or id attributes in HTML is prefixed by ecx. Every CSS rule is prefixed by ExternalClass.

<div class="ExternalClass MsgBodyContainer" id="bodyreadMessagePartBodyControl289f" data-link="class{:~tag.cssClasses(PlainText, IsContentFiltered)}">
    <title>Email Spec Test 001</title>
    <style>
        .ExternalClass html, .ExternalClass { background:red; font:1em sans-serif; }
        .ExternalClass html ~ * { background:red; }
        .ExternalClass .ecxfoo, .ExternalClass .ecxbar { background:green; color:#fff; }
        .ExternalClass .ecxfoo:hover { background:#000 !important; }
        @media only screen and (min-width:30em) {
            .ExternalClass .ecxfoo { background:green; }
            .ExternalClass .ecxbar { background:green;  }
        }
    </style>
    <div class="ecxfoo ecxbar">Hello World.</div>
</div>

Known bugs

The old Outlook.com is famous for two major bugs in its prefixing:

In the new Outlook.com (and Office 365), the HTML is wrapped with a <div class="rps_4efd"> (where the last four characters are dynamic). Every class or id attributes in HTML is prefixed by x_.

<div class="rps_4efd">
    <style type="text/css">
        <!-- .rps_4efd html, .rps_4efd body { background: red; font: 1em sans-serif; }
        .rps_4efd .x_foo, .rps_4efd .x_bar { background: green; color: #fff; }
        -->
    </style>
    <div>
        <div>
            <div class="x_foo x_bar">Hello World.</div>
        </div>
    </div>
</div>

Yahoo

The desktop webmail of Yahoo wraps the HTML with a <div id="yiv2779339103"> (with a dynamic identifier). Every class or id attributes in HTML is prefixed by yiv2779339103 (dynamic as well). Every CSS rule is prefixed by #yiv2779339103.

<div id="yiv2779339103">
    <title>Email Spec Test 001</title>
    <style>
        #yiv2779339103 html, #yiv2779339103 body {background:red;font:1em sans-serif;}
        #yiv2779339103 html  * {background:red;}
        #yiv2779339103 .yiv2779339103foo, #yiv2779339103 .yiv2779339103bar {background:green;color:#fff;}
        #yiv2779339103 .yiv2779339103foo:hover {background:#000 !important;}
        @media screen and (min-width:30em){
            #yiv2779339103 .yiv2779339103foo {background:green;}
            #yiv2779339103 .yiv2779339103bar {background:green;}
        }
    </style>
    <div id="yui_3_16_0_ym19_1_1473089016217_1234">
        <div class="yiv2779339103foo yiv2779339103bar" id="yui_3_16_0_ym19_1_1473089016217_1234">Hello World.</div>
    </div>
</div>

Known bugs

Yahoo wrongly prefixes nested media queries. The following code…

@media only screen and (min-width:320px) {
    @media only screen and (max-width:480px) {
        .box { background:#001F3F; }
    }

    .box { background:#FF851B; }
}

…is transformed by Yahoo into:

@media screen and (min-width:320px) {
    @media screen and (max-width:480px)
    #yiv6851014585 .yiv6851014585box { background:#001F3F; }
}

#yiv6851014585 .yiv6851014585box { background:#FF851B; }
#yiv6851014585

Even though use cases for nested media queries is still pretty rare, the parsing/prefixing bug causes the code to have a different meaning. This used to happen for regular media queries as well until Yahoo fixed it in 2015.

Orange

The desktop webmail of french ISP Orange wraps the HTML with a <div id="message">. Every CSS rule is prefixed by #message.

<div id="message" class="mail-content-read">
    <title>Email Spec Test 001</title>
    <style type="text/css">
        #message html,#message  body { background:red; font:1em sans-serif;}
        #message html ~ * { background:red;}
        #message .foo,#message  .bar { background:green; color:#fff;}
        #message .foo:hover { background:#000 !important;}
        @media only screen and (min-width:30em) {
            .foo { background:green;}
            #message .bar { background:green;}
        }
        }
    </style>
    <div class="foo bar">Hello World.</div>
</div>

Known bugs

The first CSS rule in a media query isn't prefixed. This creates specificity issues, as all other rules are prefixed with an id, except this one.

Orange Pro

Orange also has a brand new webmail for professionals released this summer. It wraps the HTML with a <div id="sandBox">. Every class or id attributes in HTML is suffixed by _sandBox. Every CSS rule is prefixed by #sandBox and suffixed by _sandBox.

<div id="sandBox" class="oo-mail-viewer-sandbox">
    <title>Email Spec Test 001</title>
    <style>
        #sandBox html_sandBox, #sandBox body _sandBox { background:red; font:1em sans-serif; }
        #sandBox html ~ * { background:red; }
        #sandBox .foo _sandBox, #sandBox .bar _sandBox { background:green; color:#fff; }
        #sandBox .foo:hover _sandBox { background:#000 !important; }
        @media only screen and (min-width:30em) _sandBox {
            #sandBox .foo _sandBox { background:green; }
            #sandBox .bar _sandBox { background:green;  }
        }
    </style>
    <div class="foo_sandBox bar_sandBox">Hello World.</div>
</div>

Known bugs

The whole thing is pretty terrible. Not a single CSS rule is prefixed correctly. The suffix _sandBox is added even for HTML tag names or media queries. And it's added with a space before it, so it's not even correct for class names.

SFR

The desktop webmail of french ISP SFR uses a similar webmail as Orange's. It wraps the HTML with a <div id="message">. Every CSS rule is prefixed by #message.

<div id="message" class="messageBody ">
    <title>Email Spec Test 001</title>
    <style type="text/css">
        #message html,#message  body { background:red; font:1em sans-serif;}
        #message html ~ * { background:red;}
        #message .foo,#message  .bar { background:green; color:#fff;}
        #message .foo:hover { background:#000 !important;}
        @media only screen and (min-width:30em) {
            .foo { background:green;}
            #message .bar { background:green;}
        }
        }
    </style>
    <div class="foo bar">Hello World.</div>
</div>

Known bugs

The same bug as Orange's happens.

La Poste

The desktop webmail of french ESP La Poste embeds the HTML within an <iframe>. It applies a class on the body of the iframe (<body class="MsgBody">). No CSS rule is prefixed.

<iframe name="zv__MSG-1840__MSG__body__iframe">
    …
    <body class="MsgBody MsgBody-html ">
        <table>…</table>
        <br>
        <br>
        <title>Email Spec Test 001</title>
        <style>
            html, body { background:red; font:1em sans-serif; }
            html ~ * { background:red; }
            .foo, .bar { background:green; color:#fff; }
            .foo:hover { background:#000 !important; }
            @media only screen and (min-width:30em) {
                .foo { background:green; }
                .bar { background:green; }
            }
        </style>
        <div class="foo bar">Hello World.</div>
    </body>
</iframe>

AOL

The desktop webmail of AOL embeds the HTML within an <iframe>. It wraps the HTML with a <div class="aolReplacedBody">. Every CSS rule is prefixed by .aolReplacedBody. The class or id attributes in HTML are not prefixed. AOL uses a scoped attribute on the <style> element.

<div id="AOLMsgPart_2_722ade9b-ef91-43c1-8bc8-b6386fea2f88">
    <title>Email Spec Test 001</title>
    <style scoped>
        .aolReplacedBody html,.aolReplacedBody { background:red; font:1em sans-serif; }
        .aolReplacedBody html ~ * { background:red; }
        .aolReplacedBody .foo, .aolReplacedBody  .bar { background:green; color:#fff; }
        .aolReplacedBody .foo:hover { background:#000 !important; }
        @media only screen and (min-width:30em) {
            .aolReplacedBody .foo { background:green; }
            .aolReplacedBody .bar { background:green; }
        }
    </style>
    <div class="aolReplacedBody">
        <div class="foo bar">Hello World.</div>
    </div>
</div>

Yandex

The desktop webmail of Yandex doesn't seem to support <style> tags, nor class or id attributes in HTML.

<div class="b-message-body">
    <div class="b-message-body__content">
        <div>Hello World.</div>
    </div>
</div>

Main takeaways


Do you have more examples ? I feel that the email specification should address prefixing by providing guidelines as to how to prefix CSS rules, and what types of rules and declarations need prefixing.

dyspop commented 8 years ago

Nice to see the historical bugs, though hard to figure out the "state of things" now (as it always is with email). Usually I think it's a losing battle, but in this case the context is great.

It really seems like recommending scoped styles is just the way to go, what could we possible aim to accomplish if that works?

hteumeuleu commented 8 years ago

I agree that scoped styles is a great recommandation. However, given the current support for scoped (it only works in Firefox), it should only be applied as an extra way to secure styles.

So my main interrogation regarding prefixing is: should we recommend a way to prefix? (for example "add a class before each rule" or "prefer using ids", etc.)

bloodyowl commented 8 years ago

I think that the way to go in order to get rid of these bugs is to render emails in a sandboxed iframe.

const render = (email, target) => {
  const iframe = document.createElement("iframe")
  iframe.sandbox = ""
  iframe.style.width = "100%"
  iframe.style.height = "0"
  let timer
  const tick = () => {
    iframe.style.border = "none"
    iframe.style.height = iframe.contentDocument.documentElement.scrollHeight + "px"
    timer = requestAnimationFrame(tick)
  }
  iframe.onload = () => {
    timer = requestAnimationFrame(tick)
  }
  target.appendChild(iframe)
  iframe.contentDocument.open()
  iframe.contentDocument.write("<html></html>")
  iframe.contentDocument.documentElement.innerHTML = email
  iframe.contentDocument.close()
  return () => {
    cancelAnimationFrame(timer)
    iframe.remove()
  }
}
hteumeuleu commented 8 years ago

@bloodyowl It's an interesting idea, and I've been wondering why not more webmails were using iframes. (From my tests, only La Poste and AOL, both on desktop, use them.)

The first thing that came to my mind is that I think iframes usually provide a poor user experience. They spawn weird behaviors with double scrollbars. And it appears the implementation can be tricky in a responsive context. (La Poste does this extremely bad for example. And in your code example, requiring requestAnimationFrame to adjust the iframe height seems like a bad performance idea.) But this is mostly technical stuff, and nothing that couldn't be fixed ultimately.

However, a lot of webmails like to include contextual tools within an HTML email. For example, Gmail includes dynamic buttons that appear over big images without links.

Gmail's download tooltip

This is typically injected right within the email itself. So this means there needs to be prefixing to prevent a developer to maliciously target this code.

hteumeuleu commented 8 years ago

Google has rolled out a new update for Gmail with support for media queries, classes and ids. This changes prefixing a little bit. Here's the result now.

<style>
    div.m156faf23324c98b3 html,
    div.m156faf23324c98b3 body { background:red; font:1em sans-serif }

    div.m156faf23324c98b3 html~* { background:red }

    div.m156faf23324c98b3 .m_7791551661652896834foo,
    div.m156faf23324c98b3 .m_7791551661652896834bar { background:green; color:#fff }

    div.m156faf23324c98b3 .m_7791551661652896834foo:hover { background:#000!important }

    @media only screen and (min-width:30em) {
        div.m156faf23324c98b3 .m_7791551661652896834foo { background:green }
        div.m156faf23324c98b3 .m_7791551661652896834bar { background:green }
    }
</style>
<div id=":9m" class="a3s aXjCH m156faf23324c98b3">
    <u></u>
    <div>
        <div class="m_7791551661652896834foo m_7791551661652896834bar">Hello World.</div>
    </div>
</div>
Malvoz commented 3 years ago

With the risk of broadening the scope (no pun intended) of this issue, there may be other incentives for email clients to utilize iframes (as suggested in https://github.com/W3CGHtmail/html-email-spec/issues/1#issuecomment-247652582) besides style scoping. Below are examples of 2 email clients, where Client A does not utilize iframes for email messages, whereas Client B does:

Client A

<html> <!-- Email client's document -->
  <meta http-equiv="Content-Security-Policy" content="..."> <!-- Client's strict CSP -->

  <div>
    <!-- Email message -->
  </div>
</html>

Issues:

  1. No style encapsulation.
  2. Client needs to rewrite id, class, name, for, aria-labelledby/aria-controls/aria-owns (etc.) attribute values.
  3. The document's CSP may prohibit features of elements (elements that may technically otherwise be supported):

Client B

<html> <!-- Email client's document -->
  <meta http-equiv="Content-Security-Policy" content="..."> <!-- Client's strict CSP -->

  <iframe>
    #document
    <meta http-equiv="Content-Security-Policy" content="..."> <!-- Client's relaxed CSP -->

    <div>
      <!-- Email message -->
    </div>
  </iframe>
</html>

Benefits:

  1. Style encapsulation from the embedding document.
  2. Attribute value encapsulation from the embedding document.
  3. Potential for clients to set a relaxed CSP without compromising the integrity of the embedding document.
  4. Potential for clients to apply other email message-targeted security measurements:

While iframes help with encapsulation, issues may still arise from clients inserting their own elements into the email (for example buttons that appear over images as noted in https://github.com/W3CGHtmail/html-email-spec/issues/1#issuecomment-250957300). In this case Shadow DOM encapsulation seems like a good solution. For example, a client's inserted button placed over an image could be placed in a Shadow root that resets any inheritable styles.


re https://github.com/W3CGHtmail/html-email-spec/issues/1#issuecomment-245066812:

given the current support for scoped

There is a new proposal for scoping/namespacing with re-designed CSS @scope rule.