summaryrefslogtreecommitdiff
path: root/addons/mail
diff options
context:
space:
mode:
authorThibault Delavallée <tde@openerp.com>2012-08-15 21:56:52 +0200
committerThibault Delavallée <tde@openerp.com>2012-08-15 21:56:52 +0200
commit61274acd896f72e6bec98deca8faa93d9895b118 (patch)
treecd839020bde7def97753e5dd890d1012dde456b7 /addons/mail
parenta6482fe07c57805976529eebd48225b060a9a49c (diff)
parent9958acdc3286f761b6ce76142598d628736a5540 (diff)
[MERGE] Merged with addons/trunk.
bzr revid: tde@openerp.com-20120815195652-839w3fyumomq0ubf
Diffstat (limited to 'addons/mail')
-rw-r--r--addons/mail/__init__.py2
-rw-r--r--addons/mail/data/mail_group_data.xml2
-rw-r--r--addons/mail/doc/mail_openchatter_howto.rst1
-rw-r--r--addons/mail/mail_alias.py27
-rw-r--r--addons/mail/mail_alias_view.xml17
-rw-r--r--addons/mail/mail_followers_view.xml1
-rw-r--r--addons/mail/mail_group.py19
-rw-r--r--addons/mail/mail_group_view.xml9
-rw-r--r--addons/mail/mail_message.py13
-rw-r--r--addons/mail/mail_message_view.xml11
-rw-r--r--addons/mail/mail_thread.py275
-rw-r--r--addons/mail/res_config_view.xml1
-rw-r--r--addons/mail/res_partner.py18
-rw-r--r--addons/mail/res_partner_view.xml1
-rw-r--r--addons/mail/res_users.py131
-rw-r--r--addons/mail/res_users_view.xml13
-rwxr-xr-xaddons/mail/static/scripts/openerp_mailgate.py37
-rw-r--r--addons/mail/tests/__init__.py27
-rw-r--r--addons/mail/tests/test_mail.py112
-rw-r--r--addons/mail/wizard/mail_compose_message.py6
-rw-r--r--addons/mail/wizard/mail_compose_message_view.xml4
21 files changed, 467 insertions, 260 deletions
diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py
index 7da72f95fac..289b9080fe7 100644
--- a/addons/mail/__init__.py
+++ b/addons/mail/__init__.py
@@ -25,8 +25,8 @@ import mail_followers
import mail_thread
import mail_group
import ir_needaction
-import res_users
import res_partner
+import res_users
import report
import wizard
import res_config
diff --git a/addons/mail/data/mail_group_data.xml b/addons/mail/data/mail_group_data.xml
index 443b43b62d6..2152706d056 100644
--- a/addons/mail/data/mail_group_data.xml
+++ b/addons/mail/data/mail_group_data.xml
@@ -19,7 +19,7 @@
<value>Welcome to OpenERP!</value>
<value>Your homepage is a summary of messages you received and key information about documents you follow.
-The top menu bar contains all applications you installed. You can use this &lt;i&gt;Settings&lt;/i&gt; menu to intall more applications, activate others features or give access to new users.
+The top menu bar contains all applications you installed. You can use this &lt;i&gt;Settings&lt;/i&gt; menu to install more applications, activate others features or give access to new users.
To setup your preferences (name, email signature, avatar), click on the top right corner.</value>
</function>
diff --git a/addons/mail/doc/mail_openchatter_howto.rst b/addons/mail/doc/mail_openchatter_howto.rst
index 4c8797e7f8b..789acdd87eb 100644
--- a/addons/mail/doc/mail_openchatter_howto.rst
+++ b/addons/mail/doc/mail_openchatter_howto.rst
@@ -45,7 +45,6 @@ Use the thread viewer widget inside your form view by using the mail_thread widg
<record model="ir.ui.view" id="my_task_form_view">
<field name="name">My Task</field>
<field name="model">my.task</field>
- <field name="type">form</field>
<field name="priority">1</field>
<field name="arch" type="xml">
<form>
diff --git a/addons/mail/mail_alias.py b/addons/mail/mail_alias.py
index 807ed5dbe61..82b20f39480 100644
--- a/addons/mail/mail_alias.py
+++ b/addons/mail/mail_alias.py
@@ -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)
-
-
diff --git a/addons/mail/mail_alias_view.xml b/addons/mail/mail_alias_view.xml
index 61a89b774ca..e0d8e173ebc 100644
--- a/addons/mail/mail_alias_view.xml
+++ b/addons/mail/mail_alias_view.xml
@@ -6,19 +6,16 @@
<record model="ir.ui.view" id="view_mail_alias_form">
<field name="name">mail.alias.form</field>
<field name="model">mail.alias</field>
- <field name="type">form</field>
<field name="arch" type="xml">
<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>
@@ -29,7 +26,6 @@
<record model="ir.ui.view" id="view_mail_alias_tree">
<field name="name">mail.alias.tree</field>
<field name="model">mail.alias</field>
- <field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Alias">
<field name="alias_name"/>
@@ -44,7 +40,6 @@
<record model="ir.ui.view" id="view_mail_alias_search">
<field name="name">mail.alias.search</field>
<field name="model">mail.alias</field>
- <field name="type">search</field>
<field name="arch" type="xml">
<search string="Search Alias">
<field name="alias_name"/>
diff --git a/addons/mail/mail_followers_view.xml b/addons/mail/mail_followers_view.xml
index b324cfb1ae9..62e93430675 100644
--- a/addons/mail/mail_followers_view.xml
+++ b/addons/mail/mail_followers_view.xml
@@ -21,7 +21,6 @@
<record model="ir.ui.view" id="view_notification_tree">
<field name="name">mail.notification.tree</field>
<field name="model">mail.notification</field>
- <field name="type">tree</field>
<field name="priority">10</field>
<field name="arch" type="xml">
<tree string="Notifications">
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index 306a758eda2..6a14fef83aa 100644
--- a/addons/mail/mail_group.py
+++ b/addons/mail/mail_group.py
@@ -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.Model):
"""
@@ -53,7 +51,6 @@ class mail_group(osv.Model):
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)
@@ -120,6 +117,7 @@ class mail_group(osv.Model):
'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):
@@ -135,12 +133,14 @@ class mail_group(osv.Model):
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)
@@ -160,7 +160,8 @@ class mail_group(osv.Model):
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)
diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml
index cbbe999f7d5..a71ec972326 100644
--- a/addons/mail/mail_group_view.xml
+++ b/addons/mail/mail_group_view.xml
@@ -11,7 +11,6 @@
<record model="ir.ui.view" id="view_group_kanban">
<field name="name">mail.group.kanban</field>
<field name="model">mail.group</field>
- <field name="type">kanban</field>
<field name="priority" eval="10"/>
<field name="arch" type="xml">
<kanban>
@@ -55,7 +54,6 @@
<record model="ir.ui.view" id="view_group_form">
<field name="name">mail.group.form</field>
<field name="model">mail.group</field>
- <field name="type">form</field>
<field name="priority" eval="10"/>
<field name="arch" type="xml">
<form string="Group Form" version="7.0">
@@ -66,6 +64,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"/>
@@ -96,7 +99,6 @@
<record model="ir.ui.view" id="view_group_tree">
<field name="name">mail.group.tree</field>
<field name="model">mail.group</field>
- <field name="type">tree</field>
<field name="priority" eval="10"/>
<field name="arch" type="xml">
<tree string="Groups">
@@ -110,7 +112,6 @@
<record model="ir.ui.view" id="view_group_search">
<field name="name">mail.group.search</field>
<field name="model">mail.group</field>
- <field name="type">search</field>
<field name="priority" eval="10"/>
<field name="arch" type="xml">
<search string="Search Groups">
diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py
index b96b6e3791e..7801a9b42d9 100644
--- a/addons/mail/mail_message.py
+++ b/addons/mail/mail_message.py
@@ -93,6 +93,9 @@ class mail_message_common(osv.TransientModel):
return result
def name_get(self, cr, uid, ids, context=None):
+ # name_get may receive int id instead of an id list
+ if isinstance(ids, (int, long)):
+ ids = [ids]
res = []
for message in self.browse(cr, uid, ids, context=context):
name = ''
@@ -129,7 +132,7 @@ class mail_message_common(osv.TransientModel):
help="Type of message, usually 'html' or 'plain', used to select "\
"plain-text or rich-text contents accordingly"),
'body_text': fields.text('Text Contents', help="Plain-text version of the message"),
- 'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML version of the message"),
+ 'body_html': fields.html('Rich-text Contents', help="Rich-text/HTML version of the message"),
'body': fields.function(get_body, fnct_search = search_body, type='text',
string='Message Content', store=True,
help="Content of the message. This content equals the body_text field "\
@@ -208,12 +211,13 @@ class mail_message(osv.Model):
'partner_id': fields.many2one('res.partner', 'Related partner',
help="Deprecated field. Use partner_ids instead."),
'partner_ids': fields.many2many('res.partner',
- 'mail_message_destination_partner_rel',
+ 'mail_message_res_partner_rel',
'message_id', 'partner_id', 'Destination partners',
help="When sending emails through the social network composition wizard"\
"you may choose to send a copy of the mail to partners."),
'user_id': fields.many2one('res.users', 'Related User', readonly=1),
- 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'),
+ 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel',
+ 'message_id', 'attachment_id', 'Attachments'),
'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
'state': fields.selection([
('outgoing', 'Outgoing'),
@@ -371,7 +375,8 @@ class mail_message(osv.Model):
}
email_msg_id = self.create(cr, uid, msg_vals, context)
attachment_ids = []
- for fname, fcontent in attachments.iteritems():
+ for attachment in attachments:
+ fname, fcontent = attachment
attachment_data = {
'name': fname,
'datas_fname': fname,
diff --git a/addons/mail/mail_message_view.xml b/addons/mail/mail_message_view.xml
index b967035bec3..0aa5261d7e7 100644
--- a/addons/mail/mail_message_view.xml
+++ b/addons/mail/mail_message_view.xml
@@ -5,7 +5,6 @@
<record model="ir.ui.view" id="view_message_tree_short">
<field name="name">mail.message.tree.short</field>
<field name="model">mail.message</field>
- <field name="type">tree</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<tree string="Messages">
@@ -22,7 +21,6 @@
<record model="ir.ui.view" id="view_message_form_short">
<field name="name">mail.message.form.short</field>
<field name="model">mail.message</field>
- <field name="type">form</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<form string="Message" version="7.0">
@@ -44,7 +42,7 @@
</group>
<notebook>
<page string="Body (Rich)">
- <field name="body_html" widget="text_html"/>
+ <field name="body_html"/>
</page>
<page string="Body (Plain)">
<field name="body_text" widget="text"/>
@@ -59,7 +57,6 @@
<record model="ir.ui.view" id="view_message_search_short">
<field name="name">mail.message.search.short</field>
<field name="model">mail.message</field>
- <field name="type">search</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<search string="Messages Search">
@@ -74,7 +71,6 @@
<record model="ir.ui.view" id="view_message_search_wall">
<field name="name">mail.message.search.wall</field>
<field name="model">mail.message</field>
- <field name="type">search</field>
<field name="priority">25</field>
<field name="arch" type="xml">
<search string="Messages Search">
@@ -110,7 +106,6 @@
<record model="ir.ui.view" id="view_email_message_form">
<field name="name">mail.message.form</field>
<field name="model">mail.message</field>
- <field name="type">form</field>
<field name="arch" type="xml">
<form string="Email message" version="7.0">
<sheet>
@@ -138,7 +133,7 @@
</group>
<notebook>
<page string="Body (Rich)">
- <field name="body_html" widget="text_html"/>
+ <field name="body_html"/>
</page>
<page string="Body (Plain)">
<field name="body_text" widget="text"/>
@@ -178,7 +173,6 @@
<record model="ir.ui.view" id="view_email_message_tree">
<field name="name">mail.message.tree</field>
<field name="model">mail.message</field>
- <field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Emails" colors="grey:state in ('sent', 'cancel');blue:state=='outgoing';red:state=='exception';black:state=='received'">
<field name="date"/>
@@ -202,7 +196,6 @@
<record model="ir.ui.view" id="view_email_message_search">
<field name="name">mail.message.search</field>
<field name="model">mail.message</field>
- <field name="type">search</field>
<field name="arch" type="xml">
<search string="Email Search">
<field name="email_from" filter_domain="['|' '|',('email_from','ilike',self), ('email_to','ilike',self), ('subject','ilike',self)]" string="Email"/>
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index 473dffe21fd..5bd9582b7cd 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -21,19 +21,24 @@
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 many2many_reference(fields.many2many):
""" many2many_reference is an override of fields.many2many. It manages
many2many-like table where one id is given by two fields, res_model
@@ -257,10 +262,9 @@ class mail_thread(osv.Model):
context = {}
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
@@ -406,7 +410,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:
@@ -606,10 +609,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)
@@ -665,79 +664,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.
-
- :param string model: the thread model for which a new message
- must be processed
- :param message: source of the RFC2822 mail
+ """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 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
@@ -747,66 +794,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
@@ -840,7 +862,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)
@@ -876,7 +898,7 @@ class mail_thread(osv.Model):
for thread in self.browse(cr, uid, ids, context=context):
l = set()
for message in thread.message_ids:
- l.add((message.user_id and message.user_id.user_email) or '')
+ l.add((message.user_id and message.user_id.email) or '')
l.add(message.email_from or '')
l.add(message.email_cc or '')
res[thread.id] = filter(None, l)
@@ -896,7 +918,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]
@@ -1071,9 +1092,9 @@ class mail_thread(osv.Model):
if not user.notification_email_pref == 'all' and \
not (user.notification_email_pref == 'to_me' and user.id in user_to_push_from_parse_ids):
continue
- if not user.user_email:
+ if not user.email:
continue
- email_to = '%s, %s' % (email_to, user.user_email)
+ email_to = '%s, %s' % (email_to, user.email)
email_to = email_to.lstrip(', ')
# did not find any email address: not necessary to create an email
@@ -1084,7 +1105,7 @@ class mail_thread(osv.Model):
current_user = res_users_obj.browse(cr, uid, [uid], context=context)[0]
email_from = new_msg_values.get('email_from')
if not email_from:
- email_from = current_user.user_email
+ email_from = current_user.email
# get email content, create it (with mail_message.create)
email_values = self.message_create_notify_get_email_dict(cr, uid, new_msg_values, email_from, email_to, context)
diff --git a/addons/mail/res_config_view.xml b/addons/mail/res_config_view.xml
index a17a1f22a80..c4bc23c9357 100644
--- a/addons/mail/res_config_view.xml
+++ b/addons/mail/res_config_view.xml
@@ -5,7 +5,6 @@
<field name="name">base.config.settings.mail.alias</field>
<field name="model">base.config.settings</field>
<field name="inherit_id" ref="base_setup.view_general_configuration"/>
- <field name="type">form</field>
<field name="arch" type="xml">
<xpath expr="/form/group[last()]/div[last()]/div[last()]" position='after' version="7.0">
<div>
diff --git a/addons/mail/res_partner.py b/addons/mail/res_partner.py
index 3dc1f91d857..d3a61b31a8a 100644
--- a/addons/mail/res_partner.py
+++ b/addons/mail/res_partner.py
@@ -20,20 +20,28 @@
##############################################################################
from osv import osv
-from osv import fields
-class res_partner_mail(osv.osv):
+class res_partner_mail(osv.Model):
""" Inherits partner and adds CRM information in the partner form """
_name = "res.partner"
_inherit = ['res.partner', 'mail.thread']
def message_search_get_domain(self, cr, uid, ids, context=None):
""" Override of message_search_get_domain for partner discussion page.
- The purpose is to add messages directly sent to the partner.
+ The purpose is to add messages directly sent to the partner. It also
+ adds messages pushed to the related user, if any, using @login.
"""
initial_domain = super(res_partner_mail, self).message_search_get_domain(cr, uid, ids, context=context)
- if self._name == 'res.partner': # to avoid models inheriting from res.partner
- search_domain = ['|'] + initial_domain + ['|', ('partner_id', 'in', ids), ('partner_ids', 'in', ids)]
+ # to avoid models inheriting from res.partner
+ if self._name != 'res.partner':
+ return initial_domain
+ # add message linked to the partner
+ search_domain = ['|'] + initial_domain + ['|', ('partner_id', 'in', ids), ('partner_ids', 'in', ids)]
+ # if partner is linked to a user: find @login
+ res_users_obj = self.pool.get('res.users')
+ user_ids = res_users_obj.search(cr, uid, [('partner_id', 'in', ids)], context=context)
+ for user in res_users_obj.browse(cr, uid, user_ids, context=context):
+ search_domain = ['|'] + search_domain + ['|', ('body_text', 'like', '@%s' % (user.login)), ('body_html', 'like', '@%s' % (user.login))]
return search_domain
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/mail/res_partner_view.xml b/addons/mail/res_partner_view.xml
index 8b4d89f89f4..2b8d38f2b99 100644
--- a/addons/mail/res_partner_view.xml
+++ b/addons/mail/res_partner_view.xml
@@ -5,7 +5,6 @@
<record id="view_emails_partner_info_form" model="ir.ui.view">
<field name="name">res.partner.emails.info.inherit</field>
<field name="model">res.partner</field>
- <field name="type">form</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="after">
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index 8a590fc318f..1c7f002f1fa 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -28,32 +28,32 @@ from tools.translate import _
_logger = logging.getLogger(__name__)
-class res_users(osv.osv):
+class res_users(osv.Model):
""" Update of res.users class
- add a preference about sending emails about notifications
- make a new user follow itself
- add a welcome message
"""
_name = 'res.users'
- _inherit = ['res.users', 'mail.thread']
+ _inherit = ['res.users']
_inherits = {'mail.alias': 'alias_id'}
-
+
_columns = {
'notification_email_pref': fields.selection([
- ('all', 'All feeds'),
- ('comments', 'Only comments'),
- ('to_me', 'Only when sent directly to me'),
+ ('all', 'All Feeds'),
+ ('to_me', 'Only send directly to me'),
('none', 'Never')
], 'Receive Feeds by Email', required=True,
help="Choose in which case you want to receive an email when you "\
- "receive new feeds."),
+ "receive new feeds."),
'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", required=True,
- help="Email address internally associated with this user. Incoming emails will appear "
- "in the user's notifications."),
+ help="Email address internally associated with this user. Incoming "\
+ "emails will appear in the user's notifications."),
}
_defaults = {
'notification_email_pref': 'to_me',
+ 'alias_domain': False, # always hide alias during creation
}
def __init__(self, pool, cr):
@@ -67,44 +67,62 @@ 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()
+ # create user that follows its related partner
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)
-
user = self.browse(cr, uid, user_id, context=context)
- # make user follow itself
- self.message_subscribe(cr, uid, [user_id], [user_id], context=context)
+ self.pool.get('res.partner').message_subscribe(cr, uid, [user.partner_id.id], [user_id], context=context)
+ # alias
+ mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context)
# create a welcome message
- company_name = user.company_id.name if user.company_id else _('the company')
- message = _('%s joined the %s network! Take a moment to welcome %s.') % (user.name, company_name, user.name)
- self.message_append_note(cr, uid, [user_id], body=message, type='comment', context=context)
+ self.create_welcome_message(cr, uid, user, context=context)
return user_id
-
+
+ def create_welcome_message(self, cr, uid, user, context=None):
+ company_name = user.company_id.name if user.company_id else _('the company')
+ subject = '''%s has joined %s.''' % (user.name, company_name)
+ body = '''Welcome to OpenERP !'''
+ # TODO change 1 into user.id but catch errors
+ return self.pool.get('res.partner').message_append_note(cr, 1, [user.partner_id.id],
+ subject=subject, body=body, type='comment', content_subtype='html', context=context)
+
def write(self, cr, uid, ids, vals, context=None):
# User alias is sync'ed with login
if vals.get('login'): vals['alias_name'] = vals['login']
@@ -117,28 +135,56 @@ class res_users(osv.osv):
res = super(res_users, self).unlink(cr, uid, ids, context=context)
alias_pool.unlink(cr, uid, alias_ids, context=context)
return res
-
- def message_search_get_domain(self, cr, uid, ids, context=None):
- """ Override of message_search_get_domain for partner discussion page.
- The purpose is to add messages directly sent to user using
- @user_login.
- """
- initial_domain = super(res_users, self).message_search_get_domain(cr, uid, ids, context=context)
- custom_domain = []
+
+ # --------------------------------------------------
+ # Wrappers on partner methods for Chatter
+ # #FIXME: another branch holds a refactoring of mail.thread
+ # that should help cleaning those wrappers
+ # --------------------------------------------------
+
+ def message_append(self, cr, uid, threads, subject, body_text=None, body_html=None,
+ type='email', email_date=None, parent_id=False,
+ content_subtype='plain', state=None,
+ partner_ids=None, email_from=False, email_to=False,
+ email_cc=None, email_bcc=None, reply_to=None,
+ headers=None, message_id=False, references=None,
+ attachments=None, original=None, context=None):
+ for user in self.browse(cr, uid, threads, context=context):
+ user.partner_id.message_append(subject, body_text, body_html, type, email_date, parent_id,
+ content_subtype, state, partner_ids, email_from, email_to, email_cc, email_bcc, reply_to,
+ headers, message_id, references, attachments, original)
+
+ def message_read(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
+ limit=100, offset=0, domain=None, context=None):
+ for user in self.browse(cr, uid, ids, context=context):
+ return user.partner_id.message_read(fetch_ancestors, ancestor_ids, limit, offset, domain)
+
+ def message_read_subscribers(self, cr, uid, ids, fields=['id', 'name', 'image_small'], context=None):
for user in self.browse(cr, uid, ids, context=context):
- if custom_domain:
- custom_domain += ['|']
- custom_domain += ['|', ('body_text', 'like', '@%s' % (user.login)), ('body_html', 'like', '@%s' % (user.login))]
- return ['|'] + initial_domain + custom_domain
+ return user.partner_id.message_read_subscribers(fields)
-class res_users_mail_group(osv.osv):
+ def message_search(self, cr, uid, ids, fetch_ancestors=False, ancestor_ids=None,
+ limit=100, offset=0, domain=None, count=False, context=None):
+ for user in self.browse(cr, uid, ids, context=context):
+ return user.partner_id.message_search(fetch_ancestors, ancestor_ids, limit, offset, domain, count)
+
+ def message_subscribe(self, cr, uid, ids, user_ids = None, context=None):
+ for user in self.browse(cr, uid, ids, context=context):
+ return user.partner_id.message_subscribe(user_ids)
+
+ def message_unsubscribe(self, cr, uid, ids, user_ids = None, context=None):
+ for user in self.browse(cr, uid, ids, context=context):
+ return user.partner_id.message_unsubscribe(user_ids)
+
+
+class res_users_mail_group(osv.Model):
""" Update of res.groups class
- if adding/removing users from a group, check mail.groups linked to
this user group, and subscribe / unsubscribe them from the discussion
group. This is done by overriding the write method.
"""
_name = 'res.users'
- _inherit = ['res.users', 'mail.thread']
+ _inherit = ['res.users']
def write(self, cr, uid, ids, vals, context=None):
write_res = super(res_users_mail_group, self).write(cr, uid, ids, vals, context=context)
@@ -150,9 +196,8 @@ class res_users_mail_group(osv.osv):
mail_group_ids = mail_group_obj.search(cr, uid, [('group_ids', 'in', user_group_ids)], context=context)
mail_group_obj.message_subscribe(cr, uid, mail_group_ids, ids, context=context)
return write_res
-
-class res_groups_mail_group(osv.osv):
+class res_groups_mail_group(osv.Model):
""" Update of res.groups class
- if adding/removing users from a group, check mail.groups linked to
this user group, and subscribe / unsubscribe them from the discussion
@@ -170,3 +215,5 @@ class res_groups_mail_group(osv.osv):
mail_group_ids = mail_group_obj.search(cr, uid, [('group_ids', 'in', ids)], context=context)
mail_group_obj.message_subscribe(cr, uid, mail_group_ids, user_ids, context=context)
return super(res_groups_mail_group, self).write(cr, uid, ids, vals, context=context)
+
+# vim:et:
diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml
index d1429f0afbc..005d71cb09f 100644
--- a/addons/mail/res_users_view.xml
+++ b/addons/mail/res_users_view.xml
@@ -9,7 +9,7 @@
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
<field name="arch" type="xml">
<data>
- <field name="user_email" position="before">
+ <field name="email" position="before">
<field name="notification_email_pref" readonly="0"/>
</field>
</data>
@@ -23,17 +23,12 @@
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<data>
- <field name="user_email" position="before">
+ <field name="email" position="before">
<field name="notification_email_pref"/>
</field>
- <xpath expr="/form/sheet" position="after">
- <div class="oe_chatter">
- <field name="message_ids" colspan="4" widget="mail_thread" nolabel="1"/>
- </div>
- </xpath>
- <field name="user_email" position="after">
+ <field name="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>
diff --git a/addons/mail/static/scripts/openerp_mailgate.py b/addons/mail/static/scripts/openerp_mailgate.py
index 64dafc56785..99ebc8f770b 100755
--- a/addons/mail/static/scripts/openerp_mailgate.py
+++ b/addons/mail/static/scripts/openerp_mailgate.py
@@ -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',
diff --git a/addons/mail/tests/__init__.py b/addons/mail/tests/__init__.py
new file mode 100644
index 00000000000..d63c5634cc3
--- /dev/null
+++ b/addons/mail/tests/__init__.py
@@ -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: \ No newline at end of file
diff --git a/addons/mail/tests/test_mail.py b/addons/mail/tests/test_mail.py
new file mode 100644
index 00000000000..0940d72c490
--- /dev/null
+++ b/addons/mail/tests/test_mail.py
@@ -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)
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py
index d26511464f8..fe87b7584e0 100644
--- a/addons/mail/wizard/mail_compose_message.py
+++ b/addons/mail/wizard/mail_compose_message.py
@@ -100,7 +100,7 @@ class mail_compose_message(osv.TransientModel):
# Try to provide default email_from if not specified yet
if not result.get('email_from'):
current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
- result['email_from'] = current_user.user_email or False
+ result['email_from'] = current_user.email or False
return result
_columns = {
@@ -133,7 +133,7 @@ class mail_compose_message(osv.TransientModel):
result.update({
'model': model,
'res_id': res_id,
- 'email_from': user.user_email or tools.config.get('email_from', False),
+ 'email_from': user.email or tools.config.get('email_from', False),
'body_html': False,
'body_text': False,
'subject': False,
@@ -223,7 +223,7 @@ class mail_compose_message(osv.TransientModel):
'dest_partner_ids': dest_partner_ids,
'model': message_data.model or False,
'res_id': message_data.res_id or False,
- 'email_from': current_user.user_email or message_data.email_to or False,
+ 'email_from': current_user.email or message_data.email_to or False,
'email_to': message_data.reply_to or message_data.email_from or False,
'email_cc': message_data.email_cc or False,
'user_id': uid,
diff --git a/addons/mail/wizard/mail_compose_message_view.xml b/addons/mail/wizard/mail_compose_message_view.xml
index 8fa4eaa9c50..73fb03cdc28 100644
--- a/addons/mail/wizard/mail_compose_message_view.xml
+++ b/addons/mail/wizard/mail_compose_message_view.xml
@@ -4,7 +4,6 @@
<record model="ir.ui.view" id="email_compose_message_wizard_form">
<field name="name">mail.compose.message.form</field>
<field name="model">mail.compose.message</field>
- <field name="type">form</field>
<field name="arch" type="xml">
<form string="Compose Email" version="7.0">
<group>
@@ -41,7 +40,6 @@
<field name="name">mail.compose.message.form</field>
<field name="model">mail.compose.message</field>
<field name="priority">18</field>
- <field name="type">form</field>
<field name="arch" type="xml">
<form string="Compose Email" version="7.0" >
<group>
@@ -50,7 +48,7 @@
<field name="body_text" colspan="2" nolabel="1" placeholder="What are you working on ?"
class="oe_mail_compose_message_body_text"/>
<field name="body_html" colspan="2" nolabel="1" placeholder="What are you working on HTML ?"
- class="oe_mail_compose_message_body_html oe_mail_compose_message_invisible" widget="text_html"/>
+ class="oe_mail_compose_message_body_html oe_mail_compose_message_invisible"/>
<field name="dest_partner_ids" colspan="2" nolabel="1" widget="many2many_tags"
placeholder="Add contacts to notify..."
class="oe_mail_compose_message_partner_ids oe_mail_compose_message_invisible"/>