Restyling of the config todo list
[odoo/odoo.git] / addons / mail / wizard / mail_compose_message.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
6 #
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.
11 #
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.
16 #
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/>
19 #
20 ##############################################################################
21
22 import re
23
24 import tools
25 from mail.mail_message import to_email
26 from osv import osv
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 _
31
32 # main mako-like expression pattern
33 EXPRESSION_PATTERN = re.compile('(\$\{.+?\})')
34
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.
38
39        The behavior of the wizard can be modified through the use of context
40        parameters, among which are:
41
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
51                         composed is related
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'``.
57     """
58     _name = 'mail.compose.message'
59     _inherit = 'mail.message.common'
60     _description = 'E-mail composition wizard'
61
62     def default_get(self, cr, uid, fields, context=None):
63         """Overridden to provide specific defaults depending on the context
64            parameters.
65
66            :param dict context: several context values will modify the behavior
67                                 of the wizard, cfr. the class description.
68         """
69         if context is None:
70             context = {}
71         result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
72         vals = {}
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
80             # replying
81             vals = self.get_message_data(cr, uid, int(context['active_id']), context)
82         else:
83             # default mode
84             result['model'] = context.get('active_model', False)
85         for field in vals:
86             if field in fields:
87                 result.update({field : vals[field]})
88
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
95                 if res_id:
96                     result['res_id'] = res_id
97
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
102         return result
103
104     _columns = {
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'),
108     }
109
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``.
114
115            The default implementation returns an empty dictionary, and is meant
116            to be overridden by subclasses.
117
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.
122         """
123         return {}
124
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'``.
130
131            :param int message_id: id of the mail.message to which the user
132                                   is replying.
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'``.
137         """
138         if context is None:
139             context = {}
140         result = {}
141         mail_message = self.pool.get('mail.message')
142         if message_id:
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
156                 re_prefix = _("Re:")
157                 if not (subject.startswith('Re:') or subject.startswith(re_prefix)):
158                     subject = "%s %s" % (re_prefix, subject)
159             result.update({
160                     'subtype' : 'plain', # default to the text version due to quoting
161                     'body_text' : body,
162                     'subject' : subject,
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,
169                     'user_id' : uid,
170
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,
175                 })
176         return result
177
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.
186
187            :param dict context: several context values will modify the behavior
188                                 of the wizard, cfr. the class description.
189         '''
190         if context is None:
191             context = {}
192         mail_message = self.pool.get('mail.message')
193         for mail in self.browse(cr, uid, ids, context=context):
194             attachment = {}
195             for attach in mail.attachment_ids:
196                 attachment[attach.datas_fname] = attach.datas
197             references = None
198             headers = {}
199
200             body =  mail.body_html if mail.subtype == 'html' else mail.body_text
201
202             # Reply Email
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
206
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']
212                 else:
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))
216
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)
225
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)
232             else:
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)
240
241         return {'type': 'ir.actions.act_window_close'}
242
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
246            containing:
247
248                 * ``user``: browse_record of the current user
249                 * ``object``: browse_record of the document record this mail is
250                               related to
251                 * ``context``: the context passed to the mail composition wizard
252
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.
256         """
257         if context is None:
258             context = {}
259         def merge(match):
260             exp = str(match.group()[2:-1]).strip()
261             result = eval(exp,
262                           {
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
266                           })
267             if result in (None, False):
268                 return ""
269             return tools.ustr(result)
270         return template and EXPRESSION_PATTERN.sub(merge, template)
271
272 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: