codekingpro/portable-devtools
114k
1"""2 flask_security.mail_util3 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~4 5 Utility class providing methods for validating, normalizing and sending emails.6 7 :copyright: (c) 2020-2024 by J. Christopher Wagner (jwag).8 :license: MIT, see LICENSE for more details.9 10 While this default implementation uses Flask-Mailman - we want to make sure that11 Flask-Mailman isn't REQUIRED (if this implementation isn't used).12"""13 14from __future__ import annotations15 16import typing as t17 18import email_validator19from flask import current_app20 21from .utils import config_value, get_message22 23if t.TYPE_CHECKING: # pragma: no cover24 import flask25 26 27class EmailValidateException(ValueError):28 """This is raised for any email validation errors.29 This can be used by custom MailUtil implementations to provide30 custom error messages.31 """32 33 def __init__(self, message: str) -> None:34 self.msg = message35 36 37class MailUtil:38 """39 Utility class providing methods for validating, normalizing and sending emails.40 41 This default class uses the email_validator package to handle validation and42 normalization, and the flask_mailman package (if initialized) to send emails.43 44 To provide your own implementation, pass in the class as ``mail_util_cls``45 at init time. Your class will be instantiated once as part of app initialization.46 47 .. versionadded:: 4.0.048 """49 50 def __init__(self, app: flask.Flask):51 """Instantiate class.52 53 :param app: The Flask application being initialized.54 """55 pass56 57 def send_mail(58 self,59 template: str,60 subject: str,61 recipient: str,62 sender: str | tuple,63 body: str,64 html: str | None,65 **kwargs: t.Any,66 ) -> None:67 """Send an email via the Flask-Mailman or Flask-Mail or other mail extension.68 69 :param template: the Template name. The message has already been rendered70 however this might be useful to differentiate why the email is being sent.71 :param subject: Email subject72 :param recipient: Email recipient73 :param sender: who to send email as (see :py:data:`SECURITY_EMAIL_SENDER`)74 :param body: the rendered body (text)75 :param html: the rendered body (html)76 :param kwargs: the entire context77 78 It is possible that sender is a lazy_string for localization (unlikely but..)79 so we cast to str() here to force localization.80 """81 82 if current_app.extensions.get("mailman", None):83 from flask_mailman import EmailMultiAlternatives, Mail84 85 # Flask-Mailman doesn't appear to take a tuple - a bug has been filed86 # but not sure they will fix it (parts of Flask-Mailman work - but not87 # the actual email headers).88 if isinstance(sender, tuple) and len(sender) == 2:89 # sender = (str(sender[0]), str(sender[1]))90 sender = f"{str(sender[0])} <{str(sender[1])}>"91 else:92 sender = str(sender)93 94 mail: Mail = current_app.extensions.get("mailman")95 with mail.get_connection() as connection:96 msg = EmailMultiAlternatives(97 subject,98 body=body,99 from_email=sender,100 to=[recipient],101 connection=connection,102 )103 if html:104 msg.attach_alternative(html, "text/html")105 msg.send()106 107 elif current_app.extensions.get("mail", None): # pragma: no cover108 from flask_mail import Message109 110 # In Flask-Mail, sender can be a two element tuple -- (name, address)111 if isinstance(sender, tuple) and len(sender) == 2:112 sender = (str(sender[0]), str(sender[1]))113 else:114 sender = str(sender)115 msg = Message(subject, sender=sender, recipients=[recipient])116 msg.body = body117 msg.html = html118 119 mail = current_app.extensions.get("mail")120 mail.send(msg) # type: ignore121 122 else: # pragma: no cover123 raise ValueError("No email extension configured")124 125 def normalize(self, email: str) -> str:126 """127 Given an input email - return a normalized version or128 raise EmailValidateException if field value isn't syntactically valid.129 130 This is called by forms that use email as an identity to be looked up.131 132 Must be called in app context and uses :py:data:`SECURITY_EMAIL_VALIDATOR_ARGS`133 config variable to pass any relevant arguments to134 email_validator.validate_email() method.135 136 This defaults to NOT checking for deliverability (i.e. DNS checks).137 """138 validator_args = config_value("EMAIL_VALIDATOR_ARGS") or {}139 validator_args["check_deliverability"] = False140 try:141 valid = email_validator.validate_email(email, **validator_args)142 return valid.normalized143 except ValueError:144 raise EmailValidateException(get_message("INVALID_EMAIL_ADDRESS")[0])145 146 def validate(self, email: str) -> str:147 """148 Validate the given email.149 If valid, the normalized version is returned.150 This is used by forms/views that require an email that likely can have an151 actual email sent to it.152 153 Must be called in app context and uses :py:data:`SECURITY_EMAIL_VALIDATOR_ARGS`154 config variable to pass any relevant arguments to155 email_validator.validate_email() method.156 157 EmailValidationException is thrown on invalid email.158 """159 160 validator_args = config_value("EMAIL_VALIDATOR_ARGS") or {}161 try:162 valid = email_validator.validate_email(email, **validator_args)163 return valid.normalized164 except ValueError:165 raise EmailValidateException(get_message("INVALID_EMAIL_ADDRESS")[0])166 