1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>
20 ##############################################################################
27 from osv import fields
28 from tools.safe_eval import safe_eval as eval
29 from tools.translate import _
31 from ..mail_message import to_email
33 # main mako-like expression pattern
34 EXPRESSION_PATTERN = re.compile('(\$\{.+?\})')
36 class mail_compose_message(osv.TransientModel):
37 """Generic Email composition wizard. This wizard is meant to be inherited
38 at model and view level to provide specific wizard features.
40 The behavior of the wizard can be modified through the use of context
41 parameters, among which are:
43 * mail.compose.message.mode: if set to 'reply', the wizard is in
44 reply to a previous message mode and pre-populated with the original
45 quote. If set to 'comment', it means you are writing a new message to
46 be attached to a document. If set to 'mass_mail', the wizard is in
47 mass mailing where the mail details can contain template placeholders
48 that will be merged with actual data before being sent to each
50 * active_model: model name of the document to which the mail being
52 * active_id: id of the document to which the mail being composed is
53 related, or id of the message to which user is replying,
54 in case ``mail.compose.message.mode == 'reply'``
55 * active_ids: ids of the documents to which the mail being composed is
56 related, in case ``mail.compose.message.mode == 'mass_mail'``.
58 _name = 'mail.compose.message'
59 _inherit = 'mail.message.common'
60 _description = 'Email composition wizard'
62 def default_get(self, cr, uid, fields, context=None):
63 """ Overridden to provide specific defaults depending on the context
67 - comment: default mode; active_model, active_id = model and ID of a
68 document we are commenting,
69 - reply: active_id = ID of a mail.message to which we are replying.
70 From this message we can find the related model and res_id,
71 - mass_mailing mode: active_model, active_id = model and ID of a
72 document we are commenting,
74 :param dict context: several context values will modify the behavior
75 of the wizard, cfr. the class description.
79 compose_mode = context.get('mail.compose.message.mode', 'comment')
80 active_model = context.get('active_model')
81 active_id = context.get('active_id')
82 result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
84 # get default values according to the composition mode
86 if compose_mode in ['reply']:
87 vals = self.get_message_data(cr, uid, int(context['active_id']), context=context)
88 elif compose_mode in ['comment', 'mass_mail'] and active_model and active_id:
89 vals = self.get_value(cr, uid, active_model, active_id, context)
92 result[field] = vals[field]
94 # link to model and record if not done yet
95 if not result.get('model') and active_model:
96 result['model'] = active_model
97 if not result.get('res_id') and active_id:
98 result['res_id'] = active_id
99 # if result['model'] == 'mail.message' and not result.get('parent_id'):
100 # result['parent_id'] = context.get('active_id')
102 # Try to provide default email_from if not specified yet
103 if not result.get('email_from'):
104 current_user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
105 result['email_from'] = current_user.user_email or False
110 'dest_partner_ids': fields.many2many('res.partner',
111 'email_message_send_partner_rel',
112 'wizard_id', 'partner_id', 'Destination partners',
113 help="When sending emails through the social network composition wizard"\
114 "you may choose to send a copy of the mail to partners."),
115 'attachment_ids': fields.many2many('ir.attachment','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'),
116 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
117 'filter_id': fields.many2one('ir.filters', 'Filters'),
120 def get_value(self, cr, uid, model, res_id, context=None):
121 """ Returns a defaults-like dict with initial values for the composition
122 wizard when sending an email related to the document record
123 identified by ``model`` and ``res_id``.
125 The default implementation returns an empty dictionary, and is meant
126 to be overridden by subclasses.
128 :param str model: model name of the document record this mail is
130 :param int res_id: id of the document record this mail is related to.
131 :param dict context: several context values will modify the behavior
132 of the wizard, cfr. the class description.
135 user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
139 'email_from': user.user_email or tools.config.get('email_from', False),
140 'body_html': '<br />---<br />' + tools.ustr(user.signature or ''),
144 def get_message_data(self, cr, uid, message_id, context=None):
145 """ Returns a defaults-like dict with initial values for the composition
146 wizard when replying to the given message (e.g. including the quote
147 of the initial message, and the correct recipient). It should not be
148 called unless ``context['mail.compose.message.mode'] == 'reply'``.
150 :param int message_id: id of the mail.message to which the user
152 :param dict context: several context values will modify the behavior
153 of the wizard, cfr. the class description.
161 current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
162 message_data = self.pool.get('mail.message').browse(cr, uid, message_id, context)
165 reply_subject = tools.ustr(message_data.subject or '')
166 if not (reply_subject.startswith('Re:') or reply_subject.startswith(re_prefix)):
167 reply_subject = "%s %s" % (re_prefix, reply_subject)
168 # Form the bodies (text and html). We use the plain text version of the
169 # original mail, by default, as it is easier to quote than the HTML
170 # version. TODO: make it possible to switch to HTML on the fly
171 sent_date = _('On %(date)s, ') % {'date': message_data.date} if message_data.date else ''
172 sender = _('%(sender_name)s wrote:') % {'sender_name': tools.ustr(message_data.email_from or _('You'))}
173 body_text = message_data.body_text or ''
174 quoted_body_text = '> %s' % tools.ustr(body_text.replace('\n', "\n> ") or '')
175 quoted_body_html = '> %s' % tools.ustr(body_text.replace('\n', "<br />> ") or '')
176 reply_body_text = '\n%s%s\n%s\n%s' % (sent_date, sender, quoted_body_text, current_user.signature)
177 reply_body_html = '\n%s%s\n%s\n%s' % (sent_date, sender, quoted_body_html, current_user.signature)
178 # Update header and references
180 reply_references = message_data.references and tools.ustr(message_data.references) or False
181 reply_message_id = message_data.message_id or False
183 reply_references = (reply_references or '') + " " + mail_wiz.message_id
184 reply_headers['In-Reply-To'] = mail_wiz.message_id
187 'body_text': reply_body_text,
188 'body_html': quoted_body_html,
189 'subject': reply_subject,
190 'attachment_ids': [],
191 'dest_partner_ids': [1],
192 'model': message_data.model or False,
193 'res_id': message_data.res_id or False,
194 'email_from': current_user.user_email or message_data.email_to or False,
195 'email_to': message_data.reply_to or message_data.email_from or False,
196 'email_cc': message_data.email_cc or False,
198 # pass msg-id and references of mail we're replying to, to construct the
199 # new ones later when sending
200 'message_id': reply_message_id,
201 'references': reply_references,
202 'headers': reply_headers,
206 def send_mail(self, cr, uid, ids, context=None):
207 '''Process the wizard contents and proceed with sending the corresponding
208 email(s), rendering any template patterns on the fly if needed.
209 If the wizard is in mass-mail mode (context['mail.compose.message.mode'] is
210 set to ``'mass_mail'``), the resulting email(s) are scheduled for being
211 sent the next time the mail.message scheduler runs, or the next time
212 ``mail.message.process_email_queue`` is called.
213 Otherwise the new message is sent immediately.
215 :param dict context: several context values will modify the behavior
216 of the wizard, cfr. the class description.
220 mail_message = self.pool.get('mail.message')
221 for mail_wiz in self.browse(cr, uid, ids, context=context):
224 for attach in mail_wiz.attachment_ids:
225 attachment[attach.datas_fname] = attach.datas and attach.datas.decode('base64')
227 # composition wizard options
228 email_mode = context.get('email_mode')
229 formatting = context.get('formatting')
231 # default message values according to the wizard options
233 content_subtype = 'html'
234 subject = mail_wiz.subject
236 content_subtype = 'text'
244 body = mail_wiz.body_html if mail_wiz.content_subtype == 'html' else mail_wiz.body_text
246 # Get model, and check whether it is OpenChatter enabled, aka inherit from mail.thread
247 if context.get('mail.compose.message.mode') == 'mass_mail':
248 if context.get('active_ids') and context.get('active_model'):
249 active_ids = context['active_ids']
250 active_model = context['active_model']
252 active_model = mail_wiz.model
253 active_model_pool = self.pool.get(active_model)
254 active_ids = active_model_pool.search(cr, uid, ast.literal_eval(mail_wiz.filter_id.domain), context=ast.literal_eval(mail_wiz.filter_id.context))
256 active_model = mail_wiz.model
257 active_ids = [int(mail_wiz.res_id)]
258 active_model_pool = self.pool.get(active_model)
259 if hasattr(active_model_pool, 'message_append'):
260 mail_thread_enabled = True
262 mail_thread_enabled = False
264 if context.get('mail.compose.message.mode') == 'mass_mail':
265 # Mass mailing: must render the template patterns
266 for active_id in active_ids:
267 rendered_subject = self.render_template(cr, uid, subject, active_model, active_id)
268 rendered_body = self.render_template(cr, uid, body, active_model, active_id)
269 email_from = self.render_template(cr, uid, mail_wiz.email_from, active_model, active_id)
270 email_to = self.render_template(cr, uid, mail_wiz.email_to, active_model, active_id)
271 email_cc = self.render_template(cr, uid, mail_wiz.email_cc, active_model, active_id)
272 email_bcc = self.render_template(cr, uid, mail_wiz.email_bcc, active_model, active_id)
273 reply_to = self.render_template(cr, uid, mail_wiz.reply_to, active_model, active_id)
275 # in mass-mailing mode we only schedule the mail for sending, it will be
276 # processed as soon as the mail scheduler runs.
277 if mail_thread_enabled:
278 active_model_pool.message_append(cr, uid, [active_id],
279 rendered_subject, body_text=mail_wiz.body_text, body_html=mail_wiz.body_html, content_subtype=mail_wiz.content_subtype, state='outgoing',
280 email_to=email_to, email_from=email_from, email_cc=email_cc, email_bcc=email_bcc,
281 reply_to=reply_to, references=references, attachments=attachment, headers=headers, context=context)
283 mail_message.schedule_with_attach(cr, uid, email_from, to_email(email_to), subject, rendered_body,
284 model=mail_wiz.model, email_cc=to_email(email_cc), email_bcc=to_email(email_bcc), reply_to=reply_to,
285 attachments=attachment, references=references, res_id=active_id,
286 content_subtype=mail_wiz.content_subtype, headers=headers, context=context)
288 # normal mode - no mass-mailing
289 if mail_thread_enabled:
290 msg_ids = active_model_pool.message_append(cr, uid, active_ids,
291 subject, body_text=mail_wiz.body_text, body_html=mail_wiz.body_html, content_subtype=content_subtype, state='outgoing',
292 email_to=mail_wiz.email_to, email_from=mail_wiz.email_from, email_cc=mail_wiz.email_cc, email_bcc=mail_wiz.email_bcc,
293 reply_to=mail_wiz.reply_to, references=references, attachments=attachment, headers=headers, context=context,
296 msg_ids = [mail_message.schedule_with_attach(cr, uid, mail_wiz.email_from, to_email(mail_wiz.email_to), subject, body_text,
297 model=mail_wiz.model, email_cc=to_email(mail_wiz.email_cc), email_bcc=to_email(mail_wiz.email_bcc), reply_to=mail_wiz.reply_to,
298 attachments=attachment, references=references, res_id=int(mail_wiz.res_id),
299 content_subtype=mail_wiz.content_subtype, headers=headers, context=context)]
300 # in normal mode, we send the email immediately, as the user expects us to (delay should be sufficiently small)
301 mail_message.send(cr, uid, msg_ids, context=context)
303 return {'type': 'ir.actions.act_window_close'}
305 def render_template(self, cr, uid, template, model, res_id, context=None):
306 """Render the given template text, replace mako-like expressions ``${expr}``
307 with the result of evaluating these expressions with an evaluation context
310 * ``user``: browse_record of the current user
311 * ``object``: browse_record of the document record this mail is
313 * ``context``: the context passed to the mail composition wizard
315 :param str template: the template text to render
316 :param str model: model name of the document record this mail is related to.
317 :param int res_id: id of the document record this mail is related to.
322 exp = str(match.group()[2:-1]).strip()
325 'user' : self.pool.get('res.users').browse(cr, uid, uid, context=context),
326 'object' : self.pool.get(model).browse(cr, uid, res_id, context=context),
327 'context': dict(context), # copy context to prevent side-effects of eval
329 if result in (None, False):
331 return tools.ustr(result)
332 return template and EXPRESSION_PATTERN.sub(merge, template)
335 class mail_compose_message_extended(osv.TransientModel):
336 """ Extension of 'mail.compose.message' to support default field values related
337 to CRM-like models that follow the following conventions:
339 1. The model object must have an attribute '_mail_compose_message' equal to True.
341 2. The model should define the following fields:
342 - 'name' as subject of the message (required);
343 - 'email_from' as destination email address (required);
344 - 'email_cc' as cc email addresses (required);
345 - 'section_id.reply_to' as reply-to address (optional).
347 _inherit = 'mail.compose.message'
349 def get_value(self, cr, uid, model, res_id, context=None):
350 """ Overrides the default implementation to provide more default field values
351 related to the corresponding CRM case.
353 result = super(mail_compose_message_extended, self).get_value(cr, uid, model, res_id, context=context)
354 model_obj = self.pool.get(model)
355 if getattr(model_obj, '_mail_compose_message', False) and res_id:
356 data = model_obj.browse(cr, uid , res_id, context)
358 'email_to': data.email_from or False,
359 'email_cc': tools.ustr(data.email_cc or ''),
360 'subject': data.name or False,
362 if hasattr(data, 'section_id'):
363 result['reply_to'] = data.section_id and data.section_id.reply_to or False
366 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: