diff --git a/app/assets/images/email/README.md b/app/assets/images/email/README.md index c93a28f3b..408f9a490 100644 --- a/app/assets/images/email/README.md +++ b/app/assets/images/email/README.md @@ -16,6 +16,7 @@ the mailer view to reference the new file. Never mutate an existing file. ## Email stylesheet -The email CSS lives at `app/assets/stylesheets/email.css` — not in this -folder — but it's referenced by mailer layouts via the pipeline path -`email.css`. +The email CSS is embedded directly in the email `` via +`app/views/shared_mailers/_header.html.haml`. It is no longer loaded from the +asset pipeline, so there is no external stylesheet URL to go stale or change +after an email is sent. diff --git a/app/assets/stylesheets/email.css b/app/assets/stylesheets/email.css deleted file mode 100644 index 3102d95c3..000000000 --- a/app/assets/stylesheets/email.css +++ /dev/null @@ -1,226 +0,0 @@ -/* ------------------------------------- - GLOBAL -------------------------------------- */ -* { - margin:0; - padding:0; -} -* { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; } - -img { - max-width: 100%; -} -.collapse { - margin:0; - padding:0; -} -body { - -webkit-font-smoothing:antialiased; - -webkit-text-size-adjust:none; - width: 100%!important; - height: 100%; -} - - -/* ------------------------------------- - ELEMENTS -------------------------------------- */ -a { color: #006cc2;} - -.btn { - text-decoration:none; - color: #FFF; - background-color: #006cc2; - padding:10px 16px; - font-weight:bold; - margin-right:10px; - text-align:center; - cursor:pointer; - display: inline-block; -} - -p.callout { - padding:15px; - background-color:#ECF8FF; - margin-bottom: 15px; -} -.callout a { - font-weight:bold; - color: #2BA6CB; -} - -table.social { -/* padding:15px; */ - background-color: #ebebeb; - -} -.social .soc-btn { - padding: 3px 7px; - font-size:13px; - margin-bottom:10px; - text-decoration:none; - color: #FFF;font-weight:bold; - display:block; - text-align:center; -} -.social .soc-btn.fb { background-color: #3B5998!important; } -.social .soc-btn.bs { background-color: #1daced!important; } -.social .soc-btn.ms { background-color: #000!important; } -.social .soc-btn.sl { background-color: #2EB67D!important; } -.social .soc-btn.yt { background-color: #FF0000!important; } -.social .soc-btn.li { background-color: #0077B5!important; } - -.sidebar .soc-btn { - display:block; - width:100%; -} - -/* ------------------------------------- - HEADER -------------------------------------- */ -table.head-wrap { width: 100%;} - -.header.container table td.logo { padding: 15px; } -.header.container table td.label { padding: 15px; padding-left:0px;} - - -/* ------------------------------------- - BODY -------------------------------------- */ -table.body-wrap { width: 100%;} - - -/* ------------------------------------- - FOOTER -------------------------------------- */ -table.footer-wrap { width: 100%; clear:both!important; -} -.footer-wrap .container td.content p { border-top: 1px solid rgb(253,253,253); padding-top:15px;} -.footer-wrap .container td.content p { - font-size:10px; - font-weight: bold; - -} - - -/* ------------------------------------- - TYPOGRAPHY -------------------------------------- */ -h1,h2,h3,h4,h5,h6 { -font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; line-height: 1.1; margin-bottom:15px; color:#000; -} -h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; text-transform: none; } - -h1 { font-weight:200; font-size: 44px;} -h2 { font-weight:200; font-size: 37px;} -h3 { font-weight:500; font-size: 27px;} -h4 { font-weight:500; font-size: 23px;} -h5 { font-weight:900; font-size: 17px;} -h6 { font-weight:900; font-size: 14px; text-transform: uppercase; color:#444;} - -.collapse { margin:0!important;} - -p, ul { - margin-bottom: 10px; - font-weight: normal; - font-size:15px; - line-height:1.6; -} -p.lead { font-size:18px; } -p.last { margin-bottom:0px;} - -ul li { - margin-left:5px; - list-style-position: inside; -} - -/* ------------------------------------- - SIDEBAR -------------------------------------- */ -ul.sidebar { - background:#ebebeb; - display:block; - list-style-type: none; -} -ul.sidebar li { display: block; margin:0;} -ul.sidebar li a { - text-decoration:none; - color: #666; - padding:10px 16px; -/* font-weight:bold; */ - margin-right:10px; -/* text-align:center; */ - cursor:pointer; - border-bottom: 1px solid #777777; - border-top: 1px solid #FFFFFF; - display:block; - margin:0; -} -ul.sidebar li a.last { border-bottom-width:0px;} -ul.sidebar li a h1,ul.sidebar li a h2,ul.sidebar li a h3,ul.sidebar li a h4,ul.sidebar li a h5,ul.sidebar li a h6,ul.sidebar li a p { margin-bottom:0!important;} - - - -/* --------------------------------------------------- - RESPONSIVENESS - Nuke it from orbit. It's the only way to be sure. ------------------------------------------------------- */ - -/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */ -.container { - display:block!important; - max-width:600px!important; - margin:0 auto!important; /* makes it centered */ - clear:both!important; -} - -/* This should also be a block element, so that it will fill 100% of the .container */ -.content { - padding:15px; - max-width:600px; - margin:0 auto; - display:block; -} - -/* Let's make sure tables in the content area are 100% wide */ -.content table { width: 100%; } - - -/* Odds and ends */ -.column { - width: 300px; - float:left; -} -.column tr td { padding: 15px; } -.column-wrap { - padding:0!important; - margin:0 auto; - max-width:600px!important; -} -.column table { width:100%;} -.social .column { - width: 280px; - min-width: 279px; - float:left; -} - -/* Be sure to place a .clear element after each set of columns, just to be safe */ -.clear { display: block; clear: both; } - - -/* ------------------------------------------- - PHONE - For clients that support media queries. - Nothing fancy. --------------------------------------------- */ -@media only screen and (max-width: 600px) { - - a[class="btn"] { display:block!important; margin-bottom:10px!important; background-image:none!important; margin-right:0!important;} - - div[class="column"] { width: auto!important; float:none!important;} - - table.social div[class="column"] { - width:auto!important; - } - -} diff --git a/app/views/shared_mailers/_header.html.haml b/app/views/shared_mailers/_header.html.haml index c68ebd13b..5b1860e29 100644 --- a/app/views/shared_mailers/_header.html.haml +++ b/app/views/shared_mailers/_header.html.haml @@ -1,9 +1,236 @@ !!! -%html{ xmlns: "http://www.w3.org/1999/xhtml" } +%html{ xmlns: 'http://www.w3.org/1999/xhtml' } %head - %meta{name: "viewport", content: "width=device-width" } + %meta{ name: 'viewport', content: 'width=device-width' } - %meta{ 'http-equiv' => "Content-Type", content: "text/html; charset=UTF-8" } - %title=title + %meta{ 'http-equiv': 'Content-Type', content: 'text/html; charset=UTF-8' } + %title= title - %link{rel: "stylesheet", type: "text/css", href: asset_path("email.css")} + %style{ type: 'text/css' } + :plain + /* ------------------------------------- + GLOBAL + ------------------------------------- */ + * { + margin:0; + padding:0; + } + * { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; } + + img { + max-width: 100%; + } + .collapse { + margin:0; + padding:0; + } + body { + -webkit-font-smoothing:antialiased; + -webkit-text-size-adjust:none; + width: 100%!important; + height: 100%; + } + + + /* ------------------------------------- + ELEMENTS + ------------------------------------- */ + a { color: #006cc2;} + + .btn { + text-decoration:none; + color: #FFF; + background-color: #006cc2; + padding:10px 16px; + font-weight:bold; + margin-right:10px; + text-align:center; + cursor:pointer; + display: inline-block; + } + + p.callout { + padding:15px; + background-color:#ECF8FF; + margin-bottom: 15px; + } + .callout a { + font-weight:bold; + color: #2BA6CB; + } + + table.social { + /* padding:15px; */ + background-color: #ebebeb; + + } + .social .soc-btn { + padding: 3px 7px; + font-size:13px; + margin-bottom:10px; + text-decoration:none; + color: #FFF;font-weight:bold; + display:block; + text-align:center; + } + .social .soc-btn.fb { background-color: #3B5998!important; } + .social .soc-btn.bs { background-color: #1daced!important; } + .social .soc-btn.ms { background-color: #000!important; } + .social .soc-btn.sl { background-color: #2EB67D!important; } + .social .soc-btn.yt { background-color: #FF0000!important; } + .social .soc-btn.li { background-color: #0077B5!important; } + + .sidebar .soc-btn { + display:block; + width:100%; + } + + /* ------------------------------------- + HEADER + ------------------------------------- */ + table.head-wrap { width: 100%;} + + .header.container table td.logo { padding: 15px; } + .header.container table td.label { padding: 15px; padding-left:0px;} + + + /* ------------------------------------- + BODY + ------------------------------------- */ + table.body-wrap { width: 100%;} + + + /* ------------------------------------- + FOOTER + ------------------------------------- */ + table.footer-wrap { width: 100%; clear:both!important; + } + .footer-wrap .container td.content p { border-top: 1px solid rgb(253,253,253); padding-top:15px;} + .footer-wrap .container td.content p { + font-size:10px; + font-weight: bold; + + } + + + /* ------------------------------------- + TYPOGRAPHY + ------------------------------------- */ + h1,h2,h3,h4,h5,h6 { + font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; line-height: 1.1; margin-bottom:15px; color:#000; + } + h1 small, h2 small, h3 small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; text-transform: none; } + + h1 { font-weight:200; font-size: 44px;} + h2 { font-weight:200; font-size: 37px;} + h3 { font-weight:500; font-size: 27px;} + h4 { font-weight:500; font-size: 23px;} + h5 { font-weight:900; font-size: 17px;} + h6 { font-weight:900; font-size: 14px; text-transform: uppercase; color:#444;} + + .collapse { margin:0!important;} + + p, ul { + margin-bottom: 10px; + font-weight: normal; + font-size:15px; + line-height:1.6; + } + p.lead { font-size:18px; } + p.last { margin-bottom:0px;} + + ul li { + margin-left:5px; + list-style-position: inside; + } + + /* ------------------------------------- + SIDEBAR + ------------------------------------- */ + ul.sidebar { + background:#ebebeb; + display:block; + list-style-type: none; + } + ul.sidebar li { display: block; margin:0;} + ul.sidebar li a { + text-decoration:none; + color: #666; + padding:10px 16px; + /* font-weight:bold; */ + margin-right:10px; + /* text-align:center; */ + cursor:pointer; + border-bottom: 1px solid #777777; + border-top: 1px solid #FFFFFF; + display:block; + margin:0; + } + ul.sidebar li a.last { border-bottom-width:0px;} + ul.sidebar li a h1,ul.sidebar li a h2,ul.sidebar li a h3,ul.sidebar li a h4,ul.sidebar li a h5,ul.sidebar li a h6,ul.sidebar li a p { margin-bottom:0!important;} + + + + /* --------------------------------------------------- + RESPONSIVENESS + Nuke it from orbit. It's the only way to be sure. + ------------------------------------------------------ */ + + /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */ + .container { + display:block!important; + max-width:600px!important; + margin:0 auto!important; /* makes it centered */ + clear:both!important; + } + + /* This should also be a block element, so that it will fill 100% of the .container */ + .content { + padding:15px; + max-width:600px; + margin:0 auto; + display:block; + } + + /* Let's make sure tables in the content area are 100% wide */ + .content table { width: 100%; } + + + /* Odds and ends */ + .column { + width: 300px; + float:left; + } + .column tr td { padding: 15px; } + .column-wrap { + padding:0!important; + margin:0 auto; + max-width:600px!important; + } + .column table { width:100%;} + .social .column { + width: 280px; + min-width: 279px; + float:left; + } + + /* Be sure to place a .clear element after each set of columns, just to be safe */ + .clear { display: block; clear: both; } + + + /* ------------------------------------------- + PHONE + For clients that support media queries. + Nothing fancy. + -------------------------------------------- */ + @media only screen and (max-width: 600px) { + + a[class="btn"] { display:block!important; margin-bottom:10px!important; background-image:none!important; margin-right:0!important;} + + div[class="column"] { width: auto!important; float:none!important;} + + table.social div[class="column"] { + width:auto!important; + } + + } diff --git a/app/views/shared_mailers/_social.html.haml b/app/views/shared_mailers/_social.html.haml index 86b9974be..0810dc874 100644 --- a/app/views/shared_mailers/_social.html.haml +++ b/app/views/shared_mailers/_social.html.haml @@ -8,14 +8,12 @@ %td %h5 Connect with us: - -# background-color is inlined on each link because email clients only - -# reliably render inline styles; see issue #2634 %p - %a{ href: "https://slack.codebar.io/", class: 'soc-btn sl', style: 'background-color: #2EB67D;' } Join us on Slack - %a{ href: "https://www.linkedin.com/company/codebarcommunity", class: "soc-btn li", style: 'background-color: #0077B5;' } Follow us on LinkedIn - %a{ href: "https://www.facebook.com/codebarHQ", class: 'soc-btn fb', style: 'background-color: #3B5998;' } Follow us on Facebook - %a{ href: "https://bsky.app/profile/codebar.bsky.social", class: "soc-btn bs", style: 'background-color: #1daced;' } Follow us on Bluesky - %a{ href: "https://www.youtube.com/channel/UCEYz232agE47GHUq8wneBCA", class: "soc-btn yt", style: 'background-color: #FF0000;' } Follow us on YouTube + %a{ href: "https://slack.codebar.io/", class: 'soc-btn sl'} Join us on Slack + %a{ href: "https://www.linkedin.com/company/codebarcommunity", class: "soc-btn li" } Follow us on LinkedIn + %a{ href: "https://www.facebook.com/codebarHQ", class: 'soc-btn fb'} Follow us on Facebook + %a{ href: "https://bsky.app/profile/codebar.bsky.social", class: "soc-btn bs" } Follow us on Bluesky + %a{ href: "https://www.youtube.com/channel/UCEYz232agE47GHUq8wneBCA", class: "soc-btn yt" } Follow us on YouTube diff --git a/spec/mailers/event_invitation_mailer_spec.rb b/spec/mailers/event_invitation_mailer_spec.rb index 81826f9cc..4ee4279f0 100644 --- a/spec/mailers/event_invitation_mailer_spec.rb +++ b/spec/mailers/event_invitation_mailer_spec.rb @@ -67,6 +67,12 @@ expect(email.body.encoded).to match('hello@codebar.io') end + it_behaves_like 'email with social link colours' do + def send_email + described_class.invite_student(event, member, invitation).deliver_now + end + end + describe 'XSS protection' do let(:event_with_html) do Fabricate(:event, diff --git a/spec/mailers/feedback_request_mailer_spec.rb b/spec/mailers/feedback_request_mailer_spec.rb index 002e843db..6b35f4fd8 100644 --- a/spec/mailers/feedback_request_mailer_spec.rb +++ b/spec/mailers/feedback_request_mailer_spec.rb @@ -29,6 +29,12 @@ expect(email.subject).to eq(email_subject) expect(email.from).to eq(['meetings@codebar.io']) end + + it_behaves_like 'email with social link colours' do + def send_email + described_class.request_feedback(workshop, member, feedback_request).deliver_now + end + end end context 'virtual workshop' do diff --git a/spec/mailers/meeting_invitation_mailer_spec.rb b/spec/mailers/meeting_invitation_mailer_spec.rb index ea6744f54..c7aff91b9 100644 --- a/spec/mailers/meeting_invitation_mailer_spec.rb +++ b/spec/mailers/meeting_invitation_mailer_spec.rb @@ -101,4 +101,10 @@ expect(mail.body.encoded).to match('hello@codebar.io') end end + + it_behaves_like 'email with social link colours' do + def send_email + described_class.invite(meeting, member, invitation).deliver_now + end + end end diff --git a/spec/mailers/virtual_workshop_invitation_mailer_spec.rb b/spec/mailers/virtual_workshop_invitation_mailer_spec.rb index f50e6fdef..511fc4f88 100644 --- a/spec/mailers/virtual_workshop_invitation_mailer_spec.rb +++ b/spec/mailers/virtual_workshop_invitation_mailer_spec.rb @@ -100,6 +100,12 @@ expect(email.body.encoded).to match(workshop.chapter.email) end + it_behaves_like 'email with social link colours' do + def send_email + described_class.invite_student(workshop, member, invitation).deliver_now + end + end + it '#waitlist_reminder' do email_subject = "Reminder: you're on the codebar waiting list " \ "(#{humanize_date(workshop.date_and_time, with_time: true)})" diff --git a/spec/mailers/workshop_invitation_mailer_spec.rb b/spec/mailers/workshop_invitation_mailer_spec.rb index 0584a2f81..c941cb5c5 100644 --- a/spec/mailers/workshop_invitation_mailer_spec.rb +++ b/spec/mailers/workshop_invitation_mailer_spec.rb @@ -110,14 +110,9 @@ expect(email.body.encoded).to match(workshop.chapter.email) end - it '#invite_student renders social links with inline background colours' do - # asserts on the template output before premailer runs, so the colours - # don't depend on email.css being resolved (see issue #2634) - mail = described_class.invite_student(workshop, member, invitation) - - html = mail.body.decoded - ['#2EB67D', '#0077B5', '#3B5998', '#1daced', '#FF0000'].each do |colour| - expect(html).to include("background-color: #{colour}") + it_behaves_like 'email with social link colours' do + def send_email + described_class.invite_student(workshop, member, invitation).deliver_now end end diff --git a/spec/support/shared_examples/behaves_like_email_with_social_links.rb b/spec/support/shared_examples/behaves_like_email_with_social_links.rb new file mode 100644 index 000000000..b5d71fea8 --- /dev/null +++ b/spec/support/shared_examples/behaves_like_email_with_social_links.rb @@ -0,0 +1,12 @@ +RSpec.shared_examples 'email with social link colours' do + it 'inlines social link background colours and embeds the stylesheet' do + send_email + + html = ActionMailer::Base.deliveries.last.body.encoded + ['#2EB67D', '#0077B5', '#3B5998', '#1daced', '#FF0000'].each do |colour| + expect(html).to include("background-color: #{colour}") + end + expect(html).not_to include('/assets/email.css') + expect(html).to match(/