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 163 164 165
|
"""
flask_security.mail_util
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Utility class providing methods for validating, normalizing and sending emails.
:copyright: (c) 2020-2024 by J. Christopher Wagner (jwag).
:license: MIT, see LICENSE for more details.
While this default implementation uses Flask-Mailman - we want to make sure that
Flask-Mailman isn't REQUIRED (if this implementation isn't used).
"""
from __future__ import annotations
import typing as t
import email_validator
from flask import current_app
from .utils import config_value, get_message
if t.TYPE_CHECKING: # pragma: no cover
import flask
class EmailValidateException(ValueError):
"""This is raised for any email validation errors.
This can be used by custom MailUtil implementations to provide
custom error messages.
"""
def __init__(self, message: str) -> None:
self.msg = message
class MailUtil:
"""
Utility class providing methods for validating, normalizing and sending emails.
This default class uses the email_validator package to handle validation and
normalization, and the flask_mailman package (if initialized) to send emails.
To provide your own implementation, pass in the class as ``mail_util_cls``
at init time. Your class will be instantiated once as part of app initialization.
.. versionadded:: 4.0.0
"""
def __init__(self, app: flask.Flask):
"""Instantiate class.
:param app: The Flask application being initialized.
"""
pass
def send_mail(
self,
template: str,
subject: str,
recipient: str,
sender: str | tuple,
body: str,
html: str | None,
**kwargs: t.Any,
) -> None:
"""Send an email via the Flask-Mailman or Flask-Mail or other mail extension.
:param template: the Template name. The message has already been rendered
however this might be useful to differentiate why the email is being sent.
:param subject: Email subject
:param recipient: Email recipient
:param sender: who to send email as (see :py:data:`SECURITY_EMAIL_SENDER`)
:param body: the rendered body (text)
:param html: the rendered body (html)
:param kwargs: the entire context
It is possible that sender is a lazy_string for localization (unlikely but..)
so we cast to str() here to force localization.
"""
if current_app.extensions.get("mailman", None):
from flask_mailman import EmailMultiAlternatives, Mail
# Flask-Mailman doesn't appear to take a tuple - a bug has been filed
# but not sure they will fix it (parts of Flask-Mailman work - but not
# the actual email headers).
if isinstance(sender, tuple) and len(sender) == 2:
# sender = (str(sender[0]), str(sender[1]))
sender = f"{str(sender[0])} <{str(sender[1])}>"
else:
sender = str(sender)
mail: Mail = current_app.extensions.get("mailman")
with mail.get_connection() as connection:
msg = EmailMultiAlternatives(
subject,
body=body,
from_email=sender,
to=[recipient],
connection=connection,
)
if html:
msg.attach_alternative(html, "text/html")
msg.send()
elif current_app.extensions.get("mail", None): # pragma: no cover
from flask_mail import Message
# In Flask-Mail, sender can be a two element tuple -- (name, address)
if isinstance(sender, tuple) and len(sender) == 2:
sender = (str(sender[0]), str(sender[1]))
else:
sender = str(sender)
msg = Message(subject, sender=sender, recipients=[recipient])
msg.body = body
msg.html = html
mail = current_app.extensions.get("mail")
mail.send(msg) # type: ignore
else: # pragma: no cover
raise ValueError("No email extension configured")
def normalize(self, email: str) -> str:
"""
Given an input email - return a normalized version or
raise EmailValidateException if field value isn't syntactically valid.
This is called by forms that use email as an identity to be looked up.
Must be called in app context and uses :py:data:`SECURITY_EMAIL_VALIDATOR_ARGS`
config variable to pass any relevant arguments to
email_validator.validate_email() method.
This defaults to NOT checking for deliverability (i.e. DNS checks).
"""
validator_args = config_value("EMAIL_VALIDATOR_ARGS") or {}
validator_args["check_deliverability"] = False
try:
valid = email_validator.validate_email(email, **validator_args)
return valid.normalized
except ValueError:
raise EmailValidateException(get_message("INVALID_EMAIL_ADDRESS")[0])
def validate(self, email: str) -> str:
"""
Validate the given email.
If valid, the normalized version is returned.
This is used by forms/views that require an email that likely can have an
actual email sent to it.
Must be called in app context and uses :py:data:`SECURITY_EMAIL_VALIDATOR_ARGS`
config variable to pass any relevant arguments to
email_validator.validate_email() method.
EmailValidationException is thrown on invalid email.
"""
validator_args = config_value("EMAIL_VALIDATOR_ARGS") or {}
try:
valid = email_validator.validate_email(email, **validator_args)
return valid.normalized
except ValueError:
raise EmailValidateException(get_message("INVALID_EMAIL_ADDRESS")[0])
|