This commit is contained in:
Martin Kleinschrodt 2021-10-16 14:58:10 +02:00
parent ab258892ea
commit 96c0e69cee
24 changed files with 1859 additions and 445 deletions

View File

@ -0,0 +1,356 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Cutom Font Embed CSS -->
<link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700" rel="stylesheet" type="text/css" />
<title>Pure VPN</title>
</head>
<body
leftmargin="0"
marginwidth="0"
topmargin="0"
marginheight="0"
offset="0"
style="
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background-color: #ffffff;
height: 100% !important;
width: 100% !important;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
margin: auto;
max-width: 600px;
font-family: Verdana, Geneva, sans-serif;
color: #2b2b2b;
font-size: 14px;
line-height: 1.4;
background: url('https://images.purevpn-tools.com/public/images/2791_eml_pur_bg_dull.png') #fff
no-repeat top;
background-size: contain;
"
>
<tr>
<!-- Spacing -->
<td style="height: 48px"></td>
</tr>
<tr>
<td style="text-align: center">
<img src="https://images.purevpn-tools.com/public/images/2791_eml_pure_logo.png" alt="logo" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 52px"></td>
</tr>
<tr>
<td>
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="max-width: 450px; margin: auto; padding: 0 15px"
>
<tr>
<td style="color: #000; font-size: 22px; font-weight: bold; line-height: 34px">
Hi there!
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 16px; font-weight: normal; line-height: 24px">
You have been asked by {{ invitedBy }} to confirm your membership with the PurePass
organization <strong>{{ orgName }}</strong>.
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 30px"></td>
</tr>
<tr>
<td>
<a href="{{ acceptInviteUrl }}"
><button
style="
align-items: center;
padding: 23px 50px;
height: 60px;
background: #28c675;
border-radius: 50px;
cursor: pointer;
border: 0;
font-weight: bold;
font-size: 14px;
line-height: 100%;
text-align: center;
color: #ffffff;
"
>
Confirm Membership
</button></a
>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 40px"></td>
</tr>
<tr>
<td style="border-bottom: 2px dashed #d9d3de"></td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 40px"></td>
</tr>
<tr>
<td
style="
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #78747b;
"
>
Have a great day! we hope youll have a great experience with us! <br />
<br />
Got a question? Get in touch with via 24/7 Live Chat or email us at help@purevpn.com.
</td>
</tr>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 80px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background: #fbfbfc">
<tr>
<td>
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="background: #fbfbfc; max-width: 450px; margin: auto; padding: 0 15px"
>
<tr>
<!-- Spacing -->
<td style="height: 30px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td style="max-width: 50%">
<img
src="https://images.purevpn-tools.com/public/images/2791_eml_pure_btm_logo.png"
alt="bottom_logo"
/>
</td>
<td style="max-width: 50%">
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
align="right"
style="text-align: center"
>
<tr>
<td>
<a href="https://www.facebook.com/PureVPNcom"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_fb_icon.png"
alt="facebook"
/></a>
</td>
<td>
<a href="https://twitter.com/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_twtr_icon.png"
alt="twitter"
/></a>
</td>
<td>
<a href="https://www.linkedin.com/company/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_lkndin_icon.png"
alt="linked in"
/></a>
</td>
<td>
<a href="https://www.youtube.com/user/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_ytube_icon.png"
alt="youtube"
/></a>
</td>
</tr>
</table>
</td>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<hr style="border: 0; border-bottom: 1px solid #eae6ed" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">Download PureVPN on your:</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<a
href="https://purevpn-dialer-assets.s3.amazonaws.com/windows/app/purevpn_setup.exe"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Windows</a
>
<a
href="https://purevpn-dialer-assets.s3.amazonaws.com/mac/app/purevpn_setup.pkg"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Mac</a
>
<a
href="https://play.google.com/store/apps/details?id=com.gaditek.purevpnics&hl=en"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Android</a
>
<a
href="https://itunes.apple.com/us/app/purevpn-complete-online-privacy/id594506418?mt=8"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>iOS</a
>
<a
href="https://chrome.google.com/webstore/detail/purevpn-free-vpn-proxy-un/bfidboloedlamgdmenmlbipfnccokknp"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Chrome</a
>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/purevpn-for-privacy-security/"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
"
>Firefox</a
>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<hr style="border: 0; border-bottom: 1px solid #eae6ed" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 20px"></td>
</tr>
<tr>
<td style="color: #000; font-size: 11px; font-weight: bold">Got Questions?</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 5px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">
Contact to our
<a
href="https://secure.livechatinc.com/licence/4454601/open_chat.cgi?utm_source=Expired&utm_medium=Email&utm_campaign=360%20is%20Coming%20Live%20Chat"
style="color: #2c8fdc; text-decoration: none"
>
24x7 Live Chat!</a
>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 15px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">
@ 2007 - 2020 PureVPN All Rights Reserved
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 35px"></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,236 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>${title}</title>
<style>
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class="body"] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class="body"] p,
table[class="body"] ul,
table[class="body"] ol,
table[class="body"] td,
table[class="body"] span,
table[class="body"] a {
font-size: 14px !important;
}
table[class="body"] .wrapper,
table[class="body"] .article {
padding: 10px !important;
}
table[class="body"] .content {
padding: 0 !important;
}
table[class="body"] .container {
padding: 0 !important;
width: 100% !important;
}
table[class="body"] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class="body"] .btn table {
width: 100% !important;
}
table[class="body"] .btn a {
width: 100% !important;
}
table[class="body"] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: #3498db !important;
}
.btn-primary a:hover {
background-color: #3498db !important;
border-color: #3498db !important;
}
}
</style>
</head>
<body
class=""
style="
background-color: #f6f6f6;
font-family: sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 14px;
line-height: 1.4;
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
class="body"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
background-color: #f6f6f6;
"
>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top">&nbsp;</td>
<td
class="container"
style="
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
display: block;
margin: 0 auto;
max-width: 580px;
padding: 10px;
width: 580px;
"
>
<div
class="content"
style="box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px"
>
<!-- START CENTERED WHITE CONTAINER -->
<span
class="preheader"
style="
color: transparent;
display: none;
height: 0;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
mso-hide: all;
visibility: hidden;
width: 0;
"
>${preview}</span
>
<table
class="main"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
background: #ffffff;
border-radius: 5px;
"
>
<!-- START MAIN CONTENT AREA -->
<tr>
<td
class="wrapper"
style="
font-family: sans-serif;
font-size: 14px;
vertical-align: top;
box-sizing: border-box;
padding: 20px;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
>
<tr>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top">
${content}
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; margin-top: 10px; text-align: center; width: 100%">
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
border-collapse: separate;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
width: 100%;
"
>
<tr>
<td
class="content-block"
style="
font-family: sans-serif;
vertical-align: top;
padding-bottom: 10px;
padding-top: 10px;
font-size: 12px;
color: #999999;
text-align: center;
"
>
<span
class="apple-link"
style="color: #999999; font-size: 12px; text-align: center"
>${address}</span
>
<!--<br> Don't like these emails? <a href="" style="text-decoration: underline; color: #999999; font-size: 12px; text-align: center;">Unsubscribe</a>.-->
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top">&nbsp;</td>
</tr>
</table>
</body>
</html>

7
assets/email/error.html Normal file
View File

@ -0,0 +1,7 @@
<p>The following error occurred at {{ time }}:</p>
<p>
Code: {{ code }} <br />
Message: {{ message }} <br />
Event ID: {{ eventId }}`
</p>

View File

@ -0,0 +1,356 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Cutom Font Embed CSS -->
<link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700" rel="stylesheet" type="text/css" />
<title>Pure VPN</title>
</head>
<body
leftmargin="0"
marginwidth="0"
topmargin="0"
marginheight="0"
offset="0"
style="
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background-color: #ffffff;
height: 100% !important;
width: 100% !important;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
margin: auto;
max-width: 600px;
font-family: Verdana, Geneva, sans-serif;
color: #2b2b2b;
font-size: 14px;
line-height: 1.4;
background: url('https://images.purevpn-tools.com/public/images/2791_eml_pur_bg_dull.png') #fff
no-repeat top;
background-size: contain;
"
>
<tr>
<!-- Spacing -->
<td style="height: 48px"></td>
</tr>
<tr>
<td style="text-align: center">
<img src="https://images.purevpn-tools.com/public/images/2791_eml_pure_logo.png" alt="logo" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 52px"></td>
</tr>
<tr>
<td>
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="max-width: 450px; margin: auto; padding: 0 15px"
>
<tr>
<td style="color: #000; font-size: 22px; font-weight: bold; line-height: 34px">
Hi there!
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 16px; font-weight: normal; line-height: 24px">
Good news! {{ invitee }} has accepted your invite to join
<strong>{{ orgName }}</strong>. Please confirm their membership to complete the process!
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 30px"></td>
</tr>
<tr>
<td>
<a href="{{ confirmMemberUrl }}"
><button
style="
align-items: center;
padding: 23px 50px;
height: 60px;
background: #28c675;
border-radius: 50px;
cursor: pointer;
border: 0;
font-weight: bold;
font-size: 14px;
line-height: 100%;
text-align: center;
color: #ffffff;
"
>
Confirm Membership
</button></a
>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 40px"></td>
</tr>
<tr>
<td style="border-bottom: 2px dashed #d9d3de"></td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 40px"></td>
</tr>
<tr>
<td
style="
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #78747b;
"
>
Have a great day! we hope youll have a great experience with us! <br />
<br />
Got a question? Get in touch with via 24/7 Live Chat or email us at help@purevpn.com.
</td>
</tr>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 80px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background: #fbfbfc">
<tr>
<td>
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="background: #fbfbfc; max-width: 450px; margin: auto; padding: 0 15px"
>
<tr>
<!-- Spacing -->
<td style="height: 30px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td style="max-width: 50%">
<img
src="https://images.purevpn-tools.com/public/images/2791_eml_pure_btm_logo.png"
alt="bottom_logo"
/>
</td>
<td style="max-width: 50%">
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
align="right"
style="text-align: center"
>
<tr>
<td>
<a href="https://www.facebook.com/PureVPNcom"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_fb_icon.png"
alt="facebook"
/></a>
</td>
<td>
<a href="https://twitter.com/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_twtr_icon.png"
alt="twitter"
/></a>
</td>
<td>
<a href="https://www.linkedin.com/company/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_lkndin_icon.png"
alt="linked in"
/></a>
</td>
<td>
<a href="https://www.youtube.com/user/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_ytube_icon.png"
alt="youtube"
/></a>
</td>
</tr>
</table>
</td>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<hr style="border: 0; border-bottom: 1px solid #eae6ed" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">Download PureVPN on your:</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<a
href="https://purevpn-dialer-assets.s3.amazonaws.com/windows/app/purevpn_setup.exe"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Windows</a
>
<a
href="https://purevpn-dialer-assets.s3.amazonaws.com/mac/app/purevpn_setup.pkg"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Mac</a
>
<a
href="https://play.google.com/store/apps/details?id=com.gaditek.purevpnics&hl=en"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Android</a
>
<a
href="https://itunes.apple.com/us/app/purevpn-complete-online-privacy/id594506418?mt=8"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>iOS</a
>
<a
href="https://chrome.google.com/webstore/detail/purevpn-free-vpn-proxy-un/bfidboloedlamgdmenmlbipfnccokknp"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Chrome</a
>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/purevpn-for-privacy-security/"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
"
>Firefox</a
>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<hr style="border: 0; border-bottom: 1px solid #eae6ed" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 20px"></td>
</tr>
<tr>
<td style="color: #000; font-size: 11px; font-weight: bold">Got Questions?</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 5px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">
Contact to our
<a
href="https://secure.livechatinc.com/licence/4454601/open_chat.cgi?utm_source=Expired&utm_medium=Email&utm_campaign=360%20is%20Coming%20Live%20Chat"
style="color: #2c8fdc; text-decoration: none"
>
24x7 Live Chat!</a
>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 15px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">
@ 2007 - 2020 PureVPN All Rights Reserved
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 35px"></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,356 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Cutom Font Embed CSS -->
<link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700" rel="stylesheet" type="text/css" />
<title>Pure VPN</title>
</head>
<body
leftmargin="0"
marginwidth="0"
topmargin="0"
marginheight="0"
offset="0"
style="
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background-color: #ffffff;
height: 100% !important;
width: 100% !important;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
margin: auto;
max-width: 600px;
font-family: Verdana, Geneva, sans-serif;
color: #2b2b2b;
font-size: 14px;
line-height: 1.4;
background: url('https://images.purevpn-tools.com/public/images/2791_eml_pur_bg_dull.png') #fff
no-repeat top;
background-size: contain;
"
>
<tr>
<!-- Spacing -->
<td style="height: 48px"></td>
</tr>
<tr>
<td style="text-align: center">
<img src="https://images.purevpn-tools.com/public/images/2791_eml_pure_logo.png" alt="logo" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 52px"></td>
</tr>
<tr>
<td>
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="max-width: 450px; margin: auto; padding: 0 15px"
>
<tr>
<td style="color: #000; font-size: 22px; font-weight: bold; line-height: 34px">
Hi there!
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 16px; font-weight: normal; line-height: 24px">
Good news! You now have access to the PurePass organization
<strong>{{ orgName }}</strong>.
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 30px"></td>
</tr>
<tr>
<td>
<a href="{{ openAppUrl }}"
><button
style="
align-items: center;
padding: 23px 50px;
height: 60px;
background: #28c675;
border-radius: 50px;
cursor: pointer;
border: 0;
font-weight: bold;
font-size: 14px;
line-height: 100%;
text-align: center;
color: #ffffff;
"
>
Open PurePass
</button></a
>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 40px"></td>
</tr>
<tr>
<td style="border-bottom: 2px dashed #d9d3de"></td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 40px"></td>
</tr>
<tr>
<td
style="
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #78747b;
"
>
Have a great day! we hope youll have a great experience with us! <br />
<br />
Got a question? Get in touch with via 24/7 Live Chat or email us at help@purevpn.com.
</td>
</tr>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 80px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background: #fbfbfc">
<tr>
<td>
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="background: #fbfbfc; max-width: 450px; margin: auto; padding: 0 15px"
>
<tr>
<!-- Spacing -->
<td style="height: 30px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td style="max-width: 50%">
<img
src="https://images.purevpn-tools.com/public/images/2791_eml_pure_btm_logo.png"
alt="bottom_logo"
/>
</td>
<td style="max-width: 50%">
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
align="right"
style="text-align: center"
>
<tr>
<td>
<a href="https://www.facebook.com/PureVPNcom"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_fb_icon.png"
alt="facebook"
/></a>
</td>
<td>
<a href="https://twitter.com/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_twtr_icon.png"
alt="twitter"
/></a>
</td>
<td>
<a href="https://www.linkedin.com/company/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_lkndin_icon.png"
alt="linked in"
/></a>
</td>
<td>
<a href="https://www.youtube.com/user/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_ytube_icon.png"
alt="youtube"
/></a>
</td>
</tr>
</table>
</td>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<hr style="border: 0; border-bottom: 1px solid #eae6ed" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">Download PureVPN on your:</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<a
href="https://purevpn-dialer-assets.s3.amazonaws.com/windows/app/purevpn_setup.exe"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Windows</a
>
<a
href="https://purevpn-dialer-assets.s3.amazonaws.com/mac/app/purevpn_setup.pkg"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Mac</a
>
<a
href="https://play.google.com/store/apps/details?id=com.gaditek.purevpnics&hl=en"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Android</a
>
<a
href="https://itunes.apple.com/us/app/purevpn-complete-online-privacy/id594506418?mt=8"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>iOS</a
>
<a
href="https://chrome.google.com/webstore/detail/purevpn-free-vpn-proxy-un/bfidboloedlamgdmenmlbipfnccokknp"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Chrome</a
>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/purevpn-for-privacy-security/"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
"
>Firefox</a
>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<hr style="border: 0; border-bottom: 1px solid #eae6ed" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 20px"></td>
</tr>
<tr>
<td style="color: #000; font-size: 11px; font-weight: bold">Got Questions?</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 5px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">
Contact to our
<a
href="https://secure.livechatinc.com/licence/4454601/open_chat.cgi?utm_source=Expired&utm_medium=Email&utm_campaign=360%20is%20Coming%20Live%20Chat"
style="color: #2c8fdc; text-decoration: none"
>
24x7 Live Chat!</a
>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 15px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">
@ 2007 - 2020 PureVPN All Rights Reserved
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 35px"></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,356 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Cutom Font Embed CSS -->
<link href="http://fonts.googleapis.com/css?family=Open+Sans:400,600,700" rel="stylesheet" type="text/css" />
<title>Pure VPN</title>
</head>
<body
leftmargin="0"
marginwidth="0"
topmargin="0"
marginheight="0"
offset="0"
style="
margin: 0;
padding: 0;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
background-color: #ffffff;
height: 100% !important;
width: 100% !important;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
style="
width: 100%;
margin: auto;
max-width: 600px;
font-family: Verdana, Geneva, sans-serif;
color: #2b2b2b;
font-size: 14px;
line-height: 1.4;
background: url('https://images.purevpn-tools.com/public/images/2791_eml_pur_bg_dull.png') #fff
no-repeat top;
background-size: contain;
"
>
<tr>
<!-- Spacing -->
<td style="height: 48px"></td>
</tr>
<tr>
<td style="text-align: center">
<img src="https://images.purevpn-tools.com/public/images/2791_eml_pure_logo.png" alt="logo" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 52px"></td>
</tr>
<tr>
<td>
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="max-width: 450px; margin: auto; padding: 0 15px"
>
<tr>
<td style="color: #000; font-size: 22px; font-weight: bold; line-height: 34px">
Hi there!
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 16px; font-weight: normal; line-height: 24px">
You have been invited by {{ invitedBy }} to join their PurePass organization
<strong>{{ orgName }}</strong>!
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 30px"></td>
</tr>
<tr>
<td>
<a href="{{ acceptInviteUrl }}"
><button
style="
align-items: center;
padding: 23px 50px;
height: 60px;
background: #28c675;
border-radius: 50px;
cursor: pointer;
border: 0;
font-weight: bold;
font-size: 14px;
line-height: 100%;
text-align: center;
color: #ffffff;
"
>
Join {{ orgName }}
</button></a
>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 40px"></td>
</tr>
<tr>
<td style="border-bottom: 2px dashed #d9d3de"></td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 40px"></td>
</tr>
<tr>
<td
style="
font-style: normal;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #78747b;
"
>
Have a great day! we hope youll have a great experience with us! <br />
<br />
Got a question? Get in touch with via 24/7 Live Chat or email us at help@purevpn.com.
</td>
</tr>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 80px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background: #fbfbfc">
<tr>
<td>
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
style="background: #fbfbfc; max-width: 450px; margin: auto; padding: 0 15px"
>
<tr>
<!-- Spacing -->
<td style="height: 30px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<td style="max-width: 50%">
<img
src="https://images.purevpn-tools.com/public/images/2791_eml_pure_btm_logo.png"
alt="bottom_logo"
/>
</td>
<td style="max-width: 50%">
<table
border="0"
cellpadding="0"
cellspacing="0"
width="100%"
align="right"
style="text-align: center"
>
<tr>
<td>
<a href="https://www.facebook.com/PureVPNcom"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_fb_icon.png"
alt="facebook"
/></a>
</td>
<td>
<a href="https://twitter.com/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_twtr_icon.png"
alt="twitter"
/></a>
</td>
<td>
<a href="https://www.linkedin.com/company/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_lkndin_icon.png"
alt="linked in"
/></a>
</td>
<td>
<a href="https://www.youtube.com/user/purevpn"
><img
src="https://images.purevpn-tools.com/public/images/2791_eml_ytube_icon.png"
alt="youtube"
/></a>
</td>
</tr>
</table>
</td>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<hr style="border: 0; border-bottom: 1px solid #eae6ed" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">Download PureVPN on your:</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0">
<tr>
<td>
<a
href="https://purevpn-dialer-assets.s3.amazonaws.com/windows/app/purevpn_setup.exe"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Windows</a
>
<a
href="https://purevpn-dialer-assets.s3.amazonaws.com/mac/app/purevpn_setup.pkg"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Mac</a
>
<a
href="https://play.google.com/store/apps/details?id=com.gaditek.purevpnics&hl=en"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Android</a
>
<a
href="https://itunes.apple.com/us/app/purevpn-complete-online-privacy/id594506418?mt=8"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>iOS</a
>
<a
href="https://chrome.google.com/webstore/detail/purevpn-free-vpn-proxy-un/bfidboloedlamgdmenmlbipfnccokknp"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
border-right: 1px solid #78747b;
padding-right: 5px;
"
>Chrome</a
>
<a
href="https://addons.mozilla.org/en-US/firefox/addon/purevpn-for-privacy-security/"
style="
text-decoration: none;
color: #2c8fdc;
font-size: 11px;
"
>Firefox</a
>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 10px"></td>
</tr>
<tr>
<td>
<hr style="border: 0; border-bottom: 1px solid #eae6ed" />
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 20px"></td>
</tr>
<tr>
<td style="color: #000; font-size: 11px; font-weight: bold">Got Questions?</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 5px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">
Contact to our
<a
href="https://secure.livechatinc.com/licence/4454601/open_chat.cgi?utm_source=Expired&utm_medium=Email&utm_campaign=360%20is%20Coming%20Live%20Chat"
style="color: #2c8fdc; text-decoration: none"
>
24x7 Live Chat!</a
>
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 15px"></td>
</tr>
<tr>
<td style="color: #78747b; font-size: 11px">
@ 2007 - 2020 PureVPN All Rights Reserved
</td>
</tr>
<tr>
<!-- Spacing -->
<td style="height: 35px"></td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -172,7 +172,7 @@ body {
--start-background: var(--color-background-dark);
--start-logo-height: 4em;
--start-logo-width: auto;
--start-form-shadow: rgb(0 0 0 / 10%) 0px 0px 2em -0.5em;
--start-form-shadow: rgb(0 0 0 / 10%) 0px 0px 2em -1em;
--start-form-background: var(--color-background);
/* MENU */

View File

@ -137,9 +137,8 @@ export class InviteRecipient extends Routing(StateMixin(LitElement)) {
<div class="stretch large padded">${$l("Invite")}</div>
<div class="small tag ${status.class}">
<pl-icon icon="${status.icon}"></pl-icon>
<div>${status.text}</div>
<pl-icon icon="${status.icon}" class="inline"></pl-icon>
${status.text}
</div>
</header>
@ -156,7 +155,7 @@ export class InviteRecipient extends Routing(StateMixin(LitElement)) {
<div class="bold big margined centering layout">
<div class="tag highlight">
<pl-icon icon="members"></pl-icon>
<pl-icon icon="members" class="large"></pl-icon>
<div>${this._invite.org.name}</div>
</div>
</div>

View File

@ -169,9 +169,8 @@ export class InviteView extends Routing(StateMixin(LitElement)) {
<div class="stretch large padded">${$l("Invite")}</div>
<div class="small tag ${status.class}">
<pl-icon icon="${status.icon}"></pl-icon>
<div>${status.text}</div>
<pl-icon icon="${status.icon}" class="inline"></pl-icon>
${status.text}
</div>
</header>

View File

@ -176,7 +176,7 @@ export class InviteView extends Routing(StateMixin(LitElement)) {
<ptc-scroller class="stretch">
<div class="tags">
<div class="tag ${status.class}">
<pl-icon icon="${status.icon}"></pl-icon>
<pl-icon icon="${status.icon}" class="inline"></pl-icon>
<div>${status.text}</div>
</div>

View File

@ -148,10 +148,10 @@ export class MemberView extends Routing(StateMixin(LitElement)) {
vaults: [...this._vaults],
groups: [...this._groups],
});
this._saveButton.success();
this._saveButton?.success();
this.requestUpdate();
} catch (e) {
this._saveButton.fail();
this._saveButton?.fail();
alert(e.message || $l("Something went wrong. Please try again later!"), { type: "warning" });
throw e;
}

View File

@ -95,6 +95,7 @@ export const reset = css`
vertical-align: baseline;
background: none;
-webkit-tap-highlight-color: transparent;
scrollbar-width: thin;
}
ol,

View File

@ -1,6 +1,6 @@
import { Auth, Authenticator, AuthenticatorStatus, AuthRequest, AuthServer, AuthType } from "../auth";
import { Messenger } from "../messenger";
import { EmailAuthMessage } from "../messages/mfa";
import { EmailAuthMessage } from "../messenger";
import { ErrorCode, Err } from "../error";
import { randomNumber } from "../util";
@ -37,7 +37,7 @@ export class EmailAuthServer implements AuthServer {
email,
verificationCode,
};
this.messenger.send(authenticator.state.email, new EmailAuthMessage(verificationCode));
this.messenger.send(authenticator.state.email, new EmailAuthMessage({ code: verificationCode }));
return { email };
}

View File

@ -1,177 +0,0 @@
export const fontFamily = "sans-serif";
export const fontSizeSmall = "12px";
export const fontSize = "14px";
export const fontSizeBig = "16px";
export const colorText = "#444444";
export const colorHighlight = "#3498db";
export const colorHover = "#3498db";
export const colorBackground = "#f6f6f6";
export const colorFooter = "#999999";
const footer = process.env.PL_EMAIL_FOOTER;
export function paragraph(content: string, styles = "") {
return `
<p style="font-family: ${fontFamily}; font-size: ${fontSize}; font-weight: normal; margin: 0; Margin-bottom: 15px; ${styles}">${content}</p>
`;
}
export function button(content: string, url: string) {
return `
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
<tbody>
<tr>
<td align="left" style="font-family: ${fontFamily}; font-size: ${fontSize}; vertical-align: top; padding-bottom: 15px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: auto;">
<tbody>
<tr>
<td style="font-family: ${fontFamily}; font-size: ${fontSize}; vertical-align: top; background-color: ${colorHighlight}; border-radius: 5px; text-align: center;"> <a href="${url}" target="_blank" style="display: inline-block; color: #ffffff; background-color: ${colorHighlight}; border: solid 1px ${colorHighlight}; border-radius: 5px; box-sizing: border-box; cursor: pointer; text-decoration: none; font-size: ${fontSize}; font-weight: bold; margin: 0; padding: 12px 25px; text-transform: capitalize; border-color: ${colorHighlight};">${content}</a> </td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
`;
}
export function base(content: string, preview = "", title = "") {
return `
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>${title}</title>
<style>
/* -------------------------------------
RESPONSIVE AND MOBILE FRIENDLY STYLES
------------------------------------- */
@media only screen and (max-width: 620px) {
table[class=body] h1 {
font-size: 28px !important;
margin-bottom: 10px !important;
}
table[class=body] p,
table[class=body] ul,
table[class=body] ol,
table[class=body] td,
table[class=body] span,
table[class=body] a {
font-size: ${fontSizeBig} !important;
}
table[class=body] .wrapper,
table[class=body] .article {
padding: 10px !important;
}
table[class=body] .content {
padding: 0 !important;
}
table[class=body] .container {
padding: 0 !important;
width: 100% !important;
}
table[class=body] .main {
border-left-width: 0 !important;
border-radius: 0 !important;
border-right-width: 0 !important;
}
table[class=body] .btn table {
width: 100% !important;
}
table[class=body] .btn a {
width: 100% !important;
}
table[class=body] .img-responsive {
height: auto !important;
max-width: 100% !important;
width: auto !important;
}
}
/* -------------------------------------
PRESERVE THESE STYLES IN THE HEAD
------------------------------------- */
@media all {
.ExternalClass {
width: 100%;
}
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
.apple-link a {
color: inherit !important;
font-family: inherit !important;
font-size: inherit !important;
font-weight: inherit !important;
line-height: inherit !important;
text-decoration: none !important;
}
.btn-primary table td:hover {
background-color: ${colorHover} !important;
}
.btn-primary a:hover {
background-color: ${colorHover} !important;
border-color: ${colorHover} !important;
}
}
</style>
</head>
<body class="" style="background-color: ${colorBackground}; font-family: ${fontFamily}; -webkit-font-smoothing: antialiased; font-size: ${fontSize}; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: ${colorBackground};">
<tr>
<td style="font-family: ${fontFamily}; font-size: ${fontSize}; vertical-align: top;">&nbsp;</td>
<td class="container" style="font-family: ${fontFamily}; font-size: ${fontSize}; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
<!-- START CENTERED WHITE CONTAINER -->
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">${preview}</span>
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 5px;">
<!-- START MAIN CONTENT AREA -->
<tr>
<td class="wrapper" style="font-family: ${fontFamily}; font-size: ${fontSize}; vertical-align: top; box-sizing: border-box; padding: 20px;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td style="font-family: ${fontFamily}; font-size: ${fontSize}; vertical-align: top;">
${content}
</td>
</tr>
</table>
</td>
</tr>
<!-- END MAIN CONTENT AREA -->
</table>
<!-- START FOOTER -->
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
<tr>
<td class="content-block" style="font-family: ${fontFamily}; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: ${fontSizeSmall}; color: ${colorFooter}; text-align: center;">
<span class="apple-link" style="color: ${colorFooter}; font-size: ${fontSizeSmall}; text-align: center;">${footer}</span>
<!--<br> Don't like these emails? <a href="" style="text-decoration: underline; color: ${colorFooter}; font-size: ${fontSizeSmall}; text-align: center;">Unsubscribe</a>.-->
</td>
</tr>
</table>
</div>
<!-- END FOOTER -->
<!-- END CENTERED WHITE CONTAINER -->
</div>
</td>
<td style="font-family: ${fontFamily}; font-size: ${fontSize}; vertical-align: top;">&nbsp;</td>
</tr>
</table>
</body>
</html>
`;
}

View File

@ -1,5 +0,0 @@
// export { LoginMessage } from "./login";
export { InviteCreatedMessage } from "./invite-created";
export { InviteAcceptedMessage } from "./invite-accepted";
export { MemberAddedMessage } from "./member-added";
export { EmailAuthMessage } from "./mfa";

View File

@ -1,45 +0,0 @@
import { Invite } from "../invite";
import { Message } from "../messenger";
import { base as baseHTML, paragraph as p, button } from "./base-html";
export class InviteAcceptedMessage implements Message {
constructor(private invite: Invite, private link: string) {}
get title() {
return `${this.invite.invitee!.name || this.invite.email} has accepted your invite!`;
}
get text() {
const { invitee, org, email } = this.invite;
return `
Hi there!
Good news! ${invitee!.name || email} has accepted your invite to join ${org!.name}!
Visit the following link to add them:
${this.link}
Have a great day!`;
}
get html() {
const { invitee, org, email } = this.invite;
return baseHTML(
`
${p("Hi there!")}
${p(`
Good news! <strong>${invitee!.name || email}</strong> has
accepted your invite to join <strong>${org!.name}</strong>!
`)}
${button("Add Them Now", this.link)}
${p(`Have a great day!`)}
`,
this.title,
this.title
);
}
}

View File

@ -1,56 +0,0 @@
import { Invite } from "../invite";
import { Message } from "../messenger";
import { base as baseHTML, paragraph as p, button } from "./base-html";
export class InviteCreatedMessage implements Message {
constructor(public invite: Invite, public link: string) {}
get title() {
const { org, invitedBy, purpose } = this.invite;
return purpose === "confirm_membership"
? `Confirm your membership for the "${org!.name}" org on Padloc!`
: `${invitedBy!.name || invitedBy!.email} wants you to join the "${org!.name}" org on Padloc!`;
}
get text() {
const { org, invitedBy, purpose } = this.invite;
return `
Hi there!
${
purpose === "confirm_membership"
? `Please use the link below to reconfirm your membership for the "${org!.name}" org!`
: `You have been invited by ${invitedBy!.name || invitedBy!.email} to join their org ` +
`"${org!.name}" on Padloc! To accept the invite, please visit the link below:`
}
${this.link}
Have a great day!`;
}
get html() {
const { org, invitedBy, purpose } = this.invite;
return baseHTML(
`
${p("Hi there!")}
${p(
purpose === "confirm_membership"
? `Please use the link below to reconfirm your membership for the ` +
`<strong>${org!.name}</strong> org!`
: `You have been invited by <strong>${invitedBy!.name || invitedBy!.email}</strong> ` +
`to join their org <strong>${org!.name}</strong> on Padloc!
`
)}
${button(purpose === "confirm_membership" ? "Confirm Membership" : `Join ${org!.name}`, this.link)}
${p(`Have a great day!`)}
`,
this.title,
this.title
);
}
}

View File

@ -1,44 +0,0 @@
import { Org } from "../org";
import { Message } from "../messenger";
import { base as baseHTML, paragraph as p, button } from "./base-html";
export class MemberAddedMessage implements Message {
constructor(public org: Org, public link: string) {}
get title() {
return `You have successfully joined ${this.org.name} on Padloc!`;
}
get text() {
const { name } = this.org;
return `
Hi there!
You now have access to ${name} on Padloc! You can view it using the following link:
${this.link}
Have a great day`;
}
get html() {
const { name } = this.org;
return baseHTML(
`
${p("Hi there!")}
${p(`
You now have access to <strong>${name}</strong> on Padloc!
`)}
${button(`View ${name}`, this.link)}
${p(`Have a great day!`)}
`,
this.title,
this.title
);
}
}

View File

@ -1,47 +0,0 @@
import { Message } from "../messenger";
import { base as baseHTML, paragraph as p, colorBackground } from "./base-html";
export class EmailAuthMessage implements Message {
constructor(public code: string) {}
title = "Verify Your Email Address";
get text() {
return `
Hi there!
Your email verifiation code is:
${this.code.toUpperCase()}
Have a great day!`;
}
get html() {
return baseHTML(
`
${p("Hi there!")}
${p(`Your email verifiation code is:`)}
${p(
this.code.toUpperCase(),
`
background-color: ${colorBackground};\
padding: 15px;\
border-radius: 10px;\
font-size: 30px;\
font-family: monospace;\
text-align: center;\
letter-spacing: 0.2em;\
`
)}
${p(`Have a great day!`)}
`,
this.title,
this.title
);
}
}

View File

@ -1,25 +1,80 @@
export type MessageData = { [param: string]: string };
/**
* A message to be sent to a Padloc user
* A message to be sent to a user
*/
export interface Message {
export abstract class Message<T extends MessageData> {
/** Message title */
title: string;
abstract get title(): string;
/** Message body, in plain text */
text: string;
/** Template name */
abstract readonly template: string;
/** Message body, formated as html */
html: string;
constructor(public readonly data: T) {}
}
export class EmailAuthMessage extends Message<{ code: string }> {
template = "email-auth";
get title() {
return "Verify Your Email Address";
}
}
abstract class OrgInviteMessage extends Message<{ orgName: string; invitedBy: string; acceptInviteUrl: string }> {}
export class JoinOrgInviteMessage extends OrgInviteMessage {
template = "join-org-invite";
get title() {
return `${this.data.invitedBy} wants you to join the "${this.data.orgName}" org on PurePass!`;
}
}
export class ConfirmMembershipInviteMessage extends OrgInviteMessage {
template = "confirm-org-member-invite";
get title() {
return `Confirm your membership for the "${this.data.orgName}" org on PurePass!`;
}
}
export class JoinOrgInviteAcceptedMessage extends Message<{
orgName: string;
invitee: string;
confirmMemberUrl: string;
}> {
template = "join-org-invite-accepted";
get title() {
return `${this.data.invitee} has accepted your invite!`;
}
}
export class JoinOrgInviteCompletedMessage extends Message<{ orgName: string; openAppUrl: string }> {
template = "join-org-invite-completed";
get title() {
return `You have successfully joined ${this.data.orgName} on PurePass!`;
}
}
export class ErrorMessage extends Message<{ code: string; message: string; time: string; eventId: string }> {
template = "error";
get title() {
return "Padloc Error Notification";
}
}
/**
* Generic interface for sending messages to Padloc users
* Generic interface for sending messages to PurePass users
*/
export interface Messenger {
/**
* Sends a message to a given address
*/
send(addr: string, msg: Message): Promise<void>;
send<T extends MessageData>(addr: string, msg: Message<T>): Promise<void>;
}
/**
@ -31,23 +86,23 @@ export class StubMessenger implements Messenger {
* An array of messages passed to the [[send]] method. Sorted from
* most recent to oldest.
*/
messages: { recipient: string; message: Message }[] = [];
messages: { recipient: string; message: Message<any> }[] = [];
async send(recipient: string, message: Message) {
async send<T extends MessageData>(recipient: string, message: Message<T>) {
this.messages.unshift({ recipient, message });
}
/**
* Returns the most recent message sent to `addr`.
*/
lastMessage(addr: string): Message | null {
lastMessage(addr: string): Message<any> | null {
const msg = this.messages.find(({ recipient }) => recipient === addr);
return msg ? msg.message : null;
}
}
export class ConsoleMessenger implements Messenger {
async send(recipient: string, message: Message) {
console.log(`Message send to ${recipient}: ${message.text}`);
async send(recipient: string, message: Message<any>) {
console.log(`Message send to ${recipient}: ${message.data}`);
}
}

View File

@ -20,7 +20,7 @@ export class VaultQuota extends Serializable {
}
items = -1;
storage = -1;
storage = 1000;
}
export class OrgQuota extends Serializable {
@ -29,9 +29,9 @@ export class OrgQuota extends Serializable {
Object.assign(this, vals);
}
members = -1;
groups = -1;
vaults = -1;
members = 50;
groups = 10;
vaults = 10;
}
export class AccountQuota extends Serializable {
@ -41,7 +41,7 @@ export class AccountQuota extends Serializable {
}
vaults = 1;
orgs = -1;
orgs = 3;
features: Feature[] = [];
}

View File

@ -45,11 +45,17 @@ import { Err, ErrorCode } from "./error";
import { Vault, VaultID } from "./vault";
import { Org, OrgID, OrgRole } from "./org";
import { Invite } from "./invite";
import { Messenger } from "./messenger";
import {
ConfirmMembershipInviteMessage,
ErrorMessage,
JoinOrgInviteAcceptedMessage,
JoinOrgInviteCompletedMessage,
JoinOrgInviteMessage,
Messenger,
} from "./messenger";
import { Server as SRPServer, SRPSession } from "./srp";
import { DeviceInfo } from "./platform";
import { getIdFromEmail, uuid } from "./util";
import { InviteCreatedMessage, InviteAcceptedMessage, MemberAddedMessage } from "./messages";
import { loadLanguage } from "@padloc/locale/src/translate";
import { Logger } from "./log";
import { PBES2Container } from "./container";
@ -1075,18 +1081,24 @@ export class Controller extends API {
signupRequest.status = AuthRequestStatus.Verified;
auth.authRequests.push(signupRequest);
params.set("next", path);
params.set("mfaToken", signupRequest.token);
params.set("mfaId", signupRequest.id);
params.set("mfaVerified", "true");
path = "signup/";
params.set("authToken", signupRequest.token);
params.set("email", invite.email);
path = "signup/choose-password";
}
await this.storage.save(auth);
const messageClass =
invite.purpose === "confirm_membership" ? ConfirmMembershipInviteMessage : JoinOrgInviteMessage;
// Send invite link to invitees email address
this.messenger.send(
invite.email,
new InviteCreatedMessage(invite, `${this.config.clientUrl}/${path}?${params.toString()}`)
new messageClass({
orgName: invite.org.name,
invitedBy: invite.invitedBy!.name || invite.invitedBy!.email,
acceptInviteUrl: `${this.config.clientUrl}/${path}?${params.toString()}`,
})
);
})()
);
@ -1095,9 +1107,15 @@ export class Controller extends API {
for (const invite of removedInvites) {
promises.push(
(async () => {
const auth = await this._getAuth(invite.email);
auth.invites = auth.invites.filter((inv) => inv.id !== invite.id);
await this.storage.save(auth);
try {
const auth = await this._getAuth(invite.email);
auth.invites = auth.invites.filter((inv) => inv.id !== invite.id);
await this.storage.save(auth);
} catch (e) {
if (e.code !== ErrorCode.NOT_FOUND) {
throw e;
}
}
})()
);
}
@ -1106,9 +1124,15 @@ export class Controller extends API {
for (const { id } of removedMembers) {
promises.push(
(async () => {
const acc = await this.storage.get(Account, id);
acc.orgs = acc.orgs.filter((o) => o.id !== org.id);
await this.storage.save(acc);
try {
const acc = await this.storage.get(Account, id);
acc.orgs = acc.orgs.filter((o) => o.id !== org.id);
await this.storage.save(acc);
} catch (e) {
if (e.code !== ErrorCode.NOT_FOUND) {
throw e;
}
}
})()
);
}
@ -1120,7 +1144,10 @@ export class Controller extends API {
if (member.id !== account.id) {
this.messenger.send(
member.email,
new MemberAddedMessage(org, `${this.config.clientUrl}/org/${org.id}`)
new JoinOrgInviteCompletedMessage({
orgName: org.name,
openAppUrl: `${this.config.clientUrl}/org/${org.id}`,
})
);
}
}
@ -1407,7 +1434,11 @@ export class Controller extends API {
// the recipient has accepted the invite
this.messenger.send(
invite.invitedBy.email,
new InviteAcceptedMessage(invite, `${this.config.clientUrl}/invite/${org.id}/${invite.id}`)
new JoinOrgInviteAcceptedMessage({
orgName: org.name,
invitee: invite.invitee.name || invite.invitee.email,
confirmMemberUrl: `${this.config.clientUrl}/invite/${org.id}/${invite.id}`,
})
);
}
@ -1898,15 +1929,15 @@ export class Server {
});
if (e.report && this.config.reportErrors) {
this.messenger.send(this.config.reportErrors, {
title: "Padloc Error Notification",
text:
`The following error occurred at ${e.time}:\n\n` +
`Code: ${e.code}\n` +
`Message: ${e.message}\n` +
`Event ID: ${evt.id}`,
html: "",
});
this.messenger.send(
this.config.reportErrors,
new ErrorMessage({
time: e.time.toISOString(),
code: e.code,
message: e.message,
eventId: evt.id,
})
);
}
}
}

View File

@ -1,6 +1,9 @@
import { Message, Messenger } from "@padloc/core/src/messenger";
import { Message, MessageData, Messenger } from "@padloc/core/src/messenger";
import { createTransport, Transporter, TransportOptions } from "nodemailer";
import { Config, ConfigParam } from "@padloc/core/src/config";
import { readFileSync, readdirSync } from "fs";
import { Err, ErrorCode } from "@padloc/core/src/error";
import { resolve } from "path";
export class SMTPConfig extends Config {
constructor(init: Partial<SMTPConfig> = {}) {
@ -23,38 +26,67 @@ export class SMTPConfig extends Config {
@ConfigParam("string", true)
password: string = "";
templateDir: string = "";
@ConfigParam()
from?: string;
}
export class SMTPSender implements Messenger {
private transporter: Transporter;
private _transporter: Transporter;
constructor(private opts: SMTPConfig) {
private _templates = new Map<string, string>();
constructor(private config: SMTPConfig) {
let auth = null;
if (opts.user && opts.password) {
if (config.user && config.password) {
auth = {
user: opts.user,
pass: opts.password,
user: config.user,
pass: config.password,
};
}
this.transporter = createTransport({
host: opts.host,
port: opts.port,
secure: opts.secure,
this._transporter = createTransport({
host: config.host,
port: config.port,
secure: config.secure,
auth: auth,
} as TransportOptions);
this._loadTemplates(this.config.templateDir);
}
send(email: string, message: Message) {
private _loadTemplates(templateDir: string) {
const files = readdirSync(templateDir);
for (const fileName of files) {
this._templates.set(fileName.replace(/\.html$/, ""), readFileSync(resolve(templateDir, fileName), "utf-8"));
}
}
private _getMessageContent<T extends MessageData>(message: Message<T>) {
let html = this._templates.get(message.template);
if (!html) {
throw new Err(ErrorCode.SERVER_ERROR, `Template not found: ${message.template}`);
}
for (const [name, value] of Object.entries(message.data)) {
html = html.replace(new RegExp(`{{ ?${name} ?}}`, "gi"), value);
}
return { html };
}
async send<T extends MessageData>(email: string, message: Message<T>) {
const { html } = this._getMessageContent(message);
let opts = {
from: this.opts.from || this.opts.user,
from: this.config.from || this.config.user,
to: email,
subject: message.title,
text: message.text,
html: message.html,
text: html,
html,
};
return this.transporter.sendMail(opts);
return this._transporter.sendMail(opts);
}
}

View File

@ -29,6 +29,7 @@ import { TotpAuthConfig, TotpAuthServer } from "@padloc/core/src/auth/totp";
import { EmailAuthServer } from "@padloc/core/src/auth/email";
import { PublicKeyAuthServer } from "@padloc/core/src/auth/public-key";
import { StripeProvisioner } from "./provisioning/stripe";
import { resolve } from "path";
async function initDataStorage({ backend, leveldb, mongodb }: DataStorageConfig) {
switch (backend) {
@ -55,6 +56,9 @@ async function initLogger(config: LoggingConfig) {
async function initEmailSender({ backend, smtp }: EmailConfig) {
switch (backend) {
case "smtp":
if (!smtp!.templateDir) {
smtp!.templateDir = resolve(process.env.PL_ASSETS_DIR || "../../assets", "email");
}
return new SMTPSender(smtp!);
case "console":
return new ConsoleMessenger();