##############################################################################
import logging
-import re
from openerp import tools
from email.header import decode_header
-from openerp import SUPERUSER_ID
+from email.utils import formataddr
+from openerp import SUPERUSER_ID, api
from openerp.osv import osv, orm, fields
from openerp.tools import html_email_clean
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
-try:
- from mako.template import Template as MakoTemplate
-except ImportError:
- _logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
-
""" Some tools for parsing / creating email fields """
def decode(text):
def default_get(self, cr, uid, fields, context=None):
# protection for `default_type` values leaking from menu action context (e.g. for invoices)
- if context and context.get('default_type') and context.get('default_type') not in self._columns['type'].selection:
+ if context and context.get('default_type') and context.get('default_type') not in [
+ val[0] for val in self._columns['type'].selection]:
context = dict(context, default_type=None)
return super(mail_message, self).default_get(cr, uid, fields, context=context)
- def _shorten_name(self, name):
- if len(name) <= (self._message_record_name_length + 3):
- return name
- return name[:self._message_record_name_length] + '...'
-
- def _get_record_name(self, cr, uid, ids, name, arg, context=None):
- """ Return the related document name, using name_get. It is done using
- SUPERUSER_ID, to be sure to have the record name correctly stored. """
- # TDE note: regroup by model/ids, to have less queries to perform
- result = dict.fromkeys(ids, False)
- for message in self.read(cr, uid, ids, ['model', 'res_id'], context=context):
- if not message.get('model') or not message.get('res_id') or message['model'] not in self.pool:
- continue
- result[message['id']] = self.pool[message['model']].name_get(cr, SUPERUSER_ID, [message['res_id']], context=context)[0][1]
- return result
-
def _get_to_read(self, cr, uid, ids, name, arg, context=None):
""" Compute if the message is unread by the current user. """
res = dict((id, False) for id in ids)
notif_ids = notif_obj.search(cr, uid, [
('partner_id', 'in', [partner_id]),
('message_id', 'in', ids),
- ('read', '=', False),
+ ('is_read', '=', False),
], context=context)
for notif in notif_obj.browse(cr, uid, notif_ids, context=context):
res[notif.message_id.id] = True
def _search_to_read(self, cr, uid, obj, name, domain, context=None):
""" Search for messages to read by the current user. Condition is
- inversed because we search unread message on a read column. """
- return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.read', '=', not domain[0][2])]
+ inversed because we search unread message on a is_read column. """
+ return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.is_read', '=', not domain[0][2])]
def _get_starred(self, cr, uid, ids, name, arg, context=None):
""" Compute if the message is unread by the current user. """
return res
def _search_starred(self, cr, uid, obj, name, domain, context=None):
- """ Search for messages to read by the current user. Condition is
- inversed because we search unread message on a read column. """
+ """ Search for starred messages by the current user."""
return ['&', ('notification_ids.partner_id.user_ids', 'in', [uid]), ('notification_ids.starred', '=', domain[0][2])]
- def name_get(self, cr, uid, ids, context=None):
- # name_get may receive int id instead of an id list
- if isinstance(ids, (int, long)):
- ids = [ids]
- res = []
- for message in self.browse(cr, uid, ids, context=context):
- name = '%s: %s' % (message.subject or '', strip_tags(message.body or '') or '')
- res.append((message.id, self._shorten_name(name.lstrip(' :'))))
- return res
-
_columns = {
'type': fields.selection([
('email', 'Email'),
('comment', 'Comment'),
('notification', 'System notification'),
- ], 'Type',
+ ], 'Type', size=12,
help="Message type: email for email message, notification for system "\
"message, comment for other messages such as user replies"),
'email_from': fields.char('From',
help="Email address of the sender. This field is set when no matching partner is found for incoming emails."),
'reply_to': fields.char('Reply-To',
help='Reply email address. Setting the reply_to bypasses the automatic thread creation.'),
+ 'no_auto_thread': fields.boolean('No threading for answers',
+ help='Answers do not go in the original document\' discussion thread. This has an impact on the generated message-id.'),
'author_id': fields.many2one('res.partner', 'Author', select=1,
ondelete='set null',
help="Author of the message. If not set, email_from may hold an email address that did not match any partner."),
'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'),
'model': fields.char('Related Document Model', size=128, select=1),
'res_id': fields.integer('Related Document ID', select=1),
- 'record_name': fields.function(_get_record_name, type='char',
- store=True, string='Message Record Name',
- help="Name get of the related document."),
+ 'record_name': fields.char('Message Record Name', help="Name get of the related document."),
'notification_ids': fields.one2many('mail.notification', 'message_id',
string='Notifications', auto_join=True,
help='Technical field holding the message notifications. Use notified_partner_ids to access notified partners.'),
'subject': fields.char('Subject'),
'date': fields.datetime('Date'),
- 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1),
+ 'message_id': fields.char('Message-Id', help='Message unique identifier', select=1, readonly=1, copy=False),
'body': fields.html('Contents', help='Automatically sanitized HTML contents'),
'to_read': fields.function(_get_to_read, fnct_search=_search_to_read,
type='boolean', string='To read',
def _get_default_from(self, cr, uid, context=None):
this = self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context)
if this.alias_name and this.alias_domain:
- return '%s <%s@%s>' % (this.name, this.alias_name, this.alias_domain)
+ return formataddr((this.name, '%s@%s' % (this.alias_name, this.alias_domain)))
elif this.email:
- return '%s <%s>' % (this.name, this.email)
- return False
+ return formataddr((this.name, this.email))
+ raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
def _get_default_author(self, cr, uid, context=None):
return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
def download_attachment(self, cr, uid, id_message, attachment_id, context=None):
""" Return the content of linked attachments. """
- message = self.browse(cr, uid, id_message, context=context)
- if attachment_id in [attachment.id for attachment in message.attachment_ids]:
+ # this will fail if you cannot read the message
+ message_values = self.read(cr, uid, [id_message], ['attachment_ids'], context=context)[0]
+ if attachment_id in message_values['attachment_ids']:
attachment = self.pool.get('ir.attachment').browse(cr, SUPERUSER_ID, attachment_id, context=context)
if attachment.datas and attachment.datas_fname:
return {
# Notification API
#------------------------------------------------------
+ @api.cr_uid_ids_context
def set_message_read(self, cr, uid, msg_ids, read, create_missing=True, context=None):
""" Set messages as (un)read. Technically, the notifications related
to uid are set to (un)read. If for some msg_ids there are missing
user_pid = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
domain = [('partner_id', '=', user_pid), ('message_id', 'in', msg_ids)]
if not create_missing:
- domain += [('read', '=', not read)]
+ domain += [('is_read', '=', not read)]
notif_ids = notification_obj.search(cr, uid, domain, context=context)
# all message have notifications: already set them as (un)read
if len(notif_ids) == len(msg_ids) or not create_missing:
- notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+ notification_obj.write(cr, uid, notif_ids, {'is_read': read}, context=context)
return len(notif_ids)
# some messages do not have notifications: find which one, create notification, update read status
notified_msg_ids = [notification.message_id.id for notification in notification_obj.browse(cr, uid, notif_ids, context=context)]
to_create_msg_ids = list(set(msg_ids) - set(notified_msg_ids))
for msg_id in to_create_msg_ids:
- notification_obj.create(cr, uid, {'partner_id': user_pid, 'read': read, 'message_id': msg_id}, context=context)
- notification_obj.write(cr, uid, notif_ids, {'read': read}, context=context)
+ notification_obj.create(cr, uid, {'partner_id': user_pid, 'is_read': read, 'message_id': msg_id}, context=context)
+ notification_obj.write(cr, uid, notif_ids, {'is_read': read}, context=context)
return len(notif_ids)
+ @api.cr_uid_ids_context
def set_message_starred(self, cr, uid, msg_ids, starred, create_missing=True, context=None):
""" Set messages as (un)starred. Technically, the notifications related
to uid are set to (un)starred.
'starred': starred
}
if starred:
- values['read'] = False
+ values['is_read'] = False
notif_ids = notification_obj.search(cr, uid, domain, context=context)
exp_domain = domain + [('id', '<', min(message_unload_ids + message_ids))]
else:
exp_domain = domain + ['!', ('id', 'child_of', message_unload_ids + parent_tree.keys())]
- ids = self.search(cr, uid, exp_domain, context=context, limit=1)
- if ids:
+ more_count = self.search_count(cr, uid, exp_domain, context=context)
+ if more_count:
# inside a thread: prepend
if parent_id:
messages.insert(0, _get_expandable(exp_domain, -1, parent_id, True))
return True
+ @api.cr_uid_context
def message_read(self, cr, uid, ids=None, domain=None, message_unload_ids=None,
thread_level=0, context=None, parent_id=False, limit=None):
""" Read messages from mail.message, and get back a list of structured
thread_level=thread_level, message_unload_ids=message_unload_ids, domain=domain, parent_id=parent_id, context=context)
return message_list
+ def get_likers_list(self, cr, uid, ids, limit=10, context=None):
+ """ Return the people list who liked this message. """
+ voter_names = []
+ message = self.browse(cr, uid, ids, context=context)
+ for voter in message.vote_user_ids[:limit]:
+ voter_names.append(voter.name)
+ if len(message.vote_user_ids) > limit:
+ voter_names.append(_("and %s others like this") % (len(message.vote_user_ids) - limit))
+ return voter_names
+
#------------------------------------------------------
# mail_message internals
#------------------------------------------------------
return allowed_ids
def _search(self, cr, uid, args, offset=0, limit=None, order=None,
- context=None, count=False, access_rights_uid=None):
+ context=None, count=False, access_rights_uid=None):
""" Override that adds specific access rights of mail.message, to remove
ids uid could not see according to our custom rules. Please refer
to check_access_rule for more details about those rules.
"""
# Rules do not apply to administrator
if uid == SUPERUSER_ID:
- return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
+ return super(mail_message, self)._search(
+ cr, uid, args, offset=offset, limit=limit, order=order,
context=context, count=count, access_rights_uid=access_rights_uid)
# Perform a super with count as False, to have the ids, not a counter
- ids = super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
+ ids = super(mail_message, self)._search(
+ cr, uid, args, offset=offset, limit=limit, order=order,
context=context, count=False, access_rights_uid=access_rights_uid)
if not ids and count:
return 0
author_ids, partner_ids, allowed_ids = set([]), set([]), set([])
model_ids = {}
- messages = super(mail_message, self).read(cr, uid, ids, ['author_id', 'model', 'res_id', 'notified_partner_ids'], context=context)
- for message in messages:
- if message.get('author_id') and message.get('author_id')[0] == pid:
- author_ids.add(message.get('id'))
- elif pid in message.get('notified_partner_ids'):
- partner_ids.add(message.get('id'))
- elif message.get('model') and message.get('res_id'):
- model_ids.setdefault(message.get('model'), {}).setdefault(message.get('res_id'), set()).add(message.get('id'))
+ # check read access rights before checking the actual rules on the given ids
+ super(mail_message, self).check_access_rights(cr, access_rights_uid or uid, 'read')
+
+ cr.execute("""SELECT DISTINCT m.id, m.model, m.res_id, m.author_id, n.partner_id
+ FROM "%s" m LEFT JOIN "mail_notification" n
+ ON n.message_id=m.id AND n.partner_id = (%%s)
+ WHERE m.id = ANY (%%s)""" % self._table, (pid, ids,))
+ for id, rmod, rid, author_id, partner_id in cr.fetchall():
+ if author_id == pid:
+ author_ids.add(id)
+ elif partner_id == pid:
+ partner_ids.add(id)
+ elif rmod and rid:
+ model_ids.setdefault(rmod, {}).setdefault(rid, set()).add(id)
allowed_ids = self._find_allowed_doc_ids(cr, uid, model_ids, context=context)
final_ids = author_ids | partner_ids | allowed_ids
- uid has write or create access on the related document if model, res_id
- otherwise: raise
"""
- def _generate_model_record_ids(msg_val, msg_ids=[]):
+ def _generate_model_record_ids(msg_val, msg_ids):
""" :param model_record_ids: {'model': {'res_id': (msg_id, msg_id)}, ... }
:param message_values: {'msg_id': {'model': .., 'res_id': .., 'author_id': ..}}
"""
partner_id = self.pool['res.users'].browse(cr, SUPERUSER_ID, uid, context=None).partner_id.id
# Read mail_message.ids to have their values
- message_values = dict.fromkeys(ids, {})
+ message_values = dict((res_id, {}) for res_id in ids)
cr.execute('SELECT DISTINCT id, model, res_id, author_id, parent_id FROM "%s" WHERE id = ANY (%%s)' % self._table, (ids,))
for id, rmod, rid, author_id, parent_id in cr.fetchall():
message_values[id] = {'model': rmod, 'res_id': rid, 'author_id': author_id, 'parent_id': parent_id}
author_ids = []
if operation == 'read' or operation == 'write':
author_ids = [mid for mid, message in message_values.iteritems()
- if message.get('author_id') and message.get('author_id') == partner_id]
+ if message.get('author_id') and message.get('author_id') == partner_id]
elif operation == 'create':
author_ids = [mid for mid, message in message_values.iteritems()
- if not message.get('model') and not message.get('res_id')]
+ if not message.get('model') and not message.get('res_id')]
# Parent condition, for create (check for received notifications for the created message parent)
notified_ids = []
if operation == 'create':
parent_ids = [message.get('parent_id') for mid, message in message_values.iteritems()
- if message.get('parent_id')]
+ if message.get('parent_id')]
not_ids = not_obj.search(cr, SUPERUSER_ID, [('message_id.id', 'in', parent_ids), ('partner_id', '=', partner_id)], context=context)
not_parent_ids = [notif.message_id.id for notif in not_obj.browse(cr, SUPERUSER_ID, not_ids, context=context)]
notified_ids += [mid for mid, message in message_values.iteritems()
- if message.get('parent_id') in not_parent_ids]
+ if message.get('parent_id') in not_parent_ids]
# Notification condition, for read (check for received notifications and create (in message_follower_ids)) -> could become an ir.rule, but not till we do not have a many2one variable field
other_ids = set(ids).difference(set(author_ids), set(notified_ids))
('res_model', '=', doc_model),
('res_id', 'in', list(doc_ids)),
('partner_id', '=', partner_id),
- ], context=context)
+ ], context=context)
fol_mids = [follower.res_id for follower in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)]
notified_ids += [mid for mid, message in message_values.iteritems()
- if message.get('model') == doc_model and message.get('res_id') in fol_mids]
+ if message.get('model') == doc_model and message.get('res_id') in fol_mids]
# CRUD: Access rights related to the document
other_ids = other_ids.difference(set(notified_ids))
else:
self.pool['mail.thread'].check_mail_message_access(cr, uid, mids, operation, model_obj=model_obj, context=context)
document_related_ids += [mid for mid, message in message_values.iteritems()
- if message.get('model') == model and message.get('res_id') in mids]
+ if message.get('model') == model and message.get('res_id') in mids]
# Calculate remaining ids: if not void, raise an error
other_ids = other_ids.difference(set(document_related_ids))
if not other_ids:
return
raise orm.except_orm(_('Access Denied'),
- _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % \
- (self._description, operation))
+ _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
+ (self._description, operation))
+
+ def _get_record_name(self, cr, uid, values, context=None):
+ """ Return the related document name, using name_get. It is done using
+ SUPERUSER_ID, to be sure to have the record name correctly stored. """
+ if not values.get('model') or not values.get('res_id') or values['model'] not in self.pool:
+ return False
+ return self.pool[values['model']].name_get(cr, SUPERUSER_ID, [values['res_id']], context=context)[0][1]
def _get_reply_to(self, cr, uid, values, context=None):
""" Return a specific reply_to: alias of the document through message_get_reply_to
or take the email_from
"""
- email_reply_to = None
-
- ir_config_parameter = self.pool.get("ir.config_parameter")
- catchall_domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)
-
- # model, res_id, email_from: comes from values OR related message
model, res_id, email_from = values.get('model'), values.get('res_id'), values.get('email_from')
-
- # if model and res_id: try to use ``message_get_reply_to`` that returns the document alias
- if not email_reply_to and model and res_id and catchall_domain and hasattr(self.pool[model], 'message_get_reply_to'):
- email_reply_to = self.pool[model].message_get_reply_to(cr, uid, [res_id], context=context)[0]
- # no alias reply_to -> catchall alias
- if not email_reply_to and catchall_domain:
- catchall_alias = ir_config_parameter.get_param(cr, uid, "mail.catchall.alias", context=context)
- if catchall_alias:
- email_reply_to = '%s@%s' % (catchall_alias, catchall_domain)
- # still no reply_to -> reply_to will be the email_from
- if not email_reply_to and email_from:
- email_reply_to = email_from
-
- # format 'Document name <email_address>'
- if email_reply_to and model and res_id:
- emails = tools.email_split(email_reply_to)
- if emails:
- email_reply_to = emails[0]
- document_name = self.pool[model].name_get(cr, SUPERUSER_ID, [res_id], context=context)[0]
- if document_name:
- # sanitize document name
- sanitized_doc_name = re.sub(r'[^\w+.]+', '-', document_name[1])
- # generate reply to
- email_reply_to = _('"Followers of %s" <%s>') % (sanitized_doc_name, email_reply_to)
-
- return email_reply_to
+ ctx = dict(context, thread_model=model)
+ return self.pool['mail.thread'].message_get_reply_to(cr, uid, [res_id], default=email_from, context=ctx)[res_id]
def _get_message_id(self, cr, uid, values, context=None):
- if values.get('reply_to'):
+ if values.get('no_auto_thread', False) is True:
message_id = tools.generate_tracking_message_id('reply_to')
elif values.get('res_id') and values.get('model'):
message_id = tools.generate_tracking_message_id('%(res_id)s-%(model)s' % values)
return message_id
def create(self, cr, uid, values, context=None):
- if context is None:
- context = {}
+ context = dict(context or {})
default_starred = context.pop('default_starred', False)
if 'email_from' not in values: # needed to compute reply_to
values['email_from'] = self._get_default_from(cr, uid, context=context)
- if 'message_id' not in values:
+ if not values.get('message_id'):
values['message_id'] = self._get_message_id(cr, uid, values, context=context)
if 'reply_to' not in values:
values['reply_to'] = self._get_reply_to(cr, uid, values, context=context)
+ if 'record_name' not in values and 'default_record_name' not in context:
+ values['record_name'] = self._get_record_name(cr, uid, values, context=context)
newid = super(mail_message, self).create(cr, uid, values, context)
- if values['email_from']:
- self._notify(cr, uid, newid, context=context,
+
+ self._notify(cr, uid, newid, context=context,
force_send=context.get('mail_notify_force_send', True),
user_signature=context.get('mail_notify_user_signature', True))
# TDE FIXME: handle default_starred. Why not setting an inv on starred ?
self.pool.get('ir.attachment').unlink(cr, uid, attachments_to_delete, context=context)
return super(mail_message, self).unlink(cr, uid, ids, context=context)
- def copy(self, cr, uid, id, default=None, context=None):
- """ Overridden to avoid duplicating fields that are unique to each email """
- if default is None:
- default = {}
- default.update(message_id=False, headers=False)
- return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
-
#------------------------------------------------------
# Messaging API
#------------------------------------------------------
- # TDE note: this code is not used currently, will be improved in a future merge, when quoted context
- # will be added to email send for notifications. Currently only WIP.
- MAIL_TEMPLATE = """<div>
- % if message:
- ${display_message(message)}
- % endif
- % for ctx_msg in context_messages:
- ${display_message(ctx_msg)}
- % endfor
- % if add_expandable:
- ${display_expandable()}
- % endif
- ${display_message(header_message)}
- </div>
-
- <%def name="display_message(message)">
- <div>
- Subject: ${message.subject}<br />
- Body: ${message.body}
- </div>
- </%def>
-
- <%def name="display_expandable()">
- <div>This is an expandable.</div>
- </%def>
- """
-
- def message_quote_context(self, cr, uid, id, context=None, limit=3, add_original=False):
- """
- 1. message.parent_id = False: new thread, no quote_context
- 2. get the lasts messages in the thread before message
- 3. get the message header
- 4. add an expandable between them
-
- :param dict quote_context: options for quoting
- :return string: html quote
- """
- add_expandable = False
-
- message = self.browse(cr, uid, id, context=context)
- if not message.parent_id:
- return ''
- context_ids = self.search(cr, uid, [
- ('parent_id', '=', message.parent_id.id),
- ('id', '<', message.id),
- ], limit=limit, context=context)
-
- if len(context_ids) >= limit:
- add_expandable = True
- context_ids = context_ids[0:-1]
-
- context_ids.append(message.parent_id.id)
- context_messages = self.browse(cr, uid, context_ids, context=context)
- header_message = context_messages.pop()
-
- try:
- if not add_original:
- message = False
- result = MakoTemplate(self.MAIL_TEMPLATE).render_unicode(message=message,
- context_messages=context_messages,
- header_message=header_message,
- add_expandable=add_expandable,
- # context kw would clash with mako internals
- ctx=context,
- format_exceptions=True)
- result = result.strip()
- return result
- except Exception:
- _logger.exception("failed to render mako template for quoting message")
- return ''
- return result
-
def _notify(self, cr, uid, newid, context=None, force_send=False, 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
cr, SUPERUSER_ID, [
('res_model', '=', message.model),
('res_id', '=', message.res_id),
- ('subtype_ids', 'in', message.subtype_id.id)
], context=context)
- partners_to_notify |= set(fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context))
+ partners_to_notify |= set(
+ fo.partner_id.id for fo in fol_obj.browse(cr, SUPERUSER_ID, fol_ids, context=context)
+ if message.subtype_id.id in [st.id for st in fo.subtype_ids]
+ )
# remove me from notified partners, unless the message is written on my own wall
if message.subtype_id and message.author_id and message.model == "res.partner" and message.res_id == message.author_id.id:
partners_to_notify |= set([message.author_id.id])
notification_obj.create(cr, uid, {
'message_id': message.parent_id.id,
'partner_id': partner.id,
- 'read': True,
+ 'is_read': True,
}, context=context)
-
- #------------------------------------------------------
- # Tools
- #------------------------------------------------------
-
- def check_partners_email(self, cr, uid, partner_ids, context=None):
- """ Verify that selected partner_ids have an email_address defined.
- Otherwise throw a warning. """
- partner_wo_email_lst = []
- for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
- if not partner.email:
- partner_wo_email_lst.append(partner)
- if not partner_wo_email_lst:
- return {}
- warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
- for partner in partner_wo_email_lst:
- warning_msg += '\n- %s' % (partner.name)
- return {'warning': {
- 'title': _('Partners email addresses not found'),
- 'message': warning_msg,
- }
- }