API - refactor imports

This commit is contained in:
Sam
2021-01-20 16:47:00 +01:00
parent fdeaf54aa9
commit d3ce0ad1e5
54 changed files with 110 additions and 67 deletions

View File

117
fittrackee/emails/email.py Normal file
View File

@ -0,0 +1,117 @@
import logging
import smtplib
import ssl
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Dict, Optional, Type, Union
from flask import Flask
from jinja2 import Environment, FileSystemLoader
from .utils_email import parse_email_url
email_log = logging.getLogger('fittrackee_api_email')
email_log.setLevel(logging.DEBUG)
class EmailMessage:
def __init__(
self, sender: str, recipient: str, subject: str, html: str, text: str
) -> None:
self.sender = sender
self.recipient = recipient
self.subject = subject
self.html = html
self.text = text
def generate_message(self) -> MIMEMultipart:
message = MIMEMultipart('alternative')
message['Subject'] = self.subject
message['From'] = self.sender
message['To'] = self.recipient
part1 = MIMEText(self.text, 'plain')
part2 = MIMEText(self.html, 'html')
message.attach(part1)
message.attach(part2)
return message
class EmailTemplate:
def __init__(self, template_directory: str) -> None:
self._env = Environment(loader=FileSystemLoader(template_directory))
def get_content(
self, template_name: str, lang: str, part: str, data: Dict
) -> str:
template = self._env.get_template(f'{template_name}/{lang}/{part}')
return template.render(data)
def get_all_contents(self, template: str, lang: str, data: Dict) -> Dict:
output = {}
for part in ['subject.txt', 'body.txt', 'body.html']:
output[part] = self.get_content(template, lang, part, data)
return output
def get_message(
self, template: str, lang: str, sender: str, recipient: str, data: Dict
) -> MIMEMultipart:
output = self.get_all_contents(template, lang, data)
message = EmailMessage(
sender,
recipient,
output['subject.txt'],
output['body.html'],
output['body.txt'],
)
return message.generate_message()
class Email:
def __init__(self, app: Optional[Flask] = None) -> None:
self.host = 'localhost'
self.port = 1025
self.use_tls = False
self.use_ssl = False
self.username = None
self.password = None
self.sender_email = 'no-reply@example.com'
self.email_template: Optional[EmailTemplate] = None
if app is not None:
self.init_email(app)
def init_email(self, app: Flask) -> None:
parsed_url = parse_email_url(app.config['EMAIL_URL'])
self.host = parsed_url['host']
self.port = parsed_url['port']
self.use_tls = parsed_url['use_tls']
self.use_ssl = parsed_url['use_ssl']
self.username = parsed_url['username']
self.password = parsed_url['password']
self.sender_email = app.config['SENDER_EMAIL']
self.email_template = EmailTemplate(app.config['TEMPLATES_FOLDER'])
@property
def smtp(self) -> Type[Union[smtplib.SMTP_SSL, smtplib.SMTP]]:
return smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
def send(
self, template: str, lang: str, recipient: str, data: Dict
) -> None:
if not self.email_template:
raise Exception('No email template defined.')
message = self.email_template.get_message(
template, lang, self.sender_email, recipient, data
)
connection_params = {}
if self.use_ssl or self.use_tls:
context = ssl.create_default_context()
if self.use_ssl:
connection_params.update({'context': context})
with self.smtp(
self.host, self.port, **connection_params # type: ignore
) as smtp:
smtp.login(self.username, self.password) # type: ignore
if self.use_tls:
smtp.starttls(context=context)
smtp.sendmail(self.sender_email, recipient, message.as_string())
smtp.quit()

View File

@ -0,0 +1,2 @@
class InvalidEmailUrlScheme(Exception):
...

View File

@ -0,0 +1,268 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" content=""/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Fittrackee - Password reset request</title>
<style type="text/css" rel="stylesheet" media="all">
body {
background-color: #F4F4F7;
color: #51545E;
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
body,
td,
th {
font-family: Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
font-size: 22px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p {
color: #51545E;
margin: .4em 0 1.1875em;
font-size: 16px;
line-height: 1.625;
}
p.sub {
color: #6B6E76;
font-size: 13px;
}
.button {
background-color: #3869D4;
border-top: 10px solid #3869D4;
border-right: 18px solid #3869D4;
border-bottom: 10px solid #3869D4;
border-left: 18px solid #3869D4;
display: inline-block;
color: #FFF;
text-decoration: none;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #22BC66;
border-top: 10px solid #22BC66;
border-right: 18px solid #22BC66;
border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
background-color: #F4F4F7;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
}
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead-name {
font-size: 16px;
font-weight: bold;
color: #A8AAAF;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
.email-body {
width: 100%;
margin: 0;
padding: 0;
background-color: #FFFFFF;
}
.email-body-inner {
width: 570px;
margin: 0 auto;
padding: 0;
background-color: #FFFFFF;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EAEAEC;
}
.content-cell {
padding: 35px;
}
@media only screen and (max-width: 600px) {
.email-body-inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body-inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #333333 !important;
color: #FFF !important;
}
p,
h1 {
color: #FFF !important;
}
.email-masthead-name {
text-shadow: none !important;
}
}
</style>
<!--[if mso]>
<style type="text/css">
.f-fallback {
font-family: Arial, sans-serif;
}
</style>
<![endif]-->
</head>
<body>
<span class="preheader">Use this link to reset your password. The link is only valid for {{ expiration_delay }}.</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead-name">
FitTrackee
</a>
</td>
</tr>
<tr>
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>Hi {{username}},</h1>
<p>You recently requested to reset your password for your account. Use the button below to reset it.
<strong>This password reset link is only valid for {{ expiration_delay }}.</strong>
</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{password_reset_url}}" class="f-fallback button button--green" target="_blank">Reset your password</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>
For security, this request was received from a {{operating_system}} device using {{browser_name}}.
If you did not request a password reset, please ignore this email.
</p>
<p>Thanks,
<br>The FitTrackee Team</p>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">If youre having trouble with the button above, copy and paste the URL below into your web browser.</p>
<p class="f-fallback sub">{{password_reset_url}}</p>
</td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell" align="center">
<p class="f-fallback sub align-center">&copy; FitTrackee.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,10 @@
Hi {{username}},
You recently requested to reset your password for your FitTrackee account. Use the button below to reset it. This password reset link is only valid for {{ expiration_delay }}.
Reset your password ( {{ password_reset_url }} )
For security, this request was received from a {{operating_system}} device using {{browser_name}}. If you did not request a password reset, please ignore this email.
Thanks,
The FitTrackee Team

View File

@ -0,0 +1 @@
FitTrackee - Password reset request

View File

@ -0,0 +1,269 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="x-apple-disable-message-reformatting" content=""/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>FitTrackee - Réinitialiser le mot de passe</title>
<style type="text/css" rel="stylesheet" media="all">
body {
background-color: #F4F4F7;
color: #51545E;
width: 100% !important;
height: 100%;
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
mso-hide: all;
font-size: 1px;
line-height: 1px;
max-height: 0;
max-width: 0;
opacity: 0;
overflow: hidden;
}
body,
td,
th {
font-family: Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
font-size: 22px;
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p {
color: #51545E;
margin: .4em 0 1.1875em;
font-size: 16px;
line-height: 1.625;
}
p.sub {
color: #6B6E76;
font-size: 13px;
}
.button {
background-color: #3869D4;
border-top: 10px solid #3869D4;
border-right: 18px solid #3869D4;
border-bottom: 10px solid #3869D4;
border-left: 18px solid #3869D4;
display: inline-block;
color: #FFF;
text-decoration: none;
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #22BC66;
border-top: 10px solid #22BC66;
border-right: 18px solid #22BC66;
border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
text-align: center !important;
}
}
.email-wrapper {
width: 100%;
margin: 0;
padding: 0;
background-color: #F4F4F7;
}
.email-content {
width: 100%;
margin: 0;
padding: 0;
}
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead-name {
font-size: 16px;
font-weight: bold;
color: #A8AAAF;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
.email-body {
width: 100%;
margin: 0;
padding: 0;
background-color: #FFFFFF;
}
.email-body-inner {
width: 570px;
margin: 0 auto;
padding: 0;
background-color: #FFFFFF;
}
.body-action {
width: 100%;
margin: 30px auto;
padding: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EAEAEC;
}
.content-cell {
padding: 35px;
}
@media only screen and (max-width: 600px) {
.email-body-inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,
.email-body-inner,
.email-content,
.email-wrapper,
.email-masthead,
.email-footer {
background-color: #333333 !important;
color: #FFF !important;
}
p,
h1 {
color: #FFF !important;
}
.email-masthead-name {
text-shadow: none !important;
}
}
</style>
<!--[if mso]>
<style type="text/css">
.f-fallback {
font-family: Arial, sans-serif;
}
</style>
<![endif]-->
</head>
<body>
<span class="preheader">Utiliser ce lien pour réinitialiser le mot de passe. Ce lien n'est valide que pendant {{ expiration_delay }}.</span>
<table class="email-wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="email-content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="email-masthead">
<a href="https://example.com" class="f-fallback email-masthead-name">
FitTrackee
</a>
</td>
</tr>
<tr>
<td class="email-body" width="100%" cellpadding="0" cellspacing="0">
<table class="email-body-inner" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell">
<div class="f-fallback">
<h1>Bonjour {{username}},</h1>
<p>Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
Cliquez sur le bouton ci-dessous pour le réinitialiser.
<strong>Cette réinitialisation n'est valide que pendant {{ expiration_delay }}.</strong>
</p>
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table width="100%" border="0" cellspacing="0" cellpadding="0" role="presentation">
<tr>
<td align="center">
<a href="{{password_reset_url}}" class="f-fallback button button--green" target="_blank">Réinitialiser le mot de passe</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<p>
Pour vérification, cette demande a été reçue à partir d'un appareil sous {{operating_system}}, utilisant le navigateur {{browser_name}}.
Si vous n'avez pas demandé de réinitalisation, vous pouvez ignorer cet e-mail.
</p>
<p>Merci,
<br>L'équipe FitTrackee</p>
<table class="body-sub" role="presentation">
<tr>
<td>
<p class="f-fallback sub">Si vous avez des problèmes avec le bouton, vous pouvez copier et coller le lien suivant dans votre navigateur</p>
<p class="f-fallback sub">{{password_reset_url}}</p>
</td>
</tr>
</table>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>
<table class="email-footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell" align="center">
<p class="f-fallback sub align-center">&copy; FitTrackee.</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,12 @@
Bonjour {{username}},
Vous avez récemment demander la réinitilisation du mot de passe de votre compte sur FitTrackee.
Cliquez sur le lien ci-dessous pour le réinitialiser. Ce lien n'est valide que pendant {{ expiration_delay }}.
Réinitialiser le mot de passe: ( {{ password_reset_url }} )
Pour vérification, cette demande a été reçue à partir d'un appareil sous {{operating_system}}, utilisant le navigateur {{browser_name}}.
Si vous n'avez pas demandé de réinitalisation, vous pouvez ignorer cet e-mail.
Merci,
L'équipe FitTrackee

View File

@ -0,0 +1 @@
FitTrackee - Réinitialiser votre mot de passe

View File

@ -0,0 +1,20 @@
from typing import Dict
from urllib3.util import parse_url
from .exceptions import InvalidEmailUrlScheme
def parse_email_url(email_url: str) -> Dict:
parsed_url = parse_url(email_url)
if parsed_url.scheme != 'smtp':
raise InvalidEmailUrlScheme()
credentials = parsed_url.auth.split(':')
return {
'host': parsed_url.host,
'port': parsed_url.port,
'use_tls': True if parsed_url.query == 'tls=True' else False,
'use_ssl': True if parsed_url.query == 'ssl=True' else False,
'username': credentials[0],
'password': credentials[1],
}