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 |
21 from logilab.common.deprecation import class_renamed |
14 from logilab.common.deprecation import class_renamed, deprecated |
22 |
15 |
23 from cubicweb import RegistryException |
16 from cubicweb import RegistryException |
24 from cubicweb.selectors import implements, yes |
17 from cubicweb.selectors import implements, yes |
25 from cubicweb.view import EntityView, Component |
18 from cubicweb.view import Component |
26 from cubicweb.common.mail import format_mail |
19 from cubicweb.common.mail import NotificationView, parse_message_id |
27 |
|
28 from cubicweb.server.pool import PreCommitOperation |
20 from cubicweb.server.pool import PreCommitOperation |
29 from cubicweb.server.hookhelper import SendMailOp |
21 from cubicweb.server.hookhelper import SendMailOp |
30 from cubicweb.server.hooksmanager import Hook |
22 from cubicweb.server.hooksmanager import Hook |
|
23 |
|
24 parse_message_id = deprecated('parse_message_id is now defined in cubicweb.common.mail') |
31 |
25 |
32 |
26 |
33 class RecipientsFinder(Component): |
27 class RecipientsFinder(Component): |
34 """this component is responsible to find recipients of a notification |
28 """this component is responsible to find recipients of a notification |
35 |
29 |
123 RenderAndSendNotificationView(session, view=view) |
118 RenderAndSendNotificationView(session, view=view) |
124 |
119 |
125 |
120 |
126 # abstract or deactivated notification views and mixin ######################## |
121 # abstract or deactivated notification views and mixin ######################## |
127 |
122 |
128 class NotificationView(EntityView): |
123 class NotificationView(NotificationView): |
129 """abstract view implementing the email API |
124 """overriden to delay actual sending of mails to a commit operation by |
|
125 default |
|
126 """ |
130 |
127 |
131 all you have to do by default is : |
128 def send_on_commit(self, recipients, msg): |
132 * set id and accepts attributes to match desired events and entity types |
|
133 * set a content attribute to define the content of the email (unless you |
|
134 override call) |
|
135 """ |
|
136 # XXX refactor this class to work with len(rset) > 1 |
|
137 |
|
138 msgid_timestamp = True |
|
139 |
|
140 def recipients(self): |
|
141 finder = self.vreg['components'].select('recipients_finder', self.req, |
|
142 rset=self.rset) |
|
143 return finder.recipients() |
|
144 |
|
145 def subject(self): |
|
146 entity = self.entity(self.row or 0, self.col or 0) |
|
147 subject = self.req._(self.message) |
|
148 etype = entity.dc_type() |
|
149 eid = entity.eid |
|
150 login = self.user_login() |
|
151 return self.req._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals() |
|
152 |
|
153 def user_login(self): |
|
154 # req is actually a session (we are on the server side), and we have to |
|
155 # prevent nested internal session |
|
156 return self.req.actual_session().user.login |
|
157 |
|
158 def context(self, **kwargs): |
|
159 entity = self.entity(self.row or 0, self.col or 0) |
|
160 for key, val in kwargs.iteritems(): |
|
161 if val and isinstance(val, unicode) and val.strip(): |
|
162 kwargs[key] = self.req._(val) |
|
163 kwargs.update({'user': self.user_login(), |
|
164 'eid': entity.eid, |
|
165 'etype': entity.dc_type(), |
|
166 'url': entity.absolute_url(), |
|
167 'title': entity.dc_long_title(),}) |
|
168 return kwargs |
|
169 |
|
170 def cell_call(self, row, col=0, **kwargs): |
|
171 self.w(self.req._(self.content) % self.context(**kwargs)) |
|
172 |
|
173 def construct_message_id(self, eid): |
|
174 return construct_message_id(self.config.appid, eid, self.msgid_timestamp) |
|
175 |
|
176 def render_and_send(self, **kwargs): |
|
177 """generate and send an email message for this view""" |
|
178 self._kwargs = kwargs |
|
179 recipients = self.recipients() |
|
180 if not recipients: |
|
181 self.info('skipping %s notification, no recipients', self.id) |
|
182 return |
|
183 if not isinstance(recipients[0], tuple): |
|
184 from warnings import warn |
|
185 warn('recipients should now return a list of 2-uple (email, language)', |
|
186 DeprecationWarning, stacklevel=1) |
|
187 lang = self.vreg.property_value('ui.language') |
|
188 recipients = zip(recipients, repeat(lang)) |
|
189 if self.rset is not None: |
|
190 entity = self.entity(self.row or 0, self.col or 0) |
|
191 # if the view is using timestamp in message ids, no way to reference |
|
192 # previous email |
|
193 if not self.msgid_timestamp: |
|
194 refs = [self.construct_message_id(eid) |
|
195 for eid in entity.notification_references(self)] |
|
196 else: |
|
197 refs = () |
|
198 msgid = self.construct_message_id(entity.eid) |
|
199 else: |
|
200 refs = () |
|
201 msgid = None |
|
202 userdata = self.req.user_data() |
|
203 origlang = self.req.lang |
|
204 for emailaddr, lang in recipients: |
|
205 self.req.set_language(lang) |
|
206 # since the same view (eg self) may be called multiple time and we |
|
207 # need a fresh stream at each iteration, reset it explicitly |
|
208 self.w = None |
|
209 # XXX call render before subject to set .row/.col attributes on the |
|
210 # view |
|
211 content = self.render(row=0, col=0, **kwargs) |
|
212 subject = self.subject() |
|
213 msg = format_mail(userdata, [emailaddr], content, subject, |
|
214 config=self.config, msgid=msgid, references=refs) |
|
215 self.send([emailaddr], msg) |
|
216 # restore language |
|
217 self.req.set_language(origlang) |
|
218 |
|
219 def send(self, recipients, msg): |
|
220 SendMailOp(self.req, recipients=recipients, msg=msg) |
129 SendMailOp(self.req, recipients=recipients, msg=msg) |
221 |
130 send = send_on_commit |
222 |
|
223 def construct_message_id(appid, eid, withtimestamp=True): |
|
224 if withtimestamp: |
|
225 addrpart = 'eid=%s×tamp=%.10f' % (eid, time()) |
|
226 else: |
|
227 addrpart = 'eid=%s' % eid |
|
228 # we don't want any equal sign nor trailing newlines |
|
229 leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=') |
|
230 return '<%s@%s.%s>' % (leftpart, appid, gethostname()) |
|
231 |
|
232 |
|
233 def parse_message_id(msgid, appid): |
|
234 if msgid[0] == '<': |
|
235 msgid = msgid[1:] |
|
236 if msgid[-1] == '>': |
|
237 msgid = msgid[:-1] |
|
238 try: |
|
239 values, qualif = msgid.split('@') |
|
240 padding = len(values) % 4 |
|
241 values = b64decode(str(values + '='*padding), '.-') |
|
242 values = dict(v.split('=') for v in values.split('&')) |
|
243 fromappid, host = qualif.split('.', 1) |
|
244 except: |
|
245 return None |
|
246 if appid != fromappid or host != gethostname(): |
|
247 return None |
|
248 return values |
|
249 |
|
250 |
131 |
251 class StatusChangeMixIn(object): |
132 class StatusChangeMixIn(object): |
252 id = 'notif_status_change' |
133 id = 'notif_status_change' |
253 msgid_timestamp = True |
134 msgid_timestamp = True |
254 message = _('status changed') |
135 message = _('status changed') |