summaryrefslogtreecommitdiff
path: root/addons/mail/wizard/mail_compose_message.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/mail/wizard/mail_compose_message.py')
-rw-r--r--addons/mail/wizard/mail_compose_message.py272
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: