#!/usr/bin/env python3 # vim:sw=4 ts=4 et: import email import smtplib import ssl import socket import sys import os try: from xtermcolor import colorize COLOR_ERROR = 0xff0000 COLOR_WARN = 0xffff00 COLOR_GOOD = 0x00ff00 def print_error(val="", *args, **kwargs): return print(colorize(val, COLOR_ERROR), *args, **kwargs) def print_warn(val="", *args, **kwargs): return print(colorize(val, COLOR_WARN), *args, **kwargs) def print_good(val="", *args, **kwargs): return print(colorize(val, COLOR_GOOD), *args, **kwargs) except ImportError: print("Install the python3-xtermcolor package for coloured output") print_error = print print_warn = print print_good = print try: import yaml except ImportError: print_error("ERROR: python yaml module not installed - run the following and try again:", file=sys.stderr) print("sudo apt-get install python3-yaml", file=sys.stderr) sys.exit(1) def do_tls(conn, sslv): # Possible values of smtp_sslv: none|peer|client_once|fail_if_no_peer_cert try: # Creating a context with the purpose of server authentication implies verifying the certificate if not hasattr(ssl,'create_default_context'): # ssl.create_default_context is in Python 3.4+ print_warn('WARNING: cannot attempt verification of server certificate:') print_warn(' (need Python 3.4+ to attempt verification)') # Damn you, openssl. Why don't you support IPv6? if conn.sock.family == socket.AddressFamily.AF_INET: print_warn(' You can verify the certificate manually by running:') print_warn(' echo quit | openssl s_client -CAfile /etc/ssl/certs/ca-certificates.crt \\') print_warn(' -starttls smtp -connect {}:{}'.format(*conn.sock.getpeername()[0:2])) return conn.starttls() sslcontext=ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) # The None below looks like might be a typo but it's not - it represents the ActiveRecord default (to verify) if sslv in (None, 'peer', 'client_once', 'fail_if_no_peer_cert'): # defaults are good conn.starttls(context=sslcontext) elif sslv in ('none',): # disable cert checking sslcontext.check_hostname = False sslcontext.verify_mode = ssl.CERT_NONE conn.starttls(context=sslcontext) else: raise ValueError('invalid value for DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: {}'.format(sslv)) except smtplib.SMTPException as e: if (sslv is None) and ('STARTTLS extension not supported by server' in e.args[0]): print_warn("unable to establish TLS, continuing: {}".format(e.args[0])) else: raise ### Start of execution ### cfgfile = sys.argv[1] try: destemail = sys.argv[2] except IndexError: destemail = input('Enter your email address: ') srcemail = 'nobody+launcher-mailtest@discourse.org' # Read in the container yaml and grab the env section cfgdata = yaml.safe_load(open(cfgfile).read()) envdata = cfgdata['env'] # Here are the variables we'll test smtp_addr = envdata.get('DISCOURSE_SMTP_ADDRESS') smtp_port = envdata.get('DISCOURSE_SMTP_PORT') smtp_user = envdata.get('DISCOURSE_SMTP_USER_NAME') smtp_pass = envdata.get('DISCOURSE_SMTP_PASSWORD') smtp_sslv = envdata.get('DISCOURSE_SMTP_OPENSSL_VERIFY_MODE') # Yoink out the settings from the file - we'll print them and put them in the email testinfo = 'DISCOURSE_SMTP_ settings:\n' for k,v in filter(lambda x: x[0].startswith('DISCOURSE_SMTP_'), envdata.items()): if 'PASSWORD' in k: v = '(hidden)' testinfo += ' {} = {}\n'.format(k,v) print(testinfo) # Ensure at least smtp-addr is specified - everything else is optional if smtp_addr is None: print_error("ERROR: DISCOURSE_SMTP_ADDRESS not specified", file=sys.stderr) sys.exit(1) if (smtp_user is None and smtp_pass is not None) or (smtp_user is not None and smtp_pass is None): print_error("ERROR: both username and password must be specified for auth", file=sys.stderr) sys.exit(1) # Do we have a known good set of parameters? known_good_settings = { ('smtp.mandrillapp.com', 587): 'Mandrill', } try: print_good('You are correctly configured to use: {}'.format(known_good_settings[smtp_addr,smtp_port])) except KeyError: pass # Try and ensure the test is valid if destemail.split('@',1)[1] in smtp_addr: print_warn('WARNING: {} may be allowed to relay mail to {}, this may not be a valid test!'.format(smtp_addr, destemail)) # Outbound port smtp? if smtp_port == 25 or smtp_port is None: print_warn('WARNING: many networks block outbound port 25 - consider an alternative (587?)') # Outbound port smtps? if smtp_port == 465: print_warn("WARNING: I can't yet handle testing port 465.") print_warn(" It's probably wrong though - most servers use 587 or 25 for submission.") # Outbound port submission? if smtp_port == 587: if smtp_user is None: print_warn('WARNING: trying to use the submission (587) port without authenticating will probably fail') # Build the message and send! msg = email.message.Message() msg.add_header('From', 'nobody+launcher-mailtest@discourse.org') msg.add_header('To', destemail) msg.add_header('Subject', 'discourse launcher mailtest for {}'.format(os.path.basename(cfgfile))) msg.set_payload(testinfo) try: smtp = smtplib.SMTP(smtp_addr, smtp_port, timeout=5) #smtp.debuglevel=1 do_tls(smtp,smtp_sslv) if smtp_user: smtp.login(smtp_user, smtp_pass) result = smtp.sendmail('nobody+launcher-mailtest@discourse.org', destemail, msg.as_string()) except socket.gaierror as e: print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) print(" Ensure that the host '{}' exists".format(smtp_addr), file=sys.stderr) sys.exit(1) except socket.timeout as e: print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) print(" Ensure that the host '{}' is up and port {} is reachable".format(smtp_addr, smtp_port), file=sys.stderr) print(" If your settings are known-good, ensure outbound port {} is not blocked".format(smtp_port), file=sys.stderr) sys.exit(1) except smtplib.SMTPConnectError as e: print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) print(" Ensure that the host '{}' is up and port {} is reachable".format(smtp_addr, smtp_port), file=sys.stderr) sys.exit(1) except ssl.SSLError as e: print_error("ERROR: unable to establish TLS: {}".format(e.args[-1]), file=sys.stderr) if 'certificate verify failed' in e.args[-1]: print(" Fix the host certificate or disable validation".format(smtp_addr), file=sys.stderr) sys.exit(1) except smtplib.SMTPRecipientsRefused as e: print_error("ERROR: {}".format(e.args[-1].popitem()[1][1].decode()), file=sys.stderr) print(" You must provide a username/password to send to this host", file=sys.stderr) sys.exit(1) except smtplib.SMTPAuthenticationError as e: print_error("ERROR: {}".format(e.args[-1].decode()), file=sys.stderr) print(" Check to ensure your username and password are correct", file=sys.stderr) sys.exit(1) except smtplib.SMTPException as e: print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) if 'SMTP AUTH extension not supported by server' in e.args[0]: print(" Authorization is not available - you may need to use TLS", file=sys.stderr) sys.exit(1) except ValueError: print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) sys.exit(1) print_good("Success!")