Integrate pad rewriting into cli.
[easyotp.git] / SecureMail.py
blobd8e379b668349f40932a828d14a77e84aa2a3cc9
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
19 FILL_INTERVAL = None
21 # Real subject used for all encrypted messages
22 # Note: make a Gmail filter that filters (Secure Message) into the Secure tag
23 FAKE_SUBJECT = "(Secure Message)"
25 # Dummy message used in channel filling if there is nothing to send.
26 DUMMY_SUBJECT = "Filler Message"
27 # TODO: should be the person you're always sending to
28 FILLER_TO = "shellreef@gmail.com"
30 class SecureMail(threading.Thread):
31 def __init__(self, username=None, password=None):
32 """Prompt for login and password, or using stored values if possible, then login."""
34 if username is None:
35 try:
36 username = file(".otplogin").read().strip()
37 except:
38 username = raw_input("Username: ")
40 if password is None:
41 try:
42 password = file(".otppass").read().strip()
43 except:
44 password = getpass.getpass()
46 threading.Thread.__init__(self)
47 return self.login(username, password)
50 def login(self, username, password):
51 """Login to a Gmail account, IMAP and SMTP server, with login and password."""
53 # IMAP SSL for incoming messages
54 self.mail_in = imaplib.IMAP4_SSL("imap.gmail.com", 993)
56 self.username = username
58 try:
59 typ, data = self.mail_in.login(username, password)
60 except imaplib:
61 raise "Login failure: %s" % (sys.exc_info(),)
62 else:
63 assert typ == "OK", "imap login returned: %s %s" % (status, message)
64 print "Logged in:", typ, data
66 # Always have "Secure" mailbox selected
67 typ, num_msgs = self.mail_in.select(mailbox="Secure")
68 if typ != "OK":
69 raise ("imap select failure: %s %s" % (typ, num_msgs) +
70 "The 'Secure' tag doesn't exist. Tag some of your EMOTP messages" +
71 "using a new label, named 'Secure'")
73 # Outgoing mail, SMTP server
74 self.mail_out = smtplib.SMTP("smtp.gmail.com", 25)
75 ret = self.mail_out.ehlo()
76 assert ret[0] == 250, "SMTP server EHLO failed: %s" % (ret,)
78 ret = self.mail_out.starttls()
79 assert ret[0] == 220, "SMTP server STARTTLS failed: %s" % (ret,)
81 ret = self.mail_out.ehlo()
82 assert ret[0] == 250, "SMTP server EHLO over TLS failed: %s" % (ret,)
84 ret = self.mail_out.noop()
85 assert ret[0] == 250, "SMTP server NOOP failed: %s" % (ret,)
87 ret = self.mail_out.login(username, password)
88 assert ret[0] == 235, "SMTP server login failed: %s" % (ret,)
89 print "Logged in to SMTP server: %s" % (ret,)
91 # Start channel filler
92 self.sendq = Queue.Queue()
93 self.start()
95 # Channel filling thread
96 # Problem: computer needs to be online to fill channel!
97 def run(self):
98 if FILL_INTERVAL is None:
99 print "Channel filling disabled - messages will send immediately"
100 return
102 while True:
103 try:
104 item = self.sendq.get_nowait()
105 except Queue.Empty:
106 print "Sending filler message"
107 body = "" # empty, they should have padding!
108 to = FILLER_TO # todo: find out other person's email
109 subject = DUMMY_SUBJECT
111 # Note: subject is encrypted and sent in body, so it is secure
112 self.send_now(to, subject, body)
113 else:
114 print "Sending queued message now"
115 to, subject, body = item
116 self.send_now(to, subject, body)
118 time.sleep(FILL_INTERVAL)
123 def get_messages(self):
124 msgs = []
126 try:
127 typ, all_msgs_string = self.mail_in.search(None, 'ALL')
128 except imaplib.error, e:
129 raise "imap search failed: %s" % (e,)
131 all_msgs = all_msgs_string[0].split()
132 for num in all_msgs:
133 typ, body = self.mail_in.fetch(num, "(BODY[])")
134 msg = email.message_from_string(body[0][1])
136 enc_body = str(msg.get_payload())
137 fake_subject = msg.get("Subject") # not encrypted
138 sender = msg.get("From")
139 id = msg.get("Message-ID")
141 #print 'Message %s\n%s\n' % (num, data[0][1])
142 if "--EMOTP_BEGIN--" not in enc_body:
143 continue
145 subject_plus_body = cotp.decode(enc_body).strip()
147 if "<body>" in subject_plus_body:
148 # encrypted subject
149 subject, body = subject_plus_body.split("<body>")
150 else:
151 subject, body = fake_subject, subject_plus_body
153 if subject == DUMMY_SUBJECT:
154 # ignore filler
155 continue
157 msgs.append({"body": body,
158 "body-enc": enc_body,
159 "fake-subject": fake_subject,
160 "subject": subject,
161 "sender": sender,
162 "id": id,
163 "num": num})
165 return msgs
167 def __iter__(self):
168 """Fetch messages from server."""
169 self.msgs = self.get_messages()
170 return iter(self.msgs)
172 def __getitem__(self, k):
173 """Lookup a message of a given number. Messages
174 must have been previous fetched from server up by __iter__."""
176 return self.msgs[k]
178 def replace(self, k, subject, body):
179 """Replace the message that 'k' decrypts to with 'new', by
180 rewriting the pad. The same ciphertext now decrypts to 'new' instead
181 of what it used to."""
182 new = subject + "<body>" + body
183 ret = cotp.replace(self.msgs[k]["body-enc"] + "\n" + new)
185 # Re-decrypt on our side for convenience
186 subject_plus_body = cotp.decode(self.msgs[k]["body-enc"]).strip()
188 if "<body>" in subject_plus_body:
189 # encrypted subject
190 subject, body = subject_plus_body.split("<body>")
191 else:
192 subject, body = self.msgs[k]["fake-subject"], subject_plus_body
194 self.msgs[k]["subject"] = subject
195 self.msgs[k]["body"] = body
197 return ret
199 def send(self, to, subject, body):
200 """Send, in a timeslot if channel filling is enabled, or immediately
201 if channel filling is disabled."""
202 if FILL_INTERVAL is None:
203 print "Sending %s bytes now" % (len(body,))
204 return self.send_now(to, subject, body)
205 else:
206 self.sendq.put((to, subject, body))
207 print "Enqueued to send at next interval, pending: %s" % (self.sendq.qsize(),)
208 return None
210 def send_now(self, to, subject, body):
211 """Send an encrypted message immediately."""
212 from_address = "%s@gmail.com" % (self.username,)
213 # TODO: encode with different pads based on 'to' email
214 enc_body = cotp.encode(subject + "<body>" + body)
215 return self.mail_out.sendmail(from_address,
216 [to],
217 "\r\n".join([
218 "From: %s" % (from_address,),
219 "To: %s" % (to,),
220 "Subject: %s" % (FAKE_SUBJECT,),
222 enc_body]))
224 def __del__(self):
225 self.mail_in.close()
226 self.mail_in.logout()
227 self.mail_out.quit()
229 def main():
230 ms = SecureMail("shellreef", getpass.getpass())
232 for m in ms.get_messages():
233 print m["sender"], m["subject"]
235 if __name__ == "__main__":
236 main()