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 | |
39 | sslcontext=ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH) | |
40 | # The None below looks like might be a typo but it's not - it represents the ActiveRecord default (to verify) | |
41 | if sslv in (None, 'peer', 'client_once', 'fail_if_no_peer_cert'): | |
42 | # defaults are good | |
43 | conn.starttls(context=sslcontext) | |
44 | elif sslv in ('none',): | |
45 | # disable cert checking | |
46 | sslcontext.check_hostname = False | |
47 | sslcontext.verify_mode = ssl.CERT_NONE | |
48 | conn.starttls(context=sslcontext) | |
49 | else: | |
50 | raise ValueError('invalid value for DISCOURSE_SMTP_OPENSSL_VERIFY_MODE: {}'.format(sslv)) | |
51 | except smtplib.SMTPException as e: | |
52 | if (sslv is None) and ('STARTTLS extension not supported by server' in e.args[0]): | |
76a9915e | 53 | print_warn("unable to establish TLS, continuing: {}".format(e.args[0])) |
7936ebaa MB |
54 | else: |
55 | raise | |
56 | ||
57 | ### Start of execution ### | |
58 | cfgfile = sys.argv[1] | |
59 | try: | |
60 | destemail = sys.argv[2] | |
61 | except IndexError: | |
62 | destemail = input('Enter your email address: ') | |
63 | srcemail = 'nobody+launcher-mailtest@discourse.org' | |
64 | ||
65 | # Read in the container yaml and grab the env section | |
66 | cfgdata = yaml.safe_load(open(cfgfile).read()) | |
67 | envdata = cfgdata['env'] | |
68 | ||
69 | # Here are the variables we'll test | |
70 | smtp_addr = envdata.get('DISCOURSE_SMTP_ADDRESS') | |
71 | smtp_port = envdata.get('DISCOURSE_SMTP_PORT') | |
72 | smtp_user = envdata.get('DISCOURSE_SMTP_USER_NAME') | |
73 | smtp_pass = envdata.get('DISCOURSE_SMTP_PASSWORD') | |
74 | smtp_sslv = envdata.get('DISCOURSE_SMTP_OPENSSL_VERIFY_MODE') | |
75 | ||
76 | # Yoink out the settings from the file - we'll print them and put them in the email | |
77 | testinfo = 'DISCOURSE_SMTP_ settings:\n' | |
78 | for k,v in filter(lambda x: x[0].startswith('DISCOURSE_SMTP_'), envdata.items()): | |
79 | if 'PASSWORD' in k: | |
80 | v = '(hidden)' | |
81 | testinfo += ' {} = {}\n'.format(k,v) | |
82 | print(testinfo) | |
83 | ||
84 | # Ensure at least smtp-addr is specified - everything else is optional | |
85 | if smtp_addr is None: | |
76a9915e | 86 | print_error("ERROR: DISCOURSE_SMTP_ADDRESS not specified", file=sys.stderr) |
7936ebaa MB |
87 | sys.exit(1) |
88 | ||
89 | if (smtp_user is None and smtp_pass is not None) or (smtp_user is not None and smtp_pass is None): | |
76a9915e | 90 | print_error("ERROR: both username and password must be specified for auth", file=sys.stderr) |
7936ebaa MB |
91 | sys.exit(1) |
92 | ||
76a9915e MB |
93 | # Do we have a known good set of parameters? |
94 | known_good_settings = { | |
95 | ('smtp.mandrillapp.com', 587): 'Mandrill', | |
96 | } | |
97 | try: | |
98 | print_good('You are correctly configured to use: {}'.format(known_good_settings[smtp_addr,smtp_port])) | |
99 | except KeyError: | |
100 | pass | |
101 | ||
7936ebaa MB |
102 | # Try and ensure the test is valid |
103 | if destemail.split('@',1)[1] in smtp_addr: | |
76a9915e MB |
104 | print_warn('WARNING: {} may be allowed to relay mail to {}, this may not be a valid test!'.format(smtp_addr, destemail)) |
105 | ||
106 | # Outbound port smtp? | |
107 | if smtp_port == 25 or smtp_port is None: | |
108 | print_warn('WARNING: many networks block outbound port 25 - consider an alternative (587?)') | |
109 | ||
110 | # Outbound port submission? | |
111 | if smtp_port == 587: | |
112 | if smtp_user is None: | |
113 | print_warn('WARNING: trying to use the submission (587) port without authenticating will probably fail') | |
7936ebaa | 114 | |
76a9915e | 115 | # Build the message and send! |
7936ebaa MB |
116 | msg = email.message.Message() |
117 | msg.add_header('From', 'nobody+launcher-mailtest@discourse.org') | |
118 | msg.add_header('To', destemail) | |
119 | msg.add_header('Subject', 'discourse launcher mailtest for {}'.format(os.path.basename(cfgfile))) | |
120 | msg.set_payload(testinfo) | |
121 | ||
122 | try: | |
123 | smtp = smtplib.SMTP(smtp_addr, smtp_port, timeout=5) | |
124 | #smtp.debuglevel=1 | |
125 | do_tls(smtp,smtp_sslv) | |
126 | if smtp_user: | |
127 | smtp.login(smtp_user, smtp_pass) | |
128 | result = smtp.sendmail('nobody+launcher-mailtest@discourse.org', destemail, msg.as_string()) | |
129 | except socket.gaierror as e: | |
76a9915e | 130 | print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) |
7936ebaa MB |
131 | print(" Ensure that the host '{}' exists".format(smtp_addr), file=sys.stderr) |
132 | sys.exit(1) | |
133 | except socket.timeout as e: | |
76a9915e MB |
134 | print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) |
135 | print(" Ensure that the host '{}' is up and port {} is reachable".format(smtp_addr, smtp_port), file=sys.stderr) | |
136 | print(" If your settings are known-good, ensure outbound port {} is not blocked".format(smtp_port), file=sys.stderr) | |
7936ebaa MB |
137 | sys.exit(1) |
138 | except smtplib.SMTPConnectError as e: | |
76a9915e MB |
139 | print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) |
140 | print(" Ensure that the host '{}' is up and port {} is reachable".format(smtp_addr, smtp_port), file=sys.stderr) | |
7936ebaa MB |
141 | sys.exit(1) |
142 | except ssl.SSLError as e: | |
76a9915e | 143 | print_error("ERROR: unable to establish TLS: {}".format(e.args[-1]), file=sys.stderr) |
7936ebaa MB |
144 | if 'certificate verify failed' in e.args[-1]: |
145 | print(" Fix the host certificate or disable validation".format(smtp_addr), file=sys.stderr) | |
146 | sys.exit(1) | |
147 | except smtplib.SMTPRecipientsRefused as e: | |
76a9915e | 148 | print_error("ERROR: {}".format(e.args[-1].popitem()[1][1].decode()), file=sys.stderr) |
7936ebaa MB |
149 | print(" You must provide a username/password to send to this host", file=sys.stderr) |
150 | sys.exit(1) | |
151 | except smtplib.SMTPAuthenticationError as e: | |
76a9915e | 152 | print_error("ERROR: {}".format(e.args[-1].decode()), file=sys.stderr) |
7936ebaa MB |
153 | print(" Check to ensure your username and password are correct", file=sys.stderr) |
154 | sys.exit(1) | |
155 | except smtplib.SMTPException as e: | |
76a9915e | 156 | print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) |
7936ebaa MB |
157 | if 'SMTP AUTH extension not supported by server' in e.args[0]: |
158 | print(" Authorization is not available - you may need to use TLS", file=sys.stderr) | |
159 | sys.exit(1) | |
160 | except ValueError: | |
76a9915e | 161 | print_error("ERROR: {}".format(e.args[-1]), file=sys.stderr) |
7936ebaa MB |
162 | sys.exit(1) |
163 | ||
76a9915e | 164 | print_good("Success!") |