diff options
Diffstat (limited to 'addons/mail/wizard/mail_compose_message.py')
| -rw-r--r-- | addons/mail/wizard/mail_compose_message.py | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py new file mode 100644 index 00000000000..0bfaf06890a --- /dev/null +++ b/addons/mail/wizard/mail_compose_message.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/> +# +############################################################################## + +import re + +import tools +from mail.mail_message import to_email +from osv import osv +from osv import fields +from tools.safe_eval import safe_eval as eval +from tools.safe_eval import literal_eval +from tools.translate import _ + +# main mako-like expression pattern +EXPRESSION_PATTERN = re.compile('(\$\{.+?\})') + +class mail_compose_message(osv.osv_memory): + """Generic E-mail composition wizard. This wizard is meant to be inherited + at model and view level to provide specific wizard features. + + The behavior of the wizard can be modified through the use of context + parameters, among which are: + + * mail.compose.message.mode: if set to 'reply', the wizard is in + reply mode and pre-populated with the original quote. + If set to 'mass_mail', the wizard is in mass mailing + where the mail details can contain template placeholders + that will be merged with actual data before being sent + to each recipient. Recipients will be derived from the + records determined via ``context['active_model']`` and + ``context['active_ids']``. + * active_model: model name of the document to which the mail being + composed is related + * active_id: id of the document to which the mail being composed is + related, or id of the message to which user is replying, + in case ``mail.compose.message.mode == 'reply'`` + * active_ids: ids of the documents to which the mail being composed is + related, in case ``mail.compose.message.mode == 'mass_mail'``. + """ + _name = 'mail.compose.message' + _inherit = 'mail.message.common' + _description = 'E-mail composition wizard' + + def default_get(self, cr, uid, fields, context=None): + """Overridden to provide specific defaults depending on the context + parameters. + + :param dict context: several context values will modify the behavior + of the wizard, cfr. the class description. + """ + if context is None: + context = {} + result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context) + vals = {} + reply_mode = context.get('mail.compose.message.mode') == 'reply' + if (not reply_mode) and context.get('active_model') and context.get('active_id'): + # normal mode when sending an email related to any document, as specified by + # active_model and active_id in context + vals = self.get_value(cr, uid, context.get('active_model'), context.get('active_id'), context) + elif reply_mode and context.get('active_id'): + # reply mode, consider active_id is the ID of a mail.message to which we're + # replying + vals = self.get_message_data(cr, uid, int(context['active_id']), context) + else: + # default mode + result['model'] = context.get('active_model', False) + if vals: + for field in fields: + result.update({field : vals.get(field, False)}) + + # link to model and record if not done yet + if not result.get('model') or not result.get('res_id'): + active_model = context.get('active_model') + res_id = context.get('active_id') + if active_model and active_model not in (self._name, 'mail.message'): + result['model'] = active_model + if res_id: + result['res_id'] = res_id + + # Try to provide default email_from if not specified yet + if not result.get('email_from'): + current_user = self.pool.get('res.users').browse(cr, uid, uid, context) + result['email_from'] = current_user.user_email or False + return result + + _columns = { + 'attachment_ids': fields.many2many('ir.attachment','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'), + 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"), + 'filter_id': fields.many2one('ir.filters', 'Filters'), + } + + def get_value(self, cr, uid, model, res_id, context=None): + """Returns a defaults-like dict with initial values for the composition + wizard when sending an email related to the document record identified + by ``model`` and ``res_id``. + + The default implementation returns an empty dictionary, and is meant + to be overridden by subclasses. + + :param str model: model name of the document record this mail is related to. + :param int res_id: id of the document record this mail is related to. + :param dict context: several context values will modify the behavior + of the wizard, cfr. the class description. + """ + return {} + + def get_message_data(self, cr, uid, message_id, context=None): + """Returns a defaults-like dict with initial values for the composition + wizard when replying to the given message (e.g. including the quote + of the initial message, and the correct recipient). + Should not be called unless ``context['mail.compose.message.mode'] == 'reply'``. + + :param int message_id: id of the mail.message to which the user + is replying. + :param dict context: several context values will modify the behavior + of the wizard, cfr. the class description. + When calling this method, the ``'mail'`` value + in the context should be ``'reply'``. + """ + if context is None: + context = {} + result = {} + mail_message = self.pool.get('mail.message') + if message_id: + message_data = mail_message.browse(cr, uid, message_id, context) + subject = tools.ustr(message_data.subject or '') + # we use the plain text version of the original mail, by default, + # as it is easier to quote than the HTML version. + # XXX TODO: make it possible to switch to HTML on the fly + current_user = self.pool.get('res.users').browse(cr, uid, uid, context) + body = message_data.body_text or current_user.signature + if context.get('mail.compose.message.mode') == 'reply': + sent_date = _('On %(date)s, ') % {'date': message_data.date} if message_data.date else '' + sender = _('%(sender_name)s wrote:') % {'sender_name': tools.ustr(message_data.email_from or _('You'))} + quoted_body = '> %s' % tools.ustr(body.replace('\n', "\n> ") or '') + body = '\n'.join(["\n", (sent_date + sender), quoted_body]) + body += "\n" + current_user.signature + re_prefix = _("Re:") + if not (subject.startswith('Re:') or subject.startswith(re_prefix)): + subject = "%s %s" % (re_prefix, subject) + result.update({ + 'subtype' : 'plain', # default to the text version due to quoting + 'body_text' : body, + 'subject' : subject, + 'attachment_ids' : [], + 'model' : message_data.model or False, + 'res_id' : message_data.res_id or False, + 'email_from' : current_user.user_email or message_data.email_to or False, + 'email_to' : message_data.reply_to or message_data.email_from or False, + 'email_cc' : message_data.email_cc or False, + 'user_id' : uid, + + # pass msg-id and references of mail we're replying to, to construct the + # new ones later when sending + 'message_id' : message_data.message_id or False, + 'references' : message_data.references and tools.ustr(message_data.references) or False, + }) + return result + + def send_mail(self, cr, uid, ids, context=None): + '''Process the wizard contents and proceed with sending the corresponding + email(s), rendering any template patterns on the fly if needed. + If the wizard is in mass-mail mode (context['mail.compose.message.mode'] is + set to ``'mass_mail'``), the resulting email(s) are scheduled for being + sent the next time the mail.message scheduler runs, or the next time + ``mail.message.process_email_queue`` is called. + Otherwise the new message is sent immediately. + + :param dict context: several context values will modify the behavior + of the wizard, cfr. the class description. + ''' + if context is None: + context = {} + mail_message = self.pool.get('mail.message') + for mail in self.browse(cr, uid, ids, context=context): + attachment = {} + for attach in mail.attachment_ids: + attachment[attach.datas_fname] = attach.datas + references = None + headers = {} + + body = mail.body_html if mail.subtype == 'html' else mail.body_text + + # Reply Email + if context.get('mail.compose.message.mode') == 'reply' and mail.message_id: + references = (mail.references or '') + " " + mail.message_id + headers['In-Reply-To'] = mail.message_id + + if context.get('mail.compose.message.mode') == 'mass_mail': + # Mass mailing: must render the template patterns + if context.get('active_ids') and context.get('active_model'): + active_ids = context['active_ids'] + active_model = context['active_model'] + else: + active_model = mail.model + active_model_pool = self.pool.get(active_model) + active_ids = active_model_pool.search(cr, uid, literal_eval(mail.filter_id.domain), context=literal_eval(mail.filter_id.context)) + + for active_id in active_ids: + subject = self.render_template(cr, uid, mail.subject, active_model, active_id) + rendered_body = self.render_template(cr, uid, body, active_model, active_id) + email_from = self.render_template(cr, uid, mail.email_from, active_model, active_id) + email_to = self.render_template(cr, uid, mail.email_to, active_model, active_id) + email_cc = self.render_template(cr, uid, mail.email_cc, active_model, active_id) + email_bcc = self.render_template(cr, uid, mail.email_bcc, active_model, active_id) + reply_to = self.render_template(cr, uid, mail.reply_to, active_model, active_id) + + # in mass-mailing mode we only schedule the mail for sending, it will be + # processed as soon as the mail scheduler runs. + mail_message.schedule_with_attach(cr, uid, email_from, to_email(email_to), subject, rendered_body, + model=mail.model, email_cc=to_email(email_cc), email_bcc=to_email(email_bcc), reply_to=reply_to, + attachments=attachment, references=references, res_id=int(mail.res_id), + subtype=mail.subtype, headers=headers, context=context) + else: + # normal mode - no mass-mailing + msg_id = mail_message.schedule_with_attach(cr, uid, mail.email_from, to_email(mail.email_to), mail.subject, body, + model=mail.model, email_cc=to_email(mail.email_cc), email_bcc=to_email(mail.email_bcc), reply_to=mail.reply_to, + attachments=attachment, references=references, res_id=int(mail.res_id), + subtype=mail.subtype, headers=headers, context=context) + # in normal mode, we send the email immediately, as the user expects us to (delay should be sufficiently small) + mail_message.send(cr, uid, [msg_id], context) + + return {'type': 'ir.actions.act_window_close'} + + def render_template(self, cr, uid, template, model, res_id, context=None): + """Render the given template text, replace mako-like expressions ``${expr}`` + with the result of evaluating these expressions with an evaluation context + containing: + + * ``user``: browse_record of the current user + * ``object``: browse_record of the document record this mail is + related to + * ``context``: the context passed to the mail composition wizard + + :param str template: the template text to render + :param str model: model name of the document record this mail is related to. + :param int res_id: id of the document record this mail is related to. + """ + if context is None: + context = {} + def merge(match): + exp = str(match.group()[2:-1]).strip() + result = eval(exp, + { + 'user' : self.pool.get('res.users').browse(cr, uid, uid, context=context), + 'object' : self.pool.get(model).browse(cr, uid, res_id, context=context), + 'context': dict(context), # copy context to prevent side-effects of eval + }) + if result in (None, False): + return "" + return tools.ustr(result) + return template and EXPRESSION_PATTERN.sub(merge, template) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: |
