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 f0e6a5179b4..8cd9e573862 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_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/mail/mail_alias.py b/addons/mail/mail_alias.py
index 807ed5dbe61..430cdb930b4 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..0034d241987 100644
--- a/addons/mail/mail_alias_view.xml
+++ b/addons/mail/mail_alias_view.xml
@@ -11,14 +11,12 @@
diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py
index fe8881d4de9..2fca7bfae2e 100644
--- a/addons/mail/mail_thread.py
+++ b/addons/mail/mail_thread.py
@@ -560,79 +560,109 @@ 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.
+
+ 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: model, thread_id, custom_values, user_id
+ """
+ assert isinstance(message, email.Message), 'message must be an email.Message at this point'
+
+ # 1. Verify if this is a reply to an existing thread
+ references = message.get('References') or message.get('In-Reply-To')
+ ref_match = 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'):
+ 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_to = message.get_all('Delivered-To', []) or (message.get_all('To', []) + message.get_all('Cc', []))
+ local_parts = [e.split('@')[0] for e in to_email(u','.join(decode(rcpt_to)))]
+ if local_parts:
+ mail_alias = self.pool.get('mail.alias')
+ alias_ids = mail_alias.search(cr, uid, [('alias_name', 'in', local_parts)])
+ if len(alias_ids) > 1:
+ _logger.warning('Multiple mail.aliases match for mail with Message-Id %s, keeping first one only: %s',
+ message.get('Message-Id'), alias_ids)
+ alias = mail_alias.browse(cr, uid, alias_ids[0], 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)
+ return alias.alias_model_id.model, alias.alias_model_id.alias_force_thread_id, \
+ alias.alias_defaults, user_id
+
+ # 3. Fallback to the provided parameters, if they work
+ model_pool = self.pool.get(model)
+ 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."
+ 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,64 +672,39 @@ 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'
-
+ model, thread_id, custom_values, user_id = self.message_route(cr, uid, msg_txt, model,
+ thread_id, custom_values,
+ context=context)
+ if self._name != model:
+ context.update({'thread_model': model})
+ 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')
- 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)
+ 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:
- model_pool.message_update(cr, uid, [thread_id], msg, {}, context=context)
- # To forward the email to other followers
+ 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)
- # Set as Unread
model_pool.message_mark_as_unread(cr, uid, [thread_id], context=context)
return thread_id
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',