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 ##############################################################################
25 from mail.mail_message import to_email
27 from osv import fields
28 from tools.safe_eval import safe_eval as eval
29 from tools.safe_eval import literal_eval
30 from tools.translate import _
32 # main mako-like expression pattern
33 EXPRESSION_PATTERN = re.compile('(\$\{.+?\})')
35 class mail_compose_message(osv.osv_memory):
36 """Generic E-mail composition wizard. This wizard is meant to be inherited
37 at model and view level to provide specific wizard features.
39 The behavior of the wizard can be modified through the use of context
40 parameters, among which are:
42 * mail.compose.message.mode: if set to 'reply', the wizard is in
43 reply mode and pre-populated with the original quote.
44 If set to 'mass_mail', the wizard is in mass mailing
45 where the mail details can contain template placeholders
46 that will be merged with actual data before being sent
47 to each recipient. Recipients will be derived from the
48 records determined via ``context['active_model']`` and
49 ``context['active_ids']``.
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 = 'E-mail composition wizard'
62 def default_get(self, cr, uid, fields, context=None):
63 """Overridden to provide specific defaults depending on the context
66 :param dict context: several context values will modify the behavior
67 of the wizard, cfr. the class description.
71 result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
73 reply_mode = context.get('mail.compose.message.mode') == 'reply'
74 if (not reply_mode) and context.get('active_model') and context.get('active_id'):
75 # normal mode when sending an email related to any document, as specified by
76 # active_model and active_id in context
77 vals = self.get_value(cr, uid, context.get('active_model'), context.get('active_id'), context)
78 elif reply_mode and context.get('active_id'):
79 # reply mode, consider active_id is the ID of a mail.message to which we're
81 vals = self.get_message_data(cr, uid, int(context['active_id']), context)
84 result['model'] = context.get('active_model', False)
87 result.update({field : vals[field]})
89 # link to model and record if not done yet
90 if not result.get('model') or not result.get('res_id'):
91 active_model = context.get('active_model')
92 res_id = context.get('active_id')
93 if active_model and active_model not in (self._name, 'mail.message'):
94 result['model'] = active_model
96 result['res_id'] = res_id
98 # Try to provide default email_from if not specified yet
99 if not result.get('email_from'):
100 current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
101 result['email_from'] = current_user.user_email or False
105 'attachment_ids': fields.many2many('ir.attachment','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'),
106 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
107 'filter_id': fields.many2one('ir.filters', 'Filters'),
110 def get_value(self, cr, uid, model, res_id, context=None):
111 """Returns a defaults-like dict with initial values for the composition
112 wizard when sending an email related to the document record identified
113 by ``model`` and ``res_id``.
115 The default implementation returns an empty dictionary, and is meant
116 to be overridden by subclasses.
118 :param str model: model name of the document record this mail is related to.
119 :param int res_id: id of the document record this mail is related to.
120 :param dict context: several context values will modify the behavior
121 of the wizard, cfr. the class description.
125 def get_message_data(self, cr, uid, message_id, context=None):
126 """Returns a defaults-like dict with initial values for the composition
127 wizard when replying to the given message (e.g. including the quote
128 of the initial message, and the correct recipient).
129 Should not be called unless ``context['mail.compose.message.mode'] == 'reply'``.
131 :param int message_id: id of the mail.message to which the user
133 :param dict context: several context values will modify the behavior
134 of the wizard, cfr. the class description.
135 When calling this method, the ``'mail'`` value
136 in the context should be ``'reply'``.
141 mail_message = self.pool.get('mail.message')
143 message_data = mail_message.browse(cr, uid, message_id, context)
144 subject = tools.ustr(message_data.subject or '')
145 # we use the plain text version of the original mail, by default,
146 # as it is easier to quote than the HTML version.
147 # XXX TODO: make it possible to switch to HTML on the fly
148 current_user = self.pool.get('res.users').browse(cr, uid, uid, context)
149 body = message_data.body_text or current_user.signature
150 if context.get('mail.compose.message.mode') == 'reply':
151 sent_date = _('On %(date)s, ') % {'date': message_data.date} if message_data.date else ''
152 sender = _('%(sender_name)s wrote:') % {'sender_name': tools.ustr(message_data.email_from or _('You'))}
153 quoted_body = '> %s' % tools.ustr(body.replace('\n', "\n> ") or '')
154 body = '\n'.join(["\n", (sent_date + sender), quoted_body])
155 body += "\n" + current_user.signature
157 if not (subject.startswith('Re:') or subject.startswith(re_prefix)):
158 subject = "%s %s" % (re_prefix, subject)
160 'subtype' : 'plain', # default to the text version due to quoting
163 'attachment_ids' : [],
164 'model' : message_data.model or False,
165 'res_id' : message_data.res_id or False,
166 'email_from' : current_user.user_email or message_data.email_to or False,
167 'email_to' : message_data.reply_to or message_data.email_from or False,
168 'email_cc' : message_data.email_cc or False,
171 # pass msg-id and references of mail we're replying to, to construct the
172 # new ones later when sending
173 'message_id' : message_data.message_id or False,
174 'references' : message_data.references and tools.ustr(message_data.references) or False,
178 def send_mail(self, cr, uid, ids, context=None):
179 '''Process the wizard contents and proceed with sending the corresponding
180 email(s), rendering any template patterns on the fly if needed.
181 If the wizard is in mass-mail mode (context['mail.compose.message.mode'] is
182 set to ``'mass_mail'``), the resulting email(s) are scheduled for being
183 sent the next time the mail.message scheduler runs, or the next time
184 ``mail.message.process_email_queue`` is called.
185 Otherwise the new message is sent immediately.
187 :param dict context: several context values will modify the behavior
188 of the wizard, cfr. the class description.
192 mail_message = self.pool.get('mail.message')
193 for mail in self.browse(cr, uid, ids, context=context):
195 for attach in mail.attachment_ids:
196 attachment[attach.datas_fname] = attach.datas
200 body = mail.body_html if mail.subtype == 'html' else mail.body_text
203 if context.get('mail.compose.message.mode') == 'reply' and mail.message_id:
204 references = (mail.references or '') + " " + mail.message_id
205 headers['In-Reply-To'] = mail.message_id
207 if context.get('mail.compose.message.mode') == 'mass_mail':
208 # Mass mailing: must render the template patterns
209 if context.get('active_ids') and context.get('active_model'):
210 active_ids = context['active_ids']
211 active_model = context['active_model']
213 active_model = mail.model
214 active_model_pool = self.pool.get(active_model)
215 active_ids = active_model_pool.search(cr, uid, literal_eval(mail.filter_id.domain), context=literal_eval(mail.filter_id.context))
217 for active_id in active_ids:
218 subject = self.render_template(cr, uid, mail.subject, active_model, active_id)
219 rendered_body = self.render_template(cr, uid, body, active_model, active_id)
220 email_from = self.render_template(cr, uid, mail.email_from, active_model, active_id)
221 email_to = self.render_template(cr, uid, mail.email_to, active_model, active_id)
222 email_cc = self.render_template(cr, uid, mail.email_cc, active_model, active_id)
223 email_bcc = self.render_template(cr, uid, mail.email_bcc, active_model, active_id)
224 reply_to = self.render_template(cr, uid, mail.reply_to, active_model, active_id)
226 # in mass-mailing mode we only schedule the mail for sending, it will be
227 # processed as soon as the mail scheduler runs.
228 mail_message.schedule_with_attach(cr, uid, email_from, to_email(email_to), subject, rendered_body,
229 model=mail.model, email_cc=to_email(email_cc), email_bcc=to_email(email_bcc), reply_to=reply_to,
230 attachments=attachment, references=references, res_id=int(mail.res_id),
231 subtype=mail.subtype, headers=headers, context=context)
233 # normal mode - no mass-mailing
234 msg_id = mail_message.schedule_with_attach(cr, uid, mail.email_from, to_email(mail.email_to), mail.subject, body,
235 model=mail.model, email_cc=to_email(mail.email_cc), email_bcc=to_email(mail.email_bcc), reply_to=mail.reply_to,
236 attachments=attachment, references=references, res_id=int(mail.res_id),
237 subtype=mail.subtype, headers=headers, context=context)
238 # in normal mode, we send the email immediately, as the user expects us to (delay should be sufficiently small)
239 mail_message.send(cr, uid, [msg_id], context=context)
241 return {'type': 'ir.actions.act_window_close'}
243 def render_template(self, cr, uid, template, model, res_id, context=None):
244 """Render the given template text, replace mako-like expressions ``${expr}``
245 with the result of evaluating these expressions with an evaluation context
248 * ``user``: browse_record of the current user
249 * ``object``: browse_record of the document record this mail is
251 * ``context``: the context passed to the mail composition wizard
253 :param str template: the template text to render
254 :param str model: model name of the document record this mail is related to.
255 :param int res_id: id of the document record this mail is related to.
260 exp = str(match.group()[2:-1]).strip()
263 'user' : self.pool.get('res.users').browse(cr, uid, uid, context=context),
264 'object' : self.pool.get(model).browse(cr, uid, res_id, context=context),
265 'context': dict(context), # copy context to prevent side-effects of eval
267 if result in (None, False):
269 return tools.ustr(result)
270 return template and EXPRESSION_PATTERN.sub(merge, template)
272 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: