1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2009-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 Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (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 Affero General Public License for more details
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>
20 ##############################################################################
26 from email.utils import parsedate
30 from osv import osv, fields
31 from tools.translate import _
32 from mail_message import decode, to_email
34 _logger = logging.getLogger('mail')
36 class mail_thread(osv.osv):
37 '''Mixin model, meant to be inherited by any model that needs to
38 act as a discussion topic on which messages can be attached.
39 Public methods are prefixed with ``message_`` in order to avoid
40 name collisions with methods of the models that will inherit
43 ``mail.thread`` adds a one2many of mail.messages, acting as the
44 thread's history, and a few methods that may be overridden to
45 implement model-specific behavior upon arrival of new messages.
47 Inheriting classes are not required to implement any method, as the
48 default implementation will work for any model. However it is common
49 to override at least the ``message_new`` and ``message_update``
50 methods (calling ``super``) to add model-specific behavior at
51 creation and update of a thread.
55 _description = 'Email Thread'
58 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', readonly=True),
61 def message_thread_followers(self, cr, uid, ids, context=None):
62 """Returns a list of email addresses of the people following
63 this thread, including the sender of each mail, and the
64 people who were in CC of the messages, if any.
67 if isinstance(ids, (str, int, long)):
69 for thread in self.browse(cr, uid, ids, context=context):
71 for message in thread.message_ids:
72 l.add((message.user_id and message.user_id.user_email) or '')
73 l.add(message.email_from or '')
74 l.add(message.email_cc or '')
75 res[thread.id] = filter(None, l)
78 def copy(self, cr, uid, id, default=None, context=None):
79 """Overrides default copy method to empty the thread of
80 messages attached to this record, as the copied object
81 will have its own thread and does not have to share it.
88 return super(mail_thread, self).copy(cr, uid, id, default, context=context)
90 def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
91 """Called by ``message_process`` when a new message is received
92 for a given thread model, if the message did not belong to
94 The default behavior is to create a new record of the corresponding
95 model (based on some very basic info extracted from the message),
96 then attach the message to the newly created record
97 (by calling ``message_append_dict``).
98 Additional behavior may be implemented by overriding this method.
100 :param dict msg_dict: a map containing the email details and
101 attachments. See ``message_process`` and
102 ``mail.message.parse`` for details.
103 :param dict custom_values: optional dictionary of additional
104 field values to pass to create()
105 when creating the new thread record.
106 Be careful, these values may override
107 any other values coming from the message.
108 :param dict context: if a ``thread_model`` value is present
109 in the context, its value will be used
110 to determine the model of the record
111 to create (instead of the current model).
113 :return: the id of the newly created thread object
117 model = context.get('thread_model') or self._name
118 model_pool = self.pool.get(model)
119 fields = model_pool.fields_get(cr, uid, context=context)
120 data = model_pool.default_get(cr, uid, fields, context=context)
121 if 'name' in fields and not data.get('name'):
122 data['name'] = msg_dict.get('from','')
123 if custom_values and isinstance(custom_values, dict):
124 data.update(custom_values)
125 res_id = model_pool.create(cr, uid, data, context=context)
126 self.message_append_dict(cr, uid, [res_id], msg_dict, context=context)
129 def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None):
130 """Called by ``message_process`` when a new message is received
131 for an existing thread. The default behavior is to create a
132 new mail.message in the given thread (by calling
133 ``message_append_dict``)
134 Additional behavior may be implemented by overriding this
137 :param dict msg_dict: a map containing the email details and
138 attachments. See ``message_process`` and
139 ``mail.message.parse()`` for details.
140 :param dict context: if a ``thread_model`` value is present
141 in the context, its value will be used
142 to determine the model of the thread to
143 update (instead of the current model).
145 return self.message_append_dict(cr, uid, ids, msg_dict, context=context)
147 def message_append(self, cr, uid, threads, subject, body_text=None, email_to=False,
148 email_from=False, email_cc=None, email_bcc=None, reply_to=None,
149 email_date=None, message_id=False, references=None,
150 attachments=None, body_html=None, subtype=None, headers=None,
151 original=None, context=None):
152 """Creates a new mail.message attached to the current mail.thread,
153 containing all the details passed as parameters. All attachments
154 will be attached to the thread record as well as to the actual
156 If only the ``threads`` and ``subject`` parameters are provided,
157 a *event log* message is created, without the usual envelope
158 attributes (sender, recipients, etc.).
160 :param threads: list of thread ids, or list of browse_records representing
161 threads to which a new message should be attached
162 :param subject: subject of the message, or description of the event if this
163 is an *event log* entry.
164 :param email_to: Email-To / Recipient address
165 :param email_from: Email From / Sender address if any
166 :param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any
167 :param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any
168 :param reply_to: reply_to header
169 :param email_date: email date string if different from now, in server timezone
170 :param message_id: optional email identifier
171 :param references: optional email references
172 :param body_text: plaintext contents of the mail or log message
173 :param body_html: html contents of the mail or log message
174 :param subtype: optional type of message: 'plain' or 'html', corresponding to the main
175 body contents (body_text or body_html).
176 :param headers: mail headers to store
177 :param dict attachments: map of attachment filenames to binary contents, if any.
178 :param str original: optional full source of the RFC2822 email, for reference
179 :param dict context: if a ``thread_model`` value is present
180 in the context, its value will be used
181 to determine the model of the thread to
182 update (instead of the current model).
186 if attachments is None:
190 edate = parsedate(email_date)
191 if edate is not None:
192 email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
194 if all(isinstance(thread_id, (int, long)) for thread_id in threads):
195 model = context.get('thread_model') or self._name
196 model_pool = self.pool.get(model)
197 threads = model_pool.browse(cr, uid, threads, context=context)
199 ir_attachment = self.pool.get('ir.attachment')
200 mail_message = self.pool.get('mail.message')
202 for thread in threads:
204 for fname, fcontent in attachments.items():
205 if isinstance(fcontent, unicode):
206 fcontent = fcontent.encode('utf-8')
209 'datas': base64.b64encode(str(fcontent)),
210 'datas_fname': fname,
211 'description': _('Mail attachment'),
212 'res_model': thread._name,
215 to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
217 partner_id = hasattr(thread, 'partner_id') and (thread.partner_id and thread.partner_id.id or False) or False
218 if not partner_id and thread._name == 'res.partner':
219 partner_id = thread.id
223 'model' : thread._name,
224 'partner_id': partner_id,
226 'date': time.strftime('%Y-%m-%d %H:%M:%S'),
227 'message_id': message_id,
228 'body_text': body_text or (hasattr(thread, 'description') and thread.description or False),
229 'attachment_ids': [(6, 0, to_attach)],
230 'state' : 'received',
234 for param in (email_to, email_cc, email_bcc):
235 if isinstance(param, list):
236 param = ", ".join(param)
238 'subject': subject or _('History'),
240 'model' : thread._name,
242 'date': email_date or time.strftime('%Y-%m-%d %H:%M:%S'),
243 'body_text': body_text,
244 'email_to': email_to,
245 'email_from': email_from or \
246 (hasattr(thread, 'user_id') and thread.user_id and thread.user_id.user_email),
247 'email_cc': email_cc,
248 'email_bcc': email_bcc,
249 'partner_id': partner_id,
250 'references': references,
251 'message_id': message_id,
252 'attachment_ids': [(6, 0, to_attach)],
253 'state' : 'received',
254 'body_html': body_html,
257 'reply_to': reply_to,
258 'original': original,
260 mail_message.create(cr, uid, data, context=context)
263 def message_append_dict(self, cr, uid, ids, msg_dict, context=None):
264 """Creates a new mail.message attached to the given threads (``ids``),
265 with the contents of ``msg_dict``, by calling ``message_append``
266 with the mail details. All attachments in msg_dict will be
267 attached to the object record as well as to the actual
270 :param dict msg_dict: a map containing the email details and
271 attachments. See ``message_process()`` and
272 ``mail.message.parse()`` for details on
274 :param dict context: if a ``thread_model`` value is present
275 in the context, its value will be used
276 to determine the model of the thread to
277 update (instead of the current model).
279 return self.message_append(cr, uid, ids,
280 subject = msg_dict.get('subject'),
281 body_text = msg_dict.get('body_text'),
282 email_to = msg_dict.get('to'),
283 email_from = msg_dict.get('from'),
284 email_cc = msg_dict.get('cc'),
285 email_bcc = msg_dict.get('bcc'),
286 reply_to = msg_dict.get('reply'),
287 email_date = msg_dict.get('date'),
288 message_id = msg_dict.get('message-id'),
289 references = msg_dict.get('references')\
290 or msg_dict.get('in-reply-to'),
291 attachments = msg_dict.get('attachments'),
292 body_html= msg_dict.get('body_html'),
293 subtype = msg_dict.get('subtype'),
294 headers = msg_dict.get('headers'),
295 original = msg_dict.get('original'),
299 def message_process(self, cr, uid, model, message, custom_values=None,
300 save_original=False, strip_attachments=False,
302 """Process an incoming RFC2822 email message related to the
303 given thread model, relying on ``mail.message.parse()``
304 for the parsing operation, and then calling ``message_new``
305 (if the thread record did not exist) or ``message_update``
306 (if it did), then calling ``message_forward`` to automatically
307 notify other people that should receive this message.
309 :param string model: the thread model for which a new message
311 :param message: source of the RFC2822 mail
312 :type message: string or xmlrpclib.Binary
313 :type dict custom_values: optional dictionary of field values
314 to pass to ``message_new`` if a new
315 record needs to be created. Ignored
316 if the thread record already exists.
317 :param bool save_original: whether to keep a copy of the original
318 email source attached to the message after it is imported.
319 :param bool strip_attachments: whether to strip all attachments
320 before processing the message, in order to save some space.
322 # extract message bytes - we are forced to pass the message as binary because
323 # we don't know its encoding until we parse its headers and hence can't
324 # convert it to utf-8 for transport between the mailgate script and here.
325 if isinstance(message, xmlrpclib.Binary):
326 message = str(message.data)
328 model_pool = self.pool.get(model)
329 if self._name != model:
330 if context is None: context = {}
331 context.update({'thread_model': model})
333 mail_message = self.pool.get('mail.message')
337 # Warning: message_from_string doesn't always work correctly on unicode,
338 # we must use utf-8 strings here :-(
339 if isinstance(message, unicode):
340 message = message.encode('utf-8')
341 msg_txt = email.message_from_string(message)
342 msg = mail_message.parse_message(msg_txt, save_original=save_original)
344 if strip_attachments and 'attachments' in msg:
345 del msg['attachments']
347 # Create New Record into particular model
348 def create_record(msg):
349 if hasattr(model_pool, 'message_new'):
350 return model_pool.message_new(cr, uid, msg,
354 if msg.get('references') or msg.get('in-reply-to'):
355 references = msg.get('references') or msg.get('in-reply-to')
356 if '\r\n' in references:
357 references = references.split('\r\n')
359 references = references.split(' ')
360 for ref in references:
362 res_id = tools.reference_re.search(ref)
364 res_id = res_id.group(1)
366 res_id = tools.res_re.search(msg['subject'])
368 res_id = res_id.group(1)
371 if model_pool.exists(cr, uid, res_id):
372 if hasattr(model_pool, 'message_update'):
373 model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
375 # referenced thread was not found, we'll have to create a new one
378 res_id = create_record(msg)
379 #To forward the email to other followers
380 self.message_forward(cr, uid, model, [res_id], msg_txt, context=context)
383 # for backwards-compatibility with old scripts
384 process_email = message_process
386 def message_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
387 """Sends an email to all people following the given threads.
388 The emails are forwarded immediately, not queued for sending,
391 :param str model: thread model
392 :param list thread_ids: ids of the thread records
393 :param msg: email.message.Message object to forward
394 :param email_error: optional email address to notify in case
395 of any delivery error during the forward.
398 model_pool = self.pool.get(model)
399 smtp_server_obj = self.pool.get('ir.mail_server')
400 mail_message = self.pool.get('mail.message')
401 for res in model_pool.browse(cr, uid, thread_ids, context=context):
402 if hasattr(model_pool, 'message_thread_followers'):
403 followers = model_pool.message_thread_followers(cr, uid, [res.id])[res.id]
405 followers = self.message_thread_followers(cr, uid, [res.id])[res.id]
406 message_followers_emails = to_email(','.join(filter(None, followers)))
407 message_recipients = to_email(','.join(filter(None,
408 [decode(msg['from']),
410 decode(msg['cc'])])))
411 forward_to = [i for i in message_followers_emails if (i and (i not in message_recipients))]
413 # TODO: we need an interface for this for all types of objects, not just leads
414 if hasattr(res, 'section_id'):
416 msg['reply-to'] = res.section_id.reply_to
418 smtp_from, = to_email(msg['from'])
419 msg['from'] = smtp_from
420 msg['to'] = ", ".join(forward_to)
421 msg['message-id'] = tools.generate_tracking_message_id(res.id)
422 if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
423 subj = msg['subject']
424 del msg['subject'], msg['to'], msg['cc'], msg['bcc']
425 msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
426 msg['to'] = email_error
427 smtp_server_obj.send_email(cr, uid, msg)
430 def message_partner_by_email(self, cr, uid, email, context=None):
431 """Attempts to return the id of a partner address matching
432 the given ``email``, and the corresponding partner id.
433 Can be used by classes using the ``mail.thread`` mixin
434 to lookup the partner and use it in their implementation
435 of ``message_new`` to link the new record with a
436 corresponding partner.
437 The keys used in the returned dict are meant to map
438 to usual names for relationships towards a partner
439 and one of its addresses.
441 :param email: email address for which a partner
442 should be searched for.
444 :return: a map of the following form::
446 { 'partner_address_id': id or False,
447 'partner_id': pid or False }
449 address_pool = self.pool.get('res.partner.address')
451 'partner_address_id': False,
455 email = to_email(email)[0]
456 address_ids = address_pool.search(cr, uid, [('email', '=', email)])
458 address = address_pool.browse(cr, uid, address_ids[0])
459 res['partner_address_id'] = address_ids[0]
460 res['partner_id'] = address.partner_id.id
463 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: