diff --git a/addons/email_template/__init__.py b/addons/email_template/__init__.py new file mode 100755 index 00000000000..c959f7204da --- /dev/null +++ b/addons/email_template/__init__.py @@ -0,0 +1,5 @@ +import email_template_account +import email_template +import email_template_mailbox +import wizard + diff --git a/addons/email_template/__openerp__.py b/addons/email_template/__openerp__.py new file mode 100755 index 00000000000..9c5f7bb5c70 --- /dev/null +++ b/addons/email_template/__openerp__.py @@ -0,0 +1,24 @@ +{ + "name" : "Email Template for Open ERP", + "version" : "0.7 RC", + "author" : "Open ERP", + "website" : "http://openerp.com", + "category" : "Added functionality", + "depends" : ['base'], + "description": """ + Email Template is extraction of Power Email basically just to send the emails. + """, + "init_xml": ['email_template_scheduler_data.xml'], + "update_xml": [ + 'security/email_template_security.xml', + 'email_template_workflow.xml', + 'email_template_account_view.xml', + 'email_template_view.xml', + 'email_template_mailbox_view.xml', + 'wizard/email_template_send_wizard_view.xml', + ], + "installable": True, + "active": False, +} + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py new file mode 100755 index 00000000000..6c46da458c6 --- /dev/null +++ b/addons/email_template/email_template.py @@ -0,0 +1,698 @@ +import base64 +import random +import time +import types +import netsvc + +LOGGER = netsvc.Logger() + +TEMPLATE_ENGINES = [] + +from osv import osv, fields +from tools.translate import _ +from mako.template import Template #For backward combatibility +try: + from mako.template import Template as MakoTemplate + from mako import exceptions + TEMPLATE_ENGINES.append(('mako', 'Mako Templates')) +except: + LOGGER.notifyChannel( + _("Email Template"), + netsvc.LOG_ERROR, + _("Mako templates not installed") + ) +try: + from django.template import Context, Template as DjangoTemplate + #Workaround for bug: + #http://code.google.com/p/django-tagging/issues/detail?id=110 + from django.conf import settings + settings.configure() + #Workaround ends + TEMPLATE_ENGINES.append(('django', 'Django Template')) +except: + LOGGER.notifyChannel( + _("Email Template"), + netsvc.LOG_ERROR, + _("Django templates not installed") + ) + +import email_template_engines +import tools +import report +import pooler + + +def get_value(cursor, user, recid, message=None, template=None, context=None): + """ + Evaluates an expression and returns its value + @param cursor: Database Cursor + @param user: ID of current user + @param recid: ID of the target record under evaluation + @param message: The expression to be evaluated + @param template: BrowseRecord object of the current template + @param context: Open ERP Context + @return: Computed message (unicode) or u"" + """ + pool = pooler.get_pool(cursor.dbname) + if message is None: + message = {} + #Returns the computed expression + if message: + try: + message = tools.ustr(message) + object = pool.get(template.model_int_name).browse(cursor, user, recid, context) + env = { + 'user':pool.get('res.users').browse(cursor, user, user, context), + 'db':cursor.dbname + } + if template.template_language == 'mako': + templ = MakoTemplate(message, input_encoding='utf-8') + reply = MakoTemplate(message).render_unicode(object=object, + peobject=object, + env=env, + format_exceptions=True) + elif template.template_language == 'django': + templ = DjangoTemplate(message) + env['object'] = object + env['peobject'] = object + reply = templ.render(Context(env)) + return reply or False + except Exception: + return u"" + else: + return message + +class email_template(osv.osv): + "Templates for sending Email" + + _name = "email.template" + _description = 'Email Templates for Models' + + def change_model(self, cursor, user, ids, object_name, context=None): + if object_name: + mod_name = self.pool.get('ir.model').read( + cursor, + user, + object_name, + ['model'], context)['model'] + else: + mod_name = False + return { + 'value':{'model_int_name':mod_name} + } + + _columns = { + 'name' : fields.char('Name of Template', size=100, required=True), + 'object_name':fields.many2one('ir.model', 'Model'), + 'model_int_name':fields.char('Model Internal Name', size=200,), + 'def_to':fields.char( + 'Recepient (To)', + size=250, + help="The default recepient of email." + "Placeholders can be used here."), + 'def_cc':fields.char( + 'Default CC', + size=250, + help="The default CC for the email." + " Placeholders can be used here."), + 'def_bcc':fields.char( + 'Default BCC', + size=250, + help="The default BCC for the email." + " Placeholders can be used here."), + 'lang':fields.char( + 'Language', + size=250, + help="The default language for the email." + " Placeholders can be used here. " + "eg. ${object.partner_id.lang}"), + 'def_subject':fields.char( + 'Default Subject', + size=200, + help="The default subject of email." + " Placeholders can be used here.", + translate=True), + 'def_body_text':fields.text( + 'Standard Body (Text)', + help="The text version of the mail", + translate=True), + 'def_body_html':fields.text( + 'Body (Text-Web Client Only)', + help="The text version of the mail", + translate=True), + 'use_sign':fields.boolean( + 'Use Signature', + help="the signature from the User details" + "will be appened to the mail"), + 'file_name':fields.char( + 'File Name Pattern', + size=200, + help="File name pattern can be specified with placeholders." + "eg. 2009_SO003.pdf", + translate=True), + 'report_template':fields.many2one( + 'ir.actions.report.xml', + 'Report to send'), + 'ref_ir_act_window':fields.many2one( + 'ir.actions.act_window', + 'Window Action', + readonly=True), + 'ref_ir_value':fields.many2one( + 'ir.values', + 'Wizard Button', + readonly=True), + 'allowed_groups':fields.many2many( + 'res.groups', + 'template_group_rel', + 'templ_id', 'group_id', + string="Allowed User Groups", + help="Only users from these groups will be" + " allowed to send mails from this Template"), + 'enforce_from_account':fields.many2one( + 'email_template.account', + string="Enforce From Account", + help="Emails will be sent only from this account.", + domain="[('company','=','yes')]"), + 'model_object_field':fields.many2one( + 'ir.model.fields', + string="Field", + help="Select the field from the model you want to use." + "\nIf it is a relationship field you will be able to " + "choose the nested values in the box below\n(Note:If " + "there are no values make sure you have selected the" + " correct model)", + store=False), + 'sub_object':fields.many2one( + 'ir.model', + 'Sub-model', + help='When a relation field is used this field' + ' will show you the type of field you have selected', + store=False), + 'sub_model_object_field':fields.many2one( + 'ir.model.fields', + 'Sub Field', + help="When you choose relationship fields " + "this field will specify the sub value you can use.", + store=False), + 'null_value':fields.char( + 'Null Value', + help="This Value is used if the field is empty", + size=50, store=False), + 'copyvalue':fields.char( + 'Expression', + size=100, + help="Copy and paste the value in the " + "location you want to use a system value.", + store=False), + 'table_html':fields.text( + 'HTML code', + help="Copy this html code to your HTML message" + " body for displaying the info in your mail.", + store=False), + #Template language(engine eg.Mako) specifics + 'template_language':fields.selection( + TEMPLATE_ENGINES, + 'Templating Language', + required=True + ) + } + + _defaults = { + + } + _sql_constraints = [ + ('name', 'unique (name)', _('The template name must be unique !')) + ] + + def create(self, cr, uid, vals, context=None): + id = super(email_template, self).create(cr, uid, vals, context) + src_obj = self.pool.get('ir.model').read(cr, uid, vals['object_name'], ['model'], context)['model'] + vals['ref_ir_act_window'] = self.pool.get('ir.actions.act_window').create(cr, uid, { + 'name': _("%s Mail Form") % vals['name'], + 'type': 'ir.actions.act_window', + 'res_model': 'email_template.send.wizard', + 'src_model': src_obj, + 'view_type': 'form', + 'context': "{'src_model':'%s','template_id':'%d','src_rec_id':active_id,'src_rec_ids':active_ids}" % (src_obj, id), + 'view_mode':'form,tree', + 'view_id': self.pool.get('ir.ui.view').search(cr, uid, [('name', '=', 'email_template.send.wizard.form')], context=context)[0], + 'target': 'new', + 'auto_refresh':1 + }, context) + vals['ref_ir_value'] = self.pool.get('ir.values').create(cr, uid, { + 'name': _('Send Mail (%s)') % vals['name'], + 'model': src_obj, + 'key2': 'client_action_multi', + 'value': "ir.actions.act_window," + str(vals['ref_ir_act_window']), + 'object': True, + }, context) + self.write(cr, uid, id, { + 'ref_ir_act_window': vals['ref_ir_act_window'], + 'ref_ir_value': vals['ref_ir_value'], + }, context) + return id + + def unlink(self, cr, uid, ids, context=None): + for template in self.browse(cr, uid, ids, context): + obj = self.pool.get(template.object_name.model) + try: + if template.ref_ir_act_window: + self.pool.get('ir.actions.act_window').unlink(cr, uid, template.ref_ir_act_window.id, context) + if template.ref_ir_value: + self.pool.get('ir.values').unlink(cr, uid, template.ref_ir_value.id, context) + except: + raise osv.except_osv(_("Warning"), _("Deletion of Record failed")) + return super(email_template, self).unlink(cr, uid, ids, context) + + def copy(self, cr, uid, id, default=None, context=None): + if default is None: + default = {} + default = default.copy() + old = self.read(cr, uid, id, ['name'], context=context) + new_name = _("Copy of template ") + old.get('name', 'No Name') + check = self.search(cr, uid, [('name', '=', new_name)], context=context) + if check: + new_name = new_name + '_' + random.choice('abcdefghij') + random.choice('lmnopqrs') + random.choice('tuvwzyz') + default.update({'name':new_name}) + return super(email_template, self).copy(cr, uid, id, default, context) + + def compute_pl(self, + model_object_field, + sub_model_object_field, + null_value, template_language='mako'): + """ + Returns the expression based on data provided + @param model_object_field: First level field + @param sub_model_object_field: Second level drilled down field (M2O) + @param null_value: What has to be returned if the value is empty + @param template_language: The language used for templating + @return: computed expression + """ + #Configure for MAKO + copy_val = '' + if template_language == 'mako': + if model_object_field: + copy_val = "${object." + model_object_field + if sub_model_object_field: + copy_val += "." + sub_model_object_field + if null_value: + copy_val += " or '" + null_value + "'" + if model_object_field: + copy_val += "}" + elif template_language == 'django': + if model_object_field: + copy_val = "{{object." + model_object_field + if sub_model_object_field: + copy_val += "." + sub_model_object_field + if null_value: + copy_val = copy_val + '|default:"' + null_value + '"' + copy_val = copy_val + "}}" + return copy_val + + def onchange_model_object_field(self, cr, uid, ids, model_object_field, template_language, context=None): + if not model_object_field: + return {} + result = {} + field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context) + #Check if field is relational + if field_obj.ttype in ['many2one', 'one2many', 'many2many']: + res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context) + if res_ids: + result['sub_object'] = res_ids[0] + result['copyvalue'] = self.compute_pl(False, + False, + False, + template_language) + result['sub_model_object_field'] = False + result['null_value'] = False + else: + #Its a simple field... just compute placeholder + result['sub_object'] = False + result['copyvalue'] = self.compute_pl(field_obj.name, + False, + False, + template_language + ) + result['sub_model_object_field'] = False + result['null_value'] = False + return {'value':result} + + def onchange_sub_model_object_field(self, cr, uid, ids, model_object_field, sub_model_object_field, template_language, context=None): + if not model_object_field or not sub_model_object_field: + return {} + result = {} + field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context) + if field_obj.ttype in ['many2one', 'one2many', 'many2many']: + res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context) + sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context) + if res_ids: + result['sub_object'] = res_ids[0] + result['copyvalue'] = self.compute_pl(field_obj.name, + sub_field_obj.name, + False, + template_language + ) + result['sub_model_object_field'] = sub_model_object_field + result['null_value'] = False + else: + #Its a simple field... just compute placeholder + result['sub_object'] = False + result['copyvalue'] = self.compute_pl(field_obj.name, + False, + False, + template_language + ) + result['sub_model_object_field'] = False + result['null_value'] = False + return {'value':result} + + def onchange_null_value(self, cr, uid, ids, model_object_field, sub_model_object_field, null_value, template_language, context=None): + if not model_object_field and not null_value: + return {} + result = {} + field_obj = self.pool.get('ir.model.fields').browse(cr, uid, model_object_field, context) + if field_obj.ttype in ['many2one', 'one2many', 'many2many']: + res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_obj.relation)], context=context) + sub_field_obj = self.pool.get('ir.model.fields').browse(cr, uid, sub_model_object_field, context) + if res_ids: + result['sub_object'] = res_ids[0] + result['copyvalue'] = self.compute_pl(field_obj.name, + sub_field_obj.name, + null_value, + template_language + ) + result['sub_model_object_field'] = sub_model_object_field + result['null_value'] = null_value + else: + #Its a simple field... just compute placeholder + result['sub_object'] = False + result['copyvalue'] = self.compute_pl(field_obj.name, + False, + null_value, + template_language + ) + result['sub_model_object_field'] = False + result['null_value'] = null_value + return {'value':result} + + def generate_attach_reports(self, + cursor, + user, + template, + record_id, + mail, + context=None): + """ + Generate report to be attached and attach it + to the email + + @param cursor: Database Cursor + @param user: ID of User + @param template: Browse record of + template + @param record_id: ID of the target model + for which this mail has + to be generated + @param mail: Browse record of email object + @return: True + """ + reportname = 'report.' + \ + self.pool.get('ir.actions.report.xml').read( + cursor, + user, + template.report_template.id, + ['report_name'], + context)['report_name'] + service = netsvc.LocalService(reportname) + data = {} + data['model'] = template.model_int_name + (result, format) = service.create(cursor, + user, + [record_id], + data, + context) + attachment_obj = self.pool.get('ir.attachment') + new_att_vals = { + 'name':mail.subject + ' (Email Attachment)', + 'datas':base64.b64encode(result), + 'datas_fname':tools.ustr( + get_value( + cursor, + user, + record_id, + template.file_name, + template, + context + ) or 'Report') + "." + format, + 'description':mail.subject or "No Description", + 'res_model':'email_template.mailbox', + 'res_id':mail.id + } + attachment_id = attachment_obj.create(cursor, + user, + new_att_vals, + context) + if attachment_id: + self.pool.get('email_template.mailbox').write( + cursor, + user, + mail.id, + { + 'attachments_ids':[ + [6, 0, [attachment_id]] + ], + 'mail_type':'multipart/mixed' + }, + context) + return True + + def generate_mailbox_item_from_template(self, + cursor, + user, + template, + record_id, + context=None): + """ + Generates an email from the template for + record record_id of target object + + @param cursor: Database Cursor + @param user: ID of User + @param template: Browse record of + template + @param record_id: ID of the target model + for which this mail has + to be generated + @return: ID of created object + """ + if context is None: + context = {} + #If account to send from is in context select it, else use enforced account + if 'account_id' in context.keys(): + from_account = self.pool.get('email_template.account').read( + cursor, + user, + context.get('account_id'), + ['name', 'email_id'], + context + ) + else: + from_account = { + 'id':template.enforce_from_account.id, + 'name':template.enforce_from_account.name, + 'email_id':template.enforce_from_account.email_id + } + lang = get_value(cursor, + user, + record_id, + template.lang, + template, + context) + if lang: + ctx = context.copy() + ctx.update({'lang':lang}) + template = self.browse(cursor, user, template_id, context=ctx) + mailbox_values = { + 'email_from': tools.ustr(from_account['name']) + \ + "<" + tools.ustr(from_account['email_id']) + ">", + 'email_to':get_value(cursor, + user, + record_id, + template.def_to, + template, + context), + 'email_cc':get_value(cursor, + user, + record_id, + template.def_cc, + template, + context), + 'email_bcc':get_value(cursor, + user, + record_id, + template.def_bcc, + template, + context), + 'subject':get_value(cursor, + user, + record_id, + template.def_subject, + template, + context), + 'body_text':get_value(cursor, + user, + record_id, + template.def_body_text, + template, + context), + 'body_html':get_value(cursor, + user, + record_id, + template.def_body_html, + template, + context), + 'account_id' :from_account['id'], + #This is a mandatory field when automatic emails are sent + 'state':'na', + 'folder':'drafts', + 'mail_type':'multipart/alternative' + } + #Use signatures if allowed + if template.use_sign: + sign = self.pool.get('res.users').read(cursor, + user, + user, + ['signature'], + context)['signature'] + if mailbox_values['body_text']: + mailbox_values['body_text'] += sign + if mailbox_values['body_html']: + mailbox_values['body_html'] += sign + mailbox_id = self.pool.get('email_template.mailbox').create( + cursor, + user, + mailbox_values, + context) + return mailbox_id + + def generate_mail(self, + cursor, + user, + template_id, + record_ids, + context=None): + if context is None: + context = {} + template = self.browse(cursor, user, template_id, context=context) + if not template: + raise Exception("The requested template could not be loaded") + for record_id in record_ids: + mailbox_id = self._generate_mailbox_item_from_template( + cursor, + user, + template, + record_id, + context) + mail = self.pool.get('email_template.mailbox').browse( + cursor, + user, + mailbox_id, + context=context + ) + if template.report_template: + self._generate_attach_reports( + cursor, + user, + template, + record_id, + mail, + context + ) + self.pool.get('email_template.mailbox').write( + cursor, + user, + mailbox_id, + {'folder':'outbox'}, + context=context + ) + return True + +email_template() + +class email_template_preview(osv.osv_memory): + _name = "email_template.preview" + _description = "Email Template Preview" + + def _get_model_recs(self, cr, uid, context=None): + if context is None: + context = {} + #Fills up the selection box which allows records from the selected object to be displayed + self.context = context + if 'active_id' in context.keys(): +# context['active_id'] = 5 + ref_obj_id = self.pool.get('email.template').read(cr, uid, context['active_id'], ['object_name'], context) + ref_obj_name = self.pool.get('ir.model').read(cr, uid, ref_obj_id[0], ['model'], context)['model'] + ref_obj_ids = self.pool.get(ref_obj_name).search(cr, uid, [], context=context) + ref_obj_recs = self.pool.get(ref_obj_name).name_get(cr, uid, ref_obj_ids, context) + return ref_obj_recs + + def _default_model(self, cursor, user, context=None): + """ + Returns the default value for model field + @param cursor: Database Cursor + @param user: ID of current user + @param context: Open ERP Context + """ + return self.pool.get('email.template').read( + cursor, + user, + context['active_id'], + ['object_name'], + context)['object_name'] + + _columns = { + 'ref_template':fields.many2one( + 'email.template', + 'Template', readonly=True), + 'rel_model':fields.many2one('ir.model', 'Model', readonly=True), + 'rel_model_ref':fields.selection(_get_model_recs, 'Referred Document'), + 'to':fields.char('To', size=250, readonly=True), + 'cc':fields.char('CC', size=250, readonly=True), + 'bcc':fields.char('BCC', size=250, readonly=True), + 'subject':fields.char('Subject', size=200, readonly=True), + 'body_text':fields.text('Body', readonly=True), + 'body_html':fields.text('Body', readonly=True), + 'report':fields.char('Report Name', size=100, readonly=True), + } + _defaults = { + 'ref_template': lambda self, cr, uid, ctx:ctx['active_id'], + 'rel_model': _default_model + } + + def on_change_ref(self, cr, uid, ids, rel_model_ref, context=None): + if context is None: + context = {} + if not rel_model_ref: + return {} + vals = {} + if context == {}: + context = self.context + template = self.pool.get('email.template').browse(cr, uid, context['active_id'], context) + #Search translated template + lang = get_value(cr, uid, rel_model_ref, template.lang, template, context) + if lang: + ctx = context.copy() + ctx.update({'lang':lang}) + template = self.pool.get('email.template').browse(cr, uid, context['active_id'], ctx) + vals['to'] = get_value(cr, uid, rel_model_ref, template.def_to, template, context) + vals['cc'] = get_value(cr, uid, rel_model_ref, template.def_cc, template, context) + vals['bcc'] = get_value(cr, uid, rel_model_ref, template.def_bcc, template, context) + vals['subject'] = get_value(cr, uid, rel_model_ref, template.def_subject, template, context) + vals['body_text'] = get_value(cr, uid, rel_model_ref, template.def_body_text, template, context) + vals['body_html'] = get_value(cr, uid, rel_model_ref, template.def_body_html, template, context) + vals['report'] = get_value(cr, uid, rel_model_ref, template.file_name, template, context) + return {'value':vals} + +email_template_preview() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/email_template/email_template_account.py b/addons/email_template/email_template_account.py new file mode 100755 index 00000000000..2503d19cd10 --- /dev/null +++ b/addons/email_template/email_template_account.py @@ -0,0 +1,440 @@ +from osv import osv, fields +from html2text import html2text +import re +import smtplib +import base64 +from email import Encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.header import decode_header, Header +from email.utils import formatdate +import re +import netsvc +import string +import email +import time, datetime +import email_template_engines +from tools.translate import _ +import tools + +class email_template_account(osv.osv): + """ + Object to store email account settings + """ + _name = "email_template.account" + _known_content_types = ['multipart/mixed', + 'multipart/alternative', + 'multipart/related', + 'text/plain', + 'text/html' + ] + _columns = { + 'name': fields.char('Email Account Desc', + size=64, required=True, + readonly=True, select=True, + states={'draft':[('readonly', False)]}), + 'user':fields.many2one('res.users', + 'Related User', required=True, + readonly=True, states={'draft':[('readonly', False)]}), + 'email_id': fields.char('Email ID', + size=120, required=True, + readonly=True, states={'draft':[('readonly', False)]} , + help=" eg:yourname@yourdomain.com "), + 'smtpserver': fields.char('Server', + size=120, required=True, + readonly=True, states={'draft':[('readonly', False)]}, + help="Enter name of outgoing server,eg:smtp.gmail.com "), + 'smtpport': fields.integer('SMTP Port ', + size=64, required=True, + readonly=True, states={'draft':[('readonly', False)]}, + help="Enter port number,eg:SMTP-587 "), + 'smtpuname': fields.char('User Name', + size=120, required=False, + readonly=True, states={'draft':[('readonly', False)]}), + 'smtppass': fields.char('Password', + size=120, invisible=True, + required=False, readonly=True, + states={'draft':[('readonly', False)]}), + 'smtptls':fields.boolean('Use TLS', + states={'draft':[('readonly', False)]}, readonly=True), + + 'smtpssl':fields.boolean('Use SSL/TLS (only in python 2.6)', + states={'draft':[('readonly', False)]}, readonly=True), + 'send_pref':fields.selection([ + ('html', 'HTML otherwise Text'), + ('text', 'Text otherwise HTML'), + ('both', 'Both HTML & Text') + ], 'Mail Format', required=True), + 'allowed_groups':fields.many2many( + 'res.groups', + 'account_group_rel', 'templ_id', 'group_id', + string="Allowed User Groups", + help="Only users from these groups will be" \ + "allowed to send mails from this ID"), + 'company':fields.selection([ + ('yes', 'Yes'), + ('no', 'No') + ], 'Company Mail A/c', + readonly=True, + help="Select if this mail account does not belong" \ + "to specific user but the organisation as a whole." \ + "eg:info@somedomain.com", + required=True, states={ + 'draft':[('readonly', False)] + }), + + 'state':fields.selection([ + ('draft', 'Initiated'), + ('suspended', 'Suspended'), + ('approved', 'Approved') + ], + 'Account Status', required=True, readonly=True), + } + + _defaults = { + 'name':lambda self, cursor, user, context:self.pool.get( + 'res.users' + ).read( + cursor, + user, + user, + ['name'], + context + )['name'], + 'smtpssl':lambda * a:True, + 'state':lambda * a:'draft', + 'user':lambda self, cursor, user, context:user, + 'send_pref':lambda * a: 'html', + 'smtptls':lambda * a:True, + } + + _sql_constraints = [ + ( + 'email_uniq', + 'unique (email_id)', + 'Another setting already exists with this email ID !') + ] + + def _constraint_unique(self, cursor, user, ids): + """ + This makes sure that you dont give personal + users two accounts with same ID (Validated in sql constaints) + However this constraint exempts company accounts. + Any no of co accounts for a user is allowed + """ + if self.read(cursor, user, ids, ['company'])[0]['company'] == 'no': + accounts = self.search(cursor, user, [ + ('user', '=', user), + ('company', '=', 'no') + ]) + if len(accounts) > 1 : + return False + else : + return True + else: + return True + + _constraints = [ + (_constraint_unique, + 'Error: You are not allowed to have more than 1 account.', + []) + ] + + def on_change_emailid(self, cursor, user, ids, name=None, email_id=None, context=None): + """ + Called when the email ID field changes. + + UI enhancement + Writes the same email value to the smtpusername + and incoming username + """ + #TODO: Check and remove the write. Is it needed? + self.write(cursor, user, ids, {'state':'draft'}, context=context) + return { + 'value': { + 'state': 'draft', + 'smtpuname':email_id, + 'isuser':email_id + } + } + + def get_outgoing_server(self, cursor, user, ids, context=None): + """ + Returns the Out Going Connection (SMTP) object + + @attention: DO NOT USE except_osv IN THIS METHOD + @param cursor: Database Cursor + @param user: ID of current user + @param ids: ID/list of ids of current object for + which connection is required + First ID will be chosen from lists + @param context: Context + + @return: SMTP server object or Exception + """ + #Type cast ids to integer + if type(ids) == list: + ids = ids[0] + this_object = self.browse(cursor, user, ids, context) + if this_object: + if this_object.smtpserver and this_object.smtpport: + try: + if this_object.smtpssl: + serv = smtplib.SMTP_SSL(this_object.smtpserver, this_object.smtpport) + else: + serv = smtplib.SMTP(this_object.smtpserver, this_object.smtpport) + if this_object.smtptls: + serv.ehlo() + serv.starttls() + serv.ehlo() + except Exception, error: + raise error + try: + if serv.has_extn('AUTH') or this_object.smtpuname or this_object.smtppass: + serv.login(this_object.smtpuname, this_object.smtppass) + except Exception, error: + raise error + return serv + raise Exception(_("SMTP SERVER or PORT not specified")) + raise Exception(_("Core connection for the given ID does not exist")) + + def check_outgoing_connection(self, cursor, user, ids, context=None): + """ + checks SMTP credentials and confirms if outgoing connection works + (Attached to button) + @param cursor: Database Cursor + @param user: ID of current user + @param ids: list of ids of current object for + which connection is required + @param context: Context + """ + try: + self.get_outgoing_server(cursor, user, ids, context) + raise osv.except_osv(_("SMTP Test Connection Was Successful"), '') + except osv.except_osv, success_message: + raise success_message + except Exception, error: + raise osv.except_osv( + _("Out going connection test failed"), + _("Reason: %s") % error + ) + + def do_approval(self, cr, uid, ids, context={}): + #TODO: Check if user has rights + self.write(cr, uid, ids, {'state':'approved'}, context=context) +# wf_service = netsvc.LocalService("workflow") + + def smtp_connection(self, cursor, user, id, context=None): + """ + This method should now wrap smtp_connection + """ + #This function returns a SMTP server object + logger = netsvc.Logger() + core_obj = self.browse(cursor, user, id, context) + if core_obj.smtpserver and core_obj.smtpport and core_obj.state == 'approved': + try: + serv = self.get_outgoing_server(cursor, user, id, context) + except Exception, error: + logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed on login. Probable Reason:Could not login to server\nError: %s") % (id, error)) + return False + #Everything is complete, now return the connection + return serv + else: + logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id) + return False + +#**************************** MAIL SENDING FEATURES ***********************# + def split_to_ids(self, ids_as_str): + """ + Identifies email IDs separated by separators + and returns a list + TODO: Doc this + "a@b.com,c@bcom; d@b.com;e@b.com->['a@b.com',...]" + """ + email_sep_by_commas = ids_as_str \ + .replace('; ', ',') \ + .replace(';', ',') \ + .replace(', ', ',') + return email_sep_by_commas.split(',') + + def get_ids_from_dict(self, addresses={}): + """ + TODO: Doc this + """ + result = {'all':[]} + keys = ['To', 'CC', 'BCC'] + for each in keys: + ids_as_list = self.split_to_ids(addresses.get(each, u'')) + while u'' in ids_as_list: + ids_as_list.remove(u'') + result[each] = ids_as_list + result['all'].extend(ids_as_list) + return result + + def send_mail(self, cr, uid, ids, addresses, subject='', body=None, payload=None, context=None): + #TODO: Replace all this crap with a single email object + if body is None: + body = {} + if payload is None: + payload = {} + if context is None: + context = {} + logger = netsvc.Logger() + for id in ids: + core_obj = self.browse(cr, uid, id, context) + serv = self.smtp_connection(cr, uid, id) + if serv: + try: + msg = MIMEMultipart() + if subject: + msg['Subject'] = subject + sender_name = Header(core_obj.name, 'utf-8').encode() + msg['From'] = sender_name + " <" + core_obj.email_id + ">" + msg['Organization'] = tools.ustr(core_obj.user.company_id.name) + msg['Date'] = formatdate() + addresses_l = self.get_ids_from_dict(addresses) + if addresses_l['To']: + msg['To'] = u','.join(addresses_l['To']) + if addresses_l['CC']: + msg['CC'] = u','.join(addresses_l['CC']) +# if addresses_l['BCC']: +# msg['BCC'] = u','.join(addresses_l['BCC']) + if body.get('text', False): + temp_body_text = body.get('text', '') + l = len(temp_body_text.replace(' ', '').replace('\r', '').replace('\n', '')) + if l == 0: + body['text'] = u'No Mail Message' + # Attach parts into message container. + # According to RFC 2046, the last part of a multipart message, in this case + # the HTML message, is best and preferred. + if core_obj.send_pref == 'text' or core_obj.send_pref == 'both': + body_text = body.get('text', u'No Mail Message') + body_text = tools.ustr(body_text) + msg.attach(MIMEText(body_text.encode("utf-8"), _charset='UTF-8')) + if core_obj.send_pref == 'html' or core_obj.send_pref == 'both': + html_body = body.get('html', u'') + if len(html_body) == 0 or html_body == u'': + html_body = body.get('text', u'

No Mail Message

').replace('\n', '
').replace('\r', '
') + html_body = tools.ustr(html_body) + msg.attach(MIMEText(html_body.encode("utf-8"), _subtype='html', _charset='UTF-8')) + #Now add attachments if any + for file in payload.keys(): + part = MIMEBase('application', "octet-stream") + part.set_payload(base64.decodestring(payload[file])) + part.add_header('Content-Disposition', 'attachment; filename="%s"' % file) + Encoders.encode_base64(part) + msg.attach(part) + except Exception, error: + logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:MIME Error\nDescription: %s") % (id, error)) + return error + try: + #print msg['From'],toadds + serv.sendmail(msg['From'], addresses_l['all'], msg.as_string()) + except Exception, error: + logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Server Send Error\nDescription: %s") % (id, error)) + return error + #The mail sending is complete + serv.close() + logger.notifyChannel(_("Email Template"), netsvc.LOG_INFO, _("Mail from Account %s successfully Sent.") % (id)) + return True + else: + logger.notifyChannel(_("Email Template"), netsvc.LOG_ERROR, _("Mail from Account %s failed. Probable Reason:Account not approved") % id) + + def extracttime(self, time_as_string): + """ + TODO: DOC THis + """ + logger = netsvc.Logger() + #The standard email dates are of format similar to: + #Thu, 8 Oct 2009 09:35:42 +0200 + #print time_as_string + date_as_date = False + convertor = {'+':1, '-':-1} + try: + time_as_string = time_as_string.replace(',', '') + date_list = time_as_string.split(' ') + date_temp_str = ' '.join(date_list[1:5]) + if len(date_list) >= 6: + sign = convertor.get(date_list[5][0], False) + else: + sign = False + try: + dt = datetime.datetime.strptime( + date_temp_str, + "%d %b %Y %H:%M:%S") + except: + try: + dt = datetime.datetime.strptime( + date_temp_str, + "%d %b %Y %H:%M") + except: + return False + if sign: + try: + offset = datetime.timedelta( + hours=sign * int( + date_list[5][1:3] + ), + minutes=sign * int( + date_list[5][3:5] + ) + ) + except Exception, e2: + """Looks like UT or GMT, just forget decoding""" + return False + else: + offset = datetime.timedelta(hours=0) + dt = dt + offset + date_as_date = dt.strftime('%Y-%m-%d %H:%M:%S') + #print date_as_date + except Exception, e: + logger.notifyChannel( + _("Email Template"), + netsvc.LOG_WARNING, + _( + "Datetime Extraction failed.Date:%s \ + \tError:%s") % ( + time_as_string, + e) + ) + return date_as_date + + def send_receive(self, cr, uid, ids, context=None): + self.get_mails(cr, uid, ids, context) + for id in ids: + ctx = context.copy() + ctx['filters'] = [('account_id', '=', id)] + self.pool.get('email_template.mailbox').send_all_mail(cr, uid, [], context=ctx) + return True + + def decode_header_text(self, text): + """ Decode internationalized headers RFC2822. + To, CC, BCC, Subject fields can contain + text slices with different encodes, like: + =?iso-8859-1?Q?Enric_Mart=ED?= , + =?Windows-1252?Q?David_G=F3mez?= + Sometimes they include extra " character at the beginning/ + end of the contact name, like: + "=?iso-8859-1?Q?Enric_Mart=ED?=" + and decode_header() does not work well, so we use regular + expressions (?= ? ? ?=) to split the text slices + """ + if not text: + return text + p = re.compile("(=\?.*?\?.\?.*?\?=)") + text2 = '' + try: + for t2 in p.split(text): + text2 += ''.join( + [s.decode( + t or 'ascii' + ) for (s, t) in decode_header(t2)] + ).encode('utf-8') + except: + return text + return text2 + +email_template_account() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/email_template/email_template_account_view.xml b/addons/email_template/email_template_account_view.xml new file mode 100755 index 00000000000..5f11fa033f4 --- /dev/null +++ b/addons/email_template/email_template_account_view.xml @@ -0,0 +1,110 @@ + + + + + + + + + email_template.account.form + email_template.account + form + +
+ + + + + + + + + + + + +