1# Copyright (c) 2014, Menno Smits
2# Released subject to the New BSD License
3# Please see http://en.wikipedia.org/wiki/BSD_licenses
4
5from __future__ import unicode_literals
6
7from collections import namedtuple
8from email.utils import formataddr
9
10import six
11
12from .util import to_unicode
13
14
15class Envelope(
16    namedtuple(
17        "Envelope",
18        "date subject from_ sender reply_to to " + "cc bcc in_reply_to message_id",
19    )
20):
21    r"""Represents envelope structures of messages. Returned when parsing
22    ENVELOPE responses.
23
24    :ivar date: A datetime instance that represents the "Date" header.
25    :ivar subject: A string that contains the "Subject" header.
26    :ivar from\_: A tuple of Address objects that represent one or more
27      addresses from the "From" header, or None if header does not exist.
28    :ivar sender: As for from\_ but represents the "Sender" header.
29    :ivar reply_to: As for from\_ but represents the "Reply-To" header.
30    :ivar to: As for from\_ but represents the "To" header.
31    :ivar cc: As for from\_ but represents the "Cc" header.
32    :ivar bcc: As for from\_ but represents the "Bcc" recipients.
33    :ivar in_reply_to: A string that contains the "In-Reply-To" header.
34    :ivar message_id: A string that contains the "Message-Id" header.
35
36    A particular issue to watch out for is IMAP's handling of "group
37    syntax" in address fields. This is often encountered as a
38    recipient header of the form::
39
40        undisclosed-recipients:;
41
42    but can also be expressed per this more general example::
43
44        A group: a@example.com, B <b@example.org>;
45
46    This example would yield the following Address tuples::
47
48      Address(name=None, route=None, mailbox=u'A group', host=None)
49      Address(name=None, route=None, mailbox=u'a', host=u'example.com')
50      Address(name=u'B', route=None, mailbox=u'b', host=u'example.org')
51      Address(name=None, route=None, mailbox=None, host=None)
52
53    The first Address, where ``host`` is ``None``, indicates the start
54    of the group. The ``mailbox`` field contains the group name. The
55    final Address, where both ``mailbox`` and ``host`` are ``None``,
56    indicates the end of the group.
57
58    See :rfc:`3501#section-7.4.2` and :rfc:`2822` for further details.
59
60    """
61
62
63class Address(namedtuple("Address", "name route mailbox host")):
64    """Represents electronic mail addresses. Used to store addresses in
65    :py:class:`Envelope`.
66
67    :ivar name: The address "personal name".
68    :ivar route: SMTP source route (rarely used).
69    :ivar mailbox: Mailbox name (what comes just before the @ sign).
70    :ivar host: The host/domain name.
71
72    As an example, an address header that looks like::
73
74        Mary Smith <mary@foo.com>
75
76    would be represented as::
77
78        Address(name=u'Mary Smith', route=None, mailbox=u'mary', host=u'foo.com')
79
80    See :rfc:`2822` for more detail.
81
82    See also :py:class:`Envelope` for information about handling of
83    "group syntax".
84    """
85
86    def __str__(self):
87        if self.mailbox and self.host:
88            address = to_unicode(self.mailbox) + "@" + to_unicode(self.host)
89        else:
90            address = to_unicode(self.mailbox or self.host)
91
92        return formataddr((to_unicode(self.name), address))
93
94
95class SearchIds(list):
96    """
97    Contains a list of message ids as returned by IMAPClient.search().
98
99    The *modseq* attribute will contain the MODSEQ value returned by
100    the server (only if the SEARCH command sent involved the MODSEQ
101    criteria). See :rfc:`4551` for more details.
102    """
103
104    def __init__(self, *args):
105        list.__init__(self, *args)
106        self.modseq = None
107
108
109class BodyData(tuple):
110    """
111    Returned when parsing BODY and BODYSTRUCTURE responses.
112    """
113
114    @classmethod
115    def create(cls, response):
116        # In case of multipart messages we will see at least 2 tuples
117        # at the start. Nest these in to a list so that the returned
118        # response tuple always has a consistent number of elements
119        # regardless of whether the message is multipart or not.
120        if isinstance(response[0], tuple):
121            # Multipart, find where the message part tuples stop
122            for i, part in enumerate(response):
123                if isinstance(part, six.binary_type):
124                    break
125            return cls(([cls.create(part) for part in response[:i]],) + response[i:])
126        else:
127            return cls(response)
128
129    @property
130    def is_multipart(self):
131        return isinstance(self[0], list)
132