├── .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 | --------------------------------------------------------------------------------