bazarr/libs/apprise/plugins/NotifyEmail.py

987 lines
34 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
#
2019-06-07 11:16:07 +00:00
# Copyright (C) 2019 Chris Caron <lead2gold@gmail.com>
# All rights reserved.
#
2019-06-07 11:16:07 +00:00
# This code is licensed under the MIT License.
#
2019-06-07 11:16:07 +00:00
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files(the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions :
#
2019-06-07 11:16:07 +00:00
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
2019-06-07 11:16:07 +00:00
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import re
import smtplib
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
2022-10-11 01:19:24 +00:00
from email.utils import formataddr, make_msgid
2020-09-14 12:24:26 +00:00
from email.header import Header
from email import charset
2019-06-07 11:16:07 +00:00
from socket import error as SocketError
from datetime import datetime
from .NotifyBase import NotifyBase
from ..URLBase import PrivacyMode
2022-10-11 01:19:24 +00:00
from ..common import NotifyFormat, NotifyType
from ..conversion import convert_between
from ..utils import is_email, parse_emails
2019-06-07 11:16:07 +00:00
from ..AppriseLocale import gettext_lazy as _
2020-09-14 12:24:26 +00:00
# Globally Default encoding mode set to Quoted Printable.
charset.add_charset('utf-8', charset.QP, charset.QP, 'utf-8')
2022-10-11 01:19:24 +00:00
class WebBaseLogin:
"""
This class is just used in conjunction of the default emailers
to best formulate a login to it using the data detected
"""
# User Login must be Email Based
EMAIL = 'Email'
# User Login must UserID Based
USERID = 'UserID'
2019-06-07 11:16:07 +00:00
# Secure Email Modes
2022-10-11 01:19:24 +00:00
class SecureMailMode:
2019-06-07 11:16:07 +00:00
SSL = "ssl"
STARTTLS = "starttls"
# Define all of the secure modes (used during validation)
SECURE_MODES = (
SecureMailMode.SSL,
SecureMailMode.STARTTLS,
)
# To attempt to make this script stupid proof, if we detect an email address
# that is part of the this table, we can pre-use a lot more defaults if they
# aren't otherwise specified on the users input.
2019-06-07 11:16:07 +00:00
EMAIL_TEMPLATES = (
# Google GMail
(
'Google Mail',
2019-06-07 11:16:07 +00:00
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>gmail\.com)$', re.I),
{
'port': 587,
'smtp_host': 'smtp.gmail.com',
'secure': True,
2019-06-07 11:16:07 +00:00
'secure_mode': SecureMailMode.STARTTLS,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Yandex
(
'Yandex',
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>yandex\.(com|ru|ua|by|kz|uz|tr|fr))$', re.I),
{
'port': 465,
'smtp_host': 'smtp.yandex.ru',
'secure': True,
'secure_mode': SecureMailMode.SSL,
'login_type': (WebBaseLogin.USERID, )
},
),
# Microsoft Hotmail
(
'Microsoft Hotmail',
2019-06-07 11:16:07 +00:00
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
2022-10-11 01:19:24 +00:00
r'(?P<domain>(outlook|hotmail|live)\.com(\.au)?)$', re.I),
{
'port': 587,
'smtp_host': 'smtp-mail.outlook.com',
'secure': True,
2019-06-07 11:16:07 +00:00
'secure_mode': SecureMailMode.STARTTLS,
'login_type': (WebBaseLogin.EMAIL, )
},
),
2020-09-14 12:24:26 +00:00
# Microsoft Office 365 (Email Server)
# You must specify an authenticated sender address in the from= settings
# and a valid email in the to= to deliver your emails to
(
'Microsoft Office 365',
re.compile(
r'^[^@]+@(?P<domain>(smtp\.)?office365\.com)$', re.I),
{
'port': 587,
'smtp_host': 'smtp.office365.com',
'secure': True,
'secure_mode': SecureMailMode.STARTTLS,
},
),
# Yahoo Mail
(
'Yahoo Mail',
2019-06-07 11:16:07 +00:00
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>yahoo\.(ca|com))$', re.I),
{
'port': 465,
'smtp_host': 'smtp.mail.yahoo.com',
'secure': True,
2019-06-07 11:16:07 +00:00
'secure_mode': SecureMailMode.STARTTLS,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Fast Mail (Series 1)
(
'Fast Mail',
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>fastmail\.(com|cn|co\.uk|com\.au|de|es|fm|fr|im|'
r'in|jp|mx|net|nl|org|se|to|tw|uk|us))$', re.I),
{
'port': 465,
'smtp_host': 'smtp.fastmail.com',
'secure': True,
'secure_mode': SecureMailMode.SSL,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Fast Mail (Series 2)
(
'Fast Mail Extended Addresses',
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>123mail\.org|airpost\.net|eml\.cc|fmail\.co\.uk|'
r'fmgirl\.com|fmguy\.com|mailbolt\.com|mailcan\.com|'
r'mailhaven\.com|mailmight\.com|ml1\.net|mm\.st|myfastmail\.com|'
r'proinbox\.com|promessage\.com|rushpost\.com|sent\.(as|at|com)|'
r'speedymail\.org|warpmail\.net|xsmail\.com|150mail\.com|'
r'150ml\.com|16mail\.com|2-mail\.com|4email\.net|50mail\.com|'
r'allmail\.net|bestmail\.us|cluemail\.com|elitemail\.org|'
r'emailcorner\.net|emailengine\.(net|org)|emailgroups\.net|'
r'emailplus\.org|emailuser\.net|f-m\.fm|fast-email\.com|'
r'fast-mail\.org|fastem\.com|fastemail\.us|fastemailer\.com|'
r'fastest\.cc|fastimap\.com|fastmailbox\.net|fastmessaging\.com|'
r'fea\.st|fmailbox\.com|ftml\.net|h-mail\.us|hailmail\.net|'
r'imap-mail\.com|imap\.cc|imapmail\.org|inoutbox\.com|'
r'internet-e-mail\.com|internet-mail\.org|internetemails\.net|'
r'internetmailing\.net|jetemail\.net|justemail\.net|'
r'letterboxes\.org|mail-central\.com|mail-page\.com|'
r'mailandftp\.com|mailas\.com|mailc\.net|mailforce\.net|'
r'mailftp\.com|mailingaddress\.org|mailite\.com|mailnew\.com|'
r'mailsent\.net|mailservice\.ms|mailup\.net|mailworks\.org|'
r'mymacmail\.com|nospammail\.net|ownmail\.net|petml\.com|'
r'postinbox\.com|postpro\.net|realemail\.net|reallyfast\.biz|'
r'reallyfast\.info|speedpost\.net|ssl-mail\.com|swift-mail\.com|'
r'the-fastest\.net|the-quickest\.com|theinternetemail\.com|'
r'veryfast\.biz|veryspeedy\.net|yepmail\.net)$', re.I),
{
'port': 465,
'smtp_host': 'smtp.fastmail.com',
'secure': True,
'secure_mode': SecureMailMode.SSL,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Zoho Mail (Free)
2019-06-07 11:16:07 +00:00
(
'Zoho Mail',
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>zoho(mail)?\.com)$', re.I),
2019-06-07 11:16:07 +00:00
{
'port': 587,
2019-06-07 11:16:07 +00:00
'smtp_host': 'smtp.zoho.com',
'secure': True,
'secure_mode': SecureMailMode.STARTTLS,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# SendGrid (Email Server)
# You must specify an authenticated sender address in the from= settings
# and a valid email in the to= to deliver your emails to
(
'SendGrid',
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>(\.smtp)?sendgrid\.(com|net))$', re.I),
{
'port': 465,
'smtp_host': 'smtp.sendgrid.net',
'secure': True,
'secure_mode': SecureMailMode.SSL,
'login_type': (WebBaseLogin.USERID, )
},
),
# 163.com
(
'163.com',
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>163\.com)$', re.I),
{
'port': 465,
'smtp_host': 'smtp.163.com',
'secure': True,
'secure_mode': SecureMailMode.SSL,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Foxmail.com
(
'Foxmail.com',
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>(foxmail|qq)\.com)$', re.I),
{
'port': 587,
'smtp_host': 'smtp.qq.com',
'secure': True,
'secure_mode': SecureMailMode.STARTTLS,
'login_type': (WebBaseLogin.EMAIL, )
},
),
# Catch All
(
'Custom',
2019-06-07 11:16:07 +00:00
re.compile(
r'^((?P<label>[^+]+)\+)?(?P<id>[^@]+)@'
r'(?P<domain>.+)$', re.I),
{
# Setting smtp_host to None is a way of
# auto-detecting it based on other parameters
# specified. There is no reason to ever modify
# this Catch All
'smtp_host': None,
},
),
)
class NotifyEmail(NotifyBase):
"""
A wrapper to Email Notifications
"""
# The default descriptive name associated with the Notification
service_name = 'E-Mail'
# The default simple (insecure) protocol
protocol = 'mailto'
# The default secure protocol
secure_protocol = 'mailtos'
# A URL that takes you to the setup/help of the specific protocol
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_email'
2019-06-07 11:16:07 +00:00
# Default Notify Format
notify_format = NotifyFormat.HTML
# Default Non-Encryption Port
default_port = 25
# Default Secure Port
default_secure_port = 587
2019-06-07 11:16:07 +00:00
# Default Secure Mode
default_secure_mode = SecureMailMode.STARTTLS
# Default SMTP Timeout (in seconds)
socket_connect_timeout = 15
2019-06-07 11:16:07 +00:00
# Define object templates
templates = (
2020-03-31 00:18:11 +00:00
'{schema}://{host}',
'{schema}://{host}:{port}',
'{schema}://{host}/{targets}',
'{schema}://{host}:{port}/{targets}',
'{schema}://{user}@{host}',
'{schema}://{user}@{host}:{port}',
'{schema}://{user}@{host}/{targets}',
'{schema}://{user}@{host}:{port}/{targets}',
2019-06-07 11:16:07 +00:00
'{schema}://{user}:{password}@{host}',
'{schema}://{user}:{password}@{host}:{port}',
'{schema}://{user}:{password}@{host}/{targets}',
'{schema}://{user}:{password}@{host}:{port}/{targets}',
)
# Define our template tokens
template_tokens = dict(NotifyBase.template_tokens, **{
'user': {
'name': _('User Name'),
'type': 'string',
},
'password': {
'name': _('Password'),
'type': 'string',
'private': True,
},
'host': {
'name': _('Domain'),
'type': 'string',
'required': True,
},
'port': {
'name': _('Port'),
'type': 'int',
'min': 1,
'max': 65535,
},
'targets': {
'name': _('Target Emails'),
'type': 'list:string',
},
})
template_args = dict(NotifyBase.template_args, **{
'to': {
'name': _('To Email'),
'type': 'string',
'map_to': 'targets',
},
'from': {
'name': _('From Email'),
'type': 'string',
'map_to': 'from_addr',
},
'name': {
'name': _('From Name'),
'type': 'string',
'map_to': 'from_name',
},
'cc': {
'name': _('Carbon Copy'),
'type': 'list:string',
},
'bcc': {
'name': _('Blind Carbon Copy'),
'type': 'list:string',
},
'smtp': {
'name': _('SMTP Server'),
'type': 'string',
'map_to': 'smtp_host',
},
2019-06-07 11:16:07 +00:00
'mode': {
'name': _('Secure Mode'),
'type': 'choice:string',
'values': SECURE_MODES,
'default': SecureMailMode.STARTTLS,
'map_to': 'secure_mode',
},
2022-10-11 01:19:24 +00:00
'reply': {
'name': _('Reply To'),
'type': 'list:string',
'map_to': 'reply_to',
},
2019-06-07 11:16:07 +00:00
})
# Define any kwargs we're using
template_kwargs = {
'headers': {
'name': _('Email Header'),
'prefix': '+',
},
}
def __init__(self, smtp_host=None, from_name=None,
from_addr=None, secure_mode=None, targets=None, cc=None,
2022-10-11 01:19:24 +00:00
bcc=None, reply_to=None, headers=None, **kwargs):
"""
Initialize Email Object
2019-06-07 11:16:07 +00:00
The smtp_host and secure_mode can be automatically detected depending
on how the URL was built
"""
super(NotifyEmail, self).__init__(**kwargs)
# Handle SMTP vs SMTPS (Secure vs UnSecure)
if not self.port:
if self.secure:
self.port = self.default_secure_port
else:
self.port = self.default_port
2020-09-14 12:24:26 +00:00
# Acquire Email 'To'
self.targets = list()
2019-06-07 11:16:07 +00:00
# Acquire Carbon Copies
self.cc = set()
# Acquire Blind Carbon Copies
self.bcc = set()
2022-10-11 01:19:24 +00:00
# Acquire Reply To
self.reply_to = set()
2020-09-14 12:24:26 +00:00
# For tracking our email -> name lookups
self.names = {}
self.headers = {}
if headers:
# Store our extra headers
self.headers.update(headers)
# Now we want to construct the To and From email
# addresses from the URL provided
2019-06-07 11:16:07 +00:00
self.from_addr = from_addr
2020-03-31 00:18:11 +00:00
if self.user and not self.from_addr:
2019-06-07 11:16:07 +00:00
# detect our email address
self.from_addr = '{}@{}'.format(
re.split(r'[\s@]+', self.user)[0],
self.host,
)
2020-09-14 12:24:26 +00:00
result = is_email(self.from_addr)
if not result:
# Parse Source domain based on from_addr
2019-06-07 11:16:07 +00:00
msg = 'Invalid ~From~ email specified: {}'.format(self.from_addr)
self.logger.warning(msg)
raise TypeError(msg)
2020-09-14 12:24:26 +00:00
# Store our email address
self.from_addr = result['full_email']
# Set our from name
self.from_name = from_name if from_name else result['name']
2022-10-11 01:19:24 +00:00
# Store our lookup
self.names[self.from_addr] = \
self.from_name if self.from_name else False
# Now detect the SMTP Server
2019-06-07 11:16:07 +00:00
self.smtp_host = \
2022-10-11 01:19:24 +00:00
smtp_host if isinstance(smtp_host, str) else ''
2019-06-07 11:16:07 +00:00
# Now detect secure mode
self.secure_mode = self.default_secure_mode \
2022-10-11 01:19:24 +00:00
if not isinstance(secure_mode, str) \
2019-06-07 11:16:07 +00:00
else secure_mode.lower()
if self.secure_mode not in SECURE_MODES:
msg = 'The secure mode specified ({}) is invalid.'\
.format(secure_mode)
self.logger.warning(msg)
raise TypeError(msg)
2020-09-14 12:24:26 +00:00
if targets:
# Validate recipients (to:) and drop bad ones:
for recipient in parse_emails(targets):
result = is_email(recipient)
if result:
self.targets.append(
(result['name'] if result['name'] else False,
result['full_email']))
continue
self.logger.warning(
'Dropped invalid To email '
'({}) specified.'.format(recipient),
)
2020-09-14 12:24:26 +00:00
else:
# If our target email list is empty we want to add ourselves to it
self.targets.append(
(self.from_name if self.from_name else False, self.from_addr))
# Validate recipients (cc:) and drop bad ones:
for recipient in parse_emails(cc):
email = is_email(recipient)
if email:
self.cc.add(email['full_email'])
# Index our name (if one exists)
self.names[email['full_email']] = \
email['name'] if email['name'] else False
continue
self.logger.warning(
'Dropped invalid Carbon Copy email '
'({}) specified.'.format(recipient),
)
# Validate recipients (bcc:) and drop bad ones:
2020-09-14 12:24:26 +00:00
for recipient in parse_emails(bcc):
email = is_email(recipient)
if email:
self.bcc.add(email['full_email'])
# Index our name (if one exists)
self.names[email['full_email']] = \
email['name'] if email['name'] else False
continue
self.logger.warning(
'Dropped invalid Blind Carbon Copy email '
'({}) specified.'.format(recipient),
)
2022-10-11 01:19:24 +00:00
# Validate recipients (reply-to:) and drop bad ones:
for recipient in parse_emails(reply_to):
email = is_email(recipient)
if email:
self.reply_to.add(email['full_email'])
# Index our name (if one exists)
self.names[email['full_email']] = \
email['name'] if email['name'] else False
continue
self.logger.warning(
'Dropped invalid Reply To email '
'({}) specified.'.format(recipient),
)
# Apply any defaults based on certain known configurations
2022-10-11 01:19:24 +00:00
self.NotifyEmailDefaults(secure_mode=secure_mode, **kwargs)
2020-03-31 00:18:11 +00:00
# if there is still no smtp_host then we fall back to the hostname
if not self.smtp_host:
self.smtp_host = self.host
return
2022-10-11 01:19:24 +00:00
def NotifyEmailDefaults(self, secure_mode=None, port=None, **kwargs):
"""
A function that prefills defaults based on the email
it was provided.
"""
2020-03-31 00:18:11 +00:00
if self.smtp_host or not self.user:
# SMTP Server was explicitly specified, therefore it is assumed
# the caller knows what he's doing and is intentionally
2020-03-31 00:18:11 +00:00
# over-riding any smarts to be applied. We also can not apply
# any default if there was no user specified.
return
# detect our email address using our user/host combo
from_addr = '{}@{}'.format(
re.split(r'[\s@]+', self.user)[0],
self.host,
)
2019-06-07 11:16:07 +00:00
for i in range(len(EMAIL_TEMPLATES)): # pragma: no branch
self.logger.trace('Scanning %s against %s' % (
from_addr, EMAIL_TEMPLATES[i][0]
))
match = EMAIL_TEMPLATES[i][1].match(from_addr)
if match:
self.logger.info(
'Applying %s Defaults' %
2019-06-07 11:16:07 +00:00
EMAIL_TEMPLATES[i][0],
)
2022-10-11 01:19:24 +00:00
# the secure flag can not be altered if defined in the template
2019-06-07 11:16:07 +00:00
self.secure = EMAIL_TEMPLATES[i][2]\
.get('secure', self.secure)
2022-10-11 01:19:24 +00:00
# The SMTP Host check is already done above; if it was
# specified we wouldn't even reach this part of the code.
2019-06-07 11:16:07 +00:00
self.smtp_host = EMAIL_TEMPLATES[i][2]\
.get('smtp_host', self.smtp_host)
2022-10-11 01:19:24 +00:00
# The following can be over-ridden if defined manually in the
# Apprise URL. Otherwise they take on the template value
if not port:
self.port = EMAIL_TEMPLATES[i][2]\
.get('port', self.port)
if not secure_mode:
self.secure_mode = EMAIL_TEMPLATES[i][2]\
.get('secure_mode', self.secure_mode)
2019-06-07 11:16:07 +00:00
# Adjust email login based on the defined usertype. If no entry
# was specified, then we default to having them all set (which
# basically implies that there are no restrictions and use use
# whatever was specified)
login_type = EMAIL_TEMPLATES[i][2]\
.get('login_type', [])
2019-06-07 11:16:07 +00:00
if login_type:
# only apply additional logic to our user if a login_type
# was specified.
if is_email(self.user) and \
WebBaseLogin.EMAIL not in login_type:
# Email specified but login type
# not supported; switch it to user id
self.user = match.group('id')
2019-06-07 11:16:07 +00:00
elif WebBaseLogin.USERID not in login_type:
# user specified but login type
# not supported; switch it to email
self.user = '{}@{}'.format(self.user, self.host)
break
2022-10-11 01:19:24 +00:00
def _get_charset(self, input_string):
"""
Get utf-8 charset if non ascii string only
Encode an ascii string to utf-8 is bad for email deliverability
because some anti-spam gives a bad score for that
like SUBJ_EXCESS_QP flag on Rspamd
"""
if not input_string:
return None
return 'utf-8' if not all(ord(c) < 128 for c in input_string) else None
def send(self, body, title='', notify_type=NotifyType.INFO, attach=None,
**kwargs):
"""
Perform Email Notification
"""
2020-09-14 12:24:26 +00:00
# Initialize our default from name
from_name = self.from_name if self.from_name else self.app_desc
2019-06-07 11:16:07 +00:00
# error tracking (used for function return)
has_error = False
2020-09-14 12:24:26 +00:00
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
2019-06-07 11:16:07 +00:00
# Create a copy of the targets list
emails = list(self.targets)
while len(emails):
# Get our email to notify
2020-09-14 12:24:26 +00:00
to_name, to_addr = emails.pop(0)
# Strip target out of cc list if in To or Bcc
cc = (self.cc - self.bcc - set([to_addr]))
2020-09-14 12:24:26 +00:00
# Strip target out of bcc list if in To
bcc = (self.bcc - set([to_addr]))
2022-10-11 01:19:24 +00:00
# Strip target out of reply_to list if in To
reply_to = (self.reply_to - set([to_addr]))
2020-09-14 12:24:26 +00:00
2022-10-11 01:19:24 +00:00
# Format our cc addresses to support the Name field
cc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in cc]
2020-09-14 12:24:26 +00:00
2022-10-11 01:19:24 +00:00
# Format our bcc addresses to support the Name field
bcc = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in bcc]
2020-09-14 12:24:26 +00:00
2022-10-11 01:19:24 +00:00
if reply_to:
# Format our reply-to addresses to support the Name field
reply_to = [formataddr(
(self.names.get(addr, False), addr), charset='utf-8')
for addr in reply_to]
2020-09-14 12:24:26 +00:00
2019-06-07 11:16:07 +00:00
self.logger.debug(
'Email From: {} <{}>'.format(from_name, self.from_addr))
self.logger.debug('Email To: {}'.format(to_addr))
2020-09-14 12:24:26 +00:00
if cc:
self.logger.debug('Email Cc: {}'.format(', '.join(cc)))
2020-09-14 12:24:26 +00:00
if bcc:
self.logger.debug('Email Bcc: {}'.format(', '.join(bcc)))
2022-10-11 01:19:24 +00:00
if reply_to:
self.logger.debug(
'Email Reply-To: {}'.format(', '.join(reply_to))
)
2019-06-07 11:16:07 +00:00
self.logger.debug('Login ID: {}'.format(self.user))
self.logger.debug(
'Delivery: {}:{}'.format(self.smtp_host, self.port))
2019-06-07 11:16:07 +00:00
# Prepare Email Message
if self.notify_format == NotifyFormat.HTML:
2022-10-11 01:19:24 +00:00
base = MIMEMultipart("alternative")
base.attach(MIMEText(
convert_between(
NotifyFormat.HTML, NotifyFormat.TEXT, body),
'plain', 'utf-8')
)
base.attach(MIMEText(body, 'html', 'utf-8'))
2019-06-07 11:16:07 +00:00
else:
2022-10-11 01:19:24 +00:00
base = MIMEText(body, 'plain', 'utf-8')
if attach:
2022-10-11 01:19:24 +00:00
mixed = MIMEMultipart("mixed")
mixed.attach(base)
# Now store our attachments
for attachment in attach:
if not attachment:
# We could not load the attachment; take an early
# exit since this isn't what the end user wanted
2020-03-25 01:14:36 +00:00
# We could not access the attachment
self.logger.error(
'Could not access attachment {}.'.format(
attachment.url(privacy=True)))
return False
2020-03-25 01:14:36 +00:00
self.logger.debug(
'Preparing Email attachment {}'.format(
attachment.url(privacy=True)))
with open(attachment.path, "rb") as abody:
app = MIMEApplication(abody.read())
app.set_type(attachment.mimetype)
app.add_header(
'Content-Disposition',
'attachment; filename="{}"'.format(
2020-09-14 12:24:26 +00:00
Header(attachment.name, 'utf-8')),
)
2022-10-11 01:19:24 +00:00
mixed.attach(app)
base = mixed
2022-10-11 01:19:24 +00:00
# Apply any provided custom headers
for k, v in self.headers.items():
base[k] = Header(v, self._get_charset(v))
base['Subject'] = Header(title, self._get_charset(title))
base['From'] = formataddr(
(from_name if from_name else False, self.from_addr),
charset='utf-8')
base['To'] = formataddr((to_name, to_addr), charset='utf-8')
base['Message-ID'] = make_msgid(domain=self.smtp_host)
base['Date'] = \
datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000")
base['X-Application'] = self.app_id
if cc:
base['Cc'] = ','.join(cc)
if reply_to:
base['Reply-To'] = ','.join(reply_to)
2019-06-07 11:16:07 +00:00
# bind the socket variable to the current namespace
socket = None
# Always call throttle before any remote server i/o is made
self.throttle()
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
socket = socket_func(
self.smtp_host,
self.port,
None,
timeout=self.socket_connect_timeout,
2019-06-07 11:16:07 +00:00
)
2019-06-07 11:16:07 +00:00
if self.secure and self.secure_mode == SecureMailMode.STARTTLS:
# Handle Secure Connections
self.logger.debug('Securing connection with STARTTLS...')
socket.starttls()
2019-06-07 11:16:07 +00:00
if self.user and self.password:
# Apply Login credetials
self.logger.debug('Applying user credentials...')
socket.login(self.user, self.password)
2019-06-07 11:16:07 +00:00
# Send the email
socket.sendmail(
self.from_addr,
[to_addr] + list(cc) + list(bcc),
base.as_string())
2019-06-07 11:16:07 +00:00
self.logger.info(
'Sent Email notification to "{}".'.format(to_addr))
2019-06-07 11:16:07 +00:00
except (SocketError, smtplib.SMTPException, RuntimeError) as e:
self.logger.warning(
2020-09-14 12:24:26 +00:00
'A Connection error occurred sending Email '
2019-06-07 11:16:07 +00:00
'notification to {}.'.format(self.smtp_host))
self.logger.debug('Socket Exception: %s' % str(e))
# Mark our 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
def url(self, privacy=False, *args, **kwargs):
2019-06-07 11:16:07 +00:00
"""
Returns the URL built dynamically based on specified arguments.
"""
2020-09-14 12:24:26 +00:00
# Define an URL parameters
params = {
2019-06-07 11:16:07 +00:00
'from': self.from_addr,
'mode': self.secure_mode,
'smtp': self.smtp_host,
'user': self.user,
}
# Append our headers into our parameters
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
2020-09-14 12:24:26 +00:00
# Extend our parameters
params.update(self.url_parameters(privacy=privacy, *args, **kwargs))
if self.from_name:
params['name'] = self.from_name
if len(self.cc) > 0:
# Handle our Carbon Copy Addresses
2020-09-14 12:24:26 +00:00
params['cc'] = ','.join(
['{}{}'.format(
'' if not e not in self.names
else '{}:'.format(self.names[e]), e) for e in self.cc])
if len(self.bcc) > 0:
# Handle our Blind Carbon Copy Addresses
2020-09-14 12:24:26 +00:00
params['bcc'] = ','.join(
['{}{}'.format(
'' if not e not in self.names
else '{}:'.format(self.names[e]), e) for e in self.bcc])
2022-10-11 01:19:24 +00:00
if self.reply_to:
# Handle our Reply-To Addresses
params['reply'] = ','.join(
['{}{}'.format(
'' if not e not in self.names
else '{}:'.format(self.names[e]), e)
for e in self.reply_to])
2019-06-07 11:16:07 +00:00
# pull email suffix from username (if present)
2020-03-31 00:18:11 +00:00
user = None if not self.user else self.user.split('@')[0]
2019-06-07 11:16:07 +00:00
# Determine Authentication
auth = ''
if self.user and self.password:
auth = '{user}:{password}@'.format(
user=NotifyEmail.quote(user, safe=''),
password=self.pprint(
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
2019-06-07 11:16:07 +00:00
)
2020-03-31 00:18:11 +00:00
elif user:
2019-06-07 11:16:07 +00:00
# user url
auth = '{user}@'.format(
user=NotifyEmail.quote(user, safe=''),
)
# Default Port setup
default_port = \
self.default_secure_port if self.secure else self.default_port
# a simple boolean check as to whether we display our target emails
# or not
has_targets = \
2020-09-14 12:24:26 +00:00
not (len(self.targets) == 1
and self.targets[0][1] == self.from_addr)
2019-06-07 11:16:07 +00:00
2020-09-14 12:24:26 +00:00
return '{schema}://{auth}{hostname}{port}/{targets}?{params}'.format(
2019-06-07 11:16:07 +00:00
schema=self.secure_protocol if self.secure else self.protocol,
auth=auth,
2020-09-14 12:24:26 +00:00
# never encode hostname since we're expecting it to be a valid one
hostname=self.host,
2019-06-07 11:16:07 +00:00
port='' if self.port is None or self.port == default_port
else ':{}'.format(self.port),
targets='' if not has_targets else '/'.join(
2020-09-14 12:24:26 +00:00
[NotifyEmail.quote('{}{}'.format(
'' if not e[0] else '{}:'.format(e[0]), e[1]),
safe='') for e in self.targets]),
params=NotifyEmail.urlencode(params),
2019-06-07 11:16:07 +00:00
)
@staticmethod
def parse_url(url):
"""
Parses the URL and returns enough arguments that can allow
2020-09-14 12:24:26 +00:00
us to re-instantiate this object.
"""
results = NotifyBase.parse_url(url)
if not results:
# We're done early as we couldn't load the results
return results
2019-06-07 11:16:07 +00:00
# The From address is a must; either through the use of templates
# from= entry and/or merging the user and hostname together, this
# must be calculated or parse_url will fail.
from_addr = ''
2019-06-07 11:16:07 +00:00
# The server we connect to to send our mail to
smtp_host = ''
2019-06-07 11:16:07 +00:00
# Get our potential email targets; if none our found we'll just
# add one to ourselves
results['targets'] = NotifyEmail.split_path(results['fullpath'])
# Attempt to detect 'from' email address
if 'from' in results['qsd'] and len(results['qsd']['from']):
2019-06-07 11:16:07 +00:00
from_addr = NotifyEmail.unquote(results['qsd']['from'])
# Attempt to detect 'to' email address
if 'to' in results['qsd'] and len(results['qsd']['to']):
2020-09-14 12:24:26 +00:00
results['targets'].append(results['qsd']['to'])
if 'name' in results['qsd'] and len(results['qsd']['name']):
# Extract from name to associate with from address
2019-06-07 11:16:07 +00:00
results['from_name'] = NotifyEmail.unquote(results['qsd']['name'])
# Store SMTP Host if specified
if 'smtp' in results['qsd'] and len(results['qsd']['smtp']):
# Extract the smtp server
2019-06-07 11:16:07 +00:00
smtp_host = NotifyEmail.unquote(results['qsd']['smtp'])
if 'mode' in results['qsd'] and len(results['qsd']['mode']):
# Extract the secure mode to over-ride the default
results['secure_mode'] = results['qsd']['mode'].lower()
# Handle Carbon Copy Addresses
if 'cc' in results['qsd'] and len(results['qsd']['cc']):
2020-09-14 12:24:26 +00:00
results['cc'] = results['qsd']['cc']
# Handle Blind Carbon Copy Addresses
if 'bcc' in results['qsd'] and len(results['qsd']['bcc']):
2020-09-14 12:24:26 +00:00
results['bcc'] = results['qsd']['bcc']
2022-10-11 01:19:24 +00:00
# Handle Reply To Addresses
if 'reply' in results['qsd'] and len(results['qsd']['reply']):
results['reply_to'] = results['qsd']['reply']
2019-06-07 11:16:07 +00:00
results['from_addr'] = from_addr
results['smtp_host'] = smtp_host
# Add our Meta Headers that the user can provide with their outbound
# emails
results['headers'] = {NotifyBase.unquote(x): NotifyBase.unquote(y)
for x, y in results['qsd+'].items()}
return results