| 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 |