[FIX] mass_mailing: fixes:
[odoo/odoo.git] / addons / mass_mailing / models / mass_mailing.py
1 # -*- coding: utf-8 -*-
2
3 from datetime import datetime
4 from dateutil import relativedelta
5 import json
6 import random
7
8 from openerp import tools
9 from openerp.exceptions import Warning
10 from openerp.tools.safe_eval import safe_eval as eval
11 from openerp.tools.translate import _
12 from openerp.osv import osv, fields
13
14
15 class MassMailingCategory(osv.Model):
16     """Model of categories of mass mailing, i.e. marketing, newsletter, ... """
17     _name = 'mail.mass_mailing.category'
18     _description = 'Mass Mailing Category'
19     _order = 'name'
20
21     _columns = {
22         'name': fields.char('Name', required=True),
23     }
24
25
26 class MassMailingContact(osv.Model):
27     """Model of a contact. This model is different from the partner model
28     because it holds only some basic information: name, email. The purpose is to
29     be able to deal with large contact list to email without bloating the partner
30     base."""
31     _name = 'mail.mass_mailing.contact'
32     _description = 'Mass Mailing Contact'
33     _order = 'email'
34     _rec_name = 'email'
35
36     _columns = {
37         'name': fields.char('Name'),
38         'email': fields.char('Email', required=True),
39         'create_date': fields.datetime('Create Date'),
40         'list_id': fields.many2one(
41             'mail.mass_mailing.list', string='Mailing List',
42             ondelete='cascade', required=True,
43         ),
44         'opt_out': fields.boolean('Opt Out', help='The contact has chosen not to receive mails anymore from this list'),
45     }
46
47     def _get_latest_list(self, cr, uid, context={}):
48         lid = self.pool.get('mail.mass_mailing.list').search(cr, uid, [], limit=1, order='id desc', context=context)
49         return lid and lid[0] or False
50
51     _defaults = {
52         'list_id': _get_latest_list
53     }
54
55     def name_create(self, cr, uid, name, context=None):
56         name, email = self.pool['res.partner']._parse_partner_name(name, context=context)
57         if name and not email:
58             email = name
59         if email and not name:
60             name = email
61         rec_id = self.create(cr, uid, {'name': name, 'email': email}, context=context)
62         return self.name_get(cr, uid, [rec_id], context)[0]
63
64     def message_get_default_recipients(self, cr, uid, ids, context=None):
65         res = {}
66         for record in self.browse(cr, uid, ids, context=context):
67             res[record.id] = {'partner_ids': [], 'email_to': record.email, 'email_cc': False}
68         return res
69
70
71 class MassMailingList(osv.Model):
72     """Model of a contact list. """
73     _name = 'mail.mass_mailing.list'
74     _order = 'name'
75     _description = 'Mailing List'
76
77     def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None):
78         result = dict.fromkeys(ids, 0)
79         Contacts = self.pool.get('mail.mass_mailing.contact')
80         for group in Contacts.read_group(cr, uid, [('list_id', 'in', ids), ('opt_out', '!=', True)], ['list_id'], ['list_id'], context=context):
81             result[group['list_id'][0]] = group['list_id_count']
82         return result
83
84     _columns = {
85         'name': fields.char('Mailing List', required=True),
86         'contact_nbr': fields.function(
87             _get_contact_nbr, type='integer',
88             string='Number of Contacts',
89         ),
90     }
91
92
93 class MassMailingStage(osv.Model):
94     """Stage for mass mailing campaigns. """
95     _name = 'mail.mass_mailing.stage'
96     _description = 'Mass Mailing Campaign Stage'
97     _order = 'sequence'
98
99     _columns = {
100         'name': fields.char('Name', required=True, translate=True),
101         'sequence': fields.integer('Sequence'),
102     }
103
104     _defaults = {
105         'sequence': 0,
106     }
107
108
109 class MassMailingCampaign(osv.Model):
110     """Model of mass mailing campaigns. """
111     _name = "mail.mass_mailing.campaign"
112     _description = 'Mass Mailing Campaign'
113
114     def _get_statistics(self, cr, uid, ids, name, arg, context=None):
115         """ Compute statistics of the mass mailing campaign """
116         Statistics = self.pool['mail.mail.statistics']
117         results = dict.fromkeys(ids, False)
118         for cid in ids:
119             stat_ids = Statistics.search(cr, uid, [('mass_mailing_campaign_id', '=', cid)], context=context)
120             stats = Statistics.browse(cr, uid, stat_ids, context=context)
121             results[cid] = {
122                 'total': len(stats),
123                 'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]),
124                 'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]),
125                 'sent': len([s for s in stats if not s.sent is False]),
126                 'opened': len([s for s in stats if not s.opened is False]),
127                 'replied': len([s for s in stats if not s.replied is False]),
128                 'bounced': len([s for s in stats if not s.bounced is False]),
129             }
130             results[cid]['delivered'] = results[cid]['sent'] - results[cid]['bounced']
131             results[cid]['received_ratio'] = 100.0 * results[cid]['delivered'] / (results[cid]['total'] or 1)
132             results[cid]['opened_ratio'] = 100.0 * results[cid]['opened'] / (results[cid]['total'] or 1)
133             results[cid]['replied_ratio'] = 100.0 * results[cid]['replied'] / (results[cid]['total'] or 1)
134         return results
135
136     _columns = {
137         'name': fields.char('Name', required=True),
138         'stage_id': fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
139         'user_id': fields.many2one(
140             'res.users', 'Responsible',
141             required=True,
142         ),
143         'category_ids': fields.many2many(
144             'mail.mass_mailing.category', 'mail_mass_mailing_category_rel',
145             'category_id', 'campaign_id', string='Categories'),
146         'mass_mailing_ids': fields.one2many(
147             'mail.mass_mailing', 'mass_mailing_campaign_id',
148             'Mass Mailings',
149         ),
150         'unique_ab_testing': fields.boolean(
151             'AB Testing',
152             help='If checked, recipients will be mailed only once, allowing to send'
153                  'various mailings in a single campaign to test the effectiveness'
154                  'of the mailings.'),
155         'color': fields.integer('Color Index'),
156         # stat fields
157         'total': fields.function(
158             _get_statistics, string='Total',
159             type='integer', multi='_get_statistics'
160         ),
161         'scheduled': fields.function(
162             _get_statistics, string='Scheduled',
163             type='integer', multi='_get_statistics'
164         ),
165         'failed': fields.function(
166             _get_statistics, string='Failed',
167             type='integer', multi='_get_statistics'
168         ),
169         'sent': fields.function(
170             _get_statistics, string='Sent Emails',
171             type='integer', multi='_get_statistics'
172         ),
173         'delivered': fields.function(
174             _get_statistics, string='Delivered',
175             type='integer', multi='_get_statistics',
176         ),
177         'opened': fields.function(
178             _get_statistics, string='Opened',
179             type='integer', multi='_get_statistics',
180         ),
181         'replied': fields.function(
182             _get_statistics, string='Replied',
183             type='integer', multi='_get_statistics'
184         ),
185         'bounced': fields.function(
186             _get_statistics, string='Bounced',
187             type='integer', multi='_get_statistics'
188         ),
189         'received_ratio': fields.function(
190             _get_statistics, string='Received Ratio',
191             type='integer', multi='_get_statistics',
192         ),
193         'opened_ratio': fields.function(
194             _get_statistics, string='Opened Ratio',
195             type='integer', multi='_get_statistics',
196         ),
197         'replied_ratio': fields.function(
198             _get_statistics, string='Replied Ratio',
199             type='integer', multi='_get_statistics',
200         ),
201     }
202
203     def _get_default_stage_id(self, cr, uid, context=None):
204         stage_ids = self.pool['mail.mass_mailing.stage'].search(cr, uid, [], limit=1, context=context)
205         return stage_ids and stage_ids[0] or False
206
207     _defaults = {
208         'user_id': lambda self, cr, uid, ctx=None: uid,
209         'stage_id': lambda self, *args: self._get_default_stage_id(*args),
210     }
211
212     def get_recipients(self, cr, uid, ids, model=None, context=None):
213         """Return the recipients of a mailing campaign. This is based on the statistics
214         build for each mailing. """
215         Statistics = self.pool['mail.mail.statistics']
216         res = dict.fromkeys(ids, False)
217         for cid in ids:
218             domain = [('mass_mailing_campaign_id', '=', cid)]
219             if model:
220                 domain += [('model', '=', model)]
221             stat_ids = Statistics.search(cr, uid, domain, context=context)
222             res[cid] = set(stat.res_id for stat in Statistics.browse(cr, uid, stat_ids, context=context))
223         return res
224
225
226 class MassMailing(osv.Model):
227     """ MassMailing models a wave of emails for a mass mailign campaign.
228     A mass mailing is an occurence of sending emails. """
229
230     _name = 'mail.mass_mailing'
231     _description = 'Mass Mailing'
232     # number of periods for tracking mail_mail statistics
233     _period_number = 6
234     _order = 'sent_date DESC'
235
236     def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, date_begin, context=None):
237         """ Generic method to generate data for bar chart values using SparklineBarWidget.
238             This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
239
240             :param obj: the target model (i.e. crm_lead)
241             :param domain: the domain applied to the read_group
242             :param list read_fields: the list of fields to read in the read_group
243             :param str value_field: the field used to compute the value of the bar slice
244             :param str groupby_field: the fields used to group
245
246             :return list section_result: a list of dicts: [
247                                                 {   'value': (int) bar_column_value,
248                                                     'tootip': (str) bar_column_tooltip,
249                                                 }
250                                             ]
251         """
252         date_begin = date_begin.date()
253         section_result = [{'value': 0,
254                            'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'),
255                            } for i in range(0, self._period_number)]
256         group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
257         field_col_info = obj._all_columns.get(groupby_field.split(':')[0])
258         pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT
259         for group in group_obj:
260             group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date()
261             timedelta = relativedelta.relativedelta(group_begin_date, date_begin)
262             section_result[timedelta.days] = {'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field)}
263         return section_result
264
265     def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None):
266         """ Get the daily statistics of the mass mailing. This is done by a grouping
267         on opened and replied fields. Using custom format in context, we obtain
268         results for the next 6 days following the mass mailing date. """
269         obj = self.pool['mail.mail.statistics']
270         res = {}
271         for mailing in self.browse(cr, uid, ids, context=context):
272             res[mailing.id] = {}
273             date = mailing.sent_date if mailing.sent_date else mailing.create_date
274             date_begin = datetime.strptime(date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
275             date_end = date_begin + relativedelta.relativedelta(days=self._period_number - 1)
276             date_begin_str = date_begin.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
277             date_end_str = date_end.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)
278             domain = [('mass_mailing_id', '=', mailing.id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)]
279             res[mailing.id]['opened_daily'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['opened'], 'opened_count', 'opened:day', date_begin, context=context))
280             domain = [('mass_mailing_id', '=', mailing.id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)]
281             res[mailing.id]['replied_daily'] = json.dumps(self.__get_bar_values(cr, uid, obj, domain, ['replied'], 'replied_count', 'replied:day', date_begin, context=context))
282         return res
283
284     def _get_statistics(self, cr, uid, ids, name, arg, context=None):
285         """ Compute statistics of the mass mailing campaign """
286         Statistics = self.pool['mail.mail.statistics']
287         results = dict.fromkeys(ids, False)
288         for mid in ids:
289             stat_ids = Statistics.search(cr, uid, [('mass_mailing_id', '=', mid)], context=context)
290             stats = Statistics.browse(cr, uid, stat_ids, context=context)
291             results[mid] = {
292                 'total': len(stats),
293                 'failed': len([s for s in stats if not s.scheduled is False and s.sent is False and not s.exception is False]),
294                 'scheduled': len([s for s in stats if not s.scheduled is False and s.sent is False and s.exception is False]),
295                 'sent': len([s for s in stats if not s.sent is False]),
296                 'opened': len([s for s in stats if not s.opened is False]),
297                 'replied': len([s for s in stats if not s.replied is False]),
298                 'bounced': len([s for s in stats if not s.bounced is False]),
299             }
300             results[mid]['delivered'] = results[mid]['sent'] - results[mid]['bounced']
301             results[mid]['received_ratio'] = 100.0 * results[mid]['delivered'] / (results[mid]['total'] or 1)
302             results[mid]['opened_ratio'] = 100.0 * results[mid]['opened'] / (results[mid]['total'] or 1)
303             results[mid]['replied_ratio'] = 100.0 * results[mid]['replied'] / (results[mid]['total'] or 1)
304         return results
305
306     def _get_mailing_model(self, cr, uid, context=None):
307         res = []
308         for model_name in self.pool:
309             model = self.pool[model_name]
310             if hasattr(model, '_mail_mass_mailing') and getattr(model, '_mail_mass_mailing'):
311                 res.append((model._name, getattr(model, '_mail_mass_mailing')))
312         res.append(('mail.mass_mailing.contact', _('Mailing List')))
313         return res
314
315     # indirections for inheritance
316     _mailing_model = lambda self, *args, **kwargs: self._get_mailing_model(*args, **kwargs)
317
318     _columns = {
319         'name': fields.char('Subject', required=True),
320         'email_from': fields.char('From', required=True),
321         'create_date': fields.datetime('Creation Date'),
322         'sent_date': fields.datetime('Sent Date', oldname='date'),
323         'body_html': fields.html('Body'),
324         'attachment_ids': fields.many2many(
325             'ir.attachment', 'mass_mailing_ir_attachments_rel',
326             'mass_mailing_id', 'attachment_id', 'Attachments'
327         ),
328         'mass_mailing_campaign_id': fields.many2one(
329             'mail.mass_mailing.campaign', 'Mass Mailing Campaign',
330             ondelete='set null',
331         ),
332         'state': fields.selection(
333             [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')],
334             string='Status', required=True,
335         ),
336         'color': fields.related(
337             'mass_mailing_campaign_id', 'color',
338             type='integer', string='Color Index',
339         ),
340         # mailing options
341         'reply_to_mode': fields.selection(
342             [('thread', 'In Document'), ('email', 'Specified Email Address')],
343             string='Reply-To Mode', required=True,
344         ),
345         'reply_to': fields.char('Reply To', help='Preferred Reply-To Address'),
346         # recipients
347         'mailing_model': fields.selection(_mailing_model, string='Recipients Model', required=True),
348         'mailing_domain': fields.char('Domain', oldname='domain'),
349         'contact_list_ids': fields.many2many(
350             'mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
351             string='Mailing Lists',
352         ),
353         'contact_ab_pc': fields.integer(
354             'AB Testing percentage',
355             help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
356         ),
357         # statistics data
358         'statistics_ids': fields.one2many(
359             'mail.mail.statistics', 'mass_mailing_id',
360             'Emails Statistics',
361         ),
362         'total': fields.function(
363             _get_statistics, string='Total',
364             type='integer', multi='_get_statistics',
365         ),
366         'scheduled': fields.function(
367             _get_statistics, string='Scheduled',
368             type='integer', multi='_get_statistics',
369         ),
370         'failed': fields.function(
371             _get_statistics, string='Failed',
372             type='integer', multi='_get_statistics',
373         ),
374         'sent': fields.function(
375             _get_statistics, string='Sent',
376             type='integer', multi='_get_statistics',
377         ),
378         'delivered': fields.function(
379             _get_statistics, string='Delivered',
380             type='integer', multi='_get_statistics',
381         ),
382         'opened': fields.function(
383             _get_statistics, string='Opened',
384             type='integer', multi='_get_statistics',
385         ),
386         'replied': fields.function(
387             _get_statistics, string='Replied',
388             type='integer', multi='_get_statistics',
389         ),
390         'bounced': fields.function(
391             _get_statistics, string='Bounced',
392             type='integer', multi='_get_statistics',
393         ),
394         'received_ratio': fields.function(
395             _get_statistics, string='Received Ratio',
396             type='integer', multi='_get_statistics',
397         ),
398         'opened_ratio': fields.function(
399             _get_statistics, string='Opened Ratio',
400             type='integer', multi='_get_statistics',
401         ),
402         'replied_ratio': fields.function(
403             _get_statistics, string='Replied Ratio',
404             type='integer', multi='_get_statistics',
405         ),
406         # daily ratio
407         'opened_daily': fields.function(
408             _get_daily_statistics, string='Opened',
409             type='char', multi='_get_daily_statistics',
410         ),
411         'replied_daily': fields.function(
412             _get_daily_statistics, string='Replied',
413             type='char', multi='_get_daily_statistics',
414         )
415     }
416
417     def default_get(self, cr, uid, fields, context=None):
418         res = super(MassMailing, self).default_get(cr, uid, fields, context=context)
419         if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model'):
420             if res['mailing_model'] in ['res.partner', 'mail.mass_mailing.contact']:
421                 res['reply_to_mode'] = 'email'
422             else:
423                 res['reply_to_mode'] = 'thread'
424         return res
425
426     _defaults = {
427         'state': 'draft',
428         'email_from': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
429         'reply_to': lambda self, cr, uid, ctx=None: self.pool['mail.message']._get_default_from(cr, uid, context=ctx),
430         'mailing_model': 'mail.mass_mailing.contact',
431         'contact_ab_pc': 100,
432     }
433
434     #------------------------------------------------------
435     # Technical stuff
436     #------------------------------------------------------
437
438     def copy_data(self, cr, uid, id, default=None, context=None):
439         if default is None:
440             default = {}
441         mailing = self.browse(cr, uid, id, context=context)
442         default.update({
443             'state': 'draft',
444             'statistics_ids': [],
445             'name': _('%s (duplicate)') % mailing.name,
446             'sent_date': False,
447         })
448         return super(MassMailing, self).copy_data(cr, uid, id, default, context=context)
449
450     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
451         """ Override read_group to always display all states. """
452         if groupby and groupby[0] == "state":
453             # Default result structure
454             # states = self._get_state_list(cr, uid, context=context)
455             states = [('draft', 'Draft'), ('test', 'Tested'), ('done', 'Sent')]
456             read_group_all_states = [{
457                 '__context': {'group_by': groupby[1:]},
458                 '__domain': domain + [('state', '=', state_value)],
459                 'state': state_value,
460                 'state_count': 0,
461             } for state_value, state_name in states]
462             # Get standard results
463             read_group_res = super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
464             # Update standard results with default results
465             result = []
466             for state_value, state_name in states:
467                 res = filter(lambda x: x['state'] == state_value, read_group_res)
468                 if not res:
469                     res = filter(lambda x: x['state'] == state_value, read_group_all_states)
470                 res[0]['state'] = [state_value, state_name]
471                 result.append(res[0])
472             return result
473         else:
474             return super(MassMailing, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
475
476     #------------------------------------------------------
477     # Views & Actions
478     #------------------------------------------------------
479
480     def on_change_model_and_list(self, cr, uid, ids, mailing_model, list_ids, context=None):
481         value = {}
482         if mailing_model == 'mail.mass_mailing.contact':
483             list_ids = map(lambda item: item if isinstance(item, (int, long)) else [lid for lid in item[2]], list_ids)
484             if list_ids:
485                 value['mailing_domain'] = "[('list_id', 'in', %s)]" % list_ids
486             else:
487                 value['mailing_domain'] = "[('list_id', '=', False)]"
488         else:
489             value['mailing_domain'] = False
490         return {'value': value}
491
492     def action_duplicate(self, cr, uid, ids, context=None):
493         copy_id = None
494         for mid in ids:
495             copy_id = self.copy(cr, uid, mid, context=context)
496         if copy_id:
497             return {
498                 'type': 'ir.actions.act_window',
499                 'view_type': 'form',
500                 'view_mode': 'form',
501                 'res_model': 'mail.mass_mailing',
502                 'res_id': copy_id,
503                 'context': context,
504             }
505         return False
506
507     def action_test_mailing(self, cr, uid, ids, context=None):
508         ctx = dict(context, default_mass_mailing_id=ids[0])
509         return {
510             'name': _('Test Mailing'),
511             'type': 'ir.actions.act_window',
512             'view_mode': 'form',
513             'res_model': 'mail.mass_mailing.test',
514             'target': 'new',
515             'context': ctx,
516         }
517
518     def action_edit_html(self, cr, uid, ids, context=None):
519         if not len(ids) == 1:
520             raise ValueError('One and only one ID allowed for this action')
521         mail = self.browse(cr, uid, ids[0], context=context)
522         url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d&template_model=%s&enable_editor=1' % (ids[0], mail.mailing_model)
523         return {
524             'name': _('Open with Visual Editor'),
525             'type': 'ir.actions.act_url',
526             'url': url,
527             'target': 'self',
528         }
529
530     #------------------------------------------------------
531     # Email Sending
532     #------------------------------------------------------
533
534     def get_recipients(self, cr, uid, mailing, context=None):
535         if mailing.mailing_domain:
536             domain = eval(mailing.mailing_domain)
537             res_ids = self.pool[mailing.mailing_model].search(cr, uid, domain, context=context)
538         else:
539             res_ids = []
540             domain = [('id', 'in', res_ids)]
541
542         # randomly choose a fragment
543         if mailing.contact_ab_pc < 100:
544             contact_nbr = self.pool[mailing.mailing_model].search(cr, uid, domain, count=True, context=context)
545             topick = int(contact_nbr / 100.0 * mailing.contact_ab_pc)
546             if mailing.mass_mailing_campaign_id and mailing.mass_mailing_campaign_id.unique_ab_testing:
547                 already_mailed = self.pool['mail.mass_mailing.campaign'].get_recipients(cr, uid, [mailing.mass_mailing_campaign_id.id], context=context)[mailing.mass_mailing_campaign_id.id]
548             else:
549                 already_mailed = set([])
550             remaining = set(res_ids).difference(already_mailed)
551             if topick > len(remaining):
552                 topick = len(remaining)
553             res_ids = random.sample(remaining, topick)
554         return res_ids
555
556     def send_mail(self, cr, uid, ids, context=None):
557         author_id = self.pool['res.users'].browse(cr, uid, uid, context=context).partner_id.id
558         for mailing in self.browse(cr, uid, ids, context=context):
559             # instantiate an email composer + send emails
560             res_ids = self.get_recipients(cr, uid, mailing, context=context)
561             if not res_ids:
562                 raise Warning('Please select recipients.')
563             comp_ctx = dict(context, active_ids=res_ids)
564             composer_values = {
565                 'author_id': author_id,
566                 'body': mailing.body_html,
567                 'subject': mailing.name,
568                 'model': mailing.mailing_model,
569                 'email_from': mailing.email_from,
570                 'record_name': False,
571                 'composition_mode': 'mass_mail',
572                 'mass_mailing_id': mailing.id,
573                 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
574                 'same_thread': mailing.reply_to_mode == 'thread',
575             }
576             if mailing.reply_to_mode == 'email':
577                 composer_values['reply_to'] = mailing.reply_to
578             composer_id = self.pool['mail.compose.message'].create(cr, uid, composer_values, context=comp_ctx)
579             self.pool['mail.compose.message'].send_mail(cr, uid, [composer_id], context=comp_ctx)
580             self.write(cr, uid, [mailing.id], {'sent_date': fields.datetime.now(), 'state': 'done'}, context=context)
581         return True