merged
[odoo/odoo.git] / addons / crm / scripts / openerp-mailgate / openerp-mailgate.py
1 #!/usr/bin/python
2 # -*- encoding: utf-8 -*-
3 ##############################################################################
4 #
5 #    OpenERP, Open Source Management Solution   
6 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
7 #    $Id$
8 #
9 #    This program is free software: you can redistribute it and/or modify
10 #    it under the terms of the GNU General Public License as published by
11 #    the Free Software Foundation, either version 3 of the License, or
12 #    (at your option) any later version.
13 #
14 #    This program is distributed in the hope that it will be useful,
15 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
16 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 #    GNU General Public License for more details.
18 #
19 #    You should have received a copy of the GNU General Public License
20 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 #
22 ##############################################################################
23
24 import re
25 import smtplib
26 import email, mimetypes
27 from email.Header import decode_header
28 from email.MIMEText import MIMEText
29 import xmlrpclib
30 import os
31 import binascii
32 import time, socket
33
34 email_re = re.compile(r"""
35     ([a-zA-Z][\w\.-]*[a-zA-Z0-9]     # username part
36     @                                # mandatory @ sign
37     [a-zA-Z0-9][\w\.-]*              # domain must start with a letter ... Ged> why do we include a 0-9 then?
38      \.
39      [a-z]{2,3}                      # TLD
40     )
41     """, re.VERBOSE)
42 case_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
43 command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
44 reference_re = re.compile("<.*-tinycrm-(\\d+)@(.*)>", re.UNICODE)
45
46 priorities = {
47     '1': '1 (Highest)',
48     '2': '2 (High)',
49     '3': '3 (Normal)',
50     '4': '4 (Low)',
51     '5': '5 (Lowest)',
52 }
53
54 class rpc_proxy(object):
55     def __init__(self, uid, passwd, host='localhost', port=8069, path='object', dbname='terp'):
56         self.rpc = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (host, port, path))
57         self.user_id = uid
58         self.passwd = passwd
59         self.dbname = dbname
60
61     def __call__(self, *request):
62         return self.rpc.execute(self.dbname, self.user_id, self.passwd, *request)
63
64 class email_parser(object):
65     def __init__(self, uid, password, section, email, email_default, dbname, host):
66         self.rpc = rpc_proxy(uid, password, host=host, dbname=dbname)
67         try:
68             self.section_id = int(section)
69         except:
70             self.section_id = self.rpc('crm.case.section', 'search', [('code','=',section)])[0]
71         self.email = email
72         self.email_default = email_default
73         self.canal_id = False
74
75     def email_get(self, email_from):
76         res = email_re.search(email_from)
77         return res and res.group(1)
78
79     def partner_get(self, email):
80         mail = self.email_get(email)
81         adr_ids = self.rpc('res.partner.address', 'search', [('email', '=', mail)])
82         if not adr_ids:
83             return {}
84         adr = self.rpc('res.partner.address', 'read', adr_ids, ['partner_id'])
85         return {
86             'partner_address_id': adr[0]['id'],
87             'partner_id': adr[0]['partner_id'][0]
88         }
89
90     def _decode_header(self, s):
91         from email.Header import decode_header
92         s = decode_header(s)
93         return ''.join(map(lambda x:x[0].decode(x[1] or 'ascii', 'replace'), s))
94
95     def msg_new(self, msg):
96         message = self.msg_body_get(msg)
97         data = {
98             'name': self._decode_header(msg['Subject']),
99             'section_id': self.section_id,
100             'email_from': self._decode_header(msg['From']),
101             'email_cc': self._decode_header(msg['Cc'] or ''),
102             'canal_id': self.canal_id,
103             'user_id': False,
104             'description': message['body'],
105             'history_line': [(0, 0, {'description': message['body'], 'email': msg['From'] })],
106         }
107         try:
108             data.update(self.partner_get(self._decode_header(msg['From'])))
109         except Exception, e:
110             import netsvc
111             netsvc.Logger().notifyChannel('mailgate', netsvc.LOG_ERROR, "%s" % e)
112
113         try:
114             id = self.rpc('crm.case', 'create', data)
115
116         except Exception,e:
117             if getattr(e,'faultCode','') and 'AccessError' in e.faultCode:
118                 e = '\n\nThe Specified user does not have an access to the CRM case.'
119             print e
120         attachments = message['attachment']
121
122         for attach in attachments or []:
123             data_attach = {
124                 'name': str(attach),
125                 'datas':binascii.b2a_base64(str(attachments[attach])),
126                 'datas_fname': str(attach),
127                 'description': 'Mail attachment',
128                 'res_model': 'crm.case',
129                 'res_id': id
130             }
131             self.rpc('ir.attachment', 'create', data_attach)
132
133         return id
134
135 #   #change the return type format to dictionary
136 #   {
137 #       'body':'body part',
138 #       'attachment':{
139 #                       'file_name':'file data',
140 #                       'file_name':'file data',
141 #                       'file_name':'file data',
142 #                   }
143 #   }
144 #   #
145     def msg_body_get(self, msg):
146         message = {};
147         message['body'] = u'';
148         message['attachment'] = {};
149         attachment = message['attachment'];
150         counter = 1;
151         def replace(match):
152             return ''
153             
154         for part in msg.walk():
155             if part.get_content_maintype() == 'multipart':
156                 continue
157
158             if part.get_content_maintype()=='text' and part.get_content_subtype() in ('plain','html'):
159                 buf = part.get_payload(decode=True)
160                 if buf:
161                     txt = buf.decode(part.get_charsets()[0] or 'ascii', 'replace')
162                     txt = re.sub("<(\w)>", replace, txt)
163                     txt = re.sub("<\/(\w)>", replace, txt)
164                     message['body'] += txt
165             elif part.get_content_maintype()=='application' or part.get_content_maintype()=='image' or part.get_content_maintype()=='text':
166                 filename = part.get_filename();
167                 if filename :
168                     attachment[filename] = part.get_payload(decode=True);
169                 else:
170                     filename = 'attach_file'+str(counter);
171                     counter += 1;
172                     attachment[filename] = part.get_payload(decode=True);
173                 #end if
174             #end if
175             message['attachment'] = attachment
176         #end for
177         return message
178     #end def
179
180     def msg_user(self, msg, id):
181         body = self.msg_body_get(msg)
182
183         # handle email body commands (ex: Set-State: Draft)
184         actions = {}
185         body_data=''
186         for line in body['body'].split('\n'):
187             res = command_re.match(line)
188             if res:
189                 actions[res.group(1).lower()] = res.group(2).lower()
190             else:
191                 body_data += line+'\n'
192         body['body'] = body_data
193
194         data = {
195             'description': body['body'],
196             'history_line': [(0, 0, {'description': body['body'], 'email': msg['From']})],
197         }
198         act = 'case_close'
199         if 'state' in actions:
200             if actions['state'] in ['draft','close','cancel','open','pending']:
201                 act = 'case_' + actions['state']
202
203         for k1,k2 in [('cost','planned_cost'),('revenue','planned_revenue'),('probability','probability')]:
204             try:
205                 data[k2] = float(actions[k1])
206             except:
207                 pass
208
209         if 'priority' in actions:
210             if actions['priority'] in ('1','2','3','4','5'):
211                 data['priority'] = actions['priority']
212
213         if 'partner' in actions:
214             data['email_from'] = actions['partner'][:128]
215
216         if 'user' in actions:
217             uids = self.rpc('res.users', 'name_search', actions['user'])
218             if uids:
219                 data['user_id'] = uids[0][0]
220
221         self.rpc('crm.case', act, [id])
222         self.rpc('crm.case', 'write', [id], data)
223         return id
224
225     def msg_send(self, msg, emails, priority=None):
226         if not len(emails):
227             return False
228         del msg['To']
229         msg['To'] = emails[0]
230         if len(emails)>1:
231             if 'Cc' in msg:
232                 del msg['Cc']
233             msg['Cc'] = ','.join(emails[1:])
234         del msg['Reply-To']
235         msg['Reply-To'] = self.email
236         if priority:
237             msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
238         s = smtplib.SMTP()
239         s.connect()
240         s.sendmail(self.email, emails, msg.as_string())
241         s.close()
242         return True
243
244     def msg_partner(self, msg, id):
245         message = self.msg_body_get(msg)
246         body = message['body']
247         act = 'case_open'
248         self.rpc('crm.case', act, [id])
249         body2 = '\n'.join(map(lambda l: '> '+l, (body or '').split('\n')))
250         data = {
251             'description':body,
252             'history_line': [(0, 0, {'description': body, 'email': msg['From'][:84]})],
253         }
254         self.rpc('crm.case', 'write', [id], data)
255         return id
256
257     def msg_test(self, msg, case_str):
258         if not case_str:
259             return (False, False)
260         emails = self.rpc('crm.case', 'emails_get', int(case_str))
261         return (int(case_str), emails)
262
263     def parse(self, msg):
264         case_str = reference_re.search(msg.get('References', ''))
265         if case_str:
266             case_str = case_str.group(1)
267         else:
268             case_str = case_re.search(msg.get('Subject', ''))
269             if case_str:
270                 case_str = case_str.group(1)
271         (case_id, emails) = self.msg_test(msg, case_str)
272         if case_id:
273             if emails[0] and self.email_get(emails[0])==self.email_get(self._decode_header(msg['From'])):
274                 self.msg_user(msg, case_id)
275             else:
276                 self.msg_partner(msg, case_id)
277         else:
278             case_id = self.msg_new(msg)
279             subject = self._decode_header(msg['subject'])
280             if msg.get('Subject', ''):
281                 del msg['Subject']
282             msg['Subject'] = '['+str(case_id)+'] '+subject
283             msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(case_id)+'@'+socket.gethostname()+'>'
284
285         emails = self.rpc('crm.case', 'emails_get', case_id)
286         priority = emails[3]
287         em = [emails[0], emails[1]] + (emails[2] or '').split(',')
288         emails = map(self.email_get, filter(None, em))
289
290         mm = [self._decode_header(msg['From']), self._decode_header(msg['To'])]+self._decode_header(msg.get('Cc','')).split(',')
291         msg_mails = map(self.email_get, filter(None, mm))
292
293         emails = filter(lambda m: m and m not in msg_mails, emails)
294         try:
295             self.msg_send(msg, emails, priority)
296         except:
297             if self.email_default:
298                 a = self._decode_header(msg['Subject'])
299                 del msg['Subject']
300                 msg['Subject'] = '[OpenERP-CaseError] ' + a
301                 self.msg_send(msg, self.email_default.split(','))
302         return emails
303
304 if __name__ == '__main__':
305     import sys, optparse
306     parser = optparse.OptionParser(
307         usage='usage: %prog [options]',
308         version='%prog v1.0')
309
310     group = optparse.OptionGroup(parser, "Note",
311         "This program parse a mail from standard input and communicate "
312         "with the Open ERP server for case management in the CRM module.")
313     parser.add_option_group(group)
314
315     parser.add_option("-u", "--user", dest="userid", help="ID of the user in Open ERP", default=1, type='int')
316     parser.add_option("-p", "--password", dest="password", help="Password of the user in Open ERP", default='admin')
317     parser.add_option("-e", "--email", dest="email", help="Email address used in the From field of outgoing messages")
318     parser.add_option("-s", "--section", dest="section", help="ID or code of the case section", default="support")
319     parser.add_option("-m", "--default", dest="default", help="Default eMail in case of any trouble.", default=None)
320     parser.add_option("-d", "--dbname", dest="dbname", help="Database name (default: terp)", default='terp')
321     parser.add_option("--host", dest="host", help="Hostname of the Open ERP Server", default="localhost")
322
323
324     (options, args) = parser.parse_args()
325     parser = email_parser(options.userid, options.password, options.section, options.email, options.default, dbname=options.dbname, host=options.host)
326
327     msg_txt = email.message_from_file(sys.stdin)
328
329     try :
330         parser.parse(msg_txt)
331     except Exception,e:
332         if getattr(e,'faultCode','') and 'Connection unexpectedly closed' in e.faultCode:
333             print e
334  
335 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
336