Deprecate the mail API (#7263)

This commit is contained in:
Kaileshwar R K 2026-03-25 02:01:12 +05:30 committed by GitHub
parent bfe34492fa
commit 54a4c3af89
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 83 additions and 280 deletions

View File

@ -128,7 +128,6 @@ Built-in services
topics/logging
topics/stats
topics/email
topics/telnetconsole
:doc:`topics/logging`
@ -137,9 +136,6 @@ Built-in services
:doc:`topics/stats`
Collect statistics about your scraping crawler.
:doc:`topics/email`
Send email notifications when certain events occur.
:doc:`topics/telnetconsole`
Inspect a running crawler using a built-in Python console.

View File

@ -73,12 +73,6 @@ In the future we plan to add support for the ``async def`` syntax to these APIs
or replace them with other APIs where changing the existing ones isn't
possible.
These APIs don't have a coroutine-based counterpart:
- :class:`~scrapy.mail.MailSender`
- :meth:`~scrapy.mail.MailSender.send`
These APIs have a coroutine-based implementation and a Deferred-based one:
- :class:`scrapy.crawler.Crawler`:
@ -137,18 +131,11 @@ wrapping a :class:`~twisted.internet.defer.Deferred` object into a
:class:`~asyncio.Future` object or vice versa. See :ref:`asyncio-await-dfd` for
more information about this.
For example:
- The :meth:`MailSender.send() <scrapy.mail.MailSender.send>` method returns
a :class:`~twisted.internet.defer.Deferred` object that fires when the
email is sent. You can use this object directly in Deferred-based code or
convert it into a :class:`~asyncio.Future` object with
:func:`~scrapy.utils.defer.maybe_deferred_to_future`.
- A custom scheduler needs to define an ``open()`` method that can return a
:class:`~twisted.internet.defer.Deferred` object. You can write a method
that works with Deferreds and returns one directly, or you can write a
coroutine and convert it into a function that returns a Deferred with
:func:`~scrapy.utils.defer.deferred_f_from_coro_f`.
For example: a custom scheduler needs to define an ``open()`` method that can
return a :class:`~twisted.internet.defer.Deferred` object. You can write a
method that works with Deferreds and returns one directly, or you can write a
coroutine and convert it into a function that returns a Deferred with
:func:`~scrapy.utils.defer.deferred_f_from_coro_f`.
General usage

View File

@ -1,185 +0,0 @@
.. _topics-email:
==============
Sending e-mail
==============
.. module:: scrapy.mail
:synopsis: Email sending facility
Although Python makes sending e-mails relatively easy via the :mod:`smtplib`
library, Scrapy provides its own facility for sending e-mails which is very
easy to use and it's implemented using :doc:`Twisted non-blocking IO
<twisted:core/howto/defer-intro>`, to avoid interfering with the non-blocking
IO of the crawler. It also provides a simple API for sending attachments and
it's very easy to configure, with a few :ref:`settings
<topics-email-settings>`.
Quick example
=============
There are two ways to instantiate the mail sender. You can instantiate it using
the standard ``__init__`` method:
.. code-block:: python
from scrapy.mail import MailSender
mailer = MailSender()
Or you can instantiate it passing a :class:`scrapy.Crawler` instance, which
will respect the :ref:`settings <topics-email-settings>`:
.. skip: start
.. code-block:: python
mailer = MailSender.from_crawler(crawler)
And here is how to use it to send an e-mail (without attachments):
.. code-block:: python
mailer.send(
to=["someone@example.com"],
subject="Some subject",
body="Some body",
cc=["another@example.com"],
)
.. skip: end
MailSender class reference
==========================
The MailSender :ref:`components <topics-components>` is the preferred class to
use for sending emails from Scrapy, as it uses :doc:`Twisted non-blocking IO
<twisted:core/howto/defer-intro>`, like the rest of the framework.
.. class:: MailSender(smtphost=None, mailfrom=None, smtpuser=None, smtppass=None, smtpport=None)
:param smtphost: the SMTP host to use for sending the emails. If omitted, the
:setting:`MAIL_HOST` setting will be used.
:type smtphost: str
:param mailfrom: the address used to send emails (in the ``From:`` header).
If omitted, the :setting:`MAIL_FROM` setting will be used.
:type mailfrom: str
:param smtpuser: the SMTP user. If omitted, the :setting:`MAIL_USER`
setting will be used. If not given, no SMTP authentication will be
performed.
:type smtphost: str or bytes
:param smtppass: the SMTP pass for authentication.
:type smtppass: str or bytes
:param smtpport: the SMTP port to connect to
:type smtpport: int
:param smtptls: enforce using SMTP STARTTLS
:type smtptls: bool
:param smtpssl: enforce using a secure SSL connection
:type smtpssl: bool
.. method:: send(to, subject, body, cc=None, attachs=(), mimetype='text/plain', charset=None)
Send email to the given recipients.
:param to: the e-mail recipients as a string or as a list of strings
:type to: str or list
:param subject: the subject of the e-mail
:type subject: str
:param cc: the e-mails to CC as a string or as a list of strings
:type cc: str or list
:param body: the e-mail body
:type body: str
:param attachs: an iterable of tuples ``(attach_name, mimetype,
file_object)`` where ``attach_name`` is a string with the name that will
appear on the e-mail's attachment, ``mimetype`` is the mimetype of the
attachment and ``file_object`` is a readable file object with the
contents of the attachment
:type attachs: collections.abc.Iterable
:param mimetype: the MIME type of the e-mail
:type mimetype: str
:param charset: the character encoding to use for the e-mail contents
:type charset: str
.. _topics-email-settings:
Mail settings
=============
These settings define the default ``__init__`` method values of the :class:`MailSender`
class, and can be used to configure e-mail notifications in your project without
writing any code (for those extensions and code that uses :class:`MailSender`).
.. setting:: MAIL_FROM
MAIL_FROM
---------
Default: ``'scrapy@localhost'``
Sender email to use (``From:`` header) for sending emails.
.. setting:: MAIL_HOST
MAIL_HOST
---------
Default: ``'localhost'``
SMTP host to use for sending emails.
.. setting:: MAIL_PORT
MAIL_PORT
---------
Default: ``25``
SMTP port to use for sending emails.
.. setting:: MAIL_USER
MAIL_USER
---------
Default: ``None``
User to use for SMTP authentication. If disabled no SMTP authentication will be
performed.
.. setting:: MAIL_PASS
MAIL_PASS
---------
Default: ``None``
Password to use for SMTP authentication, along with :setting:`MAIL_USER`.
.. setting:: MAIL_TLS
MAIL_TLS
--------
Default: ``False``
Enforce using STARTTLS. STARTTLS is a way to take an existing insecure connection, and upgrade it to a secure connection using SSL/TLS.
.. setting:: MAIL_SSL
MAIL_SSL
--------
Default: ``False``
Enforce connecting using an SSL encrypted connection

View File

@ -175,20 +175,16 @@ Memory usage extension
Monitors the memory used by the Scrapy process that runs the spider and:
1. sends a notification e-mail when it exceeds a certain value
2. closes the spider when it exceeds a certain value
The notification e-mails can be triggered when a certain warning value is
reached (:setting:`MEMUSAGE_WARNING_MB`) and when the maximum value is reached
(:setting:`MEMUSAGE_LIMIT_MB`) which will also cause the spider to be closed
and the Scrapy process to be terminated.
1. sends a :signal:`memusage_warning_reached` signal when it exceeds
:setting:`MEMUSAGE_WARNING_MB`
2. closes the spider with the `"memusage_exceeded"` reason when it exceeds
:setting:`MEMUSAGE_LIMIT_MB`
This extension is enabled by the :setting:`MEMUSAGE_ENABLED` setting and
can be configured with the following settings:
* :setting:`MEMUSAGE_LIMIT_MB`
* :setting:`MEMUSAGE_WARNING_MB`
* :setting:`MEMUSAGE_NOTIFY_MAIL`
* :setting:`MEMUSAGE_CHECK_INTERVAL_SECONDS`
Memory debugger extension
@ -332,24 +328,6 @@ closing the spider. If the spider generates more than that number of errors,
it will be closed with the reason ``closespider_errorcount``. If zero (or non
set), spiders won't be closed by number of errors.
StatsMailer extension
~~~~~~~~~~~~~~~~~~~~~
.. module:: scrapy.extensions.statsmailer
:synopsis: StatsMailer extension
.. class:: StatsMailer
This simple extension can be used to send a notification e-mail every time a
domain has finished scraping, including the Scrapy stats collected. The email
will be sent to all recipients specified in the :setting:`STATSMAILER_RCPTS`
setting.
Emails can be sent using the :class:`~scrapy.mail.MailSender` class. To see a
full list of parameters, including examples on how to instantiate
:class:`~scrapy.mail.MailSender` and use mail settings, see
:ref:`topics-email`.
.. module:: scrapy.extensions.debug
:synopsis: Extensions for debugging Scrapy

View File

@ -1562,13 +1562,12 @@ MEMUSAGE_ENABLED
Default: ``True``
Scope: ``scrapy.extensions.memusage``
Scope: ``scrapy.extensions.memusage.MemoryUsage``
Whether to enable the memory usage extension. This extension keeps track of
a peak memory used by the process (it writes it to stats). It can also
optionally shutdown the Scrapy process when it exceeds a memory limit
(see :setting:`MEMUSAGE_LIMIT_MB`), and notify by email when that happened
(see :setting:`MEMUSAGE_NOTIFY_MAIL`).
(see :setting:`MEMUSAGE_LIMIT_MB`).
See :ref:`topics-extensions-ref-memusage`.
@ -1579,10 +1578,11 @@ MEMUSAGE_LIMIT_MB
Default: ``0``
Scope: ``scrapy.extensions.memusage``
Scope: ``scrapy.extensions.memusage.MemoryUsage``
The maximum amount of memory to allow (in megabytes) before shutting down
Scrapy (if MEMUSAGE_ENABLED is True). If zero, no check will be performed.
Scrapy (if :setting:`MEMUSAGE_ENABLED` is ``True``). If zero, no check will be
performed.
See :ref:`topics-extensions-ref-memusage`.
@ -1593,7 +1593,7 @@ MEMUSAGE_CHECK_INTERVAL_SECONDS
Default: ``60.0``
Scope: ``scrapy.extensions.memusage``
Scope: ``scrapy.extensions.memusage.MemoryUsage``
The :ref:`Memory usage extension <topics-extensions-ref-memusage>`
checks the current memory usage, versus the limits set by
@ -1604,23 +1604,6 @@ This sets the length of these intervals, in seconds.
See :ref:`topics-extensions-ref-memusage`.
.. setting:: MEMUSAGE_NOTIFY_MAIL
MEMUSAGE_NOTIFY_MAIL
--------------------
Default: ``False``
Scope: ``scrapy.extensions.memusage``
A list of emails to notify if the memory limit has been reached.
Example::
MEMUSAGE_NOTIFY_MAIL = ['user@example.com']
See :ref:`topics-extensions-ref-memusage`.
.. setting:: MEMUSAGE_WARNING_MB
MEMUSAGE_WARNING_MB
@ -1628,10 +1611,13 @@ MEMUSAGE_WARNING_MB
Default: ``0``
Scope: ``scrapy.extensions.memusage``
Scope: ``scrapy.extensions.memusage.MemoryUsage``
The maximum amount of memory to allow (in megabytes) before sending a warning
email notifying about it. If zero, no warning will be produced.
The maximum amount of memory to allow (in megabytes) before sending a
:signal:`memusage_warning_reached` signal (if :setting:`MEMUSAGE_ENABLED` is
``True``). If zero, no signal will be sent.
See :ref:`topics-extensions-ref-memusage`.
.. setting:: NEWSPIDER_MODULE
@ -1983,16 +1969,6 @@ finishes.
For more info see: :ref:`topics-stats`.
.. setting:: STATSMAILER_RCPTS
STATSMAILER_RCPTS
-----------------
Default: ``[]`` (empty list)
Send Scrapy stats after spiders finish scraping. See
:class:`~scrapy.extensions.statsmailer.StatsMailer` for more info.
.. setting:: TELNETCONSOLE_ENABLED
TELNETCONSOLE_ENABLED

View File

@ -356,6 +356,18 @@ feed_exporter_closed
This signal supports :ref:`asynchronous handlers <signal-deferred>`.
memusage_warning_reached
~~~~~~~~~~~~~~~~~~~~~~~~
.. signal:: memusage_warning_reached
.. function:: memusage_warning_reached()
Sent by the :class:`~scrapy.extensions.memusage.MemoryUsage` extension when the
memory usage reaches the warning threshold (:setting:`MEMUSAGE_WARNING_MB`).
This signal does not support :ref:`asynchronous handlers <signal-deferred>`.
Request signals
---------------

View File

@ -9,13 +9,13 @@ from __future__ import annotations
import logging
import socket
import sys
import warnings
from importlib import import_module
from pprint import pformat
from typing import TYPE_CHECKING
from scrapy import signals
from scrapy.exceptions import NotConfigured
from scrapy.mail import MailSender
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
from scrapy.utils.asyncio import AsyncioLoopingCall, create_looping_call
from scrapy.utils.defer import _schedule_coro
from scrapy.utils.engine import get_engine_status
@ -45,12 +45,23 @@ class MemoryUsage:
self.crawler: Crawler = crawler
self.warned: bool = False
self.notify_mails: list[str] = crawler.settings.getlist("MEMUSAGE_NOTIFY_MAIL")
if self.notify_mails: # pragma: no cover
from scrapy.mail import MailSender # noqa: PLC0415
warnings.warn(
"The 'MEMUSAGE_NOTIFY_MAIL' setting is deprecated and will be removed "
"in a future release. Please use the 'memusage_warning_reached' and 'spider_closed' "
"signals to implement custom notifications.",
category=ScrapyDeprecationWarning,
stacklevel=2,
)
self.mail = MailSender.from_crawler(crawler)
self.limit: int = crawler.settings.getint("MEMUSAGE_LIMIT_MB") * 1024 * 1024
self.warning: int = crawler.settings.getint("MEMUSAGE_WARNING_MB") * 1024 * 1024
self.check_interval: float = crawler.settings.getfloat(
"MEMUSAGE_CHECK_INTERVAL_SECONDS"
)
self.mail: MailSender = MailSender.from_crawler(crawler)
crawler.signals.connect(self.engine_started, signal=signals.engine_started)
crawler.signals.connect(self.engine_stopped, signal=signals.engine_stopped)
@ -128,6 +139,7 @@ class MemoryUsage:
assert self.crawler.stats
if self.get_virtual_size() > self.warning:
self.crawler.stats.set_value("memusage/warning_reached", 1)
self.crawler.signals.send_catch_log(signal=signals.memusage_warning_reached)
mem = self.warning / 1024 / 1024
logger.warning(
"Memory usage reached %(memusage)dMiB",

View File

@ -6,10 +6,11 @@ Use STATSMAILER_RCPTS setting to enable and give the recipient mail address
from __future__ import annotations
import warnings
from typing import TYPE_CHECKING
from scrapy import Spider, signals
from scrapy.exceptions import NotConfigured
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
from scrapy.mail import MailSender
if TYPE_CHECKING:
@ -21,6 +22,12 @@ if TYPE_CHECKING:
from scrapy.crawler import Crawler
from scrapy.statscollectors import StatsCollector
warnings.warn(
"The scrapy.extensions.statsmailer module is deprecated and will be "
"removed in a future release.",
category=ScrapyDeprecationWarning,
)
class StatsMailer:
def __init__(self, stats: StatsCollector, recipients: list[str], mail: MailSender):

View File

@ -7,6 +7,7 @@ See documentation in docs/topics/email.rst
from __future__ import annotations
import logging
import warnings
from email import encoders as Encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
@ -19,6 +20,7 @@ from typing import IO, TYPE_CHECKING, Any
from twisted.internet import ssl
from twisted.internet.defer import Deferred
from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.misc import arg_to_iter
from scrapy.utils.python import to_bytes
@ -49,6 +51,13 @@ def _to_bytes_or_none(text: str | bytes | None) -> bytes | None:
return to_bytes(text)
warnings.warn(
"The scrapy.mail module is deprecated and will be removed in a future release. "
"Please use a dedicated Python mail library instead.",
category=ScrapyDeprecationWarning,
)
class MailSender:
def __init__(
self,
@ -71,7 +80,7 @@ class MailSender:
self.debug: bool = debug
@classmethod
def from_crawler(cls, crawler: Crawler) -> Self:
def from_crawler(cls, crawler: Crawler) -> Self: # pragma: no cover
settings = crawler.settings
return cls(
smtphost=settings["MAIL_HOST"],

View File

@ -12,6 +12,7 @@ spider_opened = object()
spider_idle = object()
spider_closed = object()
spider_error = object()
memusage_warning_reached = object()
request_scheduled = object()
request_dropped = object()
request_reached_downloader = object()

View File

@ -4,12 +4,18 @@ import pytest
from scrapy import signals
from scrapy.exceptions import NotConfigured
from scrapy.extensions import statsmailer
from scrapy.mail import MailSender
from scrapy.signalmanager import SignalManager
from scrapy.statscollectors import StatsCollector
from scrapy.utils.spider import DefaultSpider
pytestmark = pytest.mark.filterwarnings(
"ignore:The scrapy.extensions.statsmailer module is deprecated:scrapy.exceptions.ScrapyDeprecationWarning",
"ignore:The scrapy.mail module is deprecated:scrapy.exceptions.ScrapyDeprecationWarning",
)
from scrapy.extensions import statsmailer # noqa: E402
from scrapy.mail import MailSender # noqa: E402
@pytest.fixture
def dummy_stats():

View File

@ -5,11 +5,18 @@ import pytest
from twisted.internet import defer
from twisted.internet._sslverify import ClientTLSOptions
from scrapy.mail import MailSender
pytestmark = pytest.mark.filterwarnings(
"ignore:The scrapy.mail module is deprecated:scrapy.exceptions.ScrapyDeprecationWarning"
)
from scrapy.mail import MailSender # noqa: E402
@pytest.mark.requires_reactor # MailSender requires a reactor
class TestMailSender:
def _catch_mail_sent(self, **kwargs):
self.catched_msg = {**kwargs}
def test_send(self):
mailsender = MailSender(debug=True)
mailsender.send(
@ -88,9 +95,6 @@ class TestMailSender:
assert text.get_charset() == Charset("us-ascii")
assert attach.get_payload(decode=True) == b"content"
def _catch_mail_sent(self, **kwargs):
self.catched_msg = {**kwargs}
def test_send_utf8(self):
subject = "sübjèçt"
body = "bödÿ-àéïöñß"