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