diff options
| author | Thibault Delavallée <tde@openerp.com> | 2012-08-15 21:56:52 +0200 |
|---|---|---|
| committer | Thibault Delavallée <tde@openerp.com> | 2012-08-15 21:56:52 +0200 |
| commit | 61274acd896f72e6bec98deca8faa93d9895b118 (patch) | |
| tree | cd839020bde7def97753e5dd890d1012dde456b7 /addons/mail | |
| parent | a6482fe07c57805976529eebd48225b060a9a49c (diff) | |
| parent | 9958acdc3286f761b6ce76142598d628736a5540 (diff) | |
[MERGE] Merged with addons/trunk.
bzr revid: tde@openerp.com-20120815195652-839w3fyumomq0ubf
Diffstat (limited to 'addons/mail')
| -rw-r--r-- | addons/mail/__init__.py | 2 | ||||
| -rw-r--r-- | addons/mail/data/mail_group_data.xml | 2 | ||||
| -rw-r--r-- | addons/mail/doc/mail_openchatter_howto.rst | 1 | ||||
| -rw-r--r-- | addons/mail/mail_alias.py | 27 | ||||
| -rw-r--r-- | addons/mail/mail_alias_view.xml | 17 | ||||
| -rw-r--r-- | addons/mail/mail_followers_view.xml | 1 | ||||
| -rw-r--r-- | addons/mail/mail_group.py | 19 | ||||
| -rw-r--r-- | addons/mail/mail_group_view.xml | 9 | ||||
| -rw-r--r-- | addons/mail/mail_message.py | 13 | ||||
| -rw-r--r-- | addons/mail/mail_message_view.xml | 11 | ||||
| -rw-r--r-- | addons/mail/mail_thread.py | 275 | ||||
| -rw-r--r-- | addons/mail/res_config_view.xml | 1 | ||||
| -rw-r--r-- | addons/mail/res_partner.py | 18 | ||||
| -rw-r--r-- | addons/mail/res_partner_view.xml | 1 | ||||
| -rw-r--r-- | addons/mail/res_users.py | 131 | ||||
| -rw-r--r-- | addons/mail/res_users_view.xml | 13 | ||||
| -rwxr-xr-x | addons/mail/static/scripts/openerp_mailgate.py | 37 | ||||
| -rw-r--r-- | addons/mail/tests/__init__.py | 27 | ||||
| -rw-r--r-- | addons/mail/tests/test_mail.py | 112 | ||||
| -rw-r--r-- | addons/mail/wizard/mail_compose_message.py | 6 | ||||
| -rw-r--r-- | addons/mail/wizard/mail_compose_message_view.xml | 4 |
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 <i>Settings</i> 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 <i>Settings</i> 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"/> |
