Skip to content

Commit 0b6d1df

Browse files
authored
New mail layout and fix incomplete link in mail (#615)
* add new mail layout [WIP] * use language strings for mail content * fix token * remove unused funcs * Adjust email cli * create constant for CSS styles * remove unused import * restruct email building and adapt unstyled mails * output error in unittest assert * output body in test * Update regex to read token from html mail * Remove print in unittest
1 parent a3912ae commit 0b6d1df

File tree

6 files changed

+253
-56
lines changed

6 files changed

+253
-56
lines changed

gramps_webapi/__main__.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@
3434
import waitress # type: ignore
3535
import webbrowser
3636

37+
from .api.tasks import (
38+
send_email_confirm_email,
39+
send_email_reset_password,
40+
)
3741
from .api.search import get_search_indexer, get_semantic_search_indexer
3842
from .api.util import get_db_manager, list_trees, close_db
3943
from .app import create_app
@@ -136,6 +140,36 @@ def open_webbrowser_after_start():
136140
print("Stopping Gramps Web API server...")
137141

138142

143+
@cli.group("email", help="Manage email tools.")
144+
@click.pass_context
145+
def email(ctx):
146+
app = ctx.obj["app"]
147+
148+
149+
@email.command("reset-pw")
150+
@click.argument("mail_to")
151+
@click.argument("username")
152+
@click.pass_context
153+
def send_reset_pw_email(ctx, mail_to, username):
154+
"""Send dummy reset password email mail."""
155+
app = ctx.obj["app"]
156+
app.logger.info(f"Send reset-pw mail to {mail_to} ...")
157+
with app.app_context():
158+
send_email_reset_password(mail_to, username, "")
159+
160+
161+
@email.command("confirm-email")
162+
@click.argument("mail_to")
163+
@click.argument("username")
164+
@click.pass_context
165+
def send_confirm_email(ctx, mail_to, username):
166+
"""Send dummy confirm email mail."""
167+
app = ctx.obj["app"]
168+
app.logger.info(f"Send confirm-email mail to {mail_to} ...")
169+
with app.app_context():
170+
send_email_confirm_email(mail_to, username, "")
171+
172+
139173
@cli.group("user", help="Manage users.")
140174
@click.pass_context
141175
def user(ctx):

gramps_webapi/api/emails.py

Lines changed: 189 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,62 +21,69 @@
2121

2222
from gettext import gettext as _
2323

24+
EMAIL_CSS_STYLES = """
25+
body {
26+
font-family: Arial, sans-serif;
27+
background-color: #f4f4f4;
28+
margin: 0;
29+
padding: 0;
30+
}
31+
.container {
32+
max-width: 600px;
33+
margin: 20px 10px;
34+
background: #ffffff;
35+
padding: 20px;
36+
border-radius: 10px;
37+
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
38+
}
39+
.header {
40+
text-align: left;
41+
font-size: 24px;
42+
color: #333;
43+
}
44+
.content {
45+
text-align: left;
46+
font-size: 16px;
47+
color: #555;
48+
}
49+
.button {
50+
display: inline-block;
51+
background-color: #6D4C41;
52+
color: #ffffff;
53+
padding: 10px 20px;
54+
font-size: 16px;
55+
text-decoration: none;
56+
border-radius: 4px;
57+
}
58+
.greeting {
59+
font-size: 20px;
60+
}
61+
"""
2462

25-
def email_reset_pw(base_url: str, token: str):
26-
"""Reset password e-mail text."""
27-
intro = _(
28-
"You are receiving this e-mail because you (or someone else) "
29-
"have requested the reset of the password for your account."
30-
)
31-
32-
action = _(
33-
"Please click on the following link, or paste this into your browser "
34-
"to complete the process:"
35-
)
36-
37-
end = _(
38-
"If you did not request this, please ignore this e-mail "
39-
"and your password will remain unchanged."
40-
)
41-
return f"""{intro}
42-
43-
{action}
4463

45-
{base_url}/api/users/-/password/reset/?jwt={token}
64+
def email_reset_pw(base_url: str, user_name: str, token: str):
65+
"""Reset password e-mail text."""
66+
url = f"""{base_url}/api/users/-/password/reset/?jwt={token}"""
4667

47-
{end}
48-
"""
68+
body = email_body_reset_pw(user_name=user_name, url=url)
69+
body_html = email_htmlbody_reset_pw(user_name=user_name, url=url)
70+
return (body, body_html)
4971

5072

51-
def email_confirm_email(base_url: str, token: str):
73+
def email_confirm_email(base_url: str, user_name: str, token: str):
5274
"""Confirm e-mail address e-mail text."""
53-
intro = (
54-
_(
55-
"You are receiving this message because this e-mail address "
56-
"was used to register a new account at %s."
57-
)
58-
% base_url
59-
)
60-
action = _(
61-
"Please click on the following link, or paste this into your browser "
62-
"to confirm your email address. You will be able to log on once a "
63-
"tree owner reviews and approves your account."
64-
)
65-
66-
return f"""{intro}
67-
68-
{action}
75+
url = f"""{base_url}/api/users/-/email/confirm/?jwt={token}"""
6976

70-
{base_url}/api/users/-/email/confirm/?jwt={token}
71-
"""
77+
body = email_body_confirm_email(user_name=user_name, url=url)
78+
body_html = email_htmlbody_confirm_email(user_name=user_name, url=url)
79+
return (body, body_html)
7280

7381

7482
def email_new_user(base_url: str, username: str, fullname: str, email: str):
7583
"""E-mail notifying owners of a new registered user."""
7684
intro = _("A new user registered at %s.") % base_url
7785
next_step = _(
78-
"Please review this user registration and assign a role to "
79-
"enable access:"
86+
"Please review this user registration and assign a role to " "enable access:"
8087
)
8188
label_username = _("User name")
8289
label_fullname = _("Full name")
@@ -91,3 +98,142 @@ def email_new_user(base_url: str, username: str, fullname: str, email: str):
9198
9299
{user_details}
93100
"""
101+
102+
103+
def email_htmltemplate(header: str, containerContent: str) -> str:
104+
return """
105+
<!DOCTYPE html>
106+
<html>
107+
<head>
108+
<meta charset="UTF-8">
109+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
110+
<title>%(header)s</title>
111+
<style>
112+
%(styles)s
113+
</style>
114+
</head>
115+
<body>
116+
<div class="container">
117+
%(containerContent)s
118+
</div>
119+
</body>
120+
</html>
121+
""" % {
122+
"header": header,
123+
"containerContent": containerContent,
124+
"styles": EMAIL_CSS_STYLES,
125+
}
126+
127+
128+
def email_body_reset_pw(user_name: str, url: str) -> str:
129+
header = _("Reset Your Password")
130+
greeting = _("Hi %s,") % user_name
131+
descMail = _(
132+
"You are receiving this e-mail because you (or someone else) have requested the reset of the password for your account."
133+
)
134+
descLink = _(
135+
"Please click on the following link, or paste this into your browser to complete the process:"
136+
)
137+
descIgnore = _(
138+
"If you did not request a password reset, please ignore this email and your password will remain unchanged."
139+
)
140+
return f"""{header}
141+
142+
{greeting}
143+
{descMail}
144+
{descLink}
145+
146+
{url}
147+
148+
{descIgnore}
149+
"""
150+
151+
152+
def email_htmlbody_reset_pw(user_name: str, url: str) -> str:
153+
header = _("Reset Your Password")
154+
greeting = _("Hi %s,") % user_name
155+
descMail = _(
156+
"You are receiving this e-mail because you (or someone else) have requested the reset of the password for your account."
157+
)
158+
descAction = _("Click the button below to set a new password:")
159+
buttonLabel = _("Reset Password")
160+
descIgnore = _(
161+
"If you did not request a password reset, please ignore this email and your password will remain unchanged."
162+
)
163+
164+
containerContent = """
165+
<div class="header">%(header)s</div>
166+
<div class="content">
167+
<p class="greeting">%(greeting)s</p>
168+
<p>%(descMail)s</p>
169+
<p>%(descAction)s</p>
170+
<a href="%(url)s" class="button">%(buttonLabel)s</a>
171+
<p>%(descIgnore)s</p>
172+
</div>
173+
""" % {
174+
"greeting": greeting,
175+
"url": url,
176+
"header": header,
177+
"descMail": descMail,
178+
"descAction": descAction,
179+
"buttonLabel": buttonLabel,
180+
"descIgnore": descIgnore,
181+
}
182+
183+
return email_htmltemplate(header, containerContent)
184+
185+
186+
def email_body_confirm_email(user_name: str, url: str) -> str:
187+
header = _("Confirm your e-mail address")
188+
welcome = _("Welcome to Gramps Web")
189+
greeting = _("Hi %s,") % user_name
190+
descLink = _(
191+
"Please click on the following link, or paste this into your browser "
192+
"to confirm your email address. You will be able to log on once a "
193+
"tree owner reviews and approves your account."
194+
)
195+
descFurtherAction = _(
196+
"You will be able to log on once a tree owner reviews and approves your account."
197+
)
198+
199+
return f"""{header}
200+
201+
{welcome}
202+
{greeting}
203+
{descLink}
204+
205+
{url}
206+
207+
{descFurtherAction}
208+
"""
209+
210+
211+
def email_htmlbody_confirm_email(user_name: str, url: str) -> str:
212+
header = _("Confirm your e-mail address")
213+
welcome = _("Welcome to Gramps Web")
214+
greeting = _("Hi %s,") % user_name
215+
descAction = _(
216+
"Thank you for registering! Please confirm your email address by clicking the button below:"
217+
)
218+
buttonLabel = _("Confirm Email")
219+
descFurtherAction = _(
220+
"You will be able to log on once a tree owner reviews and approves your account."
221+
)
222+
223+
containerContent = """
224+
<div class="header">%(welcome)s</div>
225+
<div class="content">
226+
<p class="greeting">%(greeting)s</p>
227+
<p>%(descAction)s</p>
228+
<a href="%(url)s" class="button">%(buttonLabel)s</a>
229+
<p>%(descFurtherAction)s</p>
230+
</div>
231+
""" % {
232+
"url": url,
233+
"welcome": welcome,
234+
"greeting": greeting,
235+
"descAction": descAction,
236+
"buttonLabel": buttonLabel,
237+
"descFurtherAction": descFurtherAction,
238+
}
239+
return email_htmltemplate(header, containerContent)

gramps_webapi/api/resources/user.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,12 @@ def post(self, args, user_name: str):
362362
# link does not expire
363363
expires_delta=False,
364364
)
365-
run_task(send_email_confirm_email, email=args["email"], token=token)
365+
run_task(
366+
send_email_confirm_email,
367+
email=args["email"],
368+
user_name=user_name,
369+
token=token,
370+
)
366371
return "", 201
367372

368373

@@ -480,7 +485,9 @@ def post(self, user_name):
480485
expires_delta=datetime.timedelta(hours=1),
481486
)
482487
try:
483-
task = run_task(send_email_reset_password, email=email, token=token)
488+
task = run_task(
489+
send_email_reset_password, email=email, user_name=user_name, token=token
490+
)
484491
except ValueError:
485492
abort_with_message(500, "Error while trying to send e-mail")
486493
if isinstance(task, AsyncResult):

gramps_webapi/api/tasks.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,25 @@ def clip_progress(x: float) -> float:
8787

8888

8989
@shared_task()
90-
def send_email_reset_password(email: str, token: str):
90+
def send_email_reset_password(email: str, user_name: str, token: str):
9191
"""Send an email for password reset."""
9292
base_url = get_config("BASE_URL").rstrip("/")
93-
body = email_reset_pw(base_url=base_url, token=token)
93+
body, body_html = email_reset_pw(
94+
base_url=base_url, user_name=user_name, token=token
95+
)
9496
subject = _("Reset your Gramps password")
95-
send_email(subject=subject, body=body, to=[email])
97+
send_email(subject=subject, body=body, to=[email], body_html=body_html)
9698

9799

98100
@shared_task()
99-
def send_email_confirm_email(email: str, token: str):
101+
def send_email_confirm_email(email: str, user_name: str, token: str):
100102
"""Send an email to confirm an e-mail address."""
101103
base_url = get_config("BASE_URL").rstrip("/")
102-
body = email_confirm_email(base_url=base_url, token=token)
104+
body, body_html = email_confirm_email(
105+
base_url=base_url, user_name=user_name, token=token
106+
)
103107
subject = _("Confirm your e-mail address")
104-
send_email(subject=subject, body=body, to=[email])
108+
send_email(subject=subject, body=body, to=[email], body_html=body_html)
105109

106110

107111
@shared_task()

gramps_webapi/api/util.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,11 +509,17 @@ def get_buffer_for_file(filename: str, delete=True, not_found=False) -> BinaryIO
509509

510510

511511
def send_email(
512-
subject: str, body: str, to: Sequence[str], from_email: str | None = None
512+
subject: str,
513+
body: str,
514+
to: Sequence[str],
515+
from_email: str | None = None,
516+
body_html: str | None = None,
513517
) -> None:
514518
"""Send an e-mail message."""
515519
msg = EmailMessage()
516520
msg.set_content(body)
521+
if body_html:
522+
msg.add_alternative(body_html, subtype="html")
517523
msg["Subject"] = subject
518524
if not from_email:
519525
from_email = get_config("DEFAULT_FROM_EMAIL")

0 commit comments

Comments
 (0)