Improve efficiency of NotifyEmail plugin (#679)

When addressing multiple recipients, use the same session to the SMTP
server as designated with the Apprise URL. In this way, subsequent full
roundtrips will be saved.

As many SMTP servers are employing connection rate limiting, as well as
connection accept delays, this will considerably improve both robustness
and performance.
This commit is contained in:
Andreas Motl 2022-11-03 11:26:46 -07:00 committed by GitHub
parent b64e1b7ce0
commit e7255df1da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 109 additions and 46 deletions

View File

@ -22,10 +22,11 @@ The contributors have been listed in chronological order:
* Hitesh Sondhi <hitesh@cropsly.com>
* Mar 2019 - Added Flock Support
* Andreas Motl <andreas@getkotori.org>
* Andreas Motl <andreas.motl@panodata.org>
* Mar 2020 - Fix XMPP Support
* Oct 2022 - Drop support for Python 2
* Oct 2022 - Add support for Python 3.11
* Oct 2022 - Improve efficiency of NotifyEmail
* Joey Espinosa <@particledecay>
* Apr 3rd 2022 - Added Ntfy Support

View File

@ -23,8 +23,10 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import dataclasses
import re
import smtplib
import typing as t
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
@ -282,6 +284,13 @@ EMAIL_TEMPLATES = (
)
@dataclasses.dataclass
class EmailMessage:
recipient: str
to_addrs: t.List[str]
body: str
class NotifyEmail(NotifyBase):
"""
A wrapper to Email Notifications
@ -659,15 +668,14 @@ class NotifyEmail(NotifyBase):
# Initialize our default from name
from_name = self.from_name if self.from_name else self.app_desc
# error tracking (used for function return)
has_error = False
if not self.targets:
# There is no one to email; we're done
self.logger.warning(
'There are no Email recipients to notify')
return False
messages: t.List[EmailMessage] = []
# Create a copy of the targets list
emails = list(self.targets)
while len(emails):
@ -778,6 +786,19 @@ class NotifyEmail(NotifyBase):
if reply_to:
base['Reply-To'] = ','.join(reply_to)
message = EmailMessage(
recipient=to_addr,
to_addrs=[to_addr] + list(cc) + list(bcc),
body=base.as_string())
messages.append(message)
return self.submit(messages)
def submit(self, messages: t.List[EmailMessage]):
# error tracking (used for function return)
has_error = False
# bind the socket variable to the current namespace
socket = None
@ -808,22 +829,30 @@ class NotifyEmail(NotifyBase):
self.logger.debug('Applying user credentials...')
socket.login(self.user, self.password)
# Send the email
# Send the emails
for message in messages:
try:
socket.sendmail(
self.from_addr,
[to_addr] + list(cc) + list(bcc),
base.as_string())
message.to_addrs,
message.body)
self.logger.info(
'Sent Email notification to "{}".'.format(to_addr))
f'Sent Email notification to "{message.recipient}".')
except (SocketError, smtplib.SMTPException, RuntimeError) as e:
self.logger.warning(
f'Sending email to "{message.recipient}" failed. '
f'Reason: {e}')
# Mark as failure
has_error = True
except (SocketError, smtplib.SMTPException, RuntimeError) as e:
self.logger.warning(
'A Connection error occurred sending Email '
'notification to {}.'.format(self.smtp_host))
self.logger.debug('Socket Exception: %s' % str(e))
f'Connection error while submitting email to {self.smtp_host}.'
f' Reason: {e}')
# Mark our failure
# Mark as failure
has_error = True
finally:

View File

@ -2,6 +2,7 @@
certifi
# Application dependencies.
dataclasses; python_version<"3.7"
requests
requests-oauthlib
click >= 5.0

View File

@ -538,6 +538,38 @@ def test_plugin_email_smtplib_send_okay(mock_smtplib):
AttachBase.max_file_size = max_file_size
@mock.patch('smtplib.SMTP')
def test_plugin_email_smtplib_send_multiple_recipients(mock_smtplib):
"""
Verify that NotifyEmail() will use a single SMTP session for submitting
multiple emails.
"""
# Defaults to HTML
obj = Apprise.instantiate(
'mailto://user:pass@mail.example.org?'
'to=foo@example.net,bar@example.com&'
'cc=baz@example.org&bcc=qux@example.org', suppress_exceptions=False)
assert isinstance(obj, NotifyEmail)
assert obj.notify(
body='body', title='test', notify_type=NotifyType.INFO) is True
assert mock_smtplib.mock_calls == [
mock.call('mail.example.org', 25, None, timeout=15),
mock.call().login('user', 'pass'),
mock.call().sendmail(
'user@mail.example.org',
['foo@example.net', 'baz@example.org', 'qux@example.org'],
mock.ANY),
mock.call().sendmail(
'user@mail.example.org',
['bar@example.com', 'baz@example.org', 'qux@example.org'],
mock.ANY),
mock.call().quit(),
]
@mock.patch('smtplib.SMTP')
def test_plugin_email_smtplib_internationalization(mock_smtp):
"""