merge
[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 import re
23 import email, mimetypes
24 from email.Header import decode_header
25 from email.MIMEText import MIMEText
26 import xmlrpclib
27 import os
28 import binascii
29 import time, socket
30
31 from tools.translate import _
32
33 import tools
34 from osv import fields,osv,orm
35 from osv.orm import except_orm
36 import email
37 import netsvc
38 from poplib import POP3, POP3_SSL
39 from imaplib import IMAP4, IMAP4_SSL   
40
41 class mail_gateway_server(osv.osv):
42     _name = "mail.gateway.server"
43     _description = "Email Gateway Server"
44     _columns = {
45         'name': fields.char('Server Address',size=64,required=True ,help="IMAP/POP Address Of Email gateway Server"),
46         'login': fields.char('User',size=64,required=True,help="User Login Id of Email gateway"),
47         'password': fields.char('Password',size=64,required=True,help="User Password Of Email gateway"),
48         'server_type': fields.selection([("pop","POP"),("imap","Imap")],"Type of Server", required=True, help="Type of Email gateway Server"),
49         'port': fields.integer("Port" , help="Port Of Email gateway Server. If port is omitted, the standard POP3 port (110) is used for POP EMail Server and the standard IMAP4 port (143) is used for IMAP Sever."),
50         'ssl': fields.boolean('SSL',help ="Use Secure Authentication"),
51         'active': fields.boolean('Active', help="If the active field is set to true, it will allow you to hide the email gateway server without removing it."),
52     }
53     _defaults = {
54         'server_type':lambda * a:'pop',
55         'active':lambda * a:True,
56     }
57     
58     def check_duplicate(self, cr, uid, ids):
59         vals = self.read(cr, uid, ids, ['name', 'login'])[0]
60         cr.execute("select count(id) from mail_gateway_server \
61                         where name='%s' and login='%s'" % \
62                         (vals['name'], vals['login']))
63         res = cr.fetchone()
64         if res:
65             if res[0] > 1:
66                 return False
67         return True 
68
69     _constraints = [
70         (check_duplicate, 'Warning! Can\'t have duplicate server configuration!', ['name', 'login'])
71     ]
72     
73     def onchange_server_type(self, cr, uid, ids, server_type=False, ssl=False):
74         port = 0
75         if server_type == 'pop':
76             port = ssl and 995 or 110
77         elif server_type == 'imap':
78             port = ssl and 993 or 143
79         return {'value':{'port':port}}
80     
81 mail_gateway_server()
82
83
84 class mail_gateway(osv.osv):
85     _name = "mail.gateway"
86     _description = "Email Gateway"
87
88     _columns = {
89         'name': fields.char('Name',size=64,help="Name of Mail Gateway."),
90         'server_id': fields.many2one('mail.gateway.server',"Gateway Server", required=True),
91         'object_id': fields.many2one('ir.model',"Model", required=True),
92         'reply_to': fields.char('TO', size=64, help="Email address used in reply to/from of outgoing messages"),
93         'email_default': fields.char('Default eMail',size=64,help="Default eMail in case of any trouble."),        
94         'mail_history': fields.one2many("mail.gateway.history","gateway_id","History", readonly=True)
95     }
96     _defaults = {
97         'reply_to': lambda * a:tools.config.get('email_from',False)
98     }
99
100     def _fetch_mails(self, cr, uid, ids=False, context={}):
101         '''
102         Function called by the scheduler to fetch mails
103         '''
104         cr.execute('select * from mail_gateway gateway \
105                 inner join mail_gateway_server server \
106                 on server.id = gateway.server_id where server.active = True')
107         ids2 = map(lambda x: x[0], cr.fetchall() or [])
108         return self.fetch_mails(cr, uid, ids=ids2, context=context)
109
110     def parse_mail(self, cr, uid, gateway_id, email_message, context={}):
111         msg_id, res_id, note = (False, False, False)        
112         mail_history_obj = self.pool.get('mail.gateway.history')        
113         mailgateway = self.browse(cr, uid, gateway_id, context=context)
114         try :
115             msg_txt = email.message_from_string(email_message)
116             msg_id =  msg_txt['Message-ID']
117             res_id = self.msg_parse(cr, uid, gateway_id, msg_txt)            
118         except Exception, e:
119             import traceback
120             note = "Error in Parsing Mail: %s " %(str(e))            
121             netsvc.Logger().notifyChannel('Emailgate: Parsing mail:%s' % (mailgateway and (mailgateway.name or
122                      '%s (%s)'%(mailgateway.server_id.login, mailgateway.server_id.name))) or ''
123                      , netsvc.LOG_ERROR, traceback.format_exc())
124
125         mail_history_obj.create(cr, uid, {'name': msg_id, 'res_id': res_id, 'gateway_id': mailgateway.id, 'note': note})
126         return res_id,  note
127
128     def fetch_mails(self, cr, uid, ids=[], context={}):        
129         log_messages = []
130         mailgate_server = False
131         new_messages = []
132         for mailgateway in self.browse(cr, uid, ids):
133             try :
134                 mailgate_server = mailgateway.server_id
135                 if not mailgate_server.active:
136                     continue
137                 mailgate_name =  mailgateway.name or "%s (%s)" % (mailgate_server.login, mailgate_server.name)
138                 res_model = mailgateway.object_id.name
139                 log_messages.append("Mail Server : %s" % mailgate_name)
140                 log_messages.append("="*40)
141                 new_messages = []
142                 if mailgate_server.server_type == 'pop':
143                     if mailgate_server.ssl:
144                         pop_server = POP3_SSL(mailgate_server.name or 'localhost', mailgate_server.port or 995)
145                     else:
146                         pop_server = POP3(mailgate_server.name or 'localhost', mailgate_server.port or 110)
147                     pop_server.user(mailgate_server.login)
148                     pop_server.pass_(mailgate_server.password)
149                     pop_server.list()
150                     (numMsgs, totalSize) = pop_server.stat()
151                     for i in range(1, numMsgs + 1):
152                         (header, msges, octets) = pop_server.retr(i)
153                         res_id, note = self.parse_mail(cr, uid, mailgateway.id, '\n'.join(msges))
154                         log = ''
155                         if res_id:
156                             log = _('Object Successfully Created : %d of %s'% (res_id, res_model))
157                         if note:
158                             log = note
159                         log_messages.append(log)
160                         new_messages.append(i)
161                     pop_server.quit()
162
163                 elif mailgate_server.server_type == 'imap':
164                     if mailgate_server.ssl:
165                         imap_server = IMAP4_SSL(mailgate_server.name or 'localhost', mailgate_server.port or 993)
166                     else:
167                         imap_server = IMAP4(mailgate_server.name or 'localhost', mailgate_server.port or 143)
168                     imap_server.login(mailgate_server.login, mailgate_server.password)
169                     imap_server.select()
170                     typ, data = imap_server.search(None, '(UNSEEN)')
171                     for num in data[0].split():
172                         typ, data = imap_server.fetch(num, '(RFC822)')
173                         res_id, note = self.parse_mail(cr, uid, mailgateway.id, data[0][1])
174                         log = ''
175                         if res_id:
176                             log = _('Object Successfully Created/Modified: %d of %s'% (res_id, res_model))
177                         if note:
178                             log = note
179                         log_messages.append(log)
180                         new_messages.append(num)
181                     imap_server.close()
182                     imap_server.logout()
183
184             except Exception, e:
185                  import traceback
186                  log_messages.append("Error in Fetching Mail: %s " %(str(e)))                 
187                  netsvc.Logger().notifyChannel('Emailgate: Fetching mail:[%d]%s' % 
188                     (mailgate_server and mailgate_server.id or 0, mailgate_server and mailgate_server.name or ''),
189                      netsvc.LOG_ERROR, traceback.format_exc())
190
191             log_messages.append("-"*25)
192             log_messages.append("Total Read Mail: %d\n\n" %(len(new_messages)))
193         return log_messages    
194
195     def emails_get(self, email_from):
196         res = tools.email_re.search(email_from)
197         return res and res.group(1)
198
199     def partner_get(self, cr, uid, email):
200         mail = self.emails_get(email)
201         adr_ids = self.pool.get('res.partner.address').search(cr, uid, [('email', '=', mail)])
202         if not adr_ids:
203             return {}
204         adr = self.pool.get('res.partner.address').read(cr, uid, adr_ids, ['partner_id'])
205         res = {}
206         if len(adr):
207             res = {
208                 'partner_address_id': adr[0]['id'],
209                 'partner_id': adr[0].get('partner_id',False) and adr[0]['partner_id'][0] or False
210             }
211         return res
212
213     def _to_decode(self, s, charsets):
214        for charset in charsets:
215            if charset:
216                try:
217                    return s.decode(charset)
218                except UnicodeError:  
219                     pass         
220        try:
221            return s.decode('ascii')
222        except UnicodeError:
223            return s 
224
225     def _decode_header(self, s):        
226         from email.Header import decode_header
227         s = decode_header(s)
228         return ''.join(map(lambda x:self._to_decode(x[0], x[1]), s))
229
230     def msg_new(self, cr, uid, msg, model):
231         message = self.msg_body_get(msg)
232         res_model = self.pool.get(model)
233         res_id = res_model.msg_new(cr, uid, msg)
234         if res_id:
235             attachments = message['attachment']
236
237             for attach in attachments or []:
238                 data_attach = {
239                     'name': str(attach),
240                     'datas':binascii.b2a_base64(str(attachments[attach])),
241                     'datas_fname': str(attach),
242                     'description': 'Mail attachment',
243                     'res_model': model,
244                     'res_id': res_id
245                 }
246                 self.pool.get('ir.attachment').create(cr, uid, data_attach)
247         return res_id
248
249
250     def msg_body_get(self, msg):        
251         message = {};
252         message['body'] = '';
253         message['attachment'] = {};
254         attachment = message['attachment'];
255         counter = 1;
256         def replace(match):
257             return ''        
258         for part in msg.walk():
259             if part.get_content_maintype() == 'multipart':
260                 continue
261
262             if part.get_content_maintype()=='text':
263                 buf = part.get_payload(decode=True)
264                 if buf:
265                     txt = self._to_decode(buf, part.get_charsets)
266                     txt = re.sub("<(\w)>", replace, txt)
267                     txt = re.sub("<\/(\w)>", replace, txt)
268                 if txt and part.get_content_subtype() == 'plain':
269                     message['body'] += txt 
270                 elif txt and part.get_content_subtype() == 'html':                                                               
271                     message['body'] += tools.html2plaintext(txt)  
272                 
273                 filename = part.get_filename();
274                 if filename :
275                     attachment[filename] = part.get_payload(decode=True);
276                     
277             elif part.get_content_maintype()=='application' or part.get_content_maintype()=='image' or part.get_content_maintype()=='text':
278                 filename = part.get_filename();
279                 if filename :
280                     attachment[filename] = part.get_payload(decode=True);
281                 else:
282                     filename = 'attach_file'+str(counter);
283                     counter += 1;
284                     attachment[filename] = part.get_payload(decode=True);
285                 #end if
286             #end if
287             message['attachment'] = attachment
288         #end for              
289         return message
290     #end def
291
292     def msg_update(self, cr, uid, msg, res_id, res_model, user_email):        
293         if user_email and self.emails_get(user_email)==self.emails_get(self._decode_header(msg['From'])):
294             return self.msg_user(cr, uid, msg, res_id, res_model)
295         else:
296             return self.msg_partner(cr, uid, msg, res_id, res_model)
297
298     def msg_act_get(self, msg):
299         body = self.msg_body_get(msg)
300
301         # handle email body commands (ex: Set-State: Draft)
302         actions = {}
303         body_data = ''
304         for line in body['body'].split('\n'):
305             res = tools.command_re.match(line)
306             if res:
307                 actions[res.group(1).lower()] = res.group(2).lower()
308             else:
309                 body_data += line+'\n'
310         return actions, body_data
311
312     def msg_user(self, cr, uid, msg, res_id, res_model):
313         actions, body_data = self.msg_act_get(msg)        
314         data = {}
315         if 'user' in actions:
316             uids = self.pool.get('res.users').name_search(cr, uid, actions['user'])
317             if uids:
318                 data['user_id'] = uids[0][0]
319
320         res_model = self.pool.get(res_model)        
321         return res_model.msg_update(cr, uid, res_id, msg, data=data, default_act='pending')        
322
323     def msg_send(self, msg, reply_to, emails, priority=None, res_id=False):         
324         if not emails:
325             return False                
326         msg_to = [emails[0]]
327         msg_subject = msg['Subject']        
328         msg_cc = []
329         msg_body = self.msg_body_get(msg)        
330         if len(emails)>1:            
331             msg_cc = emails[1:]
332         msg_attachment = map(lambda x: (x[0], x[1]), msg_body['attachment'].items())                  
333         return tools.email_send(reply_to, msg_to, msg_subject , msg_body['body'], email_cc=msg_cc, 
334                          reply_to=reply_to, attach=msg_attachment, openobject_id=res_id, priority=priority)
335         
336
337     def msg_partner(self, cr, uid, msg, res_id, res_model):
338         res_model = self.pool.get(res_model)        
339         return res_model.msg_update(cr, uid, res_id, msg, data={}, default_act='open')      
340
341     
342
343     def msg_parse(self, cr, uid, mailgateway_id, msg):
344         mailgateway = self.browse(cr, uid, mailgateway_id)
345         res_model = mailgateway.object_id.model
346         res_str = tools.reference_re.search(msg.get('References', ''))
347         if res_str:
348             res_str = res_str.group(1)
349         else:
350             res_str = tools.res_re.search(msg.get('Subject', ''))
351             if res_str:
352                 res_str = res_str.group(1)
353
354         def msg_test(res_str):
355             emails = ('', '', '', '')
356             if not res_str:
357                 return (False, emails)  
358             res_str = int(res_str)          
359             if hasattr(self.pool.get(res_model), 'emails_get'):
360                 emails = self.pool.get(res_model).emails_get(cr, uid, [res_str])[0]
361             return (res_str, emails)
362
363         (res_id, emails) = msg_test(res_str)
364         user_email, from_email, cc_email, priority = emails
365         if res_id:
366             self.msg_update(cr, uid, msg, res_id, res_model, user_email)
367             
368         else:
369             res_id = self.msg_new(cr, uid, msg, res_model)
370             (res_id, emails) = msg_test(res_id)
371             user_email, from_email, cc_email, priority = emails
372             subject = self._decode_header(msg['subject'])
373             if msg.get('Subject', ''):
374                 del msg['Subject']
375             msg['Subject'] = '[%s] %s' %(str(res_id), subject)            
376
377         em = [user_email or '', from_email] + (cc_email or '').split(',')
378         emails = map(self.emails_get, filter(None, em))
379         mm = [self._decode_header(msg['From']), self._decode_header(msg['To'])]+self._decode_header(msg.get('Cc','')).split(',')
380         msg_mails = map(self.emails_get, filter(None, mm))
381         emails = filter(lambda m: m and m not in msg_mails, emails)
382         try:
383             self.msg_send(msg, mailgateway.reply_to, emails, priority, res_id)
384             if hasattr(self.pool.get(res_model), 'msg_send'):
385                 emails = self.pool.get(res_model).msg_send(cr, uid, res_id)
386         except Exception, e:
387             if mailgateway.email_default:
388                 a = self._decode_header(msg['Subject'])
389                 del msg['Subject']
390                 msg['Subject'] = '[OpenERP-Error] ' + a
391                 self.msg_send(msg, mailgateway.reply_to, mailgateway.email_default.split(','), res_id)
392             raise e 
393         return res_id
394
395 mail_gateway()
396
397 class mail_gateway_history(osv.osv):
398     _name = "mail.gateway.history"
399     _description = "Mail Gateway History"
400     _columns = {
401         'name': fields.char('Message Id', size=64, help="Message Id in Email Server."),
402         'res_id': fields.integer("Resource ID"),        
403         'gateway_id': fields.many2one('mail.gateway',"Mail Gateway", required=True),
404         'model_id':fields.related('gateway_id', 'object_id', type='many2one', relation='ir.model', string='Model'), 
405         'note': fields.text('Notes'),
406         'create_date': fields.datetime('Created Date'),
407     }
408     _order = 'id desc'
409 mail_gateway_history()