"""Common utilies to format / semd emails.:organization: Logilab:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses"""__docformat__="restructuredtext en"frombase64importb64encode,b64decodefromitertoolsimportrepeatfromtimeimporttimefromemail.MIMEMultipartimportMIMEMultipartfromemail.MIMETextimportMIMETextfromemail.MIMEImageimportMIMEImagefromemail.HeaderimportHeadertry:fromsocketimportgethostnameexceptImportError:defgethostname():# gaereturn'XXX'fromcubicweb.viewimportEntityViewfromcubicweb.entityimportEntitydefheader(ustring):returnHeader(ustring.encode('UTF-8'),'UTF-8')defaddrheader(uaddr,uname=None):# even if an email address should be ascii, encode it using utf8 since# automatic tests may generate non ascii email addressaddr=uaddr.encode('UTF-8')ifuname:return'%s <%s>'%(header(uname).encode(),addr)returnaddrdefconstruct_message_id(appid,eid,withtimestamp=True):ifwithtimestamp:addrpart='eid=%s×tamp=%.10f'%(eid,time())else:addrpart='eid=%s'%eid# we don't want any equal sign nor trailing newlinesleftpart=b64encode(addrpart,'.-').rstrip().rstrip('=')return'<%s@%s.%s>'%(leftpart,appid,gethostname())defparse_message_id(msgid,appid):ifmsgid[0]=='<':msgid=msgid[1:]ifmsgid[-1]=='>':msgid=msgid[:-1]try:values,qualif=msgid.split('@')padding=len(values)%4values=b64decode(str(values+'='*padding),'.-')values=dict(v.split('=')forvinvalues.split('&'))fromappid,host=qualif.split('.',1)except:returnNoneifappid!=fromappidorhost!=gethostname():returnNonereturnvaluesdefformat_mail(uinfo,to_addrs,content,subject="",cc_addrs=(),msgid=None,references=(),config=None):"""Sends an Email to 'e_addr' with content 'content', and subject 'subject' to_addrs and cc_addrs are expected to be a list of email address without name """asserttype(content)isunicode,repr(content)msg=MIMEText(content.encode('UTF-8'),'plain','UTF-8')# safety: keep only the first newlinesubject=subject.splitlines()[0]msg['Subject']=header(subject)ifuinfo.get('email'):email=uinfo['email']elifconfigandconfig['sender-addr']:email=unicode(config['sender-addr'])else:email=u''ifuinfo.get('name'):name=uinfo['name']elifconfigandconfig['sender-addr']:name=unicode(config['sender-name'])else:name=u''msg['From']=addrheader(email,name)ifconfigandconfig['sender-addr']andconfig['sender-addr']!=email:appaddr=addrheader(config['sender-addr'],config['sender-name'])msg['Reply-to']='%s, %s'%(msg['From'],appaddr)elifemail:msg['Reply-to']=msg['From']ifconfigisnotNone:msg['X-CW']=config.appidunique_addrs=lambdaaddrs:sorted(set(addrforaddrinaddrsifaddrisnotNone))msg['To']=', '.join(addrheader(addr)foraddrinunique_addrs(to_addrs))ifcc_addrs:msg['Cc']=', '.join(addrheader(addr)foraddrinunique_addrs(cc_addrs))ifmsgid:msg['Message-id']=msgidifreferences:msg['References']=', '.join(references)returnmsgclassHtmlEmail(MIMEMultipart):def__init__(self,subject,textcontent,htmlcontent,sendermail=None,sendername=None,recipients=None,ccrecipients=None):MIMEMultipart.__init__(self,'related')self['Subject']=header(subject)self.preamble='This is a multi-part message in MIME format.'# Attach alternative text messagealternative=MIMEMultipart('alternative')self.attach(alternative)msgtext=MIMEText(textcontent.encode('UTF-8'),'plain','UTF-8')alternative.attach(msgtext)# Attach html messagemsghtml=MIMEText(htmlcontent.encode('UTF-8'),'html','UTF-8')alternative.attach(msghtml)ifsendermailorsendername:self['From']=addrheader(sendermail,sendername)ifrecipients:self['To']=', '.join(addrheader(addr)foraddrinrecipientsifaddrisnotNone)ifccrecipients:self['Cc']=', '.join(addrheader(addr)foraddrinccrecipientsifaddrisnotNone)defattach_image(self,data,htmlId):image=MIMEImage(data)image.add_header('Content-ID','<%s>'%htmlId)self.attach(image)classNotificationView(EntityView):"""abstract view implementing the "email" API (eg to simplify sending notification) """# XXX refactor this class to work with len(rset) > 1msgid_timestamp=True# this is usually the method to calldefrender_and_send(self,**kwargs):"""generate and send an email message for this view"""delayed=kwargs.pop('delay_to_commit',None)forrecipients,msginself.render_emails(**kwargs):ifdelayedisNone:self.send(recipients,msg)elifdelayed:self.send_on_commit(recipients,msg)else:self.send_now(recipients,msg)defcell_call(self,row,col=0,**kwargs):self.w(self.req._(self.content)%self.context(**kwargs))defrender_emails(self,**kwargs):"""generate and send emails for this view (one per recipient)"""self._kwargs=kwargsrecipients=self.recipients()ifnotrecipients:self.info('skipping %s notification, no recipients',self.id)returnifself.rsetisnotNone:entity=self.entity(self.rowor0,self.color0)# if the view is using timestamp in message ids, no way to reference# previous emailifnotself.msgid_timestamp:refs=[self.construct_message_id(eid)foreidinentity.notification_references(self)]else:refs=()msgid=self.construct_message_id(entity.eid)else:refs=()msgid=Nonereq=self.reqself.user_data=req.user_data()origlang=req.langforsomethinginrecipients:ifisinstance(something,Entity):# hi-jack self.req to get a session for the returned userself.req=self.req.hijack_user(something)emailaddr=something.get_email()else:emailaddr,lang=somethingself.req.set_language(lang)# since the same view (eg self) may be called multiple time and we# need a fresh stream at each iteration, reset it explicitlyself.w=None# XXX call render before subject to set .row/.col attributes on the# viewtry:content=self.render(row=0,col=0,**kwargs)subject=self.subject()exceptSkipEmail:continuemsg=format_mail(self.user_data,[emailaddr],content,subject,config=self.config,msgid=msgid,references=refs)yield[emailaddr],msg# restore languagereq.set_language(origlang)# recipients / email sending ###############################################defrecipients(self):"""return a list of either 2-uple (email, language) or user entity to who this email should be sent """finder=self.vreg['components'].select('recipients_finder',self.req,rset=self.rset,row=self.rowor0,col=self.color0)returnfinder.recipients()defsend_now(self,recipients,msg):self.config.sendmails([(msg,recipients)])defsend_on_commit(self,recipients,msg):raiseNotImplementedErrorsend=send_now# email generation helpers #################################################defconstruct_message_id(self,eid):returnconstruct_message_id(self.config.appid,eid,self.msgid_timestamp)defformat_field(self,attr,value):return':%(attr)s: %(value)s'%{'attr':attr,'value':value}defformat_section(self,attr,value):return'%(attr)s\n%(ul)s\n%(value)s\n'%{'attr':attr,'ul':'-'*len(attr),'value':value}defsubject(self):entity=self.entity(self.rowor0,self.color0)subject=self.req._(self.message)etype=entity.dc_type()eid=entity.eidlogin=self.user_data['login']returnself.req._('%(subject)s%(etype)s #%(eid)s (%(login)s)')%locals()defcontext(self,**kwargs):entity=self.entity(self.rowor0,self.color0)forkey,valinkwargs.iteritems():ifvalandisinstance(val,unicode)andval.strip():kwargs[key]=self.req._(val)kwargs.update({'user':self.user_data['login'],'eid':entity.eid,'etype':entity.dc_type(),'url':entity.absolute_url(),'title':entity.dc_long_title(),})returnkwargsclassSkipEmail(Exception):"""raise this if you decide to skip an email during its generation"""