├── .gitignore
├── Idler.py
├── PyMail.py
├── SubProcessor.py
├── Text.py
├── config.txt
├── imaplib2.py
└── readme.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.pyc
3 |
--------------------------------------------------------------------------------
/Idler.py:
--------------------------------------------------------------------------------
1 | from threading import Thread, Event
2 |
3 |
4 | class Idler(object):
5 | def init(self, conn):
6 | self.thread = Thread(target=self.idle)
7 | self.M = conn
8 | self.event = Event()
9 |
10 | def start(self):
11 | self.thread.start()
12 |
13 | def stop(self):
14 | # This is a neat trick to make thread end. Took me a
15 | # while to figure that one out!
16 | self.event.set()
17 |
18 | def join(self):
19 | self.thread.join()
20 |
21 | def idle(self):
22 | # Starting an unending loop here
23 | while True:
24 | # This is part of the trick to make the loop stop
25 | # when the stop() command is given
26 | if self.event.isSet():
27 | return
28 | self.needsync = False
29 | # A callback method that gets called when a new
30 | # email arrives. Very basic, but that's good.
31 |
32 | def callback(args):
33 | if not self.event.isSet():
34 | self.needsync = True
35 | self.event.set()
36 | # Do the actual idle call. This returns immediately,
37 | # since it's asynchronous.
38 | self.M.idle(callback=callback)
39 | # This waits until the event is set. The event is
40 | # set by the callback, when the server 'answers'
41 | # the idle call and the callback function gets
42 | # called.
43 | self.event.wait()
44 | # Because the function sets the needsync variable,
45 | # this helps escape the loop without doing
46 | # anything if the stop() is called. Kinda neat
47 | # solution.
48 | if self.needsync:
49 | self.event.clear()
50 | self.dosync()
51 |
52 | # The method that gets called when a new email arrives.
53 | # Replace it with something better.
54 | def dosync(self):
55 | from PyMail import process_inbox
56 | print "Got an event!"
57 | process_inbox()
58 |
--------------------------------------------------------------------------------
/PyMail.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import imaplib2
3 | from threading import *
4 | import email
5 | import Text
6 | import time
7 | import os
8 | import SubProcessor
9 |
10 | M = None
11 | idler = None
12 | thread = None
13 | process = SubProcessor.SubProcessor(os.getcwd())
14 | phonenum = ""
15 | sender = ""
16 | emailPass = ""
17 | recipient = ""
18 |
19 |
20 | def check():
21 | try:
22 | process_inbox()
23 | except:
24 | Text.sendText("Error", sender, emailPass, phonenum)
25 |
26 |
27 | def process_inbox():
28 | rv, data = M.search(None, "(UNSEEN)")
29 | if rv != 'OK':
30 | print "No Messages Found!"
31 | return
32 |
33 | for num in data[0].split():
34 | rv, data = M.fetch(num, '(RFC822)')
35 |
36 | if rv != 'OK':
37 | print "Error getting message ", num
38 | return
39 |
40 | msg = email.message_from_string(data[0][1])
41 |
42 | msg = msg.as_string()
43 |
44 | msgBody = msg[msg.index('
') + 4: msg.index(' | ')].strip()
45 |
46 | M.store(num, '+FLAGS', '\\Deleted')
47 |
48 | response = ""
49 | if(not process.authorized):
50 | response = process.authorize(phonenum, msgBody)
51 | else:
52 | response = process.run(msgBody)
53 | if(response.strip() != ""):
54 | Text.sendText(response, sender, emailPass, phonenum)
55 | else:
56 | Text.sendText(msgBody + " was successfully called.", sender, emailPass, phonenum)
57 |
58 | M.expunge()
59 | print "Leaving Box"
60 |
61 |
62 | def dosync():
63 | print "Got an event!"
64 | check()
65 |
66 |
67 | def idle():
68 | while True:
69 | if event.isSet():
70 | return
71 |
72 | def callback(args):
73 | if not event.isSet():
74 | needsync = True
75 | event.set()
76 |
77 | M.idle(callback=callback)
78 |
79 | event.wait()
80 |
81 | if needsync:
82 | event.clear()
83 | dosync()
84 |
85 |
86 | config = open('config.txt', 'r')
87 |
88 | sender = config.readline()
89 | emailPass = config.readline()
90 | phonenum = config.readline().strip()
91 |
92 | Text.sendText("Send password.", sender, emailPass, phonenum)
93 |
94 |
95 | M = imaplib2.IMAP4_SSL('imap.gmail.com')
96 | M.login(sender, emailPass)
97 | M.select("inbox")
98 | check()
99 |
100 | #init
101 | thread = Thread(target=idle)
102 | event = Event()
103 |
104 | #start
105 | thread.start()
106 |
107 | time.sleep(60*60)
108 |
109 | #stop
110 | event.set()
111 |
112 | #join
113 | thread.join()
114 |
115 | M.expunge()
116 | M.close()
117 | M.logout()
118 | print "DONE!"
119 |
--------------------------------------------------------------------------------
/SubProcessor.py:
--------------------------------------------------------------------------------
1 | import subprocess
2 | import os
3 |
4 | class SubProcessor():
5 |
6 | def __init__(self, cwd):
7 | self.cwd = cwd
8 | try:
9 | config = open('config.txt', 'r')
10 | print config.readline().strip()
11 | print config.readline().strip()
12 | usr = config.readline().strip()
13 | print usr
14 | pss = config.readline().strip()
15 | print pss
16 | self.users = {usr: pss}
17 | except IOError:
18 | print "IOError - no config file"
19 | self.authorized = False
20 |
21 | def authorize(self, user, password):
22 | correct_pw = ""
23 | try:
24 | password = password.strip()
25 | user = user.strip()
26 |
27 | print password
28 | print user
29 |
30 | print self.users
31 |
32 | correct_pw = self.users[user]
33 |
34 | if correct_pw == password:
35 | self.authorized = True
36 | return "Authorization success."
37 | else:
38 | return "Authorization failed. Try again."
39 | except KeyError:
40 | return "Authorization failed. Try again."
41 |
42 |
43 | def run(self, command):
44 | if (not self.authorized):
45 | return "Server access not authorized."
46 | # change directory to current (as defined by previously executed commands)
47 | os.chdir(self.cwd)
48 |
49 | # handle directory changes
50 | args = command.split(" ")
51 | dirChanged = False
52 | if (args[0] == "cd"):
53 | #command += "; pwd"
54 | dirChanged = True
55 |
56 | # handle incorrect commands
57 | try:
58 | output = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True);
59 | except subprocess.CalledProcessError as e:
60 | output = e.output.replace(":", "")
61 | dirChanged = False
62 |
63 | # change directory if necessary
64 | if (dirChanged):
65 | self.cwd = subprocess.check_output(command+"; pwd", shell=True).strip()
66 | return output
67 |
--------------------------------------------------------------------------------
/Text.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import smtplib
4 |
5 |
6 | def sendText(text, sender, password, recipient):
7 | servr = smtplib.SMTP("smtp.gmail.com:587")
8 | servr.starttls()
9 | servr.login(sender, password)
10 |
11 | servr.sendmail(sender, recipient, text.replace('/n/n', '/n'))
12 |
13 | servr.quit()
14 |
15 | print "Sent Text: " + text
16 |
--------------------------------------------------------------------------------
/config.txt:
--------------------------------------------------------------------------------
1 | ianlinuxserver@gmail.com
2 | LinuxMint2015!
3 | 3038153710@mms.att.net
4 | hackrice2016
5 |
--------------------------------------------------------------------------------
/imaplib2.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | """Threaded IMAP4 client.
4 |
5 | Based on RFC 3501 and original imaplib module.
6 |
7 | Public classes: IMAP4
8 | IMAP4_SSL
9 | IMAP4_stream
10 |
11 | Public functions: Internaldate2Time
12 | ParseFlags
13 | Time2Internaldate
14 | """
15 |
16 |
17 | __all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
18 | "Internaldate2Time", "ParseFlags", "Time2Internaldate")
19 |
20 | __version__ = "2.45"
21 | __release__ = "2"
22 | __revision__ = "45"
23 | __credits__ = """
24 | Authentication code contributed by Donn Cave June 1998.
25 | String method conversion by ESR, February 2001.
26 | GET/SETACL contributed by Anthony Baxter April 2001.
27 | IMAP4_SSL contributed by Tino Lange March 2002.
28 | GET/SETQUOTA contributed by Andreas Zeidler June 2002.
29 | PROXYAUTH contributed by Rick Holbert November 2002.
30 | IDLE via threads suggested by Philippe Normand January 2005.
31 | GET/SETANNOTATION contributed by Tomas Lindroos June 2005.
32 | COMPRESS/DEFLATE contributed by Bron Gondwana May 2009.
33 | STARTTLS from Jython's imaplib by Alan Kennedy.
34 | ID contributed by Dave Baggett November 2009.
35 | Improved untagged responses handling suggested by Dave Baggett November 2009.
36 | Improved thread naming, and 0 read detection contributed by Grant Edwards June 2010.
37 | Improved timeout handling contributed by Ivan Vovnenko October 2010.
38 | Timeout handling further improved by Ethan Glasser-Camp December 2010.
39 | Time2Internaldate() patch to match RFC2060 specification of English month names from bugs.python.org/issue11024 March 2011.
40 | starttls() bug fixed with the help of Sebastian Spaeth April 2011.
41 | Threads now set the "daemon" flag (suggested by offlineimap-project) April 2011.
42 | Single quoting introduced with the help of Vladimir Marek August 2011.
43 | Support for specifying SSL version by Ryan Kavanagh July 2013.
44 | Fix for gmail "read 0" error provided by Jim Greenleaf August 2013.
45 | Fix for offlineimap "indexerror: string index out of range" bug provided by Eygene Ryabinkin August 2013.
46 | Fix for missing idle_lock in _handler() provided by Franklin Brook August 2014.
47 | Conversion to Python3 provided by F. Malina February 2015.
48 | Fix for READ-ONLY error from multiple EXAMINE/SELECT calls by Pierre-Louis Bonicoli March 2015.
49 | Fix for null strings appended to untagged responses by Pierre-Louis Bonicoli March 2015.
50 | Fix for correct byte encoding for _CRAM_MD5_AUTH taken from python3.5 imaplib.py June 2015."""
51 | __author__ = "Piers Lauder "
52 | __URL__ = "http://imaplib2.sourceforge.net"
53 | __license__ = "Python License"
54 |
55 | import binascii, errno, os, random, re, select, socket, sys, time, threading, zlib
56 |
57 | try:
58 | import queue # py3
59 | string_types = str
60 | except ImportError:
61 | import Queue as queue # py2
62 | string_types = basestring
63 |
64 |
65 | select_module = select
66 |
67 | # Globals
68 |
69 | CRLF = '\r\n'
70 | Debug = None # Backward compatibility
71 | IMAP4_PORT = 143
72 | IMAP4_SSL_PORT = 993
73 |
74 | IDLE_TIMEOUT_RESPONSE = '* IDLE TIMEOUT\r\n'
75 | IDLE_TIMEOUT = 60*29 # Don't stay in IDLE state longer
76 | READ_POLL_TIMEOUT = 30 # Without this timeout interrupted network connections can hang reader
77 | READ_SIZE = 32768 # Consume all available in socket
78 |
79 | DFLT_DEBUG_BUF_LVL = 3 # Level above which the logging output goes directly to stderr
80 |
81 | AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
82 |
83 | # Commands
84 |
85 | CMD_VAL_STATES = 0
86 | CMD_VAL_ASYNC = 1
87 | NONAUTH, AUTH, SELECTED, LOGOUT = 'NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'
88 |
89 | Commands = {
90 | # name valid states asynchronous
91 | 'APPEND': ((AUTH, SELECTED), False),
92 | 'AUTHENTICATE': ((NONAUTH,), False),
93 | 'CAPABILITY': ((NONAUTH, AUTH, SELECTED), True),
94 | 'CHECK': ((SELECTED,), True),
95 | 'CLOSE': ((SELECTED,), False),
96 | 'COMPRESS': ((AUTH,), False),
97 | 'COPY': ((SELECTED,), True),
98 | 'CREATE': ((AUTH, SELECTED), True),
99 | 'DELETE': ((AUTH, SELECTED), True),
100 | 'DELETEACL': ((AUTH, SELECTED), True),
101 | 'EXAMINE': ((AUTH, SELECTED), False),
102 | 'EXPUNGE': ((SELECTED,), True),
103 | 'FETCH': ((SELECTED,), True),
104 | 'GETACL': ((AUTH, SELECTED), True),
105 | 'GETANNOTATION':((AUTH, SELECTED), True),
106 | 'GETQUOTA': ((AUTH, SELECTED), True),
107 | 'GETQUOTAROOT': ((AUTH, SELECTED), True),
108 | 'ID': ((NONAUTH, AUTH, LOGOUT, SELECTED), True),
109 | 'IDLE': ((SELECTED,), False),
110 | 'LIST': ((AUTH, SELECTED), True),
111 | 'LOGIN': ((NONAUTH,), False),
112 | 'LOGOUT': ((NONAUTH, AUTH, LOGOUT, SELECTED), False),
113 | 'LSUB': ((AUTH, SELECTED), True),
114 | 'MYRIGHTS': ((AUTH, SELECTED), True),
115 | 'NAMESPACE': ((AUTH, SELECTED), True),
116 | 'NOOP': ((NONAUTH, AUTH, SELECTED), True),
117 | 'PARTIAL': ((SELECTED,), True),
118 | 'PROXYAUTH': ((AUTH,), False),
119 | 'RENAME': ((AUTH, SELECTED), True),
120 | 'SEARCH': ((SELECTED,), True),
121 | 'SELECT': ((AUTH, SELECTED), False),
122 | 'SETACL': ((AUTH, SELECTED), False),
123 | 'SETANNOTATION':((AUTH, SELECTED), True),
124 | 'SETQUOTA': ((AUTH, SELECTED), False),
125 | 'SORT': ((SELECTED,), True),
126 | 'STARTTLS': ((NONAUTH,), False),
127 | 'STATUS': ((AUTH, SELECTED), True),
128 | 'STORE': ((SELECTED,), True),
129 | 'SUBSCRIBE': ((AUTH, SELECTED), False),
130 | 'THREAD': ((SELECTED,), True),
131 | 'UID': ((SELECTED,), True),
132 | 'UNSUBSCRIBE': ((AUTH, SELECTED), False),
133 | }
134 |
135 | UID_direct = ('SEARCH', 'SORT', 'THREAD')
136 |
137 |
138 | def Int2AP(num):
139 |
140 | """string = Int2AP(num)
141 | Return 'num' converted to a string using characters from the set 'A'..'P'
142 | """
143 |
144 | val, a2p = [], 'ABCDEFGHIJKLMNOP'
145 | num = int(abs(num))
146 | while num:
147 | num, mod = divmod(num, 16)
148 | val.insert(0, a2p[mod])
149 | return ''.join(val)
150 |
151 |
152 |
153 | class Request(object):
154 |
155 | """Private class to represent a request awaiting response."""
156 |
157 | def __init__(self, parent, name=None, callback=None, cb_arg=None, cb_self=False):
158 | self.parent = parent
159 | self.name = name
160 | self.callback = callback # Function called to process result
161 | if not cb_self:
162 | self.callback_arg = cb_arg # Optional arg passed to "callback"
163 | else:
164 | self.callback_arg = (self, cb_arg) # Self reference required in callback arg
165 |
166 | self.tag = '%s%s' % (parent.tagpre, parent.tagnum)
167 | parent.tagnum += 1
168 |
169 | self.ready = threading.Event()
170 | self.response = None
171 | self.aborted = None
172 | self.data = None
173 |
174 |
175 | def abort(self, typ, val):
176 | self.aborted = (typ, val)
177 | self.deliver(None)
178 |
179 |
180 | def get_response(self, exc_fmt=None):
181 | self.callback = None
182 | if __debug__: self.parent._log(3, '%s:%s.ready.wait' % (self.name, self.tag))
183 | self.ready.wait()
184 |
185 | if self.aborted is not None:
186 | typ, val = self.aborted
187 | if exc_fmt is None:
188 | exc_fmt = '%s - %%s' % typ
189 | raise typ(exc_fmt % str(val))
190 |
191 | return self.response
192 |
193 |
194 | def deliver(self, response):
195 | if self.callback is not None:
196 | self.callback((response, self.callback_arg, self.aborted))
197 | return
198 |
199 | self.response = response
200 | self.ready.set()
201 | if __debug__: self.parent._log(3, '%s:%s.ready.set' % (self.name, self.tag))
202 |
203 |
204 |
205 |
206 | class IMAP4(object):
207 |
208 | """Threaded IMAP4 client class.
209 |
210 | Instantiate with:
211 | IMAP4(host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None)
212 |
213 | host - host's name (default: localhost);
214 | port - port number (default: standard IMAP4 port);
215 | debug - debug level (default: 0 - no debug);
216 | debug_file - debug stream (default: sys.stderr);
217 | identifier - thread identifier prefix (default: host);
218 | timeout - timeout in seconds when expecting a command response (default: no timeout),
219 | debug_buf_lvl - debug level at which buffering is turned off.
220 |
221 | All IMAP4rev1 commands are supported by methods of the same name.
222 |
223 | Each command returns a tuple: (type, [data, ...]) where 'type'
224 | is usually 'OK' or 'NO', and 'data' is either the text from the
225 | tagged response, or untagged results from command. Each 'data' is
226 | either a string, or a tuple. If a tuple, then the first part is the
227 | header of the response, and the second part contains the data (ie:
228 | 'literal' value).
229 |
230 | Errors raise the exception class .error("").
231 | IMAP4 server errors raise .abort(""), which is
232 | a sub-class of 'error'. Mailbox status changes from READ-WRITE to
233 | READ-ONLY raise the exception class .readonly(""),
234 | which is a sub-class of 'abort'.
235 |
236 | "error" exceptions imply a program error.
237 | "abort" exceptions imply the connection should be reset, and
238 | the command re-tried.
239 | "readonly" exceptions imply the command should be re-tried.
240 |
241 | All commands take two optional named arguments:
242 | 'callback' and 'cb_arg'
243 | If 'callback' is provided then the command is asynchronous, so after
244 | the command is queued for transmission, the call returns immediately
245 | with the tuple (None, None).
246 | The result will be posted by invoking "callback" with one arg, a tuple:
247 | callback((result, cb_arg, None))
248 | or, if there was a problem:
249 | callback((None, cb_arg, (exception class, reason)))
250 |
251 | Otherwise the command is synchronous (waits for result). But note
252 | that state-changing commands will both block until previous commands
253 | have completed, and block subsequent commands until they have finished.
254 |
255 | All (non-callback) arguments to commands are converted to strings,
256 | except for AUTHENTICATE, and the last argument to APPEND which is
257 | passed as an IMAP4 literal. If necessary (the string contains any
258 | non-printing characters or white-space and isn't enclosed with
259 | either parentheses or double or single quotes) each string is
260 | quoted. However, the 'password' argument to the LOGIN command is
261 | always quoted. If you want to avoid having an argument string
262 | quoted (eg: the 'flags' argument to STORE) then enclose the string
263 | in parentheses (eg: "(\Deleted)"). If you are using "sequence sets"
264 | containing the wildcard character '*', then enclose the argument
265 | in single quotes: the quotes will be removed and the resulting
266 | string passed unquoted. Note also that you can pass in an argument
267 | with a type that doesn't evaluate to 'string_types' (eg: 'bytearray')
268 | and it will be converted to a string without quoting.
269 |
270 | There is one instance variable, 'state', that is useful for tracking
271 | whether the client needs to login to the server. If it has the
272 | value "AUTH" after instantiating the class, then the connection
273 | is pre-authenticated (otherwise it will be "NONAUTH"). Selecting a
274 | mailbox changes the state to be "SELECTED", closing a mailbox changes
275 | back to "AUTH", and once the client has logged out, the state changes
276 | to "LOGOUT" and no further commands may be issued.
277 |
278 | Note: to use this module, you must read the RFCs pertaining to the
279 | IMAP4 protocol, as the semantics of the arguments to each IMAP4
280 | command are left to the invoker, not to mention the results. Also,
281 | most IMAP servers implement a sub-set of the commands available here.
282 |
283 | Note also that you must call logout() to shut down threads before
284 | discarding an instance.
285 | """
286 |
287 | class error(Exception): pass # Logical errors - debug required
288 | class abort(error): pass # Service errors - close and retry
289 | class readonly(abort): pass # Mailbox status changed to READ-ONLY
290 |
291 |
292 | continuation_cre = re.compile(r'\+( (?P.*))?')
293 | literal_cre = re.compile(r'.*{(?P\d+)}$')
294 | mapCRLF_cre = re.compile(r'\r\n|\r|\n')
295 | # Need to quote "atom-specials" :-
296 | # "(" / ")" / "{" / SP / 0x00 - 0x1f / 0x7f / "%" / "*" / DQUOTE / "\" / "]"
297 | # so match not the inverse set
298 | mustquote_cre = re.compile(r"[^!#$&'+,./0-9:;<=>?@A-Z\[^_`a-z|}~-]")
299 | response_code_cre = re.compile(r'\[(?P[A-Z-]+)( (?P[^\]]*))?\]')
300 | # sequence_set_cre = re.compile(r"^[0-9]+(:([0-9]+|\*))?(,[0-9]+(:([0-9]+|\*))?)*$")
301 | untagged_response_cre = re.compile(r'\* (?P[A-Z-]+)( (?P.*))?')
302 | untagged_status_cre = re.compile(r'\* (?P\d+) (?P[A-Z-]+)( (?P.*))?')
303 |
304 |
305 | def __init__(self, host=None, port=None, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None):
306 |
307 | self.state = NONAUTH # IMAP4 protocol state
308 | self.literal = None # A literal argument to a command
309 | self.tagged_commands = {} # Tagged commands awaiting response
310 | self.untagged_responses = [] # [[typ: [data, ...]], ...]
311 | self.mailbox = None # Current mailbox selected
312 | self.is_readonly = False # READ-ONLY desired state
313 | self.idle_rqb = None # Server IDLE Request - see _IdleCont
314 | self.idle_timeout = None # Must prod server occasionally
315 |
316 | self._expecting_data = False # Expecting message data
317 | self._expecting_data_len = 0 # How many characters we expect
318 | self._accumulated_data = [] # Message data accumulated so far
319 | self._literal_expected = None # Message data descriptor
320 |
321 | self.compressor = None # COMPRESS/DEFLATE if not None
322 | self.decompressor = None
323 |
324 | # Create unique tag for this session,
325 | # and compile tagged response matcher.
326 |
327 | self.tagnum = 0
328 | self.tagpre = Int2AP(random.randint(4096, 65535))
329 | self.tagre = re.compile(r'(?P'
330 | + self.tagpre
331 | + r'\d+) (?P[A-Z]+) (?P.*)')
332 |
333 | if __debug__: self._init_debug(debug, debug_file, debug_buf_lvl)
334 |
335 | self.resp_timeout = timeout # Timeout waiting for command response
336 |
337 | if timeout is not None and timeout < READ_POLL_TIMEOUT:
338 | self.read_poll_timeout = timeout
339 | else:
340 | self.read_poll_timeout = READ_POLL_TIMEOUT
341 | self.read_size = READ_SIZE
342 |
343 | # Open socket to server.
344 |
345 | self.open(host, port)
346 |
347 | if __debug__:
348 | if debug:
349 | self._mesg('connected to %s on port %s' % (self.host, self.port))
350 |
351 | # Threading
352 |
353 | if identifier is not None:
354 | self.identifier = identifier
355 | else:
356 | self.identifier = self.host
357 | if self.identifier:
358 | self.identifier += ' '
359 |
360 | self.Terminate = self.TerminateReader = False
361 |
362 | self.state_change_free = threading.Event()
363 | self.state_change_pending = threading.Lock()
364 | self.commands_lock = threading.Lock()
365 | self.idle_lock = threading.Lock()
366 |
367 | self.ouq = queue.Queue(10)
368 | self.inq = queue.Queue()
369 |
370 | self.wrth = threading.Thread(target=self._writer)
371 | self.wrth.setDaemon(True)
372 | self.wrth.start()
373 | self.rdth = threading.Thread(target=self._reader)
374 | self.rdth.setDaemon(True)
375 | self.rdth.start()
376 | self.inth = threading.Thread(target=self._handler)
377 | self.inth.setDaemon(True)
378 | self.inth.start()
379 |
380 | # Get server welcome message,
381 | # request and store CAPABILITY response.
382 |
383 | try:
384 | self.welcome = self._request_push(tag='continuation').get_response('IMAP4 protocol error: %s')[1]
385 |
386 | if self._get_untagged_response('PREAUTH'):
387 | self.state = AUTH
388 | if __debug__: self._log(1, 'state => AUTH')
389 | elif self._get_untagged_response('OK'):
390 | if __debug__: self._log(1, 'state => NONAUTH')
391 | else:
392 | raise self.error('unrecognised server welcome message: %s' % repr(self.welcome))
393 |
394 | typ, dat = self.capability()
395 | if dat == [None]:
396 | raise self.error('no CAPABILITY response from server')
397 | self.capabilities = tuple(dat[-1].upper().split())
398 | if __debug__: self._log(1, 'CAPABILITY: %r' % (self.capabilities,))
399 |
400 | for version in AllowedVersions:
401 | if not version in self.capabilities:
402 | continue
403 | self.PROTOCOL_VERSION = version
404 | break
405 | else:
406 | raise self.error('server not IMAP4 compliant')
407 | except:
408 | self._close_threads()
409 | raise
410 |
411 |
412 | def __getattr__(self, attr):
413 | # Allow UPPERCASE variants of IMAP4 command methods.
414 | if attr in Commands:
415 | return getattr(self, attr.lower())
416 | raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
417 |
418 |
419 |
420 | # Overridable methods
421 |
422 |
423 | def open(self, host=None, port=None):
424 | """open(host=None, port=None)
425 | Setup connection to remote server on "host:port"
426 | (default: localhost:standard IMAP4 port).
427 | This connection will be used by the routines:
428 | read, send, shutdown, socket."""
429 |
430 | self.host = self._choose_nonull_or_dflt('', host)
431 | self.port = self._choose_nonull_or_dflt(IMAP4_PORT, port)
432 | self.sock = self.open_socket()
433 | self.read_fd = self.sock.fileno()
434 |
435 |
436 | def open_socket(self):
437 | """open_socket()
438 | Open socket choosing first address family available."""
439 |
440 | msg = (-1, 'could not open socket')
441 | for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
442 | af, socktype, proto, canonname, sa = res
443 | try:
444 | s = socket.socket(af, socktype, proto)
445 | except socket.error as msg:
446 | continue
447 | try:
448 | for i in (0, 1):
449 | try:
450 | s.connect(sa)
451 | break
452 | except socket.error as msg:
453 | if len(msg.args) < 2 or msg.args[0] != errno.EINTR:
454 | raise
455 | else:
456 | raise socket.error(msg)
457 | except socket.error as msg:
458 | s.close()
459 | continue
460 | break
461 | else:
462 | raise socket.error(msg)
463 |
464 | return s
465 |
466 |
467 | def ssl_wrap_socket(self):
468 |
469 | # Allow sending of keep-alive messages - seems to prevent some servers
470 | # from closing SSL, leading to deadlocks.
471 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
472 |
473 | try:
474 | import ssl
475 | if self.ca_certs is not None:
476 | cert_reqs = ssl.CERT_REQUIRED
477 | else:
478 | cert_reqs = ssl.CERT_NONE
479 |
480 | if self.ssl_version == "tls1":
481 | ssl_version = ssl.PROTOCOL_TLSv1
482 | elif self.ssl_version == "ssl2":
483 | ssl_version = ssl.PROTOCOL_SSLv2
484 | elif self.ssl_version == "ssl3":
485 | ssl_version = ssl.PROTOCOL_SSLv3
486 | elif self.ssl_version == "ssl23" or self.ssl_version is None:
487 | ssl_version = ssl.PROTOCOL_SSLv23
488 | else:
489 | raise socket.sslerror("Invalid SSL version requested: %s", self.ssl_version)
490 |
491 | self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, ca_certs=self.ca_certs, cert_reqs=cert_reqs, ssl_version=ssl_version)
492 | ssl_exc = ssl.SSLError
493 | self.read_fd = self.sock.fileno()
494 | except ImportError:
495 | # No ssl module, and socket.ssl has no fileno(), and does not allow certificate verification
496 | raise socket.sslerror("imaplib2 SSL mode does not work without ssl module")
497 |
498 | if self.cert_verify_cb is not None:
499 | cert_err = self.cert_verify_cb(self.sock.getpeercert(), self.host)
500 | if cert_err:
501 | raise ssl_exc(cert_err)
502 |
503 |
504 |
505 | def start_compressing(self):
506 | """start_compressing()
507 | Enable deflate compression on the socket (RFC 4978)."""
508 |
509 | # rfc 1951 - pure DEFLATE, so use -15 for both windows
510 | self.decompressor = zlib.decompressobj(-15)
511 | self.compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15)
512 |
513 |
514 | def read(self, size):
515 | """data = read(size)
516 | Read at most 'size' bytes from remote."""
517 |
518 | if self.decompressor is None:
519 | return self.sock.recv(size)
520 |
521 | if self.decompressor.unconsumed_tail:
522 | data = self.decompressor.unconsumed_tail
523 | else:
524 | data = self.sock.recv(READ_SIZE)
525 |
526 | return self.decompressor.decompress(data, size)
527 |
528 |
529 | def send(self, data):
530 | """send(data)
531 | Send 'data' to remote."""
532 |
533 | if self.compressor is not None:
534 | data = self.compressor.compress(data)
535 | data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
536 |
537 | if bytes != str:
538 | self.sock.sendall(bytes(data, 'utf8'))
539 | else:
540 | self.sock.sendall(data)
541 |
542 |
543 | def shutdown(self):
544 | """shutdown()
545 | Close I/O established in "open"."""
546 |
547 | self.sock.close()
548 |
549 |
550 | def socket(self):
551 | """socket = socket()
552 | Return socket instance used to connect to IMAP4 server."""
553 |
554 | return self.sock
555 |
556 |
557 |
558 | # Utility methods
559 |
560 |
561 | def enable_compression(self):
562 | """enable_compression()
563 | Ask the server to start compressing the connection.
564 | Should be called from user of this class after instantiation, as in:
565 | if 'COMPRESS=DEFLATE' in imapobj.capabilities:
566 | imapobj.enable_compression()"""
567 |
568 | try:
569 | typ, dat = self._simple_command('COMPRESS', 'DEFLATE')
570 | if typ == 'OK':
571 | self.start_compressing()
572 | if __debug__: self._log(1, 'Enabled COMPRESS=DEFLATE')
573 | finally:
574 | self._release_state_change()
575 |
576 |
577 | def pop_untagged_responses(self):
578 | """ for typ,data in pop_untagged_responses(): pass
579 | Generator for any remaining untagged responses.
580 | Returns and removes untagged responses in order of reception.
581 | Use at your own risk!"""
582 |
583 | while self.untagged_responses:
584 | self.commands_lock.acquire()
585 | try:
586 | yield self.untagged_responses.pop(0)
587 | finally:
588 | self.commands_lock.release()
589 |
590 |
591 | def recent(self, **kw):
592 | """(typ, [data]) = recent()
593 | Return 'RECENT' responses if any exist,
594 | else prompt server for an update using the 'NOOP' command.
595 | 'data' is None if no new messages,
596 | else list of RECENT responses, most recent last."""
597 |
598 | name = 'RECENT'
599 | typ, dat = self._untagged_response(None, [None], name)
600 | if dat != [None]:
601 | return self._deliver_dat(typ, dat, kw)
602 | kw['untagged_response'] = name
603 | return self.noop(**kw) # Prod server for response
604 |
605 |
606 | def response(self, code, **kw):
607 | """(code, [data]) = response(code)
608 | Return data for response 'code' if received, or None.
609 | Old value for response 'code' is cleared."""
610 |
611 | typ, dat = self._untagged_response(code, [None], code.upper())
612 | return self._deliver_dat(typ, dat, kw)
613 |
614 |
615 |
616 |
617 | # IMAP4 commands
618 |
619 |
620 | def append(self, mailbox, flags, date_time, message, **kw):
621 | """(typ, [data]) = append(mailbox, flags, date_time, message)
622 | Append message to named mailbox.
623 | All args except `message' can be None."""
624 |
625 | name = 'APPEND'
626 | if not mailbox:
627 | mailbox = 'INBOX'
628 | if flags:
629 | if (flags[0],flags[-1]) != ('(',')'):
630 | flags = '(%s)' % flags
631 | else:
632 | flags = None
633 | if date_time:
634 | date_time = Time2Internaldate(date_time)
635 | else:
636 | date_time = None
637 | self.literal = self.mapCRLF_cre.sub(CRLF, message)
638 | try:
639 | return self._simple_command(name, mailbox, flags, date_time, **kw)
640 | finally:
641 | self._release_state_change()
642 |
643 |
644 | def authenticate(self, mechanism, authobject, **kw):
645 | """(typ, [data]) = authenticate(mechanism, authobject)
646 | Authenticate command - requires response processing.
647 |
648 | 'mechanism' specifies which authentication mechanism is to
649 | be used - it must appear in .capabilities in the
650 | form AUTH=.
651 |
652 | 'authobject' must be a callable object:
653 |
654 | data = authobject(response)
655 |
656 | It will be called to process server continuation responses.
657 | It should return data that will be encoded and sent to server.
658 | It should return None if the client abort response '*' should
659 | be sent instead."""
660 |
661 | self.literal = _Authenticator(authobject).process
662 | try:
663 | typ, dat = self._simple_command('AUTHENTICATE', mechanism.upper())
664 | if typ != 'OK':
665 | self._deliver_exc(self.error, dat[-1], kw)
666 | self.state = AUTH
667 | if __debug__: self._log(1, 'state => AUTH')
668 | finally:
669 | self._release_state_change()
670 | return self._deliver_dat(typ, dat, kw)
671 |
672 |
673 | def capability(self, **kw):
674 | """(typ, [data]) = capability()
675 | Fetch capabilities list from server."""
676 |
677 | name = 'CAPABILITY'
678 | kw['untagged_response'] = name
679 | return self._simple_command(name, **kw)
680 |
681 |
682 | def check(self, **kw):
683 | """(typ, [data]) = check()
684 | Checkpoint mailbox on server."""
685 |
686 | return self._simple_command('CHECK', **kw)
687 |
688 |
689 | def close(self, **kw):
690 | """(typ, [data]) = close()
691 | Close currently selected mailbox.
692 |
693 | Deleted messages are removed from writable mailbox.
694 | This is the recommended command before 'LOGOUT'."""
695 |
696 | if self.state != 'SELECTED':
697 | raise self.error('No mailbox selected.')
698 | try:
699 | typ, dat = self._simple_command('CLOSE')
700 | finally:
701 | self.state = AUTH
702 | if __debug__: self._log(1, 'state => AUTH')
703 | self._release_state_change()
704 | return self._deliver_dat(typ, dat, kw)
705 |
706 |
707 | def copy(self, message_set, new_mailbox, **kw):
708 | """(typ, [data]) = copy(message_set, new_mailbox)
709 | Copy 'message_set' messages onto end of 'new_mailbox'."""
710 |
711 | return self._simple_command('COPY', message_set, new_mailbox, **kw)
712 |
713 |
714 | def create(self, mailbox, **kw):
715 | """(typ, [data]) = create(mailbox)
716 | Create new mailbox."""
717 |
718 | return self._simple_command('CREATE', mailbox, **kw)
719 |
720 |
721 | def delete(self, mailbox, **kw):
722 | """(typ, [data]) = delete(mailbox)
723 | Delete old mailbox."""
724 |
725 | return self._simple_command('DELETE', mailbox, **kw)
726 |
727 |
728 | def deleteacl(self, mailbox, who, **kw):
729 | """(typ, [data]) = deleteacl(mailbox, who)
730 | Delete the ACLs (remove any rights) set for who on mailbox."""
731 |
732 | return self._simple_command('DELETEACL', mailbox, who, **kw)
733 |
734 |
735 | def examine(self, mailbox='INBOX', **kw):
736 | """(typ, [data]) = examine(mailbox='INBOX')
737 | Select a mailbox for READ-ONLY access. (Flushes all untagged responses.)
738 | 'data' is count of messages in mailbox ('EXISTS' response).
739 | Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
740 | other responses should be obtained via "response('FLAGS')" etc."""
741 |
742 | return self.select(mailbox=mailbox, readonly=True, **kw)
743 |
744 |
745 | def expunge(self, **kw):
746 | """(typ, [data]) = expunge()
747 | Permanently remove deleted items from selected mailbox.
748 | Generates 'EXPUNGE' response for each deleted message.
749 | 'data' is list of 'EXPUNGE'd message numbers in order received."""
750 |
751 | name = 'EXPUNGE'
752 | kw['untagged_response'] = name
753 | return self._simple_command(name, **kw)
754 |
755 |
756 | def fetch(self, message_set, message_parts, **kw):
757 | """(typ, [data, ...]) = fetch(message_set, message_parts)
758 | Fetch (parts of) messages.
759 | 'message_parts' should be a string of selected parts
760 | enclosed in parentheses, eg: "(UID BODY[TEXT])".
761 | 'data' are tuples of message part envelope and data,
762 | followed by a string containing the trailer."""
763 |
764 | name = 'FETCH'
765 | kw['untagged_response'] = name
766 | return self._simple_command(name, message_set, message_parts, **kw)
767 |
768 |
769 | def getacl(self, mailbox, **kw):
770 | """(typ, [data]) = getacl(mailbox)
771 | Get the ACLs for a mailbox."""
772 |
773 | kw['untagged_response'] = 'ACL'
774 | return self._simple_command('GETACL', mailbox, **kw)
775 |
776 |
777 | def getannotation(self, mailbox, entry, attribute, **kw):
778 | """(typ, [data]) = getannotation(mailbox, entry, attribute)
779 | Retrieve ANNOTATIONs."""
780 |
781 | kw['untagged_response'] = 'ANNOTATION'
782 | return self._simple_command('GETANNOTATION', mailbox, entry, attribute, **kw)
783 |
784 |
785 | def getquota(self, root, **kw):
786 | """(typ, [data]) = getquota(root)
787 | Get the quota root's resource usage and limits.
788 | (Part of the IMAP4 QUOTA extension defined in rfc2087.)"""
789 |
790 | kw['untagged_response'] = 'QUOTA'
791 | return self._simple_command('GETQUOTA', root, **kw)
792 |
793 |
794 | def getquotaroot(self, mailbox, **kw):
795 | # Hmmm, this is non-std! Left for backwards-compatibility, sigh.
796 | # NB: usage should have been defined as:
797 | # (typ, [QUOTAROOT responses...]) = getquotaroot(mailbox)
798 | # (typ, [QUOTA responses...]) = response('QUOTA')
799 | """(typ, [[QUOTAROOT responses...], [QUOTA responses...]]) = getquotaroot(mailbox)
800 | Get the list of quota roots for the named mailbox."""
801 |
802 | typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
803 | typ, quota = self._untagged_response(typ, dat, 'QUOTA')
804 | typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
805 | return self._deliver_dat(typ, [quotaroot, quota], kw)
806 |
807 |
808 | def id(self, *kv_pairs, **kw):
809 | """(typ, [data]) = .id(kv_pairs)
810 | 'kv_pairs' is a possibly empty list of keys and values.
811 | 'data' is a list of ID key value pairs or NIL.
812 | NB: a single argument is assumed to be correctly formatted and is passed through unchanged
813 | (for backward compatibility with earlier version).
814 | Exchange information for problem analysis and determination.
815 | The ID extension is defined in RFC 2971. """
816 |
817 | name = 'ID'
818 | kw['untagged_response'] = name
819 |
820 | if not kv_pairs:
821 | data = 'NIL'
822 | elif len(kv_pairs) == 1:
823 | data = kv_pairs[0] # Assume invoker passing correctly formatted string (back-compat)
824 | else:
825 | data = '(%s)' % ' '.join([(arg and self._quote(arg) or 'NIL') for arg in kv_pairs])
826 |
827 | return self._simple_command(name, data, **kw)
828 |
829 |
830 | def idle(self, timeout=None, **kw):
831 | """"(typ, [data]) = idle(timeout=None)
832 | Put server into IDLE mode until server notifies some change,
833 | or 'timeout' (secs) occurs (default: 29 minutes),
834 | or another IMAP4 command is scheduled."""
835 |
836 | name = 'IDLE'
837 | self.literal = _IdleCont(self, timeout).process
838 | try:
839 | return self._simple_command(name, **kw)
840 | finally:
841 | self._release_state_change()
842 |
843 |
844 | def list(self, directory='""', pattern='*', **kw):
845 | """(typ, [data]) = list(directory='""', pattern='*')
846 | List mailbox names in directory matching pattern.
847 | 'data' is list of LIST responses.
848 |
849 | NB: for 'pattern':
850 | % matches all except separator ( so LIST "" "%" returns names at root)
851 | * matches all (so LIST "" "*" returns whole directory tree from root)"""
852 |
853 | name = 'LIST'
854 | kw['untagged_response'] = name
855 | return self._simple_command(name, directory, pattern, **kw)
856 |
857 |
858 | def login(self, user, password, **kw):
859 | """(typ, [data]) = login(user, password)
860 | Identify client using plaintext password.
861 | NB: 'password' will be quoted."""
862 |
863 | try:
864 | typ, dat = self._simple_command('LOGIN', user, self._quote(password))
865 | if typ != 'OK':
866 | self._deliver_exc(self.error, dat[-1], kw)
867 | self.state = AUTH
868 | if __debug__: self._log(1, 'state => AUTH')
869 | finally:
870 | self._release_state_change()
871 | return self._deliver_dat(typ, dat, kw)
872 |
873 |
874 | def login_cram_md5(self, user, password, **kw):
875 | """(typ, [data]) = login_cram_md5(user, password)
876 | Force use of CRAM-MD5 authentication."""
877 |
878 | self.user, self.password = user, password
879 | return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH, **kw)
880 |
881 |
882 | def _CRAM_MD5_AUTH(self, challenge):
883 | """Authobject to use with CRAM-MD5 authentication."""
884 | import hmac
885 | pwd = (self.password.encode('utf-8') if isinstance(self.password, str)
886 | else self.password)
887 | return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest()
888 |
889 |
890 | def logout(self, **kw):
891 | """(typ, [data]) = logout()
892 | Shutdown connection to server.
893 | Returns server 'BYE' response.
894 | NB: You must call this to shut down threads before discarding an instance."""
895 |
896 | self.state = LOGOUT
897 | if __debug__: self._log(1, 'state => LOGOUT')
898 |
899 | try:
900 | try:
901 | typ, dat = self._simple_command('LOGOUT')
902 | except:
903 | typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
904 | if __debug__: self._log(1, dat)
905 |
906 | self._close_threads()
907 | finally:
908 | self._release_state_change()
909 |
910 | if __debug__: self._log(1, 'connection closed')
911 |
912 | bye = self._get_untagged_response('BYE', leave=True)
913 | if bye:
914 | typ, dat = 'BYE', bye
915 | return self._deliver_dat(typ, dat, kw)
916 |
917 |
918 | def lsub(self, directory='""', pattern='*', **kw):
919 | """(typ, [data, ...]) = lsub(directory='""', pattern='*')
920 | List 'subscribed' mailbox names in directory matching pattern.
921 | 'data' are tuples of message part envelope and data."""
922 |
923 | name = 'LSUB'
924 | kw['untagged_response'] = name
925 | return self._simple_command(name, directory, pattern, **kw)
926 |
927 |
928 | def myrights(self, mailbox, **kw):
929 | """(typ, [data]) = myrights(mailbox)
930 | Show my ACLs for a mailbox (i.e. the rights that I have on mailbox)."""
931 |
932 | name = 'MYRIGHTS'
933 | kw['untagged_response'] = name
934 | return self._simple_command(name, mailbox, **kw)
935 |
936 |
937 | def namespace(self, **kw):
938 | """(typ, [data, ...]) = namespace()
939 | Returns IMAP namespaces ala rfc2342."""
940 |
941 | name = 'NAMESPACE'
942 | kw['untagged_response'] = name
943 | return self._simple_command(name, **kw)
944 |
945 |
946 | def noop(self, **kw):
947 | """(typ, [data]) = noop()
948 | Send NOOP command."""
949 |
950 | if __debug__: self._dump_ur(3)
951 | return self._simple_command('NOOP', **kw)
952 |
953 |
954 | def partial(self, message_num, message_part, start, length, **kw):
955 | """(typ, [data, ...]) = partial(message_num, message_part, start, length)
956 | Fetch truncated part of a message.
957 | 'data' is tuple of message part envelope and data.
958 | NB: obsolete."""
959 |
960 | name = 'PARTIAL'
961 | kw['untagged_response'] = 'FETCH'
962 | return self._simple_command(name, message_num, message_part, start, length, **kw)
963 |
964 |
965 | def proxyauth(self, user, **kw):
966 | """(typ, [data]) = proxyauth(user)
967 | Assume authentication as 'user'.
968 | (Allows an authorised administrator to proxy into any user's mailbox.)"""
969 |
970 | try:
971 | return self._simple_command('PROXYAUTH', user, **kw)
972 | finally:
973 | self._release_state_change()
974 |
975 |
976 | def rename(self, oldmailbox, newmailbox, **kw):
977 | """(typ, [data]) = rename(oldmailbox, newmailbox)
978 | Rename old mailbox name to new."""
979 |
980 | return self._simple_command('RENAME', oldmailbox, newmailbox, **kw)
981 |
982 |
983 | def search(self, charset, *criteria, **kw):
984 | """(typ, [data]) = search(charset, criterion, ...)
985 | Search mailbox for matching messages.
986 | 'data' is space separated list of matching message numbers."""
987 |
988 | name = 'SEARCH'
989 | kw['untagged_response'] = name
990 | if charset:
991 | return self._simple_command(name, 'CHARSET', charset, *criteria, **kw)
992 | return self._simple_command(name, *criteria, **kw)
993 |
994 |
995 | def select(self, mailbox='INBOX', readonly=False, **kw):
996 | """(typ, [data]) = select(mailbox='INBOX', readonly=False)
997 | Select a mailbox. (Flushes all untagged responses.)
998 | 'data' is count of messages in mailbox ('EXISTS' response).
999 | Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
1000 | other responses should be obtained via "response('FLAGS')" etc."""
1001 |
1002 | self.mailbox = mailbox
1003 |
1004 | self.is_readonly = bool(readonly)
1005 | if readonly:
1006 | name = 'EXAMINE'
1007 | else:
1008 | name = 'SELECT'
1009 | try:
1010 | rqb = self._command(name, mailbox)
1011 | typ, dat = rqb.get_response('command: %s => %%s' % rqb.name)
1012 | if typ != 'OK':
1013 | if self.state == SELECTED:
1014 | self.state = AUTH
1015 | if __debug__: self._log(1, 'state => AUTH')
1016 | if typ == 'BAD':
1017 | self._deliver_exc(self.error, '%s command error: %s %s. Data: %.100s' % (name, typ, dat, mailbox), kw)
1018 | return self._deliver_dat(typ, dat, kw)
1019 | self.state = SELECTED
1020 | if __debug__: self._log(1, 'state => SELECTED')
1021 | finally:
1022 | self._release_state_change()
1023 |
1024 | if self._get_untagged_response('READ-ONLY', leave=True) and not readonly:
1025 | if __debug__: self._dump_ur(1)
1026 | self._deliver_exc(self.readonly, '%s is not writable' % mailbox, kw)
1027 | typ, dat = self._untagged_response(typ, [None], 'EXISTS')
1028 | return self._deliver_dat(typ, dat, kw)
1029 |
1030 |
1031 | def setacl(self, mailbox, who, what, **kw):
1032 | """(typ, [data]) = setacl(mailbox, who, what)
1033 | Set a mailbox acl."""
1034 |
1035 | try:
1036 | return self._simple_command('SETACL', mailbox, who, what, **kw)
1037 | finally:
1038 | self._release_state_change()
1039 |
1040 |
1041 | def setannotation(self, *args, **kw):
1042 | """(typ, [data]) = setannotation(mailbox[, entry, attribute]+)
1043 | Set ANNOTATIONs."""
1044 |
1045 | kw['untagged_response'] = 'ANNOTATION'
1046 | return self._simple_command('SETANNOTATION', *args, **kw)
1047 |
1048 |
1049 | def setquota(self, root, limits, **kw):
1050 | """(typ, [data]) = setquota(root, limits)
1051 | Set the quota root's resource limits."""
1052 |
1053 | kw['untagged_response'] = 'QUOTA'
1054 | try:
1055 | return self._simple_command('SETQUOTA', root, limits, **kw)
1056 | finally:
1057 | self._release_state_change()
1058 |
1059 |
1060 | def sort(self, sort_criteria, charset, *search_criteria, **kw):
1061 | """(typ, [data]) = sort(sort_criteria, charset, search_criteria, ...)
1062 | IMAP4rev1 extension SORT command."""
1063 |
1064 | name = 'SORT'
1065 | if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
1066 | sort_criteria = '(%s)' % sort_criteria
1067 | kw['untagged_response'] = name
1068 | return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw)
1069 |
1070 |
1071 | def starttls(self, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", **kw):
1072 | """(typ, [data]) = starttls(keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23")
1073 | Start TLS negotiation as per RFC 2595."""
1074 |
1075 | name = 'STARTTLS'
1076 |
1077 | if name not in self.capabilities:
1078 | raise self.abort('TLS not supported by server')
1079 |
1080 | if hasattr(self, '_tls_established') and self._tls_established:
1081 | raise self.abort('TLS session already established')
1082 |
1083 | # Must now shutdown reader thread after next response, and restart after changing read_fd
1084 |
1085 | self.read_size = 1 # Don't consume TLS handshake
1086 | self.TerminateReader = True
1087 |
1088 | try:
1089 | typ, dat = self._simple_command(name)
1090 | finally:
1091 | self._release_state_change()
1092 | self.rdth.join()
1093 | self.TerminateReader = False
1094 | self.read_size = READ_SIZE
1095 |
1096 | if typ != 'OK':
1097 | # Restart reader thread and error
1098 | self.rdth = threading.Thread(target=self._reader)
1099 | self.rdth.setDaemon(True)
1100 | self.rdth.start()
1101 | raise self.error("Couldn't establish TLS session: %s" % dat)
1102 |
1103 | self.keyfile = keyfile
1104 | self.certfile = certfile
1105 | self.ca_certs = ca_certs
1106 | self.cert_verify_cb = cert_verify_cb
1107 | self.ssl_version = ssl_version
1108 |
1109 | try:
1110 | self.ssl_wrap_socket()
1111 | finally:
1112 | # Restart reader thread
1113 | self.rdth = threading.Thread(target=self._reader)
1114 | self.rdth.setDaemon(True)
1115 | self.rdth.start()
1116 |
1117 | typ, dat = self.capability()
1118 | if dat == [None]:
1119 | raise self.error('no CAPABILITY response from server')
1120 | self.capabilities = tuple(dat[-1].upper().split())
1121 |
1122 | self._tls_established = True
1123 |
1124 | typ, dat = self._untagged_response(typ, dat, name)
1125 | return self._deliver_dat(typ, dat, kw)
1126 |
1127 |
1128 | def status(self, mailbox, names, **kw):
1129 | """(typ, [data]) = status(mailbox, names)
1130 | Request named status conditions for mailbox."""
1131 |
1132 | name = 'STATUS'
1133 | kw['untagged_response'] = name
1134 | return self._simple_command(name, mailbox, names, **kw)
1135 |
1136 |
1137 | def store(self, message_set, command, flags, **kw):
1138 | """(typ, [data]) = store(message_set, command, flags)
1139 | Alters flag dispositions for messages in mailbox."""
1140 |
1141 | if (flags[0],flags[-1]) != ('(',')'):
1142 | flags = '(%s)' % flags # Avoid quoting the flags
1143 | kw['untagged_response'] = 'FETCH'
1144 | return self._simple_command('STORE', message_set, command, flags, **kw)
1145 |
1146 |
1147 | def subscribe(self, mailbox, **kw):
1148 | """(typ, [data]) = subscribe(mailbox)
1149 | Subscribe to new mailbox."""
1150 |
1151 | try:
1152 | return self._simple_command('SUBSCRIBE', mailbox, **kw)
1153 | finally:
1154 | self._release_state_change()
1155 |
1156 |
1157 | def thread(self, threading_algorithm, charset, *search_criteria, **kw):
1158 | """(type, [data]) = thread(threading_alogrithm, charset, search_criteria, ...)
1159 | IMAPrev1 extension THREAD command."""
1160 |
1161 | name = 'THREAD'
1162 | kw['untagged_response'] = name
1163 | return self._simple_command(name, threading_algorithm, charset, *search_criteria, **kw)
1164 |
1165 |
1166 | def uid(self, command, *args, **kw):
1167 | """(typ, [data]) = uid(command, arg, ...)
1168 | Execute "command arg ..." with messages identified by UID,
1169 | rather than message number.
1170 | Assumes 'command' is legal in current state.
1171 | Returns response appropriate to 'command'."""
1172 |
1173 | command = command.upper()
1174 | if command in UID_direct:
1175 | resp = command
1176 | else:
1177 | resp = 'FETCH'
1178 | kw['untagged_response'] = resp
1179 | return self._simple_command('UID', command, *args, **kw)
1180 |
1181 |
1182 | def unsubscribe(self, mailbox, **kw):
1183 | """(typ, [data]) = unsubscribe(mailbox)
1184 | Unsubscribe from old mailbox."""
1185 |
1186 | try:
1187 | return self._simple_command('UNSUBSCRIBE', mailbox, **kw)
1188 | finally:
1189 | self._release_state_change()
1190 |
1191 |
1192 | def xatom(self, name, *args, **kw):
1193 | """(typ, [data]) = xatom(name, arg, ...)
1194 | Allow simple extension commands notified by server in CAPABILITY response.
1195 | Assumes extension command 'name' is legal in current state.
1196 | Returns response appropriate to extension command 'name'."""
1197 |
1198 | name = name.upper()
1199 | if not name in Commands:
1200 | Commands[name] = ((self.state,), False)
1201 | try:
1202 | return self._simple_command(name, *args, **kw)
1203 | finally:
1204 | self._release_state_change()
1205 |
1206 |
1207 |
1208 | # Internal methods
1209 |
1210 |
1211 | def _append_untagged(self, typ, dat):
1212 |
1213 | # Append new 'dat' to end of last untagged response if same 'typ',
1214 | # else append new response.
1215 |
1216 | if dat is None: dat = ''
1217 |
1218 | self.commands_lock.acquire()
1219 |
1220 | if self.untagged_responses:
1221 | urn, urd = self.untagged_responses[-1]
1222 | if urn != typ:
1223 | urd = None
1224 | else:
1225 | urd = None
1226 |
1227 | if urd is None:
1228 | urd = []
1229 | self.untagged_responses.append([typ, urd])
1230 |
1231 | urd.append(dat)
1232 |
1233 | self.commands_lock.release()
1234 |
1235 | if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%.80s"]' % (typ, len(urd)-1, dat))
1236 |
1237 |
1238 | def _check_bye(self):
1239 |
1240 | bye = self._get_untagged_response('BYE', leave=True)
1241 | if bye:
1242 | raise self.abort(bye[-1])
1243 |
1244 |
1245 | def _checkquote(self, arg):
1246 |
1247 | # Must quote command args if "atom-specials" present,
1248 | # and not already quoted. NB: single quotes are removed.
1249 |
1250 | if not isinstance(arg, string_types):
1251 | return arg
1252 | if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
1253 | return arg
1254 | if len(arg) >= 2 and (arg[0],arg[-1]) in (("'","'"),):
1255 | return arg[1:-1]
1256 | if arg and self.mustquote_cre.search(arg) is None:
1257 | return arg
1258 | return self._quote(arg)
1259 |
1260 |
1261 | def _choose_nonull_or_dflt(self, dflt, *args):
1262 | if isinstance(dflt, string_types):
1263 | dflttyp = string_types # Allow any string type
1264 | else:
1265 | dflttyp = type(dflt)
1266 | for arg in args:
1267 | if arg is not None:
1268 | if isinstance(arg, dflttyp):
1269 | return arg
1270 | if __debug__: self._log(0, 'bad arg is %s, expecting %s' % (type(arg), dflttyp))
1271 | return dflt
1272 |
1273 |
1274 | def _command(self, name, *args, **kw):
1275 |
1276 | if Commands[name][CMD_VAL_ASYNC]:
1277 | cmdtyp = 'async'
1278 | else:
1279 | cmdtyp = 'sync'
1280 |
1281 | if __debug__: self._log(1, '[%s] %s %s' % (cmdtyp, name, args))
1282 |
1283 | if __debug__: self._log(3, 'state_change_pending.acquire')
1284 | self.state_change_pending.acquire()
1285 |
1286 | self._end_idle()
1287 |
1288 | if cmdtyp == 'async':
1289 | self.state_change_pending.release()
1290 | if __debug__: self._log(3, 'state_change_pending.release')
1291 | else:
1292 | # Need to wait for all async commands to complete
1293 | self._check_bye()
1294 | self.commands_lock.acquire()
1295 | if self.tagged_commands:
1296 | self.state_change_free.clear()
1297 | need_event = True
1298 | else:
1299 | need_event = False
1300 | self.commands_lock.release()
1301 | if need_event:
1302 | if __debug__: self._log(3, 'sync command %s waiting for empty commands Q' % name)
1303 | self.state_change_free.wait()
1304 | if __debug__: self._log(3, 'sync command %s proceeding' % name)
1305 |
1306 | if self.state not in Commands[name][CMD_VAL_STATES]:
1307 | self.literal = None
1308 | raise self.error('command %s illegal in state %s'
1309 | % (name, self.state))
1310 |
1311 | self._check_bye()
1312 |
1313 | if name in ('EXAMINE', 'SELECT'):
1314 | self.commands_lock.acquire()
1315 | self.untagged_responses = [] # Flush all untagged responses
1316 | self.commands_lock.release()
1317 | else:
1318 | for typ in ('OK', 'NO', 'BAD'):
1319 | while self._get_untagged_response(typ):
1320 | continue
1321 |
1322 | if not self.is_readonly and self._get_untagged_response('READ-ONLY', leave=True):
1323 | self.literal = None
1324 | raise self.readonly('mailbox status changed to READ-ONLY')
1325 |
1326 | if self.Terminate:
1327 | raise self.abort('connection closed')
1328 |
1329 | rqb = self._request_push(name=name, **kw)
1330 |
1331 | data = '%s %s' % (rqb.tag, name)
1332 | for arg in args:
1333 | if arg is None: continue
1334 | data = '%s %s' % (data, self._checkquote(arg))
1335 |
1336 | literal = self.literal
1337 | if literal is not None:
1338 | self.literal = None
1339 | if isinstance(literal, string_types):
1340 | literator = None
1341 | data = '%s {%s}' % (data, len(literal))
1342 | else:
1343 | literator = literal
1344 |
1345 | if __debug__: self._log(4, 'data=%s' % data)
1346 |
1347 | rqb.data = '%s%s' % (data, CRLF)
1348 |
1349 | if literal is None:
1350 | self.ouq.put(rqb)
1351 | return rqb
1352 |
1353 | # Must setup continuation expectancy *before* ouq.put
1354 | crqb = self._request_push(tag='continuation')
1355 |
1356 | self.ouq.put(rqb)
1357 |
1358 | while True:
1359 | # Wait for continuation response
1360 |
1361 | ok, data = crqb.get_response('command: %s => %%s' % name)
1362 | if __debug__: self._log(4, 'continuation => %s, %s' % (ok, data))
1363 |
1364 | # NO/BAD response?
1365 |
1366 | if not ok:
1367 | break
1368 |
1369 | # Send literal
1370 |
1371 | if literator is not None:
1372 | literal = literator(data, rqb)
1373 |
1374 | if literal is None:
1375 | break
1376 |
1377 | if literator is not None:
1378 | # Need new request for next continuation response
1379 | crqb = self._request_push(tag='continuation')
1380 |
1381 | if __debug__: self._log(4, 'write literal size %s' % len(literal))
1382 | crqb.data = '%s%s' % (literal, CRLF)
1383 | self.ouq.put(crqb)
1384 |
1385 | if literator is None:
1386 | break
1387 |
1388 | return rqb
1389 |
1390 |
1391 | def _command_complete(self, rqb, kw):
1392 |
1393 | # Called for non-callback commands
1394 |
1395 | self._check_bye()
1396 | typ, dat = rqb.get_response('command: %s => %%s' % rqb.name)
1397 | if typ == 'BAD':
1398 | if __debug__: self._print_log()
1399 | raise self.error('%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data))
1400 | if 'untagged_response' in kw:
1401 | return self._untagged_response(typ, dat, kw['untagged_response'])
1402 | return typ, dat
1403 |
1404 |
1405 | def _command_completer(self, cb_arg_list):
1406 |
1407 | # Called for callback commands
1408 | response, cb_arg, error = cb_arg_list
1409 | rqb, kw = cb_arg
1410 | rqb.callback = kw['callback']
1411 | rqb.callback_arg = kw.get('cb_arg')
1412 | if error is not None:
1413 | if __debug__: self._print_log()
1414 | typ, val = error
1415 | rqb.abort(typ, val)
1416 | return
1417 | bye = self._get_untagged_response('BYE', leave=True)
1418 | if bye:
1419 | rqb.abort(self.abort, bye[-1])
1420 | return
1421 | typ, dat = response
1422 | if typ == 'BAD':
1423 | if __debug__: self._print_log()
1424 | rqb.abort(self.error, '%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data))
1425 | return
1426 | if __debug__: self._log(4, '_command_completer(%s, %s, None) = %s' % (response, cb_arg, rqb.tag))
1427 | if 'untagged_response' in kw:
1428 | response = self._untagged_response(typ, dat, kw['untagged_response'])
1429 | rqb.deliver(response)
1430 |
1431 |
1432 | def _deliver_dat(self, typ, dat, kw):
1433 |
1434 | if 'callback' in kw:
1435 | kw['callback'](((typ, dat), kw.get('cb_arg'), None))
1436 | return typ, dat
1437 |
1438 |
1439 | def _deliver_exc(self, exc, dat, kw):
1440 |
1441 | if 'callback' in kw:
1442 | kw['callback']((None, kw.get('cb_arg'), (exc, dat)))
1443 | raise exc(dat)
1444 |
1445 |
1446 | def _end_idle(self):
1447 |
1448 | self.idle_lock.acquire()
1449 | irqb = self.idle_rqb
1450 | if irqb is None:
1451 | self.idle_lock.release()
1452 | return
1453 | self.idle_rqb = None
1454 | self.idle_timeout = None
1455 | self.idle_lock.release()
1456 | irqb.data = 'DONE%s' % CRLF
1457 | self.ouq.put(irqb)
1458 | if __debug__: self._log(2, 'server IDLE finished')
1459 |
1460 |
1461 | def _get_untagged_response(self, name, leave=False):
1462 |
1463 | self.commands_lock.acquire()
1464 |
1465 | for i, (typ, dat) in enumerate(self.untagged_responses):
1466 | if typ == name:
1467 | if not leave:
1468 | del self.untagged_responses[i]
1469 | self.commands_lock.release()
1470 | if __debug__: self._log(5, '_get_untagged_response(%s) => %.80s' % (name, dat))
1471 | return dat
1472 |
1473 | self.commands_lock.release()
1474 | return None
1475 |
1476 |
1477 | def _match(self, cre, s):
1478 |
1479 | # Run compiled regular expression 'cre' match method on 's'.
1480 | # Save result, return success.
1481 |
1482 | self.mo = cre.match(s)
1483 | return self.mo is not None
1484 |
1485 |
1486 | def _put_response(self, resp):
1487 |
1488 | if self._expecting_data:
1489 | rlen = len(resp)
1490 | dlen = min(self._expecting_data_len, rlen)
1491 | if __debug__: self._log(5, '_put_response expecting data len %s, got %s' % (self._expecting_data_len, rlen))
1492 | self._expecting_data_len -= dlen
1493 | self._expecting_data = (self._expecting_data_len != 0)
1494 | if rlen <= dlen:
1495 | self._accumulated_data.append(resp)
1496 | return
1497 | self._accumulated_data.append(resp[:dlen])
1498 | resp = resp[dlen:]
1499 |
1500 | if self._accumulated_data:
1501 | typ, dat = self._literal_expected
1502 | self._append_untagged(typ, (dat, ''.join(self._accumulated_data)))
1503 | self._accumulated_data = []
1504 |
1505 | # Protocol mandates all lines terminated by CRLF
1506 | resp = resp[:-2]
1507 | if __debug__: self._log(5, '_put_response(%s)' % resp)
1508 |
1509 | if 'continuation' in self.tagged_commands:
1510 | continuation_expected = True
1511 | else:
1512 | continuation_expected = False
1513 |
1514 | if self._literal_expected is not None:
1515 | dat = resp
1516 | if self._match(self.literal_cre, dat):
1517 | self._literal_expected[1] = dat
1518 | self._expecting_data = True
1519 | self._expecting_data_len = int(self.mo.group('size'))
1520 | if __debug__: self._log(4, 'expecting literal size %s' % self._expecting_data_len)
1521 | return
1522 | typ = self._literal_expected[0]
1523 | self._literal_expected = None
1524 | if dat:
1525 | self._append_untagged(typ, dat) # Tail
1526 | if __debug__: self._log(4, 'literal completed')
1527 | else:
1528 | # Command completion response?
1529 | if self._match(self.tagre, resp):
1530 | tag = self.mo.group('tag')
1531 | typ = self.mo.group('type')
1532 | dat = self.mo.group('data')
1533 | if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat):
1534 | self._append_untagged(self.mo.group('type'), self.mo.group('data'))
1535 | if not tag in self.tagged_commands:
1536 | if __debug__: self._log(1, 'unexpected tagged response: %s' % resp)
1537 | else:
1538 | self._request_pop(tag, (typ, [dat]))
1539 | else:
1540 | dat2 = None
1541 |
1542 | # '*' (untagged) responses?
1543 |
1544 | if not self._match(self.untagged_response_cre, resp):
1545 | if self._match(self.untagged_status_cre, resp):
1546 | dat2 = self.mo.group('data2')
1547 |
1548 | if self.mo is None:
1549 | # Only other possibility is '+' (continuation) response...
1550 |
1551 | if self._match(self.continuation_cre, resp):
1552 | if not continuation_expected:
1553 | if __debug__: self._log(1, "unexpected continuation response: '%s'" % resp)
1554 | return
1555 | self._request_pop('continuation', (True, self.mo.group('data')))
1556 | return
1557 |
1558 | if __debug__: self._log(1, "unexpected response: '%s'" % resp)
1559 | return
1560 |
1561 | typ = self.mo.group('type')
1562 | dat = self.mo.group('data')
1563 | if dat is None: dat = '' # Null untagged response
1564 | if dat2: dat = dat + ' ' + dat2
1565 |
1566 | # Is there a literal to come?
1567 |
1568 | if self._match(self.literal_cre, dat):
1569 | self._expecting_data = True
1570 | self._expecting_data_len = int(self.mo.group('size'))
1571 | if __debug__: self._log(4, 'read literal size %s' % self._expecting_data_len)
1572 | self._literal_expected = [typ, dat]
1573 | return
1574 |
1575 | self._append_untagged(typ, dat)
1576 | if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat):
1577 | self._append_untagged(self.mo.group('type'), self.mo.group('data'))
1578 |
1579 | if typ != 'OK': # NO, BYE, IDLE
1580 | self._end_idle()
1581 |
1582 | # Command waiting for aborted continuation response?
1583 |
1584 | if continuation_expected:
1585 | self._request_pop('continuation', (False, resp))
1586 |
1587 | # Bad news?
1588 |
1589 | if typ in ('NO', 'BAD', 'BYE'):
1590 | if typ == 'BYE':
1591 | self.Terminate = True
1592 | if __debug__: self._log(1, '%s response: %s' % (typ, dat))
1593 |
1594 |
1595 | def _quote(self, arg):
1596 |
1597 | return '"%s"' % arg.replace('\\', '\\\\').replace('"', '\\"')
1598 |
1599 |
1600 | def _release_state_change(self):
1601 |
1602 | if self.state_change_pending.locked():
1603 | self.state_change_pending.release()
1604 | if __debug__: self._log(3, 'state_change_pending.release')
1605 |
1606 |
1607 | def _request_pop(self, name, data):
1608 |
1609 | self.commands_lock.acquire()
1610 | rqb = self.tagged_commands.pop(name)
1611 | if not self.tagged_commands:
1612 | need_event = True
1613 | else:
1614 | need_event = False
1615 | self.commands_lock.release()
1616 |
1617 | if __debug__: self._log(4, '_request_pop(%s, %s) [%d] = %s' % (name, data, len(self.tagged_commands), rqb.tag))
1618 | rqb.deliver(data)
1619 |
1620 | if need_event:
1621 | if __debug__: self._log(3, 'state_change_free.set')
1622 | self.state_change_free.set()
1623 |
1624 |
1625 | def _request_push(self, tag=None, name=None, **kw):
1626 |
1627 | self.commands_lock.acquire()
1628 | rqb = Request(self, name=name, **kw)
1629 | if tag is None:
1630 | tag = rqb.tag
1631 | self.tagged_commands[tag] = rqb
1632 | self.commands_lock.release()
1633 | if __debug__: self._log(4, '_request_push(%s, %s, %s) = %s' % (tag, name, repr(kw), rqb.tag))
1634 | return rqb
1635 |
1636 |
1637 | def _simple_command(self, name, *args, **kw):
1638 |
1639 | if 'callback' in kw:
1640 | # Note: old calling sequence for back-compat with python <2.6
1641 | self._command(name, callback=self._command_completer, cb_arg=kw, cb_self=True, *args)
1642 | return (None, None)
1643 | return self._command_complete(self._command(name, *args), kw)
1644 |
1645 |
1646 | def _untagged_response(self, typ, dat, name):
1647 |
1648 | if typ == 'NO':
1649 | return typ, dat
1650 | data = self._get_untagged_response(name)
1651 | if not data:
1652 | return typ, [None]
1653 | while True:
1654 | dat = self._get_untagged_response(name)
1655 | if not dat:
1656 | break
1657 | data += dat
1658 | if __debug__: self._log(4, '_untagged_response(%s, ?, %s) => %.80s' % (typ, name, data))
1659 | return typ, data
1660 |
1661 |
1662 |
1663 | # Threads
1664 |
1665 |
1666 | def _close_threads(self):
1667 |
1668 | if __debug__: self._log(1, '_close_threads')
1669 |
1670 | self.ouq.put(None)
1671 | self.wrth.join()
1672 |
1673 | if __debug__: self._log(1, 'call shutdown')
1674 |
1675 | self.shutdown()
1676 |
1677 | self.rdth.join()
1678 | self.inth.join()
1679 |
1680 |
1681 | def _handler(self):
1682 |
1683 | resp_timeout = self.resp_timeout
1684 |
1685 | threading.currentThread().setName(self.identifier + 'handler')
1686 |
1687 | time.sleep(0.1) # Don't start handling before main thread ready
1688 |
1689 | if __debug__: self._log(1, 'starting')
1690 |
1691 | typ, val = self.abort, 'connection terminated'
1692 |
1693 | while not self.Terminate:
1694 |
1695 | self.idle_lock.acquire()
1696 | if self.idle_timeout is not None:
1697 | timeout = self.idle_timeout - time.time()
1698 | if timeout <= 0:
1699 | timeout = 1
1700 | if __debug__:
1701 | if self.idle_rqb is not None:
1702 | self._log(5, 'server IDLING, timeout=%.2f' % timeout)
1703 | else:
1704 | timeout = resp_timeout
1705 | self.idle_lock.release()
1706 |
1707 | try:
1708 | line = self.inq.get(True, timeout)
1709 | except queue.Empty:
1710 | if self.idle_rqb is None:
1711 | if resp_timeout is not None and self.tagged_commands:
1712 | if __debug__: self._log(1, 'response timeout')
1713 | typ, val = self.abort, 'no response after %s secs' % resp_timeout
1714 | break
1715 | continue
1716 | if self.idle_timeout > time.time():
1717 | continue
1718 | if __debug__: self._log(2, 'server IDLE timedout')
1719 | line = IDLE_TIMEOUT_RESPONSE
1720 |
1721 | if line is None:
1722 | if __debug__: self._log(1, 'inq None - terminating')
1723 | break
1724 |
1725 | if not isinstance(line, string_types):
1726 | typ, val = line
1727 | break
1728 |
1729 | try:
1730 | self._put_response(line)
1731 | except:
1732 | typ, val = self.error, 'program error: %s - %s' % sys.exc_info()[:2]
1733 | break
1734 |
1735 | self.Terminate = True
1736 |
1737 | if __debug__: self._log(1, 'terminating: %s' % repr(val))
1738 |
1739 | while not self.ouq.empty():
1740 | try:
1741 | qel = self.ouq.get_nowait()
1742 | if qel is not None:
1743 | qel.abort(typ, val)
1744 | except queue.Empty:
1745 | break
1746 | self.ouq.put(None)
1747 |
1748 | self.commands_lock.acquire()
1749 | for name in list(self.tagged_commands.keys()):
1750 | rqb = self.tagged_commands.pop(name)
1751 | rqb.abort(typ, val)
1752 | self.state_change_free.set()
1753 | self.commands_lock.release()
1754 | if __debug__: self._log(3, 'state_change_free.set')
1755 |
1756 | if __debug__: self._log(1, 'finished')
1757 |
1758 |
1759 | if hasattr(select_module, "poll"):
1760 |
1761 | def _reader(self):
1762 |
1763 | threading.currentThread().setName(self.identifier + 'reader')
1764 |
1765 | if __debug__: self._log(1, 'starting using poll')
1766 |
1767 | def poll_error(state):
1768 | PollErrors = {
1769 | select.POLLERR: 'Error',
1770 | select.POLLHUP: 'Hang up',
1771 | select.POLLNVAL: 'Invalid request: descriptor not open',
1772 | }
1773 | return ' '.join([PollErrors[s] for s in PollErrors.keys() if (s & state)])
1774 |
1775 | line_part = ''
1776 |
1777 | poll = select.poll()
1778 |
1779 | poll.register(self.read_fd, select.POLLIN)
1780 |
1781 | rxzero = 0
1782 | terminate = False
1783 | read_poll_timeout = self.read_poll_timeout * 1000 # poll() timeout is in millisecs
1784 |
1785 | while not (terminate or self.Terminate):
1786 | if self.state == LOGOUT:
1787 | timeout = 10
1788 | else:
1789 | timeout = read_poll_timeout
1790 | try:
1791 | r = poll.poll(timeout)
1792 | if __debug__: self._log(5, 'poll => %s' % repr(r))
1793 | if not r:
1794 | continue # Timeout
1795 |
1796 | fd,state = r[0]
1797 |
1798 | if state & select.POLLIN:
1799 | data = self.read(self.read_size) # Drain ssl buffer if present
1800 | start = 0
1801 | dlen = len(data)
1802 | if __debug__: self._log(5, 'rcvd %s' % dlen)
1803 | if dlen == 0:
1804 | rxzero += 1
1805 | if rxzero > 5:
1806 | raise IOError("Too many read 0")
1807 | time.sleep(0.1)
1808 | continue # Try again
1809 | rxzero = 0
1810 |
1811 | while True:
1812 | if bytes != str:
1813 | stop = data.find(b'\n', start)
1814 | if stop < 0:
1815 | line_part += data[start:].decode(errors='ignore')
1816 | break
1817 | stop += 1
1818 | line_part, start, line = \
1819 | '', stop, line_part + data[start:stop].decode(errors='ignore')
1820 | else:
1821 | stop = data.find('\n', start)
1822 | if stop < 0:
1823 | line_part += data[start:]
1824 | break
1825 | stop += 1
1826 | line_part, start, line = \
1827 | '', stop, line_part + data[start:stop]
1828 | if __debug__: self._log(4, '< %s' % line)
1829 | self.inq.put(line)
1830 | if self.TerminateReader:
1831 | terminate = True
1832 |
1833 | if state & ~(select.POLLIN):
1834 | raise IOError(poll_error(state))
1835 | except:
1836 | reason = 'socket error: %s - %s' % sys.exc_info()[:2]
1837 | if __debug__:
1838 | if not self.Terminate:
1839 | self._print_log()
1840 | if self.debug: self.debug += 4 # Output all
1841 | self._log(1, reason)
1842 | self.inq.put((self.abort, reason))
1843 | break
1844 |
1845 | poll.unregister(self.read_fd)
1846 |
1847 | if __debug__: self._log(1, 'finished')
1848 |
1849 | else:
1850 |
1851 | # No "poll" - use select()
1852 |
1853 | def _reader(self):
1854 |
1855 | threading.currentThread().setName(self.identifier + 'reader')
1856 |
1857 | if __debug__: self._log(1, 'starting using select')
1858 |
1859 | line_part = ''
1860 |
1861 | rxzero = 0
1862 | terminate = False
1863 |
1864 | while not (terminate or self.Terminate):
1865 | if self.state == LOGOUT:
1866 | timeout = 1
1867 | else:
1868 | timeout = self.read_poll_timeout
1869 | try:
1870 | r,w,e = select.select([self.read_fd], [], [], timeout)
1871 | if __debug__: self._log(5, 'select => %s, %s, %s' % (r,w,e))
1872 | if not r: # Timeout
1873 | continue
1874 |
1875 | data = self.read(self.read_size) # Drain ssl buffer if present
1876 | start = 0
1877 | dlen = len(data)
1878 | if __debug__: self._log(5, 'rcvd %s' % dlen)
1879 | if dlen == 0:
1880 | rxzero += 1
1881 | if rxzero > 5:
1882 | raise IOError("Too many read 0")
1883 | time.sleep(0.1)
1884 | continue # Try again
1885 | rxzero = 0
1886 |
1887 | while True:
1888 | if bytes != str:
1889 | stop = data.find(b'\n', start)
1890 | if stop < 0:
1891 | line_part += data[start:].decode(errors='ignore')
1892 | break
1893 | stop += 1
1894 | line_part, start, line = \
1895 | '', stop, line_part + data[start:stop].decode(errors='ignore')
1896 | else:
1897 | stop = data.find('\n', start)
1898 | if stop < 0:
1899 | line_part += data[start:]
1900 | break
1901 | stop += 1
1902 | line_part, start, line = \
1903 | '', stop, line_part + data[start:stop]
1904 | if __debug__: self._log(4, '< %s' % line)
1905 | self.inq.put(line)
1906 | if self.TerminateReader:
1907 | terminate = True
1908 | except:
1909 | reason = 'socket error: %s - %s' % sys.exc_info()[:2]
1910 | if __debug__:
1911 | if not self.Terminate:
1912 | self._print_log()
1913 | if self.debug: self.debug += 4 # Output all
1914 | self._log(1, reason)
1915 | self.inq.put((self.abort, reason))
1916 | break
1917 |
1918 | if __debug__: self._log(1, 'finished')
1919 |
1920 |
1921 | def _writer(self):
1922 |
1923 | threading.currentThread().setName(self.identifier + 'writer')
1924 |
1925 | if __debug__: self._log(1, 'starting')
1926 |
1927 | reason = 'Terminated'
1928 |
1929 | while not self.Terminate:
1930 | rqb = self.ouq.get()
1931 | if rqb is None:
1932 | break # Outq flushed
1933 |
1934 | try:
1935 | self.send(rqb.data)
1936 | if __debug__: self._log(4, '> %s' % rqb.data)
1937 | except:
1938 | reason = 'socket error: %s - %s' % sys.exc_info()[:2]
1939 | if __debug__:
1940 | if not self.Terminate:
1941 | self._print_log()
1942 | if self.debug: self.debug += 4 # Output all
1943 | self._log(1, reason)
1944 | rqb.abort(self.abort, reason)
1945 | break
1946 |
1947 | self.inq.put((self.abort, reason))
1948 |
1949 | if __debug__: self._log(1, 'finished')
1950 |
1951 |
1952 |
1953 | # Debugging
1954 |
1955 |
1956 | if __debug__:
1957 |
1958 | def _init_debug(self, debug=None, debug_file=None, debug_buf_lvl=None):
1959 | self.debug_lock = threading.Lock()
1960 |
1961 | self.debug = self._choose_nonull_or_dflt(0, debug, Debug)
1962 | self.debug_file = self._choose_nonull_or_dflt(sys.stderr, debug_file)
1963 | self.debug_buf_lvl = self._choose_nonull_or_dflt(DFLT_DEBUG_BUF_LVL, debug_buf_lvl)
1964 |
1965 | self._cmd_log_len = 20
1966 | self._cmd_log_idx = 0
1967 | self._cmd_log = {} # Last `_cmd_log_len' interactions
1968 | if self.debug:
1969 | self._mesg('imaplib2 version %s' % __version__)
1970 | self._mesg('imaplib2 debug level %s, buffer level %s' % (self.debug, self.debug_buf_lvl))
1971 |
1972 |
1973 | def _dump_ur(self, lvl):
1974 | if lvl > self.debug:
1975 | return
1976 |
1977 | l = self.untagged_responses
1978 | if not l:
1979 | return
1980 |
1981 | t = '\n\t\t'
1982 | l = ['%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or '') for x in l]
1983 | self.debug_lock.acquire()
1984 | self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1985 | self.debug_lock.release()
1986 |
1987 |
1988 | def _log(self, lvl, line):
1989 | if lvl > self.debug:
1990 | return
1991 |
1992 | if line[-2:] == CRLF:
1993 | line = line[:-2] + '\\r\\n'
1994 |
1995 | tn = threading.currentThread().getName()
1996 |
1997 | if lvl <= 1 or self.debug > self.debug_buf_lvl:
1998 | self.debug_lock.acquire()
1999 | self._mesg(line, tn)
2000 | self.debug_lock.release()
2001 | if lvl != 1:
2002 | return
2003 |
2004 | # Keep log of last `_cmd_log_len' interactions for debugging.
2005 | self.debug_lock.acquire()
2006 | self._cmd_log[self._cmd_log_idx] = (line, tn, time.time())
2007 | self._cmd_log_idx += 1
2008 | if self._cmd_log_idx >= self._cmd_log_len:
2009 | self._cmd_log_idx = 0
2010 | self.debug_lock.release()
2011 |
2012 |
2013 | def _mesg(self, s, tn=None, secs=None):
2014 | if secs is None:
2015 | secs = time.time()
2016 | if tn is None:
2017 | tn = threading.currentThread().getName()
2018 | tm = time.strftime('%M:%S', time.localtime(secs))
2019 | try:
2020 | self.debug_file.write(' %s.%02d %s %s\n' % (tm, (secs*100)%100, tn, s))
2021 | self.debug_file.flush()
2022 | finally:
2023 | pass
2024 |
2025 |
2026 | def _print_log(self):
2027 | self.debug_lock.acquire()
2028 | i, n = self._cmd_log_idx, self._cmd_log_len
2029 | if n: self._mesg('last %d log messages:' % n)
2030 | while n:
2031 | try:
2032 | self._mesg(*self._cmd_log[i])
2033 | except:
2034 | pass
2035 | i += 1
2036 | if i >= self._cmd_log_len:
2037 | i = 0
2038 | n -= 1
2039 | self.debug_lock.release()
2040 |
2041 |
2042 |
2043 | class IMAP4_SSL(IMAP4):
2044 |
2045 | """IMAP4 client class over SSL connection
2046 |
2047 | Instantiate with:
2048 | IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None)
2049 |
2050 | host - host's name (default: localhost);
2051 | port - port number (default: standard IMAP4 SSL port);
2052 | keyfile - PEM formatted file that contains your private key (default: None);
2053 | certfile - PEM formatted certificate chain file (default: None);
2054 | ca_certs - PEM formatted certificate chain file used to validate server certificates (default: None);
2055 | cert_verify_cb - function to verify authenticity of server certificates (default: None);
2056 | ssl_version - SSL version to use (default: "ssl23", choose from: "tls1","ssl2","ssl3","ssl23");
2057 | debug - debug level (default: 0 - no debug);
2058 | debug_file - debug stream (default: sys.stderr);
2059 | identifier - thread identifier prefix (default: host);
2060 | timeout - timeout in seconds when expecting a command response.
2061 | debug_buf_lvl - debug level at which buffering is turned off.
2062 |
2063 | For more documentation see the docstring of the parent class IMAP4.
2064 | """
2065 |
2066 |
2067 | def __init__(self, host=None, port=None, keyfile=None, certfile=None, ca_certs=None, cert_verify_cb=None, ssl_version="ssl23", debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None):
2068 | self.keyfile = keyfile
2069 | self.certfile = certfile
2070 | self.ca_certs = ca_certs
2071 | self.cert_verify_cb = cert_verify_cb
2072 | self.ssl_version = ssl_version
2073 | IMAP4.__init__(self, host, port, debug, debug_file, identifier, timeout, debug_buf_lvl)
2074 |
2075 |
2076 | def open(self, host=None, port=None):
2077 | """open(host=None, port=None)
2078 | Setup secure connection to remote server on "host:port"
2079 | (default: localhost:standard IMAP4 SSL port).
2080 | This connection will be used by the routines:
2081 | read, send, shutdown, socket, ssl."""
2082 |
2083 | self.host = self._choose_nonull_or_dflt('', host)
2084 | self.port = self._choose_nonull_or_dflt(IMAP4_SSL_PORT, port)
2085 | self.sock = self.open_socket()
2086 | self.ssl_wrap_socket()
2087 |
2088 |
2089 | def read(self, size):
2090 | """data = read(size)
2091 | Read at most 'size' bytes from remote."""
2092 |
2093 | if self.decompressor is None:
2094 | return self.sock.read(size)
2095 |
2096 | if self.decompressor.unconsumed_tail:
2097 | data = self.decompressor.unconsumed_tail
2098 | else:
2099 | data = self.sock.read(READ_SIZE)
2100 |
2101 | return self.decompressor.decompress(data, size)
2102 |
2103 |
2104 | def send(self, data):
2105 | """send(data)
2106 | Send 'data' to remote."""
2107 |
2108 | if self.compressor is not None:
2109 | data = self.compressor.compress(data)
2110 | data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
2111 |
2112 | if bytes != str:
2113 | if hasattr(self.sock, "sendall"):
2114 | self.sock.sendall(bytes(data, 'utf8'))
2115 | else:
2116 | dlen = len(data)
2117 | while dlen > 0:
2118 | sent = self.sock.write(bytes(data, 'utf8'))
2119 | if sent == dlen:
2120 | break # avoid copy
2121 | data = data[sent:]
2122 | dlen = dlen - sent
2123 | else:
2124 | if hasattr(self.sock, "sendall"):
2125 | self.sock.sendall(data)
2126 | else:
2127 | dlen = len(data)
2128 | while dlen > 0:
2129 | sent = self.sock.write(data)
2130 | if sent == dlen:
2131 | break # avoid copy
2132 | data = data[sent:]
2133 | dlen = dlen - sent
2134 |
2135 |
2136 | def ssl(self):
2137 | """ssl = ssl()
2138 | Return ssl instance used to communicate with the IMAP4 server."""
2139 |
2140 | return self.sock
2141 |
2142 |
2143 |
2144 | class IMAP4_stream(IMAP4):
2145 |
2146 | """IMAP4 client class over a stream
2147 |
2148 | Instantiate with:
2149 | IMAP4_stream(command, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None)
2150 |
2151 | command - string that can be passed to subprocess.Popen();
2152 | debug - debug level (default: 0 - no debug);
2153 | debug_file - debug stream (default: sys.stderr);
2154 | identifier - thread identifier prefix (default: host);
2155 | timeout - timeout in seconds when expecting a command response.
2156 | debug_buf_lvl - debug level at which buffering is turned off.
2157 |
2158 | For more documentation see the docstring of the parent class IMAP4.
2159 | """
2160 |
2161 |
2162 | def __init__(self, command, debug=None, debug_file=None, identifier=None, timeout=None, debug_buf_lvl=None):
2163 | self.command = command
2164 | self.host = command
2165 | self.port = None
2166 | self.sock = None
2167 | self.writefile, self.readfile = None, None
2168 | self.read_fd = None
2169 | IMAP4.__init__(self, None, None, debug, debug_file, identifier, timeout, debug_buf_lvl)
2170 |
2171 |
2172 | def open(self, host=None, port=None):
2173 | """open(host=None, port=None)
2174 | Setup a stream connection via 'self.command'.
2175 | This connection will be used by the routines:
2176 | read, send, shutdown, socket."""
2177 |
2178 | from subprocess import Popen, PIPE
2179 |
2180 | if __debug__: self._log(0, 'opening stream from command "%s"' % self.command)
2181 | self._P = Popen(self.command, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True)
2182 | self.writefile, self.readfile = self._P.stdin, self._P.stdout
2183 | self.read_fd = self.readfile.fileno()
2184 |
2185 |
2186 | def read(self, size):
2187 | """Read 'size' bytes from remote."""
2188 |
2189 | if self.decompressor is None:
2190 | return os.read(self.read_fd, size)
2191 |
2192 | if self.decompressor.unconsumed_tail:
2193 | data = self.decompressor.unconsumed_tail
2194 | else:
2195 | data = os.read(self.read_fd, READ_SIZE)
2196 |
2197 | return self.decompressor.decompress(data, size)
2198 |
2199 |
2200 | def send(self, data):
2201 | """Send data to remote."""
2202 |
2203 | if self.compressor is not None:
2204 | data = self.compressor.compress(data)
2205 | data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
2206 |
2207 | if bytes != str:
2208 | self.writefile.write(bytes(data, 'utf8'))
2209 | else:
2210 | self.writefile.write(data)
2211 | self.writefile.flush()
2212 |
2213 |
2214 | def shutdown(self):
2215 | """Close I/O established in "open"."""
2216 |
2217 | self.readfile.close()
2218 | self.writefile.close()
2219 |
2220 |
2221 | class _Authenticator(object):
2222 |
2223 | """Private class to provide en/de-coding
2224 | for base64 authentication conversation."""
2225 |
2226 | def __init__(self, mechinst):
2227 | self.mech = mechinst # Callable object to provide/process data
2228 |
2229 | def process(self, data, rqb):
2230 | ret = self.mech(self.decode(data))
2231 | if ret is None:
2232 | return '*' # Abort conversation
2233 | return self.encode(ret)
2234 |
2235 | def encode(self, inp):
2236 | #
2237 | # Invoke binascii.b2a_base64 iteratively with
2238 | # short even length buffers, strip the trailing
2239 | # line feed from the result and append. "Even"
2240 | # means a number that factors to both 6 and 8,
2241 | # so when it gets to the end of the 8-bit input
2242 | # there's no partial 6-bit output.
2243 | #
2244 | oup = ''
2245 | while inp:
2246 | if len(inp) > 48:
2247 | t = inp[:48]
2248 | inp = inp[48:]
2249 | else:
2250 | t = inp
2251 | inp = ''
2252 | e = binascii.b2a_base64(t)
2253 | if e:
2254 | oup = oup + e[:-1]
2255 | return oup
2256 |
2257 | def decode(self, inp):
2258 | if not inp:
2259 | return ''
2260 | return binascii.a2b_base64(inp)
2261 |
2262 |
2263 |
2264 |
2265 | class _IdleCont(object):
2266 |
2267 | """When process is called, server is in IDLE state
2268 | and will send asynchronous changes."""
2269 |
2270 | def __init__(self, parent, timeout):
2271 | self.parent = parent
2272 | self.timeout = parent._choose_nonull_or_dflt(IDLE_TIMEOUT, timeout)
2273 | self.parent.idle_timeout = self.timeout + time.time()
2274 |
2275 | def process(self, data, rqb):
2276 | self.parent.idle_lock.acquire()
2277 | self.parent.idle_rqb = rqb
2278 | self.parent.idle_timeout = self.timeout + time.time()
2279 | self.parent.idle_lock.release()
2280 | if __debug__: self.parent._log(2, 'server IDLE started, timeout in %.2f secs' % self.timeout)
2281 | return None
2282 |
2283 |
2284 |
2285 | MonthNames = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
2286 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
2287 |
2288 | Mon2num = dict(list(zip((x.encode() for x in MonthNames[1:]), list(range(1, 13)))))
2289 |
2290 | InternalDate = re.compile(r'.*INTERNALDATE "'
2291 | r'(?P[ 0123][0-9])-(?P[A-Z][a-z][a-z])-(?P[0-9][0-9][0-9][0-9])'
2292 | r' (?P[0-9][0-9]):(?P[0-9][0-9]):(?P[0-9][0-9])'
2293 | r' (?P[-+])(?P[0-9][0-9])(?P[0-9][0-9])'
2294 | r'"')
2295 |
2296 |
2297 | def Internaldate2Time(resp):
2298 |
2299 | """time_tuple = Internaldate2Time(resp)
2300 | Convert IMAP4 INTERNALDATE to UT."""
2301 |
2302 | mo = InternalDate.match(resp)
2303 | if not mo:
2304 | return None
2305 |
2306 | mon = Mon2num[mo.group('mon')]
2307 | zonen = mo.group('zonen')
2308 |
2309 | day = int(mo.group('day'))
2310 | year = int(mo.group('year'))
2311 | hour = int(mo.group('hour'))
2312 | min = int(mo.group('min'))
2313 | sec = int(mo.group('sec'))
2314 | zoneh = int(mo.group('zoneh'))
2315 | zonem = int(mo.group('zonem'))
2316 |
2317 | # INTERNALDATE timezone must be subtracted to get UT
2318 |
2319 | zone = (zoneh*60 + zonem)*60
2320 | if zonen == '-':
2321 | zone = -zone
2322 |
2323 | tt = (year, mon, day, hour, min, sec, -1, -1, -1)
2324 |
2325 | utc = time.mktime(tt)
2326 |
2327 | # Following is necessary because the time module has no 'mkgmtime'.
2328 | # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
2329 |
2330 | lt = time.localtime(utc)
2331 | if time.daylight and lt[-1]:
2332 | zone = zone + time.altzone
2333 | else:
2334 | zone = zone + time.timezone
2335 |
2336 | return time.localtime(utc - zone)
2337 |
2338 | Internaldate2tuple = Internaldate2Time # (Backward compatible)
2339 |
2340 |
2341 |
2342 | def Time2Internaldate(date_time):
2343 |
2344 | """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time)
2345 | Convert 'date_time' to IMAP4 INTERNALDATE representation."""
2346 |
2347 | if isinstance(date_time, (int, float)):
2348 | tt = time.localtime(date_time)
2349 | elif isinstance(date_time, (tuple, time.struct_time)):
2350 | tt = date_time
2351 | elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
2352 | return date_time # Assume in correct format
2353 | else:
2354 | raise ValueError("date_time not of a known type")
2355 |
2356 | if time.daylight and tt[-1]:
2357 | zone = -time.altzone
2358 | else:
2359 | zone = -time.timezone
2360 | return ('"%2d-%s-%04d %02d:%02d:%02d %+03d%02d"' %
2361 | ((tt[2], MonthNames[tt[1]], tt[0]) + tt[3:6] +
2362 | divmod(zone//60, 60)))
2363 |
2364 |
2365 |
2366 | FLAGS_cre = re.compile(r'.*FLAGS \((?P[^\)]*)\)')
2367 |
2368 | def ParseFlags(resp):
2369 |
2370 | """('flag', ...) = ParseFlags(line)
2371 | Convert IMAP4 flags response to python tuple."""
2372 |
2373 | mo = FLAGS_cre.match(resp)
2374 | if not mo:
2375 | return ()
2376 |
2377 | return tuple(mo.group('flags').split())
2378 |
2379 |
2380 |
2381 | if __name__ == '__main__':
2382 |
2383 | # To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]',
2384 | # or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
2385 | # or as 'python imaplib2.py -l keyfile[:certfile]|: [IMAP4_SSL_server_hostname]'
2386 | #
2387 | # Option "-d " turns on debugging (use "-d 5" for everything)
2388 | # Option "-i" tests that IDLE is interruptible
2389 | # Option "-p " allows alternate ports
2390 |
2391 | if not __debug__:
2392 | raise ValueError('Please run without -O')
2393 |
2394 | import getopt, getpass
2395 |
2396 | try:
2397 | optlist, args = getopt.getopt(sys.argv[1:], 'd:il:s:p:')
2398 | except getopt.error as val:
2399 | optlist, args = (), ()
2400 |
2401 | debug, debug_buf_lvl, port, stream_command, keyfile, certfile, idle_intr = (None,)*7
2402 | for opt,val in optlist:
2403 | if opt == '-d':
2404 | debug = int(val)
2405 | debug_buf_lvl = debug - 1
2406 | elif opt == '-i':
2407 | idle_intr = 1
2408 | elif opt == '-l':
2409 | try:
2410 | keyfile,certfile = val.split(':')
2411 | except ValueError:
2412 | keyfile,certfile = val,val
2413 | elif opt == '-p':
2414 | port = int(val)
2415 | elif opt == '-s':
2416 | stream_command = val
2417 | if not args: args = (stream_command,)
2418 |
2419 | if not args: args = ('',)
2420 | if not port: port = (keyfile is not None) and IMAP4_SSL_PORT or IMAP4_PORT
2421 |
2422 | host = args[0]
2423 |
2424 | USER = getpass.getuser()
2425 |
2426 | data = open(os.path.exists("test.data") and "test.data" or __file__).read(1000)
2427 | test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \
2428 | % {'user':USER, 'lf':'\n', 'data':data}
2429 |
2430 | test_seq1 = [
2431 | ('list', ('""', '""')),
2432 | ('list', ('""', '%')),
2433 | ('create', ('imaplib2_test0',)),
2434 | ('rename', ('imaplib2_test0', 'imaplib2_test1')),
2435 | ('CREATE', ('imaplib2_test2',)),
2436 | ('append', ('imaplib2_test2', None, None, test_mesg)),
2437 | ('list', ('', 'imaplib2_test%')),
2438 | ('select', ('imaplib2_test2',)),
2439 | ('search', (None, 'SUBJECT', 'IMAP4 test')),
2440 | ('fetch', ("'1:*'", '(FLAGS INTERNALDATE RFC822)')),
2441 | ('store', ('1', 'FLAGS', '(\Deleted)')),
2442 | ('namespace', ()),
2443 | ('expunge', ()),
2444 | ('recent', ()),
2445 | ('close', ()),
2446 | ]
2447 |
2448 | test_seq2 = (
2449 | ('select', ()),
2450 | ('response', ('UIDVALIDITY',)),
2451 | ('response', ('EXISTS',)),
2452 | ('append', (None, None, None, test_mesg)),
2453 | ('examine', ()),
2454 | ('select', ()),
2455 | ('fetch', ("'1:*'", '(FLAGS UID)')),
2456 | ('examine', ()),
2457 | ('select', ()),
2458 | ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')),
2459 | ('uid', ('SEARCH', 'ALL')),
2460 | ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')),
2461 | ('recent', ()),
2462 | )
2463 |
2464 |
2465 | AsyncError = None
2466 |
2467 | def responder(cb_arg_list):
2468 | response, cb_arg, error = cb_arg_list
2469 | global AsyncError
2470 | cmd, args = cb_arg
2471 | if error is not None:
2472 | AsyncError = error
2473 | M._log(0, '[cb] ERROR %s %.100s => %s' % (cmd, args, error))
2474 | return
2475 | typ, dat = response
2476 | M._log(0, '[cb] %s %.100s => %s %.100s' % (cmd, args, typ, dat))
2477 | if typ == 'NO':
2478 | AsyncError = (Exception, dat[0])
2479 |
2480 | def run(cmd, args, cb=True):
2481 | if AsyncError:
2482 | M._log(1, 'AsyncError %s' % repr(AsyncError))
2483 | M.logout()
2484 | typ, val = AsyncError
2485 | raise typ(val)
2486 | if not M.debug: M._log(0, '%s %.100s' % (cmd, args))
2487 | try:
2488 | if cb:
2489 | typ, dat = getattr(M, cmd)(callback=responder, cb_arg=(cmd, args), *args)
2490 | M._log(1, '%s %.100s => %s %.100s' % (cmd, args, typ, dat))
2491 | else:
2492 | typ, dat = getattr(M, cmd)(*args)
2493 | M._log(1, '%s %.100s => %s %.100s' % (cmd, args, typ, dat))
2494 | except:
2495 | M._log(1, '%s - %s' % sys.exc_info()[:2])
2496 | M.logout()
2497 | raise
2498 | if typ == 'NO':
2499 | M._log(1, 'NO')
2500 | M.logout()
2501 | raise Exception(dat[0])
2502 | return dat
2503 |
2504 | try:
2505 | threading.currentThread().setName('main')
2506 |
2507 | if keyfile is not None:
2508 | if not keyfile: keyfile = None
2509 | if not certfile: certfile = None
2510 | M = IMAP4_SSL(host=host, port=port, keyfile=keyfile, certfile=certfile, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl)
2511 | elif stream_command:
2512 | M = IMAP4_stream(stream_command, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl)
2513 | else:
2514 | M = IMAP4(host=host, port=port, debug=debug, identifier='', timeout=10, debug_buf_lvl=debug_buf_lvl)
2515 | if M.state != 'AUTH': # Login needed
2516 | PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
2517 | test_seq1.insert(0, ('login', (USER, PASSWD)))
2518 | M._log(0, 'PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
2519 | if 'COMPRESS=DEFLATE' in M.capabilities:
2520 | M.enable_compression()
2521 |
2522 | for cmd,args in test_seq1:
2523 | run(cmd, args)
2524 |
2525 | for ml in run('list', ('', 'imaplib2_test%'), cb=False):
2526 | mo = re.match(r'.*"([^"]+)"$', ml)
2527 | if mo: path = mo.group(1)
2528 | else: path = ml.split()[-1]
2529 | run('delete', (path,))
2530 |
2531 | if 'ID' in M.capabilities:
2532 | run('id', ())
2533 | run('id', ("(name imaplib2)",))
2534 | run('id', ("version", __version__, "os", os.uname()[0]))
2535 |
2536 | for cmd,args in test_seq2:
2537 | if (cmd,args) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')):
2538 | run(cmd, args)
2539 | continue
2540 |
2541 | dat = run(cmd, args, cb=False)
2542 | uid = dat[-1].split()
2543 | if not uid: continue
2544 | run('uid', ('FETCH', uid[-1],
2545 | '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
2546 | run('uid', ('STORE', uid[-1], 'FLAGS', '(\Deleted)'))
2547 | run('expunge', ())
2548 |
2549 | if 'IDLE' in M.capabilities:
2550 | run('idle', (2,), cb=False)
2551 | run('idle', (99,)) # Asynchronous, to test interruption of 'idle' by 'noop'
2552 | time.sleep(1)
2553 | run('noop', (), cb=False)
2554 |
2555 | run('append', (None, None, None, test_mesg), cb=False)
2556 | num = run('search', (None, 'ALL'), cb=False)[0].split()[0]
2557 | dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False)
2558 | M._mesg('fetch %s => %s' % (num, repr(dat)))
2559 | run('idle', (2,))
2560 | run('store', (num, '-FLAGS', '(\Seen)'), cb=False),
2561 | dat = run('fetch', (num, '(FLAGS INTERNALDATE RFC822)'), cb=False)
2562 | M._mesg('fetch %s => %s' % (num, repr(dat)))
2563 | run('uid', ('STORE', num, 'FLAGS', '(\Deleted)'))
2564 | run('expunge', ())
2565 | if idle_intr:
2566 | M._mesg('HIT CTRL-C to interrupt IDLE')
2567 | try:
2568 | run('idle', (99,), cb=False) # Synchronous, to test interruption of 'idle' by INTR
2569 | except KeyboardInterrupt:
2570 | M._mesg('Thanks!')
2571 | M._mesg('')
2572 | raise
2573 | elif idle_intr:
2574 | M._mesg('chosen server does not report IDLE capability')
2575 |
2576 | run('logout', (), cb=False)
2577 |
2578 | if debug:
2579 | M._mesg('')
2580 | M._print_log()
2581 | M._mesg('')
2582 | M._mesg('unused untagged responses in order, most recent last:')
2583 | for typ,dat in M.pop_untagged_responses(): M._mesg('\t%s %s' % (typ, dat))
2584 |
2585 | print('All tests OK.')
2586 |
2587 | except:
2588 | if not idle_intr or not 'IDLE' in M.capabilities:
2589 | print('Tests failed.')
2590 |
2591 | if not debug:
2592 | print('''
2593 | If you would like to see debugging output,
2594 | try: %s -d5
2595 | ''' % sys.argv[0])
2596 |
2597 | raise
2598 |
--------------------------------------------------------------------------------
/readme.txt:
--------------------------------------------------------------------------------
1 | This is a program which allows you to remotely control your home computer or server using SMS messages in a format which we call 'SSh over SMS'. It features two factor authentication using a password stored in the config.txt file.
2 | To use for yourself:
3 | - Build a dummy gmail account
4 | - Download this repository
5 | - Edit the time in time.sleep() at the bottom of PyMail.py to specify how long you want the program to run for. (On my server it's set up as an hourly cronjob so I run it for one hour at a time)
6 | - Edit the config file to include in this order:
7 | 1) your gmail account address
8 | 2) your gmail account password
9 | 3) the email associated with your phone number (google "email over sms" to find the extension for your carrier)
10 | 4) the password used for two-factor authentication
11 |
12 | The contact info in the current config.txt file is fake.
13 | Do not try to contact the email or phone number in the config file.
14 |
15 | Link to a video demo: https://devpost.com/software/ssh-over-sms
16 |
--------------------------------------------------------------------------------