mirror of https://github.com/caronc/apprise
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.pull/750/head
parent
b64e1b7ce0
commit
e7255df1da
|
@ -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
|
||||
|
|
|
@ -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,58 +786,79 @@ class NotifyEmail(NotifyBase):
|
|||
if reply_to:
|
||||
base['Reply-To'] = ','.join(reply_to)
|
||||
|
||||
# bind the socket variable to the current namespace
|
||||
socket = None
|
||||
message = EmailMessage(
|
||||
recipient=to_addr,
|
||||
to_addrs=[to_addr] + list(cc) + list(bcc),
|
||||
body=base.as_string())
|
||||
messages.append(message)
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
return self.submit(messages)
|
||||
|
||||
try:
|
||||
self.logger.debug('Connecting to remote SMTP server...')
|
||||
socket_func = smtplib.SMTP
|
||||
if self.secure and self.secure_mode == SecureMailMode.SSL:
|
||||
self.logger.debug('Securing connection with SSL...')
|
||||
socket_func = smtplib.SMTP_SSL
|
||||
def submit(self, messages: t.List[EmailMessage]):
|
||||
|
||||
socket = socket_func(
|
||||
self.smtp_host,
|
||||
self.port,
|
||||
None,
|
||||
timeout=self.socket_connect_timeout,
|
||||
)
|
||||
# error tracking (used for function return)
|
||||
has_error = False
|
||||
|
||||
if self.secure and self.secure_mode == SecureMailMode.STARTTLS:
|
||||
# Handle Secure Connections
|
||||
self.logger.debug('Securing connection with STARTTLS...')
|
||||
socket.starttls()
|
||||
# bind the socket variable to the current namespace
|
||||
socket = None
|
||||
|
||||
if self.user and self.password:
|
||||
# Apply Login credetials
|
||||
self.logger.debug('Applying user credentials...')
|
||||
socket.login(self.user, self.password)
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
# Send the email
|
||||
socket.sendmail(
|
||||
self.from_addr,
|
||||
[to_addr] + list(cc) + list(bcc),
|
||||
base.as_string())
|
||||
try:
|
||||
self.logger.debug('Connecting to remote SMTP server...')
|
||||
socket_func = smtplib.SMTP
|
||||
if self.secure and self.secure_mode == SecureMailMode.SSL:
|
||||
self.logger.debug('Securing connection with SSL...')
|
||||
socket_func = smtplib.SMTP_SSL
|
||||
|
||||
self.logger.info(
|
||||
'Sent Email notification to "{}".'.format(to_addr))
|
||||
socket = socket_func(
|
||||
self.smtp_host,
|
||||
self.port,
|
||||
None,
|
||||
timeout=self.socket_connect_timeout,
|
||||
)
|
||||
|
||||
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))
|
||||
if self.secure and self.secure_mode == SecureMailMode.STARTTLS:
|
||||
# Handle Secure Connections
|
||||
self.logger.debug('Securing connection with STARTTLS...')
|
||||
socket.starttls()
|
||||
|
||||
# Mark our failure
|
||||
has_error = True
|
||||
if self.user and self.password:
|
||||
# Apply Login credetials
|
||||
self.logger.debug('Applying user credentials...')
|
||||
socket.login(self.user, self.password)
|
||||
|
||||
finally:
|
||||
# Gracefully terminate the connection with the server
|
||||
if socket is not None: # pragma: no branch
|
||||
socket.quit()
|
||||
# Send the emails
|
||||
for message in messages:
|
||||
try:
|
||||
socket.sendmail(
|
||||
self.from_addr,
|
||||
message.to_addrs,
|
||||
message.body)
|
||||
|
||||
self.logger.info(
|
||||
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(
|
||||
f'Connection error while submitting email to {self.smtp_host}.'
|
||||
f' Reason: {e}')
|
||||
|
||||
# Mark as failure
|
||||
has_error = True
|
||||
|
||||
finally:
|
||||
# Gracefully terminate the connection with the server
|
||||
if socket is not None: # pragma: no branch
|
||||
socket.quit()
|
||||
|
||||
return not has_error
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
certifi
|
||||
|
||||
# Application dependencies.
|
||||
dataclasses; python_version<"3.7"
|
||||
requests
|
||||
requests-oauthlib
|
||||
click >= 5.0
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue