File: mail_util.py

package info (click to toggle)
flask-security 5.6.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,448 kB
  • sloc: python: 23,247; javascript: 204; makefile: 138
file content (165 lines) | stat: -rw-r--r-- 5,952 bytes parent folder | download | duplicates (2)
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])