-
Notifications
You must be signed in to change notification settings - Fork 86
Expand file tree
/
Copy pathemail.py
More file actions
162 lines (127 loc) · 5.26 KB
/
email.py
File metadata and controls
162 lines (127 loc) · 5.26 KB
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
159
160
161
162
import markdown
from css_inline import CSSInliner
from flask import current_app as app
from flask import render_template, url_for
from jinja2.sandbox import ImmutableSandboxedEnvironment
from markupsafe import Markup
from main import db, mail
from models.email import Email, EmailJob, EmailJobRecipient
from models.user import User
from ..config import config as app_config
def create_sandbox_env():
"""Build an safe environment for rendering emails
We want non-developers to be able to write emails, but using the main
templating system would grant them access to secrets as well as the
ability to run arbitrary code. ImmutableSandboxedEnvironment gets us
most of the way there, but doesn't know which config is sensitive,
or whether globals can be safely accessed.
To avoid being annoyingly different, we try to match what Flask
does in `create_jinja_environment`, but with a pared-down set of
config and globals.
"""
default_jinja_options = {}
if app.jinja_options != default_jinja_options:
# This code doesn't support any unusual options yet. If we've
# (say) added an extension to the main template system, it should
# be allowed or ignored here.
raise NotImplementedError
# Don't autoescape because this is used to generate plaintext output
env = ImmutableSandboxedEnvironment(autoescape=False)
config_to_copy = [
"DEBUG",
"SERVER_NAME",
]
config = {c: app.config[c] for c in config_to_copy if c in app.config}
# We don't need things like request and session for emails
env.globals.update(
url_for=url_for,
config=config,
)
return env
def build_template_context():
"""Generate context for email templates
As with the environment, we want a clean context for constructing
emails. Most context processors just don't make sense in email.
"""
ctx = {}
for func in app.template_context_processors[None]:
ctx.update(func())
# We don't need things like request and session for emails
context_to_copy = [
"external_url",
"simple_dates",
"event_start",
"event_end",
"event_year",
]
ctx = {k: ctx[k] for k in context_to_copy}
return ctx
def render_template_string_sandboxed(template_str, **kwargs):
env = create_sandbox_env()
template = env.from_string(template_str)
return template.render(**build_template_context(), **kwargs)
def format_trusted_html_email(markdown_text, subject, reason=None, **kwargs):
"""Render a Markdown-formatted string to an HTML email.
markdown_text is rendered as a template. We render in the Jinja
sandbox, with a cut-down environment. This could still expose
interesting things unintentionally, so don't run templates from
untrusted users.
**kwargs are used to substitute variables in the Markdown string,
and are considered trusted. Do not pass user-controlled data in,
unless you're happy for that user to insert arbitrary HTML into
the email.
"""
extensions = ["markdown.extensions.nl2br", "markdown.extensions.smarty"]
markdown_text = render_template_string_sandboxed(markdown_text, **kwargs)
markdown_html = Markup(markdown.markdown(markdown_text, extensions=extensions))
if not reason:
reason = f"You're receiving this email because you have a ticket for Electromagnetic Field {app_config.event_year}."
inliner = CSSInliner()
return inliner.inline(
render_template(
"admin/email/email_template.html",
subject=subject,
content=markdown_html,
reason=reason,
)
)
def format_trusted_plaintext_email(markdown_text, **kwargs):
"""Render a Markdown-formatted string to a plaintext email.
markdown_text is rendered as a template, so is considered trusted.
Do not pass user-controlled templates in, unless you're happy for
that user to run code on the server.
"""
return render_template_string_sandboxed(markdown_text, **kwargs)
def preview_trusted_email(preview_address, subject, body):
subject = "[PREVIEW] " + subject
formatted_plaintext = format_trusted_plaintext_email(body)
formatted_html = format_trusted_html_email(body, subject)
mail.send_mail(
subject=subject,
message=formatted_plaintext,
from_email=app_config.from_email("CONTACT_EMAIL"),
recipient_list=[preview_address],
html_message=formatted_html,
)
def enqueue_trusted_emails(users, subject, body, **kwargs):
"""Queue a bulk email for sending by the background email worker."""
job = EmailJob(
subject=subject,
text_body=format_trusted_plaintext_email(body, **kwargs),
html_body=format_trusted_html_email(body, subject, **kwargs),
)
db.session.add(job)
for user in users:
db.session.add(EmailJobRecipient(job=job, user=user))
def enqueue_email(
recipient: User, from_email: str, subject: str, text_body: str, html_body: Markup | None = None
) -> None:
"""Queue a transactional email for sending by the background email worker."""
email = Email(
recipient=recipient,
from_email=from_email,
subject=subject,
text_body=text_body,
html_body=html_body,
)
db.session.add(email)