6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses |
6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses |
7 """ |
7 """ |
8 __docformat__ = "restructuredtext en" |
8 __docformat__ = "restructuredtext en" |
9 _ = unicode |
9 _ = unicode |
10 |
10 |
11 from base64 import b64encode, b64decode |
|
12 from itertools import repeat |
11 from itertools import repeat |
13 from time import time |
|
14 try: |
|
15 from socket import gethostname |
|
16 except ImportError: |
|
17 def gethostname(): # gae |
|
18 return 'XXX' |
|
19 |
12 |
20 from logilab.common.textutils import normalize_text |
13 from logilab.common.textutils import normalize_text |
|
14 from logilab.common.deprecation import class_renamed, deprecated |
21 |
15 |
22 from cubicweb.selectors import yes |
16 from cubicweb.selectors import yes |
23 from cubicweb.view import EntityView, Component |
17 from cubicweb.view import Component |
24 from cubicweb.common.mail import format_mail |
18 from cubicweb.common.mail import format_mail |
25 |
19 from cubicweb.common.mail import NotificationView |
26 from cubicweb.server.hookhelper import SendMailOp |
20 from cubicweb.server.hookhelper import SendMailOp |
27 |
21 |
28 |
22 |
29 class RecipientsFinder(Component): |
23 class RecipientsFinder(Component): |
30 """this component is responsible to find recipients of a notification |
24 """this component is responsible to find recipients of a notification |
53 return dests |
47 return dests |
54 |
48 |
55 |
49 |
56 # abstract or deactivated notification views and mixin ######################## |
50 # abstract or deactivated notification views and mixin ######################## |
57 |
51 |
58 class NotificationView(EntityView): |
52 class NotificationView(NotificationView): |
59 """abstract view implementing the email API |
53 """overriden to delay actual sending of mails to a commit operation by |
60 |
54 default |
61 all you have to do by default is : |
|
62 * set id and accepts attributes to match desired events and entity types |
|
63 * set a content attribute to define the content of the email (unless you |
|
64 override call) |
|
65 """ |
55 """ |
66 # XXX refactor this class to work with len(rset) > 1 |
56 def send_on_commit(self, recipients, msg): |
67 |
|
68 msgid_timestamp = True |
|
69 |
|
70 def recipients(self): |
|
71 finder = self.vreg['components'].select('recipients_finder', self.req, |
|
72 rset=self.rset) |
|
73 return finder.recipients() |
|
74 |
|
75 def subject(self): |
|
76 entity = self.rset.get_entity(self.row or 0, self.col or 0) |
|
77 subject = self.req._(self.message) |
|
78 etype = entity.dc_type() |
|
79 eid = entity.eid |
|
80 login = self.user_login() |
|
81 return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals() |
|
82 |
|
83 def user_login(self): |
|
84 # req is actually a session (we are on the server side), and we have to |
|
85 # prevent nested internal session |
|
86 return self.req.actual_session().user.login |
|
87 |
|
88 def context(self, **kwargs): |
|
89 entity = self.rset.get_entity(self.row or 0, self.col or 0) |
|
90 for key, val in kwargs.iteritems(): |
|
91 if val and isinstance(val, unicode) and val.strip(): |
|
92 kwargs[key] = self.req._(val) |
|
93 kwargs.update({'user': self.user_login(), |
|
94 'eid': entity.eid, |
|
95 'etype': entity.dc_type(), |
|
96 'url': entity.absolute_url(), |
|
97 'title': entity.dc_long_title(),}) |
|
98 return kwargs |
|
99 |
|
100 def cell_call(self, row, col=0, **kwargs): |
|
101 self.w(self.req._(self.content) % self.context(**kwargs)) |
|
102 |
|
103 def construct_message_id(self, eid): |
|
104 return construct_message_id(self.req.vreg.config.appid, eid, self.msgid_timestamp) |
|
105 |
|
106 def render_and_send(self, **kwargs): |
|
107 """generate and send an email message for this view""" |
|
108 self._kwargs = kwargs |
|
109 recipients = self.recipients() |
|
110 if not recipients: |
|
111 self.info('skipping %s notification, no recipients', self.id) |
|
112 return |
|
113 if not isinstance(recipients[0], tuple): |
|
114 from warnings import warn |
|
115 warn('recipients should now return a list of 2-uple (email, language)', |
|
116 DeprecationWarning, stacklevel=1) |
|
117 lang = self.vreg.property_value('ui.language') |
|
118 recipients = zip(recipients, repeat(lang)) |
|
119 if self.rset is not None: |
|
120 entity = self.rset.get_entity(self.row or 0, self.col or 0) |
|
121 # if the view is using timestamp in message ids, no way to reference |
|
122 # previous email |
|
123 if not self.msgid_timestamp: |
|
124 refs = [self.construct_message_id(eid) |
|
125 for eid in entity.notification_references(self)] |
|
126 else: |
|
127 refs = () |
|
128 msgid = self.construct_message_id(entity.eid) |
|
129 else: |
|
130 refs = () |
|
131 msgid = None |
|
132 userdata = self.req.user_data() |
|
133 origlang = self.req.lang |
|
134 for emailaddr, lang in recipients: |
|
135 self.req.set_language(lang) |
|
136 # since the same view (eg self) may be called multiple time and we |
|
137 # need a fresh stream at each iteration, reset it explicitly |
|
138 self.w = None |
|
139 # XXX call render before subject to set .row/.col attributes on the |
|
140 # view |
|
141 content = self.render(row=0, col=0, **kwargs) |
|
142 subject = self.subject() |
|
143 msg = format_mail(userdata, [emailaddr], content, subject, |
|
144 config=self.req.vreg.config, msgid=msgid, references=refs) |
|
145 self.send([emailaddr], msg) |
|
146 # restore language |
|
147 self.req.set_language(origlang) |
|
148 |
|
149 def send(self, recipients, msg): |
|
150 SendMailOp(self.req, recipients=recipients, msg=msg) |
57 SendMailOp(self.req, recipients=recipients, msg=msg) |
151 |
58 send = send_on_commit |
152 |
|
153 def construct_message_id(appid, eid, withtimestamp=True): |
|
154 if withtimestamp: |
|
155 addrpart = 'eid=%s×tamp=%.10f' % (eid, time()) |
|
156 else: |
|
157 addrpart = 'eid=%s' % eid |
|
158 # we don't want any equal sign nor trailing newlines |
|
159 leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=') |
|
160 return '<%s@%s.%s>' % (leftpart, appid, gethostname()) |
|
161 |
|
162 |
|
163 def parse_message_id(msgid, appid): |
|
164 if msgid[0] == '<': |
|
165 msgid = msgid[1:] |
|
166 if msgid[-1] == '>': |
|
167 msgid = msgid[:-1] |
|
168 try: |
|
169 values, qualif = msgid.split('@') |
|
170 padding = len(values) % 4 |
|
171 values = b64decode(str(values + '='*padding), '.-') |
|
172 values = dict(v.split('=') for v in values.split('&')) |
|
173 fromappid, host = qualif.split('.', 1) |
|
174 except: |
|
175 return None |
|
176 if appid != fromappid or host != gethostname(): |
|
177 return None |
|
178 return values |
|
179 |
59 |
180 |
60 |
181 class StatusChangeMixIn(object): |
61 class StatusChangeMixIn(object): |
182 id = 'notif_status_change' |
62 id = 'notif_status_change' |
183 msgid_timestamp = True |
63 msgid_timestamp = True |