Skip to content
Back to top

# Source code for django_contact_form.forms

    """
    A base contact form for allowing users to send email messages through a web
    interface.

    """

    # SPDX-License-Identifier: BSD-3-Clause

    from django import forms
    from django.conf import settings
    from django.contrib.sites.shortcuts import get_current_site
    from django.core.mail import send_mail
    from django.template import loader
    from django.utils.translation import gettext_lazy as _

    class ContactForm(forms.Form):
        """
        The base contact form class from which all contact form classes should inherit.

        If you don't need any customization, you can use this form to provide basic
        contact-form functionality; it will collect name, email address and message.

        The :class:`~django_contact_form.views.ContactFormView` included in this application
        knows how to work with this form and can handle many types of subclasses as well
        (see below for a discussion of the important points), so in many cases it will be
        all that you need. If you'd like to use this form or a subclass of it from one of
        your own views, here's how:

        1. When you instantiate the form, pass the current :class:`~django.http.HttpRequest`
           object as the keyword argument ``request``; this is used internally by the base
           implementation, and also made available so that subclasses can add functionality
           which relies on inspecting the request (such as spam filtering).

        2. To send the message, call the form's :meth:`save` method, which accepts the
           keyword argument ``fail_silently`` and defaults it to :data:`False`. This
           argument is passed directly to Django's :func:`~django.core.mail.send_mail`
           function, and allows you to suppress or raise exceptions as needed for
           debugging. The :meth:`save` method has no return value.

        Other than that, treat it like any other form; validity checks and validated data
        are handled normally, through the :meth:`~django.forms.Form.is_valid` method and the
        :attr:`~django.forms.Form.cleaned_data` dictionary.

        Under the hood, this form uses a somewhat abstracted interface in order to make it
        easier to subclass and add functionality.

        Customizing behavior in subclasses
        ``````````````````````````````````

        The following attributes play a role in determining behavior, and any of them can be
        implemented as an attribute or as a method (for example, if you wish to have
        :attr:`from_email` be dynamic, you can implement a method named :meth:`from_email`
        instead of setting the attribute :attr:`from_email`).

        .. attribute:: from_email

           The email address (:class:`str`) to use in the ``From:`` header of the
           message. By default, this is the value of the Django setting
           ```DEFAULT_FROM_EMAIL``.

        .. attribute:: recipient_list

           A :class:`list` of recipients for the message. By default, this is the email
           addresses specified in the Django setting ``MANAGERS``.

        .. attribute:: subject_template_name

           A :class:`str`, the name of the template to use when rendering the subject line
           of the message. By default, this is
           ``"django_contact_form/contact_form_subject.txt"``.

        .. attribute:: template_name

           A :class:`str`, the name of the template to use when rendering the body of the
           message. By default, this is ``"django_contact_form/contact_form.txt"``.

        And two methods are involved in producing the contents of the message to send:

        .. automethod:: message

        .. automethod:: subject

        Finally, the message itself is generated by the following two methods:

        .. automethod:: get_message_dict

        .. automethod:: get_message_context

        Other attributes/methods
        ````````````````````````

        Meanwhile, the following attributes/methods generally should not be overridden;
        doing so may interfere with functionality, may not accomplish what you want, and
        generally any desired customization can be accomplished in a more straightforward
        way through overriding one of the attributes/methods listed above.

        .. attribute:: request

           The :class:`~django.http.HttpRequest` object representing the current
           request. This is set automatically in ``__init__()``, and is used both to
           generate a :class:`~django.template.RequestContext` for the templates and to
           allow subclasses to engage in request-specific behavior.

        .. automethod:: save

        Note that subclasses which override ``__init__`` or :meth:`save` need to accept
        ``*args`` and ``**kwargs``, and pass them via :func:`super`, in order to preserve
        behavior (each of those methods accepts at least one additional argument, and this
        application expects and requires them to do so).

        """

        name = forms.CharField(max_length=100, label=_("Your name"))
        email = forms.EmailField(max_length=200, label=_("Your email address"))
        body = forms.CharField(widget=forms.Textarea, label=_("Your message"))

        from_email = settings.DEFAULT_FROM_EMAIL

        recipient_list = [mail_tuple[1] for mail_tuple in settings.MANAGERS]

        subject_template_name = "django_contact_form/contact_form_subject.txt"

        template_name = "django_contact_form/contact_form.txt"

        def __init__(
            self, *args, data=None, files=None, request=None, recipient_list=None, **kwargs
        ):
            if request is None:
                raise TypeError("Keyword argument 'request' must be supplied")
            self.request = request
            if recipient_list is not None:
                self.recipient_list = recipient_list
            super().__init__(data=data, files=files, *args, **kwargs)  # noqa: B026

        def message(self) -> str:
            """
            Return the body of the message to send. By default, this is accomplished by
            rendering the template name specified in :attr:`template_name`.

            """
            template_name = (
                self.template_name()  # pylint: disable=not-callable
                if callable(self.template_name)
                else self.template_name
            )
            return loader.render_to_string(
                template_name, self.get_message_context(), request=self.request
            )

        def subject(self) -> str:
            """
            Return the subject line of the message to send. By default, this is
            accomplished by rendering the template name specified in
            :attr:`subject_template_name`.

            .. warning:: **Subject must be a single line**

               The subject of an email is sent in a header (named ``Subject:``). Because
               email uses newlines as a separator between headers, newlines in the subject
               can cause it to be interpreted as multiple headers; this is the `header
               injection attack <https://en.wikipedia.org/wiki/Email_injection>`_. To
               prevent this, :meth:`subject` will always force the subject to a single line
               of text, stripping all newline characters. If you override :meth:`subject`,
               be sure to either do this manually, or use :class:`super` to call the parent
               implementation.

            """
            template_name = (
                self.subject_template_name()  # pylint: disable=not-callable
                if callable(self.subject_template_name)
                else self.subject_template_name
            )
            subject = loader.render_to_string(
                template_name, self.get_message_context(), request=self.request
            )
            return "".join(subject.splitlines())

        def get_message_context(self) -> dict:
            """
            Return the context used to render the templates for the email
            subject and body.

            The default context will be a :class:`~django.template.RequestContext` (using
            the current HTTP request, so user information is available), plus the contents
            of the form's :attr:`~django.forms.Form.cleaned_data` dictionary, and one
            additional variable:

            ``site``
              If ``django.contrib.sites`` is installed, the currently-active
              :class:`~django.contrib.sites.models.Site` object. Otherwise, a
              :class:`~django.contrib.sites.requests.RequestSite` object generated from the
              request.

            """
            if not self.is_valid():
                raise ValueError("Cannot generate Context from invalid contact form")
            return dict(self.cleaned_data, site=get_current_site(self.request))

        def get_message_dict(self) -> dict:
            """
            Generate the parts of the message and return them in a dictionary suitable
            for passing as keyword arguments to Django's
            :func:`~django.core.mail.send_mail`.

            By default, this method will collect and return :attr:`from_email`,
            :attr:`recipient_list`, :meth:`message` and :meth:`subject`. Overriding this
            allows essentially unlimited customization of how the message is generated. Note
            that for compatibility, implementations which override this should support
            callables for the values of :attr:`from_email` and :attr:`recipient_list`.

            """
            if not self.is_valid():
                raise ValueError("Message cannot be sent from invalid contact form")
            message_dict = {}
            for message_part in ("from_email", "message", "recipient_list", "subject"):
                attr = getattr(self, message_part)
                message_dict[message_part] = attr() if callable(attr) else attr
            return message_dict

        def save(self, fail_silently=False):
            """
            If the form has data and is valid, construct and send the email.

            By default, this is done by obtaining the parts of the email from
            :meth:`get_message_dict` and passing the result to Django's
            :func:`~django.core.mail.send_mail` function.

            """
            send_mail(fail_silently=fail_silently, **self.get_message_dict())

    class AkismetContactForm(ContactForm):
        """
        A subclass of :class:`ContactForm` which adds spam filtering, via `the Akismet
        spam-detection service <https://akismet.com/>`_.

        Use of this class requires you to provide configuration for the Akismet web service.
        You'll need to obtain an Akismet API key, and you'll need to associate it with the
        site you'll use the contact form on. You can do this at <https://akismet.com/>. Once
        you have, put your Akismet API key in the environment variable
        ``PYTHON_AKISMET_API_KEY``, and the URL it's associated with in the environment
        variable ``PYTHON_AKISMET_BLOG_URL``.

        You will also need `the Python Akismet module <http://akismet.readthedocs.io/>`_ to
        communicate with the Akismet web service. You can install it manually (if you do,
        install at least version 24.5.0), or ``django-contact-form`` can install it
        automatically for you if you tell ``pip`` to install
        ``"django-contact-form[akismet]"``.

        Once you have an Akismet API key and URL configured, and the ``akismet`` module
        installed, you can drop in :class:`AkismetContactForm` anywhere you would have used
        :class:`ContactForm`. A URLconf is also provided in django-contact-form, at
        ``django_contact_form.akismet_urls``, which will set up :class:`AkismetContactForm`
        for you in place of the base contact form class.

        If you want to customize the spam-filtering behavior, there are two methods you can
        override:

        .. automethod:: get_akismet_check_arguments
        .. automethod:: get_akismet_client

        """

        SPAM_MESSAGE = _("Your message was classified as spam.")

        def get_akismet_check_arguments(self) -> dict:
            """
            Return the arguments which will be passed to the Akismet spam check.

            If your form contains additional fields which need to have their contents passed
            to Akismet, override this to ensure those arguments are correctly set.

            """
            return {
                "user_ip": self.request.META["REMOTE_ADDR"],
                "user_agent": self.request.META.get("HTTP_USER_AGENT"),
                "comment_author": self.cleaned_data.get("name"),
                "comment_author_email": self.cleaned_data.get("email"),
                "comment_content": self.cleaned_data["body"],
                "comment_type": "contact-form",
            }

        def get_akismet_client(self) -> "akismet.SyncClient":  # noqa: F821
            """
            Obtain and return an Akismet API client.

            By default, this will create a single API client instance and keep it resident
            in memory for the life of the Python process.

            If you need to customize the Akismet client creation (for example, to pass
            custom arguments to the Akismet API client), override this method.

            *Note:* Only synchronous Akismet clients (:class:`akismet.SyncClient`) are
            supported here; async clients (:class:`akismet.AsyncClient`) are not.

            """
            from ._akismet import (  # pylint: disable=import-outside-toplevel
                _try_get_akismet_client,
            )

            return _try_get_akismet_client()

        def clean_body(self):
            """
            Apply Akismet spam filtering to the submission.

            """
            akismet_client = self.get_akismet_client()

            if akismet_client.comment_check(**self.get_akismet_check_arguments()):
                raise forms.ValidationError(self.SPAM_MESSAGE)
            return self.cleaned_data["body"]