[MERGE] Next batch of improvements/fixes for mail.alias

bzr revid: odo@openerp.com-20120814123050-xw3ltthoc9nld76r
This commit is contained in:
Olivier Dony 2012-08-14 14:30:50 +02:00
commit 11bdbfd183
25 changed files with 448 additions and 261 deletions

View File

@ -6,9 +6,6 @@
<record id="base.menu_res_company_global" model="ir.ui.menu">
<field name="groups_id" eval="[(3, ref('base.group_no_one'))]"/>
</record>
<record id="base.menu_users" model="ir.ui.menu">
<field name="groups_id" eval="[(3, ref('base.group_no_one'))]"/>
</record>
<record id="base.menu_publisher_warranty" model="ir.ui.menu">
<field name="groups_id" eval="[(3, ref('base.group_no_one'))]"/>
</record>

View File

@ -97,9 +97,10 @@
<separator string="Emails Integration" attrs="{'invisible': [('module_crm','=',False)]}"/>
<p attrs="{'invisible': [('module_crm','=',False)]}">
OpenERP allows to automatically create leads (or others documents)
from incoming emails. You can synchronize emails with OpenERP
by connecting the mail gateway to your mail server or by manually
pressing buttons in our mail clients.
from incoming emails. You can automatically synchronize emails with OpenERP
using regular POP/IMAP accounts, using a direct email integration script for your
email server, or by manually pushing emails to OpenERP using specific
plugins for your preferred email application.
</p>
<group name="On Mail Client" attrs="{'invisible': [('module_crm','=',False)]}">
<label for="id" string="On Mail Client"/>

View File

@ -132,9 +132,10 @@ class crm_case_section(osv.osv):
return ids
_defaults = {
'active': lambda *a: 1,
'allow_unlink': lambda *a: 1,
'stage_ids': _get_stage_common
'active': 1,
'allow_unlink': 1,
'stage_ids': _get_stage_common,
'alias_domain': False, # always hide alias during creation
}
_sql_constraints = [
@ -162,16 +163,16 @@ class crm_case_section(osv.osv):
return res
def create(self, cr, uid, vals, context=None):
alias_pool = self.pool.get('mail.alias')
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
name = vals.pop('alias_name', None) or vals['name']
alias_id = alias_pool.create_unique_alias(cr, uid,
{'alias_name': name},
vals.pop('alias_name', None) # prevent errors during copy()
alias_id = mail_alias.create_unique_alias(cr, uid,
{'alias_name': vals['name']},
model_name="crm.lead",
context=context)
vals['alias_id'] = alias_id
res = super(crm_case_section, self).create(cr, uid, vals, context)
alias_pool.write(cr, uid, [vals['alias_id']],{'alias_defaults':{'section_id': res,'type':'lead'}},context)
mail_alias.write(cr, uid, [vals['alias_id']], {'alias_defaults': {'section_id': res, 'type':'lead'}}, context)
return res
def unlink(self, cr, uid, ids, context=None):

View File

@ -26,17 +26,10 @@
<record model="crm.case.channel" id="crm_case_channel_email">
<field name="name">Email</field>
</record>
<record id="mail_alias_sales_department" model="mail.alias">
<field name="alias_name">sales</field>
<field name="alias_model_id" ref="model_crm_lead"/>
<field name="alias_user_id" ref="base.user_root"/>
<field name="alias_defaults">{'type':'lead'}</field>
</record>
<record model="crm.case.section" id="section_sales_department">
<field name="name">Sales Department</field>
<field name="code">Sales</field>
<field name="alias_id" ref="mail_alias_sales_department"/>
</record>
<!-- Payment Mode -->
@ -70,5 +63,13 @@
To manage quotations and sale orders, install the module "Sales Management".</value>
</function>
<record model="mail.alias" id="default_sales_alias">
<field name="alias_name">sales</field>
<field name="alias_model_id" ref="model_crm_lead"/>
<field name="alias_user_id" ref="base.user_root"/>
<field name="alias_defaults">{'type':'lead'}</field>
</record>
</data>
</openerp>

View File

@ -1,14 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="mail_alias_marketing_department" model="mail.alias">
<field name="alias_name">info</field>
<field name="alias_model_id" ref="model_crm_lead"/>
<field name="alias_user_id" ref="base.user_root"/>
<field name="alias_defaults">{'type':'lead'}</field>
</record>
</data>
<data>
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(4,ref('base.group_sale_salesman'))]"/>

View File

@ -72,8 +72,6 @@
</record>
<record model="crm.case.section" id="section_sales_department">
<field name="name">Sales Department</field>
<field name="code">Sales</field>
<field name="stage_ids" eval="[ (4, ref('stage_lead1')), (4, ref('stage_lead2')),
(4, ref('stage_lead3')), (4, ref('stage_lead4')),
(4, ref('stage_lead5')), (4, ref('stage_lead6')),

View File

@ -96,7 +96,7 @@
<notebook colspan="4">
<page string="Sales Team">
<group>
<field name="alias_id" invisible="1"/>
<field name="alias_id" invisible="1" required="0"/>
<label for="alias_name" attrs="{'invisible': [('alias_domain', '=', False)]}"/>
<div attrs="{'invisible': [('alias_domain', '=', False)]}">
<field name="alias_name" class="oe_inline" attrs="{'required': [('alias_id', '!=', False)]}"/>@<field name="alias_domain" class="oe_inline"/>

View File

@ -21,7 +21,7 @@
</group>
</group>
<div name="config_other" version="7.0" position="inside">
<separator string="Import and Synchronize Data from an Other Application"/>
<separator string="Importing and Synchronizing with External Systems"/>
<group>
<label for="id" string="Synchronize with"/>
<div>

View File

@ -189,14 +189,11 @@ openerp_mailgate.py -u %(uid)d -p PASSWORD -o %(model)s -d %(dbname)s --host=HOS
result, data = imap_server.search(None, '(UNSEEN)')
for num in data[0].split():
result, data = imap_server.fetch(num, '(RFC822)')
if server.object_id:
res_id = mail_thread.message_process(cr, uid, server.object_id.model,
data[0][1],
save_original=server.original,
strip_attachments=(not server.attach),
context=context)
else:
res_id = mail_thread.message_catchall(cr, uid, data[0][1])
res_id = mail_thread.message_process(cr, uid, server.object_id.model,
data[0][1],
save_original=server.original,
strip_attachments=(not server.attach),
context=context)
if res_id and server.action_id:
action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id]})
imap_server.store(num, '+FLAGS', '\\Seen')
@ -217,14 +214,11 @@ openerp_mailgate.py -u %(uid)d -p PASSWORD -o %(model)s -d %(dbname)s --host=HOS
for num in range(1, numMsgs + 1):
(header, msges, octets) = pop_server.retr(num)
msg = '\n'.join(msges)
if server.object_id:
res_id = mail_thread.message_process(cr, uid, server.object_id.model,
msg,
save_original=server.original,
strip_attachments=(not server.attach),
context=context)
else:
res_id = mail_thread.message_catchall(cr, uid, data[0][1])
res_id = mail_thread.message_process(cr, uid, server.object_id.model,
msg,
save_original=server.original,
strip_attachments=(not server.attach),
context=context)
if res_id and server.action_id:
action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id]})
pop_server.dele(num)

View File

@ -518,37 +518,55 @@ class hr_job(osv.osv):
"create new applicants for this job position."),
}
def init(self, cr):
# Installation hook to create aliases for all jobs, right after _auto_init
_defaults = {
'alias_domain': False, # always hide alias during creation
}
def _auto_init(self, cr, context=None):
"""Installation hook to create aliases for all jobs and avoid constraint errors."""
# disable the unique alias_id not null constraint, to avoid spurious warning during
# super.auto_init. We'll reinstall it afterwards.
self._columns['alias_id'].required = False
super(hr_job,self)._auto_init(cr, context=context)
registry = RegistryManager.get(cr.dbname)
mail_alias = registry.get('mail.alias')
hr_job = registry.get('hr.job')
jobs_no_alias = hr_job.search(cr, SUPERUSER_ID, [('alias_id', '=', False)])
hr_jobs = registry.get('hr.job')
jobs_no_alias = hr_jobs.search(cr, SUPERUSER_ID, [('alias_id', '=', False)])
# Use read() not browse(), to avoid prefetching uninitialized inherited fields
for job_data in hr_job.read(cr, SUPERUSER_ID, jobs_no_alias, ['name']):
alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, {'alias_name': 'job_'+job_data['name'],
'alias_force_id': job_data['id']},
model_name=self._name)
hr_job.write(cr, SUPERUSER_ID, job_data['id'], {'alias_id': alias_id})
for job_data in hr_jobs.read(cr, SUPERUSER_ID, jobs_no_alias, ['name']):
alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, {'alias_name': 'job+'+job_data['name'],
'alias_defaults': {'job_id': job_data['id']}},
model_name='hr.applicant')
hr_jobs.write(cr, SUPERUSER_ID, job_data['id'], {'alias_id': alias_id})
_logger.info('Mail alias created for hr.job %s (uid %s)', job_data['name'], job_data['id'])
# Finally attempt to reinstate the missing constraint
try:
cr.execute('ALTER TABLE hr_job ALTER COLUMN alias_id SET NOT NULL')
except Exception:
pass
_logger.warning("Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\
"If you want to have it, you should update the records and execute manually:\n"\
"ALTER TABLE %s ALTER COLUMN %s SET NOT NULL",
self._table, 'alias_id', self._table, 'alias_id')
self._columns['alias_id'].required = True
def create(self, cr, uid, vals, context=None):
alias_pool = self.pool.get('mail.alias')
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
name = vals.pop('alias_name', None) or vals['name']
alias_id = alias_pool.create_unique_alias(cr, uid,
{'alias_name': "job_"+name},
vals.pop('alias_name', None) # prevent errors during copy()
alias_id = mail_alias.create_unique_alias(cr, uid,
# Using '+' allows using subaddressing for those who don't
# have a catchall domain setup.
{'alias_name': 'jobs+'+vals['name']},
model_name="hr.applicant",
context=context)
vals['alias_id'] = alias_id
res = super( hr_job, self).create(cr, uid, vals, context)
alias_pool.write(cr, uid, [vals['alias_id']], {"alias_defaults": {'job_id': res}}, context)
res = super(hr_job, self).create(cr, uid, vals, context)
mail_alias.write(cr, uid, [vals['alias_id']], {"alias_defaults": {'job_id': res}}, context)
return res
def unlink(self, cr, uid, ids, context=None):

View File

@ -458,7 +458,6 @@ You can automatically create application records from an email gateway, that you
<field name="alias_name">jobs</field>
<field name="alias_model_id" ref="model_hr_applicant"/>
<field name="alias_user_id" ref="base.user_root"/>
<field name="alias_defaults">{}</field>
</record>
</data>

View File

@ -334,7 +334,7 @@
</field>
<xpath expr="/form/sheet/h1" version="7.0" position="after">
<div colspan="4" attrs="{'invisible': [('alias_domain', '=', False)]}">
<field name="alias_id" invisible="1"/>
<field name="alias_id" invisible="1" required="0"/>
<label for="alias_name" class="oe_edit_only"/>
<field name="alias_name" nolabel="1" class="oe_inline" attrs="{'required': [('alias_id', '!=', False)]}"/>@<field name="alias_domain" nolabel="1" class="oe_inline"/>
</div>

View File

@ -20,9 +20,19 @@
##############################################################################
import re
import unicodedata
from openerp.osv import fields, osv
from openerp.tools import ustr
# Inspired by http://stackoverflow.com/questions/517923
def remove_accents(input_str):
"""Suboptimal-but-better-than-nothing way to replace accented
latin letters by an ASCII equivalent. Will obviously change the
meaning of input_str and work only for some cases"""
input_str = ustr(input_str)
nkfd_form = unicodedata.normalize('NFKD', input_str)
return u''.join([c for c in nkfd_form if not unicodedata.combining(c)])
class mail_alias(osv.Model):
"""A Mail Alias is a mapping of an email address with a given OpenERP Document
@ -40,6 +50,7 @@ class mail_alias(osv.Model):
_name = 'mail.alias'
_description = "Email Aliases"
_rec_name = 'alias_name'
_order = 'alias_model_id, alias_name'
def _get_alias_domain(self, cr, uid, ids, name, args, context=None):
ir_config_parameter = self.pool.get("ir.config_parameter")
@ -55,10 +66,9 @@ class mail_alias(osv.Model):
"corresponds. Any incoming email that does not reply to an "
"existing record will cause the creation of a new record "
"of this model (e.g. a Project Task)",
# only allow selecting mail_thread models!
#TODO kept doamin temporarily in comment, need to redefine domain
#domain="[('field_id', 'in', 'message_ids')]"
),
# hack to only allow selecting mail_thread models (we might
# (have a few false positives, though)
domain="[('field_id.name', '=', 'message_ids')]"),
'alias_user_id': fields.many2one('res.users', 'Owner',
help="The owner of records created upon receiving emails on this alias. "
"If this field is not set the system will attempt to find the right owner "
@ -76,7 +86,10 @@ class mail_alias(osv.Model):
_defaults = {
'alias_defaults': '{}',
'alias_user_id': lambda self,cr,uid, context: uid
'alias_user_id': lambda self,cr,uid,context: uid,
# looks better when creating new aliases - even if the field is informative only
'alias_domain': lambda self,cr,uid,context: self._get_alias_domain(cr,1,[1],None,None)[1]
}
_sql_constraints = [
@ -122,12 +135,10 @@ class mail_alias(osv.Model):
make it unique, and the ``alias_model_id`` value will set to the
model ID of the ``model_name`` value, if provided,
"""
alias_name = re.sub(r'\W+', '_', vals['alias_name']).lower()
alias_name = re.sub(r'[^\w+]', '-', remove_accents(vals['alias_name'])).lower()
alias_name = self._find_unique(cr, uid, alias_name, context=context)
vals['alias_name'] = alias_name
if model_name:
model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', model_name)], context=context)[0]
vals['alias_model_id'] = model_id
return self.create(cr, uid, vals, context=context)

View File

@ -10,14 +10,12 @@
<form string="Alias" version="7.0">
<sheet>
<label for="alias_name" class="oe_edit_only"/>
<h2><field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline"/></h2>
<group colspan="4" col="4">
<field name="alias_model_id" readonly="1"/>
<field name="alias_user_id" readonly="1"/>
<field name="alias_force_thread_id" readonly="1"/>
<newline/>
<separator string="Default Values" colspan="4"/>
<field name="alias_defaults" colspan="4" nolabel="1" readonly="1"/>
<h2><field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline"/></h2>
<group>
<field name="alias_model_id"/>
<field name="alias_user_id"/>
<field name="alias_force_thread_id"/>
<field name="alias_defaults"/>
</group>
</sheet>
</form>

View File

@ -25,9 +25,7 @@ import openerp.tools as tools
from operator import itemgetter
from osv import osv
from osv import fields
import tools
from tools.translate import _
from lxml import etree
class mail_group(osv.osv):
"""
@ -78,7 +76,6 @@ class mail_group(osv.osv):
def get_last_month_msg_nbr(self, cr, uid, ids, name, args, context=None):
result = {}
message_obj = self.pool.get('mail.message')
for id in ids:
lower_date = (DT.datetime.now() - DT.timedelta(days=30)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
result[id] = self.message_search(cr, uid, [id], limit=None, domain=[('date', '>=', lower_date)], count=True, context=context)
@ -153,6 +150,7 @@ class mail_group(osv.osv):
'responsible_id': (lambda s, cr, uid, ctx: uid),
'image': _get_default_image,
'parent_id': _get_menu_parent,
'alias_domain': False, # always hide alias during creation
}
def _subscribe_user_with_group_m2m_command(self, cr, uid, ids, group_ids_command, context=None):
@ -168,12 +166,14 @@ class mail_group(osv.osv):
return self.message_subscribe(cr, uid, ids, user_ids, context=context)
def create(self, cr, uid, vals, context=None):
alias_pool = self.pool.get('mail.alias')
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
name = vals.get('alias_name') or vals['name']
alias_id = alias_pool.create_unique_alias(cr, uid,
{'alias_name': "group_"+name},
model_name=self._name, context=context)
vals.pop('alias_name', None) # prevent errors during copy()
alias_id = mail_alias.create_unique_alias(cr, uid,
# Using '+' allows using subaddressing for those who don't
# have a catchall domain setup.
{'alias_name': "group+"+vals['name']},
model_name=self._name, context=context)
vals['alias_id'] = alias_id
mail_group_id = super(mail_group, self).create(cr, uid, vals, context)
@ -193,7 +193,8 @@ class mail_group(osv.osv):
newref = cobj.copy(cr, uid, ref[1], default={'params': str(params), 'name': vals['name']}, context=context)
self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,'+str(newref), 'mail_group_id': mail_group_id}, context=context)
alias_pool.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context)
mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context)
if vals.get('group_ids'):
self._subscribe_user_with_group_m2m_command(cr, uid, [mail_group_id], vals.get('group_ids'), context=context)

View File

@ -60,6 +60,11 @@
<label for="name" string="Group Name"/>
</div>
<h1><field name="name"/></h1>
<div name="alias_box" colspan="4" attrs="{'invisible': [('alias_domain', '=', False)]}">
<field name="alias_id" invisible="1" required="0"/>
<label for="alias_name" class="oe_edit_only"/>
<field name="alias_name" nolabel="1" class="oe_inline" attrs="{'required': [('alias_id', '!=', False)]}"/>@<field name="alias_domain" nolabel="1" class="oe_inline"/>
</div>
<field name="description" placeholder="Topics discussed in this group..."/>
</div>
<div class="oe_clear"/>

View File

@ -21,19 +21,26 @@
import base64
import email
from email.utils import parsedate
import logging
from mail_message import decode, to_email
from operator import itemgetter
from osv import osv, fields
import re
import time
import xmlrpclib
from email.utils import parsedate
from email.message import Message
from osv import osv, fields
from mail_message import decode, to_email
import tools
from tools.translate import _
import xmlrpclib
from tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
def decode_header(message, header, separator=' '):
return separator.join(map(decode,message.get_all(header, [])))
class mail_thread(osv.Model):
'''Mixin model, meant to be inherited by any model that needs to
act as a discussion topic on which messages can be attached.
@ -151,10 +158,9 @@ class mail_thread(osv.Model):
message_obj = self.pool.get('mail.message')
notification_obj = self.pool.get('mail.notification')
body = vals.get('body_html', '') if vals.get('content_subtype') == 'html' else vals.get('body_text', '')
# automatically subscribe the writer of the message
if vals['user_id']:
if vals.get('user_id'):
self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context)
# create message
@ -173,12 +179,11 @@ class mail_thread(osv.Model):
notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
# create the email to send
email_id = self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context)
self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context)
return msg_id
def message_get_user_ids_to_notify(self, cr, uid, thread_ids, new_msg_vals, context=None):
subscription_obj = self.pool.get('mail.subscription')
# get body
body = new_msg_vals.get('body_html', '') if new_msg_vals.get('content_subtype') == 'html' else new_msg_vals.get('body_text', '')
@ -301,7 +306,6 @@ class mail_thread(osv.Model):
threads = model_pool.browse(cr, uid, threads, context=context)
ir_attachment = self.pool.get('ir.attachment')
mail_message = self.pool.get('mail.message')
new_msg_ids = []
for thread in threads:
@ -501,10 +505,6 @@ class mail_thread(osv.Model):
""" Retrieve all attachments names """
map_id_to_name = dict((attachment_id, '') for message in messages for attachment_id in message['attachment_ids'])
map_id_to_name = {}
for msg in messages:
for attach_id in msg["attachment_ids"]:
map_id_to_name[attach_id] = '' # use empty string as a placeholder
ids = map_id_to_name.keys()
names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
@ -560,79 +560,127 @@ class mail_thread(osv.Model):
msgs = msg_obj.read(cr, uid, msg_ids, context=context)
return msgs
def _get_user(self, cr, uid, alias, context):
"""
param alias: browse record of alias.
return: int user_id.
"""
user_obj = self.pool.get('res.user')
user_id = 1
if alias.alias_user_id:
user_id = alias.alias_user_id.id
#if user_id not defined in the alias then search related user using name of Email sender
else:
from_email = msg.get('from')
user_ids = user_obj.search(cr, uid, [('name','=',from_email)], context)
if user_ids:
user_id = user_obj.browse(cr, uid, user_ids[0], context).id
return user_id
def message_catchall(self, cr, uid, message, context=None):
"""
Process incoming mail and call messsage_process using details of the mail.alias model
else raise Exception so that mailgate script will reject the mail and
send notification mail sender that this mailbox does not exist so your mail have been rejected.
"""
mail_alias = self.pool.get('mail.alias')
mail_message = self.pool.get('mail.message')
if isinstance(message, xmlrpclib.Binary):
message = str(message.data)
if isinstance(message, unicode):
message = message.encode('utf-8')
msg_txt = email.message_from_string(message)
msg = mail_message.parse_message(msg_txt)
alias_name = msg.get('to').split("@")[0] # @@@@
alias_ids = mail_alias.search(cr, uid, [('alias_name','=',alias_name)])
#if alias found then call message_process method. # @@@@
if alias_ids:
alias_id = mail_alias.browse(cr, uid, alias_ids[0], context)
user_id = self._get_user( cr, uid, alias_id, context)
alias_defaults = dict(eval(alias_id.alias_defaults or {}))
self.message_process(cr, user_id, alias_id.alias_model_id.model, message,
custom_values=alias_defaults,
thread_id=alias_id.alias_force_thread_id or False,
context=context)
else:
#if Mail box for the intended Mail Alias then give logger warning
_logger.warning("No Mail Alias Found for the name '%s'."%(alias_name))
raise # @@@@
return True
def _message_find_user_id(self, cr, uid, message, context=None):
from_local_part = to_email(decode(message.get('From')))[0]
user_ids = self.pool.get('res.users').search(cr, uid, [('login', '=', from_local_part)], context=context)
return user_ids[0] if user_ids else uid
#------------------------------------------------------
# Mail gateway
#------------------------------------------------------
# message_process will call either message_new or message_update.
def message_route(self, cr, uid, message, model=None, thread_id=None,
custom_values=None, context=None):
"""Attempt to figure out the correct target model, thread_id,
custom_values and user_id to use for an incoming message.
Multiple values may be returned, if a message had multiple
recipients matching existing mail.aliases, for example.
The following heuristics are used, in this order:
1. If the message replies to an existing thread_id, and
properly contains the thread model in the 'In-Reply-To'
header, use this model/thread_id pair, and ignore
custom_value (not needed as no creation will take place)
2. Look for a mail.alias entry matching the message
recipient, and use the corresponding model, thread_id,
custom_values and user_id.
3. Fallback to the ``model``, ``thread_id`` and ``custom_values``
provided.
4. If all the above fails, raise an exception.
:param string message: an email.message instance
:param string model: the fallback model to use if the message
does not match any of the currently configured mail aliases
(may be None if a matching alias is supposed to be present)
:type dict custom_values: optional dictionary of default field values
to pass to ``message_new`` if a new record needs to be created.
Ignored if the thread record already exists, and also if a
matching mail.alias was found (aliases define their own defaults)
:param int thread_id: optional ID of the record/thread from ``model``
to which this mail should be attached. Only used if the message
does not reply to an existing thread and does not match any mail alias.
:return: list of [model, thread_id, custom_values, user_id]
"""
assert isinstance(message, Message), 'message must be an email.message.Message at this point'
message_id = message.get('Message-Id')
# 1. Verify if this is a reply to an existing thread
references = decode_header(message, 'References') or decode_header(message, 'In-Reply-To')
ref_match = references and tools.reference_re.search(references)
if ref_match:
thread_id = int(ref_match.group(1))
model = ref_match.group(2) or model
model_pool = self.pool.get(model)
if thread_id and model and model_pool and model_pool.exists(cr, uid, thread_id) \
and hasattr(model_pool, 'message_update'):
_logger.debug('Routing mail with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s',
message_id, model, thread_id, custom_values, uid)
return [(model, thread_id, custom_values, uid)]
# 2. Look for a matching mail.alias entry
# Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
# for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
rcpt_tos = decode_header(message, 'Delivered-To') or \
','.join([decode_header(message, 'To'),
decode_header(message, 'Cc'),
decode_header(message, 'Resent-To'),
decode_header(message, 'Resent-Cc')])
local_parts = [e.split('@')[0] for e in to_email(rcpt_tos)]
if local_parts:
mail_alias = self.pool.get('mail.alias')
alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)])
if alias_ids:
routes = []
for alias in mail_alias.browse(cr, uid, alias_ids, context=context):
user_id = alias.alias_user_id.id
if not user_id:
user_id = self._message_find_user_id(cr, uid, message, context=context)
routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \
eval(alias.alias_defaults), user_id))
_logger.debug('Routing mail with Message-Id %s: direct alias match: %r', message_id, routes)
return routes
# 3. Fallback to the provided parameters, if they work
model_pool = self.pool.get(model)
if not thread_id:
# Legacy: fallback to matching [ID] in the Subject
match = tools.res_re.search(decode_header(message, 'Subject'))
thread_id = match and match.group(1)
assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
"No possible route found for incoming message with Message-Id %s. " \
"Create an appropriate mail.alias or force the destination model." % message_id
if thread_id and not model_pool.exists(cr, uid, thread_id):
_logger.warning('Received mail reply to missing document %s! Ignoring and creating new document instead for Message-Id %s',
thread_id, message_id)
thread_id = None
_logger.debug('Routing mail with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
message_id, model, thread_id, custom_values, uid)
return [(model, thread_id, custom_values, uid)]
def message_process(self, cr, uid, model, message, custom_values=None,
save_original=False, strip_attachments=False,
thread_id=None, context=None):
"""Process an incoming RFC2822 email message related to the
given thread model, relying on ``mail.message.parse()``
for the parsing operation, and then calling ``message_new``
(if the thread record did not exist) or ``message_update``
(if it did), then calling ``message_forward`` to automatically
notify other people that should receive this message.
"""Process an incoming RFC2822 email message, relying on
``mail.message.parse()`` for the parsing operation,
and ``message_route()`` to figure out the target model.
Once the target model is known, its ``message_new`` method
is called with the new message (if the thread record did not exist)
or its ``message_update`` method (if it did). Finally,
``message_forward`` is called to automatically notify other
people that should receive this message.
:param string model: the thread model for which a new message
must be processed
:param message: source of the RFC2822 mail
:param string model: the fallback model to use if the message
does not match any of the currently configured mail aliases
(may be None if a matching alias is supposed to be present)
:param message: source of the RFC2822 message
:type message: string or xmlrpclib.Binary
:type dict custom_values: optional dictionary of field values
to pass to ``message_new`` if a new
record needs to be created. Ignored
if the thread record already exists.
to pass to ``message_new`` if a new record needs to be created.
Ignored if the thread record already exists, and also if a
matching mail.alias was found (aliases define their own defaults)
:param bool save_original: whether to keep a copy of the original
email source attached to the message after it is imported.
:param bool strip_attachments: whether to strip all attachments
@ -642,66 +690,41 @@ class mail_thread(osv.Model):
overrides the automatic detection based on the message
headers.
"""
if context is None: context = {}
# extract message bytes - we are forced to pass the message as binary because
# we don't know its encoding until we parse its headers and hence can't
# convert it to utf-8 for transport between the mailgate script and here.
if isinstance(message, xmlrpclib.Binary):
message = str(message.data)
if context is None: context = {}
mail_message = self.pool.get('mail.message')
model_pool = self.pool.get(model)
if self._name != model:
context.update({'thread_model': model})
# Parse Message
# Warning: message_from_string doesn't always work correctly on unicode,
# we must use utf-8 strings here :-(
if isinstance(message, unicode):
message = message.encode('utf-8')
msg_txt = email.message_from_string(message)
msg = mail_message.parse_message(msg_txt, save_original=save_original, context=context)
# update state
msg['state'] = 'received'
routes = self.message_route(cr, uid, msg_txt, model,
thread_id, custom_values,
context=context)
msg = self.pool.get('mail.message').parse_message(msg_txt, save_original=save_original, context=context)
msg['state'] = 'received'
if strip_attachments and 'attachments' in msg:
del msg['attachments']
# Create New Record into particular model
def create_record(msg):
if hasattr(model_pool, 'message_new'):
return model_pool.message_new(cr, uid, msg,
custom_values,
context=context)
if not thread_id and (msg.get('references') or msg.get('in-reply-to')):
references = msg.get('references') or msg.get('in-reply-to')
if '\r\n' in references:
references = references.split('\r\n')
for model, thread_id, custom_values, user_id in routes:
if self._name != model:
context.update({'thread_model': model})
model_pool = self.pool.get(model)
assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
"Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
(msg['message-id'], model)
if thread_id and hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, user_id, [thread_id], msg, context=context)
else:
references = references.split(' ')
for ref in references:
ref = ref.strip()
thread_id = tools.reference_re.search(ref)
if not thread_id:
thread_id = tools.res_re.search(msg['subject'])
if thread_id:
thread_id = int(thread_id.group(1))
if not model_pool.exists(cr, uid, thread_id) or \
not hasattr(model_pool, 'message_update'):
# referenced thread not found or not updatable,
# -> create a new one
thread_id = False
if not thread_id:
thread_id = create_record(msg)
else:
model_pool.message_update(cr, uid, [thread_id], msg, {}, context=context)
# To forward the email to other followers
self.message_forward(cr, uid, model, [thread_id], msg_txt, context=context)
# Set as Unread
model_pool.message_mark_as_unread(cr, uid, [thread_id], context=context)
return thread_id
thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context)
# Forward the email to other followers
self.message_forward(cr, uid, model, [thread_id], msg_txt, context=context)
model_pool.message_mark_as_unread(cr, uid, [thread_id], context=context)
return True
def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
"""Called by ``message_process`` when a new message is received
@ -735,7 +758,7 @@ class mail_thread(osv.Model):
fields = model_pool.fields_get(cr, uid, context=context)
data = model_pool.default_get(cr, uid, fields, context=context)
if 'name' in fields and not data.get('name'):
data['name'] = msg_dict.get('from', '')
data['name'] = msg_dict.get('subject', '')
if custom_values and isinstance(custom_values, dict):
data.update(custom_values)
res_id = model_pool.create(cr, uid, data, context=context)
@ -791,7 +814,6 @@ class mail_thread(osv.Model):
"""
model_pool = self.pool.get(model)
smtp_server_obj = self.pool.get('ir.mail_server')
mail_message = self.pool.get('mail.message')
for res in model_pool.browse(cr, uid, thread_ids, context=context):
if hasattr(model_pool, 'message_thread_followers'):
followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]

View File

@ -54,6 +54,7 @@ class res_users(osv.osv):
_defaults = {
'notification_email_pref': 'to_me',
'alias_domain': False, # always hide alias during creation
}
def __init__(self, pool, cr):
@ -67,32 +68,45 @@ class res_users(osv.osv):
self.SELF_WRITEABLE_FIELDS.append('notification_email_pref')
return init_res
def init(self, cr):
# Installation hook to create aliases for all users, right after _auto_init
def _auto_init(self, cr, context=None):
"""Installation hook to create aliases for all users and avoid constraint errors."""
# disable the unique alias_id not null constraint, to avoid spurious warning during
# super.auto_init. We'll reinstall it afterwards.
self._columns['alias_id'].required = False
super(res_users,self)._auto_init(cr, context=context)
registry = RegistryManager.get(cr.dbname)
mail_alias = registry.get('mail.alias')
res_users = registry.get('res.users')
users_no_alias = res_users.search(cr, SUPERUSER_ID, [('alias_id', '=', False)])
res_users_model = registry.get('res.users')
users_no_alias = res_users_model.search(cr, SUPERUSER_ID, [('alias_id', '=', False)])
# Use read() not browse(), to avoid prefetching uninitialized inherited fields
for user_data in res_users.read(cr, SUPERUSER_ID, users_no_alias, ['login']):
for user_data in res_users_model.read(cr, SUPERUSER_ID, users_no_alias, ['login']):
alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, {'alias_name': user_data['login'],
'alias_force_id': user_data['id']},
model_name=self._name)
res_users.write(cr, SUPERUSER_ID, user_data['id'], {'alias_id': alias_id})
res_users_model.write(cr, SUPERUSER_ID, user_data['id'], {'alias_id': alias_id})
_logger.info('Mail alias created for user %s (uid %s)', user_data['login'], user_data['id'])
# Finally attempt to reinstate the missing constraint
try:
cr.execute('ALTER TABLE res_users ALTER COLUMN alias_id SET NOT NULL')
except Exception:
pass
_logger.warning("Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\
"If you want to have it, you should update the records and execute manually:\n"\
"ALTER TABLE %s ALTER COLUMN %s SET NOT NULL",
self._table, 'alias_id', self._table, 'alias_id')
self._columns['alias_id'].required = True
def create(self, cr, uid, data, context=None):
# create default alias same as the login
mail_alias = self.pool.get('mail.alias')
alias_id = mail_alias.create_unique_alias(cr, uid, {'alias_name': data['login']}, model_name=self._name, context=context)
data['alias_id'] = alias_id
data.pop('alias_name', None) # prevent errors during copy()
user_id = super(res_users, self).create(cr, uid, data, context=context)
mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context)

View File

@ -33,7 +33,7 @@
</xpath>
<field name="user_email" position="after">
<field name="alias_domain" invisible="1"/>
<field name="alias_id" readonly="1" attrs="{'invisible': [('alias_domain', '=', False)]}"/>
<field name="alias_id" readonly="1" required="0" attrs="{'invisible': [('alias_domain', '=', False)]}"/>
</field>
</data>
</field>

View File

@ -106,15 +106,15 @@ class EmailParser(object):
self.email_default = email_default
def parse(self, method, message, custom_values=None, save_original=None):
def parse(self, message, custom_values=None, save_original=None):
# pass message as bytes because we don't know its encoding until we parse its headers
# and hence can't convert it to utf-8 for transport
res_id = self.rpc('mail.thread',
method,
self.model,
xmlrpclib.Binary(message),
custom_values or {},
save_original or False)
return self.rpc('mail.thread',
'message_process',
self.model,
xmlrpclib.Binary(message),
custom_values or {},
save_original or False)
def configure_parser():
parser = optparse.OptionParser(usage='usage: %prog [options]', version='%prog v1.1')
@ -123,32 +123,32 @@ def configure_parser():
"with the OpenERP server for case management in the CRM module.")
parser.add_option_group(group)
parser.add_option("-u", "--user", dest="userid",
help="ID of the user in OpenERP",
help="OpenERP user id to connect with",
default=config.OPENERP_DEFAULT_USER_ID, type='int')
parser.add_option("-p", "--password", dest="password",
help="Password of the user in OpenERP",
help="OpenERP user password",
default=config.OPENERP_DEFAULT_PASSWORD)
parser.add_option("-o", "--model", dest="model",
help="Name or ID of crm model",
help="Name or ID of destination model",
default="crm.lead")
parser.add_option("-m", "--default", dest="default",
help="Default eMail in case of any trouble.",
help="Admin email for error notifications.",
default=None)
parser.add_option("-d", "--dbname", dest="dbname",
help="Database name (default: %default)",
help="OpenERP database name (default: %default)",
default=config.OPENERP_DEFAULT_DATABASE)
parser.add_option("--host", dest="host",
help="Hostname of the OpenERP Server",
help="OpenERP Server hostname",
default=config.OPENERP_HOSTNAME)
parser.add_option("--port", dest="port",
help="Port of the OpenERP Server",
help="OpenERP Server XML-RPC port number",
default=config.OPENERP_PORT)
parser.add_option("--custom-values", dest="custom_values",
help="Add Custom Values to the object",
help="Dictionary of extra values to pass when creating records",
default=None)
parser.add_option("-s", dest="save_original",
action="store_true",
help="Attach a copy of original email to the message entry",
help="Keep a full copy of the email source attached to each message",
default=False)
return parser
@ -160,7 +160,6 @@ def main():
parser = configure_parser()
(options, args) = parser.parse_args()
method = "message_process"
email_parser = EmailParser(options.userid,
options.password,
options.dbname,
@ -170,8 +169,6 @@ def main():
email_default= options.default)
msg_txt = sys.stdin.read()
custom_values = {}
if not options.model:
method = "message_catchall"
try:
custom_values = dict(eval(options.custom_values or "{}" ))
except:
@ -179,7 +176,7 @@ def main():
traceback.print_exc()
try:
email_parser.parse(method, msg_txt, custom_values, options.save_original or False)
email_parser.parse(msg_txt, custom_values, options.save_original or False)
except Exception:
msg = '\n'.join([
'parameters',

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import test_mail
checks = [
test_mail,
]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.tests import common
MAIL_TEMPLATE = """Return-Path: <whatever-2a840@postmaster.twitter.com>
To: {to}
Received: by mail1.openerp.com (Postfix, from userid 10002)
id 5DF9ABFB2A; Fri, 10 Aug 2012 16:16:39 +0200 (CEST)
From: Sylvie Lelitre <sylvie.lelitre@agrolait.com>
Subject: {subject}
MIME-Version: 1.0
Content-Type: multipart/alternative;
boundary="----=_Part_4200734_24778174.1344608186754"
Date: Fri, 10 Aug 2012 14:16:26 +0000
Message-ID: <1198923581.41972151344608186760.JavaMail@agrolait.com>
{extra}
------=_Part_4200734_24778174.1344608186754
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Please call me as soon as possible this afternoon!
--
Sylvie
------=_Part_4200734_24778174.1344608186754
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>=20
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf-8" />
</head>=20
<body style=3D"margin: 0; padding: 0; background: #ffffff;-webkit-text-size-adjust: 100%;">=20
<p>Please call me as soon as possible this afternoon!</p>
<p>--<br/>
Sylvie
<p>
</body>
</html>
------=_Part_4200734_24778174.1344608186754--
"""
class test_mail(common.TransactionCase):
def setUp(self):
super(test_mail, self).setUp()
self.ir_model = self.registry('ir.model')
self.mail_alias = self.registry('mail.alias')
self.mail_thread = self.registry('mail.thread')
self.mail_group = self.registry('mail.group')
self.res_users = self.registry('res.users')
# groups@.. will cause the creation of new mail groups
self.mail_group_model_id = self.ir_model.search(self.cr, self.uid, [('model','=', 'mail.group')])[0]
self.mail_alias.create(self.cr, self.uid, {'alias_name': 'groups',
'alias_model_id': self.mail_group_model_id})
# tech@... will append new messages to the 'tech' group
self.group_tech_id = self.mail_group.create(self.cr, self.uid, {'name': 'tech'})
def test_message_process(self):
# Incoming mail creates a new mail_group "frogs"
self.assertEqual(self.mail_group.search(self.cr, self.uid, [('name','=','frogs')]), [])
mail_frogs = MAIL_TEMPLATE.format(to='groups@example.com, other@gmail.com', subject='frogs', extra='')
self.mail_thread.message_process(self.cr, self.uid, None, mail_frogs)
frog_groups = self.mail_group.search(self.cr, self.uid, [('name','=','frogs')])
self.assertTrue(len(frog_groups) == 1)
# Previously-created group can be emailed now - it should have an implicit alias group+frogs@...
frog_group = self.mail_group.browse(self.cr, self.uid, frog_groups[0])
group_messages = frog_group.message_ids
self.assertTrue(len(group_messages) == 1, 'New group should only have the original message')
mail_frog_news = MAIL_TEMPLATE.format(to='Friendly Frogs <group+frogs@example.com>', subject='news', extra='')
self.mail_thread.message_process(self.cr, self.uid, None, mail_frog_news)
frog_group.refresh()
self.assertTrue(len(frog_group.message_ids) == 2, 'Group should contain 2 messages now')
# Even with a wrong destination, a reply should end up in the correct thread
mail_reply = MAIL_TEMPLATE.format(to='erroneous@example.com>', subject='Re: news',
extra='In-Reply-To: <12321321-openerp-%d-mail.group@example.com>\n'%frog_group.id)
self.mail_thread.message_process(self.cr, self.uid, None, mail_reply)
frog_group.refresh()
self.assertTrue(len(frog_group.message_ids) == 3, 'Group should contain 3 messages now')
# No model passed and no matching alias must raise
mail_spam = MAIL_TEMPLATE.format(to='noone@example.com', subject='spam', extra='')
self.assertRaises(Exception,
self.mail_thread.message_process,
self.cr, self.uid, None, mail_spam)

View File

@ -267,6 +267,7 @@ class project(osv.osv):
'type_ids': _get_type_common,
'alias_model': 'project.task',
'privacy_visibility': 'public',
'alias_domain': False, # always hide alias during creation
}
# TODO: Why not using a SQL contraints ?
@ -337,11 +338,11 @@ class project(osv.osv):
context['active_test'] = False
default['state'] = 'open'
default['tasks'] = []
default['alias_id'] = False
default.pop('alias_name', None)
default.pop('alias_id', None)
proj = self.browse(cr, uid, id, context=context)
if not default.get('name', False):
default['name'] = proj.name + _(' (copy)')
default['alias_name'] = default['name']
res = super(project, self).copy(cr, uid, id, default, context)
self.map_tasks(cr,uid,id,res,context)
return res
@ -527,11 +528,13 @@ def Project():
context = dict(context, project_creation_in_progress=True)
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
name = vals.pop('alias_name', None) or vals['name']
alias_id = mail_alias.create_unique_alias(cr, uid,
{'alias_name': "project_"+short_name(name)},
model_name=vals.get('alias_model', 'project.task'),
context=context)
vals.pop('alias_name', None) # prevent errors during copy()
alias_id = mail_alias.create_unique_alias(cr, uid,
# Using '+' allows using subaddressing for those who don't
# have a catchall domain setup.
{'alias_name': "project+"+short_name(vals['name'])},
model_name=vals.get('alias_model', 'project.task'),
context=context)
vals['alias_id'] = alias_id
project_id = super(project, self).create(cr, uid, vals, context)
mail_alias.write(cr, uid, [vals['alias_id']], {'alias_defaults': {'project_id': project_id} }, context)

View File

@ -81,7 +81,7 @@
<field name="name" string="Project Name"/>
</h1>
<div attrs="{'invisible': [('alias_domain', '=', False)]}">
<field name="alias_id" invisible="1"/>
<field name="alias_id" invisible="1" required="0"/>
<label for="alias_name" class="oe_edit_only "/>
<field name="alias_name" class="oe_inline" attrs="{'required': [('alias_id', '!=', False)]}"/>@<field name="alias_domain" class="oe_inline"/>
</div>

View File

@ -267,9 +267,6 @@ class project_issue(base_stage, osv.osv):
}),
}
def on_change_project(self, cr, uid, ids, project_id, context=None):
return {}
_defaults = {
'active': 1,
'partner_id': lambda s, cr, uid, c: s._get_default_partner(cr, uid, c),
@ -496,7 +493,7 @@ class project_issue(base_stage, osv.osv):
key = maps.get(res.group(1).lower())
update_vals[key] = res.group(2).lower()
return super(project_issue, self).message_update(cr, uid, ids, update_vals=update_vals, context=context)
return super(project_issue, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context)
# -------------------------------------------------------
# OpenChatter methods and notifications