entities.py 4.12 KB
Newer Older
Nicolas Chauvat's avatar
Nicolas Chauvat committed
1
2
3
"""entity classes for entity types provided by the cubicweb email package

:organization: Logilab
4
:copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
Nicolas Chauvat's avatar
Nicolas Chauvat committed
5
6
7
8
9
10
11
12
13
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"

import re

from logilab.common import umessage

from cubicweb.interfaces import ITree
Sylvain Thénault's avatar
Sylvain Thénault committed
14
from cubicweb.mixins import TreeMixIn
Nicolas Chauvat's avatar
Nicolas Chauvat committed
15
16
17
18
19
20
21
from cubicweb.entities import AnyEntity, fetch_config

from cubes.email.emailcites import parse_body


class Email(TreeMixIn, AnyEntity):
    """customized class for Email entities"""
Sylvain Thénault's avatar
Sylvain Thénault committed
22
    __regid__ = 'Email'
Nicolas Chauvat's avatar
Nicolas Chauvat committed
23
24
    fetch_attrs, fetch_order = fetch_config(['subject'])
    __implements__ = AnyEntity.__implements__ + (ITree,)
25

Nicolas Chauvat's avatar
Nicolas Chauvat committed
26
    tree_attribute = 'reply_to'
27
28
29
30

    def parent(self):
        """for breadcrumbs"""
        return self.thread
31

Nicolas Chauvat's avatar
Nicolas Chauvat committed
32
33
34
35
36
    def dc_title(self):
        return self.subject

    @property
    def senderaddr(self):
37
        return self.sender and self.sender[0] or None
38

Nicolas Chauvat's avatar
Nicolas Chauvat committed
39
40
    @property
    def in_reply_to(self):
41
        return self.reply_to and self.reply_to[0] or None
42

Nicolas Chauvat's avatar
Nicolas Chauvat committed
43
44
    @property
    def thread(self):
45
        return self.in_thread and self.in_thread[0] or None
46

Nicolas Chauvat's avatar
Nicolas Chauvat committed
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    def parts_in_order(self, prefered_mime_type='text/html'):
        """sort an email parts in order, selecting among alternatives according to a
        prefered mime type
        """
        parts_by_eid = {}
        alternatives = []
        result = []
        for part in self.parts:
            parts_by_eid[part.eid] = part
            if part.alternative:
                for altset in alternatives:
                    if part.eid in altset:
                        break
                else:
                    alternatives.append(set(p.eid for p in part.alternative))
                    alternatives[-1].add(part.eid)
            else:
64
                result.append(part)
Nicolas Chauvat's avatar
Nicolas Chauvat committed
65
66
67
68
69
70
71
72
        if alternatives:
            for altset in alternatives:
                selected = [parts_by_eid[peid] for peid in altset
                            if parts_by_eid[peid].content_format == prefered_mime_type]
                if selected:
                    selected = selected[0]
                else:
                    selected = parts_by_eid[altset.pop()]
73
74
                result.append(selected)
        return sorted(result, key=lambda x: x.ordernum)
Nicolas Chauvat's avatar
Nicolas Chauvat committed
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

    def references(self):
        result = set()
        message = self.umessage_headers()
        if not message:
            return result
        replyto = message.get('In-reply-to')
        if replyto:
            result.add(replyto)
        result.update(message.get_all('References', ()))
        return result

    lines_rgx = re.compile('^Lines:\s*\d+\s*\n', re.I|re.U|re.M)
    clength_rgx = re.compile('^Content-Length:\s*\d+\s*\n', re.I|re.U|re.M)
    ctype_rgx = re.compile('^Content-Type:[^:]', re.I|re.U|re.M)
90

Nicolas Chauvat's avatar
Nicolas Chauvat committed
91
92
93
94
95
96
97
98
    def umessage_headers(self):
        if not self.headers:
            return None
        headers = self.lines_rgx.sub('', self.headers)
        headers = self.clength_rgx.sub('', headers)
        headers = self.ctype_rgx.sub('Content-type: text/plain; charset=utf8', headers)
        headers = headers.encode('utf8')
        return umessage.message_from_string(headers + '\n\n')
99
100


Nicolas Chauvat's avatar
Nicolas Chauvat committed
101
102
class EmailPart(AnyEntity):
    """customized class for EmailPart entities"""
Sylvain Thénault's avatar
Sylvain Thénault committed
103
    __regid__ = 'EmailPart'
Nicolas Chauvat's avatar
Nicolas Chauvat committed
104
105
106
107

    def dc_title(self):
        return '%s (%s %s)' % (self.email.subject,
                               self.req._('part'), self.ordernum)
108

Nicolas Chauvat's avatar
Nicolas Chauvat committed
109
110
111
    @property
    def email(self):
        return self.reverse_parts[0]
112

113
114
115
    def parent(self):
        """for breadcrumbs"""
        return self.email
116

Nicolas Chauvat's avatar
Nicolas Chauvat committed
117
118
119
120
121
122
    def actual_content(self):
        """return content of this part with citations and signature removed

        this method may raise `TransformError` exception if the part can't
        be displayed as text/plain.
        """
123
        content = self.printable_value('content', format='text/plain')
Nicolas Chauvat's avatar
Nicolas Chauvat committed
124
        return parse_body(content).actual_content
125
126


Nicolas Chauvat's avatar
Nicolas Chauvat committed
127
128
class EmailThread(AnyEntity):
    """customized class for EmailThread entities"""
Sylvain Thénault's avatar
Sylvain Thénault committed
129
    __regid__ = 'EmailThread'
Nicolas Chauvat's avatar
Nicolas Chauvat committed
130
131
132
133
    fetch_attrs, fetch_order = fetch_config(['title'])

    def dc_title(self):
        return self.title