2 # -*- encoding: utf-8 -*-
3 ##############################################################################
5 # OpenERP, Open Source Management Solution
6 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
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.
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.
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/>.
22 ##############################################################################
26 import email, mimetypes
27 from email.Header import decode_header
28 from email.MIMEText import MIMEText
34 email_re = re.compile(r"""
35 ([a-zA-Z][\w\.-]*[a-zA-Z0-9] # username part
37 [a-zA-Z0-9][\w\.-]* # domain must start with a letter ... Ged> why do we include a 0-9 then?
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)
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))
61 def __call__(self, *request):
62 return self.rpc.execute(self.dbname, self.user_id, self.passwd, *request)
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)
68 self.section_id = int(section)
70 self.section_id = self.rpc('crm.case.section', 'search', [('code','=',section)])[0]
72 self.email_default = email_default
75 def email_get(self, email_from):
76 res = email_re.search(email_from)
77 return res and res.group(1)
79 def partner_get(self, email):
80 mail = self.email_get(email)
81 adr_ids = self.rpc('res.partner.address', 'search', [('email', '=', mail)])
84 adr = self.rpc('res.partner.address', 'read', adr_ids, ['partner_id'])
86 'partner_address_id': adr[0]['id'],
87 'partner_id': adr[0]['partner_id'][0]
90 def _decode_header(self, s):
91 from email.Header import decode_header
93 return ''.join(map(lambda x:x[0].decode(x[1] or 'ascii', 'replace'), s))
95 def msg_new(self, msg):
96 message = self.msg_body_get(msg)
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,
104 'description': message['body'],
105 'history_line': [(0, 0, {'description': message['body'], 'email': msg['From'] })],
108 data.update(self.partner_get(self._decode_header(msg['From'])))
111 netsvc.Logger().notifyChannel('mailgate', netsvc.LOG_ERROR, "%s" % e)
114 id = self.rpc('crm.case', 'create', data)
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.'
120 attachments = message['attachment']
122 for attach in attachments or []:
125 'datas':binascii.b2a_base64(str(attachments[attach])),
126 'datas_fname': str(attach),
127 'description': 'Mail attachment',
128 'res_model': 'crm.case',
131 self.rpc('ir.attachment', 'create', data_attach)
135 # #change the return type format to dictionary
137 # 'body':'body part',
139 # 'file_name':'file data',
140 # 'file_name':'file data',
141 # 'file_name':'file data',
145 def msg_body_get(self, msg):
147 message['body'] = u'';
148 message['attachment'] = {};
149 attachment = message['attachment'];
154 for part in msg.walk():
155 if part.get_content_maintype() == 'multipart':
158 if part.get_content_maintype()=='text' and part.get_content_subtype() in ('plain','html'):
159 buf = part.get_payload(decode=True)
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();
168 attachment[filename] = part.get_payload(decode=True);
170 filename = 'attach_file'+str(counter);
172 attachment[filename] = part.get_payload(decode=True);
175 message['attachment'] = attachment
180 def msg_user(self, msg, id):
181 body = self.msg_body_get(msg)
183 # handle email body commands (ex: Set-State: Draft)
186 for line in body['body'].split('\n'):
187 res = command_re.match(line)
189 actions[res.group(1).lower()] = res.group(2).lower()
191 body_data += line+'\n'
192 body['body'] = body_data
195 'description': body['body'],
196 'history_line': [(0, 0, {'description': body['body'], 'email': msg['From']})],
199 if 'state' in actions:
200 if actions['state'] in ['draft','close','cancel','open','pending']:
201 act = 'case_' + actions['state']
203 for k1,k2 in [('cost','planned_cost'),('revenue','planned_revenue'),('probability','probability')]:
205 data[k2] = float(actions[k1])
209 if 'priority' in actions:
210 if actions['priority'] in ('1','2','3','4','5'):
211 data['priority'] = actions['priority']
213 if 'partner' in actions:
214 data['email_from'] = actions['partner'][:128]
216 if 'user' in actions:
217 uids = self.rpc('res.users', 'name_search', actions['user'])
219 data['user_id'] = uids[0][0]
221 self.rpc('crm.case', act, [id])
222 self.rpc('crm.case', 'write', [id], data)
225 def msg_send(self, msg, emails, priority=None):
229 msg['To'] = emails[0]
233 msg['Cc'] = ','.join(emails[1:])
235 msg['Reply-To'] = self.email
237 msg['X-Priority'] = priorities.get(priority, '3 (Normal)')
240 s.sendmail(self.email, emails, msg.as_string())
244 def msg_partner(self, msg, id):
245 message = self.msg_body_get(msg)
246 body = message['body']
248 self.rpc('crm.case', act, [id])
249 body2 = '\n'.join(map(lambda l: '> '+l, (body or '').split('\n')))
252 'history_line': [(0, 0, {'description': body, 'email': msg['From'][:84]})],
254 self.rpc('crm.case', 'write', [id], data)
257 def msg_test(self, msg, case_str):
259 return (False, False)
260 emails = self.rpc('crm.case', 'emails_get', int(case_str))
261 return (int(case_str), emails)
263 def parse(self, msg):
264 case_str = reference_re.search(msg.get('References', ''))
266 case_str = case_str.group(1)
268 case_str = case_re.search(msg.get('Subject', ''))
270 case_str = case_str.group(1)
271 (case_id, emails) = self.msg_test(msg, case_str)
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)
276 self.msg_partner(msg, case_id)
278 case_id = self.msg_new(msg)
279 subject = self._decode_header(msg['subject'])
280 if msg.get('Subject', ''):
282 msg['Subject'] = '['+str(case_id)+'] '+subject
283 msg['Message-Id'] = '<'+str(time.time())+'-tinycrm-'+str(case_id)+'@'+socket.gethostname()+'>'
285 emails = self.rpc('crm.case', 'emails_get', case_id)
287 em = [emails[0], emails[1]] + (emails[2] or '').split(',')
288 emails = map(self.email_get, filter(None, em))
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))
293 emails = filter(lambda m: m and m not in msg_mails, emails)
295 self.msg_send(msg, emails, priority)
297 if self.email_default:
298 a = self._decode_header(msg['Subject'])
300 msg['Subject'] = '[OpenERP-CaseError] ' + a
301 self.msg_send(msg, self.email_default.split(','))
304 if __name__ == '__main__':
306 parser = optparse.OptionParser(
307 usage='usage: %prog [options]',
308 version='%prog v1.0')
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)
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")
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)
327 msg_txt = email.message_from_file(sys.stdin)
330 parser.parse(msg_txt)
332 if getattr(e,'faultCode','') and 'Connection unexpectedly closed' in e.faultCode:
335 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: