[MERGE]:trunk-mail-cleaning-fp

bzr revid: apa@tinyerp.com-20120831124026-g5xf61xc64lphs7d
This commit is contained in:
Amit Patel (OpenERP)
2012-08-31 18:10:26 +05:30
4 changed files with 99 additions and 136 deletions
+13 -1
View File
@@ -61,14 +61,21 @@ class mail_notification(osv.Model):
_columns = {
'partner_id': fields.many2one('res.partner', string='Contact',
ondelete='cascade', required=True, select=1),
ondelete='cascade', required=True),
'read': fields.boolean('Read'),
'message_id': fields.many2one('mail.message', string='Message',
ondelete='cascade', required=True),
}
_defaults = {
'read': False,
}
def init(self, cr):
cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('mail_notification_partner_id_read_message_id',))
if not cr.fetchone():
cr.execute('CREATE INDEX mail_notification_partner_id_read_message_id ON mail_notification (partner_id, read, message_id)')
def create(self, cr, uid, vals, context=None):
""" Override of create to check that we can not create a notification
for a message the user can not read. """
@@ -76,6 +83,11 @@ class mail_notification(osv.Model):
return super(mail_notification, self).create(cr, uid, vals, context=context)
return False
def set_message_read(self, cr, uid, msg_id, context=None):
partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
return self.write(cr, uid, notif_ids, {'read': True}, context=context)
def notify(self, cr, uid, partner_ids, msg_id, context=None):
""" Send by email the notification depending on the user preferences """
context = context or {}
+41 -47
View File
@@ -31,11 +31,8 @@ from tools.translate import _
_logger = logging.getLogger(__name__)
class mail_mail(osv.Model):
"""
Model holding RFC2822 email messages to send. This model also provides
facilities to queue and send new email messages.
"""
""" Model holding RFC2822 email messages to send. This model also provides
facilities to queue and send new email messages. """
_name = 'mail.mail'
_description = 'Outgoing Mails'
_inherits = {'mail.message': 'mail_message_id'}
@@ -64,9 +61,7 @@ class mail_mail(osv.Model):
def _get_default_from(self, cr, uid, context=None):
cur = self.pool.get('res.users').browse(cr, uid, uid, context=context)
if not cur.alias_domain:
# TDE temp: impossible to install social module, because mail.group tries to send emails ...
# raise osv.except_osv(_('Invalid Action!'), _('Unable to send email, set an alias domain in your server settings.'))
pass
raise osv.except_osv(_('Invalid Action!'), _('Unable to send email, set an alias domain in your server settings.'))
return cur.alias_name + '@' + cur.alias_domain
_defaults = {
@@ -127,69 +122,68 @@ class mail_mail(osv.Model):
message.unlink()
return True
def send(self, cr, uid, ids, auto_commit=False, context=None):
"""Sends the selected emails immediately, ignoring their current
state (mails that have already been sent should not be passed
unless they should actually be re-sent).
Emails successfully delivered are marked as 'sent', and those
that fail to be deliver are marked as 'exception', and the
corresponding error message is output in the server logs.
def _send_get_mail_subject(self, cr, uid, mail, force=False, context=None):
""" if void and related document: '<Author> posted on <Resource>'
:param mail: mail.mail browse_record """
if force or (not mail.subject and mail.model and mail.res_id):
return '%s posted on %s' % (mail.author_id.name, mail.record_name)
return mail.subject
:param bool auto_commit: whether to force a commit of the message
status after sending each message (meant
only for processing by the scheduler),
should never be True during normal
transactions (default: False)
:return: True
def send(self, cr, uid, ids, auto_commit=False, context=None):
""" Sends the selected emails immediately, ignoring their current
state (mails that have already been sent should not be passed
unless they should actually be re-sent).
Emails successfully delivered are marked as 'sent', and those
that fail to be deliver are marked as 'exception', and the
corresponding error mail is output in the server logs.
:param bool auto_commit: whether to force a commit of the mail status
after sending each mail (meant only for scheduler processing);
should never be True during normal transactions (default: False)
:return: True
"""
ir_mail_server = self.pool.get('ir.mail_server')
for message in self.browse(cr, uid, ids, context=context):
for mail in self.browse(cr, uid, ids, context=context):
try:
body = message.body_html
body = mail.body_html
subject = self._send_get_mail_subject(cr, uid, mail, context=context)
# handle attachments
attachments = []
for attach in message.attachment_ids:
for attach in mail.attachment_ids:
attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
# no subject, res_id, model: '<Author> posted on <Resource>'
if not message.subject and message.model and message.res_id:
subject = '%s posted on %s' % (message.author_id.name, message.record_name)
else:
subject = message.subject
# use only sanitized html and set its plaintexted version as alternative
body_alternative = tools.html2plaintext(body)
content_subtype_alternative = 'plain'
# build an RFC2822 email.message.Message object and send it
# without queuing
# build an RFC2822 email.message.Message object and send it without queuing
msg = ir_mail_server.build_email(
email_from = message.email_from,
email_to = tools.email_split(message.email_to),
email_from = mail.email_from,
email_to = tools.email_split(mail.email_to),
subject = subject,
body = body,
body_alternative = body_alternative,
email_cc = tools.email_split(message.email_cc),
reply_to = message.reply_to,
email_cc = tools.email_split(mail.email_cc),
reply_to = mail.reply_to,
attachments = attachments,
message_id = message.message_id,
references = message.references,
object_id = message.res_id and ('%s-%s' % (message.res_id,message.model)),
message_id = mail.message_id,
references = mail.references,
object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
subtype = 'html',
subtype_alternative = content_subtype_alternative)
res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=message.mail_server_id.id, context=context)
mail_server_id=mail.mail_server_id.id, context=context)
if res:
message.write({'state':'sent', 'message_id': res})
mail.write({'state':'sent', 'message_id': res})
else:
message.write({'state':'exception'})
message.refresh()
if message.state == 'sent':
self._postprocess_sent_message(cr, uid, message, context=context)
mail.write({'state':'exception'})
mail.refresh()
if mail.state == 'sent':
self._postprocess_sent_message(cr, uid, mail, context=context)
except Exception:
_logger.exception('failed sending mail.mail %s', message.id)
message.write({'state':'exception'})
_logger.exception('failed sending mail.mail %s', mail.id)
mail.write({'state':'exception'})
if auto_commit == True:
cr.commit()
+38 -83
View File
@@ -34,11 +34,8 @@ def decode(text):
return ''.join([tools.ustr(x[0], x[1]) for x in text])
class mail_message(osv.Model):
"""Model holding messages: system notification (replacing res.log
notifications), comments (for OpenChatter feature). This model also
provides facilities to parse new email messages. Type of messages are
differentiated using the 'type' column. """
""" Messages model: system notification (replacing res.log notifications),
comments (OpenChatter discussion) and incoming emails. """
_name = 'mail.message'
_description = 'Message'
_inherit = ['ir.needaction_mixin']
@@ -76,21 +73,17 @@ class mail_message(osv.Model):
return res
def _search_unread(self, cr, uid, obj, name, domain, context=None):
""" Search for messages unread by the current user. """
read_value = not domain[0][2]
read_cond = '' if read_value else '!= true'
""" Search for messages unread by the current user. Condition is
inversed because we search unread message on a read column. """
if domain[0][2]:
read_cond = '(read = false or read is null)'
else:
read_cond = 'read = true'
partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
cr.execute(""" SELECT mail_message.id \
FROM mail_message \
JOIN mail_notification ON ( \
mail_notification.message_id = mail_message.id ) \
WHERE mail_notification.partner_id = %%s AND \
mail_notification.read %s \
""" % read_cond, (partner_id,) )
res = cr.fetchall()
message_ids = [r[0] for r in res]
return [('id', 'in', message_ids)]
cr.execute("SELECT message_id FROM mail_notification "\
"WHERE partner_id = %%s AND %s" % read_cond,
(partner_id,))
return [('id', 'in', [r[0] for r in cr.fetchall()])]
def name_get(self, cr, uid, ids, context=None):
# name_get may receive int id instead of an id list
@@ -103,9 +96,8 @@ class mail_message(osv.Model):
return res
_columns = {
# should we keep a distinction between email and comment ?
'type': fields.selection([
('email', 'email'),
('email', 'Email'),
('comment', 'Comment'),
('notification', 'System notification'),
], 'Type',
@@ -147,7 +139,6 @@ class mail_message(osv.Model):
'author_id': _get_default_author
}
#------------------------------------------------------
# Message loading for web interface
#------------------------------------------------------
@@ -262,12 +253,11 @@ class mail_message(osv.Model):
break
# Flatten the result
# if thread_level > 0:
# result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
# if thread_level > 0:
# result = self.message_read_tree_flatten(cr, uid, result, 0, thread_level, context=context)
return result
#------------------------------------------------------
# Email api
#------------------------------------------------------
@@ -277,52 +267,47 @@ class mail_message(osv.Model):
if not cr.fetchone():
cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
def check(self, cr, uid, ids, mode, context=None):
"""
You can read/write a message if:
- you received it (a notification exists) or
- you can read the related document (res_model, res_id)
If a message is not attached to a document, normal access rights on
the mail.message object apply.
"""
if not ids:
return
def check_access_rule(self, cr, uid, ids, operation, context=None):
""" mail.message access rule check
- message received (a notification exists) -> ok
- check rules of related document if exists
- fallback on normal mail.message check """
if isinstance(ids, (int, long)):
ids = [ids]
partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
# check messages for which you have a notification
partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
not_obj = self.pool.get('mail.notification')
not_ids = not_obj.search(cr, uid, [
('partner_id', '=', partner_id),
('message_id', 'in', ids),
], context=context)
for notification in not_obj.browse(cr, uid, not_ids, context=context):
if notification.message_id.id in ids:
pass
# FP Note: we should put this again !!!
#ids.remove(notification.message_id.id)
notified_ids = [notification.message_id.id for notification in not_obj.browse(cr, uid, not_ids, context=context)
if notification.message_id.id in ids]
# check messages according to related documents
res_ids = {}
cr.execute('SELECT DISTINCT model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
for rmod, rid in cr.fetchall():
# check messages linked to an existing document
model_record_ids = {}
document_ids = []
cr.execute('SELECT DISTINCT id, model, res_id FROM mail_message WHERE id = ANY (%s)', (ids,))
for id, rmod, rid in cr.fetchall():
if not (rmod and rid):
continue
res_ids.setdefault(rmod,set()).add(rid)
document_ids.append(id)
model_record_ids.setdefault(rmod,set()).add(rid)
for model, mids in model_record_ids.items():
model_obj = self.pool.get(model)
mids = model_obj.exists(cr, uid, mids)
model_obj.check_access_rights(cr, uid, operation)
model_obj.check_access_rule(cr, uid, mids, operation, context=context)
ima_obj = self.pool.get('ir.model.access')
for model, mids in res_ids.items():
mids = self.pool.get(model).exists(cr, uid, mids)
ima_obj.check(cr, uid, model, mode)
self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)
# fall back on classic operation for other ids
other_ids = set(ids).difference(set(notified_ids), set(document_ids))
super(mail_message, self).check_access_rule(cr, uid, other_ids, operation, context=None)
def create(self, cr, uid, values, context=None):
if not values.get('message_id') and values.get('res_id') and values.get('model'):
values['message_id'] = tools.generate_tracking_message_id('%(model)s-%(res_id)s'% values)
newid = super(mail_message, self).create(cr, uid, values, context)
self.check(cr, uid, [newid], mode='create', context=context)
self.notify(cr, uid, newid, context=context)
return newid
@@ -351,39 +336,9 @@ class mail_message(osv.Model):
partners_to_notify |= extra_notified
self.pool.get('mail.notification').notify(cr, uid, list(partners_to_notify), newid, context=context)
def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'):
self.check(cr, uid, ids, 'read', context=context)
return super(mail_message, self).read(cr, uid, ids, fields_to_read, context, load)
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 = {}
self.check(cr, uid, [id], 'read', context=context)
default.update(message_id=False, headers=False)
return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
def write(self, cr, uid, ids, vals, context=None):
result = super(mail_message, self).write(cr, uid, ids, vals, context)
self.check(cr, uid, ids, 'write', context=context)
return result
def unlink(self, cr, uid, ids, context=None):
self.check(cr, uid, ids, 'unlink', context=context)
return super(mail_message, self).unlink(cr, uid, ids, context)
class mail_notification(osv.Model):
""" mail_notification is a relational table modeling messages pushed to partners.
"""
_inherit = 'mail.notification'
_columns = {
'message_id': fields.many2one('mail.message', string='Message',
ondelete='cascade', required=True, select=1),
}
def set_message_read(self, cr, uid, msg_id, context=None):
partner_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id.id
notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
return self.write(cr, uid, notif_ids, {'read': True}, context=context)
+7 -5
View File
@@ -203,17 +203,19 @@ class test_mail(common.TransactionCase):
user_raoul_id = self.res_users.create(cr, uid, {'name': 'Raoul Grosbedon', 'login': 'raoul'})
user_raoul = self.res_users.browse(cr, uid, user_raoul_id)
# Subscribe Raoul twice (niak niak) through message_subscribe_users
# Subscribe Raoul three times (niak niak) through message_subscribe_users
group_pigs.message_subscribe_users([user_raoul_id, user_raoul_id])
group_pigs.message_subscribe_users([user_raoul_id])
group_pigs.refresh()
follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
self.assertEqual(follower_ids, set([user_raoul.partner_id.id, user_admin.partner_id.id]), 'Admin and Raoul should be the only 2 Pigs group followers')
follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
self.assertEqual(len(follower_ids), 2, 'There should be 2 Pigs fans')
self.assertEqual(set(follower_ids), set([user_raoul.partner_id.id, user_admin.partner_id.id]), 'Admin and Raoul should be the only 2 Pigs fans')
# Unsubscribe Raoul twice through message_unsubscribe_users
group_pigs.message_unsubscribe_users([user_raoul_id, user_raoul_id])
group_pigs.refresh()
follower_ids = set([follower.id for follower in group_pigs.message_follower_ids])
self.assertEqual(follower_ids, set([user_admin.partner_id.id]), 'Admin must be the only Pigs fan')
follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
self.assertEqual(follower_ids, [user_admin.partner_id.id], 'Admin must be the only Pigs fan')
def test_20_message_post(self):
""" Tests designed for message_post. """