5 # Communicate with mail servers
16 # Every X seconds, send a message, and queue messages to be sent at in this
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."""
36 username
= file(".otplogin").read().strip()
38 username
= raw_input("Username: ")
42 password
= file(".otppass").read().strip()
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
59 typ
, data
= self
.mail_in
.login(username
, password
)
61 raise "Login failure: %s" % (sys
.exc_info(),)
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")
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()
95 # Channel filling thread
96 # Problem: computer needs to be online to fill channel!
98 if FILL_INTERVAL
is None:
99 print "Channel filling disabled - messages will send immediately"
104 item
= self
.sendq
.get_nowait()
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
)
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
):
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()
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
:
145 subject_plus_body
= cotp
.decode(enc_body
).strip()
147 if "<body>" in subject_plus_body
:
149 subject
, body
= subject_plus_body
.split("<body>")
151 subject
, body
= fake_subject
, subject_plus_body
153 if subject
== DUMMY_SUBJECT
:
157 msgs
.append({"body": body
,
158 "body-enc": enc_body
,
159 "fake-subject": fake_subject
,
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__."""
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
:
190 subject
, body
= subject_plus_body
.split("<body>")
192 subject
, body
= self
.msgs
[k
]["fake-subject"], subject_plus_body
194 self
.msgs
[k
]["subject"] = subject
195 self
.msgs
[k
]["body"] = body
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
)
206 self
.sendq
.put((to
, subject
, body
))
207 print "Enqueued to send at next interval, pending: %s" % (self
.sendq
.qsize(),)
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
,
218 "From: %s" % (from_address
,),
220 "Subject: %s" % (FAKE_SUBJECT
,),
226 self
.mail_in
.logout()
230 ms
= SecureMail("shellreef", getpass
.getpass())
232 for m
in ms
.get_messages():
233 print m
["sender"], m
["subject"]
235 if __name__
== "__main__":