Show
Ignore:
Timestamp:
01/27/10 08:18:56 (2 years ago)
Author:
Menno Smits <menno@…>
Branch:
default
Parents:
126:4319998fc9b5 (diff), 129:b627e75c7088 (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 COPY command implementation (#36)

Location:
imapclient
Files:
1 removed
2 modified

Legend:

Unmodified
Added
Removed
  • imapclient/imapclient.py

    r126 r130  
    500500 
    501501 
     502    def copy(self, messages, folder): 
     503        """Copy one or more messages from the current folder to another folder 
     504 
     505        @param messages: Message IDs to fetch. 
     506        @param folder: Folder name to append to. 
     507        @return: The COPY command response message returned by the 
     508          server. 
     509        """ 
     510        msg_list = messages_to_str(messages) 
     511        folder = self._encode_folder_name(folder) 
     512 
     513        if self.use_uid: 
     514            typ, data = self._imap.uid('COPY', msg_list, folder) 
     515        else: 
     516            typ, data = self._imap.copy(msg_list, folder) 
     517        self._checkok('copy', typ, data) 
     518        return data[0] 
     519 
     520 
    502521    def expunge(self): 
    503522        typ, data = self._imap.expunge() 
  • imapclient/imapclient.py

    r129 r130  
    66import imaplib 
    77import shlex 
    8 import datetime 
    98#imaplib.Debug = 5 
    109 
     
    1514__all__ = ['IMAPClient', 'DELETED', 'SEEN', 'ANSWERED', 'FLAGGED', 'DRAFT', 
    1615    'RECENT'] 
     16 
     17from response_parser import parse_response, parse_fetch_response 
     18 
    1719 
    1820# System flags 
     
    204206 
    205207        @param folder: The folder name. 
    206         @return: Number of messages in the folder. 
    207         @rtype: long int 
     208        @return: A dictionary containing the SELECT response 
     209          values. At least the EXISTS, FLAGS and RECENT keys are 
     210          guaranteed to exist. Example: 
     211            {'EXISTS': 3, 
     212             'FLAGS': ('\\Answered', '\\Flagged', '\\Deleted', ... ), 
     213             'RECENT': 0, 
     214             'PERMANENTFLAGS': ('\\Answered', '\\Flagged', '\\Deleted', ... ), 
     215             'READ-WRITE': True, 
     216             'UIDNEXT': 11, 
     217             'UIDVALIDITY': 1239278212} 
    208218        """ 
    209219        typ, data = self._imap.select(self._encode_folder_name(folder)) 
    210220        self._checkok('select', typ, data) 
    211         return long(data[0]) 
     221        return self._process_select_response(self._imap.untagged_responses) 
     222 
     223 
     224    def _process_select_response(self, resp): 
     225        out = {} 
     226        for key, value in resp.iteritems(): 
     227            key = key.upper() 
     228            if key == 'OK': 
     229                continue 
     230            elif key in ('EXISTS', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'): 
     231                value = int(value[0]) 
     232            elif key in ('FLAGS', 'PERMANENTFLAGS'): 
     233                value = parse_response(value)[0] 
     234            elif key == 'READ-WRITE': 
     235                value = True 
     236                                  
     237            out[key] = value 
     238        return out 
    212239 
    213240 
     
    327354 
    328355        self._checkok('search', typ, data) 
     356        if data == [None]: # no untagged responses... 
     357            return [] 
    329358 
    330359        return [ long(i) for i in data[0].split() ] 
     
    428457 
    429458        if self.use_uid: 
    430             typ, data = self._imap.uid('FETCH', msg_list, parts_list) 
    431         else: 
    432             typ, data = self._imap.fetch(msg_list, parts_list) 
     459            tag = self._imap._command('UID', 'FETCH', msg_list, parts_list) 
     460        else: 
     461            tag = self._imap._command('FETCH', msg_list, parts_list) 
     462        typ, data = self._imap._command_complete('FETCH', tag) 
    433463        self._checkok('fetch', typ, data) 
    434  
    435         parser = FetchParser() 
    436         return parser(data) 
     464        typ, data = self._imap._untagged_response(typ, data, 'FETCH') 
     465        # appears to be a special case - no 'untagged' responses (ie, no 
     466        # folders) results in [None] 
     467        if data == [None]: 
     468          return {} 
     469 
     470        return parse_fetch_response(data) 
    437471 
    438472 
     
    560594        self._checkok('store', typ, data) 
    561595 
    562         return self._flatten_dict(FetchParser()(data)) 
     596        return self._flatten_dict(parse_fetch_response((data))) 
    563597 
    564598 
     
    579613            return imap_utf7.encode(name) 
    580614        return name 
    581  
    582  
    583 class FetchParser(object): 
    584     """ 
    585     Parse an IMAP FETCH response and convert the return values to useful Python 
    586     values. 
    587     """ 
    588  
    589     def parse(self, response): 
    590         out = {} 
    591         for response_item in response: 
    592             msgid, data = self.parse_data(response_item) 
    593  
    594             if msgid != None: 
    595                 # Response for a new message 
    596                 current_msg_data = {} 
    597                 out[msgid] = current_msg_data 
    598  
    599             current_msg_data.update(data) 
    600  
    601         return out 
    602  
    603     __call__ = parse 
    604  
    605     def parse_data(self, data): 
    606         out = {} 
    607  
    608         if isinstance(data, str): 
    609             if data == ')': 
    610                 # End of response for current message 
    611                 return None, {} 
    612  
    613         elif isinstance(data, tuple): 
    614             data, literal_data = data 
    615  
    616         else: 
    617             raise ValueError("don't know how to handle %r" % data) 
    618  
    619         data = data.lstrip() 
    620         msgid = None 
    621         if data[0].isdigit(): 
    622             # Get message ID 
    623             msgid, data = data.split(None, 1) 
    624             msgid = long(msgid) 
    625  
    626             assert data.startswith('('), data 
    627             data = data[1:] 
    628  
    629         if data.endswith(')'): 
    630             data = data[:-1] 
    631  
    632         for name, item in FetchTokeniser().process_pairs(data): 
    633             name = name.upper() 
    634  
    635             if name == 'UID': 
    636                 # Using UID's, override the message ID 
    637                 msgid = long(item) 
    638  
    639             else: 
    640                 if isinstance(item, Literal): 
    641                     #assert len(data) == item.length 
    642                     arg = literal_data 
    643                 else: 
    644                     arg = item 
    645  
    646                 # Call handler function based on the response type 
    647                 methname = 'do_'+name.upper().replace('.', '_') 
    648                 meth = getattr(self, methname, self.do_default) 
    649                 out[name] = meth(arg) 
    650  
    651         return msgid, out 
    652  
    653     def do_INTERNALDATE(self, arg): 
    654         """Process an INTERNALDATE response 
    655  
    656         @param arg: A quoted IMAP INTERNALDATE string 
    657             (eg. " 9-Feb-2007 17:08:08 +0000") 
    658         @return: datetime.datetime instance for the given time (in UTC) 
    659         """ 
    660         arg = 'INTERNALDATE "%s"' % arg 
    661         mo = imaplib.InternalDate.match(arg) 
    662         if not mo: 
    663             raise ValueError("couldn't parse date %r" % arg) 
    664  
    665         zoneh = int(mo.group('zoneh')) 
    666         zonem = (zoneh * 60) + int(mo.group('zonem')) 
    667         if mo.group('zonen') == '-': 
    668             zonem = -zonem 
    669         tz = FixedOffset(zonem) 
    670  
    671         year = int(mo.group('year')) 
    672         mon = imaplib.Mon2num[mo.group('mon')] 
    673         day = int(mo.group('day')) 
    674         hour = int(mo.group('hour')) 
    675         min = int(mo.group('min')) 
    676         sec = int(mo.group('sec')) 
    677  
    678         dt = datetime.datetime(year, mon, day, hour, min, sec, 0, tz) 
    679  
    680         # Normalise to host system's timezone 
    681         return dt.astimezone(FixedOffset.for_system()).replace(tzinfo=None) 
    682  
    683  
    684     def do_default(self, arg): 
    685         return arg 
    686  
    687  
    688 class FetchTokeniser(object): 
    689     """ 
    690     General response tokenizer and converter 
    691     """ 
    692  
    693     QUOTED_STRING = '(?:".*?")' 
    694     PAREN_LIST = '(?:\(.*?\))' 
    695  
    696     PAIR_RE = re.compile(( 
    697         '([\w\.]+(?:\[[^\]]+\]+)?)\s+' +    # name (matches "FOO", "FOO.BAR" & "BODY[SECTION STUFF]") 
    698         '((?:\d+)' +                        # bare integer 
    699         '|(?:{\d+?})' +                     # IMAP literal 
    700         '|' + QUOTED_STRING + 
    701         '|' + PAREN_LIST + 
    702         ')\s*')) 
    703  
    704     DATA_RE = re.compile(( 
    705         '(' + QUOTED_STRING + 
    706         '|' + PAREN_LIST + 
    707         '|(?:\S+)' +            # word 
    708         ')\s*')) 
    709  
    710     def process_pairs(self, s): 
    711         """Break up and convert a string of FETCH response pairs 
    712  
    713         @param s: FETCH response string eg. "FOO 12 BAH (1 abc def "foo bar")" 
    714         @return: Tokenised and converted input return as (name, data) pairs. 
    715         """ 
    716         out = [] 
    717         for m in strict_finditer(self.PAIR_RE, s): 
    718             name, data = m.groups() 
    719             out.append((name, self.nativefy(data))) 
    720         return out 
    721  
    722     def process_list(self, s): 
    723         """Break up and convert a string of data items 
    724  
    725         @param s: FETCH response string eg. "(1 abc def "foo bar")" 
    726         @return: A list of converted items. 
    727         """ 
    728         if s == '': 
    729             return [] 
    730         out = [] 
    731         for m in strict_finditer(self.DATA_RE, s): 
    732             out.append(self.nativefy(m.group(1))) 
    733         return out 
    734  
    735     def nativefy(self, s): 
    736         if s.startswith('"'): 
    737             return s[1:-1]      # Debracket 
    738         elif s.startswith('{'): 
    739             return Literal(long(s[1:-1])) 
    740         elif s.startswith('('): 
    741             return self.process_list(s[1:-1]) 
    742         elif s.isdigit(): 
    743             return long(s) 
    744         elif s.upper() == 'NIL': 
    745             return None 
    746         else: 
    747             return s 
    748  
    749  
    750 class Literal(object): 
    751     """ 
    752     Simple class to represent a literal token in the fetch response 
    753     (eg. "{21}") 
    754     """ 
    755  
    756     def __init__(self, length): 
    757         self.length = length 
    758  
    759     def __eq__(self, other): 
    760         return self.length == other.length 
    761  
    762     def __str__(self): 
    763         return '{%d}' % self.length 
    764  
    765  
    766 def strict_finditer(regex, s): 
    767     """Like re.finditer except the regex must match from exactly where the 
    768     previous match ended and all the entire input must be matched. 
    769     """ 
    770     i = 0 
    771     matched = False 
    772     while 1: 
    773         match = regex.match(s[i:]) 
    774         if match: 
    775             matched = True 
    776             i += match.end() 
    777             yield match 
    778         else: 
    779             if (len(s) > 0 and not matched) or i < len(s): 
    780                 raise ValueError("failed to match all of input. " 
    781                         "%r remains" % s[i:]) 
    782             else: 
    783                 return 
    784615 
    785616 
     
    819650    if not dt.tzinfo: 
    820651        dt = dt.replace(tzinfo=FixedOffset.for_system()) 
    821  
    822652    return dt.strftime("%d-%b-%Y %H:%M:%S %z") 
    823      
    824  
     653 
     654