| 1 | """IMAP4 client.
|
|---|
| 2 |
|
|---|
| 3 | Based on RFC 2060.
|
|---|
| 4 |
|
|---|
| 5 | Public class: IMAP4
|
|---|
| 6 | Public variable: Debug
|
|---|
| 7 | Public functions: Internaldate2tuple
|
|---|
| 8 | Int2AP
|
|---|
| 9 | ParseFlags
|
|---|
| 10 | Time2Internaldate
|
|---|
| 11 | """
|
|---|
| 12 |
|
|---|
| 13 | # Author: Piers Lauder <[email protected]> December 1997.
|
|---|
| 14 | #
|
|---|
| 15 | # Authentication code contributed by Donn Cave <[email protected]> June 1998.
|
|---|
| 16 | # String method conversion by ESR, February 2001.
|
|---|
| 17 | # GET/SETACL contributed by Anthony Baxter <[email protected]> April 2001.
|
|---|
| 18 | # IMAP4_SSL contributed by Tino Lange <[email protected]> March 2002.
|
|---|
| 19 | # GET/SETQUOTA contributed by Andreas Zeidler <[email protected]> June 2002.
|
|---|
| 20 | # PROXYAUTH contributed by Rick Holbert <[email protected]> November 2002.
|
|---|
| 21 | # GET/SETANNOTATION contributed by Tomas Lindroos <[email protected]> June 2005.
|
|---|
| 22 |
|
|---|
| 23 | __version__ = "2.58"
|
|---|
| 24 |
|
|---|
| 25 | import binascii, os, random, re, socket, sys, time
|
|---|
| 26 |
|
|---|
| 27 | __all__ = ["IMAP4", "IMAP4_SSL", "IMAP4_stream", "Internaldate2tuple",
|
|---|
| 28 | "Int2AP", "ParseFlags", "Time2Internaldate"]
|
|---|
| 29 |
|
|---|
| 30 | # Globals
|
|---|
| 31 |
|
|---|
| 32 | CRLF = '\r\n'
|
|---|
| 33 | Debug = 0
|
|---|
| 34 | IMAP4_PORT = 143
|
|---|
| 35 | IMAP4_SSL_PORT = 993
|
|---|
| 36 | AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
|
|---|
| 37 |
|
|---|
| 38 | # Commands
|
|---|
| 39 |
|
|---|
| 40 | Commands = {
|
|---|
| 41 | # name valid states
|
|---|
| 42 | 'APPEND': ('AUTH', 'SELECTED'),
|
|---|
| 43 | 'AUTHENTICATE': ('NONAUTH',),
|
|---|
| 44 | 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
|
|---|
| 45 | 'CHECK': ('SELECTED',),
|
|---|
| 46 | 'CLOSE': ('SELECTED',),
|
|---|
| 47 | 'COPY': ('SELECTED',),
|
|---|
| 48 | 'CREATE': ('AUTH', 'SELECTED'),
|
|---|
| 49 | 'DELETE': ('AUTH', 'SELECTED'),
|
|---|
| 50 | 'DELETEACL': ('AUTH', 'SELECTED'),
|
|---|
| 51 | 'EXAMINE': ('AUTH', 'SELECTED'),
|
|---|
| 52 | 'EXPUNGE': ('SELECTED',),
|
|---|
| 53 | 'FETCH': ('SELECTED',),
|
|---|
| 54 | 'GETACL': ('AUTH', 'SELECTED'),
|
|---|
| 55 | 'GETANNOTATION':('AUTH', 'SELECTED'),
|
|---|
| 56 | 'GETQUOTA': ('AUTH', 'SELECTED'),
|
|---|
| 57 | 'GETQUOTAROOT': ('AUTH', 'SELECTED'),
|
|---|
| 58 | 'MYRIGHTS': ('AUTH', 'SELECTED'),
|
|---|
| 59 | 'LIST': ('AUTH', 'SELECTED'),
|
|---|
| 60 | 'LOGIN': ('NONAUTH',),
|
|---|
| 61 | 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
|
|---|
| 62 | 'LSUB': ('AUTH', 'SELECTED'),
|
|---|
| 63 | 'NAMESPACE': ('AUTH', 'SELECTED'),
|
|---|
| 64 | 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
|
|---|
| 65 | 'PARTIAL': ('SELECTED',), # NB: obsolete
|
|---|
| 66 | 'PROXYAUTH': ('AUTH',),
|
|---|
| 67 | 'RENAME': ('AUTH', 'SELECTED'),
|
|---|
| 68 | 'SEARCH': ('SELECTED',),
|
|---|
| 69 | 'SELECT': ('AUTH', 'SELECTED'),
|
|---|
| 70 | 'SETACL': ('AUTH', 'SELECTED'),
|
|---|
| 71 | 'SETANNOTATION':('AUTH', 'SELECTED'),
|
|---|
| 72 | 'SETQUOTA': ('AUTH', 'SELECTED'),
|
|---|
| 73 | 'SORT': ('SELECTED',),
|
|---|
| 74 | 'STATUS': ('AUTH', 'SELECTED'),
|
|---|
| 75 | 'STORE': ('SELECTED',),
|
|---|
| 76 | 'SUBSCRIBE': ('AUTH', 'SELECTED'),
|
|---|
| 77 | 'THREAD': ('SELECTED',),
|
|---|
| 78 | 'UID': ('SELECTED',),
|
|---|
| 79 | 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
|
|---|
| 80 | }
|
|---|
| 81 |
|
|---|
| 82 | # Patterns to match server responses
|
|---|
| 83 |
|
|---|
| 84 | Continuation = re.compile(r'\+( (?P<data>.*))?')
|
|---|
| 85 | Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
|
|---|
| 86 | InternalDate = re.compile(r'.*INTERNALDATE "'
|
|---|
| 87 | r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
|
|---|
| 88 | r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
|
|---|
| 89 | r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
|
|---|
| 90 | r'"')
|
|---|
| 91 | Literal = re.compile(r'.*{(?P<size>\d+)}$')
|
|---|
| 92 | MapCRLF = re.compile(r'\r\n|\r|\n')
|
|---|
| 93 | Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
|
|---|
| 94 | Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
|
|---|
| 95 | Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
|
|---|
| 96 |
|
|---|
| 97 |
|
|---|
| 98 |
|
|---|
| 99 | class IMAP4:
|
|---|
| 100 |
|
|---|
| 101 | """IMAP4 client class.
|
|---|
| 102 |
|
|---|
| 103 | Instantiate with: IMAP4([host[, port]])
|
|---|
| 104 |
|
|---|
| 105 | host - host's name (default: localhost);
|
|---|
| 106 | port - port number (default: standard IMAP4 port).
|
|---|
| 107 |
|
|---|
| 108 | All IMAP4rev1 commands are supported by methods of the same
|
|---|
| 109 | name (in lower-case).
|
|---|
| 110 |
|
|---|
| 111 | All arguments to commands are converted to strings, except for
|
|---|
| 112 | AUTHENTICATE, and the last argument to APPEND which is passed as
|
|---|
| 113 | an IMAP4 literal. If necessary (the string contains any
|
|---|
| 114 | non-printing characters or white-space and isn't enclosed with
|
|---|
| 115 | either parentheses or double quotes) each string is quoted.
|
|---|
| 116 | However, the 'password' argument to the LOGIN command is always
|
|---|
| 117 | quoted. If you want to avoid having an argument string quoted
|
|---|
| 118 | (eg: the 'flags' argument to STORE) then enclose the string in
|
|---|
| 119 | parentheses (eg: "(\Deleted)").
|
|---|
| 120 |
|
|---|
| 121 | Each command returns a tuple: (type, [data, ...]) where 'type'
|
|---|
| 122 | is usually 'OK' or 'NO', and 'data' is either the text from the
|
|---|
| 123 | tagged response, or untagged results from command. Each 'data'
|
|---|
| 124 | is either a string, or a tuple. If a tuple, then the first part
|
|---|
| 125 | is the header of the response, and the second part contains
|
|---|
| 126 | the data (ie: 'literal' value).
|
|---|
| 127 |
|
|---|
| 128 | Errors raise the exception class <instance>.error("<reason>").
|
|---|
| 129 | IMAP4 server errors raise <instance>.abort("<reason>"),
|
|---|
| 130 | which is a sub-class of 'error'. Mailbox status changes
|
|---|
| 131 | from READ-WRITE to READ-ONLY raise the exception class
|
|---|
| 132 | <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
|
|---|
| 133 |
|
|---|
| 134 | "error" exceptions imply a program error.
|
|---|
| 135 | "abort" exceptions imply the connection should be reset, and
|
|---|
| 136 | the command re-tried.
|
|---|
| 137 | "readonly" exceptions imply the command should be re-tried.
|
|---|
| 138 |
|
|---|
| 139 | Note: to use this module, you must read the RFCs pertaining to the
|
|---|
| 140 | IMAP4 protocol, as the semantics of the arguments to each IMAP4
|
|---|
| 141 | command are left to the invoker, not to mention the results. Also,
|
|---|
| 142 | most IMAP servers implement a sub-set of the commands available here.
|
|---|
| 143 | """
|
|---|
| 144 |
|
|---|
| 145 | class error(Exception): pass # Logical errors - debug required
|
|---|
| 146 | class abort(error): pass # Service errors - close and retry
|
|---|
| 147 | class readonly(abort): pass # Mailbox status changed to READ-ONLY
|
|---|
| 148 |
|
|---|
| 149 | mustquote = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
|
|---|
| 150 |
|
|---|
| 151 | def __init__(self, host = '', port = IMAP4_PORT):
|
|---|
| 152 | self.debug = Debug
|
|---|
| 153 | self.state = 'LOGOUT'
|
|---|
| 154 | self.literal = None # A literal argument to a command
|
|---|
| 155 | self.tagged_commands = {} # Tagged commands awaiting response
|
|---|
| 156 | self.untagged_responses = {} # {typ: [data, ...], ...}
|
|---|
| 157 | self.continuation_response = '' # Last continuation response
|
|---|
| 158 | self.is_readonly = False # READ-ONLY desired state
|
|---|
| 159 | self.tagnum = 0
|
|---|
| 160 |
|
|---|
| 161 | # Open socket to server.
|
|---|
| 162 |
|
|---|
| 163 | self.open(host, port)
|
|---|
| 164 |
|
|---|
| 165 | # Create unique tag for this session,
|
|---|
| 166 | # and compile tagged response matcher.
|
|---|
| 167 |
|
|---|
| 168 | self.tagpre = Int2AP(random.randint(4096, 65535))
|
|---|
| 169 | self.tagre = re.compile(r'(?P<tag>'
|
|---|
| 170 | + self.tagpre
|
|---|
| 171 | + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
|
|---|
| 172 |
|
|---|
| 173 | # Get server welcome message,
|
|---|
| 174 | # request and store CAPABILITY response.
|
|---|
| 175 |
|
|---|
| 176 | if __debug__:
|
|---|
| 177 | self._cmd_log_len = 10
|
|---|
| 178 | self._cmd_log_idx = 0
|
|---|
| 179 | self._cmd_log = {} # Last `_cmd_log_len' interactions
|
|---|
| 180 | if self.debug >= 1:
|
|---|
| 181 | self._mesg('imaplib version %s' % __version__)
|
|---|
| 182 | self._mesg('new IMAP4 connection, tag=%s' % self.tagpre)
|
|---|
| 183 |
|
|---|
| 184 | self.welcome = self._get_response()
|
|---|
| 185 | if 'PREAUTH' in self.untagged_responses:
|
|---|
| 186 | self.state = 'AUTH'
|
|---|
| 187 | elif 'OK' in self.untagged_responses:
|
|---|
| 188 | self.state = 'NONAUTH'
|
|---|
| 189 | else:
|
|---|
| 190 | raise self.error(self.welcome)
|
|---|
| 191 |
|
|---|
| 192 | typ, dat = self.capability()
|
|---|
| 193 | if dat == [None]:
|
|---|
| 194 | raise self.error('no CAPABILITY response from server')
|
|---|
| 195 | self.capabilities = tuple(dat[-1].upper().split())
|
|---|
| 196 |
|
|---|
| 197 | if __debug__:
|
|---|
| 198 | if self.debug >= 3:
|
|---|
| 199 | self._mesg('CAPABILITIES: %r' % (self.capabilities,))
|
|---|
| 200 |
|
|---|
| 201 | for version in AllowedVersions:
|
|---|
| 202 | if not version in self.capabilities:
|
|---|
| 203 | continue
|
|---|
| 204 | self.PROTOCOL_VERSION = version
|
|---|
| 205 | return
|
|---|
| 206 |
|
|---|
| 207 | raise self.error('server not IMAP4 compliant')
|
|---|
| 208 |
|
|---|
| 209 |
|
|---|
| 210 | def __getattr__(self, attr):
|
|---|
| 211 | # Allow UPPERCASE variants of IMAP4 command methods.
|
|---|
| 212 | if attr in Commands:
|
|---|
| 213 | return getattr(self, attr.lower())
|
|---|
| 214 | raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
|
|---|
| 215 |
|
|---|
| 216 |
|
|---|
| 217 |
|
|---|
| 218 | # Overridable methods
|
|---|
| 219 |
|
|---|
| 220 |
|
|---|
| 221 | def open(self, host = '', port = IMAP4_PORT):
|
|---|
| 222 | """Setup connection to remote server on "host:port"
|
|---|
| 223 | (default: localhost:standard IMAP4 port).
|
|---|
| 224 | This connection will be used by the routines:
|
|---|
| 225 | read, readline, send, shutdown.
|
|---|
| 226 | """
|
|---|
| 227 | self.host = host
|
|---|
| 228 | self.port = port
|
|---|
| 229 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|---|
| 230 | self.sock.connect((host, port))
|
|---|
| 231 | self.file = self.sock.makefile('rb')
|
|---|
| 232 |
|
|---|
| 233 |
|
|---|
| 234 | def read(self, size):
|
|---|
| 235 | """Read 'size' bytes from remote."""
|
|---|
| 236 | return self.file.read(size)
|
|---|
| 237 |
|
|---|
| 238 |
|
|---|
| 239 | def readline(self):
|
|---|
| 240 | """Read line from remote."""
|
|---|
| 241 | return self.file.readline()
|
|---|
| 242 |
|
|---|
| 243 |
|
|---|
| 244 | def send(self, data):
|
|---|
| 245 | """Send data to remote."""
|
|---|
| 246 | self.sock.sendall(data)
|
|---|
| 247 |
|
|---|
| 248 |
|
|---|
| 249 | def shutdown(self):
|
|---|
| 250 | """Close I/O established in "open"."""
|
|---|
| 251 | self.file.close()
|
|---|
| 252 | self.sock.close()
|
|---|
| 253 |
|
|---|
| 254 |
|
|---|
| 255 | def socket(self):
|
|---|
| 256 | """Return socket instance used to connect to IMAP4 server.
|
|---|
| 257 |
|
|---|
| 258 | socket = <instance>.socket()
|
|---|
| 259 | """
|
|---|
| 260 | return self.sock
|
|---|
| 261 |
|
|---|
| 262 |
|
|---|
| 263 |
|
|---|
| 264 | # Utility methods
|
|---|
| 265 |
|
|---|
| 266 |
|
|---|
| 267 | def recent(self):
|
|---|
| 268 | """Return most recent 'RECENT' responses if any exist,
|
|---|
| 269 | else prompt server for an update using the 'NOOP' command.
|
|---|
| 270 |
|
|---|
| 271 | (typ, [data]) = <instance>.recent()
|
|---|
| 272 |
|
|---|
| 273 | 'data' is None if no new messages,
|
|---|
| 274 | else list of RECENT responses, most recent last.
|
|---|
| 275 | """
|
|---|
| 276 | name = 'RECENT'
|
|---|
| 277 | typ, dat = self._untagged_response('OK', [None], name)
|
|---|
| 278 | if dat[-1]:
|
|---|
| 279 | return typ, dat
|
|---|
| 280 | typ, dat = self.noop() # Prod server for response
|
|---|
| 281 | return self._untagged_response(typ, dat, name)
|
|---|
| 282 |
|
|---|
| 283 |
|
|---|
| 284 | def response(self, code):
|
|---|
| 285 | """Return data for response 'code' if received, or None.
|
|---|
| 286 |
|
|---|
| 287 | Old value for response 'code' is cleared.
|
|---|
| 288 |
|
|---|
| 289 | (code, [data]) = <instance>.response(code)
|
|---|
| 290 | """
|
|---|
| 291 | return self._untagged_response(code, [None], code.upper())
|
|---|
| 292 |
|
|---|
| 293 |
|
|---|
| 294 |
|
|---|
| 295 | # IMAP4 commands
|
|---|
| 296 |
|
|---|
| 297 |
|
|---|
| 298 | def append(self, mailbox, flags, date_time, message):
|
|---|
| 299 | """Append message to named mailbox.
|
|---|
| 300 |
|
|---|
| 301 | (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
|
|---|
| 302 |
|
|---|
| 303 | All args except `message' can be None.
|
|---|
| 304 | """
|
|---|
| 305 | name = 'APPEND'
|
|---|
| 306 | if not mailbox:
|
|---|
| 307 | mailbox = 'INBOX'
|
|---|
| 308 | if flags:
|
|---|
| 309 | if (flags[0],flags[-1]) != ('(',')'):
|
|---|
| 310 | flags = '(%s)' % flags
|
|---|
| 311 | else:
|
|---|
| 312 | flags = None
|
|---|
| 313 | if date_time:
|
|---|
| 314 | date_time = Time2Internaldate(date_time)
|
|---|
| 315 | else:
|
|---|
| 316 | date_time = None
|
|---|
| 317 | self.literal = MapCRLF.sub(CRLF, message)
|
|---|
| 318 | return self._simple_command(name, mailbox, flags, date_time)
|
|---|
| 319 |
|
|---|
| 320 |
|
|---|
| 321 | def authenticate(self, mechanism, authobject):
|
|---|
| 322 | """Authenticate command - requires response processing.
|
|---|
| 323 |
|
|---|
| 324 | 'mechanism' specifies which authentication mechanism is to
|
|---|
| 325 | be used - it must appear in <instance>.capabilities in the
|
|---|
| 326 | form AUTH=<mechanism>.
|
|---|
| 327 |
|
|---|
| 328 | 'authobject' must be a callable object:
|
|---|
| 329 |
|
|---|
| 330 | data = authobject(response)
|
|---|
| 331 |
|
|---|
| 332 | It will be called to process server continuation responses.
|
|---|
| 333 | It should return data that will be encoded and sent to server.
|
|---|
| 334 | It should return None if the client abort response '*' should
|
|---|
| 335 | be sent instead.
|
|---|
| 336 | """
|
|---|
| 337 | mech = mechanism.upper()
|
|---|
| 338 | # XXX: shouldn't this code be removed, not commented out?
|
|---|
| 339 | #cap = 'AUTH=%s' % mech
|
|---|
| 340 | #if not cap in self.capabilities: # Let the server decide!
|
|---|
| 341 | # raise self.error("Server doesn't allow %s authentication." % mech)
|
|---|
| 342 | self.literal = _Authenticator(authobject).process
|
|---|
| 343 | typ, dat = self._simple_command('AUTHENTICATE', mech)
|
|---|
| 344 | if typ != 'OK':
|
|---|
| 345 | raise self.error(dat[-1])
|
|---|
| 346 | self.state = 'AUTH'
|
|---|
| 347 | return typ, dat
|
|---|
| 348 |
|
|---|
| 349 |
|
|---|
| 350 | def capability(self):
|
|---|
| 351 | """(typ, [data]) = <instance>.capability()
|
|---|
| 352 | Fetch capabilities list from server."""
|
|---|
| 353 |
|
|---|
| 354 | name = 'CAPABILITY'
|
|---|
| 355 | typ, dat = self._simple_command(name)
|
|---|
| 356 | return self._untagged_response(typ, dat, name)
|
|---|
| 357 |
|
|---|
| 358 |
|
|---|
| 359 | def check(self):
|
|---|
| 360 | """Checkpoint mailbox on server.
|
|---|
| 361 |
|
|---|
| 362 | (typ, [data]) = <instance>.check()
|
|---|
| 363 | """
|
|---|
| 364 | return self._simple_command('CHECK')
|
|---|
| 365 |
|
|---|
| 366 |
|
|---|
| 367 | def close(self):
|
|---|
| 368 | """Close currently selected mailbox.
|
|---|
| 369 |
|
|---|
| 370 | Deleted messages are removed from writable mailbox.
|
|---|
| 371 | This is the recommended command before 'LOGOUT'.
|
|---|
| 372 |
|
|---|
| 373 | (typ, [data]) = <instance>.close()
|
|---|
| 374 | """
|
|---|
| 375 | try:
|
|---|
| 376 | typ, dat = self._simple_command('CLOSE')
|
|---|
| 377 | finally:
|
|---|
| 378 | self.state = 'AUTH'
|
|---|
| 379 | return typ, dat
|
|---|
| 380 |
|
|---|
| 381 |
|
|---|
| 382 | def copy(self, message_set, new_mailbox):
|
|---|
| 383 | """Copy 'message_set' messages onto end of 'new_mailbox'.
|
|---|
| 384 |
|
|---|
| 385 | (typ, [data]) = <instance>.copy(message_set, new_mailbox)
|
|---|
| 386 | """
|
|---|
| 387 | return self._simple_command('COPY', message_set, new_mailbox)
|
|---|
| 388 |
|
|---|
| 389 |
|
|---|
| 390 | def create(self, mailbox):
|
|---|
| 391 | """Create new mailbox.
|
|---|
| 392 |
|
|---|
| 393 | (typ, [data]) = <instance>.create(mailbox)
|
|---|
| 394 | """
|
|---|
| 395 | return self._simple_command('CREATE', mailbox)
|
|---|
| 396 |
|
|---|
| 397 |
|
|---|
| 398 | def delete(self, mailbox):
|
|---|
| 399 | """Delete old mailbox.
|
|---|
| 400 |
|
|---|
| 401 | (typ, [data]) = <instance>.delete(mailbox)
|
|---|
| 402 | """
|
|---|
| 403 | return self._simple_command('DELETE', mailbox)
|
|---|
| 404 |
|
|---|
| 405 | def deleteacl(self, mailbox, who):
|
|---|
| 406 | """Delete the ACLs (remove any rights) set for who on mailbox.
|
|---|
| 407 |
|
|---|
| 408 | (typ, [data]) = <instance>.deleteacl(mailbox, who)
|
|---|
| 409 | """
|
|---|
| 410 | return self._simple_command('DELETEACL', mailbox, who)
|
|---|
| 411 |
|
|---|
| 412 | def expunge(self):
|
|---|
| 413 | """Permanently remove deleted items from selected mailbox.
|
|---|
| 414 |
|
|---|
| 415 | Generates 'EXPUNGE' response for each deleted message.
|
|---|
| 416 |
|
|---|
| 417 | (typ, [data]) = <instance>.expunge()
|
|---|
| 418 |
|
|---|
| 419 | 'data' is list of 'EXPUNGE'd message numbers in order received.
|
|---|
| 420 | """
|
|---|
| 421 | name = 'EXPUNGE'
|
|---|
| 422 | typ, dat = self._simple_command(name)
|
|---|
| 423 | return self._untagged_response(typ, dat, name)
|
|---|
| 424 |
|
|---|
| 425 |
|
|---|
| 426 | def fetch(self, message_set, message_parts):
|
|---|
| 427 | """Fetch (parts of) messages.
|
|---|
| 428 |
|
|---|
| 429 | (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
|
|---|
| 430 |
|
|---|
| 431 | 'message_parts' should be a string of selected parts
|
|---|
| 432 | enclosed in parentheses, eg: "(UID BODY[TEXT])".
|
|---|
| 433 |
|
|---|
| 434 | 'data' are tuples of message part envelope and data.
|
|---|
| 435 | """
|
|---|
| 436 | name = 'FETCH'
|
|---|
| 437 | typ, dat = self._simple_command(name, message_set, message_parts)
|
|---|
| 438 | return self._untagged_response(typ, dat, name)
|
|---|
| 439 |
|
|---|
| 440 |
|
|---|
| 441 | def getacl(self, mailbox):
|
|---|
| 442 | """Get the ACLs for a mailbox.
|
|---|
| 443 |
|
|---|
| 444 | (typ, [data]) = <instance>.getacl(mailbox)
|
|---|
| 445 | """
|
|---|
| 446 | typ, dat = self._simple_command('GETACL', mailbox)
|
|---|
| 447 | return self._untagged_response(typ, dat, 'ACL')
|
|---|
| 448 |
|
|---|
| 449 |
|
|---|
| 450 | def getannotation(self, mailbox, entry, attribute):
|
|---|
| 451 | """(typ, [data]) = <instance>.getannotation(mailbox, entry, attribute)
|
|---|
| 452 | Retrieve ANNOTATIONs."""
|
|---|
| 453 |
|
|---|
| 454 | typ, dat = self._simple_command('GETANNOTATION', mailbox, entry, attribute)
|
|---|
| 455 | return self._untagged_response(typ, dat, 'ANNOTATION')
|
|---|
| 456 |
|
|---|
| 457 |
|
|---|
| 458 | def getquota(self, root):
|
|---|
| 459 | """Get the quota root's resource usage and limits.
|
|---|
| 460 |
|
|---|
| 461 | Part of the IMAP4 QUOTA extension defined in rfc2087.
|
|---|
| 462 |
|
|---|
| 463 | (typ, [data]) = <instance>.getquota(root)
|
|---|
| 464 | """
|
|---|
| 465 | typ, dat = self._simple_command('GETQUOTA', root)
|
|---|
| 466 | return self._untagged_response(typ, dat, 'QUOTA')
|
|---|
| 467 |
|
|---|
| 468 |
|
|---|
| 469 | def getquotaroot(self, mailbox):
|
|---|
| 470 | """Get the list of quota roots for the named mailbox.
|
|---|
| 471 |
|
|---|
| 472 | (typ, [[QUOTAROOT responses...], [QUOTA responses]]) = <instance>.getquotaroot(mailbox)
|
|---|
| 473 | """
|
|---|
| 474 | typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
|
|---|
| 475 | typ, quota = self._untagged_response(typ, dat, 'QUOTA')
|
|---|
| 476 | typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
|
|---|
| 477 | return typ, [quotaroot, quota]
|
|---|
| 478 |
|
|---|
| 479 |
|
|---|
| 480 | def list(self, directory='""', pattern='*'):
|
|---|
| 481 | """List mailbox names in directory matching pattern.
|
|---|
| 482 |
|
|---|
| 483 | (typ, [data]) = <instance>.list(directory='""', pattern='*')
|
|---|
| 484 |
|
|---|
| 485 | 'data' is list of LIST responses.
|
|---|
| 486 | """
|
|---|
| 487 | name = 'LIST'
|
|---|
| 488 | typ, dat = self._simple_command(name, directory, pattern)
|
|---|
| 489 | return self._untagged_response(typ, dat, name)
|
|---|
| 490 |
|
|---|
| 491 |
|
|---|
| 492 | def login(self, user, password):
|
|---|
| 493 | """Identify client using plaintext password.
|
|---|
| 494 |
|
|---|
| 495 | (typ, [data]) = <instance>.login(user, password)
|
|---|
| 496 |
|
|---|
| 497 | NB: 'password' will be quoted.
|
|---|
| 498 | """
|
|---|
| 499 | typ, dat = self._simple_command('LOGIN', user, self._quote(password))
|
|---|
| 500 | if typ != 'OK':
|
|---|
| 501 | raise self.error(dat[-1])
|
|---|
| 502 | self.state = 'AUTH'
|
|---|
| 503 | return typ, dat
|
|---|
| 504 |
|
|---|
| 505 |
|
|---|
| 506 | def login_cram_md5(self, user, password):
|
|---|
| 507 | """ Force use of CRAM-MD5 authentication.
|
|---|
| 508 |
|
|---|
| 509 | (typ, [data]) = <instance>.login_cram_md5(user, password)
|
|---|
| 510 | """
|
|---|
| 511 | self.user, self.password = user, password
|
|---|
| 512 | return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH)
|
|---|
| 513 |
|
|---|
| 514 |
|
|---|
| 515 | def _CRAM_MD5_AUTH(self, challenge):
|
|---|
| 516 | """ Authobject to use with CRAM-MD5 authentication. """
|
|---|
| 517 | import hmac
|
|---|
| 518 | return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
|
|---|
| 519 |
|
|---|
| 520 |
|
|---|
| 521 | def logout(self):
|
|---|
| 522 | """Shutdown connection to server.
|
|---|
| 523 |
|
|---|
| 524 | (typ, [data]) = <instance>.logout()
|
|---|
| 525 |
|
|---|
| 526 | Returns server 'BYE' response.
|
|---|
| 527 | """
|
|---|
| 528 | self.state = 'LOGOUT'
|
|---|
| 529 | try: typ, dat = self._simple_command('LOGOUT')
|
|---|
| 530 | except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
|
|---|
| 531 | self.shutdown()
|
|---|
| 532 | if 'BYE' in self.untagged_responses:
|
|---|
| 533 | return 'BYE', self.untagged_responses['BYE']
|
|---|
| 534 | return typ, dat
|
|---|
| 535 |
|
|---|
| 536 |
|
|---|
| 537 | def lsub(self, directory='""', pattern='*'):
|
|---|
| 538 | """List 'subscribed' mailbox names in directory matching pattern.
|
|---|
| 539 |
|
|---|
| 540 | (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
|
|---|
| 541 |
|
|---|
| 542 | 'data' are tuples of message part envelope and data.
|
|---|
| 543 | """
|
|---|
| 544 | name = 'LSUB'
|
|---|
| 545 | typ, dat = self._simple_command(name, directory, pattern)
|
|---|
| 546 | return self._untagged_response(typ, dat, name)
|
|---|
| 547 |
|
|---|
| 548 | def myrights(self, mailbox):
|
|---|
| 549 | """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
|
|---|
| 550 |
|
|---|
| 551 | (typ, [data]) = <instance>.myrights(mailbox)
|
|---|
| 552 | """
|
|---|
| 553 | typ,dat = self._simple_command('MYRIGHTS', mailbox)
|
|---|
| 554 | return self._untagged_response(typ, dat, 'MYRIGHTS')
|
|---|
| 555 |
|
|---|
| 556 | def namespace(self):
|
|---|
| 557 | """ Returns IMAP namespaces ala rfc2342
|
|---|
| 558 |
|
|---|
| 559 | (typ, [data, ...]) = <instance>.namespace()
|
|---|
| 560 | """
|
|---|
| 561 | name = 'NAMESPACE'
|
|---|
| 562 | typ, dat = self._simple_command(name)
|
|---|
| 563 | return self._untagged_response(typ, dat, name)
|
|---|
| 564 |
|
|---|
| 565 |
|
|---|
| 566 | def noop(self):
|
|---|
| 567 | """Send NOOP command.
|
|---|
| 568 |
|
|---|
| 569 | (typ, [data]) = <instance>.noop()
|
|---|
| 570 | """
|
|---|
| 571 | if __debug__:
|
|---|
| 572 | if self.debug >= 3:
|
|---|
| 573 | self._dump_ur(self.untagged_responses)
|
|---|
| 574 | return self._simple_command('NOOP')
|
|---|
| 575 |
|
|---|
| 576 |
|
|---|
| 577 | def partial(self, message_num, message_part, start, length):
|
|---|
| 578 | """Fetch truncated part of a message.
|
|---|
| 579 |
|
|---|
| 580 | (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
|
|---|
| 581 |
|
|---|
| 582 | 'data' is tuple of message part envelope and data.
|
|---|
| 583 | """
|
|---|
| 584 | name = 'PARTIAL'
|
|---|
| 585 | typ, dat = self._simple_command(name, message_num, message_part, start, length)
|
|---|
| 586 | return self._untagged_response(typ, dat, 'FETCH')
|
|---|
| 587 |
|
|---|
| 588 |
|
|---|
| 589 | def proxyauth(self, user):
|
|---|
| 590 | """Assume authentication as "user".
|
|---|
| 591 |
|
|---|
| 592 | Allows an authorised administrator to proxy into any user's
|
|---|
| 593 | mailbox.
|
|---|
| 594 |
|
|---|
| 595 | (typ, [data]) = <instance>.proxyauth(user)
|
|---|
| 596 | """
|
|---|
| 597 |
|
|---|
| 598 | name = 'PROXYAUTH'
|
|---|
| 599 | return self._simple_command('PROXYAUTH', user)
|
|---|
| 600 |
|
|---|
| 601 |
|
|---|
| 602 | def rename(self, oldmailbox, newmailbox):
|
|---|
| 603 | """Rename old mailbox name to new.
|
|---|
| 604 |
|
|---|
| 605 | (typ, [data]) = <instance>.rename(oldmailbox, newmailbox)
|
|---|
| 606 | """
|
|---|
| 607 | return self._simple_command('RENAME', oldmailbox, newmailbox)
|
|---|
| 608 |
|
|---|
| 609 |
|
|---|
| 610 | def search(self, charset, *criteria):
|
|---|
| 611 | """Search mailbox for matching messages.
|
|---|
| 612 |
|
|---|
| 613 | (typ, [data]) = <instance>.search(charset, criterion, ...)
|
|---|
| 614 |
|
|---|
| 615 | 'data' is space separated list of matching message numbers.
|
|---|
| 616 | """
|
|---|
| 617 | name = 'SEARCH'
|
|---|
| 618 | if charset:
|
|---|
| 619 | typ, dat = self._simple_command(name, 'CHARSET', charset, *criteria)
|
|---|
| 620 | else:
|
|---|
| 621 | typ, dat = self._simple_command(name, *criteria)
|
|---|
| 622 | return self._untagged_response(typ, dat, name)
|
|---|
| 623 |
|
|---|
| 624 |
|
|---|
| 625 | def select(self, mailbox='INBOX', readonly=False):
|
|---|
| 626 | """Select a mailbox.
|
|---|
| 627 |
|
|---|
| 628 | Flush all untagged responses.
|
|---|
| 629 |
|
|---|
| 630 | (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=False)
|
|---|
| 631 |
|
|---|
| 632 | 'data' is count of messages in mailbox ('EXISTS' response).
|
|---|
| 633 |
|
|---|
| 634 | Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
|
|---|
| 635 | other responses should be obtained via <instance>.response('FLAGS') etc.
|
|---|
| 636 | """
|
|---|
| 637 | self.untagged_responses = {} # Flush old responses.
|
|---|
| 638 | self.is_readonly = readonly
|
|---|
| 639 | if readonly:
|
|---|
| 640 | name = 'EXAMINE'
|
|---|
| 641 | else:
|
|---|
| 642 | name = 'SELECT'
|
|---|
| 643 | typ, dat = self._simple_command(name, mailbox)
|
|---|
| 644 | if typ != 'OK':
|
|---|
| 645 | self.state = 'AUTH' # Might have been 'SELECTED'
|
|---|
| 646 | return typ, dat
|
|---|
| 647 | self.state = 'SELECTED'
|
|---|
| 648 | if 'READ-ONLY' in self.untagged_responses \
|
|---|
| 649 | and not readonly:
|
|---|
| 650 | if __debug__:
|
|---|
| 651 | if self.debug >= 1:
|
|---|
| 652 | self._dump_ur(self.untagged_responses)
|
|---|
| 653 | raise self.readonly('%s is not writable' % mailbox)
|
|---|
| 654 | return typ, self.untagged_responses.get('EXISTS', [None])
|
|---|
| 655 |
|
|---|
| 656 |
|
|---|
| 657 | def setacl(self, mailbox, who, what):
|
|---|
| 658 | """Set a mailbox acl.
|
|---|
| 659 |
|
|---|
| 660 | (typ, [data]) = <instance>.setacl(mailbox, who, what)
|
|---|
| 661 | """
|
|---|
| 662 | return self._simple_command('SETACL', mailbox, who, what)
|
|---|
| 663 |
|
|---|
| 664 |
|
|---|
| 665 | def setannotation(self, *args):
|
|---|
| 666 | """(typ, [data]) = <instance>.setannotation(mailbox[, entry, attribute]+)
|
|---|
| 667 | Set ANNOTATIONs."""
|
|---|
| 668 |
|
|---|
| 669 | typ, dat = self._simple_command('SETANNOTATION', *args)
|
|---|
| 670 | return self._untagged_response(typ, dat, 'ANNOTATION')
|
|---|
| 671 |
|
|---|
| 672 |
|
|---|
| 673 | def setquota(self, root, limits):
|
|---|
| 674 | """Set the quota root's resource limits.
|
|---|
| 675 |
|
|---|
| 676 | (typ, [data]) = <instance>.setquota(root, limits)
|
|---|
| 677 | """
|
|---|
| 678 | typ, dat = self._simple_command('SETQUOTA', root, limits)
|
|---|
| 679 | return self._untagged_response(typ, dat, 'QUOTA')
|
|---|
| 680 |
|
|---|
| 681 |
|
|---|
| 682 | def sort(self, sort_criteria, charset, *search_criteria):
|
|---|
| 683 | """IMAP4rev1 extension SORT command.
|
|---|
| 684 |
|
|---|
| 685 | (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
|
|---|
| 686 | """
|
|---|
| 687 | name = 'SORT'
|
|---|
| 688 | #if not name in self.capabilities: # Let the server decide!
|
|---|
| 689 | # raise self.error('unimplemented extension command: %s' % name)
|
|---|
| 690 | if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
|
|---|
| 691 | sort_criteria = '(%s)' % sort_criteria
|
|---|
| 692 | typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
|
|---|
| 693 | return self._untagged_response(typ, dat, name)
|
|---|
| 694 |
|
|---|
| 695 |
|
|---|
| 696 | def status(self, mailbox, names):
|
|---|
| 697 | """Request named status conditions for mailbox.
|
|---|
| 698 |
|
|---|
| 699 | (typ, [data]) = <instance>.status(mailbox, names)
|
|---|
| 700 | """
|
|---|
| 701 | name = 'STATUS'
|
|---|
| 702 | #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
|
|---|
| 703 | # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
|
|---|
| 704 | typ, dat = self._simple_command(name, mailbox, names)
|
|---|
| 705 | return self._untagged_response(typ, dat, name)
|
|---|
| 706 |
|
|---|
| 707 |
|
|---|
| 708 | def store(self, message_set, command, flags):
|
|---|
| 709 | """Alters flag dispositions for messages in mailbox.
|
|---|
| 710 |
|
|---|
| 711 | (typ, [data]) = <instance>.store(message_set, command, flags)
|
|---|
| 712 | """
|
|---|
| 713 | if (flags[0],flags[-1]) != ('(',')'):
|
|---|
| 714 | flags = '(%s)' % flags # Avoid quoting the flags
|
|---|
| 715 | typ, dat = self._simple_command('STORE', message_set, command, flags)
|
|---|
| 716 | return self._untagged_response(typ, dat, 'FETCH')
|
|---|
| 717 |
|
|---|
| 718 |
|
|---|
| 719 | def subscribe(self, mailbox):
|
|---|
| 720 | """Subscribe to new mailbox.
|
|---|
| 721 |
|
|---|
| 722 | (typ, [data]) = <instance>.subscribe(mailbox)
|
|---|
| 723 | """
|
|---|
| 724 | return self._simple_command('SUBSCRIBE', mailbox)
|
|---|
| 725 |
|
|---|
| 726 |
|
|---|
| 727 | def thread(self, threading_algorithm, charset, *search_criteria):
|
|---|
| 728 | """IMAPrev1 extension THREAD command.
|
|---|
| 729 |
|
|---|
| 730 | (type, [data]) = <instance>.thread(threading_alogrithm, charset, search_criteria, ...)
|
|---|
| 731 | """
|
|---|
| 732 | name = 'THREAD'
|
|---|
| 733 | typ, dat = self._simple_command(name, threading_algorithm, charset, *search_criteria)
|
|---|
| 734 | return self._untagged_response(typ, dat, name)
|
|---|
| 735 |
|
|---|
| 736 |
|
|---|
| 737 | def uid(self, command, *args):
|
|---|
| 738 | """Execute "command arg ..." with messages identified by UID,
|
|---|
| 739 | rather than message number.
|
|---|
| 740 |
|
|---|
| 741 | (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
|
|---|
| 742 |
|
|---|
| 743 | Returns response appropriate to 'command'.
|
|---|
| 744 | """
|
|---|
| 745 | command = command.upper()
|
|---|
| 746 | if not command in Commands:
|
|---|
| 747 | raise self.error("Unknown IMAP4 UID command: %s" % command)
|
|---|
| 748 | if self.state not in Commands[command]:
|
|---|
| 749 | raise self.error('command %s illegal in state %s'
|
|---|
| 750 | % (command, self.state))
|
|---|
| 751 | name = 'UID'
|
|---|
| 752 | typ, dat = self._simple_command(name, command, *args)
|
|---|
| 753 | if command in ('SEARCH', 'SORT'):
|
|---|
| 754 | name = command
|
|---|
| 755 | else:
|
|---|
| 756 | name = 'FETCH'
|
|---|
| 757 | return self._untagged_response(typ, dat, name)
|
|---|
| 758 |
|
|---|
| 759 |
|
|---|
| 760 | def unsubscribe(self, mailbox):
|
|---|
| 761 | """Unsubscribe from old mailbox.
|
|---|
| 762 |
|
|---|
| 763 | (typ, [data]) = <instance>.unsubscribe(mailbox)
|
|---|
| 764 | """
|
|---|
| 765 | return self._simple_command('UNSUBSCRIBE', mailbox)
|
|---|
| 766 |
|
|---|
| 767 |
|
|---|
| 768 | def xatom(self, name, *args):
|
|---|
| 769 | """Allow simple extension commands
|
|---|
| 770 | notified by server in CAPABILITY response.
|
|---|
| 771 |
|
|---|
| 772 | Assumes command is legal in current state.
|
|---|
| 773 |
|
|---|
| 774 | (typ, [data]) = <instance>.xatom(name, arg, ...)
|
|---|
| 775 |
|
|---|
| 776 | Returns response appropriate to extension command `name'.
|
|---|
| 777 | """
|
|---|
| 778 | name = name.upper()
|
|---|
| 779 | #if not name in self.capabilities: # Let the server decide!
|
|---|
| 780 | # raise self.error('unknown extension command: %s' % name)
|
|---|
| 781 | if not name in Commands:
|
|---|
| 782 | Commands[name] = (self.state,)
|
|---|
| 783 | return self._simple_command(name, *args)
|
|---|
| 784 |
|
|---|
| 785 |
|
|---|
| 786 |
|
|---|
| 787 | # Private methods
|
|---|
| 788 |
|
|---|
| 789 |
|
|---|
| 790 | def _append_untagged(self, typ, dat):
|
|---|
| 791 |
|
|---|
| 792 | if dat is None: dat = ''
|
|---|
| 793 | ur = self.untagged_responses
|
|---|
| 794 | if __debug__:
|
|---|
| 795 | if self.debug >= 5:
|
|---|
| 796 | self._mesg('untagged_responses[%s] %s += ["%s"]' %
|
|---|
| 797 | (typ, len(ur.get(typ,'')), dat))
|
|---|
| 798 | if typ in ur:
|
|---|
| 799 | ur[typ].append(dat)
|
|---|
| 800 | else:
|
|---|
| 801 | ur[typ] = [dat]
|
|---|
| 802 |
|
|---|
| 803 |
|
|---|
| 804 | def _check_bye(self):
|
|---|
| 805 | bye = self.untagged_responses.get('BYE')
|
|---|
| 806 | if bye:
|
|---|
| 807 | raise self.abort(bye[-1])
|
|---|
| 808 |
|
|---|
| 809 |
|
|---|
| 810 | def _command(self, name, *args):
|
|---|
| 811 |
|
|---|
| 812 | if self.state not in Commands[name]:
|
|---|
| 813 | self.literal = None
|
|---|
| 814 | raise self.error(
|
|---|
| 815 | 'command %s illegal in state %s' % (name, self.state))
|
|---|
| 816 |
|
|---|
| 817 | for typ in ('OK', 'NO', 'BAD'):
|
|---|
| 818 | if typ in self.untagged_responses:
|
|---|
| 819 | del self.untagged_responses[typ]
|
|---|
| 820 |
|
|---|
| 821 | if 'READ-ONLY' in self.untagged_responses \
|
|---|
| 822 | and not self.is_readonly:
|
|---|
| 823 | raise self.readonly('mailbox status changed to READ-ONLY')
|
|---|
| 824 |
|
|---|
| 825 | tag = self._new_tag()
|
|---|
| 826 | data = '%s %s' % (tag, name)
|
|---|
| 827 | for arg in args:
|
|---|
| 828 | if arg is None: continue
|
|---|
| 829 | data = '%s %s' % (data, self._checkquote(arg))
|
|---|
| 830 |
|
|---|
| 831 | literal = self.literal
|
|---|
| 832 | if literal is not None:
|
|---|
| 833 | self.literal = None
|
|---|
| 834 | if type(literal) is type(self._command):
|
|---|
| 835 | literator = literal
|
|---|
| 836 | else:
|
|---|
| 837 | literator = None
|
|---|
| 838 | data = '%s {%s}' % (data, len(literal))
|
|---|
| 839 |
|
|---|
| 840 | if __debug__:
|
|---|
| 841 | if self.debug >= 4:
|
|---|
| 842 | self._mesg('> %s' % data)
|
|---|
| 843 | else:
|
|---|
| 844 | self._log('> %s' % data)
|
|---|
| 845 |
|
|---|
| 846 | try:
|
|---|
| 847 | self.send('%s%s' % (data, CRLF))
|
|---|
| 848 | except (socket.error, OSError), val:
|
|---|
| 849 | raise self.abort('socket error: %s' % val)
|
|---|
| 850 |
|
|---|
| 851 | if literal is None:
|
|---|
| 852 | return tag
|
|---|
| 853 |
|
|---|
| 854 | while 1:
|
|---|
| 855 | # Wait for continuation response
|
|---|
| 856 |
|
|---|
| 857 | while self._get_response():
|
|---|
| 858 | if self.tagged_commands[tag]: # BAD/NO?
|
|---|
| 859 | return tag
|
|---|
| 860 |
|
|---|
| 861 | # Send literal
|
|---|
| 862 |
|
|---|
| 863 | if literator:
|
|---|
| 864 | literal = literator(self.continuation_response)
|
|---|
| 865 |
|
|---|
| 866 | if __debug__:
|
|---|
| 867 | if self.debug >= 4:
|
|---|
| 868 | self._mesg('write literal size %s' % len(literal))
|
|---|
| 869 |
|
|---|
| 870 | try:
|
|---|
| 871 | self.send(literal)
|
|---|
| 872 | self.send(CRLF)
|
|---|
| 873 | except (socket.error, OSError), val:
|
|---|
| 874 | raise self.abort('socket error: %s' % val)
|
|---|
| 875 |
|
|---|
| 876 | if not literator:
|
|---|
| 877 | break
|
|---|
| 878 |
|
|---|
| 879 | return tag
|
|---|
| 880 |
|
|---|
| 881 |
|
|---|
| 882 | def _command_complete(self, name, tag):
|
|---|
| 883 | self._check_bye()
|
|---|
| 884 | try:
|
|---|
| 885 | typ, data = self._get_tagged_response(tag)
|
|---|
| 886 | except self.abort, val:
|
|---|
| 887 | raise self.abort('command: %s => %s' % (name, val))
|
|---|
| 888 | except self.error, val:
|
|---|
| 889 | raise self.error('command: %s => %s' % (name, val))
|
|---|
| 890 | self._check_bye()
|
|---|
| 891 | if typ == 'BAD':
|
|---|
| 892 | raise self.error('%s command error: %s %s' % (name, typ, data))
|
|---|
| 893 | return typ, data
|
|---|
| 894 |
|
|---|
| 895 |
|
|---|
| 896 | def _get_response(self):
|
|---|
| 897 |
|
|---|
| 898 | # Read response and store.
|
|---|
| 899 | #
|
|---|
| 900 | # Returns None for continuation responses,
|
|---|
| 901 | # otherwise first response line received.
|
|---|
| 902 |
|
|---|
| 903 | resp = self._get_line()
|
|---|
| 904 |
|
|---|
| 905 | # Command completion response?
|
|---|
| 906 |
|
|---|
| 907 | if self._match(self.tagre, resp):
|
|---|
| 908 | tag = self.mo.group('tag')
|
|---|
| 909 | if not tag in self.tagged_commands:
|
|---|
| 910 | raise self.abort('unexpected tagged response: %s' % resp)
|
|---|
| 911 |
|
|---|
| 912 | typ = self.mo.group('type')
|
|---|
| 913 | dat = self.mo.group('data')
|
|---|
| 914 | self.tagged_commands[tag] = (typ, [dat])
|
|---|
| 915 | else:
|
|---|
| 916 | dat2 = None
|
|---|
| 917 |
|
|---|
| 918 | # '*' (untagged) responses?
|
|---|
| 919 |
|
|---|
| 920 | if not self._match(Untagged_response, resp):
|
|---|
| 921 | if self._match(Untagged_status, resp):
|
|---|
| 922 | dat2 = self.mo.group('data2')
|
|---|
| 923 |
|
|---|
| 924 | if self.mo is None:
|
|---|
| 925 | # Only other possibility is '+' (continuation) response...
|
|---|
| 926 |
|
|---|
| 927 | if self._match(Continuation, resp):
|
|---|
| 928 | self.continuation_response = self.mo.group('data')
|
|---|
| 929 | return None # NB: indicates continuation
|
|---|
| 930 |
|
|---|
| 931 | raise self.abort("unexpected response: '%s'" % resp)
|
|---|
| 932 |
|
|---|
| 933 | typ = self.mo.group('type')
|
|---|
| 934 | dat = self.mo.group('data')
|
|---|
| 935 | if dat is None: dat = '' # Null untagged response
|
|---|
| 936 | if dat2: dat = dat + ' ' + dat2
|
|---|
| 937 |
|
|---|
| 938 | # Is there a literal to come?
|
|---|
| 939 |
|
|---|
| 940 | while self._match(Literal, dat):
|
|---|
| 941 |
|
|---|
| 942 | # Read literal direct from connection.
|
|---|
| 943 |
|
|---|
| 944 | size = int(self.mo.group('size'))
|
|---|
| 945 | if __debug__:
|
|---|
| 946 | if self.debug >= 4:
|
|---|
| 947 | self._mesg('read literal size %s' % size)
|
|---|
| 948 | data = self.read(size)
|
|---|
| 949 |
|
|---|
| 950 | # Store response with literal as tuple
|
|---|
| 951 |
|
|---|
| 952 | self._append_untagged(typ, (dat, data))
|
|---|
| 953 |
|
|---|
| 954 | # Read trailer - possibly containing another literal
|
|---|
| 955 |
|
|---|
| 956 | dat = self._get_line()
|
|---|
| 957 |
|
|---|
| 958 | self._append_untagged(typ, dat)
|
|---|
| 959 |
|
|---|
| 960 | # Bracketed response information?
|
|---|
| 961 |
|
|---|
| 962 | if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
|
|---|
| 963 | self._append_untagged(self.mo.group('type'), self.mo.group('data'))
|
|---|
| 964 |
|
|---|
| 965 | if __debug__:
|
|---|
| 966 | if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
|
|---|
| 967 | self._mesg('%s response: %s' % (typ, dat))
|
|---|
| 968 |
|
|---|
| 969 | return resp
|
|---|
| 970 |
|
|---|
| 971 |
|
|---|
| 972 | def _get_tagged_response(self, tag):
|
|---|
| 973 |
|
|---|
| 974 | while 1:
|
|---|
| 975 | result = self.tagged_commands[tag]
|
|---|
| 976 | if result is not None:
|
|---|
| 977 | del self.tagged_commands[tag]
|
|---|
| 978 | return result
|
|---|
| 979 |
|
|---|
| 980 | # Some have reported "unexpected response" exceptions.
|
|---|
| 981 | # Note that ignoring them here causes loops.
|
|---|
| 982 | # Instead, send me details of the unexpected response and
|
|---|
| 983 | # I'll update the code in `_get_response()'.
|
|---|
| 984 |
|
|---|
| 985 | try:
|
|---|
| 986 | self._get_response()
|
|---|
| 987 | except self.abort, val:
|
|---|
| 988 | if __debug__:
|
|---|
| 989 | if self.debug >= 1:
|
|---|
| 990 | self.print_log()
|
|---|
| 991 | raise
|
|---|
| 992 |
|
|---|
| 993 |
|
|---|
| 994 | def _get_line(self):
|
|---|
| 995 |
|
|---|
| 996 | line = self.readline()
|
|---|
| 997 | if not line:
|
|---|
| 998 | raise self.abort('socket error: EOF')
|
|---|
| 999 |
|
|---|
| 1000 | # Protocol mandates all lines terminated by CRLF
|
|---|
| 1001 |
|
|---|
| 1002 | line = line[:-2]
|
|---|
| 1003 | if __debug__:
|
|---|
| 1004 | if self.debug >= 4:
|
|---|
| 1005 | self._mesg('< %s' % line)
|
|---|
| 1006 | else:
|
|---|
| 1007 | self._log('< %s' % line)
|
|---|
| 1008 | return line
|
|---|
| 1009 |
|
|---|
| 1010 |
|
|---|
| 1011 | def _match(self, cre, s):
|
|---|
| 1012 |
|
|---|
| 1013 | # Run compiled regular expression match method on 's'.
|
|---|
| 1014 | # Save result, return success.
|
|---|
| 1015 |
|
|---|
| 1016 | self.mo = cre.match(s)
|
|---|
| 1017 | if __debug__:
|
|---|
| 1018 | if self.mo is not None and self.debug >= 5:
|
|---|
| 1019 | self._mesg("\tmatched r'%s' => %r" % (cre.pattern, self.mo.groups()))
|
|---|
| 1020 | return self.mo is not None
|
|---|
| 1021 |
|
|---|
| 1022 |
|
|---|
| 1023 | def _new_tag(self):
|
|---|
| 1024 |
|
|---|
| 1025 | tag = '%s%s' % (self.tagpre, self.tagnum)
|
|---|
| 1026 | self.tagnum = self.tagnum + 1
|
|---|
| 1027 | self.tagged_commands[tag] = None
|
|---|
| 1028 | return tag
|
|---|
| 1029 |
|
|---|
| 1030 |
|
|---|
| 1031 | def _checkquote(self, arg):
|
|---|
| 1032 |
|
|---|
| 1033 | # Must quote command args if non-alphanumeric chars present,
|
|---|
| 1034 | # and not already quoted.
|
|---|
| 1035 |
|
|---|
| 1036 | if type(arg) is not type(''):
|
|---|
| 1037 | return arg
|
|---|
| 1038 | if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
|
|---|
| 1039 | return arg
|
|---|
| 1040 | if arg and self.mustquote.search(arg) is None:
|
|---|
| 1041 | return arg
|
|---|
| 1042 | return self._quote(arg)
|
|---|
| 1043 |
|
|---|
| 1044 |
|
|---|
| 1045 | def _quote(self, arg):
|
|---|
| 1046 |
|
|---|
| 1047 | arg = arg.replace('\\', '\\\\')
|
|---|
| 1048 | arg = arg.replace('"', '\\"')
|
|---|
| 1049 |
|
|---|
| 1050 | return '"%s"' % arg
|
|---|
| 1051 |
|
|---|
| 1052 |
|
|---|
| 1053 | def _simple_command(self, name, *args):
|
|---|
| 1054 |
|
|---|
| 1055 | return self._command_complete(name, self._command(name, *args))
|
|---|
| 1056 |
|
|---|
| 1057 |
|
|---|
| 1058 | def _untagged_response(self, typ, dat, name):
|
|---|
| 1059 |
|
|---|
| 1060 | if typ == 'NO':
|
|---|
| 1061 | return typ, dat
|
|---|
| 1062 | if not name in self.untagged_responses:
|
|---|
| 1063 | return typ, [None]
|
|---|
| 1064 | data = self.untagged_responses.pop(name)
|
|---|
| 1065 | if __debug__:
|
|---|
| 1066 | if self.debug >= 5:
|
|---|
| 1067 | self._mesg('untagged_responses[%s] => %s' % (name, data))
|
|---|
| 1068 | return typ, data
|
|---|
| 1069 |
|
|---|
| 1070 |
|
|---|
| 1071 | if __debug__:
|
|---|
| 1072 |
|
|---|
| 1073 | def _mesg(self, s, secs=None):
|
|---|
| 1074 | if secs is None:
|
|---|
| 1075 | secs = time.time()
|
|---|
| 1076 | tm = time.strftime('%M:%S', time.localtime(secs))
|
|---|
| 1077 | sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
|
|---|
| 1078 | sys.stderr.flush()
|
|---|
| 1079 |
|
|---|
| 1080 | def _dump_ur(self, dict):
|
|---|
| 1081 | # Dump untagged responses (in `dict').
|
|---|
| 1082 | l = dict.items()
|
|---|
| 1083 | if not l: return
|
|---|
| 1084 | t = '\n\t\t'
|
|---|
| 1085 | l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
|
|---|
| 1086 | self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
|
|---|
| 1087 |
|
|---|
| 1088 | def _log(self, line):
|
|---|
| 1089 | # Keep log of last `_cmd_log_len' interactions for debugging.
|
|---|
| 1090 | self._cmd_log[self._cmd_log_idx] = (line, time.time())
|
|---|
| 1091 | self._cmd_log_idx += 1
|
|---|
| 1092 | if self._cmd_log_idx >= self._cmd_log_len:
|
|---|
| 1093 | self._cmd_log_idx = 0
|
|---|
| 1094 |
|
|---|
| 1095 | def print_log(self):
|
|---|
| 1096 | self._mesg('last %d IMAP4 interactions:' % len(self._cmd_log))
|
|---|
| 1097 | i, n = self._cmd_log_idx, self._cmd_log_len
|
|---|
| 1098 | while n:
|
|---|
| 1099 | try:
|
|---|
| 1100 | self._mesg(*self._cmd_log[i])
|
|---|
| 1101 | except:
|
|---|
| 1102 | pass
|
|---|
| 1103 | i += 1
|
|---|
| 1104 | if i >= self._cmd_log_len:
|
|---|
| 1105 | i = 0
|
|---|
| 1106 | n -= 1
|
|---|
| 1107 |
|
|---|
| 1108 |
|
|---|
| 1109 |
|
|---|
| 1110 | class IMAP4_SSL(IMAP4):
|
|---|
| 1111 |
|
|---|
| 1112 | """IMAP4 client class over SSL connection
|
|---|
| 1113 |
|
|---|
| 1114 | Instantiate with: IMAP4_SSL([host[, port[, keyfile[, certfile]]]])
|
|---|
| 1115 |
|
|---|
| 1116 | host - host's name (default: localhost);
|
|---|
| 1117 | port - port number (default: standard IMAP4 SSL port).
|
|---|
| 1118 | keyfile - PEM formatted file that contains your private key (default: None);
|
|---|
| 1119 | certfile - PEM formatted certificate chain file (default: None);
|
|---|
| 1120 |
|
|---|
| 1121 | for more documentation see the docstring of the parent class IMAP4.
|
|---|
| 1122 | """
|
|---|
| 1123 |
|
|---|
| 1124 |
|
|---|
| 1125 | def __init__(self, host = '', port = IMAP4_SSL_PORT, keyfile = None, certfile = None):
|
|---|
| 1126 | self.keyfile = keyfile
|
|---|
| 1127 | self.certfile = certfile
|
|---|
| 1128 | IMAP4.__init__(self, host, port)
|
|---|
| 1129 |
|
|---|
| 1130 |
|
|---|
| 1131 | def open(self, host = '', port = IMAP4_SSL_PORT):
|
|---|
| 1132 | """Setup connection to remote server on "host:port".
|
|---|
| 1133 | (default: localhost:standard IMAP4 SSL port).
|
|---|
| 1134 | This connection will be used by the routines:
|
|---|
| 1135 | read, readline, send, shutdown.
|
|---|
| 1136 | """
|
|---|
| 1137 | self.host = host
|
|---|
| 1138 | self.port = port
|
|---|
| 1139 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|---|
| 1140 | self.sock.connect((host, port))
|
|---|
| 1141 | self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile)
|
|---|
| 1142 |
|
|---|
| 1143 |
|
|---|
| 1144 | def read(self, size):
|
|---|
| 1145 | """Read 'size' bytes from remote."""
|
|---|
| 1146 | # sslobj.read() sometimes returns < size bytes
|
|---|
| 1147 | chunks = []
|
|---|
| 1148 | read = 0
|
|---|
| 1149 | while read < size:
|
|---|
| 1150 | data = self.sslobj.read(size-read)
|
|---|
| 1151 | read += len(data)
|
|---|
| 1152 | chunks.append(data)
|
|---|
| 1153 |
|
|---|
| 1154 | return ''.join(chunks)
|
|---|
| 1155 |
|
|---|
| 1156 |
|
|---|
| 1157 | def readline(self):
|
|---|
| 1158 | """Read line from remote."""
|
|---|
| 1159 | # NB: socket.ssl needs a "readline" method, or perhaps a "makefile" method.
|
|---|
| 1160 | line = []
|
|---|
| 1161 | while 1:
|
|---|
| 1162 | char = self.sslobj.read(1)
|
|---|
| 1163 | line.append(char)
|
|---|
| 1164 | if char == "\n": return ''.join(line)
|
|---|
| 1165 |
|
|---|
| 1166 |
|
|---|
| 1167 | def send(self, data):
|
|---|
| 1168 | """Send data to remote."""
|
|---|
| 1169 | # NB: socket.ssl needs a "sendall" method to match socket objects.
|
|---|
| 1170 | bytes = len(data)
|
|---|
| 1171 | while bytes > 0:
|
|---|
| 1172 | sent = self.sslobj.write(data)
|
|---|
| 1173 | if sent == bytes:
|
|---|
| 1174 | break # avoid copy
|
|---|
| 1175 | data = data[sent:]
|
|---|
| 1176 | bytes = bytes - sent
|
|---|
| 1177 |
|
|---|
| 1178 |
|
|---|
| 1179 | def shutdown(self):
|
|---|
| 1180 | """Close I/O established in "open"."""
|
|---|
| 1181 | self.sock.close()
|
|---|
| 1182 |
|
|---|
| 1183 |
|
|---|
| 1184 | def socket(self):
|
|---|
| 1185 | """Return socket instance used to connect to IMAP4 server.
|
|---|
| 1186 |
|
|---|
| 1187 | socket = <instance>.socket()
|
|---|
| 1188 | """
|
|---|
| 1189 | return self.sock
|
|---|
| 1190 |
|
|---|
| 1191 |
|
|---|
| 1192 | def ssl(self):
|
|---|
| 1193 | """Return SSLObject instance used to communicate with the IMAP4 server.
|
|---|
| 1194 |
|
|---|
| 1195 | ssl = <instance>.socket.ssl()
|
|---|
| 1196 | """
|
|---|
| 1197 | return self.sslobj
|
|---|
| 1198 |
|
|---|
| 1199 |
|
|---|
| 1200 |
|
|---|
| 1201 | class IMAP4_stream(IMAP4):
|
|---|
| 1202 |
|
|---|
| 1203 | """IMAP4 client class over a stream
|
|---|
| 1204 |
|
|---|
| 1205 | Instantiate with: IMAP4_stream(command)
|
|---|
| 1206 |
|
|---|
| 1207 | where "command" is a string that can be passed to os.popen2()
|
|---|
| 1208 |
|
|---|
| 1209 | for more documentation see the docstring of the parent class IMAP4.
|
|---|
| 1210 | """
|
|---|
| 1211 |
|
|---|
| 1212 |
|
|---|
| 1213 | def __init__(self, command):
|
|---|
| 1214 | self.command = command
|
|---|
| 1215 | IMAP4.__init__(self)
|
|---|
| 1216 |
|
|---|
| 1217 |
|
|---|
| 1218 | def open(self, host = None, port = None):
|
|---|
| 1219 | """Setup a stream connection.
|
|---|
| 1220 | This connection will be used by the routines:
|
|---|
| 1221 | read, readline, send, shutdown.
|
|---|
| 1222 | """
|
|---|
| 1223 | self.host = None # For compatibility with parent class
|
|---|
| 1224 | self.port = None
|
|---|
| 1225 | self.sock = None
|
|---|
| 1226 | self.file = None
|
|---|
| 1227 | self.writefile, self.readfile = os.popen2(self.command)
|
|---|
| 1228 |
|
|---|
| 1229 |
|
|---|
| 1230 | def read(self, size):
|
|---|
| 1231 | """Read 'size' bytes from remote."""
|
|---|
| 1232 | return self.readfile.read(size)
|
|---|
| 1233 |
|
|---|
| 1234 |
|
|---|
| 1235 | def readline(self):
|
|---|
| 1236 | """Read line from remote."""
|
|---|
| 1237 | return self.readfile.readline()
|
|---|
| 1238 |
|
|---|
| 1239 |
|
|---|
| 1240 | def send(self, data):
|
|---|
| 1241 | """Send data to remote."""
|
|---|
| 1242 | self.writefile.write(data)
|
|---|
| 1243 | self.writefile.flush()
|
|---|
| 1244 |
|
|---|
| 1245 |
|
|---|
| 1246 | def shutdown(self):
|
|---|
| 1247 | """Close I/O established in "open"."""
|
|---|
| 1248 | self.readfile.close()
|
|---|
| 1249 | self.writefile.close()
|
|---|
| 1250 |
|
|---|
| 1251 |
|
|---|
| 1252 |
|
|---|
| 1253 | class _Authenticator:
|
|---|
| 1254 |
|
|---|
| 1255 | """Private class to provide en/decoding
|
|---|
| 1256 | for base64-based authentication conversation.
|
|---|
| 1257 | """
|
|---|
| 1258 |
|
|---|
| 1259 | def __init__(self, mechinst):
|
|---|
| 1260 | self.mech = mechinst # Callable object to provide/process data
|
|---|
| 1261 |
|
|---|
| 1262 | def process(self, data):
|
|---|
| 1263 | ret = self.mech(self.decode(data))
|
|---|
| 1264 | if ret is None:
|
|---|
| 1265 | return '*' # Abort conversation
|
|---|
| 1266 | return self.encode(ret)
|
|---|
| 1267 |
|
|---|
| 1268 | def encode(self, inp):
|
|---|
| 1269 | #
|
|---|
| 1270 | # Invoke binascii.b2a_base64 iteratively with
|
|---|
| 1271 | # short even length buffers, strip the trailing
|
|---|
| 1272 | # line feed from the result and append. "Even"
|
|---|
| 1273 | # means a number that factors to both 6 and 8,
|
|---|
| 1274 | # so when it gets to the end of the 8-bit input
|
|---|
| 1275 | # there's no partial 6-bit output.
|
|---|
| 1276 | #
|
|---|
| 1277 | oup = ''
|
|---|
| 1278 | while inp:
|
|---|
| 1279 | if len(inp) > 48:
|
|---|
| 1280 | t = inp[:48]
|
|---|
| 1281 | inp = inp[48:]
|
|---|
| 1282 | else:
|
|---|
| 1283 | t = inp
|
|---|
| 1284 | inp = ''
|
|---|
| 1285 | e = binascii.b2a_base64(t)
|
|---|
| 1286 | if e:
|
|---|
| 1287 | oup = oup + e[:-1]
|
|---|
| 1288 | return oup
|
|---|
| 1289 |
|
|---|
| 1290 | def decode(self, inp):
|
|---|
| 1291 | if not inp:
|
|---|
| 1292 | return ''
|
|---|
| 1293 | return binascii.a2b_base64(inp)
|
|---|
| 1294 |
|
|---|
| 1295 |
|
|---|
| 1296 |
|
|---|
| 1297 | Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
|
|---|
| 1298 | 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
|
|---|
| 1299 |
|
|---|
| 1300 | def Internaldate2tuple(resp):
|
|---|
| 1301 | """Convert IMAP4 INTERNALDATE to UT.
|
|---|
| 1302 |
|
|---|
| 1303 | Returns Python time module tuple.
|
|---|
| 1304 | """
|
|---|
| 1305 |
|
|---|
| 1306 | mo = InternalDate.match(resp)
|
|---|
| 1307 | if not mo:
|
|---|
| 1308 | return None
|
|---|
| 1309 |
|
|---|
| 1310 | mon = Mon2num[mo.group('mon')]
|
|---|
| 1311 | zonen = mo.group('zonen')
|
|---|
| 1312 |
|
|---|
| 1313 | day = int(mo.group('day'))
|
|---|
| 1314 | year = int(mo.group('year'))
|
|---|
| 1315 | hour = int(mo.group('hour'))
|
|---|
| 1316 | min = int(mo.group('min'))
|
|---|
| 1317 | sec = int(mo.group('sec'))
|
|---|
| 1318 | zoneh = int(mo.group('zoneh'))
|
|---|
| 1319 | zonem = int(mo.group('zonem'))
|
|---|
| 1320 |
|
|---|
| 1321 | # INTERNALDATE timezone must be subtracted to get UT
|
|---|
| 1322 |
|
|---|
| 1323 | zone = (zoneh*60 + zonem)*60
|
|---|
| 1324 | if zonen == '-':
|
|---|
| 1325 | zone = -zone
|
|---|
| 1326 |
|
|---|
| 1327 | tt = (year, mon, day, hour, min, sec, -1, -1, -1)
|
|---|
| 1328 |
|
|---|
| 1329 | utc = time.mktime(tt)
|
|---|
| 1330 |
|
|---|
| 1331 | # Following is necessary because the time module has no 'mkgmtime'.
|
|---|
| 1332 | # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
|
|---|
| 1333 |
|
|---|
| 1334 | lt = time.localtime(utc)
|
|---|
| 1335 | if time.daylight and lt[-1]:
|
|---|
| 1336 | zone = zone + time.altzone
|
|---|
| 1337 | else:
|
|---|
| 1338 | zone = zone + time.timezone
|
|---|
| 1339 |
|
|---|
| 1340 | return time.localtime(utc - zone)
|
|---|
| 1341 |
|
|---|
| 1342 |
|
|---|
| 1343 |
|
|---|
| 1344 | def Int2AP(num):
|
|---|
| 1345 |
|
|---|
| 1346 | """Convert integer to A-P string representation."""
|
|---|
| 1347 |
|
|---|
| 1348 | val = ''; AP = 'ABCDEFGHIJKLMNOP'
|
|---|
| 1349 | num = int(abs(num))
|
|---|
| 1350 | while num:
|
|---|
| 1351 | num, mod = divmod(num, 16)
|
|---|
| 1352 | val = AP[mod] + val
|
|---|
| 1353 | return val
|
|---|
| 1354 |
|
|---|
| 1355 |
|
|---|
| 1356 |
|
|---|
| 1357 | def ParseFlags(resp):
|
|---|
| 1358 |
|
|---|
| 1359 | """Convert IMAP4 flags response to python tuple."""
|
|---|
| 1360 |
|
|---|
| 1361 | mo = Flags.match(resp)
|
|---|
| 1362 | if not mo:
|
|---|
| 1363 | return ()
|
|---|
| 1364 |
|
|---|
| 1365 | return tuple(mo.group('flags').split())
|
|---|
| 1366 |
|
|---|
| 1367 |
|
|---|
| 1368 | def Time2Internaldate(date_time):
|
|---|
| 1369 |
|
|---|
| 1370 | """Convert 'date_time' to IMAP4 INTERNALDATE representation.
|
|---|
| 1371 |
|
|---|
| 1372 | Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
|
|---|
| 1373 | """
|
|---|
| 1374 |
|
|---|
| 1375 | if isinstance(date_time, (int, float)):
|
|---|
| 1376 | tt = time.localtime(date_time)
|
|---|
| 1377 | elif isinstance(date_time, (tuple, time.struct_time)):
|
|---|
| 1378 | tt = date_time
|
|---|
| 1379 | elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
|
|---|
| 1380 | return date_time # Assume in correct format
|
|---|
| 1381 | else:
|
|---|
| 1382 | raise ValueError("date_time not of a known type")
|
|---|
| 1383 |
|
|---|
| 1384 | dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
|
|---|
| 1385 | if dt[0] == '0':
|
|---|
| 1386 | dt = ' ' + dt[1:]
|
|---|
| 1387 | if time.daylight and tt[-1]:
|
|---|
| 1388 | zone = -time.altzone
|
|---|
| 1389 | else:
|
|---|
| 1390 | zone = -time.timezone
|
|---|
| 1391 | return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
|
|---|
| 1392 |
|
|---|
| 1393 |
|
|---|
| 1394 |
|
|---|
| 1395 | if __name__ == '__main__':
|
|---|
| 1396 |
|
|---|
| 1397 | # To test: invoke either as 'python imaplib.py [IMAP4_server_hostname]'
|
|---|
| 1398 | # or 'python imaplib.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
|
|---|
| 1399 | # to test the IMAP4_stream class
|
|---|
| 1400 |
|
|---|
| 1401 | import getopt, getpass
|
|---|
| 1402 |
|
|---|
| 1403 | try:
|
|---|
| 1404 | optlist, args = getopt.getopt(sys.argv[1:], 'd:s:')
|
|---|
| 1405 | except getopt.error, val:
|
|---|
| 1406 | optlist, args = (), ()
|
|---|
| 1407 |
|
|---|
| 1408 | stream_command = None
|
|---|
| 1409 | for opt,val in optlist:
|
|---|
| 1410 | if opt == '-d':
|
|---|
| 1411 | Debug = int(val)
|
|---|
| 1412 | elif opt == '-s':
|
|---|
| 1413 | stream_command = val
|
|---|
| 1414 | if not args: args = (stream_command,)
|
|---|
| 1415 |
|
|---|
| 1416 | if not args: args = ('',)
|
|---|
| 1417 |
|
|---|
| 1418 | host = args[0]
|
|---|
| 1419 |
|
|---|
| 1420 | USER = getpass.getuser()
|
|---|
| 1421 | PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
|
|---|
| 1422 |
|
|---|
| 1423 | test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER, 'lf':'\n'}
|
|---|
| 1424 | test_seq1 = (
|
|---|
| 1425 | ('login', (USER, PASSWD)),
|
|---|
| 1426 | ('create', ('/tmp/xxx 1',)),
|
|---|
| 1427 | ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
|
|---|
| 1428 | ('CREATE', ('/tmp/yyz 2',)),
|
|---|
| 1429 | ('append', ('/tmp/yyz 2', None, None, test_mesg)),
|
|---|
| 1430 | ('list', ('/tmp', 'yy*')),
|
|---|
| 1431 | ('select', ('/tmp/yyz 2',)),
|
|---|
| 1432 | ('search', (None, 'SUBJECT', 'test')),
|
|---|
| 1433 | ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
|
|---|
| 1434 | ('store', ('1', 'FLAGS', '(\Deleted)')),
|
|---|
| 1435 | ('namespace', ()),
|
|---|
| 1436 | ('expunge', ()),
|
|---|
| 1437 | ('recent', ()),
|
|---|
| 1438 | ('close', ()),
|
|---|
| 1439 | )
|
|---|
| 1440 |
|
|---|
| 1441 | test_seq2 = (
|
|---|
| 1442 | ('select', ()),
|
|---|
| 1443 | ('response',('UIDVALIDITY',)),
|
|---|
| 1444 | ('uid', ('SEARCH', 'ALL')),
|
|---|
| 1445 | ('response', ('EXISTS',)),
|
|---|
| 1446 | ('append', (None, None, None, test_mesg)),
|
|---|
| 1447 | ('recent', ()),
|
|---|
| 1448 | ('logout', ()),
|
|---|
| 1449 | )
|
|---|
| 1450 |
|
|---|
| 1451 | def run(cmd, args):
|
|---|
| 1452 | M._mesg('%s %s' % (cmd, args))
|
|---|
| 1453 | typ, dat = getattr(M, cmd)(*args)
|
|---|
| 1454 | M._mesg('%s => %s %s' % (cmd, typ, dat))
|
|---|
| 1455 | if typ == 'NO': raise dat[0]
|
|---|
| 1456 | return dat
|
|---|
| 1457 |
|
|---|
| 1458 | try:
|
|---|
| 1459 | if stream_command:
|
|---|
| 1460 | M = IMAP4_stream(stream_command)
|
|---|
| 1461 | else:
|
|---|
| 1462 | M = IMAP4(host)
|
|---|
| 1463 | if M.state == 'AUTH':
|
|---|
| 1464 | test_seq1 = test_seq1[1:] # Login not needed
|
|---|
| 1465 | M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
|
|---|
| 1466 | M._mesg('CAPABILITIES = %r' % (M.capabilities,))
|
|---|
| 1467 |
|
|---|
| 1468 | for cmd,args in test_seq1:
|
|---|
| 1469 | run(cmd, args)
|
|---|
| 1470 |
|
|---|
| 1471 | for ml in run('list', ('/tmp/', 'yy%')):
|
|---|
| 1472 | mo = re.match(r'.*"([^"]+)"$', ml)
|
|---|
| 1473 | if mo: path = mo.group(1)
|
|---|
| 1474 | else: path = ml.split()[-1]
|
|---|
| 1475 | run('delete', (path,))
|
|---|
| 1476 |
|
|---|
| 1477 | for cmd,args in test_seq2:
|
|---|
| 1478 | dat = run(cmd, args)
|
|---|
| 1479 |
|
|---|
| 1480 | if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
|
|---|
| 1481 | continue
|
|---|
| 1482 |
|
|---|
| 1483 | uid = dat[-1].split()
|
|---|
| 1484 | if not uid: continue
|
|---|
| 1485 | run('uid', ('FETCH', '%s' % uid[-1],
|
|---|
| 1486 | '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
|
|---|
| 1487 |
|
|---|
| 1488 | print '\nAll tests OK.'
|
|---|
| 1489 |
|
|---|
| 1490 | except:
|
|---|
| 1491 | print '\nTests failed.'
|
|---|
| 1492 |
|
|---|
| 1493 | if not Debug:
|
|---|
| 1494 | print '''
|
|---|
| 1495 | If you would like to see debugging output,
|
|---|
| 1496 | try: %s -d5
|
|---|
| 1497 | ''' % sys.argv[0]
|
|---|
| 1498 |
|
|---|
| 1499 | raise
|
|---|