mail.py
changeset 4023 eae23c40627a
parent 3998 94cc7cad3d2d
child 4252 6c4f109c2b03
equal deleted inserted replaced
4022:934e758a73ef 4023:eae23c40627a
       
     1 """Common utilies to format / semd emails.
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from base64 import b64encode, b64decode
       
    11 from itertools import repeat
       
    12 from time import time
       
    13 from email.MIMEMultipart import MIMEMultipart
       
    14 from email.MIMEText import MIMEText
       
    15 from email.MIMEImage import MIMEImage
       
    16 from email.Header import Header
       
    17 try:
       
    18     from socket import gethostname
       
    19 except ImportError:
       
    20     def gethostname(): # gae
       
    21         return 'XXX'
       
    22 
       
    23 from cubicweb.view import EntityView
       
    24 from cubicweb.entity import Entity
       
    25 
       
    26 def header(ustring):
       
    27     return Header(ustring.encode('UTF-8'), 'UTF-8')
       
    28 
       
    29 def addrheader(uaddr, uname=None):
       
    30     # even if an email address should be ascii, encode it using utf8 since
       
    31     # automatic tests may generate non ascii email address
       
    32     addr = uaddr.encode('UTF-8')
       
    33     if uname:
       
    34         return '%s <%s>' % (header(uname).encode(), addr)
       
    35     return addr
       
    36 
       
    37 
       
    38 def construct_message_id(appid, eid, withtimestamp=True):
       
    39     if withtimestamp:
       
    40         addrpart = 'eid=%s&timestamp=%.10f' % (eid, time())
       
    41     else:
       
    42         addrpart = 'eid=%s' % eid
       
    43     # we don't want any equal sign nor trailing newlines
       
    44     leftpart = b64encode(addrpart, '.-').rstrip().rstrip('=')
       
    45     return '<%s@%s.%s>' % (leftpart, appid, gethostname())
       
    46 
       
    47 
       
    48 def parse_message_id(msgid, appid):
       
    49     if msgid[0] == '<':
       
    50         msgid = msgid[1:]
       
    51     if msgid[-1] == '>':
       
    52         msgid = msgid[:-1]
       
    53     try:
       
    54         values, qualif = msgid.split('@')
       
    55         padding = len(values) % 4
       
    56         values = b64decode(str(values + '='*padding), '.-')
       
    57         values = dict(v.split('=') for v in values.split('&'))
       
    58         fromappid, host = qualif.split('.', 1)
       
    59     except:
       
    60         return None
       
    61     if appid != fromappid or host != gethostname():
       
    62         return None
       
    63     return values
       
    64 
       
    65 
       
    66 def format_mail(uinfo, to_addrs, content, subject="",
       
    67                 cc_addrs=(), msgid=None, references=(), config=None):
       
    68     """Sends an Email to 'e_addr' with content 'content', and subject 'subject'
       
    69 
       
    70     to_addrs and cc_addrs are expected to be a list of email address without
       
    71     name
       
    72     """
       
    73     assert type(content) is unicode, repr(content)
       
    74     msg = MIMEText(content.encode('UTF-8'), 'plain', 'UTF-8')
       
    75     # safety: keep only the first newline
       
    76     subject = subject.splitlines()[0]
       
    77     msg['Subject'] = header(subject)
       
    78     if uinfo.get('email'):
       
    79         email = uinfo['email']
       
    80     elif config and config['sender-addr']:
       
    81         email = unicode(config['sender-addr'])
       
    82     else:
       
    83         email = u''
       
    84     if uinfo.get('name'):
       
    85         name = uinfo['name']
       
    86     elif config and config['sender-addr']:
       
    87         name = unicode(config['sender-name'])
       
    88     else:
       
    89         name = u''
       
    90     msg['From'] = addrheader(email, name)
       
    91     if config and config['sender-addr'] and config['sender-addr'] != email:
       
    92         appaddr = addrheader(config['sender-addr'], config['sender-name'])
       
    93         msg['Reply-to'] = '%s, %s' % (msg['From'], appaddr)
       
    94     elif email:
       
    95         msg['Reply-to'] = msg['From']
       
    96     if config is not None:
       
    97         msg['X-CW'] = config.appid
       
    98     unique_addrs = lambda addrs: sorted(set(addr for addr in addrs if addr is not None))
       
    99     msg['To'] = ', '.join(addrheader(addr) for addr in unique_addrs(to_addrs))
       
   100     if cc_addrs:
       
   101         msg['Cc'] = ', '.join(addrheader(addr) for addr in unique_addrs(cc_addrs))
       
   102     if msgid:
       
   103         msg['Message-id'] = msgid
       
   104     if references:
       
   105         msg['References'] = ', '.join(references)
       
   106     return msg
       
   107 
       
   108 
       
   109 class HtmlEmail(MIMEMultipart):
       
   110 
       
   111     def __init__(self, subject, textcontent, htmlcontent,
       
   112                  sendermail=None, sendername=None, recipients=None, ccrecipients=None):
       
   113         MIMEMultipart.__init__(self, 'related')
       
   114         self['Subject'] = header(subject)
       
   115         self.preamble = 'This is a multi-part message in MIME format.'
       
   116         # Attach alternative text message
       
   117         alternative = MIMEMultipart('alternative')
       
   118         self.attach(alternative)
       
   119         msgtext = MIMEText(textcontent.encode('UTF-8'), 'plain', 'UTF-8')
       
   120         alternative.attach(msgtext)
       
   121         # Attach html message
       
   122         msghtml = MIMEText(htmlcontent.encode('UTF-8'), 'html', 'UTF-8')
       
   123         alternative.attach(msghtml)
       
   124         if sendermail or sendername:
       
   125             self['From'] = addrheader(sendermail, sendername)
       
   126         if recipients:
       
   127             self['To'] = ', '.join(addrheader(addr) for addr in recipients if addr is not None)
       
   128         if ccrecipients:
       
   129             self['Cc'] = ', '.join(addrheader(addr) for addr in ccrecipients if addr is not None)
       
   130 
       
   131     def attach_image(self, data, htmlId):
       
   132         image = MIMEImage(data)
       
   133         image.add_header('Content-ID', '<%s>' % htmlId)
       
   134         self.attach(image)
       
   135 
       
   136 
       
   137 class NotificationView(EntityView):
       
   138     """abstract view implementing the "email" API (eg to simplify sending
       
   139     notification)
       
   140     """
       
   141     # XXX refactor this class to work with len(rset) > 1
       
   142 
       
   143     msgid_timestamp = True
       
   144 
       
   145     # this is usually the method to call
       
   146     def render_and_send(self, **kwargs):
       
   147         """generate and send an email message for this view"""
       
   148         delayed = kwargs.pop('delay_to_commit', None)
       
   149         for recipients, msg in self.render_emails(**kwargs):
       
   150             if delayed is None:
       
   151                 self.send(recipients, msg)
       
   152             elif delayed:
       
   153                 self.send_on_commit(recipients, msg)
       
   154             else:
       
   155                 self.send_now(recipients, msg)
       
   156 
       
   157     def cell_call(self, row, col=0, **kwargs):
       
   158         self.w(self._cw._(self.content) % self.context(**kwargs))
       
   159 
       
   160     def render_emails(self, **kwargs):
       
   161         """generate and send emails for this view (one per recipient)"""
       
   162         self._kwargs = kwargs
       
   163         recipients = self.recipients()
       
   164         if not recipients:
       
   165             self.info('skipping %s notification, no recipients', self.__regid__)
       
   166             return
       
   167         if self.cw_rset is not None:
       
   168             entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
       
   169             # if the view is using timestamp in message ids, no way to reference
       
   170             # previous email
       
   171             if not self.msgid_timestamp:
       
   172                 refs = [self.construct_message_id(eid)
       
   173                         for eid in entity.notification_references(self)]
       
   174             else:
       
   175                 refs = ()
       
   176             msgid = self.construct_message_id(entity.eid)
       
   177         else:
       
   178             refs = ()
       
   179             msgid = None
       
   180         req = self._cw
       
   181         self.user_data = req.user_data()
       
   182         origlang = req.lang
       
   183         for something in recipients:
       
   184             if isinstance(something, Entity):
       
   185                 # hi-jack self._cw to get a session for the returned user
       
   186                 self._cw = self._cw.hijack_user(something)
       
   187                 emailaddr = something.get_email()
       
   188             else:
       
   189                 emailaddr, lang = something
       
   190                 self._cw.set_language(lang)
       
   191             # since the same view (eg self) may be called multiple time and we
       
   192             # need a fresh stream at each iteration, reset it explicitly
       
   193             self.w = None
       
   194             # XXX call render before subject to set .row/.col attributes on the
       
   195             #     view
       
   196             try:
       
   197                 content = self.render(row=0, col=0, **kwargs)
       
   198                 subject = self.subject()
       
   199             except SkipEmail:
       
   200                 continue
       
   201             except Exception, ex:
       
   202                 # shouldn't make the whole transaction fail because of rendering
       
   203                 # error (unauthorized or such)
       
   204                 self.exception(str(ex))
       
   205                 continue
       
   206             msg = format_mail(self.user_data, [emailaddr], content, subject,
       
   207                               config=self._cw.vreg.config, msgid=msgid, references=refs)
       
   208             yield [emailaddr], msg
       
   209         # restore language
       
   210         req.set_language(origlang)
       
   211 
       
   212     # recipients / email sending ###############################################
       
   213 
       
   214     def recipients(self):
       
   215         """return a list of either 2-uple (email, language) or user entity to
       
   216         who this email should be sent
       
   217         """
       
   218         # use super_session when available, we don't want to consider security
       
   219         # when selecting recipients_finder
       
   220         try:
       
   221             req = self._cw.super_session
       
   222         except AttributeError:
       
   223             req = self._cw
       
   224         finder = self._cw.vreg['components'].select('recipients_finder', req,
       
   225                                                     rset=self.cw_rset,
       
   226                                                     row=self.cw_row or 0,
       
   227                                                     col=self.cw_col or 0)
       
   228         return finder.recipients()
       
   229 
       
   230     def send_now(self, recipients, msg):
       
   231         self._cw.vreg.config.sendmails([(msg, recipients)])
       
   232 
       
   233     def send_on_commit(self, recipients, msg):
       
   234         raise NotImplementedError
       
   235 
       
   236     send = send_now
       
   237 
       
   238     # email generation helpers #################################################
       
   239 
       
   240     def construct_message_id(self, eid):
       
   241         return construct_message_id(self._cw.vreg.config.appid, eid, self.msgid_timestamp)
       
   242 
       
   243     def format_field(self, attr, value):
       
   244         return ':%(attr)s: %(value)s' % {'attr': attr, 'value': value}
       
   245 
       
   246     def format_section(self, attr, value):
       
   247         return '%(attr)s\n%(ul)s\n%(value)s\n' % {
       
   248             'attr': attr, 'ul': '-'*len(attr), 'value': value}
       
   249 
       
   250     def subject(self):
       
   251         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
       
   252         subject = self._cw._(self.message)
       
   253         etype = entity.dc_type()
       
   254         eid = entity.eid
       
   255         login = self.user_data['login']
       
   256         return self._cw._('%(subject)s %(etype)s #%(eid)s (%(login)s)') % locals()
       
   257 
       
   258     def context(self, **kwargs):
       
   259         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
       
   260         for key, val in kwargs.iteritems():
       
   261             if val and isinstance(val, unicode) and val.strip():
       
   262                kwargs[key] = self._cw._(val)
       
   263         kwargs.update({'user': self.user_data['login'],
       
   264                        'eid': entity.eid,
       
   265                        'etype': entity.dc_type(),
       
   266                        'url': entity.absolute_url(),
       
   267                        'title': entity.dc_long_title(),})
       
   268         return kwargs
       
   269 
       
   270 
       
   271 class SkipEmail(Exception):
       
   272     """raise this if you decide to skip an email during its generation"""