| 1 | |
|---|
| 2 | |
|---|
| 3 | |
|---|
| 4 | |
|---|
| 5 | import re |
|---|
| 6 | import select |
|---|
| 7 | import socket |
|---|
| 8 | import sys |
|---|
| 9 | import warnings |
|---|
| 10 | from datetime import datetime |
|---|
| 11 | from operator import itemgetter |
|---|
| 12 | |
|---|
| 13 | import imaplib |
|---|
| 14 | import response_lexer |
|---|
| 15 | |
|---|
| 16 | |
|---|
| 17 | try: |
|---|
| 18 | import oauth2 |
|---|
| 19 | except ImportError: |
|---|
| 20 | oauth2 = None |
|---|
| 21 | |
|---|
| 22 | import imap_utf7 |
|---|
| 23 | from fixed_offset import FixedOffset |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | __all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', 'RECENT'] |
|---|
| 27 | |
|---|
| 28 | from response_parser import parse_response, parse_fetch_response |
|---|
| 29 | |
|---|
| 30 | |
|---|
| 31 | if 'XLIST' not in imaplib.Commands: |
|---|
| 32 | imaplib.Commands['XLIST'] = imaplib.Commands['LIST'] |
|---|
| 33 | |
|---|
| 34 | |
|---|
| 35 | if 'IDLE' not in imaplib.Commands: |
|---|
| 36 | imaplib.Commands['IDLE'] = imaplib.Commands['APPEND'] |
|---|
| 37 | |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | DELETED = r'\Deleted' |
|---|
| 41 | SEEN = r'\Seen' |
|---|
| 42 | ANSWERED = r'\Answered' |
|---|
| 43 | FLAGGED = r'\Flagged' |
|---|
| 44 | DRAFT = r'\Draft' |
|---|
| 45 | RECENT = r'\Recent' |
|---|
| 46 | |
|---|
| 47 | class Namespace(tuple): |
|---|
| 48 | def __new__(cls, personal, other, shared): |
|---|
| 49 | return tuple.__new__(cls, (personal, other, shared)) |
|---|
| 50 | |
|---|
| 51 | personal = property(itemgetter(0)) |
|---|
| 52 | other = property(itemgetter(1)) |
|---|
| 53 | shared = property(itemgetter(2)) |
|---|
| 54 | |
|---|
| 55 | |
|---|
| 56 | class IMAPClient(object): |
|---|
| 57 | """ |
|---|
| 58 | A connection to the IMAP server specified by *host* is made when |
|---|
| 59 | the class is instantiated. |
|---|
| 60 | |
|---|
| 61 | *port* defaults to 143, or 993 if *ssl* is ``True``. |
|---|
| 62 | |
|---|
| 63 | If *use_uid* is ``True`` unique message UIDs be used for all calls |
|---|
| 64 | that accept message ids (defaults to ``True``). |
|---|
| 65 | |
|---|
| 66 | If *ssl* is ``True`` an SSL connection will be made (defaults to |
|---|
| 67 | ``False``). |
|---|
| 68 | |
|---|
| 69 | The *normalise_times* attribute specifies whether datetimes |
|---|
| 70 | returned by ``fetch()`` are normalised to the local system time |
|---|
| 71 | and include no timezone information (native), or are datetimes |
|---|
| 72 | that include timezone information (aware). By default |
|---|
| 73 | *normalise_times* is True (times are normalised to the local |
|---|
| 74 | system time). This attribute can be changed between ``fetch()`` |
|---|
| 75 | calls if required. |
|---|
| 76 | |
|---|
| 77 | The *debug* property can be used to enable debug logging. It can |
|---|
| 78 | be set to an integer from 0 to 5 where 0 disables debug output and |
|---|
| 79 | 5 enables full output with wire logging and parsing logs. ``True`` |
|---|
| 80 | and ``False`` can also be assigned where ``True`` sets debug level |
|---|
| 81 | 4. |
|---|
| 82 | |
|---|
| 83 | By default, debug output goes to stderr. The *log_file* attribute |
|---|
| 84 | can be assigned to an alternate file handle for writing debug |
|---|
| 85 | output to. |
|---|
| 86 | """ |
|---|
| 87 | |
|---|
| 88 | Error = imaplib.IMAP4.error |
|---|
| 89 | AbortError = imaplib.IMAP4.abort |
|---|
| 90 | ReadOnlyError = imaplib.IMAP4.readonly |
|---|
| 91 | |
|---|
| 92 | def __init__(self, host, port=None, use_uid=True, ssl=False): |
|---|
| 93 | if port is None: |
|---|
| 94 | port = ssl and 993 or 143 |
|---|
| 95 | |
|---|
| 96 | self.host = host |
|---|
| 97 | self.port = port |
|---|
| 98 | self.ssl = ssl |
|---|
| 99 | self.use_uid = use_uid |
|---|
| 100 | self.folder_encode = True |
|---|
| 101 | self.log_file = sys.stderr |
|---|
| 102 | self.normalise_times = True |
|---|
| 103 | |
|---|
| 104 | self._imap = self._create_IMAP4() |
|---|
| 105 | self._imap._mesg = self._log |
|---|
| 106 | self._idle_tag = None |
|---|
| 107 | |
|---|
| 108 | def _create_IMAP4(self): |
|---|
| 109 | |
|---|
| 110 | ImapClass = self.ssl and imaplib.IMAP4_SSL or imaplib.IMAP4 |
|---|
| 111 | return ImapClass(self.host, self.port) |
|---|
| 112 | |
|---|
| 113 | def login(self, username, password): |
|---|
| 114 | """Login using *username* and *password*, returning the |
|---|
| 115 | server response. |
|---|
| 116 | """ |
|---|
| 117 | typ, data = self._imap.login(username, password) |
|---|
| 118 | self._checkok('login', typ, data) |
|---|
| 119 | return data[0] |
|---|
| 120 | |
|---|
| 121 | def oauth_login(self, url, oauth_token, oauth_token_secret, |
|---|
| 122 | consumer_key='anonymous', consumer_secret='anonymous'): |
|---|
| 123 | """Authenticate using the OAUTH method. |
|---|
| 124 | |
|---|
| 125 | This only works with IMAP servers that support OAUTH (eg. Gmail). |
|---|
| 126 | """ |
|---|
| 127 | if oauth2: |
|---|
| 128 | token = oauth2.Token(oauth_token, oauth_token_secret) |
|---|
| 129 | consumer = oauth2.Consumer(consumer_key, consumer_secret) |
|---|
| 130 | xoauth_callable = lambda x: oauth2.build_xoauth_string(url, consumer, token) |
|---|
| 131 | |
|---|
| 132 | typ, data = self._imap.authenticate('XOAUTH', xoauth_callable) |
|---|
| 133 | self._checkok('authenticate', typ, data) |
|---|
| 134 | return data[0] |
|---|
| 135 | else: |
|---|
| 136 | raise self.Error('The optional oauth2 dependency is needed for oauth authentication') |
|---|
| 137 | |
|---|
| 138 | def logout(self): |
|---|
| 139 | """Logout, returning the server response. |
|---|
| 140 | """ |
|---|
| 141 | typ, data = self._imap.logout() |
|---|
| 142 | self._checkbye('logout', typ, data) |
|---|
| 143 | return data[0] |
|---|
| 144 | |
|---|
| 145 | |
|---|
| 146 | def capabilities(self): |
|---|
| 147 | """Returns the server capability list. |
|---|
| 148 | """ |
|---|
| 149 | return self._imap.capabilities |
|---|
| 150 | |
|---|
| 151 | |
|---|
| 152 | def has_capability(self, capability): |
|---|
| 153 | """Return ``True`` if the IMAP server has the given *capability*. |
|---|
| 154 | """ |
|---|
| 155 | |
|---|
| 156 | |
|---|
| 157 | |
|---|
| 158 | |
|---|
| 159 | |
|---|
| 160 | if capability.upper() in self._imap.capabilities: |
|---|
| 161 | return True |
|---|
| 162 | else: |
|---|
| 163 | return False |
|---|
| 164 | |
|---|
| 165 | def namespace(self): |
|---|
| 166 | """Return the namespace for the account as a (personal, other, |
|---|
| 167 | shared) tuple. |
|---|
| 168 | |
|---|
| 169 | Each element may be None if no namespace of that type exists, |
|---|
| 170 | or a sequence of (prefix, separator) pairs. |
|---|
| 171 | |
|---|
| 172 | For convenience the tuple elements may be accessed |
|---|
| 173 | positionally or using attributes named *personal*, *other* and |
|---|
| 174 | *shared*. |
|---|
| 175 | |
|---|
| 176 | See `RFC 2342 <http://tools.ietf.org/html/rfc2342>`_ for more details. |
|---|
| 177 | """ |
|---|
| 178 | typ, data = self._imap.namespace() |
|---|
| 179 | self._checkok('namespace', typ, data) |
|---|
| 180 | return Namespace(*parse_response(data)) |
|---|
| 181 | |
|---|
| 182 | def get_folder_delimiter(self): |
|---|
| 183 | """Return the folder separator used by the IMAP server. |
|---|
| 184 | |
|---|
| 185 | .. warning:: |
|---|
| 186 | |
|---|
| 187 | The implementation just picks the first folder separator |
|---|
| 188 | from the first namespace returned. This is not |
|---|
| 189 | particularly sensible. Use namespace instead(). |
|---|
| 190 | """ |
|---|
| 191 | warnings.warn(DeprecationWarning('get_folder_delimiter is going away. Use namespace() instead.')) |
|---|
| 192 | for part in self.namespace(): |
|---|
| 193 | for ns in part: |
|---|
| 194 | return ns[1] |
|---|
| 195 | raise self.Error('could not determine folder separator') |
|---|
| 196 | |
|---|
| 197 | def list_folders(self, directory="", pattern="*"): |
|---|
| 198 | """Get a listing of folders on the server as a list of |
|---|
| 199 | ``(flags, delimiter, name)`` tuples. |
|---|
| 200 | |
|---|
| 201 | Calling list_folders with no arguments will list all |
|---|
| 202 | folders. |
|---|
| 203 | |
|---|
| 204 | Specifying *directory* will limit returned folders to that |
|---|
| 205 | base directory. Specifying *pattern* will limit returned |
|---|
| 206 | folders to those with matching names. The wildcards are |
|---|
| 207 | supported in *pattern*. ``*`` matches zero or more of any |
|---|
| 208 | character and ``%`` matches 0 or more characters except the |
|---|
| 209 | folder delimiter. |
|---|
| 210 | |
|---|
| 211 | Folder names are always returned as unicode strings except if |
|---|
| 212 | folder_decode is not set. |
|---|
| 213 | """ |
|---|
| 214 | return self._do_list('LIST', directory, pattern) |
|---|
| 215 | |
|---|
| 216 | def xlist_folders(self, directory="", pattern="*"): |
|---|
| 217 | """Execute the XLIST command, returning ``(flags, delimiter, |
|---|
| 218 | name)`` tuples. |
|---|
| 219 | |
|---|
| 220 | This method returns special flags for each folder and a |
|---|
| 221 | localized name for certain folders (e.g. the name of the |
|---|
| 222 | inbox may be localized and the flags can be used to |
|---|
| 223 | determine the actual inbox, even if the name has been |
|---|
| 224 | localized. |
|---|
| 225 | |
|---|
| 226 | A ``XLIST`` response could look something like:: |
|---|
| 227 | |
|---|
| 228 | [([u'\\HasNoChildren', u'\\Inbox'], '/', u'Inbox'), |
|---|
| 229 | ([u'\\Noselect', u'\\HasChildren'], '/', u'[Gmail]'), |
|---|
| 230 | ([u'\\HasNoChildren', u'\\AllMail'], '/', u'[Gmail]/All Mail'), |
|---|
| 231 | ([u'\\HasNoChildren', u'\\Drafts'], '/', u'[Gmail]/Drafts'), |
|---|
| 232 | ([u'\\HasNoChildren', u'\\Important'], '/', u'[Gmail]/Important'), |
|---|
| 233 | ([u'\\HasNoChildren', u'\\Sent'], '/', u'[Gmail]/Sent Mail'), |
|---|
| 234 | ([u'\\HasNoChildren', u'\\Spam'], '/', u'[Gmail]/Spam'), |
|---|
| 235 | ([u'\\HasNoChildren', u'\\Starred'], '/', u'[Gmail]/Starred'), |
|---|
| 236 | ([u'\\HasNoChildren', u'\\Trash'], '/', u'[Gmail]/Trash')] |
|---|
| 237 | |
|---|
| 238 | This is a Gmail-specific IMAP extension. It is the |
|---|
| 239 | responsibility of the caller to either check for ``XLIST`` in |
|---|
| 240 | the server capabilites, or to handle the error if the server |
|---|
| 241 | doesn't support this extension. |
|---|
| 242 | |
|---|
| 243 | The *directory* and *pattern* arguments are as per |
|---|
| 244 | list_folders(). |
|---|
| 245 | """ |
|---|
| 246 | return self._do_list('XLIST', directory, pattern) |
|---|
| 247 | |
|---|
| 248 | def list_sub_folders(self, directory="", pattern="*"): |
|---|
| 249 | """Return a list of subscribed folders on the server as |
|---|
| 250 | ``(flags, delimiter, name)`` tuples. |
|---|
| 251 | |
|---|
| 252 | The default behaviour will list all subscribed folders. The |
|---|
| 253 | *directory* and *pattern* arguments are as per list_folders(). |
|---|
| 254 | """ |
|---|
| 255 | return self._do_list('LSUB', directory, pattern) |
|---|
| 256 | |
|---|
| 257 | def _do_list(self, cmd, directory, pattern): |
|---|
| 258 | typ, dat = self._imap._simple_command(cmd, directory, pattern) |
|---|
| 259 | self._checkok(cmd, typ, dat) |
|---|
| 260 | typ, dat = self._imap._untagged_response(typ, dat, cmd) |
|---|
| 261 | return self._proc_folder_list(dat) |
|---|
| 262 | |
|---|
| 263 | def _proc_folder_list(self, folder_data): |
|---|
| 264 | |
|---|
| 265 | |
|---|
| 266 | |
|---|
| 267 | folder_data = [item for item in folder_data if item not in ('', None)] |
|---|
| 268 | |
|---|
| 269 | ret = [] |
|---|
| 270 | parsed = parse_response(folder_data) |
|---|
| 271 | while parsed: |
|---|
| 272 | raw_flags, delim, raw_name = parsed[:3] |
|---|
| 273 | parsed = parsed[3:] |
|---|
| 274 | flags = [imap_utf7.decode(flag) for flag in raw_flags] |
|---|
| 275 | ret.append((flags, delim, self._decode_folder_name(raw_name))) |
|---|
| 276 | return ret |
|---|
| 277 | |
|---|
| 278 | def select_folder(self, folder, readonly=False): |
|---|
| 279 | """Set the current folder on the server. |
|---|
| 280 | |
|---|
| 281 | Future calls to methods such as search and fetch will act on |
|---|
| 282 | the selected folder. |
|---|
| 283 | |
|---|
| 284 | Returns a dictionary containing the ``SELECT`` response. At least |
|---|
| 285 | the ``EXISTS``, ``FLAGS`` and ``RECENT`` keys are guaranteed |
|---|
| 286 | to exist. An example:: |
|---|
| 287 | |
|---|
| 288 | {'EXISTS': 3, |
|---|
| 289 | 'FLAGS': ('\\Answered', '\\Flagged', '\\Deleted', ... ), |
|---|
| 290 | 'RECENT': 0, |
|---|
| 291 | 'PERMANENTFLAGS': ('\\Answered', '\\Flagged', '\\Deleted', ... ), |
|---|
| 292 | 'READ-WRITE': True, |
|---|
| 293 | 'UIDNEXT': 11, |
|---|
| 294 | 'UIDVALIDITY': 1239278212} |
|---|
| 295 | """ |
|---|
| 296 | typ, data = self._imap.select(self._encode_folder_name(folder), readonly) |
|---|
| 297 | self._checkok('select', typ, data) |
|---|
| 298 | return self._process_select_response(self._imap.untagged_responses) |
|---|
| 299 | |
|---|
| 300 | def _process_select_response(self, resp): |
|---|
| 301 | out = {} |
|---|
| 302 | for key, value in resp.iteritems(): |
|---|
| 303 | key = key.upper() |
|---|
| 304 | if key == 'OK': |
|---|
| 305 | continue |
|---|
| 306 | elif key in ('EXISTS', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'): |
|---|
| 307 | value = int(value[0]) |
|---|
| 308 | elif key in ('FLAGS', 'PERMANENTFLAGS'): |
|---|
| 309 | value = parse_response(value)[0] |
|---|
| 310 | elif key == 'READ-WRITE': |
|---|
| 311 | value = True |
|---|
| 312 | |
|---|
| 313 | out[key] = value |
|---|
| 314 | return out |
|---|
| 315 | |
|---|
| 316 | def noop(self): |
|---|
| 317 | """Execute the NOOP command. |
|---|
| 318 | |
|---|
| 319 | This command returns immediately, returning any server side |
|---|
| 320 | status updates. It can also be used to reset any auto-logout |
|---|
| 321 | timers. |
|---|
| 322 | |
|---|
| 323 | The return value is the server command response message |
|---|
| 324 | followed by a list of status responses. For example:: |
|---|
| 325 | |
|---|
| 326 | ('NOOP completed.', |
|---|
| 327 | [(4, 'EXISTS'), |
|---|
| 328 | (3, 'FETCH', ('FLAGS', ('bar', 'sne'))), |
|---|
| 329 | (6, 'FETCH', ('FLAGS', ('sne',)))]) |
|---|
| 330 | |
|---|
| 331 | """ |
|---|
| 332 | tag = self._imap._command('NOOP') |
|---|
| 333 | return self._consume_until_tagged_response(tag, 'NOOP') |
|---|
| 334 | |
|---|
| 335 | def idle(self): |
|---|
| 336 | """Put the server into IDLE mode. |
|---|
| 337 | |
|---|
| 338 | In this mode the server will return unsolicited responses |
|---|
| 339 | about changes to the selected mailbox. This method returns |
|---|
| 340 | immediately. Use ``idle_check()`` to look for IDLE responses |
|---|
| 341 | and ``idle_done()`` to stop IDLE mode. |
|---|
| 342 | |
|---|
| 343 | .. note:: |
|---|
| 344 | |
|---|
| 345 | Any other commmands issued while the server is in IDLE |
|---|
| 346 | mode will fail. |
|---|
| 347 | |
|---|
| 348 | See `RFC 2177 <http://tools.ietf.org/html/rfc2177>`_ for more |
|---|
| 349 | information about the IDLE extension. |
|---|
| 350 | """ |
|---|
| 351 | self._idle_tag = self._imap._command('IDLE') |
|---|
| 352 | resp = self._imap._get_response() |
|---|
| 353 | if resp is not None: |
|---|
| 354 | raise self.Error('Unexpected IDLE response: %s' % resp) |
|---|
| 355 | |
|---|
| 356 | def idle_check(self, timeout=None): |
|---|
| 357 | """Check for any IDLE responses sent by the server. |
|---|
| 358 | |
|---|
| 359 | This method should only be called if the server is in IDLE |
|---|
| 360 | mode (see ``idle()``). |
|---|
| 361 | |
|---|
| 362 | By default, this method will block until an IDLE response is |
|---|
| 363 | received. If *timeout* is provided, the call will block for at |
|---|
| 364 | most this many seconds while waiting for an IDLE response. |
|---|
| 365 | |
|---|
| 366 | The return value is a list of received IDLE responses. These |
|---|
| 367 | will be parsed with values converted to appropriate types. For |
|---|
| 368 | example:: |
|---|
| 369 | |
|---|
| 370 | [('OK', 'Still here'), |
|---|
| 371 | (1, 'EXISTS'), |
|---|
| 372 | (1, 'FETCH', ('FLAGS', ('\\NotJunk',)))] |
|---|
| 373 | """ |
|---|
| 374 | |
|---|
| 375 | |
|---|
| 376 | if self.ssl: |
|---|
| 377 | sock = self._imap.sslobj |
|---|
| 378 | else: |
|---|
| 379 | sock = self._imap.sock |
|---|
| 380 | sock.setblocking(0) |
|---|
| 381 | try: |
|---|
| 382 | resps = [] |
|---|
| 383 | rs, _, _ = select.select([sock], [], [], timeout) |
|---|
| 384 | if rs: |
|---|
| 385 | while True: |
|---|
| 386 | try: |
|---|
| 387 | line = self._imap._get_line() |
|---|
| 388 | except (socket.timeout, socket.error): |
|---|
| 389 | break |
|---|
| 390 | else: |
|---|
| 391 | resps.append(_parse_untagged_response(line)) |
|---|
| 392 | return resps |
|---|
| 393 | finally: |
|---|
| 394 | sock.setblocking(1) |
|---|
| 395 | |
|---|
| 396 | def idle_done(self): |
|---|
| 397 | """Take the server out of IDLE mode. |
|---|
| 398 | |
|---|
| 399 | This method should only be called if the server is already in |
|---|
| 400 | IDLE mode. |
|---|
| 401 | |
|---|
| 402 | The return value is of the form ``(command_text, |
|---|
| 403 | idle_responses)`` where *command_text* is the text sent by the |
|---|
| 404 | server when the IDLE command finished (eg. ``'Idle |
|---|
| 405 | terminated'``) and *idle_responses* is a list of parsed idle |
|---|
| 406 | responses received since the last call to ``idle_check()`` (if |
|---|
| 407 | any). These are returned in parsed form as per |
|---|
| 408 | ``idle_check()``. |
|---|
| 409 | """ |
|---|
| 410 | self._imap.send('DONE\r\n') |
|---|
| 411 | return self._consume_until_tagged_response(self._idle_tag, 'IDLE') |
|---|
| 412 | |
|---|
| 413 | def folder_status(self, folder, what=None): |
|---|
| 414 | """Return the status of *folder*. |
|---|
| 415 | |
|---|
| 416 | *what* should be a sequence of status items to query. This |
|---|
| 417 | defaults to ``('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', |
|---|
| 418 | 'UNSEEN')``. |
|---|
| 419 | |
|---|
| 420 | Returns a dictionary of the status items for the folder with |
|---|
| 421 | keys matching *what*. |
|---|
| 422 | """ |
|---|
| 423 | if what is None: |
|---|
| 424 | what = ('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN') |
|---|
| 425 | elif isinstance(what, basestring): |
|---|
| 426 | what = (what,) |
|---|
| 427 | what_ = '(%s)' % (' '.join(what)) |
|---|
| 428 | |
|---|
| 429 | typ, data = self._imap.status(self._encode_folder_name(folder), what_) |
|---|
| 430 | self._checkok('status', typ, data) |
|---|
| 431 | |
|---|
| 432 | match = _re_status.match(data[0]) |
|---|
| 433 | if not match: |
|---|
| 434 | raise self.Error('Could not get the folder status') |
|---|
| 435 | |
|---|
| 436 | out = {} |
|---|
| 437 | status_items = match.group('status_items').strip().split() |
|---|
| 438 | while status_items: |
|---|
| 439 | key = status_items.pop(0) |
|---|
| 440 | value = long(status_items.pop(0)) |
|---|
| 441 | out[key] = value |
|---|
| 442 | return out |
|---|
| 443 | |
|---|
| 444 | def close_folder(self): |
|---|
| 445 | """Close the currently selected folder, returning the server |
|---|
| 446 | response string. |
|---|
| 447 | """ |
|---|
| 448 | typ, data = self._imap.close() |
|---|
| 449 | self._checkok('close', typ, data) |
|---|
| 450 | return data[0] |
|---|
| 451 | |
|---|
| 452 | def create_folder(self, folder): |
|---|
| 453 | """Create *folder* on the server returning the server response string. |
|---|
| 454 | """ |
|---|
| 455 | typ, data = self._imap.create(self._encode_folder_name(folder)) |
|---|
| 456 | self._checkok('create', typ, data) |
|---|
| 457 | return data[0] |
|---|
| 458 | |
|---|
| 459 | def rename_folder(self, old_name, new_name): |
|---|
| 460 | """Change the name of a folder on the server. |
|---|
| 461 | """ |
|---|
| 462 | typ, data = self._imap.rename(self._encode_folder_name(old_name), |
|---|
| 463 | self._encode_folder_name(new_name)) |
|---|
| 464 | self._checkok('rename', typ, data) |
|---|
| 465 | return data[0] |
|---|
| 466 | |
|---|
| 467 | def delete_folder(self, folder): |
|---|
| 468 | """Delete *folder* on the server returning the server response string. |
|---|
| 469 | """ |
|---|
| 470 | typ, data = self._imap.delete(self._encode_folder_name(folder)) |
|---|
| 471 | self._checkok('delete', typ, data) |
|---|
| 472 | return data[0] |
|---|
| 473 | |
|---|
| 474 | def folder_exists(self, folder): |
|---|
| 475 | """Return ``True`` if *folder* exists on the server. |
|---|
| 476 | """ |
|---|
| 477 | typ, data = self._imap.list('', self._encode_folder_name(folder)) |
|---|
| 478 | self._checkok('list', typ, data) |
|---|
| 479 | data = [x for x in data if x] |
|---|
| 480 | return len(data) == 1 and data[0] != None |
|---|
| 481 | |
|---|
| 482 | def subscribe_folder(self, folder): |
|---|
| 483 | """Subscribe to *folder*, returning the server response string. |
|---|
| 484 | """ |
|---|
| 485 | typ, data = self._imap.subscribe(self._encode_folder_name(folder)) |
|---|
| 486 | self._checkok('subscribe', typ, data) |
|---|
| 487 | return data |
|---|
| 488 | |
|---|
| 489 | def unsubscribe_folder(self, folder): |
|---|
| 490 | """Unsubscribe to *folder*, returning the server response string. |
|---|
| 491 | """ |
|---|
| 492 | typ, data = self._imap.unsubscribe(self._encode_folder_name(folder)) |
|---|
| 493 | self._checkok('unsubscribe', typ, data) |
|---|
| 494 | return data |
|---|
| 495 | |
|---|
| 496 | def search(self, criteria='ALL', charset=None): |
|---|
| 497 | """Return a list of messages ids matching *criteria*. |
|---|
| 498 | |
|---|
| 499 | *criteria* should be a list of of one or more criteria |
|---|
| 500 | specifications or a single critera string. Example values |
|---|
| 501 | include:: |
|---|
| 502 | |
|---|
| 503 | 'NOT DELETED' |
|---|
| 504 | 'UNSEEN' |
|---|
| 505 | 'SINCE 1-Feb-2011' |
|---|
| 506 | |
|---|
| 507 | *charset* specifies the character set of the strings in the |
|---|
| 508 | criteria. It defaults to US-ASCII. |
|---|
| 509 | |
|---|
| 510 | See `RFC 3501 section 6.4.4 <http://tools.ietf.org/html/rfc3501#section-6.4.4>`_ |
|---|
| 511 | for more details. |
|---|
| 512 | """ |
|---|
| 513 | if not criteria: |
|---|
| 514 | raise ValueError('no criteria specified') |
|---|
| 515 | |
|---|
| 516 | if isinstance(criteria, basestring): |
|---|
| 517 | criteria = (criteria,) |
|---|
| 518 | crit_list = ['(%s)' % c for c in criteria] |
|---|
| 519 | |
|---|
| 520 | if self.use_uid: |
|---|
| 521 | if charset is None: |
|---|
| 522 | typ, data = self._imap.uid('SEARCH', *crit_list) |
|---|
| 523 | else: |
|---|
| 524 | typ, data = self._imap.uid('SEARCH', 'CHARSET', charset, |
|---|
| 525 | *crit_list) |
|---|
| 526 | else: |
|---|
| 527 | typ, data = self._imap.search(charset, *crit_list) |
|---|
| 528 | |
|---|
| 529 | self._checkok('search', typ, data) |
|---|
| 530 | if data == [None]: |
|---|
| 531 | return [] |
|---|
| 532 | |
|---|
| 533 | return [ long(i) for i in data[0].split() ] |
|---|
| 534 | |
|---|
| 535 | |
|---|
| 536 | def sort(self, sort_criteria, criteria='ALL', charset='UTF-8'): |
|---|
| 537 | """Return a list of message ids sorted by *sort_criteria* and |
|---|
| 538 | optionally filtered by *criteria*. |
|---|
| 539 | |
|---|
| 540 | Example values for *sort_criteria* include:: |
|---|
| 541 | |
|---|
| 542 | ARRIVAL |
|---|
| 543 | REVERSE SIZE |
|---|
| 544 | SUBJECT |
|---|
| 545 | |
|---|
| 546 | The *criteria* argument is as per search(). |
|---|
| 547 | See `RFC 5256 <http://tools.ietf.org/html/rfc5256>`_ for full details. |
|---|
| 548 | |
|---|
| 549 | Note that SORT is an extension to the IMAP4 standard so it may |
|---|
| 550 | not be supported by all IMAP servers. |
|---|
| 551 | """ |
|---|
| 552 | if not criteria: |
|---|
| 553 | raise ValueError('no criteria specified') |
|---|
| 554 | |
|---|
| 555 | if not self.has_capability('SORT'): |
|---|
| 556 | raise self.Error('The server does not support the SORT extension') |
|---|
| 557 | |
|---|
| 558 | if isinstance(criteria, basestring): |
|---|
| 559 | criteria = (criteria,) |
|---|
| 560 | crit_list = ['(%s)' % c for c in criteria] |
|---|
| 561 | |
|---|
| 562 | sort_criteria = seq_to_parenlist([ s.upper() for s in sort_criteria]) |
|---|
| 563 | |
|---|
| 564 | if self.use_uid: |
|---|
| 565 | typ, data = self._imap.uid('SORT', sort_criteria, charset, |
|---|
| 566 | *crit_list) |
|---|
| 567 | else: |
|---|
| 568 | typ, data = self._imap.sort(sort_criteria, charset, *crit_list) |
|---|
| 569 | |
|---|
| 570 | self._checkok('sort', typ, data) |
|---|
| 571 | |
|---|
| 572 | return [long(i) for i in data[0].split()] |
|---|
| 573 | |
|---|
| 574 | |
|---|
| 575 | def get_flags(self, messages): |
|---|
| 576 | """Return the flags set for each message in *messages*. |
|---|
| 577 | |
|---|
| 578 | The return value is a dictionary structured like this: ``{ |
|---|
| 579 | msgid1: [flag1, flag2, ... ], }``. |
|---|
| 580 | """ |
|---|
| 581 | response = self.fetch(messages, ['FLAGS']) |
|---|
| 582 | return self._flatten_dict(response) |
|---|
| 583 | |
|---|
| 584 | |
|---|
| 585 | def add_flags(self, messages, flags): |
|---|
| 586 | """Add *flags* to *messages*. |
|---|
| 587 | |
|---|
| 588 | *flags* should be a sequence of strings. |
|---|
| 589 | |
|---|
| 590 | Returns the flags set for each modified message (see |
|---|
| 591 | *get_flags*). |
|---|
| 592 | """ |
|---|
| 593 | return self._store('+FLAGS', messages, flags) |
|---|
| 594 | |
|---|
| 595 | |
|---|
| 596 | def remove_flags(self, messages, flags): |
|---|
| 597 | """Remove one or more *flags* from *messages*. |
|---|
| 598 | |
|---|
| 599 | *flags* should be a sequence of strings. |
|---|
| 600 | |
|---|
| 601 | Returns the flags set for each modified message (see |
|---|
| 602 | *get_flags*). |
|---|
| 603 | """ |
|---|
| 604 | return self._store('-FLAGS', messages, flags) |
|---|
| 605 | |
|---|
| 606 | |
|---|
| 607 | def set_flags(self, messages, flags): |
|---|
| 608 | """Set the *flags* for *messages*. |
|---|
| 609 | |
|---|
| 610 | *flags* should be a sequence of strings. |
|---|
| 611 | |
|---|
| 612 | Returns the flags set for each modified message (see |
|---|
| 613 | *get_flags*). |
|---|
| 614 | """ |
|---|
| 615 | return self._store('FLAGS', messages, flags) |
|---|
| 616 | |
|---|
| 617 | |
|---|
| 618 | def delete_messages(self, messages): |
|---|
| 619 | """Delete one or more *messages* from the currently selected |
|---|
| 620 | folder. |
|---|
| 621 | |
|---|
| 622 | Returns the flags set for each modified message (see |
|---|
| 623 | *get_flags*). |
|---|
| 624 | """ |
|---|
| 625 | return self.add_flags(messages, DELETED) |
|---|
| 626 | |
|---|
| 627 | |
|---|
| 628 | def fetch(self, messages, data, modifiers=None): |
|---|
| 629 | """Retrieve selected *data* associated with one or more *messages*. |
|---|
| 630 | |
|---|
| 631 | *data* should be specified as a sequnce of strings, one item |
|---|
| 632 | per data selector, for example ``['INTERNALDATE', |
|---|
| 633 | 'RFC822']``. |
|---|
| 634 | |
|---|
| 635 | *modifiers* are required for some extensions to the IMAP |
|---|
| 636 | protocol (eg. RFC 4551). These should be a sequnce of strings |
|---|
| 637 | if specified, for example ``['CHANGEDSINCE 123']``. |
|---|
| 638 | |
|---|
| 639 | A dictionary is returned, indexed by message number. Each item |
|---|
| 640 | in this dictionary is also a dictionary, with an entry |
|---|
| 641 | corresponding to each item in *data*. |
|---|
| 642 | |
|---|
| 643 | In addition to an element for each *data* item, the dict |
|---|
| 644 | returned for each message also contains a *SEQ* key containing |
|---|
| 645 | the sequence number for the message. This allows for mapping |
|---|
| 646 | between the UID and sequence number (when the *use_uid* |
|---|
| 647 | property is ``True``). |
|---|
| 648 | |
|---|
| 649 | Example:: |
|---|
| 650 | |
|---|
| 651 | >> c.fetch([3293, 3230], ['INTERNALDATE', 'FLAGS']) |
|---|
| 652 | {3230: {'FLAGS': ('\\Seen',), |
|---|
| 653 | 'INTERNALDATE': datetime.datetime(2011, 1, 30, 13, 32, 9), |
|---|
| 654 | 'SEQ': 84}, |
|---|
| 655 | 3293: {'FLAGS': (), |
|---|
| 656 | 'INTERNALDATE': datetime.datetime(2011, 2, 24, 19, 30, 36), |
|---|
| 657 | 'SEQ': 110}} |
|---|
| 658 | |
|---|
| 659 | """ |
|---|
| 660 | if not messages: |
|---|
| 661 | return {} |
|---|
| 662 | |
|---|
| 663 | msg_list = messages_to_str(messages) |
|---|
| 664 | data_list = seq_to_parenlist([p.upper() for p in data]) |
|---|
| 665 | modifiers_list = None |
|---|
| 666 | if modifiers is not None: |
|---|
| 667 | modifiers_list = seq_to_parenlist([m.upper() for m in modifiers]) |
|---|
| 668 | |
|---|
| 669 | if self.use_uid: |
|---|
| 670 | tag = self._imap._command('UID', 'FETCH', msg_list, data_list, modifiers_list) |
|---|
| 671 | else: |
|---|
| 672 | tag = self._imap._command('FETCH', msg_list, data_list, modifiers_list) |
|---|
| 673 | typ, data = self._imap._command_complete('FETCH', tag) |
|---|
| 674 | self._checkok('fetch', typ, data) |
|---|
| 675 | typ, data = self._imap._untagged_response(typ, data, 'FETCH') |
|---|
| 676 | return parse_fetch_response(data, self.normalise_times, self.use_uid) |
|---|
| 677 | |
|---|
| 678 | def append(self, folder, msg, flags=(), msg_time=None): |
|---|
| 679 | """Append a message to *folder*. |
|---|
| 680 | |
|---|
| 681 | *msg* should be a string contains the full message including |
|---|
| 682 | headers. |
|---|
| 683 | |
|---|
| 684 | *flags* should be a sequence of message flags to set. If not |
|---|
| 685 | specified no flags will be set. |
|---|
| 686 | |
|---|
| 687 | *msg_time* is an optional datetime instance specifying the |
|---|
| 688 | date and time to set on the message. The server will set a |
|---|
| 689 | time if it isn't specified. If *msg_time* contains timezone |
|---|
| 690 | information (tzinfo), this will be honoured. Otherwise the |
|---|
| 691 | local machine's time zone sent to the server. |
|---|
| 692 | |
|---|
| 693 | Returns the APPEND response as returned by the server. |
|---|
| 694 | """ |
|---|
| 695 | if msg_time: |
|---|
| 696 | time_val = '"%s"' % datetime_to_imap(msg_time) |
|---|
| 697 | else: |
|---|
| 698 | time_val = None |
|---|
| 699 | |
|---|
| 700 | flags_list = seq_to_parenlist(flags) |
|---|
| 701 | |
|---|
| 702 | typ, data = self._imap.append(self._encode_folder_name(folder), |
|---|
| 703 | flags_list, time_val, msg) |
|---|
| 704 | self._checkok('append', typ, data) |
|---|
| 705 | |
|---|
| 706 | return data[0] |
|---|
| 707 | |
|---|
| 708 | def copy(self, messages, folder): |
|---|
| 709 | """Copy one or more messages from the current folder to |
|---|
| 710 | *folder*. Returns the COPY response string returned by the |
|---|
| 711 | server. |
|---|
| 712 | """ |
|---|
| 713 | msg_list = messages_to_str(messages) |
|---|
| 714 | folder = self._encode_folder_name(folder) |
|---|
| 715 | |
|---|
| 716 | if self.use_uid: |
|---|
| 717 | typ, data = self._imap.uid('COPY', msg_list, folder) |
|---|
| 718 | else: |
|---|
| 719 | typ, data = self._imap.copy(msg_list, folder) |
|---|
| 720 | self._checkok('copy', typ, data) |
|---|
| 721 | return data[0] |
|---|
| 722 | |
|---|
| 723 | def expunge(self): |
|---|
| 724 | """Remove any messages from the currently selected folder that |
|---|
| 725 | have the ``\\Deleted`` flag set. |
|---|
| 726 | |
|---|
| 727 | The return value is the server response message |
|---|
| 728 | followed by a list of expunge responses. For example:: |
|---|
| 729 | |
|---|
| 730 | ('Expunge completed.', |
|---|
| 731 | [(2, 'EXPUNGE'), |
|---|
| 732 | (1, 'EXPUNGE'), |
|---|
| 733 | (0, 'RECENT')]) |
|---|
| 734 | |
|---|
| 735 | In this case, the responses indicate that the message with |
|---|
| 736 | sequence numbers 2 and 1 where deleted, leaving no recent |
|---|
| 737 | messages in the folder. |
|---|
| 738 | |
|---|
| 739 | See `RFC 3501 section 6.4.3 |
|---|
| 740 | <http://tools.ietf.org/html/rfc3501#section-6.4.3>`_ and |
|---|
| 741 | `RFC 3501 section 7.4.1 |
|---|
| 742 | <http://tools.ietf.org/html/rfc3501#section-7.4.1>`_ for more |
|---|
| 743 | details. |
|---|
| 744 | """ |
|---|
| 745 | tag = self._imap._command('EXPUNGE') |
|---|
| 746 | return self._consume_until_tagged_response(tag, 'EXPUNGE') |
|---|
| 747 | |
|---|
| 748 | def getacl(self, folder): |
|---|
| 749 | """Returns a list of ``(who, acl)`` tuples describing the |
|---|
| 750 | access controls for *folder*. |
|---|
| 751 | """ |
|---|
| 752 | typ, data = self._imap.getacl(folder) |
|---|
| 753 | self._checkok('getacl', typ, data) |
|---|
| 754 | |
|---|
| 755 | parts = list(response_lexer.TokenSource(data)) |
|---|
| 756 | parts = parts[1:] |
|---|
| 757 | return [(parts[i], parts[i+1]) for i in xrange(0, len(parts), 2)] |
|---|
| 758 | |
|---|
| 759 | def setacl(self, folder, who, what): |
|---|
| 760 | """Set an ACL (*what*) for user (*who*) for a folder. |
|---|
| 761 | |
|---|
| 762 | Set *what* to an empty string to remove an ACL. Returns the |
|---|
| 763 | server response string. |
|---|
| 764 | """ |
|---|
| 765 | typ, data = self._imap.setacl(folder, who, what) |
|---|
| 766 | self._checkok('setacl', typ, data) |
|---|
| 767 | return data[0] |
|---|
| 768 | |
|---|
| 769 | def _check_resp(self, expected, command, typ, data): |
|---|
| 770 | """Check command responses for errors. |
|---|
| 771 | |
|---|
| 772 | Raises IMAPClient.Error if the command fails. |
|---|
| 773 | """ |
|---|
| 774 | if typ != expected: |
|---|
| 775 | raise self.Error('%s failed: %r' % (command, data[0])) |
|---|
| 776 | |
|---|
| 777 | def _consume_until_tagged_response(self, tag, command): |
|---|
| 778 | tagged_commands = self._imap.tagged_commands |
|---|
| 779 | resps = [] |
|---|
| 780 | while True: |
|---|
| 781 | line = self._imap._get_response() |
|---|
| 782 | if tagged_commands[tag]: |
|---|
| 783 | break |
|---|
| 784 | resps.append(_parse_untagged_response(line)) |
|---|
| 785 | typ, data = tagged_commands.pop(tag) |
|---|
| 786 | self._checkok(command, typ, data) |
|---|
| 787 | return data[0], resps |
|---|
| 788 | |
|---|
| 789 | def _checkok(self, command, typ, data): |
|---|
| 790 | self._check_resp('OK', command, typ, data) |
|---|
| 791 | |
|---|
| 792 | def _checkbye(self, command, typ, data): |
|---|
| 793 | self._check_resp('BYE', command, typ, data) |
|---|
| 794 | |
|---|
| 795 | def _store(self, cmd, messages, flags): |
|---|
| 796 | """Worker function for the various flag manipulation methods. |
|---|
| 797 | |
|---|
| 798 | *cmd* is the STORE command to use (eg. '+FLAGS'). |
|---|
| 799 | """ |
|---|
| 800 | if not messages: |
|---|
| 801 | return {} |
|---|
| 802 | |
|---|
| 803 | msg_list = messages_to_str(messages) |
|---|
| 804 | flag_list = seq_to_parenlist(flags) |
|---|
| 805 | |
|---|
| 806 | if self.use_uid: |
|---|
| 807 | typ, data = self._imap.uid('STORE', msg_list, cmd, flag_list) |
|---|
| 808 | else: |
|---|
| 809 | typ, data = self._imap.store(msg_list, cmd, flag_list) |
|---|
| 810 | self._checkok('store', typ, data) |
|---|
| 811 | return self._flatten_dict(parse_fetch_response((data))) |
|---|
| 812 | |
|---|
| 813 | def _flatten_dict(self, fetch_dict): |
|---|
| 814 | return dict([ |
|---|
| 815 | (msgid, data.values()[0]) |
|---|
| 816 | for msgid, data in fetch_dict.iteritems() |
|---|
| 817 | ]) |
|---|
| 818 | |
|---|
| 819 | def _decode_folder_name(self, name): |
|---|
| 820 | if self.folder_encode: |
|---|
| 821 | return imap_utf7.decode(name) |
|---|
| 822 | return name |
|---|
| 823 | |
|---|
| 824 | def _encode_folder_name(self, name): |
|---|
| 825 | if self.folder_encode: |
|---|
| 826 | name = imap_utf7.encode(name) |
|---|
| 827 | |
|---|
| 828 | |
|---|
| 829 | |
|---|
| 830 | |
|---|
| 831 | |
|---|
| 832 | |
|---|
| 833 | |
|---|
| 834 | |
|---|
| 835 | |
|---|
| 836 | return _quote_arg(name) |
|---|
| 837 | |
|---|
| 838 | def __debug_get(self): |
|---|
| 839 | return self._imap.debug |
|---|
| 840 | |
|---|
| 841 | def __debug_set(self, level): |
|---|
| 842 | if level is True: |
|---|
| 843 | level = 4 |
|---|
| 844 | elif level is False: |
|---|
| 845 | level = 0 |
|---|
| 846 | self._imap.debug = level |
|---|
| 847 | |
|---|
| 848 | debug = property(__debug_get, __debug_set) |
|---|
| 849 | |
|---|
| 850 | def _log(self, text): |
|---|
| 851 | self.log_file.write('%s %s\n' % (datetime.now().strftime('%M:%S.%f'), text)) |
|---|
| 852 | self.log_file.flush() |
|---|
| 853 | |
|---|
| 854 | |
|---|
| 855 | def messages_to_str(messages): |
|---|
| 856 | """Convert a sequence of messages ids or a single integer message id |
|---|
| 857 | into an id list string for use with IMAP commands |
|---|
| 858 | """ |
|---|
| 859 | if isinstance(messages, (str, int, long)): |
|---|
| 860 | messages = (messages,) |
|---|
| 861 | elif not isinstance(messages, (tuple, list)): |
|---|
| 862 | raise ValueError('invalid message list: %r' % messages) |
|---|
| 863 | return ','.join([str(m) for m in messages]) |
|---|
| 864 | |
|---|
| 865 | |
|---|
| 866 | def seq_to_parenlist(flags): |
|---|
| 867 | """Convert a sequence of strings into parenthised list string for |
|---|
| 868 | use with IMAP commands. |
|---|
| 869 | """ |
|---|
| 870 | if isinstance(flags, str): |
|---|
| 871 | flags = (flags,) |
|---|
| 872 | elif not isinstance(flags, (tuple, list)): |
|---|
| 873 | raise ValueError('invalid flags list: %r' % flags) |
|---|
| 874 | return '(%s)' % ' '.join(flags) |
|---|
| 875 | |
|---|
| 876 | |
|---|
| 877 | def datetime_to_imap(dt): |
|---|
| 878 | """Convert a datetime instance to a IMAP datetime string. |
|---|
| 879 | |
|---|
| 880 | If timezone information is missing the current system |
|---|
| 881 | timezone is used. |
|---|
| 882 | """ |
|---|
| 883 | if not dt.tzinfo: |
|---|
| 884 | dt = dt.replace(tzinfo=FixedOffset.for_system()) |
|---|
| 885 | return dt.strftime("%d-%b-%Y %H:%M:%S %z") |
|---|
| 886 | |
|---|
| 887 | |
|---|
| 888 | def _quote_arg(arg): |
|---|
| 889 | arg = arg.replace('\\', '\\\\') |
|---|
| 890 | arg = arg.replace('"', '\\"') |
|---|
| 891 | return '"%s"' % arg |
|---|
| 892 | |
|---|
| 893 | |
|---|
| 894 | def _parse_untagged_response(text): |
|---|
| 895 | assert text.startswith('* ') |
|---|
| 896 | text = text[2:] |
|---|
| 897 | if text.startswith(('OK ', 'NO ')): |
|---|
| 898 | return tuple(text.split(' ', 1)) |
|---|
| 899 | return parse_response([text]) |
|---|
| 900 | |
|---|
| 901 | _re_status = re.compile(r'^\s*"?(?P<folder>[^"]+)"?\s+' |
|---|
| 902 | r'\((?P<status_items>.*)\)$') |
|---|