Show
Ignore:
Timestamp:
01/11/10 19:14:38 (2 years ago)
Author:
Menno Smits <menno@…>
Branch:
default
Parents:
115:7aa870b75d11 (diff), 98:bd244d16fcea (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merged from trunk

Files:
2 modified

Legend:

Unmodified
Added
Removed
  • imapclient/imapclient.py

    r115 r116  
    1111import imap_utf7 
    1212from fixed_offset import FixedOffset 
     13 
    1314 
    1415__all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', 
     
    2728 
    2829class IMAPClient(object): 
    29     ''' 
     30    """ 
    3031    A Pythonic, easy-to-use IMAP client class. 
    3132 
     
    5960    These are aliases for the imaplib.IMAP4 exceptions of the same name. Socket 
    6061    errors may also be raised in the case of network errors. 
    61     ''' 
     62    """ 
    6263 
    6364    Error = imaplib.IMAP4.error 
     
    6667 
    6768    re_sep = re.compile('^\(\("[^"]*" "([^"]+)"\)\)') 
    68     re_folder = re.compile('\([^)]*\) "[^"]+" "?([^"]+)"?') 
     69#     re_folder = re.compile('\([^)]*\) "[^"]+" "?([^"]+)"?') 
     70    re_folder = re.compile(r'\([^)]*\) "[^"]+" (?P<qqq>"?)(?P<folder>.+)(?P=qqq)') 
    6971    re_status = re.compile(r'^\s*"?(?P<folder>[^"]+)"?\s+' 
    7072                           r'\((?P<status_items>.*)\)$') 
    7173 
    7274    def __init__(self, host, port=None, use_uid=True, ssl=False): 
    73         '''Initialise object instance and connect to the remote IMAP server. 
     75        """Initialise object instance and connect to the remote IMAP server. 
    7476 
    7577        @param host: The IMAP server address/hostname to connect to. 
     
    7779        @param use_uid: Should message UIDs be used (default is True). 
    7880        @param ssl: Make an SSL connection (default is False) 
    79         ''' 
     81        """ 
    8082        if ssl: 
    8183            ImapClass = imaplib.IMAP4_SSL 
     
    9496    
    9597    def login(self, username, password): 
    96         '''Perform a simple login 
    97         ''' 
     98        """Perform a simple login 
     99        """ 
    98100        typ, data = self._imap.login(username, password) 
    99101        self._checkok('login', typ, data) 
     
    102104 
    103105    def logout(self): 
    104         '''Perform a logout 
    105         ''' 
     106        """Perform a logout 
     107        """ 
    106108        typ, data = self._imap.logout() 
    107109        self._checkbye('logout', typ, data) 
     
    110112 
    111113    def capabilities(self): 
    112         '''Returns the server capability list 
    113         ''' 
     114        """Returns the server capability list 
     115        """ 
    114116        return self._imap.capabilities 
    115117 
    116118 
    117119    def has_capability(self, capability): 
    118         '''Checks if the server has the given capability. 
     120        """Checks if the server has the given capability. 
    119121 
    120122        @param capability: capability to test (eg 'SORT') 
    121         ''' 
     123        """ 
    122124        # FIXME: this will not detect capabilities that are backwards 
    123125        # compatible with the current level. For instance the SORT 
     
    131133 
    132134    def get_folder_delimiter(self): 
    133         '''Determine the folder separator used by the IMAP server. 
     135        """Determine the folder separator used by the IMAP server. 
    134136 
    135137        @return: The folder separator. 
    136138        @rtype: string 
    137         ''' 
     139        """ 
    138140        typ, data = self._imap.namespace() 
    139141        self._checkok('namespace', typ, data) 
     
    147149 
    148150    def list_folders(self, directory="", pattern="*"): 
    149         '''Get a listing of folders on the server. 
     151        """Get a listing of folders on the server. 
    150152 
    151153        The default behaviour (no args) will list all folders for the logged in 
     
    159161            decoding). If the folder_encode attribute is False, no decoding 
    160162            will be performed and only ordinary strings will be returned. 
    161         ''' 
     163        """ 
    162164        typ, data = self._imap.list(directory, pattern) 
    163165        self._checkok('list', typ, data) 
    164  
     166        return self._proc_folder_list(data) 
     167 
     168 
     169    def list_sub_folders(self, directory="", pattern="*"): 
     170        """Get a listing of subscribed folders on the server. 
     171 
     172        The default behaviour (no args) will list all subscribed folders for the 
     173        logged in user. 
     174 
     175        @param directory: The base directory to look for folders from. 
     176        @param pattern: A pattern to match against folder names. Only folder 
     177            names matching this pattern will be returned. Wildcards accepted. 
     178        @return: A list of folder names. As per the return of list_folders(). 
     179        """ 
     180        typ, data = self._imap.lsub(directory, pattern) 
     181        self._checkok('lsub', typ, data) 
     182        return self._proc_folder_list(data) 
     183 
     184 
     185    def _proc_folder_list(self, folder_data): 
    165186        folders = [] 
    166         for line in data: 
     187        for line in folder_data: 
    167188            #TODO can the FetchParser code be adapted for use here? 
    168189            folder_text = None 
    169190            if isinstance(line, tuple): 
    170191                folder_text = line[-1] 
    171             else: 
     192            elif line: 
    172193                match = self.re_folder.match(line) 
    173194                if match: 
    174                     folder_text = match.group(1) 
     195                    folder_text = match.group('folder') 
     196                    folder_text = folder_text.replace(r'\"', '"') 
     197                    folder_text = folder_text.replace(r'\\', '\\') 
    175198            if folder_text is not None: 
    176199                folders.append(self._decode_folder_name(folder_text)) 
    177200        return folders 
    178  
    179  
    180     def list_sub_folders(self, directory="", pattern="*"): 
    181         '''Get a listing of subscribed folders on the server. 
    182  
    183         The default behaviour (no args) will list all subscribed folders for the 
    184         logged in user. 
    185  
    186         @param directory: The base directory to look for folders from. 
    187         @param pattern: A pattern to match against folder names. Only folder 
    188             names matching this pattern will be returned. Wildcards accepted. 
    189         @return: A list of folder names. As per the return of list_folders(). 
    190         ''' 
    191         typ, data = self._imap.lsub(directory, pattern) 
    192         self._checkok('lsub', typ, data) 
    193  
    194         folders = [] 
    195         for line in data: 
    196             if line: 
    197                 m = self.re_folder.match(line) 
    198                 if m: 
    199                     folders.append(self._decode_folder_name(m.group(1))) 
    200         return folders 
    201  
     201         
    202202 
    203203    def select_folder(self, folder): 
    204         '''Select the current folder on the server. Future calls to methods 
     204        """Select the current folder on the server. Future calls to methods 
    205205        such as search and fetch will act on the selected folder. 
    206206 
     
    208208        @return: Number of messages in the folder. 
    209209        @rtype: long int 
    210         ''' 
     210        """ 
    211211        typ, data = self._imap.select(self._encode_folder_name(folder)) 
    212212        self._checkok('select', typ, data) 
     
    215215 
    216216    def folder_status(self, folder, what=None): 
    217         '''Requests the status from folder. 
     217        """Requests the status from folder. 
    218218 
    219219        @param folder: The folder name. 
     
    223223            the items specified in the what parameter. 
    224224        @rtype: dict 
    225         ''' 
     225        """ 
    226226        if what is None: 
    227227            what = ('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN') 
     
    247247 
    248248    def close_folder(self): 
    249         '''Close the currently selected folder. 
     249        """Close the currently selected folder. 
    250250 
    251251        @return: Server response. 
    252         ''' 
     252        """ 
    253253        typ, data = self._imap.close() 
    254254        self._checkok('close', typ, data) 
     
    257257 
    258258    def create_folder(self, folder): 
    259         '''Create a new folder on the server. 
     259        """Create a new folder on the server. 
    260260 
    261261        @param folder: The folder name. 
    262262        @return: Server response. 
    263         ''' 
     263        """ 
    264264        typ, data = self._imap.create(self._encode_folder_name(folder)) 
    265265        self._checkok('create', typ, data) 
     
    268268 
    269269    def delete_folder(self, folder): 
    270         '''Delete a new folder on the server. 
     270        """Delete a new folder on the server. 
    271271 
    272272        @param folder: Folder name to delete. 
    273273        @return: Server response. 
    274         ''' 
     274        """ 
    275275        typ, data = self._imap.delete(self._encode_folder_name(folder)) 
    276276        self._checkok('delete', typ, data) 
     
    279279 
    280280    def folder_exists(self, folder): 
    281         '''Determine if a folder exists on the server. 
     281        """Determine if a folder exists on the server. 
    282282 
    283283        @param folder: Full folder name to look for. 
    284284        @return: True if the folder exists. False otherwise. 
    285         ''' 
     285        """ 
    286286        typ, data = self._imap.list('', self._encode_folder_name(folder)) 
    287287        self._checkok('list', typ, data) 
     
    290290 
    291291    def subscribe_folder(self, folder): 
    292         '''Subscribe to a folder. 
     292        """Subscribe to a folder. 
    293293 
    294294        @param folder: Folder name to subscribe to. 
    295295        @return: Server response message. 
    296         ''' 
     296        """ 
    297297        typ, data = self._imap.subscribe(self._encode_folder_name(folder)) 
    298298        self._checkok('subscribe', typ, data) 
     
    301301 
    302302    def unsubscribe_folder(self, folder): 
    303         '''Unsubscribe a folder. 
     303        """Unsubscribe a folder. 
    304304 
    305305        @param folder: Folder name to unsubscribe. 
    306306        @return: Server response message. 
    307         ''' 
     307        """ 
    308308        typ, data = self._imap.unsubscribe(self._encode_folder_name(folder)) 
    309309        self._checkok('unsubscribe', typ, data) 
     
    336336 
    337337    def sort(self, sort_criteria, criteria='ALL', charset='UTF-8' ): 
    338         '''Returns a list of messages sorted by sort_criteria. 
     338        """Returns a list of messages sorted by sort_criteria. 
    339339 
    340340        Note that this is an extension to the IMAP4: 
    341341        http://www.ietf.org/internet-drafts/draft-ietf-imapext-sort-19.txt 
    342         ''' 
     342        """ 
    343343        if not criteria: 
    344344            raise ValueError('no criteria specified') 
     
    365365 
    366366    def get_flags(self, messages): 
    367         '''Return the flags set for messages 
     367        """Return the flags set for messages 
    368368 
    369369        @param messages: Message IDs to check flags for 
    370370        @return: As for add_f 
    371371            { msgid1: [flag1, flag2, ... ], } 
    372         ''' 
     372        """ 
    373373        response = self.fetch(messages, ['FLAGS']) 
    374374        return self._flatten_dict(response) 
     
    376376 
    377377    def add_flags(self, messages, flags): 
    378         '''Add one or more flags to messages 
     378        """Add one or more flags to messages 
    379379 
    380380        @param messages: Message IDs to add flags to 
     
    382382        @return: The flags set for each message ID as a dictionary 
    383383            { msgid1: [flag1, flag2, ... ], } 
    384         ''' 
     384        """ 
    385385        return self._store('+FLAGS', messages, flags) 
    386386 
    387387 
    388388    def remove_flags(self, messages, flags): 
    389         '''Remove one or more flags from messages 
     389        """Remove one or more flags from messages 
    390390 
    391391        @param messages: Message IDs to remove flags from 
    392392        @param flags: Sequence of flags to remove 
    393393        @return: As for get_flags. 
    394         ''' 
     394        """ 
    395395        return self._store('-FLAGS', messages, flags) 
    396396 
    397397 
    398398    def set_flags(self, messages, flags): 
    399         '''Set the flags for messages 
     399        """Set the flags for messages 
    400400 
    401401        @param messages: Message IDs to set flags for 
    402402        @param flags: Sequence of flags to set 
    403403        @return: As for get_flags. 
    404         ''' 
     404        """ 
    405405        return self._store('FLAGS', messages, flags) 
    406406 
    407407 
    408408    def delete_messages(self, messages): 
    409         '''Short-hand method for deleting one or more messages 
     409        """Short-hand method for deleting one or more messages 
    410410 
    411411        @param messages: Message IDs to mark for deletion. 
    412412        @return: Same as for get_flags. 
    413         ''' 
     413        """ 
    414414        return self.add_flags(messages, DELETED) 
    415415 
    416416 
    417417    def fetch(self, messages, parts): 
    418         '''Retrieve selected data items for one or more messages. 
     418        """Retrieve selected data items for one or more messages. 
    419419 
    420420        @param messages: Message IDs to fetch. 
     
    424424            INTERNALDATE parts will be returned as datetime objects converted 
    425425            to the local machine's time zone. 
    426         ''' 
     426        """ 
    427427        if not messages: 
    428428            return {} 
     
    462462 
    463463    def append(self, folder, msg, flags=(), msg_time=None): 
    464         '''Append a message to a folder 
     464        """Append a message to a folder 
    465465 
    466466        @param folder: Folder name to append to. 
     
    475475        @return: The append response returned by the server. 
    476476        @rtype: str 
    477         ''' 
     477        """ 
    478478        if msg_time: 
    479479            time_val = '"%s"' % datetime_to_imap(msg_time) 
     
    497497 
    498498    def getacl(self, folder): 
    499         '''Get the ACL for a folder 
     499        """Get the ACL for a folder 
    500500 
    501501        @param folder: Folder name to get the ACL for. 
    502502        @return: A list of (who, acl) tuples 
    503         ''' 
     503        """ 
    504504        typ, data = self._imap.getacl(folder) 
    505505        self._checkok('getacl', typ, data) 
     
    515515 
    516516    def setacl(self, folder, who, what): 
    517         '''Set an ACL for a folder 
     517        """Set an ACL for a folder 
    518518 
    519519        @param folder: Folder name to set an ACL for. 
     
    521521        @param what: A string describing the ACL. Set to '' to remove an ACL. 
    522522        @return: Server response string. 
    523         ''' 
     523        """ 
    524524        typ, data = self._imap.setacl(folder, who, what) 
    525525        self._checkok('setacl', typ, data) 
     
    528528 
    529529    def _check_resp(self, expected, command, typ, data): 
    530         '''Check command responses for errors. 
     530        """Check command responses for errors. 
    531531 
    532532        @raise: Error if a command failed. 
    533         ''' 
     533        """ 
    534534        if typ != expected: 
    535535            raise self.Error('%s failed: %r' % (command, data[0])) 
     
    545545 
    546546    def _store(self, cmd, messages, flags): 
    547         '''Worker function for flag manipulation functions 
     547        """Worker function for flag manipulation functions 
    548548 
    549549        @param cmd: STORE command to use (eg. '+FLAGS') 
     
    552552        @return: The flags set for each message ID as a dictionary 
    553553            { msgid1: [flag1, flag2, ... ], } 
    554         ''' 
     554        """ 
    555555        if not messages: 
    556556            return {} 
     
    587587 
    588588class FetchParser(object): 
    589     ''' 
     589    """ 
    590590    Parse an IMAP FETCH response and convert the return values to useful Python 
    591591    values. 
    592     ''' 
     592    """ 
    593593 
    594594    def parse(self, response): 
     
    623623 
    624624        data = data.lstrip() 
     625        msgid = None 
    625626        if data[0].isdigit(): 
    626627            # Get message ID 
     
    630631            assert data.startswith('('), data 
    631632            data = data[1:] 
    632             if data.endswith(')'): 
    633                 data = data[:-1] 
    634  
    635         else: 
    636             msgid = None 
     633 
     634        if data.endswith(')'): 
     635            data = data[:-1] 
    637636 
    638637        for name, item in FetchTokeniser().process_pairs(data): 
     
    658657 
    659658    def do_INTERNALDATE(self, arg): 
    660         '''Process an INTERNALDATE response 
     659        """Process an INTERNALDATE response 
    661660 
    662661        @param arg: A quoted IMAP INTERNALDATE string 
    663662            (eg. " 9-Feb-2007 17:08:08 +0000") 
    664663        @return: datetime.datetime instance for the given time (in UTC) 
    665         ''' 
     664        """ 
    666665        arg = 'INTERNALDATE "%s"' % arg 
    667666        mo = imaplib.InternalDate.match(arg) 
     
    693692 
    694693class FetchTokeniser(object): 
    695     ''' 
     694    """ 
    696695    General response tokenizer and converter 
    697     ''' 
     696    """ 
    698697 
    699698    QUOTED_STRING = '(?:".*?")' 
     
    715714 
    716715    def process_pairs(self, s): 
    717         '''Break up and convert a string of FETCH response pairs 
     716        """Break up and convert a string of FETCH response pairs 
    718717 
    719718        @param s: FETCH response string eg. "FOO 12 BAH (1 abc def "foo bar")" 
    720719        @return: Tokenised and converted input return as (name, data) pairs. 
    721         ''' 
     720        """ 
    722721        out = [] 
    723722        for m in strict_finditer(self.PAIR_RE, s): 
     
    727726 
    728727    def process_list(self, s): 
    729         '''Break up and convert a string of data items 
     728        """Break up and convert a string of data items 
    730729 
    731730        @param s: FETCH response string eg. "(1 abc def "foo bar")" 
    732731        @return: A list of converted items. 
    733         ''' 
     732        """ 
    734733        if s == '': 
    735734            return [] 
     
    755754 
    756755class Literal(object): 
    757     ''' 
     756    """ 
    758757    Simple class to represent a literal token in the fetch response 
    759758    (eg. "{21}") 
    760     ''' 
     759    """ 
    761760 
    762761    def __init__(self, length): 
     
    771770 
    772771def strict_finditer(regex, s): 
    773     '''Like re.finditer except the regex must match from exactly where the 
     772    """Like re.finditer except the regex must match from exactly where the 
    774773    previous match ended and all the entire input must be matched. 
    775     ''' 
     774    """ 
    776775    i = 0 
    777776    matched = False 
     
    791790 
    792791def messages_to_str(messages): 
    793     '''Convert a sequence of messages ids or a single message id into an 
     792    """Convert a sequence of messages ids or a single message id into an 
    794793    message ID list for use with IMAP commands. 
    795794 
     
    797796        (eg. [1,4,5,7,8]) 
    798797    @return: Message list string (eg. "1,4,5,6,8") 
    799     ''' 
     798    """ 
    800799    if isinstance(messages, (str, int, long)): 
    801800        messages = (messages,) 
     
    806805 
    807806def seq_to_parenlist(flags): 
    808     '''Convert a sequence into parenthised list for use with IMAP commands 
     807    """Convert a sequence into parenthised list for use with IMAP commands 
    809808 
    810809    @param flags: Sequence to process (eg. ['abc', 'def']) 
    811810    @return: IMAP parenthenised list (eg. '(abc def)') 
    812     ''' 
     811    """ 
    813812    if isinstance(flags, str): 
    814813        flags = (flags,) 
     
    819818 
    820819def datetime_to_imap(dt): 
    821     '''Convert a datetime instance to a IMAP datetime string 
     820    """Convert a datetime instance to a IMAP datetime string 
    822821 
    823822    If timezone information is missing the current system timezone is used. 
    824     ''' 
     823    """ 
    825824    if not dt.tzinfo: 
    826825        dt = dt.replace(tzinfo=FixedOffset.for_system()) 
  • imapclient/imapclient.py

    r96 r116  
    1515__all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', 
    1616    'RECENT'] 
     17 
     18from response_parser import parse_fetch_response 
    1719 
    1820# System flags 
     
    327329 
    328330        self._checkok('search', typ, data) 
     331        if data == [None]: # no untagged responses... 
     332            return [] 
    329333 
    330334        return [ long(i) for i in data[0].split() ] 
     
    436440        return parser(data) 
    437441 
     442    def altfetch(self, messages, parts): 
     443        if not messages: 
     444            return {} 
     445 
     446        msg_list = messages_to_str(messages) 
     447        parts_list = seq_to_parenlist([p.upper() for p in parts]) 
     448 
     449        if self.use_uid: 
     450            tag = self._imap._command('UID', 'FETCH', msg_list, parts_list) 
     451        else: 
     452            tag = self._imap._command('FETCH', msg_list, parts_list) 
     453        typ, data = self._imap._command_complete('FETCH', tag) 
     454        self._checkok('fetch', typ, data) 
     455        typ, data = self._imap._untagged_response(typ, data, 'FETCH') 
     456        # appears to be a special case - no 'untagged' responses (ie, no 
     457        # folders) results in [None] 
     458        if data == [None]: 
     459          return {} 
     460 
     461        return parse_fetch_response(data) 
    438462 
    439463    def append(self, folder, msg, flags=(), msg_time=None): 
     
    800824    if not dt.tzinfo: 
    801825        dt = dt.replace(tzinfo=FixedOffset.for_system()) 
    802  
    803826    return dt.strftime("%d-%b-%Y %H:%M:%S %z") 
    804      
    805  
     827 
     828