diff --git a/addons/crm/crm_lead.py b/addons/crm/crm_lead.py index 9fd81712b92..daecdf86ef4 100644 --- a/addons/crm/crm_lead.py +++ b/addons/crm/crm_lead.py @@ -827,7 +827,6 @@ class crm_lead(base_stage, osv.osv): }) if msg.get('priority') in dict(crm.AVAILABLE_PRIORITIES): custom_values['priority'] = msg.get('priority') - custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context)) return super(crm_lead, self).message_new(cr, uid, msg, custom_values=custom_values, context=context) def message_update(self, cr, uid, ids, msg, update_vals=None, context=None): diff --git a/addons/crm/test/process/communication_with_customer.yml b/addons/crm/test/process/communication_with_customer.yml index 1653be10763..c958889bdc1 100644 --- a/addons/crm/test/process/communication_with_customer.yml +++ b/addons/crm/test/process/communication_with_customer.yml @@ -34,12 +34,6 @@ !python {model: crm.lead}: | lead_ids = self.search(cr, uid, [('email_from','=', 'Mr. John Right ')]) self.convert_partner(cr, uid, lead_ids, context=context) -- - Now, I search customer in regular customer list. -- - !python {model: crm.lead}: | - partner_ids = self.message_partner_by_email(cr, uid, 'Mr. John Right ') - assert partner_ids.get('partner_id'), "Customer is not found in regular customer list." - I convert one phonecall request as a customer and put into regular customer list. - diff --git a/addons/crm/wizard/crm_lead_to_partner.py b/addons/crm/wizard/crm_lead_to_partner.py index 98f114b4088..430ec3c1543 100644 --- a/addons/crm/wizard/crm_lead_to_partner.py +++ b/addons/crm/wizard/crm_lead_to_partner.py @@ -56,10 +56,6 @@ class crm_lead2partner(osv.osv_memory): if not len(lead_ids): return False this = lead.browse(cr, uid, lead_ids[0], context=context) - # Find partner address matches the email_from of the lead - res = lead.message_partner_by_email(cr, uid, this.email_from, context=context) - partner_id = res.get('partner_id', False) - # Find partner name that matches the name of the lead if not partner_id and this.partner_name: partner_ids = partner.search(cr, uid, [('name', '=', this.partner_name)], context=context) if partner_ids and len(partner_ids): diff --git a/addons/crm_claim/crm_claim.py b/addons/crm_claim/crm_claim.py index ec6252bcdca..73d5af68b52 100644 --- a/addons/crm_claim/crm_claim.py +++ b/addons/crm_claim/crm_claim.py @@ -200,7 +200,6 @@ class crm_claim(base_stage, osv.osv): }) if msg.get('priority'): custom_values['priority'] = msg.get('priority') - custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from'), context=context)) return super(crm_claim,self).message_new(cr, uid, msg, custom_values=custom_values, context=context) def message_update(self, cr, uid, ids, msg, update_vals=None, context=None): diff --git a/addons/crm_helpdesk/crm_helpdesk.py b/addons/crm_helpdesk/crm_helpdesk.py index 4b0cb2e3ffa..bb3b0891479 100644 --- a/addons/crm_helpdesk/crm_helpdesk.py +++ b/addons/crm_helpdesk/crm_helpdesk.py @@ -110,7 +110,6 @@ class crm_helpdesk(base_state, osv.osv): 'email_cc': msg.get('cc'), 'user_id': False, }) - custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from'), context=context)) return super(crm_helpdesk,self).message_new(cr, uid, msg, custom_values=custom_values, context=context) def message_update(self, cr, uid, ids, msg, update_vals=None, context=None): diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py index 3d0b704b36d..721b5a8dc6d 100644 --- a/addons/hr_recruitment/hr_recruitment.py +++ b/addons/hr_recruitment/hr_recruitment.py @@ -339,7 +339,6 @@ class hr_applicant(base_stage, osv.Model): }) if msg.get('priority'): custom_values['priority'] = msg.get('priority') - custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context)) return super(hr_applicant,self).message_new(cr, uid, msg, custom_values=custom_values, context=context) def message_update(self, cr, uid, ids, msg, update_vals=None, context=None): diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py index 9aae5f9a8b7..0380e9084a3 100644 --- a/addons/mail/mail_mail.py +++ b/addons/mail/mail_mail.py @@ -37,11 +37,12 @@ class mail_mail(osv.Model): 'email_to': fields.text('To', help='Message recipients'), 'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'), 'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'), + 'content_subtype': fields.char('Message content subtype', size=32, oldname="subtype", readonly=1, help="Type of message, usually 'html' or 'plain', used to select "\ "plain-text or rich-text contents accordingly"), - 'body_html': fields.html('Rich-text Contents', help="Rich-text/HTML version of the message"), + 'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML version of the message"), } _defaults = { diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index 43628f217f1..2fe65600389 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -105,15 +105,17 @@ class mail_message(osv.Model): select=True, ondelete='set null', help="Initial thread message."), 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'), + 'model': fields.char('Related Document Model', size=128, select=1), 'res_id': fields.integer('Related Document ID', select=1), 'record_name': fields.function(get_record_name, type='string', string='Message Record Name', help="Name get of the related document."), + 'subject': fields.char('Subject', size=128), 'date': fields.datetime('Date'), 'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1), - 'body': fields.text('Content', required=True), + 'body': fields.html('Content', required=True), } _defaults = { 'type': 'email', @@ -204,159 +206,4 @@ class mail_message(osv.Model): self.check(cr, uid, ids, 'unlink', context=context) return super(mail_message, self).unlink(cr, uid, ids, context) - # FP Note: to review - def parse_message(self, message, save_original=False, context=None): - """Parses a string or email.message.Message representing an - RFC-2822 email, and returns a generic dict holding the - message details. - :param message: the message to parse - :type message: email.message.Message | string | unicode - :param bool save_original: whether the returned dict - should include an ``original`` entry with the base64 - encoded source of the message. - :rtype: dict - :return: A dict with the following structure, where each - field may not be present if missing in original - message:: - - { 'message-id': msg_id, - 'subject': subject, - 'from': from, - 'to': to, - 'cc': cc, - 'headers' : { 'X-Mailer': mailer, - #.. all X- headers... - }, - 'content_subtype': msg_mime_subtype, - 'body_text': plaintext_body - 'body_html': html_body, - 'attachments': [('file1', 'bytes'), - ('file2', 'bytes') } - # ... - 'original': source_of_email, - } - """ - msg_txt = message - if isinstance(message, str): - msg_txt = email.message_from_string(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) - - message_id = msg_txt.get('message-id', False) - msg = {} - - if save_original: - # save original, we need to be able to read the original email sometimes - msg['original'] = message.as_string() if isinstance(message, Message) \ - else message - msg['original'] = base64.b64encode(msg['original']) # binary fields are b64 - - if not message_id: - # Very unusual situation, be we should be fault-tolerant here - message_id = time.time() - msg_txt['message-id'] = message_id - _logger.info('Parsing Message without message-id, generating a random one: %s', message_id) - - msg_fields = msg_txt.keys() - msg['id'] = message_id - msg['message-id'] = message_id - - if 'Subject' in msg_fields: - msg['subject'] = decode(msg_txt.get('Subject')) - - if 'Content-Type' in msg_fields: - msg['content-type'] = msg_txt.get('Content-Type') - - if 'From' in msg_fields: - msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom()) - - if 'To' in msg_fields: - msg['to'] = decode(msg_txt.get('To')) - - if 'Delivered-To' in msg_fields: - msg['to'] = decode(msg_txt.get('Delivered-To')) - - if 'CC' in msg_fields: - msg['cc'] = decode(msg_txt.get('CC')) - - if 'Cc' in msg_fields: - msg['cc'] = decode(msg_txt.get('Cc')) - - if 'Reply-To' in msg_fields: - msg['reply'] = decode(msg_txt.get('Reply-To')) - - if 'Date' in msg_fields: - date_hdr = decode(msg_txt.get('Date')) - # convert from email timezone to server timezone - date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone())) - date_server_datetime_str = date_server_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - msg['date'] = date_server_datetime_str - - if 'Content-Transfer-Encoding' in msg_fields: - msg['encoding'] = msg_txt.get('Content-Transfer-Encoding') - - if 'References' in msg_fields: - msg['references'] = msg_txt.get('References') - - if 'In-Reply-To' in msg_fields: - msg['in-reply-to'] = msg_txt.get('In-Reply-To') - - msg['headers'] = {} - msg['content_subtype'] = 'plain' - for item in msg_txt.items(): - if item[0].startswith('X-'): - msg['headers'].update({item[0]: item[1]}) - if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''): - encoding = msg_txt.get_content_charset() - body = msg_txt.get_payload(decode=True) - if 'text/html' in msg.get('content-type', ''): - msg['body_html'] = body - msg['content_subtype'] = 'html' - if body: - body = tools.html2plaintext(body) - msg['body_text'] = tools.ustr(body, encoding) - - attachments = [] - if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''): - body = "" - if 'multipart/alternative' in msg.get('content-type', ''): - msg['content_subtype'] = 'alternative' - else: - msg['content_subtype'] = 'mixed' - for part in msg_txt.walk(): - if part.get_content_maintype() == 'multipart': - continue - - encoding = part.get_content_charset() - filename = part.get_filename() - if part.get_content_maintype()=='text': - content = part.get_payload(decode=True) - if filename: - attachments.append((filename, content)) - content = tools.ustr(content, encoding) - if part.get_content_subtype() == 'html': - msg['body_html'] = content - msg['content_subtype'] = 'html' # html version prevails - body = tools.ustr(tools.html2plaintext(content)) - body = body.replace(' ', '') - elif part.get_content_subtype() == 'plain': - body = content - elif part.get_content_maintype() in ('application', 'image'): - if filename : - attachments.append((filename,part.get_payload(decode=True))) - else: - res = part.get_payload(decode=True) - body += tools.ustr(res, encoding) - - msg['body_text'] = body - msg['attachments'] = attachments - - # for backwards compatibility: - msg['body'] = msg['body_text'] - msg['sub_type'] = msg['content_subtype'] or 'plain' - return msg diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 74a69ac88e5..394ef32ca1e 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -29,7 +29,7 @@ from email.utils import parsedate from email.message import Message from osv import osv, fields -from mail_message import decode, to_email +from mail_message import decode, mail_tools_to_email import tools from tools.translate import _ from tools.safe_eval import safe_eval as eval @@ -497,8 +497,16 @@ class mail_thread(osv.Model): msgs = msg_obj.read(cr, uid, msg_ids, context=context) return msgs + def _message_find_partners(self, cr, uid, message, headers=['From'], context=None): + s = ', '.join([decode(message.get(h)) for h in headers) + mails = mail_tools_to_email(s) + result = [] + for m in mails: + result += self.pool.get('res.partner').search(cr, uid, [('email','ilike',m)], context=context) + return result + def _message_find_user_id(self, cr, uid, message, context=None): - from_local_part = to_email(decode(message.get('From')))[0] + from_local_part = mail_tools_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 @@ -595,7 +603,6 @@ class mail_thread(osv.Model): 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): @@ -753,8 +760,8 @@ class mail_thread(osv.Model): followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id] else: followers = self.message_thread_followers(cr, uid, [res.id])[res.id] - message_followers_emails = to_email(','.join(filter(None, followers))) - message_recipients = to_email(','.join(filter(None, + message_followers_emails = mail_tools_to_email(','.join(filter(None, followers))) + message_recipients = mail_tools_to_email(','.join(filter(None, [decode(msg['from']), decode(msg['to']), decode(msg['cc'])]))) @@ -765,7 +772,7 @@ class mail_thread(osv.Model): del msg['reply-to'] msg['reply-to'] = res.section_id.reply_to - smtp_from, = to_email(msg['from']) + smtp_from, = mail_tools_to_email(msg['from']) msg['from'] = smtp_from msg['to'] = ", ".join(forward_to) msg['message-id'] = tools.generate_tracking_message_id(res.id) @@ -777,37 +784,187 @@ class mail_thread(osv.Model): smtp_server_obj.send_email(cr, uid, msg) return True - def message_partner_by_email(self, cr, uid, email, context=None): - """Attempts to return the id of a partner address matching - the given ``email``, and the corresponding partner id. - Can be used by classes using the ``mail.thread`` mixin - to lookup the partner and use it in their implementation - of ``message_new`` to link the new record with a - corresponding partner. - The keys used in the returned dict are meant to map - to usual names for relationships towards a partner - and one of its addresses. + def parse_message(self, message, save_original=False, context=None): + """Parses a string or email.message.Message representing an + RFC-2822 email, and returns a generic dict holding the + message details. - :param email: email address for which a partner - should be searched for. + :param message: the message to parse + :type message: email.message.Message | string | unicode + :param bool save_original: whether the returned dict + should include an ``original`` entry with the base64 + encoded source of the message. :rtype: dict - :return: a map of the following form:: + :return: A dict with the following structure, where each + field may not be present if missing in original + message:: - { 'partner_address_id': id or False, - 'partner_id': pid or False } + { 'message-id': msg_id, + 'subject': subject, + 'from': from, --> author_id + 'to': to, --> partner_ids + 'cc': cc, --> partner_ids + 'headers' : { 'X-Mailer': mailer, --> to remove + #.. all X- headers... + }, + 'content_subtype': msg_mime_subtype, --> to remove + 'body_text': plaintext_body --> keep body + 'body_html': html_body, --> to remove + 'attachments': [('file1', 'bytes'), + ('file2', 'bytes') } + # ... + 'original': source_of_email, --> attachment document + } """ - partner_pool = self.pool.get('res.partner') - res = {'partner_id': False} - if email: - email = to_email(email)[0] - contact_ids = partner_pool.search(cr, uid, [('email', '=', email)]) - if contact_ids: - contact = partner_pool.browse(cr, uid, contact_ids[0]) - res['partner_id'] = contact.id - return res + msg_txt = message + attachments = [] + if isinstance(message, str): + msg_txt = email.message_from_string(message) - # for backwards-compatibility with old scripts - process_email = message_process + # 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) + + message_id = msg_txt.get('message-id', False) + msg = {} + + if save_original: + msg_original = message.as_string() if isinstance(message, Message) \ + else message + attachments.append((0, 0, { + 'name':'email.msg', + 'datas': base64.b64encode(msg_original), + 'datas_fname': 'email.msg', + 'res_model': 'mail.message', + 'description': _('original email'), + }) + + if not message_id: + # Very unusual situation, be we should be fault-tolerant here + message_id = time.time() + msg_txt['message-id'] = message_id + _logger.info('Parsing Message without message-id, generating a random one: %s', message_id) + + msg_fields = msg_txt.keys() + + msg['message_id'] = message_id + + if 'Subject' in msg_fields: + msg['subject'] = decode(msg_txt.get('Subject')) + + #if 'Content-Type' in msg_fields: + # msg['content-type'] = msg_txt.get('Content-Type') + + # find author_id + + if 'From' in msg_fields: + author_ids = self._message_find_partners(cr, uid, msg_text, ['From'], context=context) + #decode(msg_txt.get('From') or msg_txt.get_unixfrom()) ) + if author_ids: + msg['author_id'] = author_ids[0] + + partner_ids = self._message_find_partners(cr, uid, msg_text, ['From','To','Delivered-To','CC','Cc'], context=context) + msg['partner_ids'] = partner_ids + + #if 'To' in msg_fields: + # msg['to'] = decode(msg_txt.get('To')) + + #if 'Delivered-To' in msg_fields: + # msg['to'] = decode(msg_txt.get('Delivered-To')) + + #if 'CC' in msg_fields: + # msg['cc'] = decode(msg_txt.get('CC')) + + #if 'Cc' in msg_fields: + # msg['cc'] = decode(msg_txt.get('Cc')) + + #if 'Reply-To' in msg_fields: + # msg['reply'] = decode(msg_txt.get('Reply-To')) + + # FP Note: I propose to store the current datetime rather than the email date + #if 'Date' in msg_fields: + # date_hdr = decode(msg_txt.get('Date')) + # # convert from email timezone to server timezone + # date_server_datetime = dateutil.parser.parse(date_hdr).astimezone(pytz.timezone(tools.get_server_timezone())) + # date_server_datetime_str = date_server_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + # msg['date'] = date_server_datetime_str + + #if 'Content-Transfer-Encoding' in msg_fields: + # msg['encoding'] = msg_txt.get('Content-Transfer-Encoding') + + #if 'References' in msg_fields: + # msg['references'] = msg_txt.get('References') + + # FP Note: todo - find parent_id + if 'In-Reply-To' in msg_fields: + pass + + #msg['headers'] = {} + #msg['content_subtype'] = 'plain' + #for item in msg_txt.items(): + # if item[0].startswith('X-'): + # msg['headers'].update({item[0]: item[1]}) + if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''): + encoding = msg_txt.get_content_charset() + body = msg_txt.get_payload(decode=True) + if 'text/html' in msg.get('content-type', ''): + msg['body'] = body + # msg['content_subtype'] = 'html' + # if body: + # body = tools.html2plaintext(body) + # msg['body_text'] = tools.ustr(body, encoding) + + if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''): + body = "" + if 'multipart/alternative' in msg.get('content-type', ''): + msg['content_subtype'] = 'alternative' + else: + msg['content_subtype'] = 'mixed' + for part in msg_txt.walk(): + if part.get_content_maintype() == 'multipart': + continue + + encoding = part.get_content_charset() + filename = part.get_filename() + if part.get_content_maintype()=='text': + content = part.get_payload(decode=True) + if filename: + attachments.append((0, 0, { + 'name': filename, + 'datas': base64.b64encode(msg_original), + 'datas_fname': filename, + 'res_model': 'mail.message', + 'description': _('email attachment'), + })) + content = tools.ustr(content, encoding) + if part.get_content_subtype() == 'html': + msg['body'] = content + # msg['content_subtype'] = 'html' # html version prevails + # body = tools.ustr(tools.html2plaintext(content)) + # body = body.replace(' ', '') + elif part.get_content_subtype() == 'plain': + msg['body'] = content + elif part.get_content_maintype() in ('application', 'image'): + if filename: + attachments.append((0, 0, { + 'name': filename, + 'datas': part.get_payload(decode=True), + 'datas_fname': filename, + 'res_model': 'mail.message', + 'description': _('email attachment'), + })) + else: + res = part.get_payload(decode=True) + msg['body'] += tools.ustr(res, encoding) + + msg['attachments'] = attachments + + # for backwards compatibility: + # msg['body'] = msg['body_text'] + # msg['sub_type'] = msg['content_subtype'] or 'plain' + return msg #------------------------------------------------------ # Note specific diff --git a/addons/mail/res_users.py b/addons/mail/res_users.py index 1a39ed64bd9..5a8c1b0412e 100644 --- a/addons/mail/res_users.py +++ b/addons/mail/res_users.py @@ -183,7 +183,7 @@ class res_users_mail_group(osv.Model): user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] mail_group_obj = self.pool.get('mail.group') 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) + mail_group_obj.message_subscribe_users(cr, uid, mail_group_ids, ids, context=context) return write_res class res_groups_mail_group(osv.Model): @@ -203,6 +203,6 @@ class res_groups_mail_group(osv.Model): user_ids += [id for command in vals['users'] if command[0] == 6 for id in command[2]] mail_group_obj = self.pool.get('mail.group') 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) + mail_group_obj.message_subscribe_users(cr, uid, mail_group_ids, user_ids, context=context) return super(res_groups_mail_group, self).write(cr, uid, ids, vals, context=context) diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index a8a1e617ecb..db9d02bfd1e 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -461,7 +461,6 @@ class project_issue(base_stage, osv.osv): }) if msg.get('priority'): custom_values['priority'] = msg.get('priority') - custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from'), context=context)) res_id = super(project_issue, self).message_new(cr, uid, msg, custom_values=custom_values, context=context) # self.convert_to_bug(cr, uid, [res_id], context=context) diff --git a/addons/project_mailgate/project_mailgate.py b/addons/project_mailgate/project_mailgate.py index ba3cd702760..cf2bc120fbf 100644 --- a/addons/project_mailgate/project_mailgate.py +++ b/addons/project_mailgate/project_mailgate.py @@ -38,7 +38,6 @@ class project_tasks(osv.osv): 'planned_hours': 0.0, 'subject': msg.get('subject'), }) - custom_values.update(self.message_partner_by_email(cr, uid, msg.get('from', False), context=context)) return super(project_tasks,self).message_new(cr, uid, msg, custom_values=custom_values, context=context) def message_update(self, cr, uid, ids, msg, update_vals=None, context=None):