2 # -*- coding: utf-8 -*-
4 # This file is part of BridgeDB, a Tor bridge distribution system.
6 # This script sends an email request for bridges to BridgeDB and then checks if
7 # it got a response. The result is written to STATUS_FILE, which is consumed
8 # by Nagios. Whenever BridgeDB fails to respond with bridges, we will get a
11 # Run this script via crontab every three hours as follows:
12 # 0 */3 * * * path/to/nagios-email-check $(cat path/to/gmail.key)
14 # You can provide the Gmail key as an argument (as exemplified above) or by
15 # using the environment variable BRIDGEDB_GMAIL_KEY, e.g.:
16 # BRIDGEDB_GMAIL_KEY=foo path/to/nagios-email-check $(cat path/to/gmail.key)
26 # Standard Nagios return codes
27 OK, WARNING, CRITICAL, UNKNOWN = range(4)
29 FROM_EMAIL = "testbridgestorbrowser@gmail.com"
30 TO_EMAIL = "bridges@torproject.org"
31 SMTP_SERVER = "imap.gmail.com"
34 MESSAGE_FROM = TO_EMAIL
35 MESSAGE_BODY = "Here are your bridges:"
37 STATUS_FILE = "/srv/bridges.torproject.org/check/status"
39 # This will contain our test email's message ID. We later make sure that this
40 # message ID is referenced in the In-Reply-To header of BridgeDB's response.
44 def log(*args, **kwargs):
49 print("[+]", *args, file=sys.stderr, **kwargs)
52 def get_email_response(password):
54 Open our Gmail inbox and see if we got a response.
57 log("Checking for email response.")
58 mail = imaplib.IMAP4_SSL(SMTP_SERVER)
60 mail.login(FROM_EMAIL, password)
61 except Exception as e:
62 return WARNING, str(e)
66 _, data = mail.search(None, "ALL")
67 email_ids = data[0].split()
68 if len(email_ids) == 0:
69 log("Found no response.")
70 return CRITICAL, "No emails from BridgeDB found"
72 return check_email(mail, email_ids)
75 def check_email(mail, email_ids):
77 Check if we got our expected email response.
80 log("Checking {:,} emails.".format(len(email_ids)))
81 for email_id in email_ids:
82 _, data = mail.fetch(email_id, "(RFC822)")
84 # The variable `data` contains the full email object fetched by imaplib
85 # <https://docs.python.org/3/library/imaplib.html#imaplib.IMAP4.fetch>
86 # We are only interested in the response part containing the email
88 for response_part in data:
89 if isinstance(response_part, tuple):
90 m = str(response_part[1], "utf-8")
91 msg = email.message_from_string(m)
92 email_from = "{}".format(msg["From"])
93 email_body = "{}".format(msg.as_string())
94 email_reply_to = "{}".format(msg["In-Reply-To"])
96 if (MESSAGE_FROM == email_from) and \
97 (MESSAGE_BODY in email_body) and \
98 (MESSAGE_ID == email_reply_to):
99 mail.store(email_id, '+X-GM-LABELS', '\\Trash')
103 log("Found correct response (referencing {})."
105 return OK, "BridgeDB's email responder works"
107 mail.store(email_id, '+X-GM-LABELS', '\\Trash')
111 log("Found no response.")
112 return WARNING, "No emails from BridgeDB found"
115 def send_email_request(password):
117 Attempt to send a bridge request over Gmail.
123 log("Sending email.")
125 MESSAGE_ID = email.utils.make_msgid(idstring="test-bridgedb",
127 email_text = "From: %s\r\nTo: %s\r\nMessage-ID: %s\r\nSubject: %s\r\n" \
128 "\r\n%s" % (FROM_EMAIL, TO_EMAIL, MESSAGE_ID, subject, body)
131 mail = smtplib.SMTP_SSL("smtp.gmail.com", 465)
132 mail.login(FROM_EMAIL, password)
133 mail.sendmail(FROM_EMAIL, TO_EMAIL, email_text)
135 log("Email successfully sent (message ID: %s)." % MESSAGE_ID)
136 return OK, "Sent email bridge request"
137 except Exception as e:
138 log("Error while sending email: %s" % err)
139 return UNKNOWN, str(e)
142 def write_status_file(status, message):
144 Write the given `status` and `message` to our Nagios status file.
153 code = codes.get(status, UNKNOWN)
155 with open(STATUS_FILE, "w") as fd:
156 fd.write("{}\n{}: {}\n".format(code, status, message))
157 log("Wrote status='%s', message='%s' to status file." % (status, message))
160 if __name__ == "__main__":
161 status, message = None, None
163 # Our Gmail password should be in sys.argv[1].
165 if len(sys.argv) == 2:
166 password = sys.argv[1]
169 # Try to find our password in an environment variable.
172 password = os.environ["BRIDGEDB_GMAIL_KEY"]
174 log("No email password provided.")
175 write_status_file(UNKNOWN, "No email password provided")
178 # Send an email request to BridgeDB.
181 status, message = send_email_request(password)
182 except Exception as e:
183 write_status_file(UNKNOWN, repr(e))
187 log("Waiting %d seconds for a response." % wait_time)
188 time.sleep(wait_time)
190 # Check if we've received an email response.
193 status, message = get_email_response(password)
194 except KeyboardInterrupt:
195 status, message = CRITICAL, "Caught Control-C..."
196 except Exception as e:
200 write_status_file(status, message)