diff --git a/addons/mail/__init__.py b/addons/mail/__init__.py index b19aad1afe3..9c1f0d70f6f 100644 --- a/addons/mail/__init__.py +++ b/addons/mail/__init__.py @@ -21,10 +21,10 @@ import mail_alias import mail_message +import mail_mail import mail_thread import mail_group import mail_subscription -import ir_needaction import res_users import res_partner import report diff --git a/addons/mail/ir_needaction.py b/addons/mail/ir_needaction.py deleted file mode 100644 index 8193ac4f385..00000000000 --- a/addons/mail/ir_needaction.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2012-today OpenERP SA () -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see -# -############################################################################## - -from osv import osv - -class ir_needaction_mixin(osv.Model): - """ Update of ir.needaction_mixin class - - override the get_needaction_user_ids method to define the default - mail gateway need_action: when the object is unread, the object - responsible has an action to perform. - """ - _name = 'ir.needaction_mixin' - _inherit = ['ir.needaction_mixin'] - - def get_needaction_user_ids(self, cr, uid, ids, context=None): - """ Returns the user_ids that have to perform an action. It the - document mail state is unread (False), return object.user_id.id - as need_action uid. - :return: dict { record_id: [user_ids], } - """ - result = super(ir_needaction_mixin, self).get_needaction_user_ids(cr, uid, ids, context=context) - for obj in self.browse(cr, uid, ids, context=context): - if obj.message_state == False and obj.user_id: - result[obj.id].append(obj.user_id.id) - return result - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/mail_group.py b/addons/mail/mail_group.py index 61bd21439a1..e93306aed02 100644 --- a/addons/mail/mail_group.py +++ b/addons/mail/mail_group.py @@ -34,13 +34,8 @@ class mail_group(osv.osv): subscription/follow mechanism of OpenSocial. A mail group has nothing in common with res.users.group. Additional information on fields: - - ``member_ids``: user member of the groups are calculated with - ``message_get_subscribers`` method from mail.thread - - ``member_count``: calculated with member_ids - - ``is_subscriber``: calculated with member_ids - """ - + _description = 'Discussion group' _name = 'mail.group' _inherit = ['mail.thread'] @@ -51,42 +46,15 @@ class mail_group(osv.osv): for obj in self.browse(cr, uid, ids, context=context): result[obj.id] = tools.image_get_resized_images(obj.image) return result - + def _set_image(self, cr, uid, id, name, value, args, context=None): return self.write(cr, uid, [id], {'image': tools.image_resize_image_big(value)}, context=context) - - def get_member_ids(self, cr, uid, ids, field_names, args, context=None): - if context is None: - context = {} - result = dict.fromkeys(ids) - for id in ids: - result[id] = {} - result[id]['member_ids'] = self.message_get_subscribers(cr, uid, [id], context=context) - result[id]['member_count'] = len(result[id]['member_ids']) - result[id]['is_subscriber'] = uid in result[id]['member_ids'] - return result - - def search_member_ids(self, cr, uid, obj, name, args, context=None): - if context is None: - context = {} - sub_obj = self.pool.get('mail.subscription') - sub_ids = sub_obj.search(cr, uid, ['&', ('res_model', '=', obj._name), ('user_id', '=', args[0][2])], context=context) - subs = sub_obj.read(cr, uid, sub_ids, context=context) - return [('id', 'in', map(itemgetter('res_id'), subs))] - - def get_last_month_msg_nbr(self, cr, uid, ids, name, args, context=None): - result = {} - for id in ids: - lower_date = (DT.datetime.now() - DT.timedelta(days=30)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT) - result[id] = self.message_search(cr, uid, [id], limit=None, domain=[('date', '>=', lower_date)], count=True, context=context) - return result - + def _get_default_image(self, cr, uid, context=None): image_path = openerp.modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png') return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64')) - + _columns = { - #'name': fields.char('Group Name', size=64, required=True), 'description': fields.text('Description'), 'menu_id': fields.many2one('ir.ui.menu', string='Related Menu', required=True, ondelete="cascade"), 'responsible_id': fields.many2one('res.users', string='Responsible', @@ -121,17 +89,7 @@ class mail_group(osv.osv): help="Small-sized photo of the group. It is automatically "\ "resized as a 50x50px image, with aspect ratio preserved. "\ "Use this field anywhere a small image is required."), - 'member_ids': fields.function(get_member_ids, fnct_search=search_member_ids, - type='many2many', relation='res.users', string='Group members', multi='get_member_ids', - deprecated='This field will be deleted in a few hours or days, so please do not use it.'), - 'member_count': fields.function(get_member_ids, type='integer', - string='Member count', multi='get_member_ids', - deprecated='This field will be deleted in a few hours or days, so please do not use it.'), - 'is_subscriber': fields.function(get_member_ids, type='boolean', - string='Joined', multi='get_member_ids'), - 'last_month_msg_nbr': fields.function(get_last_month_msg_nbr, type='integer', - string='Messages count for last month'), - 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", + 'alias_id': fields.many2one('mail.alias', 'Alias', ondelete="cascade", help="The email address associated with this group. New emails received will automatically " "create new topics."), } @@ -150,7 +108,7 @@ class mail_group(osv.osv): 'responsible_id': (lambda s, cr, uid, ctx: uid), 'image': _get_default_image, 'parent_id': _get_menu_parent, - 'alias_domain': False, # always hide alias during creation + 'alias_domain': False, # always hide alias during creation } def _subscribe_user_with_group_m2m_command(self, cr, uid, ids, group_ids_command, context=None): @@ -169,7 +127,7 @@ class mail_group(osv.osv): mail_alias = self.pool.get('mail.alias') if not vals.get('alias_id'): vals.pop('alias_name', None) # prevent errors during copy() - alias_id = mail_alias.create_unique_alias(cr, uid, + alias_id = mail_alias.create_unique_alias(cr, uid, # Using '+' allows using subaddressing for those who don't # have a catchall domain setup. {'alias_name': "group+"+vals['name']}, @@ -194,7 +152,7 @@ class mail_group(osv.osv): self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,'+str(newref), 'mail_group_id': mail_group_id}, context=context) mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context) - + if vals.get('group_ids'): self._subscribe_user_with_group_m2m_command(cr, uid, [mail_group_id], vals.get('group_ids'), context=context) diff --git a/addons/mail/mail_group_view.xml b/addons/mail/mail_group_view.xml index 7df68bf399e..dac5333024c 100644 --- a/addons/mail/mail_group_view.xml +++ b/addons/mail/mail_group_view.xml @@ -21,18 +21,17 @@ -
+

- +
diff --git a/addons/mail/mail_mail.py b/addons/mail/mail_mail.py new file mode 100644 index 00000000000..b960745ec68 --- /dev/null +++ b/addons/mail/mail_mail.py @@ -0,0 +1,268 @@ + +class mail_mail(osv.Model): + """ + 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': 'message_id'} + _columns = { + 'message_id': fields.many2one('mail.message', 'Message', required=True), + # why do we need this on the mail, why not on the company? + 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1), + 'state': fields.selection([ + ('outgoing', 'Outgoing'), + ('sent', 'Sent'), + ('received', 'Received'), + ('exception', 'Delivery Failed'), + ('cancel', 'Cancelled'), + ], 'Status', readonly=True), + 'auto_delete': fields.boolean('Auto Delete', + help="Permanently delete this email after sending it, to save space"), + + # FP Note: to be removed, in attachment if needed + 'original': fields.binary('Original', readonly=1, + help="Original version of the message, as it was sent on the network"), + # End FP Note + + # FP Note: I propose to remove these fields and put email in attachment as an option + 'email_from': fields.char('From', size=128, help='Message sender, taken from user preferences.'), + 'email_to': fields.text('To', help='Message recipients'), + 'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'), + 'email_bcc': fields.char('Bcc', size=256, help='Blind carbon copy message recipients'), + 'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'), + 'content_subtype': fields.char('Message content subtype', size=32, + oldname="subtype", readonly=1, + help="Type of message, usually 'html' or 'plain', used to select "\ + "plain-text or rich-text contents accordingly"), + 'body_html': fields.html('Rich-text Contents', help="Rich-text/HTML version of the message"), + + } + + _defaults = { + 'state': 'received', + 'content_subtype': 'plain', + } + + # FP Note: do we need this method ? + # We should just ask to create the message before scheduling it + def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, type='email', + email_cc=None, email_bcc=None, reply_to=False, partner_ids=None, attachments=None, + message_id=False, references=False, res_id=False, content_subtype='plain', + headers=None, mail_server_id=False, auto_delete=False, context=None): + """ Schedule sending a new email message, to be sent the next time the + mail scheduler runs, or the next time :meth:`process_email_queue` is + called explicitly. + + :param string email_from: sender email address + :param list email_to: list of recipient addresses (to be joined with commas) + :param string subject: email subject (no pre-encoding/quoting necessary) + :param string body: email body, according to the ``content_subtype`` + (by default, plaintext). If html content_subtype is used, the + message will be automatically converted to plaintext and wrapped + in multipart/alternative. + :param list email_cc: optional list of string values for CC header + (to be joined with commas) + :param list email_bcc: optional list of string values for BCC header + (to be joined with commas) + :param string model: optional model name of the document this mail + is related to (this will also be used to generate a tracking id, + used to match any response related to the same document) + :param int res_id: optional resource identifier this mail is related + to (this will also be used to generate a tracking id, used to + match any response related to the same document) + :param string reply_to: optional value of Reply-To header + :param partner_ids: destination partner_ids + :param string content_subtype: optional mime content_subtype for + the text body (usually 'plain' or 'html'), must match the format + of the ``body`` parameter. Default is 'plain', making the content + part of the mail "text/plain". + :param dict attachments: map of filename to filecontents, where + filecontents is a string containing the bytes of the attachment + :param dict headers: optional map of headers to set on the outgoing + mail (may override the other headers, including Subject, + Reply-To, Message-Id, etc.) + :param int mail_server_id: optional id of the preferred outgoing + mail server for this mail + :param bool auto_delete: optional flag to turn on auto-deletion of + the message after it has been successfully sent (default to False) + """ + if context is None: + context = {} + if attachments is None: + attachments = {} + if partner_ids is None: + partner_ids = [] + attachment_obj = self.pool.get('ir.attachment') + for param in (email_to, email_cc, email_bcc): + if param and not isinstance(param, list): + param = [param] + msg_vals = { + 'subject': subject, + 'date': fields.datetime.now(), + 'user_id': uid, + 'model': model, + 'res_id': res_id, + 'type': type, + 'body_text': body if content_subtype != 'html' else False, + 'body_html': body if content_subtype == 'html' else False, + 'email_from': email_from, + 'email_to': email_to and ','.join(email_to) or '', + 'email_cc': email_cc and ','.join(email_cc) or '', + 'email_bcc': email_bcc and ','.join(email_bcc) or '', + 'partner_ids': partner_ids, + 'reply_to': reply_to, + 'message_id': message_id, + 'references': references, + 'content_subtype': content_subtype, + 'headers': headers, # serialize the dict on the fly + 'mail_server_id': mail_server_id, + 'state': 'outgoing', + 'auto_delete': auto_delete + } + email_msg_id = self.create(cr, uid, msg_vals, context) + attachment_ids = [] + for fname, fcontent in attachments.iteritems(): + attachment_data = { + 'name': fname, + 'datas_fname': fname, + 'datas': fcontent and fcontent.encode('base64'), + 'res_model': self._name, + 'res_id': email_msg_id, + } + if context.has_key('default_type'): + del context['default_type'] + attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context)) + if attachment_ids: + self.write(cr, uid, email_msg_id, { 'attachment_ids': [(6, 0, attachment_ids)]}, context=context) + return email_msg_id + + def mark_outgoing(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state':'outgoing'}, context=context) + + def cancel(self, cr, uid, ids, context=None): + return self.write(cr, uid, ids, {'state':'cancel'}, context=context) + + def process_email_queue(self, cr, uid, ids=None, context=None): + """Send immediately queued messages, committing after each + message is sent - this is not transactional and should + not be called during another transaction! + + :param list ids: optional list of emails ids to send. If passed + no search is performed, and these ids are used + instead. + :param dict context: if a 'filters' key is present in context, + this value will be used as an additional + filter to further restrict the outgoing + messages to send (by default all 'outgoing' + messages are sent). + """ + if context is None: + context = {} + if not ids: + filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')] + if 'filters' in context: + filters.extend(context['filters']) + ids = self.search(cr, uid, filters, context=context) + res = None + try: + # Force auto-commit - this is meant to be called by + # the scheduler, and we can't allow rolling back the status + # of previously sent emails! + res = self.send(cr, uid, ids, auto_commit=True, context=context) + except Exception: + _logger.exception("Failed processing mail queue") + return res + + def _postprocess_sent_message(self, cr, uid, message, context=None): + """Perform any post-processing necessary after sending ``message`` + successfully, including deleting it completely along with its + attachment if the ``auto_delete`` flag of the message was set. + Overridden by subclasses for extra post-processing behaviors. + + :param browse_record message: the message that was just sent + :return: True + """ + if message.auto_delete: + self.pool.get('ir.attachment').unlink(cr, uid, + [x.id for x in message.attachment_ids + if x.res_model == self._name and x.res_id == message.id], + context=context) + 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. + + :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 + """ + ir_mail_server = self.pool.get('ir.mail_server') + self.write(cr, uid, ids, {'state': 'outgoing'}, context=context) + for message in self.browse(cr, uid, ids, context=context): + try: + attachments = [] + for attach in message.attachment_ids: + attachments.append((attach.datas_fname, base64.b64decode(attach.datas))) + + body = message.body_html if message.content_subtype == 'html' else message.body_text + body_alternative = None + content_subtype_alternative = None + if message.content_subtype == 'html' and message.body_text: + # we have a plain text alternative prepared, pass it to + # build_message instead of letting it build one + body_alternative = message.body_text + content_subtype_alternative = 'plain' + + # handle destination_partners + partner_ids_email_to = '' + for partner in message.partner_ids: + partner_ids_email_to += '%s ' % (partner.email or '') + message_email_to = '%s %s' % (partner_ids_email_to, message.email_to or '') + + # 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=mail_tools_to_email(message_email_to), + subject=message.subject, + body=body, + body_alternative=body_alternative, + email_cc=mail_tools_to_email(message.email_cc), + email_bcc=mail_tools_to_email(message.email_bcc), + reply_to=message.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)), + subtype=message.content_subtype, + subtype_alternative=content_subtype_alternative, + headers=message.headers and ast.literal_eval(message.headers)) + res = ir_mail_server.send_email(cr, uid, msg, + mail_server_id=message.mail_server_id.id, + context=context) + if res: + message.write({'state':'sent', 'message_id': res, 'email_to': message_email_to}) + else: + message.write({'state':'exception', 'email_to': message_email_to}) + message.refresh() + if message.state == 'sent': + self._postprocess_sent_message(cr, uid, message, context=context) + except Exception: + _logger.exception('failed sending mail.message %s', message.id) + message.write({'state':'exception'}) + + if auto_commit == True: + cr.commit() + return True + diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index ee137b635ce..227a9b81f5b 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -54,93 +54,15 @@ def mail_tools_to_email(text): if not text: return [] return re.findall(r'([^ ,<@]+@[^> ,]+)', text) -# TODO: remove that after cleaning -def to_email(text): - return mail_tools_to_email(text) - -# FP Note: I propose to merge mail.message and mail.message.common -# there is a conflict for the field parent_id -# I don't understand the usage of mail.message.common -class mail_message_common(osv.TransientModel): - """ Common abstract class for holding the main attributes of a - message object. It could be reused as parent model for any - database model or wizard screen that needs to hold a kind of - message. - All internal logic should be in another model while this - model holds the basics of a message. For example, a wizard for writing - emails should inherit from this class and not from mail.message.""" - - def get_record_name(self, cr, uid, ids, name, arg, context=None): - result = dict.fromkeys(ids, '') - for message in self.browse(cr, uid, ids, context=context): - if not message.model or not message.res_id: - continue - result[message.id] = self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1] - return result - - def name_get(self, cr, uid, ids, context=None): - res = [] - for message in self.browse(cr, uid, ids, context=context): - name = '' - if message.subject: - name = '%s: ' % (message.subject) - if message.body_text: - name = name + message.body_text[0:20] - res.append((message.id, name)) - return res - - _name = 'mail.message.common' - _rec_name = 'subject' - _columns = { - 'model': fields.char('Related Document Model', size=128, select=1), - 'res_id': fields.integer('Related Document ID', select=1), - 'record_name': fields.function(get_record_name, type='string', - string='Message Record Name', - help="Name get of the related document."), - - 'subject': fields.char('Subject', size=128), - 'date': fields.datetime('Date'), - - # FP Note: I propose to remove these fields and put email in attachment as an option - 'email_from': fields.char('From', size=128, help='Message sender, taken from user preferences.'), - 'email_to': fields.text('To', help='Message recipients'), - 'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'), - 'email_bcc': fields.char('Bcc', size=256, help='Blind carbon copy message recipients'), - 'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'), - 'headers': fields.text('Message Headers', readonly=1, - help="Full message headers, e.g. SMTP session headers (usually available on inbound messages only)"), - 'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1), - 'content_subtype': fields.char('Message content subtype', size=32, - oldname="subtype", readonly=1, - help="Type of message, usually 'html' or 'plain', used to select "\ - "plain-text or rich-text contents accordingly"), - 'body_html': fields.html('Rich-text Contents', help="Rich-text/HTML version of the message"), - # END FP Note - - 'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1), - 'body': fields.text('Text Contents', help="Plain-text version of the message", required=True), - - 'parent_id': fields.many2one('mail.message.common', 'Parent Message', - select=True, ondelete='set null', - help="Parent message, used for displaying as threads with hierarchy"), - } - - _defaults = { - 'content_subtype': 'plain', - 'date': (lambda *a: fields.datetime.now()), - } - class mail_message(osv.Model): """Model holding messages: system notification (replacing res.log - notifications), comments (for OpenChatter feature) and - RFC2822 email messages. This model also provides facilities to - parse, queue and send new email messages. Type of messages - are differentiated using the 'type' column. """ + notifications), comments (for OpenChatter feature). This model also + provides facilities to parse new email messages. Type of messages are + differentiated using the 'type' column. """ _name = 'mail.message' - _inherit = 'mail.message.common' - _description = 'Mail Message (email, comment, notification)' - _order = 'date desc' + _description = 'Message' + _order = 'id desc' # FP Note: can we remove these two methods ? def open_document(self, cr, uid, ids, context=None): @@ -183,7 +105,28 @@ class mail_message(osv.Model): return action_data # END FP Note + def get_record_name(self, cr, uid, ids, name, arg, context=None): + result = dict.fromkeys(ids, '') + for message in self.browse(cr, uid, ids, context=context): + if not message.model or not message.res_id: + continue + result[message.id] = self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1] + return result + + def name_get(self, cr, uid, ids, context=None): + res = [] + for message in self.browse(cr, uid, ids, context=context): + name = '' + if message.subject: + name = '%s: ' % (message.subject) + if message.body_text: + name = name + message.body_text[0:20] + res.append((message.id, name)) + return res + + _columns = { + # should we keep a distinction between email and comment ? 'type': fields.selection([ ('email', 'email'), ('comment', 'Comment'), @@ -191,49 +134,55 @@ class mail_message(osv.Model): ], 'Type', help="Message type: email for email message, notification for system "\ "message, comment for other messages such as user replies"), - # FP Note: ro be removed - 'partner_id': fields.many2one('res.partner', 'Related partner', - help="Deprecated field. Use partner_ids instead."), - # END FP Note + # partner_id should be renamed into author_id + 'author_id': fields.many2one('res.partner', 'Author', required=True), + + # this is redundant with notifications ? 'partner_ids': fields.many2many('res.partner', 'mail_message_destination_partner_rel', 'message_id', 'partner_id', 'Destination partners', help="When sending emails through the social network composition wizard"\ "you may choose to send a copy of the mail to partners."), - 'user_id': fields.many2one('res.users', 'Author', readonly=1), - 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'), - 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1), - 'state': fields.selection([ - ('outgoing', 'Outgoing'), - ('sent', 'Sent'), - ('received', 'Received'), - ('exception', 'Delivery Failed'), - ('cancel', 'Cancelled'), - ], 'Status', readonly=True), - 'auto_delete': fields.boolean('Auto Delete', - help="Permanently delete this email after sending it, to save space"), - # FP Note: to be removed, in attachment if needed - 'original': fields.binary('Original', readonly=1, - help="Original version of the message, as it was sent on the network"), - # End FP Note + # 'user_id': fields.many2one('res.users', 'Author', readonly=1), + + # why don't we attach the file to the message using res_id, res_model of the attachment ? + 'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'), 'parent_id': fields.many2one('mail.message', 'Parent Message', select=True, ondelete='set null', help="Parent message, used for displaying as threads with hierarchy"), 'child_ids': fields.one2many('mail.message', 'parent_id', 'Child Messages'), + + + 'model': fields.char('Related Document Model', size=128, select=1), + 'res_id': fields.integer('Related Document ID', select=1), + 'record_name': fields.function(get_record_name, type='string', + string='Message Record Name', + help="Name get of the related document."), + + 'subject': fields.char('Subject', size=128), + 'date': fields.datetime('Date'), + + # FP Note: do we need this ? + 'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1), + + # END FP Note + + 'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1), + 'body': fields.text('Content', help="Content of Message", required=True), + } - _defaults = { 'type': 'email', - 'state': 'received', + 'date': (lambda *a: fields.datetime.now()), } - + #------------------------------------------------------ # Email api #------------------------------------------------------ - + def init(self, cr): cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""") if not cr.fetchone(): @@ -260,7 +209,7 @@ class mail_message(osv.Model): 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) - + def create(self, cr, uid, values, context=None): newid = super(mail_message, self).create(cr, uid, values, context) self.check(cr, uid, [newid], mode='create', context=context) @@ -273,9 +222,7 @@ class mail_message(osv.Model): for follower in modobj.follower_ids: if follower.id <> uid: follower_notify.append(follower.id) - self.pool.get('mail.notification').notify(cr, uid, follower_notify, newid, context=context) - return newid def read(self, cr, uid, ids, fields_to_read=None, context=None, load='_classic_read'): @@ -289,7 +236,7 @@ class mail_message(osv.Model): self.check(cr, uid, [id], 'read', context=context) default.update(message_id=False, original=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) @@ -299,136 +246,7 @@ class mail_message(osv.Model): self.check(cr, uid, ids, 'unlink', context=context) return super(mail_message, self).unlink(cr, uid, ids, context) - # FP Note: do we need this method ? - # We should just ask to create the message before scheduling it - def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, type='email', - email_cc=None, email_bcc=None, reply_to=False, partner_ids=None, attachments=None, - message_id=False, references=False, res_id=False, content_subtype='plain', - headers=None, mail_server_id=False, auto_delete=False, context=None): - """ Schedule sending a new email message, to be sent the next time the - mail scheduler runs, or the next time :meth:`process_email_queue` is - called explicitly. - - :param string email_from: sender email address - :param list email_to: list of recipient addresses (to be joined with commas) - :param string subject: email subject (no pre-encoding/quoting necessary) - :param string body: email body, according to the ``content_subtype`` - (by default, plaintext). If html content_subtype is used, the - message will be automatically converted to plaintext and wrapped - in multipart/alternative. - :param list email_cc: optional list of string values for CC header - (to be joined with commas) - :param list email_bcc: optional list of string values for BCC header - (to be joined with commas) - :param string model: optional model name of the document this mail - is related to (this will also be used to generate a tracking id, - used to match any response related to the same document) - :param int res_id: optional resource identifier this mail is related - to (this will also be used to generate a tracking id, used to - match any response related to the same document) - :param string reply_to: optional value of Reply-To header - :param partner_ids: destination partner_ids - :param string content_subtype: optional mime content_subtype for - the text body (usually 'plain' or 'html'), must match the format - of the ``body`` parameter. Default is 'plain', making the content - part of the mail "text/plain". - :param dict attachments: map of filename to filecontents, where - filecontents is a string containing the bytes of the attachment - :param dict headers: optional map of headers to set on the outgoing - mail (may override the other headers, including Subject, - Reply-To, Message-Id, etc.) - :param int mail_server_id: optional id of the preferred outgoing - mail server for this mail - :param bool auto_delete: optional flag to turn on auto-deletion of - the message after it has been successfully sent (default to False) - """ - if context is None: - context = {} - if attachments is None: - attachments = {} - if partner_ids is None: - partner_ids = [] - attachment_obj = self.pool.get('ir.attachment') - for param in (email_to, email_cc, email_bcc): - if param and not isinstance(param, list): - param = [param] - msg_vals = { - 'subject': subject, - 'date': fields.datetime.now(), - 'user_id': uid, - 'model': model, - 'res_id': res_id, - 'type': type, - 'body_text': body if content_subtype != 'html' else False, - 'body_html': body if content_subtype == 'html' else False, - 'email_from': email_from, - 'email_to': email_to and ','.join(email_to) or '', - 'email_cc': email_cc and ','.join(email_cc) or '', - 'email_bcc': email_bcc and ','.join(email_bcc) or '', - 'partner_ids': partner_ids, - 'reply_to': reply_to, - 'message_id': message_id, - 'references': references, - 'content_subtype': content_subtype, - 'headers': headers, # serialize the dict on the fly - 'mail_server_id': mail_server_id, - 'state': 'outgoing', - 'auto_delete': auto_delete - } - email_msg_id = self.create(cr, uid, msg_vals, context) - attachment_ids = [] - for fname, fcontent in attachments.iteritems(): - attachment_data = { - 'name': fname, - 'datas_fname': fname, - 'datas': fcontent and fcontent.encode('base64'), - 'res_model': self._name, - 'res_id': email_msg_id, - } - if context.has_key('default_type'): - del context['default_type'] - attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context)) - if attachment_ids: - self.write(cr, uid, email_msg_id, { 'attachment_ids': [(6, 0, attachment_ids)]}, context=context) - return email_msg_id - - def mark_outgoing(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'outgoing'}, context=context) - - def cancel(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'cancel'}, context=context) - - def process_email_queue(self, cr, uid, ids=None, context=None): - """Send immediately queued messages, committing after each - message is sent - this is not transactional and should - not be called during another transaction! - - :param list ids: optional list of emails ids to send. If passed - no search is performed, and these ids are used - instead. - :param dict context: if a 'filters' key is present in context, - this value will be used as an additional - filter to further restrict the outgoing - messages to send (by default all 'outgoing' - messages are sent). - """ - if context is None: - context = {} - if not ids: - filters = ['&', ('state', '=', 'outgoing'), ('type', '=', 'email')] - if 'filters' in context: - filters.extend(context['filters']) - ids = self.search(cr, uid, filters, context=context) - res = None - try: - # Force auto-commit - this is meant to be called by - # the scheduler, and we can't allow rolling back the status - # of previously sent emails! - res = self.send(cr, uid, ids, auto_commit=True, context=context) - except Exception: - _logger.exception("Failed processing mail queue") - return res - + # FP Note: to be simplified, mail.message fields only, not mail.mail def parse_message(self, message, save_original=False, context=None): """Parses a string or email.message.Message representing an RFC-2822 email, and returns a generic dict holding the @@ -585,96 +403,4 @@ class mail_message(osv.Model): msg['sub_type'] = msg['content_subtype'] or 'plain' return msg - def _postprocess_sent_message(self, cr, uid, message, context=None): - """Perform any post-processing necessary after sending ``message`` - successfully, including deleting it completely along with its - attachment if the ``auto_delete`` flag of the message was set. - Overridden by subclasses for extra post-processing behaviors. - :param browse_record message: the message that was just sent - :return: True - """ - if message.auto_delete: - self.pool.get('ir.attachment').unlink(cr, uid, - [x.id for x in message.attachment_ids - if x.res_model == self._name and x.res_id == message.id], - context=context) - 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. - - :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 - """ - ir_mail_server = self.pool.get('ir.mail_server') - self.write(cr, uid, ids, {'state': 'outgoing'}, context=context) - for message in self.browse(cr, uid, ids, context=context): - try: - attachments = [] - for attach in message.attachment_ids: - attachments.append((attach.datas_fname, base64.b64decode(attach.datas))) - - body = message.body_html if message.content_subtype == 'html' else message.body_text - body_alternative = None - content_subtype_alternative = None - if message.content_subtype == 'html' and message.body_text: - # we have a plain text alternative prepared, pass it to - # build_message instead of letting it build one - body_alternative = message.body_text - content_subtype_alternative = 'plain' - - # handle destination_partners - partner_ids_email_to = '' - for partner in message.partner_ids: - partner_ids_email_to += '%s ' % (partner.email or '') - message_email_to = '%s %s' % (partner_ids_email_to, message.email_to or '') - - # 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=mail_tools_to_email(message_email_to), - subject=message.subject, - body=body, - body_alternative=body_alternative, - email_cc=mail_tools_to_email(message.email_cc), - email_bcc=mail_tools_to_email(message.email_bcc), - reply_to=message.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)), - subtype=message.content_subtype, - subtype_alternative=content_subtype_alternative, - headers=message.headers and ast.literal_eval(message.headers)) - res = ir_mail_server.send_email(cr, uid, msg, - mail_server_id=message.mail_server_id.id, - context=context) - if res: - message.write({'state':'sent', 'message_id': res, 'email_to': message_email_to}) - else: - message.write({'state':'exception', 'email_to': message_email_to}) - message.refresh() - if message.state == 'sent': - self._postprocess_sent_message(cr, uid, message, context=context) - except Exception: - _logger.exception('failed sending mail.message %s', message.id) - message.write({'state':'exception'}) - - if auto_commit == True: - cr.commit() - return True - - - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index d6e33497d98..d3f80c08513 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -70,6 +70,19 @@ class mail_thread(osv.Model): _description = 'Email Thread' # TODO: may be we should make it _inherit ir.needaction + def _get_is_subscriber(self, cr, uid, ids, name, args, context=None): + subobj = self.pool.get('mail.subscription') + subids = subobj.search(cr, uid, [ + ('res_model','=',self._name), + ('res_id', 'in', ids), + ('user_id','=',uid)], context=context) + result = dict.fromkeys(ids, False) + for sub in subobj.browse(cr, uid, subids, context=context): + result[res_id] = True + return result + + + def _get_message_data(self, cr, uid, ids, name, args, context=None): res = {} for id in ids: @@ -98,6 +111,8 @@ class mail_thread(osv.Model): return [('id','in',[])] _columns = { + 'message_is_subscriber': fields.function(_get_is_subscriber, + type='boolean', string='Is a Follower'), 'message_follower_ids': fields.one2many('mail.subscription', 'res_id', domain=lambda self: [('res_model','=',self._name)], string='Followers')