Support filler messages.
[easyotp.git] / SecureMail.py
blob2339e7e7f942b747900c41d9c7c921c8e5f90d45
1 #!/bin/env python
2 # Created:20080603
3 # By Jeff Connelly
5 # Communicate with mail servers
7 import imaplib
8 import smtplib
9 import getpass
10 import email
11 import cotp
12 import threading
13 import Queue
14 import time
16 # Every X seconds, send a message, and queue messages to be sent at in this
17 # interval.
18 FILL_INTERVAL = 10
20 # Real subject used for all encrypted messages
21 # Note: make a Gmail filter that filters (Secure Message) into the Secure tag
22 FAKE_SUBJECT = "(Secure Message)"
24 # Dummy message used in channel filling if there is nothing to send.
25 DUMMY_SUBJECT = "Filler Message"
26 # TODO: should be the person you're always sending to
27 FILLER_TO = "shellreef@gmail.com"
29 class SecureMail(threading.Thread):
30 def __init__(self, username=None, password=None):
31 """Prompt for login and password, or using stored values if possible, then login."""
33 if username is None:
34 try:
35 username = file(".otplogin").read().strip()
36 except:
37 username = raw_input("Username: ")
39 if password is None:
40 try:
41 password = file(".otppass").read().strip()
42 except:
43 password = getpass.getpass()
45 threading.Thread.__init__(self)
46 return self.login(username, password)
49 def login(self, username, password):
50 """Login to a Gmail account, IMAP and SMTP server, with login and password."""
52 # IMAP SSL for incoming messages
53 self.mail_in = imaplib.IMAP4_SSL("imap.gmail.com", 993)
55 self.username = username
57 try:
58 typ, data = self.mail_in.login(username, password)
59 except imaplib:
60 raise "Login failure: %s" % (sys.exc_info(),)
61 else:
62 assert typ == "OK", "imap login returned: %s %s" % (status, message)
63 print "Logged in:", typ, data
65 # Always have "Secure" mailbox selected
66 typ, num_msgs = self.mail_in.select(mailbox="Secure")
67 if typ != "OK":
68 raise ("imap select failure: %s %s" % (typ, num_msgs) +
69 "The 'Secure' tag doesn't exist. Tag some of your EMOTP messages" +
70 "using a new label, named 'Secure'")
72 # Outgoing mail, SMTP server
73 self.mail_out = smtplib.SMTP("smtp.gmail.com", 25)
74 ret = self.mail_out.ehlo()
75 assert ret[0] == 250, "SMTP server EHLO failed: %s" % (ret,)
77 ret = self.mail_out.starttls()
78 assert ret[0] == 220, "SMTP server STARTTLS failed: %s" % (ret,)
80 ret = self.mail_out.ehlo()
81 assert ret[0] == 250, "SMTP server EHLO over TLS failed: %s" % (ret,)
83 ret = self.mail_out.noop()
84 assert ret[0] == 250, "SMTP server NOOP failed: %s" % (ret,)
86 ret = self.mail_out.login(username, password)
87 assert ret[0] == 235, "SMTP server login failed: %s" % (ret,)
88 print "Logged in to SMTP server: %s" % (ret,)
90 # Start channel filler
91 self.sendq = Queue.Queue()
92 self.start()
94 # Channel filling thread
95 # Problem: computer needs to be online to fill channel!
96 def run(self):
97 if FILL_INTERVAL is None:
98 print "Channel filling disabled - messages will send immediately"
99 return
101 while True:
102 try:
103 item = self.sendq.get_nowait()
104 except Queue.Empty:
105 print "Sending filler message"
106 body = "" # empty, they should have padding!
107 to = FILLER_TO # todo: find out other person's email
108 subject = DUMMY_SUBJECT
110 # Note: subject is encrypted and sent in body, so it is secure
111 self.send_now(to, subject, body)
112 else:
113 print "Sending queued message now"
114 to, subject, body = item
115 self.send_now(to, subject, body)
117 time.sleep(FILL_INTERVAL)
122 def get_messages(self):
123 msgs = []
125 try:
126 typ, all_msgs_string = self.mail_in.search(None, 'ALL')
127 except imaplib.error, e:
128 raise "imap search failed: %s" % (e,)
130 all_msgs = all_msgs_string[0].split()
131 for num in all_msgs:
132 typ, body = self.mail_in.fetch(num, "(BODY[])")
133 msg = email.message_from_string(body[0][1])
135 enc_body = str(msg.get_payload())
136 fake_subject = msg.get("Subject") # not encrypted
137 sender = msg.get("From")
138 id = msg.get("Message-ID")
140 #print 'Message %s\n%s\n' % (num, data[0][1])
141 if "--EMOTP_BEGIN--" not in enc_body:
142 continue
144 subject_plus_body = cotp.decode(enc_body)
146 if "<body>" in subject_plus_body:
147 # encrypted subject
148 subject, body = subject_plus_body.split("<body>")
149 else:
150 subject, body = fake_subject, subject_plus_body
152 msgs.append({"body": body,
153 "fake-subject": fake_subject,
154 "subject": subject,
155 "sender": sender,
156 "id": id,
157 "num": num})
159 return msgs
161 def __iter__(self):
162 """Fetch messages from server."""
163 self.msgs = self.get_messages()
164 return iter(self.msgs)
166 def __getitem__(self, k):
167 """Lookup a message of a given number. Messages
168 must have been previous fetched from server up by __iter__."""
170 return self.msgs[k]
172 def send(self, to, subject, body):
173 """Send, in a timeslot if channel filling is enabled, or immediately
174 if channel filling is disabled."""
175 if FILL_INTERVAL is None:
176 print "Sending %s bytes now" % (len(body,))
177 return self.send(to, subject, body)
178 else:
179 self.sendq.put((to, subject, body))
180 print "Enqueued to send at next interval, pending: %s" % (self.sendq.qsize(),)
181 return None
183 def send_now(self, to, subject, body):
184 """Send an encrypted message immediately."""
185 from_address = "%s@gmail.com" % (self.username,)
186 # TODO: encode with different pads based on 'to' email
187 enc_body = cotp.encode(subject + "<body>" + body)
188 return self.mail_out.sendmail(from_address,
189 [to],
190 "\r\n".join([
191 "From: %s" % (from_address,),
192 "To: %s" % (to,),
193 "Subject: %s" % (FAKE_SUBJECT,),
195 enc_body]))
197 def __del__(self):
198 self.mail_in.close()
199 self.mail_in.logout()
200 self.mail_out.quit()
202 def main():
203 ms = SecureMail("shellreef", getpass.getpass())
205 for m in ms.get_messages():
206 print m["sender"], m["subject"]
208 if __name__ == "__main__":
209 main()