root/imapclient/imapclient.py

Revision 281:83a42071e63b, 31.1 KB (checked in by Menno Smits <menno@…>, 3 months ago)

fixed indentation

Line 
1# Copyright (c) 2011, Menno Smits
2# Released subject to the New BSD License
3# Please see http://en.wikipedia.org/wiki/BSD_licenses
4
5import re
6import select
7import socket
8import sys
9import warnings
10from datetime import datetime
11from operator import itemgetter
12
13import imaplib
14import response_lexer
15
16   
17try:
18    import oauth2
19except ImportError:
20    oauth2 = None
21
22import imap_utf7
23from fixed_offset import FixedOffset
24
25
26__all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', 'RECENT']
27
28from response_parser import parse_response, parse_fetch_response
29
30# We also offer the gmail-specific XLIST command...
31if 'XLIST' not in imaplib.Commands:
32  imaplib.Commands['XLIST'] = imaplib.Commands['LIST']
33
34# ...and IDLE
35if 'IDLE' not in imaplib.Commands:
36  imaplib.Commands['IDLE'] = imaplib.Commands['APPEND']
37
38
39# System flags
40DELETED = r'\Deleted'
41SEEN = r'\Seen'
42ANSWERED = r'\Answered'
43FLAGGED = r'\Flagged'
44DRAFT = r'\Draft'
45RECENT = r'\Recent'         # This flag is read-only
46
47class 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
56class 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    # patch in custom debug log method
106        self._idle_tag = None
107
108    def _create_IMAP4(self):
109        # Create the IMAP instance in a separate method to make unit tests easier
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        # FIXME: this will not detect capabilities that are backwards
156        # compatible with the current level. For instance the SORT
157        # capabilities may in the future be named SORT2 which is
158        # still compatible with the current standard and will not
159        # be detected by this method.
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        # Filter out empty strings and None's.
265        # This also deals with the special case of - no 'untagged'
266        # responses (ie, no folders). This comes back as [None].
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        # make the socket non-blocking so the timeout can be
375        # implemented for this call
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]: # no untagged responses...
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:]       # First item is folder name
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        # imaplib assumes that if a command argument (in this case a
828        # folder name) has double quotes around it, then it doesn't
829        # need quoting. This "feature" prevents creation of folders
830        # with names that start and end with double quotes.
831        #
832        # To work around this IMAPClient performs the quoting
833        # itself. This adds start and end double quotes which also
834        # serves to fool IMAP4._checkquote into not attempting further
835        # quoting. A hack but it works.
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
855def 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
866def 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
877def 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
888def _quote_arg(arg):
889  arg = arg.replace('\\', '\\\\')
890  arg = arg.replace('"', '\\"')
891  return '"%s"' % arg
892
893
894def _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>.*)\)$')
Note: See TracBrowser for help on using the browser.