Module: check_mk
Branch: master
Commit: 14fc2ad2f5a0f6c5847a6b97b66cb6627e0ad4d9
URL:
http://git.mathias-kettner.de/git/?p=check_mk.git;a=commit;h=14fc2ad2f5a0f6…
Author: Sebastian Herbord <sh(a)mathias-kettner.de>
Date: Wed Jun 8 13:41:50 2016 +0200
3140 mail notification script can now optionally connect directly to a smtp server instead
of using sendmail
Please note that as of right now, not all features of the smtp protocol are supported
(i.e.
non-plaintext password transmission).
---
.werks/3140 | 10 ++++
ChangeLog | 1 +
notifications/mail | 116 ++++++++++++++++++++++++++++++++++---
web/plugins/wato/notifications.py | 53 +++++++++++++++++
4 files changed, 173 insertions(+), 7 deletions(-)
diff --git a/.werks/3140 b/.werks/3140
new file mode 100644
index 0000000..25c2546
--- /dev/null
+++ b/.werks/3140
@@ -0,0 +1,10 @@
+Title: mail notification script can now optionally connect directly to a smtp server
instead of using sendmail
+Level: 1
+Component: notifications
+Compatible: compat
+Version: 1.4.0i1
+Date: 1465385978
+Class: feature
+
+Please note that as of right now, not all features of the smtp protocol are supported
(i.e.
+non-plaintext password transmission).
diff --git a/ChangeLog b/ChangeLog
index 55ab7f9..33abde6 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -430,6 +430,7 @@
Notifications:
* 3263 Notifications: allow users to restrict by their contact groups...
* 3532 Rule base notifications: Three new conditions for service groups matching...
+ * 3140 mail notification script can now optionally connect directly to a smtp server
instead of using sendmail...
* 3253 FIX: sms: notification script sms now handles single quotes in the message in
the right way
* 3346 FIX: Re-added envelope sender to asciimail/mail notification plugins
* 3380 FIX: Fixed graphs in service notifications having spaces in service
descriptions
diff --git a/notifications/mail b/notifications/mail
index b26c2de..e282d7a 100755
--- a/notifications/mail
+++ b/notifications/mail
@@ -412,11 +412,9 @@ def multipart_mail(target, subject, from_address, reply_to,
content_txt, content
if reply_to:
m['Reply-To'] = reply_to
-
-
return m
-def send_mail(m, target, from_address):
+def send_mail_sendmail(m, target, from_address):
cmd = ["/usr/sbin/sendmail"]
if from_address:
cmd += ['-F', from_address, "-f", from_address]
@@ -426,7 +424,106 @@ def send_mail(m, target, from_address):
except OSError:
raise Exception("Failed to send the mail: /usr/sbin/sendmail is
missing")
p.communicate(m.as_string())
- return True
+ return 0
+
+def send_mail_smtp(message, target, from_address, context):
+ import smtplib # for the error messages
+ import socket
+ host_index = 1
+
+ retry_possible = False
+ success = False
+
+ while not success:
+ host_var = 'PARAMETER_SMTP_SMARTHOSTS_%d' % host_index
+ if host_var not in context:
+ break
+ else:
+ host_index += 1
+
+ host = context[host_var]
+ try:
+ send_mail_smtp_impl(message, target, host, from_address, context)
+ success = True
+ except socket.timeout, e:
+ sys.stderr.write("timeout connecting to \"%s\": %s\n" %
(host, str(e)))
+ except socket.gaierror, e:
+ sys.stderr.write("socket error connecting to \"%s\":
%s\n" % (host, str(e)))
+ except smtplib.SMTPRecipientsRefused, e:
+ # the exception contains a dict of failed recipients to the respective error.
since we
+ # only have one recipient there has to be exactly one element
+ errorcode, message = e.recipients.values()[0]
+ # default is to retry, these errorcodes are known to
+ if errorcode not in [
+ 450, # sender address domain not found
+ 550, # sender address unknown
+ ]:
+ retry_possible = True
+ sys.stderr.write("mail to \"%s\" refused: %d, %s\n" %
(target, errorcode, message))
+ except smtplib.SMTPHeloError, e:
+ retry_possible = True # server is acting up, this may be fixed quickly
+ sys.stderr.write("protocol error from \"%s\": %s\n" %
(host, str(e)))
+ except smtplib.SMTPSenderRefused, e:
+ sys.stderr.write("server didn't accept from-address \"%s\"
refused: %s\n" %\
+ (from_address, str(e)))
+ except smtplib.SMTPAuthenticationError, e:
+ sys.stderr.write("authentication failed on \"%s\": %s\n"
% (host, str(e)))
+ except smtplib.SMTPDataError, e:
+ retry_possible = True # unexpected error - give retry a chance
+ sys.stderr.write("unexpected error code from \"%s\":
%s\n" % (host, str(e)))
+ except smtplib.SMTPException, e:
+ retry_possible = True # who knows what went wrong, a retry might just work
+ sys.stderr.write("undocumented error code from \"%s\":
%s\n" % (host, str(e)))
+ if success:
+ return 0
+ elif retry_possible:
+ return 1
+ else:
+ return 2
+
+def send_mail_smtp_impl(message, target, host, from_address, context):
+ import smtplib
+ import types
+ def getreply_wrapper(self):
+ self.last_code, self.last_repl = smtplib.SMTP.getreply(self)
+ return self.last_code, self.last_repl
+
+ port = int(context['PARAMETER_SMTP_PORT'])
+ timeout = int(context['PARAMETER_SMTP_TIMEOUT'])
+
+ encryption = context.get('PARAMETER_SMTP_ENCRYPTION', "NONE")
+
+ if encryption == "SSL_TLS":
+ conn = smtplib.SMTP_SSL(host, port, from_address, timeout=timeout)
+ else:
+ conn = smtplib.SMTP(host, port, from_address, timeout=timeout)
+
+ # evil hack: the smtplib doesn't allow access to the reply code/message
+ # in case of success. But we want it!
+ conn.getreply = types.MethodType(getreply_wrapper, conn)
+
+ if encryption == "STARTTLS":
+ conn.starttls()
+
+ if context.get('PARAMETER_SMTP_AUTH_USER') is not None:
+ conn.login(context['PARAMETER_SMTP_AUTH_USER'],
context['PARAMETER_SMTP_AUTH_PASSWORD'])
+
+ # this call returns a dictionary with the recipients that failed + the reason, but
only
+ # if at least one succeeded, otherwise it throws an exception.
+ # since we send only one mail per call, we either get an exception or an empty dict.
+
+ # the first parameter here is actually used in the return_path header
+ try:
+ conn.sendmail(from_address, target, message.as_string())
+ sys.stdout.write("success %d - %s\n" % (conn.last_code,
conn.last_repl))
+ finally:
+ conn.quit()
+
+def send_mail(message, target, from_address, context):
+ if "PARAMETER_SMTP_PORT" in context:
+ return send_mail_smtp(message, target, from_address, context)
+ else:
+ return send_mail_sendmail(message, target, from_address)
def fetch_pnp_data(context, params):
try:
@@ -786,11 +883,16 @@ def main():
sys.stdout.write("Cannot send HTML email: empty destination email
address")
sys.exit(2)
-
# Create the mail and send it
from_address = context.get("PARAMETER_FROM")
reply_to = context.get("PARAMETER_REPLY_TO")
m = multipart_mail(mailto, subject, from_address, reply_to, content_txt,
content_html, attachments)
- send_mail(m, mailto, from_address)
+ try:
+ sys.exit(send_mail(m, mailto, from_address, context))
+ except Exception, e:
+ sys.stdout.write("unhandled exception %s" % str(e))
+ # unhandled exception, don't retry this...
+ sys.exit(2)
-main()
+if __name__ == "__main__":
+ main()
diff --git a/web/plugins/wato/notifications.py b/web/plugins/wato/notifications.py
index 8576f16..86832d4 100644
--- a/web/plugins/wato/notifications.py
+++ b/web/plugins/wato/notifications.py
@@ -115,6 +115,59 @@ register_notification_parameters(
title = _("Notification sort order for bulk
notifications"),
default = "oldest_first"
)
+ ),
+ ('smtp',
+ Dictionary(
+ title = _("Enable synchronous delivery via SMTP"),
+ help = _("Configuring this to have the notification plugin connect
directly to "
+ "the smtp server. This has the advantage of providing
better error "
+ "messages in case of an error but it does require more
configuration "
+ "and is strictly synchronous so we advice use only on
enterprise "
+ "installations using the notification spooler."),
+ elements = [
+ ("smarthosts",
+ ListOfStrings(
+ title = _("Smarthosts"),
+ orientation = "horizontal"
+ )),
+ ("port",
+ Integer(
+ title = _("Port"),
+ default_value = 25
+ )),
+ ("auth",
+ Dictionary(
+ title = _("Authentication"),
+ elements = [
+ ("method",
+ DropdownChoice(
+ title = _("Authmethod"),
+ choices = [
+ ("plaintext", _("Plaintext"))
+ ]
+ )),
+ ("user",
+ TextAscii(
+ title = _("User")
+ )),
+ ("password",
+ Password(
+ title = _("Password")
+ ))
+ ],
+ optional_keys = []
+ )),
+ ("encryption",
+ DropdownChoice(
+ title = ("Encryption"),
+ choices = [
+ ("ssl_tls", _("SSL/TLS")),
+ ("starttls", _("STARTTLS"))
+ ]
+ )),
+ ],
+ optional_keys = ["auth", "encryption"]
+ )
)
]
)