1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ###########################################################################################
24 import email, mimetypes
25 from email.Header import decode_header
26 from email.MIMEText import MIMEText
33 email_re = re.compile(r"""
34 ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part
36 [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then?
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)
53 def html2plaintext(html, body_id=None, encoding='utf-8'):
54 ## (c) Fry-IT, www.fry-it.com, 2007
56 ## download here: http://www.peterbe.com/plog/html2plaintext
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.
64 from BeautifulSoup import BeautifulSoup, SoupStrainer, Comment
69 if body_id is not None:
70 strainer = SoupStrainer(id=body_id)
72 strainer = SoupStrainer('body')
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))
85 if d['title'] == d['url'] or 'http://'+d['title'] == d['url']:
86 html = html.replace(d['tag'], d['url'])
89 html = html.replace(d['tag'], '%s [%s]' % (d['title'], i))
90 url_index.append(d['url'])
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>', '/')
100 # the only line breaks we respect is those of ending tags and
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, ' ')
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())
117 html = re.sub('<.*?>', desperate_fixer, html)
120 html = '\n'.join([x.lstrip() for x in html.splitlines()])
122 for i, url in enumerate(url_index):
125 html += '[%s] %s\n' % (i+1, url)
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))
135 def __call__(self, *request, **kwargs):
136 return self.rpc.execute(self.dbname, self.user_id, self.passwd, *request, **kwargs)
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)
142 self.model_id = int(model)
143 self.model = str(model)
145 self.model_id = self.rpc('ir.model', 'search', [('model', '=', model)])[0]
146 self.model = str(model)
148 self.email_default = email_default
149 self.canal_id = False
151 def email_get(self, email_from):
152 res = email_re.search(email_from)
153 return res and res.group(1)
155 def partner_get(self, email):
156 mail = self.email_get(email)
157 adr_ids = self.rpc('res.partner.address', 'search', [('email', '=', mail)])
160 adr = self.rpc('res.partner.address', 'read', adr_ids, ['partner_id'])
162 'partner_address_id': adr[0]['id'],
163 'partner_id': adr[0].get('partner_id', False) and adr[0]['partner_id'][0] or False
166 def _decode_header(self, s):
167 from email.Header import decode_header
169 return ''.join(map(lambda x:x[0].decode(x[1] or 'ascii', 'replace'), s))
171 def msg_new(self, msg):
172 message = self.msg_body_get(msg)
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,
179 'description': message['body'],
182 data.update(self.partner_get(self._decode_header(msg['From'])))
185 netsvc.Logger().notifyChannel('mailgate', netsvc.LOG_ERROR, "%s" % e)
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])
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.'
195 attachments = message['attachment']
197 for attach in attachments or []:
200 'datas': binascii.b2a_base64(str(attachments[attach])),
201 'datas_fname': str(attach),
202 'description': 'Mail attachment',
203 'res_model': self.model,
206 self.rpc('ir.attachment', 'create', data_attach)
210 # #change the return type format to dictionary
212 # 'body':'body part',
214 # 'file_name':'file data',
215 # 'file_name':'file data',
216 # 'file_name':'file data',
220 def msg_body_get(self, msg):
222 message['body'] = '';
223 message['attachment'] = {};
224 attachment = message['attachment'];
229 for part in msg.walk():
230 if part.get_content_maintype() == 'multipart':
233 if part.get_content_maintype()=='text':
234 buf = part.get_payload(decode=True)
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)
244 filename = part.get_filename();
246 attachment[filename] = part.get_payload(decode=True);
248 elif part.get_content_maintype()=='application' or part.get_content_maintype()=='image' or part.get_content_maintype()=='text':
249 filename = part.get_filename();
251 attachment[filename] = part.get_payload(decode=True);
253 filename = 'attach_file'+str(counter);
255 attachment[filename] = part.get_payload(decode=True);
258 message['attachment'] = attachment
263 def msg_user(self, msg, id):
264 body = self.msg_body_get(msg)
266 # handle email body commands (ex: Set-State: Draft)
269 for line in body['body'].split('\n'):
270 res = command_re.match(line)
272 actions[res.group(1).lower()] = res.group(2).lower()
274 body_data += line+'\n'
275 body['body'] = body_data
278 'description': body['body'],
281 if 'state' in actions:
282 if actions['state'] in ['draft', 'close', 'cancel', 'open', 'pending']:
283 act = 'case_' + actions['state']
285 for k1, k2 in [('cost', 'planned_cost'), ('revenue', 'planned_revenue'), ('probability', 'probability')]:
287 data[k2] = float(actions[k1])
291 if 'priority' in actions:
292 if actions['priority'] in ('1', '2', '3', '4', '5'):
293 data['priority'] = actions['priority']
295 if 'partner' in actions:
296 data['email_from'] = actions['partner'][:128]
298 if 'user' in actions:
299 uids = self.rpc('res.users', 'name_search', actions['user'])
301 data['user_id'] = uids[0][0]
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'])
308 def msg_send(self, msg, emails, priority=None):
312 msg['To'] = emails[0]
316 msg['Cc'] = ','.join(emails[1:])
318 msg['Reply-To'] = self.email
320 msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
323 s.sendmail(self.email, emails, msg.as_string())
327 def msg_partner(self, msg, id):
328 message = self.msg_body_get(msg)
329 body = message['body']
331 self.rpc(self.model, act, [id])
332 body2 = '\n'.join(map(lambda l: '> '+l, (body or '').split('\n')))
336 self.rpc(self.model, 'write', [id], data)
337 self.rpc(self.model, 'history', [id], 'Send', True, msg['From'], message['body'])
340 def msg_test(self, msg, case_str):
342 return (False, False)
343 emails = self.rpc(self.model, 'emails_get', int(case_str))
344 return (int(case_str), emails)
346 def parse(self, msg):
347 case_str = reference_re.search(msg.get('References', ''))
349 case_str = case_str.group(1)
351 case_str = case_re.search(msg.get('Subject', ''))
353 case_str = case_str.group(1)
354 (case_id, emails) = self.msg_test(msg, case_str)
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)
359 self.msg_partner(msg, case_id)
361 case_id = self.msg_new(msg)
362 subject = self._decode_header(msg['subject'])
363 if msg.get('Subject', ''):
365 msg['Subject'] = '['+str(case_id)+'] '+subject
366 msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(case_id)+'@'+socket.gethostname()+'>'
368 emails = self.rpc(self.model, 'emails_get', case_id)
370 em = [emails[0], emails[1]] + (emails[2] or '').split(',')
371 emails = map(self.email_get, filter(None, em))
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))
376 emails = filter(lambda m: m and m not in msg_mails, emails)
378 self.msg_send(msg, emails, priority)
380 if self.email_default:
381 a = self._decode_header(msg['Subject'])
383 msg['Subject'] = '[OpenERP-CaseError] ' + a
384 self.msg_send(msg, self.email_default.split(','))
385 return case_id, emails
387 if __name__ == '__main__':
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")
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)
409 msg_txt = email.message_from_file(sys.stdin)
412 parser.parse(msg_txt)
414 if getattr(e, 'faultCode', '') and 'Connection unexpectedly closed' in e.faultCode:
417 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: