lead view update, name vs partner_name
[odoo/odoo.git] / addons / crm / scripts / openerp_mailgate / openerp_mailgate.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 ##############################################################################
4 #    
5 #    OpenERP, Open Source Management Solution
6 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.     
20 #
21 ###########################################################################################
22
23 import re
24 import smtplib
25 import email, mimetypes
26 from email.Header import decode_header
27 from email.MIMEText import MIMEText
28 import xmlrpclib
29 import os
30 import binascii
31 import time, socket
32
33
34 email_re = re.compile(r"([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6})")
35 case_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
36 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
37 reference_re = re.compile("<.*-tinycrm-(\\d+)@(.*)>", re.UNICODE)
38
39 priorities = {
40     '1': '1 (Highest)', 
41     '2': '2 (High)', 
42     '3': '3 (Normal)', 
43     '4': '4 (Low)', 
44     '5': '5 (Lowest)', 
45 }
46
47 def html2plaintext(html, body_id=None, encoding='utf-8'):
48     ## (c) Fry-IT, www.fry-it.com, 2007
49     ## <peter@fry-it.com>
50     ## download here: http://www.peterbe.com/plog/html2plaintext
51     
52     
53     """ from an HTML text, convert the HTML to plain text.
54     If @body_id is provided then this is the tag where the 
55     body (not necessarily <body>) starts.
56     """
57     try:
58         from BeautifulSoup import BeautifulSoup, SoupStrainer, Comment
59     except:
60         return html
61             
62     urls = []
63     if body_id is not None:
64         strainer = SoupStrainer(id=body_id)
65     else:
66         strainer = SoupStrainer('body')
67     
68     soup = BeautifulSoup(html, parseOnlyThese=strainer, fromEncoding=encoding)
69     for link in soup.findAll('a'):
70         title = link.renderContents()
71         for url in [x[1] for x in link.attrs if x[0]=='href']:
72             urls.append(dict(url=url, tag=str(link), title=title))
73
74     html = soup.__str__()
75             
76     url_index = []
77     i = 0
78     for d in urls:
79         if d['title'] == d['url'] or 'http://'+d['title'] == d['url']:
80             html = html.replace(d['tag'], d['url'])
81         else:
82             i += 1
83             html = html.replace(d['tag'], '%s [%s]' % (d['title'], i))
84             url_index.append(d['url'])
85
86     html = html.replace('<strong>', '*').replace('</strong>', '*')
87     html = html.replace('<b>', '*').replace('</b>', '*')
88     html = html.replace('<h3>', '*').replace('</h3>', '*')
89     html = html.replace('<h2>', '**').replace('</h2>', '**')
90     html = html.replace('<h1>', '**').replace('</h1>', '**')
91     html = html.replace('<em>', '/').replace('</em>', '/')
92     
93
94     # the only line breaks we respect is those of ending tags and 
95     # breaks
96     
97     html = html.replace('\n', ' ')
98     html = html.replace('<br>', '\n')
99     html = html.replace('<tr>', '\n')
100     html = html.replace('</p>', '\n\n')
101     html = re.sub('<br\s*/>', '\n', html)
102     html = html.replace(' ' * 2, ' ')
103
104
105     # for all other tags we failed to clean up, just remove then and 
106     # complain about them on the stderr
107     def desperate_fixer(g):
108         #print >>sys.stderr, "failed to clean up %s" % str(g.group())
109         return ' '
110
111     html = re.sub('<.*?>', desperate_fixer, html)
112
113     # lstrip all lines
114     html = '\n'.join([x.lstrip() for x in html.splitlines()])
115
116     for i, url in enumerate(url_index):
117         if i == 0:
118             html += '\n\n'
119         html += '[%s] %s\n' % (i+1, url)       
120     return html
121
122 class rpc_proxy(object):
123     def __init__(self, uid, passwd, host='localhost', port=8069, path='object', dbname='terp'):        
124         self.rpc = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (host, port, path))
125         self.user_id = uid
126         self.passwd = passwd
127         self.dbname = dbname
128
129     def __call__(self, *request, **kwargs):
130         return self.rpc.execute(self.dbname, self.user_id, self.passwd, *request, **kwargs)
131
132 class email_parser(object):
133     def __init__(self, uid, password, model, email, email_default, dbname, host, port):        
134         self.rpc = rpc_proxy(uid, password, host=host, port=port, dbname=dbname)
135         try:
136             self.model_id = int(model)
137             self.model = str(model)
138         except:
139             self.model_id = self.rpc('ir.model', 'search', [('model', '=', model)])[0]
140             self.model = str(model)
141         self.email = email
142         self.email_default = email_default
143         self.canal_id = False
144
145     def email_get(self, email_from):
146         res = email_re.search(email_from)
147         return res and res.group(1)
148
149     def partner_get(self, email):
150         mail = self.email_get(email)
151         adr_ids = self.rpc('res.partner.address', 'search', [('email', '=', mail)])
152         if not adr_ids:
153             return {}
154         adr = self.rpc('res.partner.address', 'read', adr_ids, ['partner_id'])
155         return {
156             'partner_address_id': adr[0]['id'], 
157             'partner_id': adr[0].get('partner_id', False) and adr[0]['partner_id'][0] or False
158         }
159
160     def _decode_header(self, s):
161         from email.Header import decode_header
162         s = decode_header(s)
163         return ''.join(map(lambda x:x[0].decode(x[1] or 'ascii', 'replace'), s))
164
165     def msg_new(self, msg):
166         message = self.msg_body_get(msg)
167         data = {
168             'name': self._decode_header(msg['Subject']), 
169             'email_from': self._decode_header(msg['From']), 
170             'email_cc': self._decode_header(msg['Cc'] or ''), 
171             'canal_id': self.canal_id, 
172             'user_id': False, 
173             'description': message['body'], 
174         }
175         data.update(self.partner_get(self._decode_header(msg['From'])))
176
177         try:
178             id = self.rpc(self.model, 'create', data)
179             self.rpc(self.model, 'history', [id], 'Receive', True, msg['From'], message['body'])
180             #self.rpc(self.model, 'case_open', [id])
181         except Exception, e:
182             if getattr(e, 'faultCode', '') and 'AccessError' in e.faultCode:
183                 e = '\n\nThe Specified user does not have an access to the CRM case.'
184             print e
185         attachments = message['attachment']
186
187         for attach in attachments or []:
188             data_attach = {
189                 'name': str(attach), 
190                 'datas': binascii.b2a_base64(str(attachments[attach])), 
191                 'datas_fname': str(attach), 
192                 'description': 'Mail attachment', 
193                 'res_model': self.model, 
194                 'res_id': id
195             }
196             self.rpc('ir.attachment', 'create', data_attach)
197
198         return id
199
200 #   #change the return type format to dictionary
201 #   {
202 #       'body':'body part',
203 #       'attachment':{
204 #                       'file_name':'file data',
205 #                       'file_name':'file data',
206 #                       'file_name':'file data',
207 #                   }
208 #   }
209 #   #
210     def msg_body_get(self, msg):
211         message = {};
212         message['body'] = '';
213         message['attachment'] = {};
214         attachment = message['attachment'];
215         counter = 1;
216         def replace(match):
217             return ''
218             
219         for part in msg.walk():
220             if part.get_content_maintype() == 'multipart':
221                 continue
222
223             if part.get_content_maintype()=='text':
224                 buf = part.get_payload(decode=True)
225                 if buf:
226                     txt = buf.decode(part.get_charsets()[0] or 'ascii', 'replace')
227                     txt = re.sub("<(\w)>", replace, txt)
228                     txt = re.sub("<\/(\w)>", replace, txt)
229                 if txt and part.get_content_subtype() == 'plain':
230                     message['body'] += txt 
231                 elif txt and part.get_content_subtype() == 'html':                                                               
232                     message['body'] += html2plaintext(txt)  
233                 
234                 filename = part.get_filename();
235                 if filename :
236                     attachment[filename] = part.get_payload(decode=True);
237                     
238             elif part.get_content_maintype()=='application' or part.get_content_maintype()=='image' or part.get_content_maintype()=='text':
239                 filename = part.get_filename();
240                 if filename :
241                     attachment[filename] = part.get_payload(decode=True);
242                 else:
243                     filename = 'attach_file'+str(counter);
244                     counter += 1;
245                     attachment[filename] = part.get_payload(decode=True);
246                 #end if
247             #end if
248             message['attachment'] = attachment
249         #end for        
250         return message
251
252     def msg_user(self, msg, id):
253         body = self.msg_body_get(msg)
254
255         # handle email body commands (ex: Set-State: Draft)
256         actions = {}
257         body_data=''
258         for line in body['body'].split('\n'):
259             res = command_re.match(line)
260             if res:
261                 actions[res.group(1).lower()] = res.group(2).lower()
262             else:
263                 body_data += line+'\n'
264         body['body'] = body_data
265
266         data = {
267             'description': body['body'], 
268         }
269         act = 'case_pending'
270         if 'state' in actions:
271             if actions['state'] in ['draft', 'close', 'cancel', 'open', 'pending']:
272                 act = 'case_' + actions['state']
273
274         for k1, k2 in [('cost', 'planned_cost'), ('revenue', 'planned_revenue'), ('probability', 'probability')]:
275             try:
276                 data[k2] = float(actions[k1])
277             except:
278                 pass
279
280         if 'priority' in actions:
281             if actions['priority'] in ('1', '2', '3', '4', '5'):
282                 data['priority'] = actions['priority']
283
284         if 'partner' in actions:
285             data['email_from'] = actions['partner'][:128]
286
287         if 'user' in actions:
288             uids = self.rpc('res.users', 'name_search', actions['user'])
289             if uids:
290                 data['user_id'] = uids[0][0]
291
292         self.rpc(self.model, act, [id])
293         self.rpc(self.model, 'write', [id], data)
294         self.rpc(self.model, 'history', [id], 'Send', True, msg['From'], message['body'])
295         return id
296
297     def msg_send(self, msg, emails, priority=None):
298         if not len(emails):
299             return False
300         del msg['To']
301         msg['To'] = emails[0]
302         if len(emails)>1:
303             if 'Cc' in msg:
304                 del msg['Cc']
305             msg['Cc'] = ','.join(emails[1:])
306         del msg['Reply-To']
307         msg['Reply-To'] = self.email
308         if priority:
309             msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
310         s = smtplib.SMTP()
311         s.connect()
312         s.sendmail(self.email, emails, msg.as_string())
313         s.close()
314         return True
315
316     def msg_partner(self, msg, id):
317         message = self.msg_body_get(msg)
318         body = message['body']
319         act = 'case_open'
320         self.rpc(self.model, act, [id])
321         body2 = '\n'.join(map(lambda l: '> '+l, (body or '').split('\n')))
322         data = {
323             'description':body, 
324         }
325         self.rpc(self.model, 'write', [id], data)
326         self.rpc(self.model, 'history', [id], 'Send', True, msg['From'], message['body'])
327         return id
328
329     def msg_test(self, msg, case_str):
330         if not case_str:
331             return (False, False)
332         emails = self.rpc(self.model, 'emails_get', int(case_str))
333         return (int(case_str), emails)
334
335     def parse(self, msg):
336         case_str = reference_re.search(msg.get('References', ''))
337         if case_str:
338             case_str = case_str.group(1)
339         else:
340             case_str = case_re.search(msg.get('Subject', ''))
341             if case_str:
342                 case_str = case_str.group(1)
343         (case_id, emails) = self.msg_test(msg, case_str)
344         if case_id:
345             if emails[0] and self.email_get(emails[0])==self.email_get(self._decode_header(msg['From'])):
346                 self.msg_user(msg, case_id)
347             else:
348                 self.msg_partner(msg, case_id)
349         else:
350             case_id = self.msg_new(msg)
351             subject = self._decode_header(msg['subject'])
352             if msg.get('Subject', ''):
353                 del msg['Subject']
354             msg['Subject'] = '['+str(case_id)+'] '+subject
355             msg['Message-Id'] = '<'+str(time.time())+'-openerpcrm-'+str(case_id)+'@'+socket.gethostname()+'>'
356
357         emails = self.rpc(self.model, 'emails_get', case_id)
358         priority = emails[3]
359         em = [emails[0], emails[1]] + (emails[2] or '').split(',')
360         emails = map(self.email_get, filter(None, em))
361
362         mm = [self._decode_header(msg['From']), self._decode_header(msg['To'])]+self._decode_header(msg.get('Cc', '')).split(',')
363         msg_mails = map(self.email_get, filter(None, mm))
364
365         emails = filter(lambda m: m and m not in msg_mails, emails)
366         try:
367             self.msg_send(msg, emails, priority)
368         except:
369             if self.email_default:
370                 a = self._decode_header(msg['Subject'])
371                 del msg['Subject']
372                 msg['Subject'] = '[OpenERP-CaseError] ' + a
373                 self.msg_send(msg, self.email_default.split(','))
374         return case_id, emails
375
376 if __name__ == '__main__':
377     import sys, optparse
378     parser = optparse.OptionParser( usage='usage: %prog [options]', version='%prog v1.0')
379     group = optparse.OptionGroup(parser, "Note", 
380         "This program parse a mail from standard input and communicate "
381         "with the Open ERP server for case management in the CRM module.")
382     parser.add_option_group(group)
383     parser.add_option("-u", "--user", dest="userid", help="ID of the user in Open ERP", default=1, type='int')
384     parser.add_option("-p", "--password", dest="password", help="Password of the user in Open ERP", default='admin')
385     parser.add_option("-e", "--email", dest="email", help="Email address used in the From field of outgoing messages")
386     parser.add_option("-o", "--model", dest="model", help="Name or ID of crm model", default="crm.lead")
387     parser.add_option("-m", "--default", dest="default", help="Default eMail in case of any trouble.", default=None)
388     parser.add_option("-d", "--dbname", dest="dbname", help="Database name (default: terp)", default='terp')
389     parser.add_option("--host", dest="host", help="Hostname of the Open ERP Server", default="localhost")
390     parser.add_option("--port", dest="port", help="Port of the Open ERP Server", default="8069")
391
392
393     (options, args) = parser.parse_args()
394     parser = email_parser(options.userid, options.password, options.model, options.email, options.default, dbname=options.dbname, host=options.host, port=options.port)
395
396     msg_txt = email.message_from_file(sys.stdin)
397
398     parser.parse(msg_txt)
399  
400 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: