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