-
Notifications
You must be signed in to change notification settings - Fork 11
/
email.py
158 lines (126 loc) · 5.08 KB
/
email.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import binascii
import contextlib
from django.contrib.auth import get_user_model
from django.core import signing
from django.core.exceptions import ValidationError
from django.core.mail import EmailMultiAlternatives
from django.template.loader import TemplateDoesNotExist, render_to_string
from django.urls import reverse
from django.utils.translation import gettext as _
# Assumes that this is a model with an unique `email` field.
User = get_user_model()
def render_to_mail(template, context, **kwargs):
"""
Renders a mail and returns the resulting ``EmailMultiAlternatives``
instance
* ``template``: The base name of the text and HTML (optional) version of
the mail.
* ``context``: The context used to render the mail. This context instance
should contain everything required.
* Additional keyword arguments are passed to the ``EmailMultiAlternatives``
instantiation. Use those to specify the ``to``, ``headers`` etc.
arguments.
Usage example::
# Render the template myproject/hello_mail.txt (first non-empty line
# contains the subject, third to last the body) and optionally the
# template myproject/hello_mail.html containing the alternative HTML
# representation.
message = render_to_mail('myproject/hello_mail', {}, to=[email])
message.send()
"""
if not isinstance(template, (list, tuple)):
template = [template]
lines = iter(
line.rstrip()
for line in render_to_string(
["%s.txt" % t for t in template], context
).splitlines()
)
subject = ""
try:
while True:
line = next(lines)
if line:
subject = line
break
except StopIteration: # if lines is empty
pass
body = "\n".join(lines).strip("\n")
message = EmailMultiAlternatives(subject=subject, body=body, **kwargs)
with contextlib.suppress(TemplateDoesNotExist):
message.attach_alternative(
render_to_string(["%s.html" % t for t in template], context), "text/html"
)
return message
def get_signer(salt="email_registration"):
"""
Returns the signer instance used to sign and unsign the registration
link tokens
"""
return signing.TimestampSigner(salt=salt)
def get_confirmation_code(email, *, payload=""):
"""
Returns the code for the confirmation URL
The payload should be a string already.
"""
s = f"{email}:{payload}"
return get_signer().sign(signing.b64_encode(s.encode("utf-8")).decode("utf-8"))
def get_confirmation_url(email, request, name="email_registration_confirm", **kwargs):
"""
Returns the confirmation URL
"""
return request.build_absolute_uri(
reverse(name, kwargs={"code": get_confirmation_code(email, **kwargs)})
)
def send_registration_mail(email, *, request, **kwargs):
"""send_registration_mail(email, *, request, **kwargs)
Sends the registration mail
* ``email``: The email address where the registration link should be
sent to.
* ``request``: A HTTP request instance, used to construct the complete
URL (including protocol and domain) for the registration link.
* Additional keyword arguments for ``get_confirmation_url`` respectively
``get_confirmation_code``.
The mail is rendered using the following two templates:
* ``registration/email_registration_email.txt``: The first line of this
template will be the subject, the third to the last line the body of the
email.
* ``registration/email_registration_email.html``: The body of the HTML
version of the mail. This template is **NOT** available by default and
is not required either.
"""
render_to_mail(
"registration/email_registration_email",
{"url": get_confirmation_url(email, request, **kwargs)},
to=[email],
).send()
def decode(code, *, max_age):
"""decode(code, *, max_age)
Decodes the code from the registration link and returns a tuple consisting
of the verified email address and the payload which was passed through to
``get_confirmation_code``.
The maximum age in seconds of the link has to be specified as ``max_age``.
This method raises ``ValidationError`` exceptions when anything goes wrong
when verifying the signature or the expiry timeout.
"""
try:
data = get_signer().unsign(code, max_age=max_age)
except signing.SignatureExpired as exc:
raise ValidationError(
_("The link is expired. Please request another registration link."),
code="email_registration_expired",
) from exc
except signing.BadSignature as exc:
raise ValidationError(
_(
"Unable to verify the signature. Please request a new"
" registration link."
),
code="email_registration_signature",
) from exc
try:
data = signing.b64_decode(data.encode("utf-8")).decode("utf-8")
except (binascii.Error, UnicodeDecodeError):
if ":" not in data:
raise
return data.split(":", 1)