OpenERP allows to automatically create leads (or others documents)
- from incoming emails. You can synchronize emails with OpenERP
- by connecting the mail gateway to your mail server or by manually
- pressing buttons in our mail clients.
+ from incoming emails. You can automatically synchronize emails with OpenERP
+ using regular POP/IMAP accounts, using a direct email integration script for your
+ email server, or by manually pushing emails to OpenERP using specific
+ plugins for your preferred email application.
diff --git a/addons/fetchmail/fetchmail.py b/addons/fetchmail/fetchmail.py
index 62b2775a056..544a4f81b96 100644
--- a/addons/fetchmail/fetchmail.py
+++ b/addons/fetchmail/fetchmail.py
@@ -189,14 +189,11 @@ openerp_mailgate.py -u %(uid)d -p PASSWORD -o %(model)s -d %(dbname)s --host=HOS
result, data = imap_server.search(None, '(UNSEEN)')
for num in data[0].split():
result, data = imap_server.fetch(num, '(RFC822)')
- if server.object_id:
- res_id = mail_thread.message_process(cr, uid, server.object_id.model,
- data[0][1],
- save_original=server.original,
- strip_attachments=(not server.attach),
- context=context)
- else:
- res_id = mail_thread.message_catchall(cr, uid, data[0][1])
+ res_id = mail_thread.message_process(cr, uid, server.object_id.model,
+ data[0][1],
+ save_original=server.original,
+ strip_attachments=(not server.attach),
+ context=context)
if res_id and server.action_id:
action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id]})
imap_server.store(num, '+FLAGS', '\\Seen')
@@ -217,14 +214,11 @@ openerp_mailgate.py -u %(uid)d -p PASSWORD -o %(model)s -d %(dbname)s --host=HOS
for num in range(1, numMsgs + 1):
(header, msges, octets) = pop_server.retr(num)
msg = '\n'.join(msges)
- if server.object_id:
- res_id = mail_thread.message_process(cr, uid, server.object_id.model,
- msg,
- save_original=server.original,
- strip_attachments=(not server.attach),
- context=context)
- else:
- res_id = mail_thread.message_catchall(cr, uid, data[0][1])
+ res_id = mail_thread.message_process(cr, uid, server.object_id.model,
+ msg,
+ save_original=server.original,
+ strip_attachments=(not server.attach),
+ context=context)
if res_id and server.action_id:
action_pool.run(cr, uid, [server.action_id.id], {'active_id': res_id, 'active_ids':[res_id]})
pop_server.dele(num)
diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py
index 1db79172dc5..89a81a34c99 100644
--- a/addons/hr_recruitment/hr_recruitment.py
+++ b/addons/hr_recruitment/hr_recruitment.py
@@ -518,37 +518,55 @@ class hr_job(osv.osv):
"create new applicants for this job position."),
}
- def init(self, cr):
- # Installation hook to create aliases for all jobs, right after _auto_init
+ _defaults = {
+ 'alias_domain': False, # always hide alias during creation
+ }
+
+ def _auto_init(self, cr, context=None):
+ """Installation hook to create aliases for all jobs and avoid constraint errors."""
+
+ # disable the unique alias_id not null constraint, to avoid spurious warning during
+ # super.auto_init. We'll reinstall it afterwards.
+ self._columns['alias_id'].required = False
+
+ super(hr_job,self)._auto_init(cr, context=context)
+
registry = RegistryManager.get(cr.dbname)
mail_alias = registry.get('mail.alias')
- hr_job = registry.get('hr.job')
- jobs_no_alias = hr_job.search(cr, SUPERUSER_ID, [('alias_id', '=', False)])
+ hr_jobs = registry.get('hr.job')
+ jobs_no_alias = hr_jobs.search(cr, SUPERUSER_ID, [('alias_id', '=', False)])
# Use read() not browse(), to avoid prefetching uninitialized inherited fields
- for job_data in hr_job.read(cr, SUPERUSER_ID, jobs_no_alias, ['name']):
- alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, {'alias_name': 'job_'+job_data['name'],
- 'alias_force_id': job_data['id']},
- model_name=self._name)
- hr_job.write(cr, SUPERUSER_ID, job_data['id'], {'alias_id': alias_id})
+ for job_data in hr_jobs.read(cr, SUPERUSER_ID, jobs_no_alias, ['name']):
+ alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, {'alias_name': 'job+'+job_data['name'],
+ 'alias_defaults': {'job_id': job_data['id']}},
+ model_name='hr.applicant')
+ hr_jobs.write(cr, SUPERUSER_ID, job_data['id'], {'alias_id': alias_id})
_logger.info('Mail alias created for hr.job %s (uid %s)', job_data['name'], job_data['id'])
# Finally attempt to reinstate the missing constraint
try:
cr.execute('ALTER TABLE hr_job ALTER COLUMN alias_id SET NOT NULL')
except Exception:
- pass
-
+ _logger.warning("Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\
+ "If you want to have it, you should update the records and execute manually:\n"\
+ "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL",
+ self._table, 'alias_id', self._table, 'alias_id')
+
+ self._columns['alias_id'].required = True
+
def create(self, cr, uid, vals, context=None):
- alias_pool = self.pool.get('mail.alias')
+ mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
- name = vals.pop('alias_name', None) or vals['name']
- alias_id = alias_pool.create_unique_alias(cr, uid,
- {'alias_name': "job_"+name},
+ vals.pop('alias_name', None) # prevent errors during copy()
+ alias_id = mail_alias.create_unique_alias(cr, uid,
+ # Using '+' allows using subaddressing for those who don't
+ # have a catchall domain setup.
+ {'alias_name': 'jobs+'+vals['name']},
model_name="hr.applicant",
context=context)
vals['alias_id'] = alias_id
- res = super( hr_job, self).create(cr, uid, vals, context)
- alias_pool.write(cr, uid, [vals['alias_id']], {"alias_defaults": {'job_id': res}}, context)
+ res = super(hr_job, self).create(cr, uid, vals, context)
+ mail_alias.write(cr, uid, [vals['alias_id']], {"alias_defaults": {'job_id': res}}, context)
return res
def unlink(self, cr, uid, ids, context=None):
diff --git a/addons/hr_recruitment/hr_recruitment_data.xml b/addons/hr_recruitment/hr_recruitment_data.xml
index 21f4e48e42d..d90f5ab596a 100644
--- a/addons/hr_recruitment/hr_recruitment_data.xml
+++ b/addons/hr_recruitment/hr_recruitment_data.xml
@@ -458,7 +458,6 @@ You can automatically create application records from an email gateway, that you
jobs
- {}
diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml
index 3f6b1135cec..8a6296cd230 100644
--- a/addons/hr_recruitment/hr_recruitment_view.xml
+++ b/addons/hr_recruitment/hr_recruitment_view.xml
@@ -334,7 +334,7 @@
-
+ @
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 c129040c8c7..e0d8e173ebc 100644
--- a/addons/mail/mail_alias_view.xml
+++ b/addons/mail/mail_alias_view.xml
@@ -10,14 +10,12 @@
diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py
index 79416fb26a9..61bd21439a1 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.osv):
"""
@@ -78,7 +76,6 @@ class mail_group(osv.osv):
def get_last_month_msg_nbr(self, cr, uid, ids, name, args, context=None):
result = {}
- message_obj = self.pool.get('mail.message')
for id in ids:
lower_date = (DT.datetime.now() - DT.timedelta(days=30)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
result[id] = self.message_search(cr, uid, [id], limit=None, domain=[('date', '>=', lower_date)], count=True, context=context)
@@ -153,6 +150,7 @@ class mail_group(osv.osv):
'responsible_id': (lambda s, cr, uid, ctx: uid),
'image': _get_default_image,
'parent_id': _get_menu_parent,
+ 'alias_domain': False, # always hide alias during creation
}
def _subscribe_user_with_group_m2m_command(self, cr, uid, ids, group_ids_command, context=None):
@@ -168,12 +166,14 @@ class mail_group(osv.osv):
return self.message_subscribe(cr, uid, ids, user_ids, context=context)
def create(self, cr, uid, vals, context=None):
- alias_pool = self.pool.get('mail.alias')
+ mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
- name = vals.get('alias_name') or vals['name']
- alias_id = alias_pool.create_unique_alias(cr, uid,
- {'alias_name': "group_"+name},
- model_name=self._name, context=context)
+ vals.pop('alias_name', None) # prevent errors during copy()
+ alias_id = mail_alias.create_unique_alias(cr, uid,
+ # Using '+' allows using subaddressing for those who don't
+ # have a catchall domain setup.
+ {'alias_name': "group+"+vals['name']},
+ model_name=self._name, context=context)
vals['alias_id'] = alias_id
mail_group_id = super(mail_group, self).create(cr, uid, vals, context)
@@ -193,7 +193,8 @@ class mail_group(osv.osv):
newref = cobj.copy(cr, uid, ref[1], default={'params': str(params), 'name': vals['name']}, context=context)
self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,'+str(newref), 'mail_group_id': mail_group_id}, context=context)
- alias_pool.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context)
+ mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context)
+
if vals.get('group_ids'):
self._subscribe_user_with_group_m2m_command(cr, uid, [mail_group_id], vals.get('group_ids'), context=context)
diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml
index dd770836eab..7df68bf399e 100644
--- a/addons/mail/mail_group_view.xml
+++ b/addons/mail/mail_group_view.xml
@@ -60,6 +60,11 @@
+
+
+
+ @
+
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index af50bed3aa0..d03dd8252d7 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -21,19 +21,26 @@
import base64
import email
-from email.utils import parsedate
import logging
-from mail_message import decode, to_email
-from operator import itemgetter
-from osv import osv, fields
import re
import time
+import xmlrpclib
+from email.utils import parsedate
+from email.message import Message
+
+from osv import osv, fields
+from mail_message import decode, to_email
import tools
from tools.translate import _
-import xmlrpclib
+from tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
+
+def decode_header(message, header, separator=' '):
+ return separator.join(map(decode,message.get_all(header, [])))
+
+
class mail_thread(osv.Model):
'''Mixin model, meant to be inherited by any model that needs to
act as a discussion topic on which messages can be attached.
@@ -151,10 +158,9 @@ class mail_thread(osv.Model):
message_obj = self.pool.get('mail.message')
notification_obj = self.pool.get('mail.notification')
- body = vals.get('body_html', '') if vals.get('content_subtype') == 'html' else vals.get('body_text', '')
# automatically subscribe the writer of the message
- if vals['user_id']:
+ if vals.get('user_id'):
self.message_subscribe(cr, uid, [thread_id], [vals['user_id']], context=context)
# create message
@@ -173,12 +179,11 @@ class mail_thread(osv.Model):
notification_obj.create(cr, uid, {'user_id': id, 'message_id': msg_id}, context=context)
# create the email to send
- email_id = self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context)
+ self.message_create_notify_by_email(cr, uid, vals, user_to_push_ids, context=context)
return msg_id
def message_get_user_ids_to_notify(self, cr, uid, thread_ids, new_msg_vals, context=None):
- subscription_obj = self.pool.get('mail.subscription')
# get body
body = new_msg_vals.get('body_html', '') if new_msg_vals.get('content_subtype') == 'html' else new_msg_vals.get('body_text', '')
@@ -301,7 +306,6 @@ class mail_thread(osv.Model):
threads = model_pool.browse(cr, uid, threads, context=context)
ir_attachment = self.pool.get('ir.attachment')
- mail_message = self.pool.get('mail.message')
new_msg_ids = []
for thread in threads:
@@ -501,10 +505,6 @@ class mail_thread(osv.Model):
""" Retrieve all attachments names """
map_id_to_name = dict((attachment_id, '') for message in messages for attachment_id in message['attachment_ids'])
- map_id_to_name = {}
- for msg in messages:
- for attach_id in msg["attachment_ids"]:
- map_id_to_name[attach_id] = '' # use empty string as a placeholder
ids = map_id_to_name.keys()
names = self.pool.get('ir.attachment').name_get(cr, uid, ids, context=context)
@@ -560,79 +560,127 @@ class mail_thread(osv.Model):
msgs = msg_obj.read(cr, uid, msg_ids, context=context)
return msgs
-
- def _get_user(self, cr, uid, alias, context):
- """
- param alias: browse record of alias.
- return: int user_id.
- """
-
- user_obj = self.pool.get('res.user')
- user_id = 1
- if alias.alias_user_id:
- user_id = alias.alias_user_id.id
- #if user_id not defined in the alias then search related user using name of Email sender
- else:
- from_email = msg.get('from')
- user_ids = user_obj.search(cr, uid, [('name','=',from_email)], context)
- if user_ids:
- user_id = user_obj.browse(cr, uid, user_ids[0], context).id
- return user_id
-
- def message_catchall(self, cr, uid, message, context=None):
- """
- Process incoming mail and call messsage_process using details of the mail.alias model
- else raise Exception so that mailgate script will reject the mail and
- send notification mail sender that this mailbox does not exist so your mail have been rejected.
- """
- mail_alias = self.pool.get('mail.alias')
- mail_message = self.pool.get('mail.message')
- if isinstance(message, xmlrpclib.Binary):
- message = str(message.data)
- if isinstance(message, unicode):
- message = message.encode('utf-8')
- msg_txt = email.message_from_string(message)
- msg = mail_message.parse_message(msg_txt)
- alias_name = msg.get('to').split("@")[0] # @@@@
- alias_ids = mail_alias.search(cr, uid, [('alias_name','=',alias_name)])
- #if alias found then call message_process method. # @@@@
- if alias_ids:
- alias_id = mail_alias.browse(cr, uid, alias_ids[0], context)
- user_id = self._get_user( cr, uid, alias_id, context)
- alias_defaults = dict(eval(alias_id.alias_defaults or {}))
- self.message_process(cr, user_id, alias_id.alias_model_id.model, message,
- custom_values=alias_defaults,
- thread_id=alias_id.alias_force_thread_id or False,
- context=context)
- else:
- #if Mail box for the intended Mail Alias then give logger warning
- _logger.warning("No Mail Alias Found for the name '%s'."%(alias_name))
- raise # @@@@
- return True
+ def _message_find_user_id(self, cr, uid, message, context=None):
+ from_local_part = to_email(decode(message.get('From')))[0]
+ user_ids = self.pool.get('res.users').search(cr, uid, [('login', '=', from_local_part)], context=context)
+ return user_ids[0] if user_ids else uid
#------------------------------------------------------
# Mail gateway
#------------------------------------------------------
# message_process will call either message_new or message_update.
+ def message_route(self, cr, uid, message, model=None, thread_id=None,
+ custom_values=None, context=None):
+ """Attempt to figure out the correct target model, thread_id,
+ custom_values and user_id to use for an incoming message.
+ Multiple values may be returned, if a message had multiple
+ recipients matching existing mail.aliases, for example.
+
+ The following heuristics are used, in this order:
+ 1. If the message replies to an existing thread_id, and
+ properly contains the thread model in the 'In-Reply-To'
+ header, use this model/thread_id pair, and ignore
+ custom_value (not needed as no creation will take place)
+ 2. Look for a mail.alias entry matching the message
+ recipient, and use the corresponding model, thread_id,
+ custom_values and user_id.
+ 3. Fallback to the ``model``, ``thread_id`` and ``custom_values``
+ provided.
+ 4. If all the above fails, raise an exception.
+
+ :param string message: an email.message instance
+ :param string model: the fallback model to use if the message
+ does not match any of the currently configured mail aliases
+ (may be None if a matching alias is supposed to be present)
+ :type dict custom_values: optional dictionary of default field values
+ to pass to ``message_new`` if a new record needs to be created.
+ Ignored if the thread record already exists, and also if a
+ matching mail.alias was found (aliases define their own defaults)
+ :param int thread_id: optional ID of the record/thread from ``model``
+ to which this mail should be attached. Only used if the message
+ does not reply to an existing thread and does not match any mail alias.
+ :return: list of [model, thread_id, custom_values, user_id]
+ """
+ assert isinstance(message, Message), 'message must be an email.message.Message at this point'
+ message_id = message.get('Message-Id')
+
+ # 1. Verify if this is a reply to an existing thread
+ references = decode_header(message, 'References') or decode_header(message, 'In-Reply-To')
+ ref_match = references and tools.reference_re.search(references)
+ if ref_match:
+ thread_id = int(ref_match.group(1))
+ model = ref_match.group(2) or model
+ model_pool = self.pool.get(model)
+ if thread_id and model and model_pool and model_pool.exists(cr, uid, thread_id) \
+ and hasattr(model_pool, 'message_update'):
+ _logger.debug('Routing mail with Message-Id %s: direct reply to model: %s, thread_id: %s, custom_values: %s, uid: %s',
+ message_id, model, thread_id, custom_values, uid)
+ return [(model, thread_id, custom_values, uid)]
+
+ # 2. Look for a matching mail.alias entry
+ # Delivered-To is a safe bet in most modern MTAs, but we have to fallback on To + Cc values
+ # for all the odd MTAs out there, as there is no standard header for the envelope's `rcpt_to` value.
+ rcpt_tos = decode_header(message, 'Delivered-To') or \
+ ','.join([decode_header(message, 'To'),
+ decode_header(message, 'Cc'),
+ decode_header(message, 'Resent-To'),
+ decode_header(message, 'Resent-Cc')])
+ local_parts = [e.split('@')[0] for e in to_email(rcpt_tos)]
+ if local_parts:
+ mail_alias = self.pool.get('mail.alias')
+ alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)])
+ if alias_ids:
+ routes = []
+ for alias in mail_alias.browse(cr, uid, alias_ids, context=context):
+ user_id = alias.alias_user_id.id
+ if not user_id:
+ user_id = self._message_find_user_id(cr, uid, message, context=context)
+ routes.append((alias.alias_model_id.model, alias.alias_force_thread_id, \
+ eval(alias.alias_defaults), user_id))
+ _logger.debug('Routing mail with Message-Id %s: direct alias match: %r', message_id, routes)
+ return routes
+
+ # 3. Fallback to the provided parameters, if they work
+ model_pool = self.pool.get(model)
+ if not thread_id:
+ # Legacy: fallback to matching [ID] in the Subject
+ match = tools.res_re.search(decode_header(message, 'Subject'))
+ thread_id = match and match.group(1)
+ assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
+ "No possible route found for incoming message with Message-Id %s. " \
+ "Create an appropriate mail.alias or force the destination model." % message_id
+ if thread_id and not model_pool.exists(cr, uid, thread_id):
+ _logger.warning('Received mail reply to missing document %s! Ignoring and creating new document instead for Message-Id %s',
+ thread_id, message_id)
+ thread_id = None
+ _logger.debug('Routing mail with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
+ message_id, model, thread_id, custom_values, uid)
+ return [(model, thread_id, custom_values, uid)]
+
+
def message_process(self, cr, uid, model, message, custom_values=None,
save_original=False, strip_attachments=False,
thread_id=None, context=None):
- """Process an incoming RFC2822 email message related to the
- given thread model, relying on ``mail.message.parse()``
- for the parsing operation, and then calling ``message_new``
- (if the thread record did not exist) or ``message_update``
- (if it did), then calling ``message_forward`` to automatically
- notify other people that should receive this message.
+ """Process an incoming RFC2822 email message, relying on
+ ``mail.message.parse()`` for the parsing operation,
+ and ``message_route()`` to figure out the target model.
+
+ Once the target model is known, its ``message_new`` method
+ is called with the new message (if the thread record did not exist)
+ or its ``message_update`` method (if it did). Finally,
+ ``message_forward`` is called to automatically notify other
+ people that should receive this message.
- :param string model: the thread model for which a new message
- must be processed
- :param message: source of the RFC2822 mail
+ :param string model: the fallback model to use if the message
+ does not match any of the currently configured mail aliases
+ (may be None if a matching alias is supposed to be present)
+ :param message: source of the RFC2822 message
:type message: string or xmlrpclib.Binary
:type dict custom_values: optional dictionary of field values
- to pass to ``message_new`` if a new
- record needs to be created. Ignored
- if the thread record already exists.
+ to pass to ``message_new`` if a new record needs to be created.
+ Ignored if the thread record already exists, and also if a
+ matching mail.alias was found (aliases define their own defaults)
:param bool save_original: whether to keep a copy of the original
email source attached to the message after it is imported.
:param bool strip_attachments: whether to strip all attachments
@@ -642,66 +690,41 @@ class mail_thread(osv.Model):
overrides the automatic detection based on the message
headers.
"""
+ if context is None: context = {}
+
# extract message bytes - we are forced to pass the message as binary because
# we don't know its encoding until we parse its headers and hence can't
# convert it to utf-8 for transport between the mailgate script and here.
if isinstance(message, xmlrpclib.Binary):
message = str(message.data)
-
- if context is None: context = {}
-
- mail_message = self.pool.get('mail.message')
- model_pool = self.pool.get(model)
- if self._name != model:
- context.update({'thread_model': model})
-
- # Parse Message
# Warning: message_from_string doesn't always work correctly on unicode,
# we must use utf-8 strings here :-(
if isinstance(message, unicode):
message = message.encode('utf-8')
msg_txt = email.message_from_string(message)
- msg = mail_message.parse_message(msg_txt, save_original=save_original, context=context)
-
- # update state
- msg['state'] = 'received'
-
+ routes = self.message_route(cr, uid, msg_txt, model,
+ thread_id, custom_values,
+ context=context)
+ msg = self.pool.get('mail.message').parse_message(msg_txt, save_original=save_original, context=context)
+ msg['state'] = 'received'
if strip_attachments and 'attachments' in msg:
del msg['attachments']
-
- # Create New Record into particular model
- def create_record(msg):
- if hasattr(model_pool, 'message_new'):
- return model_pool.message_new(cr, uid, msg,
- custom_values,
- context=context)
- if not thread_id and (msg.get('references') or msg.get('in-reply-to')):
- references = msg.get('references') or msg.get('in-reply-to')
- if '\r\n' in references:
- references = references.split('\r\n')
+ for model, thread_id, custom_values, user_id in routes:
+ if self._name != model:
+ context.update({'thread_model': model})
+ model_pool = self.pool.get(model)
+ assert thread_id and hasattr(model_pool, 'message_update') or hasattr(model_pool, 'message_new'), \
+ "Undeliverable mail with Message-Id %s, model %s does not accept incoming emails" % \
+ (msg['message-id'], model)
+ if thread_id and hasattr(model_pool, 'message_update'):
+ model_pool.message_update(cr, user_id, [thread_id], msg, context=context)
else:
- references = references.split(' ')
- for ref in references:
- ref = ref.strip()
- thread_id = tools.reference_re.search(ref)
- if not thread_id:
- thread_id = tools.res_re.search(msg['subject'])
- if thread_id:
- thread_id = int(thread_id.group(1))
- if not model_pool.exists(cr, uid, thread_id) or \
- not hasattr(model_pool, 'message_update'):
- # referenced thread not found or not updatable,
- # -> create a new one
- thread_id = False
- if not thread_id:
- thread_id = create_record(msg)
- else:
- model_pool.message_update(cr, uid, [thread_id], msg, {}, context=context)
- # To forward the email to other followers
- self.message_forward(cr, uid, model, [thread_id], msg_txt, context=context)
- # Set as Unread
- model_pool.message_mark_as_unread(cr, uid, [thread_id], context=context)
- return thread_id
+ thread_id = model_pool.message_new(cr, user_id, msg, custom_values, context=context)
+
+ # Forward the email to other followers
+ self.message_forward(cr, uid, model, [thread_id], msg_txt, context=context)
+ model_pool.message_mark_as_unread(cr, uid, [thread_id], context=context)
+ return True
def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
"""Called by ``message_process`` when a new message is received
@@ -735,7 +758,7 @@ class mail_thread(osv.Model):
fields = model_pool.fields_get(cr, uid, context=context)
data = model_pool.default_get(cr, uid, fields, context=context)
if 'name' in fields and not data.get('name'):
- data['name'] = msg_dict.get('from', '')
+ data['name'] = msg_dict.get('subject', '')
if custom_values and isinstance(custom_values, dict):
data.update(custom_values)
res_id = model_pool.create(cr, uid, data, context=context)
@@ -791,7 +814,6 @@ class mail_thread(osv.Model):
"""
model_pool = self.pool.get(model)
smtp_server_obj = self.pool.get('ir.mail_server')
- mail_message = self.pool.get('mail.message')
for res in model_pool.browse(cr, uid, thread_ids, context=context):
if hasattr(model_pool, 'message_thread_followers'):
followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py
index 8a590fc318f..a2ca7207aa4 100644
--- a/addons/mail/res_users.py
+++ b/addons/mail/res_users.py
@@ -54,6 +54,7 @@ class res_users(osv.osv):
_defaults = {
'notification_email_pref': 'to_me',
+ 'alias_domain': False, # always hide alias during creation
}
def __init__(self, pool, cr):
@@ -67,32 +68,45 @@ class res_users(osv.osv):
self.SELF_WRITEABLE_FIELDS.append('notification_email_pref')
return init_res
- def init(self, cr):
- # Installation hook to create aliases for all users, right after _auto_init
+ def _auto_init(self, cr, context=None):
+ """Installation hook to create aliases for all users and avoid constraint errors."""
+
+ # disable the unique alias_id not null constraint, to avoid spurious warning during
+ # super.auto_init. We'll reinstall it afterwards.
+ self._columns['alias_id'].required = False
+
+ super(res_users,self)._auto_init(cr, context=context)
+
registry = RegistryManager.get(cr.dbname)
mail_alias = registry.get('mail.alias')
- res_users = registry.get('res.users')
- users_no_alias = res_users.search(cr, SUPERUSER_ID, [('alias_id', '=', False)])
+ res_users_model = registry.get('res.users')
+ users_no_alias = res_users_model.search(cr, SUPERUSER_ID, [('alias_id', '=', False)])
# Use read() not browse(), to avoid prefetching uninitialized inherited fields
- for user_data in res_users.read(cr, SUPERUSER_ID, users_no_alias, ['login']):
+ for user_data in res_users_model.read(cr, SUPERUSER_ID, users_no_alias, ['login']):
alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, {'alias_name': user_data['login'],
'alias_force_id': user_data['id']},
model_name=self._name)
- res_users.write(cr, SUPERUSER_ID, user_data['id'], {'alias_id': alias_id})
+ res_users_model.write(cr, SUPERUSER_ID, user_data['id'], {'alias_id': alias_id})
_logger.info('Mail alias created for user %s (uid %s)', user_data['login'], user_data['id'])
# Finally attempt to reinstate the missing constraint
try:
cr.execute('ALTER TABLE res_users ALTER COLUMN alias_id SET NOT NULL')
except Exception:
- pass
-
-
+ _logger.warning("Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\
+ "If you want to have it, you should update the records and execute manually:\n"\
+ "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL",
+ self._table, 'alias_id', self._table, 'alias_id')
+
+ self._columns['alias_id'].required = True
+
+
def create(self, cr, uid, data, context=None):
# create default alias same as the login
mail_alias = self.pool.get('mail.alias')
alias_id = mail_alias.create_unique_alias(cr, uid, {'alias_name': data['login']}, model_name=self._name, context=context)
data['alias_id'] = alias_id
+ data.pop('alias_name', None) # prevent errors during copy()
user_id = super(res_users, self).create(cr, uid, data, context=context)
mail_alias.write(cr, SUPERUSER_ID, [alias_id], {"alias_force_thread_id": user_id}, context)
diff --git a/addons/mail/res_users_view.xml b/addons/mail/res_users_view.xml
index d1429f0afbc..29fcea14a8a 100644
--- a/addons/mail/res_users_view.xml
+++ b/addons/mail/res_users_view.xml
@@ -33,7 +33,7 @@
-
+
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.
+#
+# 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 .
+#
+##############################################################################
+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.
+#
+# 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 .
+#
+##############################################################################
+
+from openerp.tests import common
+
+MAIL_TEMPLATE = """Return-Path:
+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
+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
+
+
+
+ =20
+
+ =20
+ =20
+
+
Please call me as soon as possible this afternoon!
+
+
--
+ Sylvie
+
+
+
+------=_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 ', 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/project/project.py b/addons/project/project.py
index 6f3b23f4746..7b848af06a3 100644
--- a/addons/project/project.py
+++ b/addons/project/project.py
@@ -267,6 +267,7 @@ class project(osv.osv):
'type_ids': _get_type_common,
'alias_model': 'project.task',
'privacy_visibility': 'public',
+ 'alias_domain': False, # always hide alias during creation
}
# TODO: Why not using a SQL contraints ?
@@ -337,11 +338,11 @@ class project(osv.osv):
context['active_test'] = False
default['state'] = 'open'
default['tasks'] = []
- default['alias_id'] = False
+ default.pop('alias_name', None)
+ default.pop('alias_id', None)
proj = self.browse(cr, uid, id, context=context)
if not default.get('name', False):
default['name'] = proj.name + _(' (copy)')
- default['alias_name'] = default['name']
res = super(project, self).copy(cr, uid, id, default, context)
self.map_tasks(cr,uid,id,res,context)
return res
@@ -527,11 +528,13 @@ def Project():
context = dict(context, project_creation_in_progress=True)
mail_alias = self.pool.get('mail.alias')
if not vals.get('alias_id'):
- name = vals.pop('alias_name', None) or vals['name']
- alias_id = mail_alias.create_unique_alias(cr, uid,
- {'alias_name': "project_"+short_name(name)},
- model_name=vals.get('alias_model', 'project.task'),
- context=context)
+ vals.pop('alias_name', None) # prevent errors during copy()
+ alias_id = mail_alias.create_unique_alias(cr, uid,
+ # Using '+' allows using subaddressing for those who don't
+ # have a catchall domain setup.
+ {'alias_name': "project+"+short_name(vals['name'])},
+ model_name=vals.get('alias_model', 'project.task'),
+ context=context)
vals['alias_id'] = alias_id
project_id = super(project, self).create(cr, uid, vals, context)
mail_alias.write(cr, uid, [vals['alias_id']], {'alias_defaults': {'project_id': project_id} }, context)
diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml
index ef88f2eef15..7d78a040b7e 100644
--- a/addons/project/project_view.xml
+++ b/addons/project/project_view.xml
@@ -81,7 +81,7 @@