180 lines
7.5 KiB
Python
Executable File
180 lines
7.5 KiB
Python
Executable File
#!/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.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!")
|