Commit | Line | Data |
---|---|---|
7936ebaa MB |
1 | #!/usr/bin/env python3 |
2 | # vim:sw=4 ts=4 et: | |
3 | ||
4 | import email | |
5 | import smtplib | |
6 | import ssl | |
7 | import socket | |
8 | import sys | |
9 | import os | |
10 | ||
76a9915e MB |
11 | try: |
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) | |
22 | except 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 |
28 | try: |
29 | import yaml | |
30 | except 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 | ||
35 | def 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 ### | |
68 | cfgfile = sys.argv[1] | |
69 | try: | |
70 | destemail = sys.argv[2] | |
71 | except IndexError: | |
72 | destemail = input('Enter your email address: ') | |
73 | srcemail = 'nobody+launcher-mailtest@discourse.org' | |
74 | ||
75 | # Read in the container yaml and grab the env section | |
76 | cfgdata = yaml.safe_load(open(cfgfile).read()) | |
77 | envdata = cfgdata['env'] | |
78 | ||
79 | # Here are the variables we'll test | |
80 | smtp_addr = envdata.get('DISCOURSE_SMTP_ADDRESS') | |
81 | smtp_port = envdata.get('DISCOURSE_SMTP_PORT') | |
82 | smtp_user = envdata.get('DISCOURSE_SMTP_USER_NAME') | |
83 | smtp_pass = envdata.get('DISCOURSE_SMTP_PASSWORD') | |
84 | smtp_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 | |
87 | testinfo = 'DISCOURSE_SMTP_ settings:\n' | |
88 | for 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) | |
92 | print(testinfo) | |
93 | ||
94 | # Ensure at least smtp-addr is specified - everything else is optional | |
95 | if smtp_addr is None: | |
76a9915e | 96 | print_error("ERROR: DISCOURSE_SMTP_ADDRESS not specified", file=sys.stderr) |
7936ebaa MB |
97 | sys.exit(1) |
98 | ||
99 | if (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? |
104 | known_good_settings = { | |
105 | ('smtp.mandrillapp.com', 587): 'Mandrill', | |
106 | } | |
107 | try: | |
108 | print_good('You are correctly configured to use: {}'.format(known_good_settings[smtp_addr,smtp_port])) | |
109 | except KeyError: | |
110 | pass | |
111 | ||
7936ebaa MB |
112 | # Try and ensure the test is valid |
113 | if 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? | |
117 | if 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? |
121 | if 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? |
126 | if 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 |
131 | msg = email.message.Message() |
132 | msg.add_header('From', 'nobody+launcher-mailtest@discourse.org') | |
133 | msg.add_header('To', destemail) | |
134 | msg.add_header('Subject', 'discourse launcher mailtest for {}'.format(os.path.basename(cfgfile))) | |
135 | msg.set_payload(testinfo) | |
136 | ||
137 | try: | |
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()) | |
144 | except 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) | |
148 | except 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) |
153 | except 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) |
157 | except 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) | |
162 | except 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) | |
166 | except 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) | |
170 | except 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 | 175 | except ValueError as e: |
76a9915e | 176 | print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) |
7936ebaa MB |
177 | sys.exit(1) |
178 | ||
76a9915e | 179 | print_good("Success!") |