Add pad rewriting, in otp_replace.
[easyotp.git] / SecureMail.py
blob04d49388b32b1480377a282571d31e37c8afcf54
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)
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 "fake-subject": fake_subject,
159 "subject": subject,
160 "sender": sender,
161 "id": id,
162 "num": num})
164 return msgs
166 def __iter__(self):
167 """Fetch messages from server."""
168 self.msgs = self.get_messages()
169 return iter(self.msgs)
171 def __getitem__(self, k):
172 """Lookup a message of a given number. Messages
173 must have been previous fetched from server up by __iter__."""
175 return self.msgs[k]
177 def send(self, to, subject, body):
178 """Send, in a timeslot if channel filling is enabled, or immediately
179 if channel filling is disabled."""
180 if FILL_INTERVAL is None:
181 print "Sending %s bytes now" % (len(body,))
182 return self.send_now(to, subject, body)
183 else:
184 self.sendq.put((to, subject, body))
185 print "Enqueued to send at next interval, pending: %s" % (self.sendq.qsize(),)
186 return None
188 def send_now(self, to, subject, body):
189 """Send an encrypted message immediately."""
190 from_address = "%s@gmail.com" % (self.username,)
191 # TODO: encode with different pads based on 'to' email
192 enc_body = cotp.encode(subject + "<body>" + body)
193 return self.mail_out.sendmail(from_address,
194 [to],
195 "\r\n".join([
196 "From: %s" % (from_address,),
197 "To: %s" % (to,),
198 "Subject: %s" % (FAKE_SUBJECT,),
200 enc_body]))
202 def __del__(self):
203 self.mail_in.close()
204 self.mail_in.logout()
205 self.mail_out.quit()
207 def main():
208 ms = SecureMail("shellreef", getpass.getpass())
210 for m in ms.get_messages():
211 print m["sender"], m["subject"]
213 if __name__ == "__main__":
214 main()