add note about multiple error messages
[discourse_docker.git] / scripts / mailtest
CommitLineData
7936ebaa
MB
1#!/usr/bin/env python3
2# vim:sw=4 ts=4 et:
3
4import email
5import smtplib
6import ssl
7import socket
8import sys
9import os
10
76a9915e
MB
11try:
12 from xtermcolor import colorize
13 COLOR_ERROR = 0xff0000
14 COLOR_WARN = 0xffff00
15 COLOR_GOOD = 0x00ff00
16 def print_error(val="", *args, **kwargs):
17 return print(colorize(val, COLOR_ERROR), *args, **kwargs)
18 def print_warn(val="", *args, **kwargs):
19 return print(colorize(val, COLOR_WARN), *args, **kwargs)
20 def print_good(val="", *args, **kwargs):
21 return print(colorize(val, COLOR_GOOD), *args, **kwargs)
22except ImportError:
23 print("Install the python3-xtermcolor package for coloured output")
24 print_error = print
25 print_warn = print
26 print_good = print
27
7936ebaa
MB
28try:
29 import yaml
30except ImportError:
76a9915e 31 print_error("ERROR: python yaml module not installed - run the following and try again:", file=sys.stderr)
7936ebaa
MB
32 print("sudo apt-get install python3-yaml", file=sys.stderr)
33 sys.exit(1)
34
35def do_tls(conn, sslv):
36 # Possible values of smtp_sslv: none|peer|client_once|fail_if_no_peer_cert
37 try:
38 # Creating a context with the purpose of server authentication implies verifying the certificate
839f0266 39 if not hasattr(ssl,'create_default_context'):
e61914cb
MB
40 # ssl.create_default_context is in Python 3.4+
41 print_warn('WARNING: cannot attempt verification of server certificate:')
42 print_warn(' (need Python 3.4+ to attempt verification)')
43 # Damn you, openssl. Why don't you support IPv6?
7221309e 44 if conn.sock.family == socket.AF_INET:
e61914cb
MB
45 print_warn(' You can verify the certificate manually by running:')
46 print_warn(' echo quit | openssl s_client -CAfile /etc/ssl/certs/ca-certificates.crt \\')
47 print_warn(' -starttls smtp -connect {}:{}'.format(*conn.sock.getpeername()[0:2]))
839f0266 48 return conn.starttls()
7936ebaa
MB
49 sslcontext=ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
50 # The None below looks like might be a typo but it's not - it represents the ActiveRecord default (to verify)
51 if sslv in (None, 'peer', 'client_once', 'fail_if_no_peer_cert'):
52 # defaults are good
53 conn.starttls(context=sslcontext)
54 elif sslv in ('none',):
55 # disable cert checking
56 sslcontext.check_hostname = False
57 sslcontext.verify_mode = ssl.CERT_NONE
58 conn.starttls(context=sslcontext)
59 else:
60 raise ValueError('invalid value for DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: {}'.format(sslv))
61 except smtplib.SMTPException as e:
62 if (sslv is None) and ('STARTTLS extension not supported by server' in e.args[0]):
76a9915e 63 print_warn("unable to establish TLS, continuing: {}".format(e.args[0]))
7936ebaa
MB
64 else:
65 raise
66
67### Start of execution ###
68cfgfile = sys.argv[1]
69try:
70 destemail = sys.argv[2]
71except IndexError:
72 destemail = input('Enter your email address: ')
73srcemail = 'nobody+launcher-mailtest@discourse.org'
74
75# Read in the container yaml and grab the env section
76cfgdata = yaml.safe_load(open(cfgfile).read())
77envdata = cfgdata['env']
78
79# Here are the variables we'll test
80smtp_addr = envdata.get('DISCOURSE_SMTP_ADDRESS')
81smtp_port = envdata.get('DISCOURSE_SMTP_PORT')
82smtp_user = envdata.get('DISCOURSE_SMTP_USER_NAME')
83smtp_pass = envdata.get('DISCOURSE_SMTP_PASSWORD')
84smtp_sslv = envdata.get('DISCOURSE_SMTP_OPENSSL_VERIFY_MODE')
85
86# Yoink out the settings from the file - we'll print them and put them in the email
87testinfo = 'DISCOURSE_SMTP_ settings:\n'
88for k,v in filter(lambda x: x[0].startswith('DISCOURSE_SMTP_'), envdata.items()):
89 if 'PASSWORD' in k:
90 v = '(hidden)'
91 testinfo += ' {} = {}\n'.format(k,v)
92print(testinfo)
93
94# Ensure at least smtp-addr is specified - everything else is optional
95if smtp_addr is None:
76a9915e 96 print_error("ERROR: DISCOURSE_SMTP_ADDRESS not specified", file=sys.stderr)
7936ebaa
MB
97 sys.exit(1)
98
99if (smtp_user is None and smtp_pass is not None) or (smtp_user is not None and smtp_pass is None):
76a9915e 100 print_error("ERROR: both username and password must be specified for auth", file=sys.stderr)
7936ebaa
MB
101 sys.exit(1)
102
76a9915e
MB
103# Do we have a known good set of parameters?
104known_good_settings = {
105 ('smtp.mandrillapp.com', 587): 'Mandrill',
106}
107try:
108 print_good('You are correctly configured to use: {}'.format(known_good_settings[smtp_addr,smtp_port]))
109except KeyError:
110 pass
111
7936ebaa
MB
112# Try and ensure the test is valid
113if destemail.split('@',1)[1] in smtp_addr:
76a9915e
MB
114 print_warn('WARNING: {} may be allowed to relay mail to {}, this may not be a valid test!'.format(smtp_addr, destemail))
115
116# Outbound port smtp?
117if smtp_port == 25 or smtp_port is None:
118 print_warn('WARNING: many networks block outbound port 25 - consider an alternative (587?)')
119
e61914cb
MB
120# Outbound port smtps?
121if smtp_port == 465:
122 print_warn("WARNING: I can't yet handle testing port 465.")
123 print_warn(" It's probably wrong though - most servers use 587 or 25 for submission.")
124
76a9915e
MB
125# Outbound port submission?
126if smtp_port == 587:
127 if smtp_user is None:
128 print_warn('WARNING: trying to use the submission (587) port without authenticating will probably fail')
7936ebaa 129
76a9915e 130# Build the message and send!
7936ebaa
MB
131msg = email.message.Message()
132msg.add_header('From', 'nobody+launcher-mailtest@discourse.org')
133msg.add_header('To', destemail)
134msg.add_header('Subject', 'discourse launcher mailtest for {}'.format(os.path.basename(cfgfile)))
135msg.set_payload(testinfo)
136
137try:
138 smtp = smtplib.SMTP(smtp_addr, smtp_port, timeout=5)
139 #smtp.debuglevel=1
140 do_tls(smtp,smtp_sslv)
141 if smtp_user:
142 smtp.login(smtp_user, smtp_pass)
143 result = smtp.sendmail('nobody+launcher-mailtest@discourse.org', destemail, msg.as_string())
144except socket.gaierror as e:
76a9915e 145 print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr)
7936ebaa
MB
146 print(" Ensure that the host '{}' exists".format(smtp_addr), file=sys.stderr)
147 sys.exit(1)
148except socket.timeout as e:
76a9915e
MB
149 print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr)
150 print(" Ensure that the host '{}' is up and port {} is reachable".format(smtp_addr, smtp_port), file=sys.stderr)
151 print(" If your settings are known-good, ensure outbound port {} is not blocked".format(smtp_port), file=sys.stderr)
7936ebaa
MB
152 sys.exit(1)
153except smtplib.SMTPConnectError as e:
76a9915e
MB
154 print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr)
155 print(" Ensure that the host '{}' is up and port {} is reachable".format(smtp_addr, smtp_port), file=sys.stderr)
7936ebaa
MB
156 sys.exit(1)
157except ssl.SSLError as e:
76a9915e 158 print_error("ERROR: unable to establish TLS: {}".format(e.args[-1]), file=sys.stderr)
7936ebaa
MB
159 if 'certificate verify failed' in e.args[-1]:
160 print(" Fix the host certificate or disable validation".format(smtp_addr), file=sys.stderr)
161 sys.exit(1)
162except smtplib.SMTPRecipientsRefused as e:
76a9915e 163 print_error("ERROR: {}".format(e.args[-1].popitem()[1][1].decode()), file=sys.stderr)
7936ebaa
MB
164 print(" You must provide a username/password to send to this host", file=sys.stderr)
165 sys.exit(1)
166except smtplib.SMTPAuthenticationError as e:
76a9915e 167 print_error("ERROR: {}".format(e.args[-1].decode()), file=sys.stderr)
7936ebaa
MB
168 print(" Check to ensure your username and password are correct", file=sys.stderr)
169 sys.exit(1)
170except smtplib.SMTPException as e:
76a9915e 171 print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr)
7936ebaa
MB
172 if 'SMTP AUTH extension not supported by server' in e.args[0]:
173 print(" Authorization is not available - you may need to use TLS", file=sys.stderr)
174 sys.exit(1)
4c634449 175except ValueError as e:
76a9915e 176 print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr)
7936ebaa
MB
177 sys.exit(1)
178
76a9915e 179print_good("Success!")