[IMP]: mail_gateway: Improvement for history
[odoo/odoo.git] / addons / mail_gateway / mail_gateway.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>)
6 #
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
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 Affero General Public License for more details
16 #
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/>
19 #
20 ##############################################################################
21
22 from osv import osv, fields
23 import time
24 import  base64
25 import re
26 import tools
27 import binascii
28 import socket
29 import email
30 from email.header import decode_header
31 import netsvc
32
33 logger = netsvc.Logger()
34
35
36 class mailgate_thread(osv.osv):
37     '''
38     Mailgateway Thread
39     '''
40     _name = 'mailgate.thread'
41     _description = 'Mailgateway Thread'
42     _rec_name = 'thread' 
43
44     _columns = {
45         'thread': fields.char('Thread', size=32, required=False), 
46         'message_ids': fields.one2many('mailgate.message', 'thread_id', 'Messages', domain=[('history', '=', True)], required=False), 
47         'log_ids': fields.one2many('mailgate.message', 'thread_id', 'Logs', domain=[('history', '=', False)], required=False), 
48         'model': fields.char('Model Name', size=64, required=False),  
49         'res_id': fields.integer('Resource ID'), 
50         }
51         
52     def _history(self, cr, uid, cases, keyword, history=False, subject=None, email=False, details=None, email_from=False, message_id=False, attach=None, context=None):
53         """
54         @param self: The object pointer
55         @param cr: the current row, from the database cursor,
56         @param uid: the current user’s ID for security checks,
57         @param cases: a browse record list
58         @param keyword: Case action keyword e.g.: If case is closed "Close" keyword is used
59         @param history: Value True/False, If True it makes entry in case History otherwise in Case Log
60         @param email: Email address if any
61         @param details: Details of case history if any 
62         @param atach: Attachment sent in email
63         @param context: A standard dictionary for contextual values"""
64         if context is None:
65             context = {}
66         if attach is None:
67             attach = []
68
69         # The mailgate sends the ids of the cases and not the object list
70
71         if all(isinstance(case_id, (int, long)) for case_id in cases):
72             cases = self.browse(cr, uid, cases, context=context)
73
74         att_obj = self.pool.get('ir.attachment')
75         obj = self.pool.get('mailgate.message')
76
77         for case in cases:
78             data = {
79                 'name': keyword, 
80                 'user_id': uid, 
81                 'model' : case._name, 
82                 'res_id': case.id, 
83                 'date': time.strftime('%Y-%m-%d %H:%M:%S'), 
84                 'thread_id': case.thread_id.id,
85                 'message_id': message_id, 
86             }
87             attachments = []
88             if history:
89                 for att in attach:
90                     attachments.append(att_obj.create(cr, uid, {'name': att[0], 'datas': base64.encodestring(att[1])}))
91                 
92                 data = {
93                         'name': subject or 'History', 
94                         'history': True, 
95                         'user_id': uid, 
96                         'model' : case._name, 
97                         'res_id': case.id,
98                         'date': time.strftime('%Y-%m-%d %H:%M:%S'), 
99                         'description': details or (hasattr(case, 'description') and case.description or False), 
100                         'email_to': email or \
101                                 (hasattr(case, 'user_id') and case.user_id and case.user_id.address_id and \
102                                     case.user_id.address_id.email) or tools.config.get('email_from', False), 
103                         'email_from': email_from or \
104                                 (hasattr(case, 'user_id') and case.user_id and case.user_id.address_id and \
105                                     case.user_id.address_id.email) or tools.config.get('email_from', False), 
106                         'partner_id': hasattr(case, 'partner_id') and (case.partner_id and case.partner_id.id or False) or False, 
107                         'thread_id': case.thread_id.id, 
108                         'message_id': message_id, 
109                         'attachment_ids': [(6, 0, attachments)]
110                         }
111             res = obj.create(cr, uid, data, context)
112             case._table.log(cr, uid, case.id, case._description + " '" + case.name + "': " + keyword, context=context)
113         return True
114     
115     __history = history = _history
116     
117
118 mailgate_thread()
119
120 class mailgate_message(osv.osv):
121     '''
122     Mailgateway Message
123     '''
124     _name = 'mailgate.message'
125     _description = 'Mailgateway Message'
126     _order = 'date desc'
127     _log_create=True
128
129     _columns = {
130         'name':fields.char('Message', size=64), 
131         'thread_id':fields.many2one('mailgate.thread', 'Thread'), 
132         'ref_id': fields.char('Reference Id', size=256, readonly=True, help="Message Id in Email Server.", select=True),
133         'date': fields.datetime('Date'), 
134         'history': fields.boolean('Is History?', required=False), 
135         'user_id': fields.many2one('res.users', 'User Responsible', readonly=True), 
136         'message': fields.text('Description'), 
137         'email_from': fields.char('Email From', size=84), 
138         'email_to': fields.char('Email To', size=84), 
139         'email_cc': fields.char('Email CC', size=84), 
140         'email_bcc': fields.char('Email BCC', size=84), 
141         'message_id': fields.char('Message Id', size=1024, readonly=True, help="Message Id on Email Server.", select=True), 
142         'description': fields.text('Description'), 
143         'partner_id': fields.many2one('res.partner', 'Partner', required=False), 
144         'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'),
145         'model': fields.char('Model Name', size=64, required=False),  
146         'res_id': fields.integer('Resource ID'), 
147     }
148
149 mailgate_message()
150
151
152 class mailgate_tool(osv.osv):
153
154     _name = 'email.server.tools'
155     _description = "Email Tools"
156     _auto = False
157     
158     def _to_decode(self, s, charsets):
159         for charset in charsets:
160             if charset:
161                 try:
162                     return s.decode(charset)
163                 except UnicodeError:
164                     pass
165         return s.decode('latin1')
166
167     def _decode_header(self, text):
168         if text:
169             text = decode_header(text.replace('\r', '')) 
170         return ''.join(map(lambda x:self._to_decode(x[0], [x[1]]), text or []))
171  
172     def to_email(self, cr, uid, text):
173         _email = re.compile(r'.*<.*@.*\..*>', re.UNICODE)
174         def record(path):
175             eml = path.group()
176             index = eml.index('<')
177             eml = eml[index:-1].replace('<', '').replace('>', '')
178             return eml
179
180         bits = _email.sub(record, text)
181         return bits
182     
183     def history(self, cr, uid, model, new_id, msg, attach, server_id=None, server_type=None):
184         try:
185             thread_id = self.pool.get(model).read(cr, uid, new_id, ['thread_id'])['thread_id'][0]
186         except Exception, e:
187             thread_id = None
188         msg_data = {
189                     'name': msg.get('subject', 'No subject'), 
190                     'date': msg.get('date') , # or time.strftime('%Y-%m-%d %H:%M:%S')??
191                     'description': msg.get('body', msg.get('from')), 
192                     'history': True,
193                     'model': model, 
194                     'email_cc': msg.get('cc'), 
195                     'email_from': msg.get('from'), 
196                     'email_to': msg.get('to'), 
197                     'message_id': msg.get('message-id'), 
198                     'ref_id': msg.get('references', msg.get('id')), 
199                     'res_id': new_id, 
200                     'server_id': server_id, 
201                     'thread_id': thread_id, 
202                     'type': server_type, 
203                     'user_id': uid, 
204                     'attachment_ids': [(6, 0, attach)]
205                     }
206         msg_id = self.pool.get('mailgate.message').create(cr, uid, msg_data)
207         return True
208     
209     def email_send(self, cr, uid, model, res_id, msg, email_default ):
210         message = email.message_from_string(str(msg))
211         subject = '['+str(res_id)+'] '+ self._decode_header(message['Subject'])
212         message['Message-Id'] = '<' + str(time.time()) + '-openerp-' + \
213                 model + '-' + str(res_id) + '@'+socket.gethostname() + '>'
214         msg_mails = []
215         mails = [self._decode_header(message['From']), self._decode_header(message['To'])]+self._decode_header(message.get('Cc', '')).split(',')
216         for mail in mails:
217             if mail:
218                 msg_mails.append(self.to_email(cr, uid, mail))
219         encoding = message.get_content_charset()
220         message['body'] = message.get_payload(decode=True)
221         if encoding:
222             message['body'] = message['body'].decode(encoding).encode('utf-8')
223
224         try:
225             tools.email_send(email_default or tools.config.get('email_from', None), msg_mails, subject, message.get('body'))
226         except Exception, e:
227             if email_default:
228                 temp_msg = '['+str(res_id)+'] ' + self._decode_header(message['Subject'])
229                 del message['Subject']
230                 message['Subject'] = '[OpenERP-FetchError] ' + temp_msg
231                 tools.email_send(email_default, email_default, message.get('Subject'), message.get('body'))
232         return True
233
234     def process_email(self, cr, uid, model, message, attach=True, server_id=None, server_type=None, context=None):
235         if not context:
236             context = {}
237         context.update({
238             'server_id': server_id
239         })
240         res_id = False
241         def create_record(msg):
242             model_pool = self.pool.get(model)
243             if hasattr(model_pool, 'message_new'):
244                 res_id = model_pool.message_new(cr, uid, msg, context)
245             else:
246                 data = {
247                     'name': msg.get('subject'), 
248                     'email_from': msg.get('from'), 
249                     'email_cc': msg.get('cc'), 
250                     'user_id': False, 
251                     'description': msg.get('body'), 
252                     'state' : 'draft', 
253                 }
254                 data.update(self.get_partner(cr, uid, msg.get('from'), context=context))
255                 res_id = model_pool.create(cr, uid, data, context=context)
256                 logger.notifyChannel('imap', netsvc.LOG_WARNING, 'method def message_new is not define in model %s. Using default method' % (model_pool._name))
257             att_ids = []
258             if attach:
259                 for attachment in msg.get('attachments', []):
260                     data_attach = {
261                         'name': attachment, 
262                         'datas': binascii.b2a_base64(str(attachments.get(attachment))), 
263                         'datas_fname': attachment, 
264                         'description': 'Mail attachment', 
265                         'res_model': model, 
266                         'res_id': res_id, 
267                     }
268                     att_ids.append(self.pool.get('ir.attachment').create(cr, uid, data_attach))
269
270             if hasattr(model_pool, '_history'):
271                 res = model_pool.browse(cr, uid, [res_id])
272                 model_pool._history(cr, uid, res, 'Receive', True, 
273                                 subject=msg.get('subject'), 
274                                 email=msg.get('to'), details=msg.get('body'), 
275                                 email_from=msg.get('from'), 
276                                 message_id=msg.get('message-id'), 
277                                 attach=msg.get('attachments', {}).items(), 
278                                 context={'model' : model})
279             else:
280                 self.history(cr, uid, model, res_id, msg, att_ids, server_id=server_id, server_type=server_type)
281             
282             return res_id
283
284         history_pool = self.pool.get('mailgate.message')
285         msg_txt = email.message_from_string(message)
286         message_id = msg_txt.get('Message-ID', False)
287
288         msg = {}
289         if not message_id:
290             return False
291
292         fields = msg_txt.keys()
293         msg['id'] = message_id
294         msg['message-id'] = message_id
295
296         def _decode_header(txt):
297             txt = txt.replace('\r', '')
298             return ' '.join(map(lambda (x, y): unicode(x, y or 'ascii'), decode_header(txt)))
299
300         if 'Subject' in fields:
301             msg['subject'] = _decode_header(msg_txt.get('Subject'))
302
303         if 'Content-Type' in fields:
304             msg['content-type'] = msg_txt.get('Content-Type')
305
306         if 'From' in fields:
307             msg['from'] = _decode_header(msg_txt.get('From'))
308
309         if 'Delivered-To' in fields:
310             msg['to'] = _decode_header(msg_txt.get('Delivered-To'))
311
312         if 'Cc' in fields:
313             msg['cc'] = _decode_header(msg_txt.get('Cc'))
314
315         if 'Reply-To' in fields:
316             msg['reply'] = _decode_header(msg_txt.get('Reply-To'))
317
318         if 'Date' in fields:
319             msg['date'] = msg_txt.get('Date')
320
321         if 'Content-Transfer-Encoding' in fields:
322             msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
323
324         if 'References' in fields:
325             msg['references'] = msg_txt.get('References')
326
327         if 'X-openerp-caseid' in fields:
328             msg['caseid'] = msg_txt.get('X-openerp-caseid')
329
330         if 'X-Priority' in fields:
331             msg['priority'] = msg_txt.get('X-priority', '3 (Normal)').split(' ')[0]
332
333         if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', None):
334             encoding = msg_txt.get_content_charset()
335             msg['body'] = msg_txt.get_payload(decode=True)
336             if encoding:
337                 msg['body'] = msg['body'].decode(encoding).encode('utf-8')
338
339         attachments = {}
340         if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', None):
341             body = ""
342             counter = 1
343             for part in msg_txt.walk():
344                 if part.get_content_maintype() == 'multipart':
345                     continue
346
347                 encoding = part.get_content_charset()
348
349                 if part.get_content_maintype()=='text':
350                     content = part.get_payload(decode=True)
351                     filename = part.get_filename()
352                     if filename :
353                         attachments[filename] = content
354                     else:
355                         if encoding:
356                             content = unicode(content, encoding)
357                         if part.get_content_subtype() == 'html':
358                             body = tools.html2plaintext(content)
359                         elif part.get_content_subtype() == 'plain':
360                             body = content
361                 elif part.get_content_maintype()=='application' or part.get_content_maintype()=='image' or part.get_content_maintype()=='text':
362                     filename = part.get_filename();
363                     if filename :
364                         attachments[filename] = part.get_payload(decode=True)
365                     else:
366                         res = part.get_payload(decode=True)
367                         if encoding:
368                             res = res.decode(encoding).encode('utf-8')
369
370                         body += res
371
372             msg['body'] = body
373             msg['attachments'] = attachments
374
375         if msg.get('references', False):
376             id = False
377             ref = msg.get('references')
378             if '\r\n' in ref:
379                 ref = msg.get('references').split('\r\n')
380             else:
381                 ref = msg.get('references').split(' ')
382             if ref:
383                 hids = history_pool.search(cr, uid, [('name', '=', ref[0].strip())])
384                 if hids:
385                     id = hids[0]
386                     history = history_pool.browse(cr, uid, id)
387                     model_pool = self.pool.get(model)
388                     context.update({
389                         'references_id':ref[0]
390                     })
391                     vals = {}
392                     if hasattr(model_pool, 'message_update'):
393                         model_pool.message_update(cr, uid, [history.res_id], vals, msg, context=context)
394                     else:
395                         logger.notifyChannel('imap', netsvc.LOG_WARNING, 'method def message_update is not define in model %s' % (model_pool._name))
396                         return False
397                 else:
398                     res_id = create_record(msg)
399
400         else:
401             res_id = create_record(msg)
402
403         return res_id
404
405     def get_partner(self, cr, uid, from_email, context=None):
406         """This function returns partner Id based on email passed
407         @param self: The object pointer
408         @param cr: the current row, from the database cursor,
409         @param uid: the current user’s ID for security checks
410         @param from_email: email address based on that function will search for the correct
411         """
412         res = {
413             'partner_address_id': False, 
414             'partner_id': False
415         }
416         from_email = self.to_email(cr, uid, from_email)
417         address_ids = self.pool.get('res.partner.address').search(cr, uid, [('email', '=', from_email)])
418         if address_ids:
419             address = self.pool.get('res.partner.address').browse(cr, uid, address_ids[0])
420             res['partner_address_id'] = address_ids[0]
421             res['partner_id'] = address.partner_id.id
422
423         return res
424
425 mailgate_tool()
426
427 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: