[MERGE] mail: add various mailgateway improvements
This merge holds several long awaited improvements for the mail gateway. It includes * store email status of notifications emails sent to customers * display this information on Chatter * improve bounce email management: track bounced notifications emails, use message_bounce fields directly in mail and not in mass_mailing * send emails to assigned user on records * remove required alias on users while keeping it possible to define alias on users and use them * handle emails forwarded to aliases as new threads instead of replies * do not store needaction for forum and blog posts * automatically unsubscribe partners that bounce on channels * some light code cleaning
This commit is contained in:
@@ -9,8 +9,6 @@
|
||||
<field name="active" eval="False"/>
|
||||
<!-- Avoid auto-including this user in any default group, just like a typical portal member -->
|
||||
<field name="groups_id" eval="[(6, 0, [ref('base.group_portal')])]"/>
|
||||
<!-- allow signuped users to have a alias -->
|
||||
<field name="alias_name">_usertemplate</field>
|
||||
</record>
|
||||
|
||||
<record id="default_template_user_config" model="ir.config_parameter">
|
||||
|
||||
@@ -18,7 +18,6 @@ class TestCrm(TestMail):
|
||||
cls.user_salesman_all = Users.create({
|
||||
'name': 'Riton La Chignole',
|
||||
'login': 'riton',
|
||||
'alias_name': 'riton',
|
||||
'email': 'riton.salesman_all@example.com',
|
||||
'groups_id': [(6, 0, [user_group_employee.id, user_group_salesman_all.id])]
|
||||
})
|
||||
|
||||
@@ -27,7 +27,6 @@ class TestEventCommon(common.TransactionCase):
|
||||
self.user_eventuser = self.Users.with_context({'no_reset_password': True}).create({
|
||||
'name': 'Armande EventUser',
|
||||
'login': 'Armande',
|
||||
'alias_name': 'armande',
|
||||
'email': 'armande.eventuser@example.com',
|
||||
'tz': 'Europe/Brussels',
|
||||
'groups_id': [(6, 0, [self.group_employee_id, self.group_event_user_id])]
|
||||
@@ -35,7 +34,6 @@ class TestEventCommon(common.TransactionCase):
|
||||
self.user_eventmanager = self.Users.with_context({'no_reset_password': True}).create({
|
||||
'name': 'Bastien EventManager',
|
||||
'login': 'bastien',
|
||||
'alias_name': 'bastien',
|
||||
'email': 'bastien.eventmanager@example.com',
|
||||
'tz': 'Europe/Brussels',
|
||||
'groups_id': [(6, 0, [
|
||||
|
||||
@@ -3,45 +3,34 @@
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, models
|
||||
from odoo.addons.mail.models.mail_thread import decode_header
|
||||
from odoo.tools import email_split
|
||||
from odoo import api, models, _
|
||||
from odoo.tools import decode_message_header, email_split
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = "mail.thread"
|
||||
|
||||
@api.model
|
||||
def message_route_verify(self, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False):
|
||||
res = super(MailThread, self).message_route_verify(message, message_dict, route, update_author, assert_model, create_fallback, allow_private)
|
||||
def message_route_verify(self, message, message_dict, route,
|
||||
update_author=True, assert_model=True,
|
||||
create_fallback=True, allow_private=False,
|
||||
drop_alias=False):
|
||||
res = super(MailThread, self).message_route_verify(
|
||||
message, message_dict, route,
|
||||
update_author=update_author,
|
||||
assert_model=assert_model,
|
||||
create_fallback=create_fallback,
|
||||
allow_private=allow_private,
|
||||
drop_alias=drop_alias)
|
||||
|
||||
if res:
|
||||
alias = route[4]
|
||||
email_from = decode_header(message, 'From')
|
||||
email_from = decode_message_header(message, 'From')
|
||||
message_id = message.get('Message-Id')
|
||||
|
||||
# Identically equal to the definition in mail module because sub methods are local
|
||||
# variables and cannot be called with super
|
||||
def _create_bounce_email(body_html):
|
||||
bounce_to = decode_header(message, 'Return-Path') or email_from
|
||||
bounce_mail_values = {
|
||||
'body_html': body_html,
|
||||
'subject': 'Re: %s' % message.get('subject'),
|
||||
'email_to': bounce_to,
|
||||
'auto_delete': True,
|
||||
}
|
||||
bounce_from = self.env['ir.mail_server']._get_default_bounce_address()
|
||||
if bounce_from:
|
||||
bounce_mail_values['email_from'] = 'MAILER-DAEMON <%s>' % bounce_from
|
||||
self.env['mail.mail'].create(bounce_mail_values).send()
|
||||
|
||||
def _warn(message):
|
||||
_logger.info('Routing mail with Message-Id %s: route %s: %s',
|
||||
message_id, route, message)
|
||||
|
||||
# Alias: check alias_contact settings for employees
|
||||
|
||||
if alias and alias.alias_contact == 'employees':
|
||||
email_address = email_split(email_from)[0]
|
||||
employee = self.env['hr.employee'].search([('work_email', 'ilike', email_address)], limit=1)
|
||||
@@ -49,7 +38,7 @@ class MailThread(models.AbstractModel):
|
||||
employee = self.env['hr.employee'].search([('user_id.email', 'ilike', email_address)], limit=1)
|
||||
if not employee:
|
||||
mail_template = self.env.ref('hr.mail_template_data_unknown_employee_email_address')
|
||||
_warn('alias %s does not accept unknown employees, skipping' % alias.alias_name)
|
||||
_create_bounce_email(mail_template.body_html)
|
||||
self._routing_warn(_('alias %s does not accept unknown employees') % alias.alias_name, _('skipping'), message_id, route, False)
|
||||
self._routing_create_bounce_email(email_from, mail_template.body_html, message)
|
||||
return False
|
||||
return res
|
||||
|
||||
@@ -18,21 +18,18 @@ class TestHrHolidaysBase(common.TransactionCase):
|
||||
self.user_hruser_id = Users.create({
|
||||
'name': 'Armande HrUser',
|
||||
'login': 'Armande',
|
||||
'alias_name': 'armande',
|
||||
'email': 'armande.hruser@example.com',
|
||||
'groups_id': [(6, 0, [group_employee_id, self.ref('base.group_hr_user')])]
|
||||
}).id
|
||||
self.user_hrmanager_id = Users.create({
|
||||
'name': 'Bastien HrManager',
|
||||
'login': 'bastien',
|
||||
'alias_name': 'bastien',
|
||||
'email': 'bastien.hrmanager@example.com',
|
||||
'groups_id': [(6, 0, [group_employee_id, self.ref('base.group_hr_manager')])]
|
||||
}).id
|
||||
self.user_employee_id = Users.create({
|
||||
'name': 'David Employee',
|
||||
'login': 'david',
|
||||
'alias_name': 'david',
|
||||
'email': 'david.employee@example.com',
|
||||
'groups_id': [(6, 0, [group_employee_id])]
|
||||
}).id
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
import models
|
||||
import wizard
|
||||
import controllers
|
||||
import tests.test_mail_model
|
||||
|
||||
@@ -39,6 +39,12 @@
|
||||
<field name="value">catchall</field>
|
||||
</record>
|
||||
|
||||
<!-- Bounce Email Alias -->
|
||||
<record id="icp_mail_bounce_alias" model="ir.config_parameter">
|
||||
<field name="key">mail.bounce.alias</field>
|
||||
<field name="value">bounce</field>
|
||||
</record>
|
||||
|
||||
<!-- Discussion subtype for messaging / Chatter -->
|
||||
<record id="mt_comment" model="mail.message.subtype">
|
||||
<field name="name">Discussions</field>
|
||||
@@ -70,7 +76,7 @@
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
% if not ctx['is_discussion'] or not len(ctx['actions']) == 0 or ctx.get('button_access') or ctx.get('is_accessible'):
|
||||
% if not ctx['is_discussion'] or not len(ctx['actions']) == 0 or ctx.get('button_access'):
|
||||
<div summary="o_mail_notification">
|
||||
<table cellspacing="0" cellpadding="0" border="0" style="width: 100%; margin-top: 5px;">
|
||||
<tbody><tr>
|
||||
|
||||
@@ -4,6 +4,7 @@ import mail_message_subtype
|
||||
import mail_tracking_value
|
||||
import mail_alias
|
||||
import mail_followers
|
||||
import mail_notification
|
||||
import mail_message
|
||||
import mail_mail
|
||||
import mail_thread
|
||||
|
||||
@@ -37,6 +37,8 @@ class Channel(models.Model):
|
||||
_inherit = ['mail.thread']
|
||||
_inherits = {'mail.alias': 'alias_id'}
|
||||
|
||||
MAX_BOUNCE_LIMIT = 10
|
||||
|
||||
def _get_default_image(self):
|
||||
image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
|
||||
return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))
|
||||
@@ -156,14 +158,17 @@ class Channel(models.Model):
|
||||
|
||||
@api.multi
|
||||
def action_unfollow(self):
|
||||
partner_id = self.env.user.partner_id.id
|
||||
return self._action_unfollow(self.env.user.partner_id)
|
||||
|
||||
@api.multi
|
||||
def _action_unfollow(self, partner):
|
||||
channel_info = self.channel_info('unsubscribe')[0] # must be computed before leaving the channel (access rights)
|
||||
result = self.write({'channel_partner_ids': [(3, partner_id)]})
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id), channel_info)
|
||||
result = self.write({'channel_partner_ids': [(3, partner.id)]})
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner.id), channel_info)
|
||||
if not self.email_send:
|
||||
notification = _('<div class="o_mail_notification">left <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
|
||||
# post 'channel left' message as root since the partner just unsubscribed from the channel
|
||||
self.sudo().message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner_id)
|
||||
self.sudo().message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id)
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
@@ -201,6 +206,13 @@ class Channel(models.Model):
|
||||
res['headers'] = repr(headers)
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def message_receive_bounce(self, email, partner, mail_id=None):
|
||||
""" Override bounce management to unsubscribe bouncing addresses """
|
||||
if partner.message_bounce >= self.MAX_BOUNCE_LIMIT:
|
||||
self._action_unfollow(partner)
|
||||
return super(Channel, self).message_receive_bounce(email, partner, mail_id=mail_id)
|
||||
|
||||
@api.multi
|
||||
def message_get_recipient_values(self, notif_message=None, recipient_ids=None):
|
||||
# real mailing list: multiple recipients (hidden by X-Forge-To)
|
||||
|
||||
@@ -129,6 +129,19 @@ class MailMail(models.Model):
|
||||
:param browse_record mail: the mail that was just sent
|
||||
:return: True
|
||||
"""
|
||||
notif_emails = self.filtered(lambda email: email.notification)
|
||||
if notif_emails:
|
||||
notifications = self.env['mail.notification'].search([
|
||||
('mail_message_id', 'in', notif_emails.mapped('mail_message_id').ids),
|
||||
('is_email', '=', True)])
|
||||
if mail_sent:
|
||||
notifications.write({
|
||||
'email_status': 'sent',
|
||||
})
|
||||
else:
|
||||
notifications.write({
|
||||
'email_status': 'exception',
|
||||
})
|
||||
if mail_sent:
|
||||
self.sudo().filtered(lambda self: self.auto_delete).unlink()
|
||||
return True
|
||||
@@ -222,9 +235,9 @@ class MailMail(models.Model):
|
||||
catchall_domain = self.env['ir.config_parameter'].get_param("mail.catchall.domain")
|
||||
if bounce_alias and catchall_domain:
|
||||
if mail.model and mail.res_id:
|
||||
headers['Return-Path'] = '%s-%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
|
||||
headers['Return-Path'] = '%s+%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
|
||||
else:
|
||||
headers['Return-Path'] = '%s-%d@%s' % (bounce_alias, mail.id, catchall_domain)
|
||||
headers['Return-Path'] = '%s+%d@%s' % (bounce_alias, mail.id, catchall_domain)
|
||||
if mail.headers:
|
||||
try:
|
||||
headers.update(safe_eval(mail.headers))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import logging
|
||||
|
||||
from email.header import decode_header
|
||||
from email.utils import formataddr
|
||||
|
||||
from odoo import _, api, fields, models, SUPERUSER_ID, tools
|
||||
@@ -14,16 +13,6 @@ from odoo.osv import expression
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def decode(text):
|
||||
"""Returns unicode() string conversion of the the given encoded smtp header text"""
|
||||
# TDE proposal: move to tools ?
|
||||
if text:
|
||||
text = decode_header(text.replace('\r', ''))
|
||||
# The joining space will not be needed as of Python 3.3
|
||||
# See https://hg.python.org/cpython/rev/8c03fe231877
|
||||
return ' '.join([tools.ustr(x[0], x[1]) for x in text])
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
""" Messages model: system notification (replacing res.log notifications),
|
||||
comments (OpenChatter discussion) and incoming emails. """
|
||||
@@ -37,11 +26,9 @@ class Message(models.Model):
|
||||
|
||||
@api.model
|
||||
def _get_default_from(self):
|
||||
if self.env.user.alias_name and self.env.user.alias_domain:
|
||||
return formataddr((self.env.user.name, '%s@%s' % (self.env.user.alias_name, self.env.user.alias_domain)))
|
||||
elif self.env.user.email:
|
||||
if self.env.user.email:
|
||||
return formataddr((self.env.user.name, self.env.user.email))
|
||||
raise UserError(_("Unable to send email, please configure the sender's email address or alias."))
|
||||
raise UserError(_("Unable to send email, please configure the sender's email address."))
|
||||
|
||||
@api.model
|
||||
def _get_default_author(self):
|
||||
@@ -93,6 +80,10 @@ class Message(models.Model):
|
||||
help='Need Action')
|
||||
channel_ids = fields.Many2many(
|
||||
'mail.channel', 'mail_message_mail_channel_rel', string='Channels')
|
||||
# notifications
|
||||
notification_ids = fields.One2many(
|
||||
'mail.notification', 'mail_message_id', 'Notifications',
|
||||
auto_join=True, copy=False)
|
||||
# user interface
|
||||
starred_partner_ids = fields.Many2many(
|
||||
'res.partner', 'mail_message_res_partner_starred_rel', string='Favorited By')
|
||||
@@ -116,20 +107,18 @@ class Message(models.Model):
|
||||
@api.multi
|
||||
def _get_needaction(self):
|
||||
""" Need action on a mail.message = notified on my channel """
|
||||
my_messages = self.sudo().filtered(lambda msg: self.env.user.partner_id in msg.needaction_partner_ids)
|
||||
my_messages = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', 'in', self.ids),
|
||||
('res_partner_id', '=', self.env.user.partner_id.id),
|
||||
('is_read', '=', False)]).mapped('mail_message_id')
|
||||
for message in self:
|
||||
message.needaction = message in my_messages
|
||||
|
||||
@api.multi
|
||||
def _is_accessible(self):
|
||||
self.ensure_one()
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def _search_needaction(self, operator, operand):
|
||||
if operator == '=' and operand:
|
||||
return [('needaction_partner_ids', 'in', self.env.user.partner_id.id)]
|
||||
return [('needaction_partner_ids', 'not in', self.env.user.partner_id.id)]
|
||||
return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', False)]
|
||||
return ['&', ('notification_ids.res_partner_id', '=', self.env.user.partner_id.id), ('notification_ids.is_read', '=', True)]
|
||||
|
||||
@api.depends('starred_partner_ids')
|
||||
def _get_starred(self):
|
||||
@@ -158,7 +147,8 @@ class Message(models.Model):
|
||||
""" Remove all needactions of the current partner. If channel_ids is
|
||||
given, restrict to messages written in one of those channels. """
|
||||
partner_id = self.env.user.partner_id.id
|
||||
if domain is None:
|
||||
delete_mode = not self.env.user.share # delete employee notifs, keep customer ones
|
||||
if domain is None and delete_mode:
|
||||
query = "DELETE FROM mail_message_res_partner_needaction_rel WHERE res_partner_id IN %s"
|
||||
args = [(partner_id,)]
|
||||
if channel_ids:
|
||||
@@ -181,7 +171,14 @@ class Message(models.Model):
|
||||
if channel_ids:
|
||||
msg_domain += [('channel_ids', 'in', channel_ids)]
|
||||
unread_messages = self.search(expression.AND([msg_domain, domain]))
|
||||
unread_messages.sudo().write({'needaction_partner_ids': [(3, partner_id)]})
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', 'in', unread_messages.ids),
|
||||
('res_partner_id', '=', self.env.user.partner_id.id),
|
||||
('is_read', '=', False)])
|
||||
if delete_mode:
|
||||
notifications.unlink()
|
||||
else:
|
||||
notifications.write({'is_read': True})
|
||||
ids = unread_messages.mapped('id')
|
||||
|
||||
notification = {'type': 'mark_as_read', 'message_ids': ids, 'channel_ids': channel_ids}
|
||||
@@ -204,14 +201,20 @@ class Message(models.Model):
|
||||
def set_message_done(self):
|
||||
""" Remove the needaction from messages for the current partner. """
|
||||
partner_id = self.env.user.partner_id
|
||||
messages = self.filtered(lambda msg: partner_id in msg.needaction_partner_ids)
|
||||
if not len(messages):
|
||||
delete_mode = not self.env.user.share # delete employee notifs, keep customer ones
|
||||
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', 'in', self.ids),
|
||||
('res_partner_id', '=', partner_id.id),
|
||||
('is_read', '=', False)])
|
||||
|
||||
if not notifications:
|
||||
return
|
||||
messages.sudo().write({'needaction_partner_ids': [(3, partner_id.id)]})
|
||||
|
||||
# notifies changes in messages through the bus. To minimize the number of
|
||||
# notifications, we need to group the messages depending on their channel_ids
|
||||
groups = []
|
||||
messages = notifications.mapped('mail_message_id')
|
||||
current_channel_ids = messages[0].channel_ids
|
||||
current_group = []
|
||||
for record in messages:
|
||||
@@ -226,6 +229,11 @@ class Message(models.Model):
|
||||
current_group = [record.id]
|
||||
current_channel_ids = record.channel_ids
|
||||
|
||||
if delete_mode:
|
||||
notifications.unlink()
|
||||
else:
|
||||
notifications.write({'is_read': True})
|
||||
|
||||
for (msg_ids, channel_ids) in groups:
|
||||
notification = {'type': 'mark_as_read', 'message_ids': msg_ids, 'channel_ids': [c.id for c in channel_ids]}
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id.id), notification)
|
||||
@@ -281,6 +289,8 @@ class Message(models.Model):
|
||||
partners |= message.partner_ids
|
||||
elif not message.subtype_id and message.partner_ids: # take specified people of message without a subtype (log)
|
||||
partners |= message.partner_ids
|
||||
if message.needaction_partner_ids: # notified
|
||||
partners |= message.needaction_partner_ids
|
||||
if message.attachment_ids:
|
||||
attachments |= message.attachment_ids
|
||||
if message.tracking_value_ids:
|
||||
@@ -322,6 +332,11 @@ class Message(models.Model):
|
||||
else:
|
||||
partner_ids = [partner_tree[partner.id] for partner in message.partner_ids
|
||||
if partner.id in partner_tree]
|
||||
|
||||
customer_email_data = []
|
||||
for notification in message.notification_ids.filtered(lambda notif: notif.res_partner_id.partner_share):
|
||||
customer_email_data.append((partner_tree[notification.res_partner_id.id][0], partner_tree[notification.res_partner_id.id][1], notification.email_status))
|
||||
|
||||
attachment_ids = []
|
||||
for attachment in message.attachment_ids:
|
||||
if attachment.id in attachments_tree:
|
||||
@@ -334,6 +349,10 @@ class Message(models.Model):
|
||||
message_dict.update({
|
||||
'author_id': author,
|
||||
'partner_ids': partner_ids,
|
||||
'customer_email_status': (all(d[2] == 'sent' for d in customer_email_data) and 'sent') or
|
||||
(any(d[2] == 'exception' for d in customer_email_data) and 'exception') or
|
||||
(any(d[2] == 'bounce' for d in customer_email_data) and 'bounce') or 'ready',
|
||||
'customer_email_data': customer_email_data,
|
||||
'attachment_ids': attachment_ids,
|
||||
'tracking_value_ids': tracking_value_ids,
|
||||
})
|
||||
@@ -725,7 +744,7 @@ class Message(models.Model):
|
||||
#------------------------------------------------------
|
||||
|
||||
@api.multi
|
||||
def _notify(self, force_send=False, user_signature=True):
|
||||
def _notify(self, force_send=False, send_after_commit=True, user_signature=True):
|
||||
""" Add the related record followers to the destination partner_ids if is not a private message.
|
||||
Call mail_notification.notify to manage the email sending
|
||||
"""
|
||||
@@ -758,11 +777,17 @@ class Message(models.Model):
|
||||
if not self._context.get('mail_notify_author', False) and self_sudo.author_id:
|
||||
partners = partners - self_sudo.author_id
|
||||
|
||||
# update message
|
||||
self.write({'channel_ids': [(6, 0, channels.ids)], 'needaction_partner_ids': [(6, 0, partners.ids)]})
|
||||
# update message, with maybe custom values
|
||||
message_values = {
|
||||
'channel_ids': [(6, 0, channels.ids)],
|
||||
'needaction_partner_ids': [(6, 0, partners.ids)]
|
||||
}
|
||||
if self.model and self.res_id and hasattr(self.env[self.model], 'message_get_message_notify_values'):
|
||||
message_values.update(self.env[self.model].browse(self.res_id).message_get_message_notify_values(self, message_values))
|
||||
self.write(message_values)
|
||||
|
||||
# notify partners and channels
|
||||
partners._notify(self, force_send=force_send, user_signature=user_signature)
|
||||
partners._notify(self, force_send=force_send, send_after_commit=send_after_commit, user_signature=user_signature)
|
||||
channels._notify(self)
|
||||
|
||||
# Discard cache, because child / parent allow reading and therefore
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Notification(models.Model):
|
||||
_name = 'mail.notification'
|
||||
_table = 'mail_message_res_partner_needaction_rel'
|
||||
_rec_name = 'res_partner_id'
|
||||
_log_access = False
|
||||
_description = 'Message Notifications'
|
||||
|
||||
mail_message_id = fields.Many2one(
|
||||
'mail.message', 'Message', index=True, ondelete='cascade', required=True)
|
||||
res_partner_id = fields.Many2one(
|
||||
'res.partner', 'Needaction Recipient', index=True, ondelete='cascade', required=True)
|
||||
is_read = fields.Boolean('Is Read', index=True)
|
||||
is_email = fields.Boolean('Sent by Email', index=True)
|
||||
email_status = fields.Selection([
|
||||
('ready', 'Ready to Send'),
|
||||
('sent', 'Sent'),
|
||||
('bounce', 'Bounced'),
|
||||
('exception', 'Exception')], 'Email Status',
|
||||
default='ready', index=True)
|
||||
|
||||
@api.model_cr
|
||||
def init(self):
|
||||
self._cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_res_partner_id_is_read_email_status_mail_message_id',))
|
||||
if not self._cr.fetchone():
|
||||
self._cr.execute('CREATE INDEX mail_notification_res_partner_id_is_read_email_status_mail_message_id ON mail_message_res_partner_needaction_rel (res_partner_id, is_read, email_status, mail_message_id)')
|
||||
+274
-224
@@ -21,20 +21,12 @@ from lxml import etree
|
||||
from werkzeug import url_encode
|
||||
|
||||
from odoo import _, api, exceptions, fields, models, tools
|
||||
from odoo.addons.mail.models.mail_message import decode
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
mail_header_msgid_re = re.compile('<[^<>]+>')
|
||||
|
||||
|
||||
def decode_header(message, header, separator=' '):
|
||||
return separator.join(map(decode, filter(None, message.get_all(header, []))))
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
''' mail_thread model is meant to be inherited by any model that needs to
|
||||
act as a discussion topic on which messages can be attached. Public
|
||||
@@ -199,7 +191,7 @@ class MailThread(models.AbstractModel):
|
||||
# search for unread messages, directly in SQL to improve performances
|
||||
self._cr.execute(""" SELECT msg.res_id FROM mail_message msg
|
||||
RIGHT JOIN mail_message_res_partner_needaction_rel rel
|
||||
ON rel.mail_message_id = msg.id AND rel.res_partner_id = %s
|
||||
ON rel.mail_message_id = msg.id AND rel.res_partner_id = %s AND (rel.is_read = false OR rel.is_read IS NULL)
|
||||
WHERE msg.model = %s AND msg.res_id in %s""",
|
||||
(self.env.user.partner_id.id, self._name, tuple(self.ids),))
|
||||
for result in self._cr.fetchall():
|
||||
@@ -795,11 +787,35 @@ class MailThread(models.AbstractModel):
|
||||
""" Find partners related to some header fields of the message.
|
||||
|
||||
:param string message: an email.message instance """
|
||||
s = ', '.join([decode(message.get(h)) for h in header_fields if message.get(h)])
|
||||
s = ', '.join([tools.decode_smtp_header(message.get(h)) for h in header_fields if message.get(h)])
|
||||
return filter(lambda x: x, self._find_partner_from_emails(tools.email_split(s)))
|
||||
|
||||
def _routing_warn(self, error_message, warn_suffix, message_id, route, raise_exception):
|
||||
""" Tools method used in message_route_verify: whether to log a warning or raise an error """
|
||||
full_message = _('Routing mail with Message-Id %s: route %s: %s') % (message_id, route, error_message)
|
||||
if raise_exception:
|
||||
raise ValueError(full_message)
|
||||
else:
|
||||
_logger.info(full_message + warn_suffix and '; %s' % warn_suffix or '')
|
||||
|
||||
def _routing_create_bounce_email(self, email_from, body_html, message):
|
||||
bounce_to = tools.decode_message_header(message, 'Return-Path') or email_from
|
||||
bounce_mail_values = {
|
||||
'body_html': body_html,
|
||||
'subject': 'Re: %s' % message.get('subject'),
|
||||
'email_to': bounce_to,
|
||||
'auto_delete': True,
|
||||
}
|
||||
bounce_from = self.env['ir.mail_server']._get_default_bounce_address()
|
||||
if bounce_from:
|
||||
bounce_mail_values['email_from'] = 'MAILER-DAEMON <%s>' % bounce_from
|
||||
self.env['mail.mail'].create(bounce_mail_values).send()
|
||||
|
||||
@api.model
|
||||
def message_route_verify(self, message, message_dict, route, update_author=True, assert_model=True, create_fallback=True, allow_private=False):
|
||||
def message_route_verify(self, message, message_dict, route,
|
||||
update_author=True, assert_model=True,
|
||||
create_fallback=True, allow_private=False,
|
||||
drop_alias=False):
|
||||
""" Verify route validity. Check and rules:
|
||||
1 - if thread_id -> check that document effectively exists; otherwise
|
||||
fallback on a message_new by resetting thread_id
|
||||
@@ -813,99 +829,82 @@ class MailThread(models.AbstractModel):
|
||||
check on alias parent document that the author is in the
|
||||
followers
|
||||
'partners': check that author_id id set
|
||||
|
||||
:param message: an email.message instance
|
||||
:param message_dict: dictionary of values that will be given to
|
||||
mail_message.create()
|
||||
:param route: route to check which is a tuple (model, thread_id,
|
||||
custom_values, uid, alias)
|
||||
:param update_author: update message_dict['author_id']. TDE TODO: move me
|
||||
:param assert_model: if an error occurs, tell whether to raise an error
|
||||
or just log a warning and try other processing or
|
||||
invalidate route
|
||||
:param create_fallback: if the route aims at updating a record and that
|
||||
record does not exists or does not support update
|
||||
either fallback on creating a new record in the
|
||||
same model or raise / warn
|
||||
:param allow_private: allow void model / thread_id routes, aka private
|
||||
discussions
|
||||
"""
|
||||
|
||||
assert isinstance(route, (list, tuple)), 'A route should be a list or a tuple'
|
||||
assert len(route) == 5, 'A route should contain 5 elements: model, thread_id, custom_values, uid, alias record'
|
||||
|
||||
message_id = message.get('Message-Id')
|
||||
email_from = decode_header(message, 'From')
|
||||
email_from = tools.decode_message_header(message, 'From')
|
||||
author_id = message_dict.get('author_id')
|
||||
model, thread_id, alias = route[0], route[1], route[4]
|
||||
record_set = None
|
||||
|
||||
def _create_bounce_email():
|
||||
bounce_to = decode_header(message, 'Return-Path') or email_from
|
||||
bounce_mail_values = {
|
||||
'body_html': '<div><p>Hello,</p>'
|
||||
'<p>The following email sent to %s cannot be accepted because this is '
|
||||
'a private email address. Only allowed people can contact us at this address.</p></div>'
|
||||
'<blockquote>%s</blockquote>' % (message.get('to'), message_dict.get('body')),
|
||||
'subject': 'Re: %s' % message.get('subject'),
|
||||
'email_to': bounce_to,
|
||||
'auto_delete': True,
|
||||
}
|
||||
bounce_from = self.env['ir.mail_server']._get_default_bounce_address()
|
||||
if bounce_from:
|
||||
bounce_mail_values['email_from'] = 'MAILER-DAEMON <%s>' % bounce_from
|
||||
self.env['mail.mail'].create(bounce_mail_values).send()
|
||||
|
||||
def _warn(message):
|
||||
_logger.info('Routing mail with Message-Id %s: route %s: %s',
|
||||
message_id, route, message)
|
||||
_generic_bounce_body_html = """<div>
|
||||
<p>Hello,</p>
|
||||
<p>The following email sent to %s cannot be accepted because this is a private email address.
|
||||
Only allowed people can contact us at this address.</p>
|
||||
</div><blockquote>%s</blockquote>""" % (message.get('to'), message_dict.get('body')),
|
||||
|
||||
# Wrong model
|
||||
if model and model not in self.env:
|
||||
if assert_model:
|
||||
assert model in self.env, 'Routing: unknown target model %s' % model
|
||||
_warn('unknown target model %s' % model)
|
||||
self._routing_warn(_('unknown target model %s') % model, '', message_id, route, assert_model)
|
||||
return ()
|
||||
|
||||
# Private message: should not contain any thread_id
|
||||
if not model and thread_id:
|
||||
if assert_model:
|
||||
if thread_id:
|
||||
raise ValueError('Routing: posting a message without model should be with a null res_id (private message), received %s.' % thread_id)
|
||||
_warn('posting a message without model should be with a null res_id (private message), received %s resetting thread_id' % thread_id)
|
||||
thread_id = 0
|
||||
# Private message: should have a parent_id (only answers)
|
||||
if not model and not message_dict.get('parent_id'):
|
||||
if assert_model:
|
||||
if not message_dict.get('parent_id'):
|
||||
raise ValueError('Routing: posting a message without model should be with a parent_id (private mesage).')
|
||||
_warn('posting a message without model should be with a parent_id (private mesage), skipping')
|
||||
return False
|
||||
# Private message
|
||||
if not model:
|
||||
# should not contain any thread_id
|
||||
if thread_id:
|
||||
self._routing_warn(_('posting a message without model should be with a null res_id (private message), received %s') % thread_id, _('resetting thread_id'), message_id, route, assert_model)
|
||||
thread_id = 0
|
||||
# should have a parent_id (only answers)
|
||||
if not message_dict.get('parent_id'):
|
||||
self._routing_warn(_('posting a message without model should be with a parent_id (private message)'), _('skipping'), message_id, route, assert_model)
|
||||
return False
|
||||
|
||||
if model and thread_id:
|
||||
record_set = self.env[model].browse(thread_id)
|
||||
elif model:
|
||||
record_set = self.env[model]
|
||||
|
||||
# Existing Document: check if exists; if not, fallback on create if allowed
|
||||
if thread_id and not record_set.exists():
|
||||
if create_fallback:
|
||||
_warn('reply to missing document (%s,%s), fall back on new document creation' % (model, thread_id))
|
||||
# Existing Document: check if exists and model accepts the mailgateway; if not, fallback on create if allowed
|
||||
if thread_id:
|
||||
if not record_set.exists() and create_fallback:
|
||||
self._routing_warn(_('reply to missing document (%s,%s), fall back on new document creation') % (model, thread_id), '', message_id, route, False)
|
||||
thread_id = None
|
||||
elif not hasattr(record_set, 'message_update') and create_fallback:
|
||||
self._routing_warn(_('model %s does not accept document update, fall back on document creation') % model, '', message_id, route, False)
|
||||
thread_id = None
|
||||
elif assert_model:
|
||||
# TDE FIXME: change assert to some real error
|
||||
assert record_set.exists(), 'Routing: reply to missing document (%s,%s)' % (model, thread_id)
|
||||
else:
|
||||
_warn('reply to missing document (%s,%s), skipping' % (model, thread_id))
|
||||
return False
|
||||
|
||||
# Existing Document: check model accepts the mailgateway
|
||||
if thread_id and model and not hasattr(record_set, 'message_update'):
|
||||
if create_fallback:
|
||||
_warn('model %s does not accept document update, fall back on document creation' % model)
|
||||
thread_id = None
|
||||
elif assert_model:
|
||||
assert hasattr(record_set, 'message_update'), 'Routing: model %s does not accept document update, crashing' % model
|
||||
else:
|
||||
_warn('model %s does not accept document update, skipping' % model)
|
||||
if not record_set.exists():
|
||||
self._routing_warn(_('reply to missing document (%s,%s)') % (model, thread_id), _('skipping'), message_id, route, assert_model)
|
||||
return False
|
||||
elif not hasattr(record_set, 'message_update'):
|
||||
self._routing_warn(_('model %s does not accept document update') % model, _('skipping'), message_id, route, assert_model)
|
||||
return False
|
||||
|
||||
# New Document: check model accepts the mailgateway
|
||||
if not thread_id and model and not hasattr(record_set, 'message_new'):
|
||||
if assert_model:
|
||||
if not hasattr(record_set, 'message_new'):
|
||||
raise ValueError(
|
||||
'Model %s does not accept document creation, crashing' % model
|
||||
)
|
||||
_warn('model %s does not accept document creation, skipping' % model)
|
||||
self._routing_warn(_('model %s does not accept document creation') % model, _('skipping'), message_id, route, assert_model)
|
||||
return False
|
||||
|
||||
# Update message author if asked
|
||||
# We do it now because we need it for aliases (contact settings)
|
||||
# Update message author if asked. We do it now because we need it for aliases (contact settings)
|
||||
if not author_id and update_author:
|
||||
author_ids = self.env['mail.thread']._find_partner_from_emails([email_from], res_model=model, res_id=thread_id)
|
||||
if author_ids:
|
||||
@@ -923,143 +922,185 @@ class MailThread(models.AbstractModel):
|
||||
set(partner.id for channel in obj.message_channel_ids for partner in channel.channel_partner_ids)
|
||||
)
|
||||
if not author_id or author_id not in accepted_partner_ids:
|
||||
_warn('alias %s restricted to internal followers, skipping' % alias.alias_name)
|
||||
_create_bounce_email()
|
||||
self._routing_warn(_('alias %s restricted to internal followers') % alias.alias_name, _('skipping'), message_id, route, False)
|
||||
self._routing_create_bounce_email(email_from, _generic_bounce_body_html, message)
|
||||
return False
|
||||
elif alias and alias.alias_contact == 'partners' and not author_id:
|
||||
_warn('alias %s does not accept unknown author, skipping' % alias.alias_name)
|
||||
_create_bounce_email()
|
||||
self._routing_warn(_('alias %s does not accept unknown author') % alias.alias_name, _('skipping'), message_id, route, False)
|
||||
self._routing_create_bounce_email(email_from, _generic_bounce_body_html, message)
|
||||
return False
|
||||
|
||||
if not model and not thread_id and not alias and not allow_private:
|
||||
return ()
|
||||
return False
|
||||
|
||||
return (model, thread_id, route[2], route[3], None if self._context.get('drop_alias', False) else route[4])
|
||||
return (model, thread_id, route[2], route[3], None if drop_alias else route[4])
|
||||
|
||||
@api.model
|
||||
def message_route(self, message, message_dict, model=None, thread_id=None, custom_values=None):
|
||||
"""Attempt to figure out the correct target model, thread_id,
|
||||
""" 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 dict message_dict: dictionary holding message variables
|
||||
: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, alias]
|
||||
* if the message replies to an existing thread by having a Message-Id
|
||||
that matches an existing mail_message.message_id, we take the original
|
||||
message model/thread_id pair and ignore custom_value as no creation will
|
||||
take place
|
||||
* if the message replies to an existing thread by having In-Reply-To or
|
||||
References matching odoo model/thread_id Message-Id and if this thread
|
||||
has messages without message_id, take this model/thread_id pair and
|
||||
ignore custom_value as no creation will take place (6.1 compatibility)
|
||||
* look for a mail.alias entry matching the message recipients and use the
|
||||
corresponding model, thread_id, custom_values and user_id. This could
|
||||
lead to a thread update or creation depending on the alias
|
||||
* fallback on provided ``model``, ``thread_id`` and ``custom_values``
|
||||
* raise an exception as no route has been found
|
||||
|
||||
:param string message: an email.message instance
|
||||
:param dict message_dict: dictionary holding parsed message variables
|
||||
: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 routes [(model, thread_id, custom_values, user_id, alias)]
|
||||
|
||||
:raises: ValueError, TypeError
|
||||
"""
|
||||
if not isinstance(message, Message):
|
||||
raise TypeError('message must be an email.message.Message at this point')
|
||||
MailMessage = self.env['mail.message']
|
||||
Alias = self.env['mail.alias']
|
||||
Alias, dest_aliases = self.env['mail.alias'], self.env['mail.alias']
|
||||
bounce_alias = self.env['ir.config_parameter'].get_param("mail.bounce.alias")
|
||||
fallback_model = model
|
||||
|
||||
# Get email.message.Message variables for future processing
|
||||
# get email.message.Message variables for future processing
|
||||
local_hostname = socket.gethostname()
|
||||
message_id = message.get('Message-Id')
|
||||
email_from = decode_header(message, 'From')
|
||||
email_to = decode_header(message, 'To')
|
||||
references = decode_header(message, 'References')
|
||||
in_reply_to = decode_header(message, 'In-Reply-To').strip()
|
||||
|
||||
# compute references to find if message is a reply to an existing thread
|
||||
references = tools.decode_message_header(message, 'References')
|
||||
in_reply_to = tools.decode_message_header(message, 'In-Reply-To').strip()
|
||||
thread_references = references or in_reply_to
|
||||
reply_match, reply_model, reply_thread_id, reply_hostname, reply_private = tools.email_references(thread_references)
|
||||
|
||||
# author and recipients
|
||||
email_from = tools.decode_message_header(message, 'From')
|
||||
email_from_localpart = (tools.email_split(email_from) or [''])[0].split('@', 1)[0].lower()
|
||||
email_to = tools.decode_message_header(message, 'To')
|
||||
email_to_localpart = (tools.email_split(email_to) or [''])[0].split('@', 1)[0].lower()
|
||||
|
||||
# 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 = ','.join([
|
||||
tools.decode_message_header(message, 'Delivered-To'),
|
||||
tools.decode_message_header(message, 'To'),
|
||||
tools.decode_message_header(message, 'Cc'),
|
||||
tools.decode_message_header(message, 'Resent-To'),
|
||||
tools.decode_message_header(message, 'Resent-Cc')])
|
||||
rcpt_tos_localparts = [e.split('@')[0] for e in tools.email_split(rcpt_tos)]
|
||||
|
||||
# 0. Verify whether this is a bounced email and use it to collect bounce data and update notifications for customers
|
||||
if bounce_alias and bounce_alias in email_to_localpart:
|
||||
# Bounce regex: typical form of bounce is bounce_alias+128-crm.lead-34@domain
|
||||
# group(1) = the mail ID; group(2) = the model (if any); group(3) = the record ID
|
||||
bounce_re = re.compile("%s\+(\d+)-?([\w.]+)?-?(\d+)?" % re.escape(bounce_alias), re.UNICODE)
|
||||
bounce_match = bounce_re.search(email_to)
|
||||
|
||||
if bounce_match:
|
||||
bounced_mail_id, bounced_model, bounced_thread_id = bounce_match.group(1), bounce_match.group(2), int(bounce_match.group(3))
|
||||
|
||||
email_part = next((part for part in message.walk() if part.get_content_type() == 'message/rfc822'), None)
|
||||
dsn_part = next((part for part in message.walk() if part.get_content_type() == 'message/delivery-status'), None)
|
||||
|
||||
partner, partner_address = self.env['res.partner'], False
|
||||
if dsn_part and len(dsn_part.get_payload()) > 1:
|
||||
dsn = dsn_part.get_payload()[1]
|
||||
final_recipient_data = tools.decode_message_header(dsn, 'Final-Recipient')
|
||||
partner_address = final_recipient_data.split(';', 1)[1].strip()
|
||||
if partner_address:
|
||||
partner = partner.sudo().search([('email', 'like', partner_address)])
|
||||
partner.message_receive_bounce(partner_address, partner, mail_id=bounced_mail_id)
|
||||
|
||||
mail_message = self.env['mail.message']
|
||||
if email_part:
|
||||
email = email_part.get_payload()[0]
|
||||
bounced_message_id = tools.mail_header_msgid_re.findall(tools.decode_message_header(email, 'Message-Id'))
|
||||
mail_message = MailMessage.sudo().search([('message_id', 'in', bounced_message_id)])
|
||||
|
||||
if partner and mail_message:
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', '=', mail_message.id),
|
||||
('res_partner_id', '=', partner.id)])
|
||||
notifications.write({
|
||||
'email_status': 'bounce'
|
||||
})
|
||||
|
||||
if bounced_model in self.env and hasattr(self.env[bounced_model], 'message_receive_bounce') and bounced_thread_id:
|
||||
self.env[bounced_model].browse(bounced_thread_id).message_receive_bounce(partner_address, partner, mail_id=bounced_mail_id)
|
||||
|
||||
_logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s: dest %s (partner %s)',
|
||||
email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id, partner_address, partner.id)
|
||||
return []
|
||||
|
||||
# 0. First check if this is a bounce message or not.
|
||||
# See http://datatracker.ietf.org/doc/rfc3462/?include_text=1
|
||||
# As all MTA does not respect this RFC (googlemail is one of them),
|
||||
# we also need to verify if the message come from "mailer-daemon"
|
||||
localpart = (tools.email_split(email_from) or [''])[0].split('@', 1)[0].lower()
|
||||
if message.get_content_type() == 'multipart/report' or localpart == 'mailer-daemon':
|
||||
_logger.info("Not routing bounce email from %s to %s with Message-Id %s",
|
||||
email_from, email_to, message_id)
|
||||
if message.get_content_type() == 'multipart/report' or email_from_localpart == 'mailer-daemon':
|
||||
_logger.info('Routing mail with Message-Id %s: not routing bounce email from %s to %s',
|
||||
message_id, email_from, email_to)
|
||||
return []
|
||||
|
||||
# 1. message is a reply to an existing message (exact match of message_id)
|
||||
ref_match = thread_references and tools.reference_re.search(thread_references)
|
||||
msg_references = mail_header_msgid_re.findall(thread_references)
|
||||
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
|
||||
if ref_match and mail_messages:
|
||||
model, thread_id = mail_messages.model, mail_messages.res_id
|
||||
alias = Alias.search([('alias_name', '=', (tools.email_split(email_to) or [''])[0].split('@', 1)[0].lower())])
|
||||
alias = alias[0] if alias else None
|
||||
route = self.with_context(drop_alias=True).message_route_verify(
|
||||
message, message_dict,
|
||||
(model, thread_id, custom_values, self._uid, alias),
|
||||
update_author=True, assert_model=False, create_fallback=True)
|
||||
if route:
|
||||
_logger.info(
|
||||
'Routing mail from %s to %s with Message-Id %s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
|
||||
email_from, email_to, message_id, model, thread_id, custom_values, self._uid)
|
||||
return [route]
|
||||
elif route is False:
|
||||
return []
|
||||
# 1. message is a reply to an existing message (exact match of message_id or compat-mode)
|
||||
if reply_model and reply_thread_id:
|
||||
other_alias = Alias.search([
|
||||
'&',
|
||||
('alias_name', '!=', False),
|
||||
('alias_name', '=', email_to_localpart)
|
||||
])
|
||||
if other_alias and other_alias.alias_model_id.model != reply_model:
|
||||
reply_match = False
|
||||
|
||||
# 2. message is a reply to an existign thread (6.1 compatibility)
|
||||
if ref_match:
|
||||
reply_thread_id = int(ref_match.group(1))
|
||||
reply_model = ref_match.group(2) or fallback_model
|
||||
reply_hostname = ref_match.group(3)
|
||||
local_hostname = socket.gethostname()
|
||||
if reply_match:
|
||||
compat_mode = False
|
||||
msg_references = tools.mail_header_msgid_re.findall(thread_references)
|
||||
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
|
||||
|
||||
# message is a reply to an existing thread (6.1 compatibility)
|
||||
# do not match forwarded emails from another OpenERP system (thread_id collision!)
|
||||
if local_hostname == reply_hostname:
|
||||
thread_id, model = reply_thread_id, reply_model
|
||||
if thread_id and model in self.env:
|
||||
record = self.env[model].browse(thread_id)
|
||||
compat_mail_msg_ids = MailMessage.search([
|
||||
('message_id', '=', False),
|
||||
('model', '=', model),
|
||||
('res_id', '=', thread_id)])
|
||||
if compat_mail_msg_ids and record.exists() and hasattr(record, 'message_update'):
|
||||
route = self.message_route_verify(
|
||||
message, message_dict,
|
||||
(model, thread_id, custom_values, self._uid, None),
|
||||
update_author=True, assert_model=True, create_fallback=True)
|
||||
if route:
|
||||
# parent is invalid for a compat-reply
|
||||
message_dict.pop('parent_id', None)
|
||||
_logger.info(
|
||||
'Routing mail from %s to %s with Message-Id %s: direct thread reply (compat-mode) to model: %s, thread_id: %s, custom_values: %s, uid: %s',
|
||||
email_from, email_to, message_id, model, thread_id, custom_values, self._uid)
|
||||
return [route]
|
||||
elif route is False:
|
||||
return []
|
||||
if not mail_messages and local_hostname == reply_hostname and reply_thread_id and reply_model in self.env:
|
||||
mail_messages = MailMessage.search([
|
||||
('message_id', '=', False),
|
||||
('model', '=', reply_model),
|
||||
('res_id', '=', reply_thread_id)])
|
||||
compat_mode = True
|
||||
|
||||
# 3. Reply to a private message
|
||||
if in_reply_to:
|
||||
mail_message_ids = MailMessage.search([
|
||||
('message_id', '=', in_reply_to),
|
||||
'!', ('message_id', 'ilike', 'reply_to')
|
||||
], limit=1)
|
||||
if mail_message_ids:
|
||||
if mail_messages:
|
||||
model, thread_id = mail_messages.model, mail_messages.res_id
|
||||
if not reply_private: # TDE note: not sure why private mode as no alias search, copying existing behavior
|
||||
dest_aliases = Alias.search([('alias_name', 'in', rcpt_tos_localparts)], limit=1)
|
||||
|
||||
# TDE Note: compat mode = without context key, why ? because
|
||||
route = self.message_route_verify(
|
||||
message, message_dict,
|
||||
(mail_message_ids.model, mail_message_ids.res_id, custom_values, self._uid, None),
|
||||
update_author=True, assert_model=True, create_fallback=True, allow_private=True)
|
||||
(model, thread_id, custom_values, self._uid, dest_aliases),
|
||||
update_author=True, assert_model=reply_private, create_fallback=True,
|
||||
allow_private=reply_private, drop_alias=True)
|
||||
if route:
|
||||
# TDE Note: compat mode: parent is invalid for a compat-reply
|
||||
# message_dict.pop('parent_id', None)
|
||||
# TDE note: add compat mode for compat mode in debug
|
||||
_logger.info(
|
||||
'Routing mail from %s to %s with Message-Id %s: direct reply to a private message: %s, custom_values: %s, uid: %s',
|
||||
email_from, email_to, message_id, mail_message_ids.id, custom_values, self._uid)
|
||||
'Routing mail from %s to %s with Message-Id %s%s: direct reply to msg: model: %s, thread_id: %s, custom_values: %s, uid: %s',
|
||||
email_from, email_to, message_id, compat_mode and ' (compat mode)' or '', model, thread_id, custom_values, self._uid)
|
||||
return [route]
|
||||
elif route is False:
|
||||
return []
|
||||
@@ -1068,20 +1109,11 @@ class MailThread(models.AbstractModel):
|
||||
message_dict.pop('parent_id', None)
|
||||
|
||||
# 4. 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 = \
|
||||
','.join([decode_header(message, 'Delivered-To'),
|
||||
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 tools.email_split(rcpt_tos)]
|
||||
if local_parts:
|
||||
aliases = Alias.search([('alias_name', 'in', local_parts)])
|
||||
if aliases:
|
||||
if rcpt_tos_localparts:
|
||||
dest_aliases = Alias.search([('alias_name', 'in', rcpt_tos_localparts)])
|
||||
if dest_aliases:
|
||||
routes = []
|
||||
for alias in aliases:
|
||||
for alias in dest_aliases:
|
||||
user_id = alias.alias_user_id.id
|
||||
if not user_id:
|
||||
# TDE note: this could cause crashes, because no clue that the user
|
||||
@@ -1103,24 +1135,16 @@ class MailThread(models.AbstractModel):
|
||||
return routes
|
||||
|
||||
# 5. Fallback to the provided parameters, if they work
|
||||
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)
|
||||
# Convert into int (bug spotted in 7.0 because of str)
|
||||
try:
|
||||
thread_id = int(thread_id)
|
||||
except:
|
||||
thread_id = False
|
||||
route = self.message_route_verify(
|
||||
message, message_dict,
|
||||
(fallback_model, thread_id, custom_values, self._uid, None),
|
||||
update_author=True, assert_model=True)
|
||||
if route:
|
||||
_logger.info(
|
||||
'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
|
||||
email_from, email_to, message_id, fallback_model, thread_id, custom_values, self._uid)
|
||||
return [route]
|
||||
if fallback_model:
|
||||
route = self.message_route_verify(
|
||||
message, message_dict,
|
||||
(fallback_model, thread_id, custom_values, self._uid, None),
|
||||
update_author=True, assert_model=True)
|
||||
if route:
|
||||
_logger.info(
|
||||
'Routing mail from %s to %s with Message-Id %s: fallback to model:%s, thread_id:%s, custom_values:%s, uid:%s',
|
||||
email_from, email_to, message_id, fallback_model, thread_id, custom_values, self._uid)
|
||||
return [route]
|
||||
|
||||
# ValueError if no routes found and if no bounce occured
|
||||
raise ValueError(
|
||||
@@ -1285,6 +1309,22 @@ class MailThread(models.AbstractModel):
|
||||
self.write(update_vals)
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def message_receive_bounce(self, email, partner, mail_id=None):
|
||||
"""Called by ``message_process`` when a bounce email (such as Undelivered
|
||||
Mail Returned to Sender) is received for an existing thread. The default
|
||||
behavior is to check is an integer ``message_bounce`` column exists.
|
||||
If it is the case, its content is incremented.
|
||||
|
||||
:param mail_id: ID of the sent email that bounced. It may not exist anymore
|
||||
but it could be usefull if the information was kept. This is
|
||||
used notably in mass mailing.
|
||||
:param RecordSet partner: partner matching the bounced email address, if any
|
||||
:param string email: email that caused the bounce """
|
||||
if 'message_bounce' in self._fields:
|
||||
for record in self:
|
||||
record.message_bounce = record.message_bounce + 1
|
||||
|
||||
def _message_extract_payload_postprocess(self, message, body, attachments):
|
||||
""" Perform some cleaning / postprocess in the body and attachments
|
||||
extracted from the email. Note that this processing is specific to the
|
||||
@@ -1355,7 +1395,7 @@ class MailThread(models.AbstractModel):
|
||||
# RFC2231
|
||||
filename = email.utils.collapse_rfc2231_value(filename).strip()
|
||||
else:
|
||||
filename = decode(filename)
|
||||
filename = tools.decode_smtp_header(filename)
|
||||
encoding = part.get_content_charset() # None if attachment
|
||||
|
||||
# 0) Inline Attachments -> attachments, with a third part in the tuple to match cid / attachment
|
||||
@@ -1432,19 +1472,19 @@ class MailThread(models.AbstractModel):
|
||||
msg_dict['message_id'] = message_id
|
||||
|
||||
if message.get('Subject'):
|
||||
msg_dict['subject'] = decode(message.get('Subject'))
|
||||
msg_dict['subject'] = tools.decode_smtp_header(message.get('Subject'))
|
||||
|
||||
# Envelope fields not stored in mail.message but made available for message_new()
|
||||
msg_dict['from'] = decode(message.get('from'))
|
||||
msg_dict['to'] = decode(message.get('to'))
|
||||
msg_dict['cc'] = decode(message.get('cc'))
|
||||
msg_dict['email_from'] = decode(message.get('from'))
|
||||
msg_dict['from'] = tools.decode_smtp_header(message.get('from'))
|
||||
msg_dict['to'] = tools.decode_smtp_header(message.get('to'))
|
||||
msg_dict['cc'] = tools.decode_smtp_header(message.get('cc'))
|
||||
msg_dict['email_from'] = tools.decode_smtp_header(message.get('from'))
|
||||
partner_ids = self._message_find_partners(message, ['To', 'Cc'])
|
||||
msg_dict['partner_ids'] = [(4, partner_id) for partner_id in partner_ids]
|
||||
|
||||
if message.get('Date'):
|
||||
try:
|
||||
date_hdr = decode(message.get('Date'))
|
||||
date_hdr = tools.decode_smtp_header(message.get('Date'))
|
||||
parsed_date = dateutil.parser.parse(date_hdr, fuzzy=True)
|
||||
if parsed_date.utcoffset() is None:
|
||||
# naive datetime, so we arbitrarily decide to make it
|
||||
@@ -1461,13 +1501,13 @@ class MailThread(models.AbstractModel):
|
||||
msg_dict['date'] = stored_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
|
||||
if message.get('In-Reply-To'):
|
||||
parent_ids = self.env['mail.message'].search([('message_id', '=', decode(message['In-Reply-To'].strip()))], limit=1)
|
||||
parent_ids = self.env['mail.message'].search([('message_id', '=', tools.decode_smtp_header(message['In-Reply-To'].strip()))], limit=1)
|
||||
if parent_ids:
|
||||
msg_dict['parent_id'] = parent_ids.id
|
||||
msg_dict['internal'] = parent_ids.subtype_id and parent_ids.subtype_id.internal or False
|
||||
|
||||
if message.get('References') and 'parent_id' not in msg_dict:
|
||||
msg_list = mail_header_msgid_re.findall(decode(message['References']))
|
||||
msg_list = tools.mail_header_msgid_re.findall(tools.decode_smtp_header(message['References']))
|
||||
parent_ids = self.env['mail.message'].search([('message_id', 'in', [x.strip() for x in msg_list])], limit=1)
|
||||
if parent_ids:
|
||||
msg_dict['parent_id'] = parent_ids.id
|
||||
@@ -1518,7 +1558,7 @@ class MailThread(models.AbstractModel):
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def _find_partner_from_emails(self, emails, res_model=None, res_id=None, check_followers=True, force_create=False):
|
||||
def _find_partner_from_emails(self, emails, res_model=None, res_id=None, check_followers=True, force_create=False, exclude_aliases=True):
|
||||
""" Utility method to find partners from email addresses. The rules are :
|
||||
1 - check in document (model | self, id) followers
|
||||
2 - try to find a matching partner that is also an user
|
||||
@@ -1530,6 +1570,9 @@ class MailThread(models.AbstractModel):
|
||||
is used.
|
||||
:param boolean check_followers: check in document followers
|
||||
:param boolean force_create: create a new partner if not found
|
||||
:param boolean exclude_aliases: do not try to find a partner that could match an alias. Normally aliases
|
||||
should not be used as partner emails but it could be the case due to some
|
||||
strange manipulation
|
||||
"""
|
||||
if res_model is None:
|
||||
res_model = self._name
|
||||
@@ -1551,12 +1594,14 @@ class MailThread(models.AbstractModel):
|
||||
if not email_address:
|
||||
partner_ids.append(partner_id)
|
||||
continue
|
||||
if exclude_aliases and self.env['mail.alias'].search([('alias_name', 'ilike', email_address)], limit=1):
|
||||
partner_ids.append(partner_id)
|
||||
continue
|
||||
|
||||
email_address = email_address[0]
|
||||
# first try: check in document's followers
|
||||
for follower in followers:
|
||||
if follower.email == email_address:
|
||||
partner_id = follower.id
|
||||
break
|
||||
partner_id = next((partner.id for partner in followers if partner.email == email_address), False)
|
||||
|
||||
# second try: check in partners that are also users
|
||||
# Escape special SQL characters in email_address to avoid invalid matches
|
||||
email_address = (email_address.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_'))
|
||||
@@ -1567,8 +1612,7 @@ class MailThread(models.AbstractModel):
|
||||
if not partners:
|
||||
# if no match with addr-spec, attempt substring match within name-addr pair
|
||||
partners = Users.search([('email', 'ilike', email_brackets)], limit=1).mapped('partner_id')
|
||||
if partners:
|
||||
partner_id = partners[0].id
|
||||
partner_id = partners.id
|
||||
# third try: check in partners
|
||||
if not partner_id:
|
||||
# exact, case-insensitive match
|
||||
@@ -1576,8 +1620,7 @@ class MailThread(models.AbstractModel):
|
||||
if not partners:
|
||||
# if no match with addr-spec, attempt substring match within name-addr pair
|
||||
partners = Partner.search([('email', 'ilike', email_brackets)], limit=1)
|
||||
if partners:
|
||||
partner_id = partners[0].id
|
||||
partner_id = partners.id
|
||||
if not partner_id and force_create:
|
||||
partner_id = self.env['res.partner'].name_create(contact)[0]
|
||||
partner_ids.append(partner_id)
|
||||
@@ -1973,10 +2016,17 @@ class MailThread(models.AbstractModel):
|
||||
"""
|
||||
if not partner_ids:
|
||||
return
|
||||
for record_id in self.ids:
|
||||
for record in self:
|
||||
record.message_post_with_view(
|
||||
'mail.message_user_assigned',
|
||||
composition_mode='mass_mail',
|
||||
partner_ids=[(4, pid) for pid in partner_ids],
|
||||
auto_delete=True,
|
||||
auto_delete_message=True,
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
messages = self.env['mail.message'].sudo().search([
|
||||
('model', '=', self._name),
|
||||
('res_id', '=', record_id),
|
||||
('res_id', '=', record.id),
|
||||
('subtype_id', '!=', False),
|
||||
('subtype_id.internal', '=', False)], limit=1)
|
||||
if messages:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo import _, api, fields, models, registry, SUPERUSER_ID
|
||||
from odoo.osv import expression
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -86,7 +86,6 @@ class Partner(models.Model):
|
||||
'tracking': tracking,
|
||||
'is_discussion': is_discussion,
|
||||
'subtype': message.subtype_id,
|
||||
'is_accessible': message._is_accessible()
|
||||
}
|
||||
|
||||
@api.model
|
||||
@@ -133,8 +132,19 @@ class Partner(models.Model):
|
||||
emails |= self.env['mail.mail'].create(create_values)
|
||||
return emails, recipients_nbr
|
||||
|
||||
@api.model
|
||||
def _notify_udpate_notifications(self, emails):
|
||||
for email in emails:
|
||||
notifications = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', '=', email.mail_message_id.id),
|
||||
('res_partner_id', 'in', email.recipient_ids.ids)])
|
||||
notifications.write({
|
||||
'is_email': True,
|
||||
'email_status': 'ready',
|
||||
})
|
||||
|
||||
@api.multi
|
||||
def _notify(self, message, force_send=False, user_signature=True):
|
||||
def _notify(self, message, force_send=False, send_after_commit=True, user_signature=True):
|
||||
# TDE TODO: model-dependant ? (like customer -> always email ?)
|
||||
message_sudo = message.sudo()
|
||||
email_channels = message.channel_ids.filtered(lambda channel: channel.email_send)
|
||||
@@ -143,14 +153,19 @@ class Partner(models.Model):
|
||||
('id', 'in', self.ids),
|
||||
('channel_ids', 'in', email_channels.ids),
|
||||
('email', '!=', message_sudo.author_id and message_sudo.author_id.email or message.email_from),
|
||||
('notify_email', '!=', 'none')])._notify_by_email(message, force_send=force_send, user_signature=user_signature)
|
||||
('notify_email', '!=', 'none')])._notify_by_email(message, force_send=force_send, send_after_commit=send_after_commit, user_signature=user_signature)
|
||||
self._notify_by_chat(message)
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def _notify_by_email(self, message, force_send=False, user_signature=True):
|
||||
def _notify_by_email(self, message, force_send=False, send_after_commit=True, user_signature=True):
|
||||
""" Method to send email linked to notified messages. The recipients are
|
||||
the recordset on which this method is called. """
|
||||
the recordset on which this method is called.
|
||||
|
||||
:param boolean force_send: send notification emails now instead of letting the scheduler handle the email queue
|
||||
:param boolean send_after_commit: send notification emails after the transaction end instead of durign the
|
||||
transaction; this option is used only if force_send is True
|
||||
:param user_signature: add current user signature to notification emails """
|
||||
if not self.ids:
|
||||
return True
|
||||
|
||||
@@ -166,7 +181,6 @@ class Partner(models.Model):
|
||||
base_template_ctx['signature'] = False
|
||||
base_mail_values = self._notify_prepare_email_values(message)
|
||||
|
||||
|
||||
# classify recipients: actions / no action
|
||||
if message.model and message.res_id and hasattr(self.env[message.model], '_message_notification_recipients'):
|
||||
recipients = self.env[message.model].browse(message.res_id)._message_notification_recipients(message, self)
|
||||
@@ -185,6 +199,9 @@ class Partner(models.Model):
|
||||
fol_values = template_fol.generate_email(message.id, fields=['body_html', 'subject'])
|
||||
# send email
|
||||
new_emails, new_recipients_nbr = self._notify_send(fol_values['body'], fol_values['subject'], recipient_template_values['followers'], **base_mail_values)
|
||||
# update notifications
|
||||
self._notify_udpate_notifications(new_emails)
|
||||
|
||||
emails |= new_emails
|
||||
recipients_nbr += new_recipients_nbr
|
||||
if recipient_template_values['not_followers']:
|
||||
@@ -196,6 +213,9 @@ class Partner(models.Model):
|
||||
not_values = template_not.generate_email(message.id, fields=['body_html', 'subject'])
|
||||
# send email
|
||||
new_emails, new_recipients_nbr = self._notify_send(not_values['body'], not_values['subject'], recipient_template_values['not_followers'], **base_mail_values)
|
||||
# update notifications
|
||||
self._notify_udpate_notifications(new_emails)
|
||||
|
||||
emails |= new_emails
|
||||
recipients_nbr += new_recipients_nbr
|
||||
|
||||
@@ -204,9 +224,24 @@ class Partner(models.Model):
|
||||
# 2. do not send emails immediately if the registry is not loaded,
|
||||
# to prevent sending email during a simple update of the database
|
||||
# using the command-line.
|
||||
test_mode = getattr(threading.currentThread(), 'testing', False)
|
||||
if force_send and recipients_nbr < recipients_max and \
|
||||
(not self.pool._init or getattr(threading.currentThread(), 'testing', False)):
|
||||
emails.send()
|
||||
(not self.pool._init or test_mode):
|
||||
email_ids = emails.ids
|
||||
dbname = self.env.cr.dbname
|
||||
|
||||
def send_notifications():
|
||||
db_registry = registry(dbname)
|
||||
with db_registry.cursor() as cr:
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
env['mail.mail'].browse(email_ids).send()
|
||||
|
||||
# unless asked specifically, send emails after the transaction to
|
||||
# avoid side effects due to emails being sent while the transaction fails
|
||||
if not test_mode and send_after_commit:
|
||||
self._cr.after('commit', send_notifications)
|
||||
else:
|
||||
emails.send()
|
||||
|
||||
return True
|
||||
|
||||
@@ -226,7 +261,7 @@ class Partner(models.Model):
|
||||
self.env.cr.execute("""
|
||||
SELECT count(*) as needaction_count
|
||||
FROM mail_message_res_partner_needaction_rel R
|
||||
WHERE R.res_partner_id = %s """, (self.env.user.partner_id.id,))
|
||||
WHERE R.res_partner_id = %s AND (R.is_read = false OR R.is_read IS NULL)""", (self.env.user.partner_id.id,))
|
||||
return self.env.cr.dictfetchall()[0].get('needaction_count')
|
||||
_logger.error('Call to needaction_count without partner_id')
|
||||
return 0
|
||||
|
||||
@@ -14,11 +14,15 @@ class Users(models.Model):
|
||||
group, and the user. This is done by overriding the write method.
|
||||
"""
|
||||
_name = 'res.users'
|
||||
_inherit = ['mail.alias.mixin', 'res.users']
|
||||
_inherit = ['res.users']
|
||||
|
||||
alias_id = fields.Many2one('mail.alias', 'Alias', ondelete="restrict", required=True,
|
||||
alias_id = fields.Many2one('mail.alias', 'Alias', ondelete="set null", required=False,
|
||||
help="Email address internally associated with this user. Incoming "\
|
||||
"emails will appear in the user's notifications.", copy=False, auto_join=True)
|
||||
alias_contact = fields.Selection([
|
||||
('everyone', 'Everyone'),
|
||||
('partners', 'Authenticated Partners'),
|
||||
('followers', 'Followers only')], string='Alias Contact Security', related='alias_id.alias_contact')
|
||||
|
||||
def __init__(self, pool, cr):
|
||||
""" Override of __init__ to add access rights on notification_email_send
|
||||
@@ -31,17 +35,9 @@ class Users(models.Model):
|
||||
type(self).SELF_WRITEABLE_FIELDS.extend(['notify_email'])
|
||||
# duplicate list to avoid modifying the original reference
|
||||
type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS)
|
||||
type(self).SELF_READABLE_FIELDS.extend(['notify_email', 'alias_domain', 'alias_name'])
|
||||
type(self).SELF_READABLE_FIELDS.extend(['notify_email'])
|
||||
return init_res
|
||||
|
||||
def get_alias_model_name(self, vals):
|
||||
return self._name
|
||||
|
||||
def get_alias_values(self):
|
||||
values = super(Users, self).get_alias_values()
|
||||
values['alias_force_thread_id'] = self.id
|
||||
return values
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
if not values.get('login', False):
|
||||
@@ -65,13 +61,6 @@ class Users(models.Model):
|
||||
self.env['mail.channel'].search([('group_ids', 'in', user_group_ids)])._subscribe_users()
|
||||
return write_res
|
||||
|
||||
@api.multi
|
||||
def copy_data(self, default=None):
|
||||
data = super(Users, self).copy_data(default)[0]
|
||||
if data and data.get('alias_name'):
|
||||
data['alias_name'] = data['login']
|
||||
return [data]
|
||||
|
||||
def _create_welcome_message(self):
|
||||
self.ensure_one()
|
||||
if not self.has_group('base.group_user'):
|
||||
|
||||
@@ -10,6 +10,9 @@ access_mail_followers_all,mail.followers.all,model_mail_followers,,1,0,0,0
|
||||
access_mail_followers_portal,mail.followers.portal,model_mail_followers,base.group_portal,1,1,1,0
|
||||
access_mail_followers_user,mail.followers.user,model_mail_followers,base.group_user,1,1,1,0
|
||||
access_mail_followers_system,mail.followers.system,model_mail_followers,base.group_system,1,1,1,1
|
||||
access_mail_notification_portal,mail.notification.portal,model_mail_notification,base.group_portal,1,0,0,0
|
||||
access_mail_notification_user,mail.notification.user,model_mail_notification,base.group_user,1,1,1,0
|
||||
access_mail_notification_system, mail.notification.system,model_mail_notification,base.group_system,1,1,1,1
|
||||
access_mail_channel_all,mail.group.all,model_mail_channel,,1,0,0,0
|
||||
access_mail_channel_user,mail.group.user,model_mail_channel,base.group_user,1,1,1,1
|
||||
access_mail_channel_partner_public,mail.channel.partner.public,model_mail_channel_partner,base.group_public,1,0,0,0
|
||||
@@ -30,3 +33,4 @@ access_mail_template,mail.template,model_mail_template,base.group_user,1,1,1,0
|
||||
access_mail_template_system,mail.template_system,model_mail_template,base.group_system,1,1,1,1
|
||||
access_mail_shortcode,mail.shortcode,model_mail_shortcode,base.group_user,1,1,1,1
|
||||
access_mail_shortcode_portal,mail.shortcode.portal,model_mail_shortcode,base.group_portal,1,0,0,0
|
||||
access_mail_test_all,mail.test.all,model_mail_test,base.group_user,1,1,1,1
|
||||
|
@@ -23,6 +23,16 @@
|
||||
<field name="perm_read" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_mail_notifications_group_user" model="ir.rule">
|
||||
<field name="name">mail.notifications: group_user: write its own entries</field>
|
||||
<field name="model_id" ref="model_mail_notification"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="domain_force">[('res_partner_id', '=', user.partner_id.id)]</field>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
<field name="perm_read" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="mail_message_subtype_rule_public" model="ir.rule">
|
||||
<field name="name">mail.message.subtype: portal/public: read public subtypes</field>
|
||||
<field name="model_id" ref="model_mail_message_subtype"/>
|
||||
|
||||
@@ -132,6 +132,8 @@ function make_message (data) {
|
||||
attachment_ids: data.attachment_ids || [],
|
||||
subject: data.subject,
|
||||
email_from: data.email_from,
|
||||
customer_email_status: data.customer_email_status,
|
||||
customer_email_data: data.customer_email_data,
|
||||
record_name: data.record_name,
|
||||
tracking_value_ids: data.tracking_value_ids,
|
||||
channel_ids: data.channel_ids,
|
||||
|
||||
@@ -32,6 +32,7 @@ return Widget.extend({
|
||||
autofocus: true,
|
||||
display_stars: true,
|
||||
display_reply_icon: false,
|
||||
display_email_icon: false,
|
||||
placeholder: _t("Say something"),
|
||||
input_less: false,
|
||||
});
|
||||
|
||||
@@ -491,6 +491,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
||||
display_empty_channel: !messages.length && !this.domain.length,
|
||||
display_no_match: !messages.length && this.domain.length,
|
||||
display_subject: this.channel.mass_mailing || this.channel.id === "channel_inbox",
|
||||
display_email_icon: false,
|
||||
display_reply_icon: true,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -72,6 +72,7 @@ var Thread = Widget.extend({
|
||||
display_document_link: true,
|
||||
display_avatar: true,
|
||||
squash_close_messages: true,
|
||||
display_email_icon: true,
|
||||
display_reply_icon: false,
|
||||
});
|
||||
this.expanded_msg_ids = [];
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply {
|
||||
|
||||
i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply, i.o_thread_message_email {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
&.fa-star {
|
||||
@@ -84,7 +85,7 @@
|
||||
opacity: @mail-thread-side-date-opacity;
|
||||
}
|
||||
|
||||
i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply {
|
||||
i.o_thread_message_star, i.o_thread_message_needaction, i.o_thread_message_reply, i.o_thread_message_email {
|
||||
opacity: @mail-thread-icon-opacity;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
@@ -146,9 +147,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_message_star, .o_thread_message_needaction, .o_thread_message_reply {
|
||||
.o_thread_message_star, .o_thread_message_needaction, .o_thread_message_reply, .o_thread_message_email {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
i.o_thread_message_email {
|
||||
&.o_thread_message_email_ready {
|
||||
color: grey;
|
||||
}
|
||||
&.o_thread_message_email_exception {
|
||||
color: red;
|
||||
opacity: 1;
|
||||
}
|
||||
&.o_thread_message_email_bounce {
|
||||
color: red;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_tooltip_container {
|
||||
display: inline;
|
||||
position: relative;
|
||||
|
||||
.o_thread_tooltip_content {
|
||||
top: 10px;
|
||||
background: @gray-lighter;
|
||||
padding: 4px;
|
||||
border: solid @gray-lighter 1px;
|
||||
border-radius: 5px;
|
||||
color: gray;
|
||||
width: 300px;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
transition: opacity 0.5s;
|
||||
i.fa-exclamation {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
.o_thread_tooltip:hover + .o_thread_tooltip_content {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_thread_title {
|
||||
|
||||
@@ -94,6 +94,21 @@
|
||||
<t t-if="message.origin_id && (message.origin_id !== options.channel_id)">
|
||||
(from <a t-att-data-oe-id="message.origin_id" href="#">#<t t-esc="message.origin_name"/></a>)
|
||||
</t>
|
||||
<span t-if="options.display_email_icon && message.customer_email_data.length" class="o_thread_tooltip_container">
|
||||
<i t-att-class="'o_thread_tooltip o_thread_message_email o_thread_message_email_' + message.customer_email_status + ' fa fa-envelope-o'"/>
|
||||
<span class="o_thread_tooltip_content">
|
||||
<t t-foreach="message.customer_email_data" t-as="customer">
|
||||
<span>
|
||||
<t t-if="customer[2] == 'sent'"><i class='fa fa-check'/></t>
|
||||
<t t-if="customer[2] == 'bounce'"><i class='fa fa-exclamation'/></t>
|
||||
<t t-if="customer[2] == 'exception'"><i class='fa fa-exclamation'/></t>
|
||||
<t t-if="customer[2] == 'ready'"><i class='fa fa-send-o'/></t>
|
||||
<t t-esc="customer[1]"/>
|
||||
</span>
|
||||
<br />
|
||||
</t>
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
<i t-if="options.display_stars && !message.is_system_notification"
|
||||
t-att-class="'fa fa-lg o_thread_message_star ' + (message.is_starred ? 'fa-star' : 'fa-star-o')"
|
||||
|
||||
@@ -56,7 +56,6 @@ class TestMail(common.SavepointCase):
|
||||
cls.user_employee = Users.create({
|
||||
'name': 'Ernest Employee',
|
||||
'login': 'ernest',
|
||||
'alias_name': 'ernest',
|
||||
'email': 'e.e@example.com',
|
||||
'signature': '--\nErnest',
|
||||
'notify_email': 'always',
|
||||
@@ -64,7 +63,6 @@ class TestMail(common.SavepointCase):
|
||||
cls.user_public = Users.create({
|
||||
'name': 'Bert Tartignole',
|
||||
'login': 'bert',
|
||||
'alias_name': 'bert',
|
||||
'email': 'b.t@example.com',
|
||||
'signature': 'SignBert',
|
||||
'notify_email': 'always',
|
||||
@@ -72,7 +70,6 @@ class TestMail(common.SavepointCase):
|
||||
cls.user_portal = Users.create({
|
||||
'name': 'Chell Gladys',
|
||||
'login': 'chell',
|
||||
'alias_name': 'chell',
|
||||
'email': 'chell@gladys.portal',
|
||||
'signature': 'SignChell',
|
||||
'notify_email': 'always',
|
||||
|
||||
@@ -9,22 +9,8 @@ class TestMailFeatures(TestMail):
|
||||
# TDE TODO: tests on the redirection controller
|
||||
|
||||
def test_alias_setup(self):
|
||||
Users = self.env['res.users'].with_context({'no_reset_password': True})
|
||||
|
||||
user_valentin = Users.create({
|
||||
'name': 'Valentin Cognito', 'email': 'valentin.cognito@gmail.com',
|
||||
'login': 'valentin.cognito', 'alias_name': 'valentin.cognito'})
|
||||
self.assertEqual(user_valentin.alias_name, user_valentin.login, "Login should be used as alias")
|
||||
|
||||
user_pagan = Users.create({
|
||||
'name': 'Pagan Le Marchant', 'email': 'plmarchant@gmail.com',
|
||||
'login': 'plmarchant@gmail.com', 'alias_name': 'plmarchant@gmail.com'})
|
||||
self.assertEqual(user_pagan.alias_name, 'plmarchant', "If login is an email, the alias should keep only the local part")
|
||||
|
||||
user_barty = Users.create({
|
||||
'name': 'Bartholomew Ironside', 'email': 'barty@gmail.com',
|
||||
'login': 'b4r+_#_R3wl$$', 'alias_name': 'b4r+_#_R3wl$$'})
|
||||
self.assertEqual(user_barty.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens')
|
||||
alias = self.env['mail.alias'].with_context(alias_model_name='mail.channel').create({'alias_name': 'b4r+_#_R3wl$$'})
|
||||
self.assertEqual(alias.alias_name, 'b4r+_-_r3wl-', 'Disallowed chars should be replaced by hyphens')
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_mail')
|
||||
def test_needaction(self):
|
||||
@@ -138,8 +124,8 @@ class TestMessagePost(TestMail):
|
||||
|
||||
# notification emails: followers + recipients - notify_email=none (partner_2) - author (user_employee)
|
||||
self.assertEqual(set(m['email_from'] for m in self._mails),
|
||||
set(['%s <%s@%s>' % (self.user_employee.name, self.user_employee.alias_name, _domain)]),
|
||||
'message_post: notification email wrong email_from: should use alias of sender')
|
||||
set(['%s <%s>' % (self.user_employee.name, self.user_employee.email)]),
|
||||
'message_post: notification email wrong email_from: should use sender email')
|
||||
self.assertEqual(set(m['email_to'][0] for m in self._mails),
|
||||
set(['%s <%s>' % (self.partner_1.name, self.partner_1.email),
|
||||
'%s <%s>' % (self.env.user.name, self.env.user.email)]))
|
||||
|
||||
@@ -298,6 +298,14 @@ class TestMailgateway(TestMail):
|
||||
'alias_model_id': self.mail_channel_model.id,
|
||||
'alias_contact': 'everyone'})
|
||||
|
||||
# test@.. will cause the creation of new mail.test
|
||||
self.mail_test_model = self.env['ir.model'].search([('model', '=', 'mail.test')], limit=1)
|
||||
self.alias_2 = self.env['mail.alias'].create({
|
||||
'alias_name': 'test',
|
||||
'alias_user_id': False,
|
||||
'alias_model_id': self.mail_test_model.id,
|
||||
'alias_contact': 'everyone'})
|
||||
|
||||
# Set a first message on public group to test update and hierarchy
|
||||
self.fake_email = self.env['mail.message'].create({
|
||||
'model': 'mail.channel',
|
||||
@@ -512,7 +520,7 @@ class TestMailgateway(TestMail):
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models')
|
||||
def test_message_process_in_reply_to(self):
|
||||
""" Incoming email using in-rely-to should go into the right destination event with a wrong destination """
|
||||
""" Incoming email using in-rely-to should go into the right destination even with a wrong destination """
|
||||
self.format_and_process(
|
||||
MAIL_TEMPLATE, email_from='valid.other@gmail.com',
|
||||
msg_id='<1198923581.41972151344608186800.JavaMail.diff1@agrolait.com>',
|
||||
@@ -524,7 +532,7 @@ class TestMailgateway(TestMail):
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models')
|
||||
def test_message_process_references(self):
|
||||
""" Incoming email using in-rely-to should go into the right destination event with a wrong destination """
|
||||
""" Incoming email using references should go into the right destination even with a wrong destination """
|
||||
self.format_and_process(
|
||||
MAIL_TEMPLATE, to='erroneous@example.com',
|
||||
extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id,
|
||||
@@ -533,6 +541,34 @@ class TestMailgateway(TestMail):
|
||||
self.assertEqual(len(self.group_public.message_ids), 2, 'message_process: group should contain one new message')
|
||||
self.assertEqual(len(self.fake_email.child_ids), 1, 'message_process: new message should be children of the existing one')
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models')
|
||||
def test_message_process_references_forward(self):
|
||||
""" Incoming email using references but with alias forward should not go into references destination """
|
||||
res_test = self.format_and_process(
|
||||
MAIL_TEMPLATE, to='test@example.com',
|
||||
subject='My Dear Forward',
|
||||
extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id,
|
||||
msg_id='<1198923581.41972151344608186800.JavaMail.4@agrolait.com>',
|
||||
target_model='mail.test')
|
||||
|
||||
self.assertEqual(len(self.group_public.message_ids), 1, 'message_process: group should not contain new message')
|
||||
self.assertEqual(len(self.fake_email.child_ids), 0, 'message_process: original email should not contain childs')
|
||||
self.assertEqual(res_test.name, 'My Dear Forward')
|
||||
self.assertEqual(len(res_test.message_ids), 1)
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models')
|
||||
def test_message_process_references_forward_cc(self):
|
||||
""" Incoming email using references but with alias forward should not go into references destination """
|
||||
self.format_and_process(
|
||||
MAIL_TEMPLATE, to='erroneous@example.com', cc='test@example.com',
|
||||
subject='My Dear Forward',
|
||||
extra='References: <2233@a.com>\r\n\t<3edss_dsa@b.com> %s' % self.fake_email.message_id,
|
||||
msg_id='<1198923581.41972151344608186800.JavaMail.4@agrolait.com>',
|
||||
target_model='mail.test')
|
||||
|
||||
self.assertEqual(len(self.group_public.message_ids), 2, 'message_process: group should contain one new message')
|
||||
self.assertEqual(len(self.fake_email.child_ids), 1, 'message_process: new message should be children of the existing one')
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models')
|
||||
def test_message_process_model_res_id(self):
|
||||
""" Incoming email with ref holding model / res_id but that does not match any message in the thread: must raise since OpenERP saas-3 """
|
||||
@@ -560,15 +596,8 @@ class TestMailgateway(TestMail):
|
||||
to='noone@example.com>', subject='spam',
|
||||
extra='In-Reply-To: <12321321-openerp-%d-mail.channel@neighbor.com>' % frog_groups.id)
|
||||
|
||||
# 4. Subject contains [<ID>] + model passed to message+process -> only attached to group, but not to mail (not in msg1.child_ids)
|
||||
self.format_and_process(
|
||||
MAIL_TEMPLATE,
|
||||
to='erroneous@example.com', email_from='other6@gmail.com',
|
||||
extra='', subject='Re: [%s] 1' % self.group_public.id, model='mail.channel',
|
||||
msg_id='<1198923581.41972151344608186803.JavaMail.5@agrolait.com>')
|
||||
|
||||
# Test created messages
|
||||
self.assertEqual(len(self.group_public.message_ids), 3, 'message_process: group should contain 6 messages')
|
||||
self.assertEqual(len(self.group_public.message_ids), 2, 'message_process: group should contain 6 messages')
|
||||
self.assertEqual(len(self.group_public.message_ids[0].child_ids), 0, 'message_process: msg1 should not have children')
|
||||
|
||||
@mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models')
|
||||
@@ -700,21 +729,42 @@ class TestMailgateway(TestMail):
|
||||
self.assertEqual(msg.needaction_partner_ids, self.user_employee.partner_id | self.env.user.partner_id,
|
||||
'message_post: private discussion: incorrect notified recipients when replying')
|
||||
|
||||
# Do bert forward it to an alias
|
||||
msg = self.env['mail.message'].browse(msg1.id)
|
||||
@mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models', 'openerp.addons.mail.models.mail_mail')
|
||||
def test_forward_parent_id(self):
|
||||
msg = self.group_pigs.sudo(self.user_employee).message_post(no_auto_thread=True, subtype='mail.mt_comment')
|
||||
self.assertNotIn(msg.model, msg.message_id)
|
||||
self.assertNotIn('-%d-' % msg.res_id, msg.message_id)
|
||||
self.assertIn('reply_to', msg.message_id)
|
||||
|
||||
# forward it to a new thread AND an existing thread
|
||||
for i, to in enumerate(['groups', 'public']):
|
||||
fw_msg_id = '<THIS.IS.A.FW.MESSAGE.%d@bert.fr>' % (i,)
|
||||
fw_message = MAIL_TEMPLATE.format(to='%s@example.com' % (to,),
|
||||
cc='',
|
||||
subject='FW: Re: 1',
|
||||
email_from='b.t@example.com',
|
||||
extra='References: %s' % msg.message_id,
|
||||
msg_id=fw_msg_id)
|
||||
self.env['mail.thread'].message_process(None, fw_message)
|
||||
fw_msg_id = '<THIS.IS.A.FW.MESSAGE.1@bert.fr>'
|
||||
fw_message = MAIL_TEMPLATE.format(to='groups@example.com',
|
||||
cc='',
|
||||
subject='FW: Re: 1',
|
||||
email_from='b.t@example.com',
|
||||
extra='In-Reply-To: %s' % msg.message_id,
|
||||
msg_id=fw_msg_id)
|
||||
self.env['mail.thread'].message_process(None, fw_message)
|
||||
msg_fw = self.env['mail.message'].search([('message_id', '=', fw_msg_id)])
|
||||
self.assertEqual(len(msg_fw), 1)
|
||||
channel = self.env['mail.channel'].search([('name', "=", msg_fw.subject)])
|
||||
self.assertEqual(len(channel), 1)
|
||||
self.assertEqual(msg_fw.model, 'mail.channel')
|
||||
self.assertFalse(msg_fw.parent_id)
|
||||
self.assertTrue(msg_fw.res_id == channel.id)
|
||||
|
||||
msg_fw = self.env['mail.message'].search([('message_id', '=', fw_msg_id)])
|
||||
self.assertEqual(len(msg_fw), 1)
|
||||
|
||||
self.assertEqual(msg_fw.model, 'mail.channel')
|
||||
self.assertFalse(msg_fw.parent_id)
|
||||
fw_msg_id = '<THIS.IS.A.FW.MESSAGE.2@bert.fr>'
|
||||
fw_message = MAIL_TEMPLATE.format(to='public@example.com',
|
||||
cc='',
|
||||
subject='FW: Re: 2',
|
||||
email_from='b.t@example.com',
|
||||
extra='In-Reply-To: %s' % msg.message_id,
|
||||
msg_id=fw_msg_id)
|
||||
self.env['mail.thread'].message_process(None, fw_message)
|
||||
msg_fw = self.env['mail.message'].search([('message_id', '=', fw_msg_id)])
|
||||
self.assertEqual(len(msg_fw), 1)
|
||||
channel = self.env['mail.channel'].search([('name', "=", msg_fw.subject)])
|
||||
self.assertEqual(len(channel), 0)
|
||||
self.assertEqual(msg_fw.model, 'mail.channel')
|
||||
self.assertFalse(msg_fw.parent_id)
|
||||
self.assertTrue(msg_fw.res_id == self.group_public.id)
|
||||
|
||||
@@ -49,8 +49,8 @@ class TestMailMessage(TestMail):
|
||||
|
||||
msg = self.env['mail.message'].sudo(self.user_employee).create({})
|
||||
self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertEqual(msg.reply_to, '%s <%s@%s>' % (self.user_employee.name, self.user_employee.alias_name, alias_domain))
|
||||
self.assertEqual(msg.email_from, '%s <%s@%s>' % (self.user_employee.name, self.user_employee.alias_name, alias_domain))
|
||||
self.assertEqual(msg.reply_to, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
|
||||
def test_mail_message_values_alias_catchall(self):
|
||||
alias_domain = 'example.com'
|
||||
@@ -61,7 +61,7 @@ class TestMailMessage(TestMail):
|
||||
msg = self.env['mail.message'].sudo(self.user_employee).create({})
|
||||
self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertEqual(msg.reply_to, '%s <%s@%s>' % (self.env.user.company_id.name, alias_catchall, alias_domain))
|
||||
self.assertEqual(msg.email_from, '%s <%s@%s>' % (self.user_employee.name, self.user_employee.alias_name, alias_domain))
|
||||
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
|
||||
def test_mail_message_values_document_no_alias(self):
|
||||
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
|
||||
@@ -85,7 +85,7 @@ class TestMailMessage(TestMail):
|
||||
})
|
||||
self.assertIn('-openerp-%d-mail.channel' % self.group_pigs.id, msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertEqual(msg.reply_to, '%s %s <%s@%s>' % (self.env.user.company_id.name, self.group_pigs.name, self.group_pigs.alias_name, alias_domain))
|
||||
self.assertEqual(msg.email_from, '%s <%s@%s>' % (self.user_employee.name, self.user_employee.alias_name, alias_domain))
|
||||
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
|
||||
def test_mail_message_values_document_alias_catchall(self):
|
||||
alias_domain = 'example.com'
|
||||
@@ -99,7 +99,7 @@ class TestMailMessage(TestMail):
|
||||
})
|
||||
self.assertIn('-openerp-%d-mail.channel' % self.group_pigs.id, msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
|
||||
self.assertEqual(msg.reply_to, '%s %s <%s@%s>' % (self.env.user.company_id.name, self.group_pigs.name, self.group_pigs.alias_name, alias_domain))
|
||||
self.assertEqual(msg.email_from, '%s <%s@%s>' % (self.user_employee.name, self.user_employee.alias_name, alias_domain))
|
||||
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
|
||||
|
||||
def test_mail_message_values_no_auto_thread(self):
|
||||
msg = self.env['mail.message'].sudo(self.user_employee).create({
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MailTest(models.Model):
|
||||
""" A mail.channel is a discussion group that may behave like a listener
|
||||
on documents. """
|
||||
_description = 'Test Mail Model'
|
||||
_name = 'mail.test'
|
||||
_mail_post_access = 'read'
|
||||
_inherit = ['mail.thread']
|
||||
|
||||
name = fields.Char()
|
||||
alias_id = fields.Many2one(
|
||||
'mail.alias', 'Alias')
|
||||
@@ -59,10 +59,24 @@
|
||||
</page>
|
||||
<page string="Recipients">
|
||||
<group>
|
||||
<field name="partner_ids" widget="many2many_tags"/>
|
||||
<field name="channel_ids" widget="many2many_tags"/>
|
||||
<field name="needaction_partner_ids" widget="many2many_tags"/>
|
||||
<field name="starred_partner_ids" widget="many2many_tags"/>
|
||||
<group>
|
||||
<field name="partner_ids" widget="many2many_tags"/>
|
||||
<field name="channel_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="needaction_partner_ids" widget="many2many_tags"/>
|
||||
<field name="starred_partner_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="notification_ids">
|
||||
<tree>
|
||||
<field name="res_partner_id"/>
|
||||
<field name="is_read"/>
|
||||
<field name="is_email"/>
|
||||
<field name="email_status"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Tracking">
|
||||
|
||||
@@ -38,5 +38,17 @@
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template id="message_user_assigned">
|
||||
<p>Dear <t t-esc="object.user_id.name"/>,</p>
|
||||
<p>You have been assigned to the <t t-esc="object._description.lower()"/> <t t-esc="object.name_get()[0][1]"/>.</p>
|
||||
<p>
|
||||
<a t-att-href="'/mail/view?model=%s&res_id=%s' % (object._name, object.id)"
|
||||
style="background-color: #9E588B; margin-top: 10px; padding: 10px; text-decoration: none; color: #fff; border-radius: 5px; font-size: 16px;">
|
||||
View <t t-esc="object._description.lower()"/>
|
||||
</a>
|
||||
</p>
|
||||
<p style="color:#9E588B;">Powered by <a href="https://www.odoo.com">Odoo</a>.</p>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -27,17 +27,8 @@
|
||||
<field name="notify_email" widget="radio"/>
|
||||
</field>
|
||||
<field name="signature" position="before">
|
||||
<label for="alias_id" string="Messaging Alias" class="oe_read_only"
|
||||
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
|
||||
<field name="alias_id" class="oe_read_only" required="0" nolabel="1"
|
||||
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
|
||||
<label for="alias_name" string="Messaging Alias" class="oe_edit_only"
|
||||
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
|
||||
<div class="oe_edit_only" attrs="{'invisible': [('alias_domain', '=', False)]}">
|
||||
<field name="alias_name" class="oe_inline"/>@<field name="alias_domain" class="oe_inline" readonly="1"/>
|
||||
</div>
|
||||
<field name="alias_contact" string="Alias Accepts Emails From"
|
||||
attrs="{'invisible': [('alias_domain', '=', False)]}"/>
|
||||
<field name="alias_id"/>
|
||||
<field name="alias_contact" attrs="{'invisible': [('alias_id', '=', False)]}"/>
|
||||
</field>
|
||||
</data>
|
||||
</field>
|
||||
|
||||
@@ -60,6 +60,6 @@ class Invite(models.TransientModel):
|
||||
'res_id': wizard.res_id,
|
||||
'no_auto_thread': True,
|
||||
})
|
||||
new_partners.with_context(auto_delete=True)._notify(message, force_send=True, user_signature=True)
|
||||
new_partners.with_context(auto_delete=True)._notify(message, force_send=True, send_after_commit=False, user_signature=True)
|
||||
message.unlink()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@@ -22,7 +22,6 @@ professional emails and reuse templates in a few clicks.
|
||||
],
|
||||
'data': [
|
||||
'security/mass_mailing_security.xml',
|
||||
'data/mail_data.xml',
|
||||
'data/mass_mailing_data.xml',
|
||||
'wizard/mail_compose_message_views.xml',
|
||||
'wizard/test_mailing_views.xml',
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Bounce Email Alias -->
|
||||
<record id="icp_mail_bounce_alias" model="ir.config_parameter">
|
||||
<field name="key">mail.bounce.alias</field>
|
||||
<field name="value">bounce</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -4,65 +4,31 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
from odoo.addons.mail.models.mail_message import decode
|
||||
from odoo.addons.mail.models.mail_thread import decode_header
|
||||
|
||||
from odoo import api, models, tools
|
||||
from odoo.tools import decode_smtp_header, decode_message_header
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
""" Update MailThread to add the feature of bounced emails and replied emails
|
||||
in message_process. """
|
||||
""" Update MailThread to add the support of bounce management in mass mailing statistics. """
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
def message_route_check_bounce(self, message):
|
||||
""" Override to verify that the email_to is the bounce alias. If it is the
|
||||
case, log the bounce, set the parent and related document as bounced and
|
||||
return False to end the routing process. """
|
||||
bounce_alias = self.env['ir.config_parameter'].get_param("mail.bounce.alias")
|
||||
message_id = message.get('Message-Id')
|
||||
email_from = decode_header(message, 'From')
|
||||
email_to = decode_header(message, 'To')
|
||||
|
||||
# 0. Verify whether this is a bounced email (wrong destination,...) -> use it to collect data, such as dead leads
|
||||
if bounce_alias and bounce_alias in email_to:
|
||||
# Bounce regex
|
||||
# Typical form of bounce is bounce_alias-128-crm.lead-34@domain
|
||||
# group(1) = the mail ID; group(2) = the model (if any); group(3) = the record ID
|
||||
bounce_re = re.compile("%s-(\d+)-?([\w.]+)?-?(\d+)?" % re.escape(bounce_alias), re.UNICODE)
|
||||
bounce_match = bounce_re.search(email_to)
|
||||
if bounce_match:
|
||||
bounced_model, bounced_thread_id = None, False
|
||||
bounced_mail_id = bounce_match.group(1)
|
||||
statistics = self.env['mail.mail.statistics'].set_bounced(mail_mail_ids=[bounced_mail_id])
|
||||
for stat in statistics:
|
||||
bounced_model = stat.model
|
||||
bounced_thread_id = stat.res_id
|
||||
_logger.info('Routing mail from %s to %s with Message-Id %s: bounced mail from mail %s, model: %s, thread_id: %s',
|
||||
email_from, email_to, message_id, bounced_mail_id, bounced_model, bounced_thread_id)
|
||||
if bounced_model and bounced_model in self.env and hasattr(self.env[bounced_model], 'message_receive_bounce') and bounced_thread_id:
|
||||
self.env[bounced_model].browse(bounced_thread_id).message_receive_bounce(mail_id=bounced_mail_id)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def message_route(self, message, message_dict, model=None, thread_id=None, custom_values=None):
|
||||
if not self.message_route_check_bounce(message):
|
||||
return []
|
||||
return super(MailThread, self).message_route(message, message_dict, model, thread_id, custom_values)
|
||||
""" Override to udpate mass mailing statistics based on bounce emails """
|
||||
bounce_alias = self.env['ir.config_parameter'].get_param("mail.bounce.alias")
|
||||
email_to = decode_message_header(message, 'To')
|
||||
email_to_localpart = (tools.email_split(email_to) or [''])[0].split('@', 1)[0].lower()
|
||||
|
||||
def message_receive_bounce(self, mail_id=None):
|
||||
"""Called by ``message_process`` when a bounce email (such as Undelivered
|
||||
Mail Returned to Sender) is received for an existing thread. The default
|
||||
behavior is to check is an integer ``message_bounce`` column exists.
|
||||
If it is the case, its content is incremented. """
|
||||
if 'message_bounce' in self._fields:
|
||||
for mail_thread in self:
|
||||
mail_thread.message_bounce = mail_thread.message_bounce + 1
|
||||
if bounce_alias and bounce_alias in email_to_localpart:
|
||||
bounce_re = re.compile("%s\+(\d+)-?([\w.]+)?-?(\d+)?" % re.escape(bounce_alias), re.UNICODE)
|
||||
bounce_match = bounce_re.search(email_to)
|
||||
if bounce_match:
|
||||
bounced_mail_id = bounce_match.group(1)
|
||||
self.env['mail.mail.statistics'].set_bounced(mail_mail_ids=[bounced_mail_id])
|
||||
|
||||
return super(MailThread, self).message_route(message, message_dict, model, thread_id, custom_values)
|
||||
|
||||
@api.model
|
||||
def message_route_process(self, message, message_dict, routes):
|
||||
@@ -70,6 +36,6 @@ class MailThread(models.AbstractModel):
|
||||
by using the References header of the incoming message and looking for
|
||||
matching message_id in mail.mail.statistics. """
|
||||
if message.get('References'):
|
||||
message_ids = [x.strip() for x in decode(message['References']).split()]
|
||||
message_ids = [x.strip() for x in decode_smtp_header(message['References']).split()]
|
||||
self.env['mail.mail.statistics'].set_replied(mail_message_ids=message_ids)
|
||||
return super(MailThread, self).message_route_process(message, message_dict, routes)
|
||||
|
||||
@@ -98,14 +98,6 @@ class MassMailingContact(models.Model):
|
||||
def message_get_default_recipients(self):
|
||||
return dict((record.id, {'partner_ids': [], 'email_to': record.email, 'email_cc': False}) for record in self)
|
||||
|
||||
def message_receive_bounce(self, mail_id=None):
|
||||
"""Called by ``message_process`` when a bounce email (such as Undelivered
|
||||
Mail Returned to Sender) is received for an existing thread. As contacts
|
||||
do not inherit form mail.thread, we have to define this method to be able
|
||||
to track bounces (see mail.thread for more details). """
|
||||
for contact in self:
|
||||
contact.message_bounce = contact.message_bounce + 1
|
||||
|
||||
|
||||
class MassMailingStage(models.Model):
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ class TestPortalProjectBase(TestProjectBase):
|
||||
self.user_noone = self.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True}).create({
|
||||
'name': 'Noemie NoOne',
|
||||
'login': 'noemie',
|
||||
'alias_name': 'noemie',
|
||||
'email': 'n.n@example.com',
|
||||
'signature': '--\nNoemie',
|
||||
'notify_email': 'always',
|
||||
|
||||
@@ -18,14 +18,12 @@ class TestProjectBase(TestMail):
|
||||
cls.user_projectuser = Users.create({
|
||||
'name': 'Armande ProjectUser',
|
||||
'login': 'Armande',
|
||||
'alias_name': 'armande',
|
||||
'email': 'armande.projectuser@example.com',
|
||||
'groups_id': [(6, 0, [user_group_employee.id, user_group_project_user.id])]
|
||||
})
|
||||
cls.user_projectmanager = Users.create({
|
||||
'name': 'Bastien ProjectManager',
|
||||
'login': 'bastien',
|
||||
'alias_name': 'bastien',
|
||||
'email': 'bastien.projectmanager@example.com',
|
||||
'groups_id': [(6, 0, [user_group_employee.id, user_group_project_manager.id])]})
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ class TestSale(AccountingTestCase):
|
||||
self.manager = self.env['res.users'].create({
|
||||
'name': 'Andrew Manager',
|
||||
'login': 'manager',
|
||||
'alias_name': 'andrew',
|
||||
'email': 'a.m@example.com',
|
||||
'signature': '--\nAndreww',
|
||||
'notify_email': 'always',
|
||||
@@ -22,7 +21,6 @@ class TestSale(AccountingTestCase):
|
||||
self.user = self.env['res.users'].create({
|
||||
'name': 'Mark User',
|
||||
'login': 'user',
|
||||
'alias_name': 'mark',
|
||||
'email': 'm.u@example.com',
|
||||
'signature': '--\nMark',
|
||||
'notify_email': 'always',
|
||||
|
||||
@@ -20,15 +20,15 @@ class TestSurvey(TransactionCase):
|
||||
User = self.env['res.users'].with_context({'no_reset_password': True})
|
||||
(group_survey_user, group_employee) = (self.ref('base.group_survey_user'), self.ref('base.group_user'))
|
||||
self.survey_manager = User.create({
|
||||
'name': 'Gustave Doré', 'login': 'Gustav', 'alias_name': 'gustav', 'email': 'gustav.dore@example.com',
|
||||
'name': 'Gustave Doré', 'login': 'Gustav','email': 'gustav.dore@example.com',
|
||||
'groups_id': [(6, 0, [self.ref('base.group_survey_manager'), group_survey_user, group_employee])]})
|
||||
|
||||
self.survey_user = User.create({
|
||||
'name': 'Lukas Peeters', 'login': 'Lukas', 'alias_name': 'lukas', 'email': 'lukas.petters@example.com',
|
||||
'name': 'Lukas Peeters', 'login': 'Lukas', 'email': 'lukas.petters@example.com',
|
||||
'groups_id': [(6, 0, [group_survey_user, group_employee])]})
|
||||
|
||||
self.user_public = User.create({
|
||||
'name': 'Wout Janssens', 'login': 'Wout', 'alias_name': 'wout', 'email': 'wout.janssens@example.com',
|
||||
'name': 'Wout Janssens', 'login': 'Wout', 'email': 'wout.janssens@example.com',
|
||||
'groups_id': [(6, 0, [self.ref('base.group_public')])]})
|
||||
|
||||
self.survey1 = self.env['survey.survey'].sudo(self.survey_manager).create({'title': "S0", 'page_ids': [(0, 0, {'title': "P0"})]})
|
||||
|
||||
@@ -217,3 +217,14 @@ class BlogPost(models.Model):
|
||||
for category, data in res.iteritems():
|
||||
res[category]['button_access'] = {'url': access_action, 'title': _('View Blog Post')}
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def message_get_message_notify_values(self, message, message_values):
|
||||
""" Override to avoid keeping all notified recipients of a comment.
|
||||
We avoid tracking needaction on post comments. Only emails should be
|
||||
sufficient. """
|
||||
if message.message_type == 'comment':
|
||||
return {
|
||||
'needaction_partner_ids': [],
|
||||
}
|
||||
return {}
|
||||
|
||||
@@ -17,7 +17,6 @@ class TestWebsiteBlogCommon(common.TransactionCase):
|
||||
self.user_employee = Users.with_context({'no_reset_password': True}).create({
|
||||
'name': 'Armande Employee',
|
||||
'login': 'armande',
|
||||
'alias_name': 'armande',
|
||||
'email': 'armande.employee@example.com',
|
||||
'notify_email': 'none',
|
||||
'groups_id': [(6, 0, [group_employee_id])]
|
||||
@@ -25,7 +24,6 @@ class TestWebsiteBlogCommon(common.TransactionCase):
|
||||
self.user_blogmanager = Users.with_context({'no_reset_password': True}).create({
|
||||
'name': 'Bastien BlogManager',
|
||||
'login': 'bastien',
|
||||
'alias_name': 'bastien',
|
||||
'email': 'bastien.blogmanager@example.com',
|
||||
'notify_email': 'none',
|
||||
'groups_id': [(6, 0, [group_blog_manager_id, group_employee_id])]
|
||||
@@ -33,7 +31,6 @@ class TestWebsiteBlogCommon(common.TransactionCase):
|
||||
self.user_public = Users.with_context({'no_reset_password': True}).create({
|
||||
'name': 'Cedric Public',
|
||||
'login': 'cedric',
|
||||
'alias_name': 'cedric',
|
||||
'email': 'cedric.public@example.com',
|
||||
'notify_email': 'none',
|
||||
'groups_id': [(6, 0, [group_public_id])]
|
||||
|
||||
@@ -810,6 +810,17 @@ class Post(models.Model):
|
||||
kwargs['record_name'] = kwargs.get('record_name') or self.parent_id and self.parent_id.name
|
||||
return super(Post, self).message_post(message_type=message_type, subtype=subtype, **kwargs)
|
||||
|
||||
@api.multi
|
||||
def message_get_message_notify_values(self, message, message_values):
|
||||
""" Override to avoid keeping all notified recipients of a comment.
|
||||
We avoid tracking needaction on post comments. Only emails should be
|
||||
sufficient. """
|
||||
if message.message_type == 'comment':
|
||||
return {
|
||||
'needaction_partner_ids': [],
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
class PostReason(models.Model):
|
||||
_name = "forum.post.reason"
|
||||
@@ -911,12 +922,3 @@ class Tags(models.Model):
|
||||
if self.env.user.karma < forum.karma_tag_create:
|
||||
raise KarmaError(_('Not enough karma to create a new Tag'))
|
||||
return super(Tags, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(vals)
|
||||
|
||||
class Message(models.Model):
|
||||
_name = 'mail.message'
|
||||
_inherit = ['mail.message']
|
||||
|
||||
@api.multi
|
||||
def _is_accessible(self):
|
||||
res = super(Message, self)._is_accessible()
|
||||
return self.model == 'forum.post' or res
|
||||
|
||||
@@ -35,7 +35,6 @@ class TestForumCommon(common.SavepointCase):
|
||||
cls.user_employee = TestUsersEnv.create({
|
||||
'name': 'Armande Employee',
|
||||
'login': 'Armande',
|
||||
'alias_name': 'armande',
|
||||
'email': 'armande.employee@example.com',
|
||||
'karma': 0,
|
||||
'groups_id': [(6, 0, [group_employee_id])]
|
||||
@@ -43,7 +42,6 @@ class TestForumCommon(common.SavepointCase):
|
||||
cls.user_portal = TestUsersEnv.create({
|
||||
'name': 'Beatrice Portal',
|
||||
'login': 'Beatrice',
|
||||
'alias_name': 'beatrice',
|
||||
'email': 'beatrice.employee@example.com',
|
||||
'karma': 0,
|
||||
'groups_id': [(6, 0, [group_portal_id])]
|
||||
@@ -51,7 +49,6 @@ class TestForumCommon(common.SavepointCase):
|
||||
cls.user_public = TestUsersEnv.create({
|
||||
'name': 'Cedric Public',
|
||||
'login': 'Cedric',
|
||||
'alias_name': 'cedric',
|
||||
'email': 'cedric.employee@example.com',
|
||||
'karma': 0,
|
||||
'groups_id': [(6, 0, [group_public_id])]
|
||||
|
||||
@@ -200,6 +200,10 @@ class Partner(models.Model, FormatAddress):
|
||||
company_id = fields.Many2one('res.company', 'Company', index=True, default=_default_company)
|
||||
color = fields.Integer(string='Color Index', default=0)
|
||||
user_ids = fields.One2many('res.users', 'partner_id', string='Users', auto_join=True)
|
||||
partner_share = fields.Boolean(
|
||||
'Share Partner', compute='_compute_partner_share', store=True,
|
||||
help="Either customer (no user), either shared user. Indicated the current partner is a customer without "
|
||||
"access or with a limited access created for sharing data.")
|
||||
contact_address = fields.Char(compute='_compute_contact_address', string='Complete Address')
|
||||
|
||||
# technical field used for managing commercial fields
|
||||
@@ -239,6 +243,11 @@ class Partner(models.Model, FormatAddress):
|
||||
for partner in self:
|
||||
partner.tz_offset = datetime.datetime.now(pytz.timezone(partner.tz or 'GMT')).strftime('%z')
|
||||
|
||||
@api.depends('user_ids.share')
|
||||
def _compute_partner_share(self):
|
||||
for partner in self:
|
||||
partner.partner_share = not partner.user_ids or any(user.share for user in partner.user_ids)
|
||||
|
||||
@api.depends(lambda self: self._display_address_depends())
|
||||
def _compute_contact_address(self):
|
||||
for partner in self:
|
||||
|
||||
+38
-4
@@ -1,21 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from lxml import etree
|
||||
import cgi
|
||||
import logging
|
||||
import lxml.html
|
||||
import lxml.html.clean as clean
|
||||
import random
|
||||
import re
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
from email.header import decode_header
|
||||
from email.utils import getaddresses, formataddr
|
||||
from lxml import etree
|
||||
|
||||
import openerp
|
||||
from openerp.loglevels import ustr
|
||||
from openerp.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -403,13 +403,16 @@ email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63})""",
|
||||
# matches a string containing only one email
|
||||
single_email_re = re.compile(r"""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$""", re.VERBOSE)
|
||||
|
||||
res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
|
||||
# update command in emails body
|
||||
command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
|
||||
|
||||
# Updated in 7.0 to match the model name as well
|
||||
# Typical form of references is <timestamp-openerp-record_id-model_name@domain>
|
||||
# group(1) = the record ID ; group(2) = the model (if any) ; group(3) = the domain
|
||||
reference_re = re.compile("<.*-open(?:object|erp)-(\\d+)(?:-([\w.]+))?[^>]*@([^>]*)>", re.UNICODE)
|
||||
discussion_re = re.compile("<.*-open(?:object|erp)-private[^>]*@([^>]*)>", re.UNICODE)
|
||||
|
||||
mail_header_msgid_re = re.compile('<[^<>]+>')
|
||||
|
||||
|
||||
def generate_tracking_message_id(res_id):
|
||||
@@ -487,3 +490,34 @@ def email_split_and_format(text):
|
||||
# is strictly required in RFC2822's `addr-spec`.
|
||||
if addr[1]
|
||||
if '@' in addr[1]]
|
||||
|
||||
def email_references(references):
|
||||
ref_match, model, thread_id, hostname, is_private = False, False, False, False, False
|
||||
if references:
|
||||
ref_match = reference_re.search(references)
|
||||
if ref_match:
|
||||
model = ref_match.group(2)
|
||||
thread_id = int(ref_match.group(1))
|
||||
hostname = ref_match.group(3)
|
||||
else:
|
||||
ref_match = discussion_re.search(references)
|
||||
if ref_match:
|
||||
is_private = True
|
||||
return (ref_match, model, thread_id, hostname, is_private)
|
||||
|
||||
# was mail_message.decode()
|
||||
def decode_smtp_header(smtp_header):
|
||||
"""Returns unicode() string conversion of the given encoded smtp header
|
||||
text. email.header decode_header method return a decoded string and its
|
||||
charset for each decoded par of the header. This method unicodes the
|
||||
decoded header and join them in a complete string. """
|
||||
if smtp_header:
|
||||
text = decode_header(smtp_header.replace('\r', ''))
|
||||
# The joining space will not be needed as of Python 3.3
|
||||
# See https://hg.python.org/cpython/rev/8c03fe231877
|
||||
return ' '.join([ustr(x[0], x[1]) for x in text])
|
||||
return u''
|
||||
|
||||
# was mail_thread.decode_header()
|
||||
def decode_message_header(message, header, separator=' '):
|
||||
return separator.join(map(decode_smtp_header, filter(None, message.get_all(header, []))))
|
||||
|
||||
Reference in New Issue
Block a user