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/logging
topics/stats topics/stats
topics/email
topics/telnetconsole topics/telnetconsole
:doc:`topics/logging` :doc:`topics/logging`
@ -137,9 +136,6 @@ Built-in services
:doc:`topics/stats` :doc:`topics/stats`
Collect statistics about your scraping crawler. Collect statistics about your scraping crawler.
:doc:`topics/email`
Send email notifications when certain events occur.
:doc:`topics/telnetconsole` :doc:`topics/telnetconsole`
Inspect a running crawler using a built-in Python console. 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 or replace them with other APIs where changing the existing ones isn't
possible. 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: These APIs have a coroutine-based implementation and a Deferred-based one:
- :class:`scrapy.crawler.Crawler`: - :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 :class:`~asyncio.Future` object or vice versa. See :ref:`asyncio-await-dfd` for
more information about this. more information about this.
For example: 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
- The :meth:`MailSender.send() <scrapy.mail.MailSender.send>` method returns method that works with Deferreds and returns one directly, or you can write a
a :class:`~twisted.internet.defer.Deferred` object that fires when the coroutine and convert it into a function that returns a Deferred with
email is sent. You can use this object directly in Deferred-based code or :func:`~scrapy.utils.defer.deferred_f_from_coro_f`.
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`.
General usage 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: 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 1. sends a :signal:`memusage_warning_reached` signal when it exceeds
2. closes the spider when it exceeds a certain value :setting:`MEMUSAGE_WARNING_MB`
2. closes the spider with the `"memusage_exceeded"` reason when it exceeds
The notification e-mails can be triggered when a certain warning value is :setting:`MEMUSAGE_LIMIT_MB`
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.
This extension is enabled by the :setting:`MEMUSAGE_ENABLED` setting and This extension is enabled by the :setting:`MEMUSAGE_ENABLED` setting and
can be configured with the following settings: can be configured with the following settings:
* :setting:`MEMUSAGE_LIMIT_MB` * :setting:`MEMUSAGE_LIMIT_MB`
* :setting:`MEMUSAGE_WARNING_MB` * :setting:`MEMUSAGE_WARNING_MB`
* :setting:`MEMUSAGE_NOTIFY_MAIL`
* :setting:`MEMUSAGE_CHECK_INTERVAL_SECONDS` * :setting:`MEMUSAGE_CHECK_INTERVAL_SECONDS`
Memory debugger extension 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 it will be closed with the reason ``closespider_errorcount``. If zero (or non
set), spiders won't be closed by number of errors. 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 .. module:: scrapy.extensions.debug
:synopsis: Extensions for debugging Scrapy :synopsis: Extensions for debugging Scrapy

View File

@ -1562,13 +1562,12 @@ MEMUSAGE_ENABLED
Default: ``True`` Default: ``True``
Scope: ``scrapy.extensions.memusage`` Scope: ``scrapy.extensions.memusage.MemoryUsage``
Whether to enable the memory usage extension. This extension keeps track of 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 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 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_LIMIT_MB`).
(see :setting:`MEMUSAGE_NOTIFY_MAIL`).
See :ref:`topics-extensions-ref-memusage`. See :ref:`topics-extensions-ref-memusage`.
@ -1579,10 +1578,11 @@ MEMUSAGE_LIMIT_MB
Default: ``0`` Default: ``0``
Scope: ``scrapy.extensions.memusage`` Scope: ``scrapy.extensions.memusage.MemoryUsage``
The maximum amount of memory to allow (in megabytes) before shutting down 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`. See :ref:`topics-extensions-ref-memusage`.
@ -1593,7 +1593,7 @@ MEMUSAGE_CHECK_INTERVAL_SECONDS
Default: ``60.0`` Default: ``60.0``
Scope: ``scrapy.extensions.memusage`` Scope: ``scrapy.extensions.memusage.MemoryUsage``
The :ref:`Memory usage extension <topics-extensions-ref-memusage>` The :ref:`Memory usage extension <topics-extensions-ref-memusage>`
checks the current memory usage, versus the limits set by 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`. 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 .. setting:: MEMUSAGE_WARNING_MB
MEMUSAGE_WARNING_MB MEMUSAGE_WARNING_MB
@ -1628,10 +1611,13 @@ MEMUSAGE_WARNING_MB
Default: ``0`` Default: ``0``
Scope: ``scrapy.extensions.memusage`` Scope: ``scrapy.extensions.memusage.MemoryUsage``
The maximum amount of memory to allow (in megabytes) before sending a warning The maximum amount of memory to allow (in megabytes) before sending a
email notifying about it. If zero, no warning will be produced. :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 .. setting:: NEWSPIDER_MODULE
@ -1983,16 +1969,6 @@ finishes.
For more info see: :ref:`topics-stats`. 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 .. setting:: TELNETCONSOLE_ENABLED
TELNETCONSOLE_ENABLED TELNETCONSOLE_ENABLED

View File

@ -356,6 +356,18 @@ feed_exporter_closed
This signal supports :ref:`asynchronous handlers <signal-deferred>`. 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 Request signals
--------------- ---------------

View File

@ -9,13 +9,13 @@ from __future__ import annotations
import logging import logging
import socket import socket
import sys import sys
import warnings
from importlib import import_module from importlib import import_module
from pprint import pformat from pprint import pformat
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from scrapy import signals from scrapy import signals
from scrapy.exceptions import NotConfigured from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
from scrapy.mail import MailSender
from scrapy.utils.asyncio import AsyncioLoopingCall, create_looping_call from scrapy.utils.asyncio import AsyncioLoopingCall, create_looping_call
from scrapy.utils.defer import _schedule_coro from scrapy.utils.defer import _schedule_coro
from scrapy.utils.engine import get_engine_status from scrapy.utils.engine import get_engine_status
@ -45,12 +45,23 @@ class MemoryUsage:
self.crawler: Crawler = crawler self.crawler: Crawler = crawler
self.warned: bool = False self.warned: bool = False
self.notify_mails: list[str] = crawler.settings.getlist("MEMUSAGE_NOTIFY_MAIL") 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.limit: int = crawler.settings.getint("MEMUSAGE_LIMIT_MB") * 1024 * 1024
self.warning: int = crawler.settings.getint("MEMUSAGE_WARNING_MB") * 1024 * 1024 self.warning: int = crawler.settings.getint("MEMUSAGE_WARNING_MB") * 1024 * 1024
self.check_interval: float = crawler.settings.getfloat( self.check_interval: float = crawler.settings.getfloat(
"MEMUSAGE_CHECK_INTERVAL_SECONDS" "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_started, signal=signals.engine_started)
crawler.signals.connect(self.engine_stopped, signal=signals.engine_stopped) crawler.signals.connect(self.engine_stopped, signal=signals.engine_stopped)
@ -128,6 +139,7 @@ class MemoryUsage:
assert self.crawler.stats assert self.crawler.stats
if self.get_virtual_size() > self.warning: if self.get_virtual_size() > self.warning:
self.crawler.stats.set_value("memusage/warning_reached", 1) 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 mem = self.warning / 1024 / 1024
logger.warning( logger.warning(
"Memory usage reached %(memusage)dMiB", "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 from __future__ import annotations
import warnings
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from scrapy import Spider, signals from scrapy import Spider, signals
from scrapy.exceptions import NotConfigured from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
from scrapy.mail import MailSender from scrapy.mail import MailSender
if TYPE_CHECKING: if TYPE_CHECKING:
@ -21,6 +22,12 @@ if TYPE_CHECKING:
from scrapy.crawler import Crawler from scrapy.crawler import Crawler
from scrapy.statscollectors import StatsCollector 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: class StatsMailer:
def __init__(self, stats: StatsCollector, recipients: list[str], mail: MailSender): 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 from __future__ import annotations
import logging import logging
import warnings
from email import encoders as Encoders from email import encoders as Encoders
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart 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 import ssl
from twisted.internet.defer import Deferred from twisted.internet.defer import Deferred
from scrapy.exceptions import ScrapyDeprecationWarning
from scrapy.utils.misc import arg_to_iter from scrapy.utils.misc import arg_to_iter
from scrapy.utils.python import to_bytes 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) 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: class MailSender:
def __init__( def __init__(
self, self,
@ -71,7 +80,7 @@ class MailSender:
self.debug: bool = debug self.debug: bool = debug
@classmethod @classmethod
def from_crawler(cls, crawler: Crawler) -> Self: def from_crawler(cls, crawler: Crawler) -> Self: # pragma: no cover
settings = crawler.settings settings = crawler.settings
return cls( return cls(
smtphost=settings["MAIL_HOST"], smtphost=settings["MAIL_HOST"],

View File

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

View File

@ -4,12 +4,18 @@ import pytest
from scrapy import signals from scrapy import signals
from scrapy.exceptions import NotConfigured from scrapy.exceptions import NotConfigured
from scrapy.extensions import statsmailer
from scrapy.mail import MailSender
from scrapy.signalmanager import SignalManager from scrapy.signalmanager import SignalManager
from scrapy.statscollectors import StatsCollector from scrapy.statscollectors import StatsCollector
from scrapy.utils.spider import DefaultSpider 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 @pytest.fixture
def dummy_stats(): def dummy_stats():

View File

@ -5,11 +5,18 @@ import pytest
from twisted.internet import defer from twisted.internet import defer
from twisted.internet._sslverify import ClientTLSOptions 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 @pytest.mark.requires_reactor # MailSender requires a reactor
class TestMailSender: class TestMailSender:
def _catch_mail_sent(self, **kwargs):
self.catched_msg = {**kwargs}
def test_send(self): def test_send(self):
mailsender = MailSender(debug=True) mailsender = MailSender(debug=True)
mailsender.send( mailsender.send(
@ -88,9 +95,6 @@ class TestMailSender:
assert text.get_charset() == Charset("us-ascii") assert text.get_charset() == Charset("us-ascii")
assert attach.get_payload(decode=True) == b"content" assert attach.get_payload(decode=True) == b"content"
def _catch_mail_sent(self, **kwargs):
self.catched_msg = {**kwargs}
def test_send_utf8(self): def test_send_utf8(self):
subject = "sübjèçt" subject = "sübjèçt"
body = "bödÿ-àéïöñß" body = "bödÿ-àéïöñß"