diff --git a/.env b/.env index b9d34b9d..33ebb174 100644 --- a/.env +++ b/.env @@ -27,14 +27,6 @@ APP_SECRET=7915fd8481d1a52cf42010ebe2caa974 DATABASE_URL=mysql://root:@127.0.0.1:3306/part-db ###< doctrine/doctrine-bundle ### -###> symfony/swiftmailer-bundle ### -# For Gmail as a transport, use: "gmail://username:password@localhost" -# For a generic SMTP server, use: "smtp://localhost:25?encryption=&auth_mode=" -# Delivery is disabled by default via "null://localhost" -MAILER_URL=null://localhost -###< symfony/swiftmailer-bundle ### - - ### Custom vars FIXER_API_KEY=CHANGEME @@ -50,4 +42,7 @@ BANNER="" # In demo mode things it is not possible for a user to change his password and his settings. DEMO_MODE=0 -### End custom vars \ No newline at end of file +### End custom vars +###> symfony/mailer ### +# MAILER_DSN=smtp://localhost +###< symfony/mailer ### diff --git a/assets/css/email.css b/assets/css/email.css new file mode 100644 index 00000000..badb5545 --- /dev/null +++ b/assets/css/email.css @@ -0,0 +1,28 @@ +/* + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +body, +html, +.body { + background: #f3f3f3 !important; +} +.header { + background: #f3f3f3; +} \ No newline at end of file diff --git a/assets/css/foundation-emails.css b/assets/css/foundation-emails.css new file mode 100644 index 00000000..acd1b982 --- /dev/null +++ b/assets/css/foundation-emails.css @@ -0,0 +1,1348 @@ +.wrapper { + width: 100%; } + +#outlook a { + padding: 0; } + +body { + width: 100% !important; + min-width: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + margin: 0; + Margin: 0; + padding: 0; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; } + +.ExternalClass { + width: 100%; } + .ExternalClass, + .ExternalClass p, + .ExternalClass span, + .ExternalClass font, + .ExternalClass td, + .ExternalClass div { + line-height: 100%; } + +#backgroundTable { + margin: 0; + Margin: 0; + padding: 0; + width: 100% !important; + line-height: 100% !important; } + +img { + outline: none; + text-decoration: none; + -ms-interpolation-mode: bicubic; + width: auto; + max-width: 100%; + clear: both; + display: block; } + +center { + width: 100%; + min-width: 580px; } + +a img { + border: none; } + +p { + margin: 0 0 0 10px; + Margin: 0 0 0 10px; } + +table { + border-spacing: 0; + border-collapse: collapse; } + +td { + word-wrap: break-word; + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; + border-collapse: collapse !important; } + +table, tr, td { + padding: 0; + vertical-align: top; + text-align: left; } + +@media only screen { + html { + min-height: 100%; + background: #f3f3f3; } } + +table.body { + background: #f3f3f3; + height: 100%; + width: 100%; } + +table.container { + background: #fefefe; + width: 580px; + margin: 0 auto; + Margin: 0 auto; + text-align: inherit; } + +table.row { + padding: 0; + width: 100%; + position: relative; } + +table.spacer { + width: 100%; } + table.spacer td { + mso-line-height-rule: exactly; } + +table.container table.row { + display: table; } + +td.columns, +td.column, +th.columns, +th.column { + margin: 0 auto; + Margin: 0 auto; + padding-left: 16px; + padding-bottom: 16px; } + td.columns .column, + td.columns .columns, + td.column .column, + td.column .columns, + th.columns .column, + th.columns .columns, + th.column .column, + th.column .columns { + padding-left: 0 !important; + padding-right: 0 !important; } + td.columns .column center, + td.columns .columns center, + td.column .column center, + td.column .columns center, + th.columns .column center, + th.columns .columns center, + th.column .column center, + th.column .columns center { + min-width: none !important; } + +td.columns.last, +td.column.last, +th.columns.last, +th.column.last { + padding-right: 16px; } + +td.columns table:not(.button), +td.column table:not(.button), +th.columns table:not(.button), +th.column table:not(.button) { + width: 100%; } + +td.large-1, +th.large-1 { + width: 32.33333px; + padding-left: 8px; + padding-right: 8px; } + +td.large-1.first, +th.large-1.first { + padding-left: 16px; } + +td.large-1.last, +th.large-1.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-1, +.collapse > tbody > tr > th.large-1 { + padding-right: 0; + padding-left: 0; + width: 48.33333px; } + +.collapse td.large-1.first, +.collapse th.large-1.first, +.collapse td.large-1.last, +.collapse th.large-1.last { + width: 56.33333px; } + +td.large-1 center, +th.large-1 center { + min-width: 0.33333px; } + +.body .columns td.large-1, +.body .column td.large-1, +.body .columns th.large-1, +.body .column th.large-1 { + width: 8.33333%; } + +td.large-2, +th.large-2 { + width: 80.66667px; + padding-left: 8px; + padding-right: 8px; } + +td.large-2.first, +th.large-2.first { + padding-left: 16px; } + +td.large-2.last, +th.large-2.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-2, +.collapse > tbody > tr > th.large-2 { + padding-right: 0; + padding-left: 0; + width: 96.66667px; } + +.collapse td.large-2.first, +.collapse th.large-2.first, +.collapse td.large-2.last, +.collapse th.large-2.last { + width: 104.66667px; } + +td.large-2 center, +th.large-2 center { + min-width: 48.66667px; } + +.body .columns td.large-2, +.body .column td.large-2, +.body .columns th.large-2, +.body .column th.large-2 { + width: 16.66667%; } + +td.large-3, +th.large-3 { + width: 129px; + padding-left: 8px; + padding-right: 8px; } + +td.large-3.first, +th.large-3.first { + padding-left: 16px; } + +td.large-3.last, +th.large-3.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-3, +.collapse > tbody > tr > th.large-3 { + padding-right: 0; + padding-left: 0; + width: 145px; } + +.collapse td.large-3.first, +.collapse th.large-3.first, +.collapse td.large-3.last, +.collapse th.large-3.last { + width: 153px; } + +td.large-3 center, +th.large-3 center { + min-width: 97px; } + +.body .columns td.large-3, +.body .column td.large-3, +.body .columns th.large-3, +.body .column th.large-3 { + width: 25%; } + +td.large-4, +th.large-4 { + width: 177.33333px; + padding-left: 8px; + padding-right: 8px; } + +td.large-4.first, +th.large-4.first { + padding-left: 16px; } + +td.large-4.last, +th.large-4.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-4, +.collapse > tbody > tr > th.large-4 { + padding-right: 0; + padding-left: 0; + width: 193.33333px; } + +.collapse td.large-4.first, +.collapse th.large-4.first, +.collapse td.large-4.last, +.collapse th.large-4.last { + width: 201.33333px; } + +td.large-4 center, +th.large-4 center { + min-width: 145.33333px; } + +.body .columns td.large-4, +.body .column td.large-4, +.body .columns th.large-4, +.body .column th.large-4 { + width: 33.33333%; } + +td.large-5, +th.large-5 { + width: 225.66667px; + padding-left: 8px; + padding-right: 8px; } + +td.large-5.first, +th.large-5.first { + padding-left: 16px; } + +td.large-5.last, +th.large-5.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-5, +.collapse > tbody > tr > th.large-5 { + padding-right: 0; + padding-left: 0; + width: 241.66667px; } + +.collapse td.large-5.first, +.collapse th.large-5.first, +.collapse td.large-5.last, +.collapse th.large-5.last { + width: 249.66667px; } + +td.large-5 center, +th.large-5 center { + min-width: 193.66667px; } + +.body .columns td.large-5, +.body .column td.large-5, +.body .columns th.large-5, +.body .column th.large-5 { + width: 41.66667%; } + +td.large-6, +th.large-6 { + width: 274px; + padding-left: 8px; + padding-right: 8px; } + +td.large-6.first, +th.large-6.first { + padding-left: 16px; } + +td.large-6.last, +th.large-6.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-6, +.collapse > tbody > tr > th.large-6 { + padding-right: 0; + padding-left: 0; + width: 290px; } + +.collapse td.large-6.first, +.collapse th.large-6.first, +.collapse td.large-6.last, +.collapse th.large-6.last { + width: 298px; } + +td.large-6 center, +th.large-6 center { + min-width: 242px; } + +.body .columns td.large-6, +.body .column td.large-6, +.body .columns th.large-6, +.body .column th.large-6 { + width: 50%; } + +td.large-7, +th.large-7 { + width: 322.33333px; + padding-left: 8px; + padding-right: 8px; } + +td.large-7.first, +th.large-7.first { + padding-left: 16px; } + +td.large-7.last, +th.large-7.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-7, +.collapse > tbody > tr > th.large-7 { + padding-right: 0; + padding-left: 0; + width: 338.33333px; } + +.collapse td.large-7.first, +.collapse th.large-7.first, +.collapse td.large-7.last, +.collapse th.large-7.last { + width: 346.33333px; } + +td.large-7 center, +th.large-7 center { + min-width: 290.33333px; } + +.body .columns td.large-7, +.body .column td.large-7, +.body .columns th.large-7, +.body .column th.large-7 { + width: 58.33333%; } + +td.large-8, +th.large-8 { + width: 370.66667px; + padding-left: 8px; + padding-right: 8px; } + +td.large-8.first, +th.large-8.first { + padding-left: 16px; } + +td.large-8.last, +th.large-8.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-8, +.collapse > tbody > tr > th.large-8 { + padding-right: 0; + padding-left: 0; + width: 386.66667px; } + +.collapse td.large-8.first, +.collapse th.large-8.first, +.collapse td.large-8.last, +.collapse th.large-8.last { + width: 394.66667px; } + +td.large-8 center, +th.large-8 center { + min-width: 338.66667px; } + +.body .columns td.large-8, +.body .column td.large-8, +.body .columns th.large-8, +.body .column th.large-8 { + width: 66.66667%; } + +td.large-9, +th.large-9 { + width: 419px; + padding-left: 8px; + padding-right: 8px; } + +td.large-9.first, +th.large-9.first { + padding-left: 16px; } + +td.large-9.last, +th.large-9.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-9, +.collapse > tbody > tr > th.large-9 { + padding-right: 0; + padding-left: 0; + width: 435px; } + +.collapse td.large-9.first, +.collapse th.large-9.first, +.collapse td.large-9.last, +.collapse th.large-9.last { + width: 443px; } + +td.large-9 center, +th.large-9 center { + min-width: 387px; } + +.body .columns td.large-9, +.body .column td.large-9, +.body .columns th.large-9, +.body .column th.large-9 { + width: 75%; } + +td.large-10, +th.large-10 { + width: 467.33333px; + padding-left: 8px; + padding-right: 8px; } + +td.large-10.first, +th.large-10.first { + padding-left: 16px; } + +td.large-10.last, +th.large-10.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-10, +.collapse > tbody > tr > th.large-10 { + padding-right: 0; + padding-left: 0; + width: 483.33333px; } + +.collapse td.large-10.first, +.collapse th.large-10.first, +.collapse td.large-10.last, +.collapse th.large-10.last { + width: 491.33333px; } + +td.large-10 center, +th.large-10 center { + min-width: 435.33333px; } + +.body .columns td.large-10, +.body .column td.large-10, +.body .columns th.large-10, +.body .column th.large-10 { + width: 83.33333%; } + +td.large-11, +th.large-11 { + width: 515.66667px; + padding-left: 8px; + padding-right: 8px; } + +td.large-11.first, +th.large-11.first { + padding-left: 16px; } + +td.large-11.last, +th.large-11.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-11, +.collapse > tbody > tr > th.large-11 { + padding-right: 0; + padding-left: 0; + width: 531.66667px; } + +.collapse td.large-11.first, +.collapse th.large-11.first, +.collapse td.large-11.last, +.collapse th.large-11.last { + width: 539.66667px; } + +td.large-11 center, +th.large-11 center { + min-width: 483.66667px; } + +.body .columns td.large-11, +.body .column td.large-11, +.body .columns th.large-11, +.body .column th.large-11 { + width: 91.66667%; } + +td.large-12, +th.large-12 { + width: 564px; + padding-left: 8px; + padding-right: 8px; } + +td.large-12.first, +th.large-12.first { + padding-left: 16px; } + +td.large-12.last, +th.large-12.last { + padding-right: 16px; } + +.collapse > tbody > tr > td.large-12, +.collapse > tbody > tr > th.large-12 { + padding-right: 0; + padding-left: 0; + width: 580px; } + +.collapse td.large-12.first, +.collapse th.large-12.first, +.collapse td.large-12.last, +.collapse th.large-12.last { + width: 588px; } + +td.large-12 center, +th.large-12 center { + min-width: 532px; } + +.body .columns td.large-12, +.body .column td.large-12, +.body .columns th.large-12, +.body .column th.large-12 { + width: 100%; } + +td.large-offset-1, +td.large-offset-1.first, +td.large-offset-1.last, +th.large-offset-1, +th.large-offset-1.first, +th.large-offset-1.last { + padding-left: 64.33333px; } + +td.large-offset-2, +td.large-offset-2.first, +td.large-offset-2.last, +th.large-offset-2, +th.large-offset-2.first, +th.large-offset-2.last { + padding-left: 112.66667px; } + +td.large-offset-3, +td.large-offset-3.first, +td.large-offset-3.last, +th.large-offset-3, +th.large-offset-3.first, +th.large-offset-3.last { + padding-left: 161px; } + +td.large-offset-4, +td.large-offset-4.first, +td.large-offset-4.last, +th.large-offset-4, +th.large-offset-4.first, +th.large-offset-4.last { + padding-left: 209.33333px; } + +td.large-offset-5, +td.large-offset-5.first, +td.large-offset-5.last, +th.large-offset-5, +th.large-offset-5.first, +th.large-offset-5.last { + padding-left: 257.66667px; } + +td.large-offset-6, +td.large-offset-6.first, +td.large-offset-6.last, +th.large-offset-6, +th.large-offset-6.first, +th.large-offset-6.last { + padding-left: 306px; } + +td.large-offset-7, +td.large-offset-7.first, +td.large-offset-7.last, +th.large-offset-7, +th.large-offset-7.first, +th.large-offset-7.last { + padding-left: 354.33333px; } + +td.large-offset-8, +td.large-offset-8.first, +td.large-offset-8.last, +th.large-offset-8, +th.large-offset-8.first, +th.large-offset-8.last { + padding-left: 402.66667px; } + +td.large-offset-9, +td.large-offset-9.first, +td.large-offset-9.last, +th.large-offset-9, +th.large-offset-9.first, +th.large-offset-9.last { + padding-left: 451px; } + +td.large-offset-10, +td.large-offset-10.first, +td.large-offset-10.last, +th.large-offset-10, +th.large-offset-10.first, +th.large-offset-10.last { + padding-left: 499.33333px; } + +td.large-offset-11, +td.large-offset-11.first, +td.large-offset-11.last, +th.large-offset-11, +th.large-offset-11.first, +th.large-offset-11.last { + padding-left: 547.66667px; } + +td.expander, +th.expander { + visibility: hidden; + width: 0; + padding: 0 !important; } + +table.container.radius { + border-radius: 0; + border-collapse: separate; } + +.block-grid { + width: 100%; + max-width: 580px; } + .block-grid td { + display: inline-block; + padding: 8px; } + +.up-2 td { + width: 274px !important; } + +.up-3 td { + width: 177px !important; } + +.up-4 td { + width: 129px !important; } + +.up-5 td { + width: 100px !important; } + +.up-6 td { + width: 80px !important; } + +.up-7 td { + width: 66px !important; } + +.up-8 td { + width: 56px !important; } + +table.text-center, +th.text-center, +td.text-center, +h1.text-center, +h2.text-center, +h3.text-center, +h4.text-center, +h5.text-center, +h6.text-center, +p.text-center, +span.text-center { + text-align: center; } + +table.text-left, +th.text-left, +td.text-left, +h1.text-left, +h2.text-left, +h3.text-left, +h4.text-left, +h5.text-left, +h6.text-left, +p.text-left, +span.text-left { + text-align: left; } + +table.text-right, +th.text-right, +td.text-right, +h1.text-right, +h2.text-right, +h3.text-right, +h4.text-right, +h5.text-right, +h6.text-right, +p.text-right, +span.text-right { + text-align: right; } + +span.text-center { + display: block; + width: 100%; + text-align: center; } + +@media only screen and (max-width: 596px) { + .small-float-center { + margin: 0 auto !important; + float: none !important; + text-align: center !important; } + .small-text-center { + text-align: center !important; } + .small-text-left { + text-align: left !important; } + .small-text-right { + text-align: right !important; } } + +img.float-left { + float: left; + text-align: left; } + +img.float-right { + float: right; + text-align: right; } + +img.float-center, +img.text-center { + margin: 0 auto; + Margin: 0 auto; + float: none; + text-align: center; } + +table.float-center, +td.float-center, +th.float-center { + margin: 0 auto; + Margin: 0 auto; + float: none; + text-align: center; } + +.hide-for-large { + display: none !important; + mso-hide: all; + overflow: hidden; + max-height: 0; + font-size: 0; + width: 0; + line-height: 0; } + @media only screen and (max-width: 596px) { + .hide-for-large { + display: block !important; + width: auto !important; + overflow: visible !important; + max-height: none !important; + font-size: inherit !important; + line-height: inherit !important; } } + +table.body table.container .hide-for-large * { + mso-hide: all; } + +@media only screen and (max-width: 596px) { + table.body table.container .hide-for-large, + table.body table.container .row.hide-for-large { + display: table !important; + width: 100% !important; } } + +@media only screen and (max-width: 596px) { + table.body table.container .callout-inner.hide-for-large { + display: table-cell !important; + width: 100% !important; } } + +@media only screen and (max-width: 596px) { + table.body table.container .show-for-large { + display: none !important; + width: 0; + mso-hide: all; + overflow: hidden; } } + +body, +table.body, +h1, +h2, +h3, +h4, +h5, +h6, +p, +td, +th, +a { + color: #0a0a0a; + font-family: Helvetica, Arial, sans-serif; + font-weight: normal; + padding: 0; + margin: 0; + Margin: 0; + text-align: left; + line-height: 1.3; } + +h1, +h2, +h3, +h4, +h5, +h6 { + color: inherit; + word-wrap: normal; + font-family: Helvetica, Arial, sans-serif; + font-weight: normal; + margin-bottom: 10px; + Margin-bottom: 10px; } + +h1 { + font-size: 34px; } + +h2 { + font-size: 30px; } + +h3 { + font-size: 28px; } + +h4 { + font-size: 24px; } + +h5 { + font-size: 20px; } + +h6 { + font-size: 18px; } + +body, +table.body, +p, +td, +th { + font-size: 16px; + line-height: 1.3; } + +p { + margin-bottom: 10px; + Margin-bottom: 10px; } + p.lead { + font-size: 20px; + line-height: 1.6; } + p.subheader { + margin-top: 4px; + margin-bottom: 8px; + Margin-top: 4px; + Margin-bottom: 8px; + font-weight: normal; + line-height: 1.4; + color: #8a8a8a; } + +small { + font-size: 80%; + color: #cacaca; } + +a { + color: #2199e8; + text-decoration: none; } + a:hover { + color: #147dc2; } + a:active { + color: #147dc2; } + a:visited { + color: #2199e8; } + +h1 a, +h1 a:visited, +h2 a, +h2 a:visited, +h3 a, +h3 a:visited, +h4 a, +h4 a:visited, +h5 a, +h5 a:visited, +h6 a, +h6 a:visited { + color: #2199e8; } + +pre { + background: #f3f3f3; + margin: 30px 0; + Margin: 30px 0; } + pre code { + color: #cacaca; } + pre code span.callout { + color: #8a8a8a; + font-weight: bold; } + pre code span.callout-strong { + color: #ff6908; + font-weight: bold; } + +table.hr { + width: 100%; } + table.hr th { + height: 0; + max-width: 580px; + border-top: 0; + border-right: 0; + border-bottom: 1px solid #0a0a0a; + border-left: 0; + margin: 20px auto; + Margin: 20px auto; + clear: both; } + +.stat { + font-size: 40px; + line-height: 1; } + p + .stat { + margin-top: -16px; + Margin-top: -16px; } + +span.preheader { + display: none !important; + visibility: hidden; + mso-hide: all !important; + font-size: 1px; + color: #f3f3f3; + line-height: 1px; + max-height: 0px; + max-width: 0px; + opacity: 0; + overflow: hidden; } + +table.button { + width: auto; + margin: 0 0 16px 0; + Margin: 0 0 16px 0; } + table.button table td { + text-align: left; + color: #fefefe; + background: #2199e8; + border: 2px solid #2199e8; } + table.button table td a { + font-family: Helvetica, Arial, sans-serif; + font-size: 16px; + font-weight: bold; + color: #fefefe; + text-decoration: none; + display: inline-block; + padding: 8px 16px 8px 16px; + border: 0 solid #2199e8; + border-radius: 3px; } + table.button.radius table td { + border-radius: 3px; + border: none; } + table.button.rounded table td { + border-radius: 500px; + border: none; } + +table.button:hover table tr td a, +table.button:active table tr td a, +table.button table tr td a:visited, +table.button.tiny:hover table tr td a, +table.button.tiny:active table tr td a, +table.button.tiny table tr td a:visited, +table.button.small:hover table tr td a, +table.button.small:active table tr td a, +table.button.small table tr td a:visited, +table.button.large:hover table tr td a, +table.button.large:active table tr td a, +table.button.large table tr td a:visited { + color: #fefefe; } + +table.button.tiny table td, +table.button.tiny table a { + padding: 4px 8px 4px 8px; } + +table.button.tiny table a { + font-size: 10px; + font-weight: normal; } + +table.button.small table td, +table.button.small table a { + padding: 5px 10px 5px 10px; + font-size: 12px; } + +table.button.large table a { + padding: 10px 20px 10px 20px; + font-size: 20px; } + +table.button.expand, +table.button.expanded { + width: 100% !important; } + table.button.expand table, + table.button.expanded table { + width: 100%; } + table.button.expand table a, + table.button.expanded table a { + text-align: center; + width: 100%; + padding-left: 0; + padding-right: 0; } + table.button.expand center, + table.button.expanded center { + min-width: 0; } + +table.button:hover table td, +table.button:visited table td, +table.button:active table td { + background: #147dc2; + color: #fefefe; } + +table.button:hover table a, +table.button:visited table a, +table.button:active table a { + border: 0 solid #147dc2; } + +table.button.secondary table td { + background: #777777; + color: #fefefe; + border: 0px solid #777777; } + +table.button.secondary table a { + color: #fefefe; + border: 0 solid #777777; } + +table.button.secondary:hover table td { + background: #919191; + color: #fefefe; } + +table.button.secondary:hover table a { + border: 0 solid #919191; } + +table.button.secondary:hover table td a { + color: #fefefe; } + +table.button.secondary:active table td a { + color: #fefefe; } + +table.button.secondary table td a:visited { + color: #fefefe; } + +table.button.success table td { + background: #3adb76; + border: 0px solid #3adb76; } + +table.button.success table a { + border: 0 solid #3adb76; } + +table.button.success:hover table td { + background: #23bf5d; } + +table.button.success:hover table a { + border: 0 solid #23bf5d; } + +table.button.alert table td { + background: #ec5840; + border: 0px solid #ec5840; } + +table.button.alert table a { + border: 0 solid #ec5840; } + +table.button.alert:hover table td { + background: #e23317; } + +table.button.alert:hover table a { + border: 0 solid #e23317; } + +table.button.warning table td { + background: #ffae00; + border: 0px solid #ffae00; } + +table.button.warning table a { + border: 0px solid #ffae00; } + +table.button.warning:hover table td { + background: #cc8b00; } + +table.button.warning:hover table a { + border: 0px solid #cc8b00; } + +table.callout { + margin-bottom: 16px; + Margin-bottom: 16px; } + +th.callout-inner { + width: 100%; + border: 1px solid #cbcbcb; + padding: 10px; + background: #fefefe; } + th.callout-inner.primary { + background: #def0fc; + border: 1px solid #444444; + color: #0a0a0a; } + th.callout-inner.secondary { + background: #ebebeb; + border: 1px solid #444444; + color: #0a0a0a; } + th.callout-inner.success { + background: #e1faea; + border: 1px solid #1b9448; + color: #fefefe; } + th.callout-inner.warning { + background: #fff3d9; + border: 1px solid #996800; + color: #fefefe; } + th.callout-inner.alert { + background: #fce6e2; + border: 1px solid #b42912; + color: #fefefe; } + +.thumbnail { + border: solid 4px #fefefe; + box-shadow: 0 0 0 1px rgba(10, 10, 10, 0.2); + display: inline-block; + line-height: 0; + max-width: 100%; + transition: box-shadow 200ms ease-out; + border-radius: 3px; + margin-bottom: 16px; } + .thumbnail:hover, .thumbnail:focus { + box-shadow: 0 0 6px 1px rgba(33, 153, 232, 0.5); } + +table.menu { + width: 580px; } + table.menu td.menu-item, + table.menu th.menu-item { + padding: 10px; + padding-right: 10px; } + table.menu td.menu-item a, + table.menu th.menu-item a { + color: #2199e8; } + +table.menu.vertical td.menu-item, +table.menu.vertical th.menu-item { + padding: 10px; + padding-right: 0; + display: block; } + table.menu.vertical td.menu-item a, + table.menu.vertical th.menu-item a { + width: 100%; } + +table.menu.vertical td.menu-item table.menu.vertical td.menu-item, +table.menu.vertical td.menu-item table.menu.vertical th.menu-item, +table.menu.vertical th.menu-item table.menu.vertical td.menu-item, +table.menu.vertical th.menu-item table.menu.vertical th.menu-item { + padding-left: 10px; } + +table.menu.text-center a { + text-align: center; } + +.menu[align="center"] { + width: auto !important; } + +body.outlook p { + display: inline !important; } + +@media only screen and (max-width: 596px) { + table.body img { + width: auto; + height: auto; } + table.body center { + min-width: 0 !important; } + table.body .container { + width: 95% !important; } + table.body .columns, + table.body .column { + height: auto !important; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding-left: 16px !important; + padding-right: 16px !important; } + table.body .columns .column, + table.body .columns .columns, + table.body .column .column, + table.body .column .columns { + padding-left: 0 !important; + padding-right: 0 !important; } + table.body .collapse .columns, + table.body .collapse .column { + padding-left: 0 !important; + padding-right: 0 !important; } + td.small-1, + th.small-1 { + display: inline-block !important; + width: 8.33333% !important; } + td.small-2, + th.small-2 { + display: inline-block !important; + width: 16.66667% !important; } + td.small-3, + th.small-3 { + display: inline-block !important; + width: 25% !important; } + td.small-4, + th.small-4 { + display: inline-block !important; + width: 33.33333% !important; } + td.small-5, + th.small-5 { + display: inline-block !important; + width: 41.66667% !important; } + td.small-6, + th.small-6 { + display: inline-block !important; + width: 50% !important; } + td.small-7, + th.small-7 { + display: inline-block !important; + width: 58.33333% !important; } + td.small-8, + th.small-8 { + display: inline-block !important; + width: 66.66667% !important; } + td.small-9, + th.small-9 { + display: inline-block !important; + width: 75% !important; } + td.small-10, + th.small-10 { + display: inline-block !important; + width: 83.33333% !important; } + td.small-11, + th.small-11 { + display: inline-block !important; + width: 91.66667% !important; } + td.small-12, + th.small-12 { + display: inline-block !important; + width: 100% !important; } + .columns td.small-12, + .column td.small-12, + .columns th.small-12, + .column th.small-12 { + display: block !important; + width: 100% !important; } + table.body td.small-offset-1, + table.body th.small-offset-1 { + margin-left: 8.33333% !important; + Margin-left: 8.33333% !important; } + table.body td.small-offset-2, + table.body th.small-offset-2 { + margin-left: 16.66667% !important; + Margin-left: 16.66667% !important; } + table.body td.small-offset-3, + table.body th.small-offset-3 { + margin-left: 25% !important; + Margin-left: 25% !important; } + table.body td.small-offset-4, + table.body th.small-offset-4 { + margin-left: 33.33333% !important; + Margin-left: 33.33333% !important; } + table.body td.small-offset-5, + table.body th.small-offset-5 { + margin-left: 41.66667% !important; + Margin-left: 41.66667% !important; } + table.body td.small-offset-6, + table.body th.small-offset-6 { + margin-left: 50% !important; + Margin-left: 50% !important; } + table.body td.small-offset-7, + table.body th.small-offset-7 { + margin-left: 58.33333% !important; + Margin-left: 58.33333% !important; } + table.body td.small-offset-8, + table.body th.small-offset-8 { + margin-left: 66.66667% !important; + Margin-left: 66.66667% !important; } + table.body td.small-offset-9, + table.body th.small-offset-9 { + margin-left: 75% !important; + Margin-left: 75% !important; } + table.body td.small-offset-10, + table.body th.small-offset-10 { + margin-left: 83.33333% !important; + Margin-left: 83.33333% !important; } + table.body td.small-offset-11, + table.body th.small-offset-11 { + margin-left: 91.66667% !important; + Margin-left: 91.66667% !important; } + table.body table.columns td.expander, + table.body table.columns th.expander { + display: none !important; } + table.body .right-text-pad, + table.body .text-pad-right { + padding-left: 10px !important; } + table.body .left-text-pad, + table.body .text-pad-left { + padding-right: 10px !important; } + table.menu { + width: 100% !important; } + table.menu td, + table.menu th { + width: auto !important; + display: inline-block !important; } + table.menu.vertical td, + table.menu.vertical th, table.menu.small-vertical td, + table.menu.small-vertical th { + display: block !important; } + table.menu[align="center"] { + width: auto !important; } + table.button.small-expand, + table.button.small-expanded { + width: 100% !important; } + table.button.small-expand table, + table.button.small-expanded table { + width: 100%; } + table.button.small-expand table a, + table.button.small-expanded table a { + text-align: center !important; + width: 100% !important; + padding-left: 0 !important; + padding-right: 0 !important; } + table.button.small-expand center, + table.button.small-expanded center { + min-width: 0; } } diff --git a/composer.json b/composer.json index 7b5c7877..968fe788 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "doctrine/annotations": "^1.6", "florianv/swap": "^4.0", "friendsofsymfony/ckeditor-bundle": "^2.0", + "gregwar/captcha-bundle": "dev-master", "league/html-to-markdown": "^4.8", "liip/imagine-bundle": "^2.2", "nyholm/psr7": "^1.1", @@ -34,6 +35,7 @@ "symfony/form": "4.4.*", "symfony/framework-bundle": "4.4.*", "symfony/http-client": "4.4.*", + "symfony/mailer": "4.4.*", "symfony/monolog-bundle": "^3.1", "symfony/orm-pack": "*", "symfony/process": "4.4.*", @@ -45,7 +47,9 @@ "symfony/web-link": "4.4.*", "symfony/webpack-encore-bundle": "^1.1", "symfony/yaml": "4.4.*", + "twig/cssinliner-extra": "^3.0", "twig/extra-bundle": "^3.0", + "twig/inky-extra": "^3.0", "twig/intl-extra": "^3.0", "webmozart/assert": "^1.4" }, diff --git a/composer.lock b/composer.lock index a8fc9087..085fb340 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "823d8542bcdaea46c533eedb6148de31", + "content-hash": "c70f8ae93e38b0f830b6d86e6ac1c3fb", "packages": [ { "name": "clue/stream-filter", @@ -1274,6 +1274,64 @@ ], "time": "2018-06-14T14:45:07+00:00" }, + { + "name": "egulias/email-validator", + "version": "2.1.11", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/92dd169c32f6f55ba570c309d83f5209cefb5e23", + "reference": "92dd169c32f6f55ba570c309d83f5209cefb5e23", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1.0.1", + "php": ">= 5.5" + }, + "require-dev": { + "dominicsayers/isemail": "dev-master", + "phpunit/phpunit": "^4.8.35||^5.7||^6.0", + "satooshi/php-coveralls": "^1.0.1", + "symfony/phpunit-bridge": "^4.4@dev" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "EmailValidator" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "time": "2019-08-13T17:33:27+00:00" + }, { "name": "florianv/exchanger", "version": "2.3.0", @@ -1473,6 +1531,114 @@ ], "time": "2019-04-15T16:29:43+00:00" }, + { + "name": "gregwar/captcha", + "version": "v1.1.7", + "source": { + "type": "git", + "url": "https://github.com/Gregwar/Captcha.git", + "reference": "cf953dd79748406e0292cea8c565399681e4d345" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Gregwar/Captcha/zipball/cf953dd79748406e0292cea8c565399681e4d345", + "reference": "cf953dd79748406e0292cea8c565399681e4d345", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "ext-mbstring": "*", + "php": ">=5.3.0", + "symfony/finder": "*" + }, + "require-dev": { + "phpunit/phpunit": "^6.4" + }, + "type": "captcha", + "autoload": { + "psr-4": { + "Gregwar\\": "src/Gregwar" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Passault", + "email": "g.passault@gmail.com", + "homepage": "http://www.gregwar.com/" + }, + { + "name": "Jeremy Livingston", + "email": "jeremy.j.livingston@gmail.com" + } + ], + "description": "Captcha generator", + "homepage": "https://github.com/Gregwar/Captcha", + "keywords": [ + "bot", + "captcha", + "spam" + ], + "time": "2018-08-17T22:57:28+00:00" + }, + { + "name": "gregwar/captcha-bundle", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Gregwar/CaptchaBundle.git", + "reference": "3ccfdf1c9324acbdfbcccea2c454075115741b3a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Gregwar/CaptchaBundle/zipball/3ccfdf1c9324acbdfbcccea2c454075115741b3a", + "reference": "3ccfdf1c9324acbdfbcccea2c454075115741b3a", + "shasum": "" + }, + "require": { + "gregwar/captcha": "~1.1", + "php": ">=5.3.9", + "symfony/form": "~2.8|~3.0|~4.0", + "symfony/framework-bundle": "~2.8|~3.0|~4.0", + "twig/twig": "^1.40|^2.9" + }, + "type": "captcha-bundle", + "autoload": { + "psr-4": { + "Gregwar\\CaptchaBundle\\": "/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Passault", + "email": "g.passault@gmail.com", + "homepage": "http://www.gregwar.com/" + }, + { + "name": "Jeremy Livingston", + "email": "jeremy.j.livingston@gmail.com" + } + ], + "description": "Captcha bundle", + "homepage": "https://github.com/Gregwar/CaptchaBundle", + "keywords": [ + "Symfony2", + "bot", + "captcha", + "code", + "security", + "spam", + "visual" + ], + "time": "2019-09-23T13:07:46+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "6.4.1", @@ -1934,6 +2100,53 @@ ], "time": "2019-10-04T05:45:14+00:00" }, + { + "name": "lorenzo/pinky", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/lorenzo/pinky.git", + "reference": "2bc1a9d5696d6496df5d5682962929165a823e57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lorenzo/pinky/zipball/2bc1a9d5696d6496df5d5682962929165a823e57", + "reference": "2bc1a9d5696d6496df5d5682962929165a823e57", + "shasum": "" + }, + "require": { + "ext-xsl": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/pinky.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jose Lorenzo Rodriguez", + "email": "jose.zap@gmail.com" + } + ], + "description": "A Foundation for Emails (Inky) template transpiler", + "keywords": [ + "email", + "foundation", + "inky", + "template", + "zurb" + ], + "time": "2019-09-16T21:23:31+00:00" + }, { "name": "monolog/monolog", "version": "1.25.2", @@ -3973,6 +4186,59 @@ "homepage": "https://symfony.com", "time": "2019-11-13T07:39:40+00:00" }, + { + "name": "symfony/css-selector", + "version": "v4.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "64acec7e0d67125e9f4656c68d4a38a42ab5a0b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/64acec7e0d67125e9f4656c68d4a38a42ab5a0b7", + "reference": "64acec7e0d67125e9f4656c68d4a38a42ab5a0b7", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony CssSelector Component", + "homepage": "https://symfony.com", + "time": "2019-10-12T00:35:04+00:00" + }, { "name": "symfony/debug", "version": "v4.4.0", @@ -5255,6 +5521,74 @@ ], "time": "2019-10-12T00:35:04+00:00" }, + { + "name": "symfony/mailer", + "version": "v4.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "050b93ce9d307de9567908aa8ab8d6fa3b970921" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/050b93ce9d307de9567908aa8ab8d6fa3b970921", + "reference": "050b93ce9d307de9567908aa8ab8d6fa3b970921", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10", + "php": "^7.1.3", + "psr/log": "~1.0", + "symfony/event-dispatcher": "^4.3", + "symfony/mime": "^4.4|^5.0", + "symfony/service-contracts": "^1.1|^2" + }, + "conflict": { + "symfony/http-kernel": "<4.4", + "symfony/sendgrid-mailer": "<4.4" + }, + "require-dev": { + "symfony/amazon-mailer": "^4.4|^5.0", + "symfony/google-mailer": "^4.4|^5.0", + "symfony/http-client-contracts": "^1.1|^2", + "symfony/mailchimp-mailer": "^4.4|^5.0", + "symfony/mailgun-mailer": "^4.4|^5.0", + "symfony/messenger": "^4.4|^5.0", + "symfony/postmark-mailer": "^4.4|^5.0", + "symfony/sendgrid-mailer": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Mailer Component", + "homepage": "https://symfony.com", + "time": "2019-11-14T14:24:33+00:00" + }, { "name": "symfony/mime", "version": "v4.4.0", @@ -7427,6 +7761,109 @@ "homepage": "https://symfony.com", "time": "2019-11-12T14:51:11+00:00" }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "2.2.2", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "dda2ee426acd6d801d5b7fd1001cde9b5f790e15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/dda2ee426acd6d801d5b7fd1001cde9b5f790e15", + "reference": "dda2ee426acd6d801d5b7fd1001cde9b5f790e15", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^5.5 || ^7.0", + "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "time": "2019-10-24T08:53:34+00:00" + }, + { + "name": "twig/cssinliner-extra", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/cssinliner-extra.git", + "reference": "431402520025e077268de4978a4206e5fb8c0103" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/431402520025e077268de4978a4206e5fb8c0103", + "reference": "431402520025e077268de4978a4206e5fb8c0103", + "shasum": "" + }, + "require": { + "php": "^7.1.3", + "tijsverkoyen/css-to-inline-styles": "^2.0", + "twig/twig": "^2.4|^3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.12-dev" + } + }, + "autoload": { + "psr-4": { + "Twig\\Extra\\CssInliner\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension to allow inlining CSS", + "homepage": "https://twig.symfony.com", + "keywords": [ + "css", + "inlining", + "twig" + ], + "time": "2019-10-17T07:27:07+00:00" + }, { "name": "twig/extra-bundle", "version": "v3.0.0", @@ -7486,6 +7923,61 @@ ], "time": "2019-10-17T07:30:08+00:00" }, + { + "name": "twig/inky-extra", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/inky-extra.git", + "reference": "7e33cb931f29e8cbc1f68eafa30e0ca7f7c6ad3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/7e33cb931f29e8cbc1f68eafa30e0ca7f7c6ad3b", + "reference": "7e33cb931f29e8cbc1f68eafa30e0ca7f7c6ad3b", + "shasum": "" + }, + "require": { + "lorenzo/pinky": "^1.0.5", + "php": "^7.1.3", + "twig/twig": "^2.4|^3.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.12-dev" + } + }, + "autoload": { + "psr-4": { + "Twig\\Extra\\Inky\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + } + ], + "description": "A Twig extension for the inky email templating engine", + "homepage": "https://twig.symfony.com", + "keywords": [ + "email", + "emails", + "inky", + "twig" + ], + "time": "2019-10-17T07:27:12+00:00" + }, { "name": "twig/intl-extra", "version": "v3.0.0", @@ -8751,59 +9243,6 @@ "homepage": "https://symfony.com", "time": "2019-10-28T20:30:34+00:00" }, - { - "name": "symfony/css-selector", - "version": "v4.4.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/css-selector.git", - "reference": "64acec7e0d67125e9f4656c68d4a38a42ab5a0b7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/64acec7e0d67125e9f4656c68d4a38a42ab5a0b7", - "reference": "64acec7e0d67125e9f4656c68d4a38a42ab5a0b7", - "shasum": "" - }, - "require": { - "php": "^7.1.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\CssSelector\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Jean-François Simon", - "email": "jeanfrancois.simon@sensiolabs.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony CssSelector Component", - "homepage": "https://symfony.com", - "time": "2019-10-12T00:35:04+00:00" - }, { "name": "symfony/debug-bundle", "version": "v4.4.0", @@ -9399,6 +9838,7 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { + "gregwar/captcha-bundle": 20, "roave/security-advisories": 20 }, "prefer-stable": false, diff --git a/config/bundles.php b/config/bundles.php index 33b0cd5c..ea66ac31 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -20,4 +20,5 @@ return [ Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true] ]; diff --git a/config/packages/mailer.yaml b/config/packages/mailer.yaml new file mode 100644 index 00000000..56a650d8 --- /dev/null +++ b/config/packages/mailer.yaml @@ -0,0 +1,3 @@ +framework: + mailer: + dsn: '%env(MAILER_DSN)%' diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index db54b6eb..0f445e82 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -5,6 +5,9 @@ twig: form_themes: ['bootstrap_4_horizontal_layout.html.twig', 'Form/extendedBootstrap4_layout.html.twig', 'Form/permissionLayout.html.twig' ] exception_controller: ~ + paths: + '%kernel.project_dir%/assets/css': css + globals: partdb_title: '%partdb_title%' default_currency: '%default_currency%' diff --git a/config/services.yaml b/config/services.yaml index 911acdaf..cdf9a90e 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -17,6 +17,8 @@ parameters: # Allow users to download attachments to server. Warning: This can be dangerous, because via that feature attackers maybe can access ressources on your intranet! allow_attachments_downloads: false demo_mode: '%env(bool:DEMO_MODE)%' # If set to true, all potentially dangerous things are disabled (like changing passwords of the own user) + sender_email: 'noreply@partdb.changeme' + sender_name: 'Part-DB Mailer' services: # default configuration for services in *this* file @@ -36,6 +38,12 @@ services: resource: '../src/Controller' tags: ['controller.service_arguments'] + App\EventSubscriber\MailFromListener: + tags: ['kernel.event_subscriber'] + arguments: + $email: '%sender_email%' + $name: '%sender_name%' + Liip\ImagineBundle\Service\FilterService: alias: 'liip_imagine.service.filter' diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index b0753ceb..2731111a 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -21,12 +21,31 @@ namespace App\Controller; +use App\Services\PasswordResetManager; +use Doctrine\ORM\EntityManagerInterface; +use Gregwar\CaptchaBundle\Type\CaptchaType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\Core\Type\RepeatedType; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; +use Symfony\Contracts\Translation\TranslatorInterface; class SecurityController extends AbstractController { + protected $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + /** * @Route("/login", name="login", methods={"GET", "POST"}) */ @@ -44,6 +63,88 @@ class SecurityController extends AbstractController ]); } + /** + * @Route("/pw_reset/request", name="pw_reset_request") + */ + public function requestPwReset(PasswordResetManager $passwordReset, Request $request) + { + $builder = $this->createFormBuilder(); + $builder->add('user', TextType::class, [ + 'label' => $this->translator->trans('pw_reset.user_or_password'), + 'constraints' => [new NotBlank()] + ]); + $builder->add('captcha', CaptchaType::class, [ + 'width' => 200, + 'height' => 50, + 'length' => 6, + ]); + $builder->add('submit', SubmitType::class, [ + 'label' => 'pw_reset.submit' + ]); + + $form = $builder->getForm(); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $passwordReset->request($form->getData()['user']); + $this->addFlash('success', $this->translator->trans('pw_reset.request.success')); + //return $this->redirectToRoute('login'); + } + + return $this->render('security/pw_reset_request.html.twig', [ + 'form' => $form->createView() + ]); + } + + /** + * @Route("/pw_reset/new_pw/{user}/{token}", name="pw_reset_new_pw") + */ + public function pwResetNewPw(PasswordResetManager $passwordReset, Request $request, string $user = null, string $token = null) + { + $data = ['username' => $user, 'token' => $token]; + $builder = $this->createFormBuilder($data); + $builder->add('username', TextType::class, [ + 'label' => $this->translator->trans('pw_reset.username') + ]); + $builder->add('token', TextType::class, [ + 'label' => $this->translator->trans('pw_reset.token') + ]); + $builder->add('new_password', RepeatedType::class, [ + 'type' => PasswordType::class, + 'first_options' => ['label' => 'user.settings.pw_new.label'], + 'second_options' => ['label' => 'user.settings.pw_confirm.label'], + 'invalid_message' => 'password_must_match', + 'constraints' => [new Length([ + 'min' => 6, + 'max' => 128, + ])], + ]); + + $builder->add('submit', SubmitType::class, [ + 'label' => 'pw_reset.submit' + ]); + + $form = $builder->getForm(); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $data = $form->getData(); + //Try to set the new password + $success = $passwordReset->setNewPassword($data['username'], $data['token'], $data['new_password']); + if (!$success) { + $this->addFlash('error', $this->translator->trans('pw_reset.new_pw.error')); + } else { + $this->addFlash('success', $this->translator->trans('pw_reset.new_pw.success')); + return $this->redirectToRoute('login'); + } + } + + + return $this->render('security/pw_reset_new_pw.html.twig', [ + 'form' => $form->createView() + ]); + } + /** * @Route("/logout", name="logout") */ diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 012fb199..745a060d 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -394,6 +394,48 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe return $this; } + /** + * Returns the encrypted password reset token + * @return string|null + */ + public function getPwResetToken(): ?string + { + return $this->pw_reset_token; + } + + /** + * Sets the encrypted password reset token + * @param string|null $pw_reset_token + * @return User + */ + public function setPwResetToken(?string $pw_reset_token): User + { + $this->pw_reset_token = $pw_reset_token; + return $this; + } + + /** + * Gets the datetime when the password reset token expires + * @return \DateTime + */ + public function getPwResetExpires(): \DateTime + { + return $this->pw_reset_expires; + } + + /** + * Sets the datetime when the password reset token expires + * @param \DateTime $pw_reset_expires + * @return User + */ + public function setPwResetExpires(\DateTime $pw_reset_expires): User + { + $this->pw_reset_expires = $pw_reset_expires; + return $this; + } + + + /************************************************ * Getters ************************************************/ diff --git a/src/EventSubscriber/MailFromListener.php b/src/EventSubscriber/MailFromListener.php new file mode 100644 index 00000000..dfd54550 --- /dev/null +++ b/src/EventSubscriber/MailFromListener.php @@ -0,0 +1,58 @@ +email = $email; + $this->name = $name; + } + + public function onMessage(MessageEvent $event): void + { + $address = new Address($this->email, $this->name); + $event->getEnvelope()->setSender($address); + $email = $event->getMessage(); + if ($email instanceof Email) { + $email->from($address); + } + } + + public static function getSubscribedEvents() + { + return [ + // should be the last one to allow header changes by other listeners first + MessageEvent::class => ['onMessage'], + ]; + } +} \ No newline at end of file diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index afb89bc9..b80badfe 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -26,6 +26,7 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping; +use Doctrine\ORM\NonUniqueResultException; use Symfony\Bridge\Doctrine\RegistryInterface; /** @@ -44,7 +45,7 @@ class UserRepository extends EntityRepository * * @return User|null */ - public function getAnonymousUser() + public function getAnonymousUser() : ?User { if ($this->anonymous_user === null) { $this->anonymous_user = $this->findOneBy([ @@ -54,4 +55,29 @@ class UserRepository extends EntityRepository return $this->anonymous_user; } + + /** + * Find a user by its name or its email. Useful for login or password reset purposes. + * @param string $name_or_password The username or the email of the user that should be found + * @return User|null The user if it is existing, null if no one matched the criteria + */ + public function findByEmailOrName(string $name_or_password) : ?User + { + if (empty($name_or_password)) { + return null; + } + + $qb = $this->createQueryBuilder('u'); + $qb->select('u') + ->where('u.name = (:name)') + ->orWhere('u.email = (:email)'); + + $qb->setParameters(['email' => $name_or_password, 'name' => $name_or_password]); + + try { + return $qb->getQuery()->getOneOrNullResult(); + } catch (NonUniqueResultException $exception) { + return null; + } + } } diff --git a/src/Services/PasswordResetManager.php b/src/Services/PasswordResetManager.php new file mode 100644 index 00000000..d573b3d9 --- /dev/null +++ b/src/Services/PasswordResetManager.php @@ -0,0 +1,135 @@ +em = $em; + $this->mailer = $mailer; + /** @var PasswordEncoderInterface passwordEncoder */ + $this->passwordEncoder = $encoderFactory->getEncoder(User::class); + $this->translator = $translator; + $this->userPasswordEncoder = $userPasswordEncoder; + } + + public function request(string $name_or_email) : void + { + $repo = $this->em->getRepository(User::class); + + //Try to find a user by the given string + $user = $repo->findByEmailOrName($name_or_email); + //Do nothing if no user was found + if ($user === null) { + return; + } + + $unencrypted_token = md5(random_bytes(32)); + $user->setPwResetToken($this->passwordEncoder->encodePassword($unencrypted_token, null)); + + //Determine the expiration datetime of + $expiration_date = new \DateTime(); + $expiration_date->add(date_interval_create_from_date_string('1 day')); + $user->setPwResetExpires($expiration_date); + + if (!empty($user->getEmail())) { + $address = new Address($user->getEmail(), $user->getFullName()); + $mail = new TemplatedEmail(); + $mail->to($address); + $mail->subject($this->translator->trans('pw_reset.email.subject')); + $mail->htmlTemplate("mail/pw_reset.html.twig"); + $mail->context([ + 'expiration_date' => $expiration_date, + 'token' => $unencrypted_token, + 'user' => $user + ]); + + //Send email + $this->mailer->send($mail); + } + + //Save changes to DB + $this->em->flush(); + } + + /** + * Sets the new password of the user with the given name, if the token is valid. + * @param string $user The name of the user, which password should be reset + * @param string $token The token that should be used to reset the password + * @param string $new_password The new password that should be applied to user + * @return bool Returns true, if the new password was applied. False, if either the username is unknown or the + * token is invalid or expired. + */ + public function setNewPassword(string $user, string $token, string $new_password) : bool + { + //Try to find the user + $repo = $this->em->getRepository(User::class); + /** @var User $user */ + $user = $repo->findOneBy(['name' => $user]); + + //If no user matching the name, show an error message + if ($user === null) { + return false; + } + + //Check if token is expired yet + if ($user->getPwResetExpires() < new \DateTime()) { + return false; + } + + //Check if token is valid + if (!$this->passwordEncoder->isPasswordValid($user->getPwResetToken(), $token, null)) { + return false; + } + + //When everything was valid, apply the new password + $user->setPassword($this->userPasswordEncoder->encodePassword($user, $new_password)); + + //Remove token + $user->setPwResetToken(null); + $user->setPwResetExpires(new \DateTime()); + + //Save to DB + $this->em->flush(); + return true; + } +} \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index f7971d5a..b48847d4 100644 --- a/symfony.lock +++ b/symfony.lock @@ -129,6 +129,9 @@ "./config/packages/dev/easy_log_handler.yaml" ] }, + "egulias/email-validator": { + "version": "2.1.11" + }, "felixfbecker/advanced-json-rpc": { "version": "v3.0.4" }, @@ -153,6 +156,12 @@ "./config/packages/fos_ckeditor.yaml" ] }, + "gregwar/captcha": { + "version": "v1.1.7" + }, + "gregwar/captcha-bundle": { + "version": "v2.0.6" + }, "guzzlehttp/guzzle": { "version": "6.3.3" }, @@ -184,6 +193,9 @@ "./config/routes/liip_imagine.yaml" ] }, + "lorenzo/pinky": { + "version": "1.0.5" + }, "monolog/monolog": { "version": "1.24.0" }, @@ -465,6 +477,18 @@ "symfony/intl": { "version": "v4.2.3" }, + "symfony/mailer": { + "version": "4.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "master", + "version": "4.3", + "ref": "15658c2a0176cda2e7dba66276a2030b52bd81b2" + }, + "files": [ + "./config/packages/mailer.yaml" + ] + }, "symfony/maker-bundle": { "version": "1.0", "recipe": { @@ -688,9 +712,18 @@ "symfony/yaml": { "version": "v4.2.3" }, + "tijsverkoyen/css-to-inline-styles": { + "version": "2.2.2" + }, + "twig/cssinliner-extra": { + "version": "v3.0.0" + }, "twig/extra-bundle": { "version": "v3.0.0" }, + "twig/inky-extra": { + "version": "v3.0.0" + }, "twig/intl-extra": { "version": "3.x-dev" }, diff --git a/templates/mail/base.html.twig b/templates/mail/base.html.twig new file mode 100644 index 00000000..666a03f7 --- /dev/null +++ b/templates/mail/base.html.twig @@ -0,0 +1,30 @@ +{% apply inky_to_html|inline_css(source('@css/foundation-emails.css'), source('@css/email.css')) %} + + + + + + +

{{ partdb_title }}

+
+
+ + + + + + {% block content %} + + {% endblock %} + + + + + +

This email was send automatically by Part-DB. Dont answer to this email.

+
+
+ + +
+{% endapply %} \ No newline at end of file diff --git a/templates/mail/pw_reset.html.twig b/templates/mail/pw_reset.html.twig new file mode 100644 index 00000000..b6ddecf9 --- /dev/null +++ b/templates/mail/pw_reset.html.twig @@ -0,0 +1,30 @@ +{% extends "mail/base.html.twig" %} + +{% block content %} + + +

Hi {{ user.fullName }},

+ somebody (hopefully you) requested an reset of your password. + If this request was not made by you, ignore this email. +
+ +
+ If this dont work for you. Go to {{ url('pw_reset_new_pw') }} + and enter the following info: + + + +

+ Username: {{ user.name }} +

+

+ Token: {{ token }} +

+
+
+
+ The reset token will be valid until {{ expiration_date|format_datetime }}. +
+
+ +{% endblock %} \ No newline at end of file diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index 8abd0c25..10602265 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -59,4 +59,6 @@ + + {% trans %}pw_reset.password_forget{% endtrans %} {% endblock %} \ No newline at end of file diff --git a/templates/security/pw_reset_new_pw.html.twig b/templates/security/pw_reset_new_pw.html.twig new file mode 100644 index 00000000..9f199081 --- /dev/null +++ b/templates/security/pw_reset_new_pw.html.twig @@ -0,0 +1,11 @@ +{% extends "main_card.html.twig" %} + +{% block card_title %} + + {% trans %}pw_reset.new_pw.header.title{% endtrans %} +{% endblock %} + +{% block card_content %} + {{ form_start(form) }} + {{ form_end(form) }} +{% endblock %} \ No newline at end of file diff --git a/templates/security/pw_reset_request.html.twig b/templates/security/pw_reset_request.html.twig new file mode 100644 index 00000000..af8db2d2 --- /dev/null +++ b/templates/security/pw_reset_request.html.twig @@ -0,0 +1,11 @@ +{% extends "main_card.html.twig" %} + +{% block card_title %} + + {% trans %}pw_reset.request.header.title{% endtrans %} +{% endblock %} + +{% block card_content %} + {{ form_start(form) }} + {{ form_end(form) }} +{% endblock %} \ No newline at end of file