diff --git a/weboob/backends/cmb/__init__.py b/weboob/backends/cmb/__init__.py
new file mode 100644
index 00000000..e83e3198
--- /dev/null
+++ b/weboob/backends/cmb/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2011 Johann Broudin
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from .backend import CmbBackend
+
+__all__ = ['CmbBackend']
diff --git a/weboob/backends/cmb/backend.py b/weboob/backends/cmb/backend.py
new file mode 100644
index 00000000..8cb432ef
--- /dev/null
+++ b/weboob/backends/cmb/backend.py
@@ -0,0 +1,230 @@
+# -*- coding: utf-8 -*-
+
+# Copyright(C) 2011 Johann Broudin
+#
+# This file is part of weboob.
+#
+# weboob is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# weboob is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with weboob. If not, see .
+
+
+from weboob.capabilities.bank import ICapBank, AccountNotFound
+from weboob.capabilities.bank import Account, Recipient, Operation
+from weboob.tools.backend import BaseBackend, BackendConfig
+from weboob.tools.value import ValueBackendPassword
+from weboob.capabilities.base import NotAvailable
+from weboob.tools.browser import BrowserIncorrectPassword, BrokenPageError
+
+from re import match
+from httplib import HTTPSConnection
+from urllib import urlencode
+
+from lxml import etree
+from datetime import date
+from StringIO import StringIO
+
+__all__ = ['CmbBackend']
+
+class WrongLoginOrPassword(Exception):
+ def __init__(self):
+ return
+
+class CmbBackend(BaseBackend, ICapBank):
+ NAME = 'cmb'
+ MAINTAINER = 'Johann Broudin'
+ EMAIL = 'Johann.Broudin@6-8.fr'
+ VERSION = '0.2'
+ LICENSE = 'AGPLv3+'
+ DESCRIPTION = 'Credit Mutuel de Bretagne'
+ CONFIG = BackendConfig(
+ ValueBackendPassword('login', label='Account ID', masked=False),
+ ValueBackendPassword('password', label='Password', masked=True))
+ connected = False
+ headers = {
+ 'User-Agent':
+ 'Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OSX; en-us) ' +
+ 'AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405'
+ }
+
+ def login(self):
+ params = urlencode({
+ 'codeEspace': 'NO',
+ 'codeEFS': '01',
+ 'codeSi': '001',
+ 'noPersonne': self.config['login'].get(),
+ 'motDePasse': self.config['password'].get()
+ })
+ conn = HTTPSConnection("www.cmb.fr")
+ headers = {'Content-Type': 'application/x-www-form-urlencoded'}
+ conn.request("POST",
+ "/domiweb/servlet/Identification",
+ params,
+ headers)
+ response = conn.getresponse()
+ if response.status == 302:
+ self.cookie = response.getheader('Set-Cookie').split(';')[0]
+ self.cookie += ';'
+ self.connected = True
+ return True
+ else:
+ raise BrowserIncorrectPassword()
+ return false
+
+ def iter_accounts(self):
+
+ if not self.connected:
+ self.login()
+
+ def do_http():
+ conn = HTTPSConnection("www.cmb.fr")
+ headers = self.headers
+ headers['Cookie'] = self.cookie
+ conn.request("GET",
+ '/domiweb/prive/particulier/releve/0-releve.act',
+ {},
+ headers)
+ response = conn.getresponse()
+ data = response.read()
+ conn.close
+ return data
+
+ data = do_http()
+ parser = etree.HTMLParser()
+ tree = etree.parse(StringIO(data), parser)
+
+ table = tree.xpath('/html/body/table')
+ if len(table) == 0:
+ title = tree.xpath('/html/head/title')[0].text
+ if title == u"Utilisateur non identifié":
+ self.login()
+ data = do_http()
+
+ parser = etree.HTMLParser()
+ tree = etree.parse(StringIO(data), parser)
+ table = tree.xpath('/html/body/table')
+ if len(table) == 0:
+ raise BrokenPageError
+ else:
+ raise BrokenPageError
+
+ for tr in table[1].getiterator('tr'):
+ if tr.get('class') != 'LnTit' and tr.get('class') != 'LnTot':
+ account = Account()
+ td = tr.xpath('td')
+
+ a = td[0].xpath('a')
+ account.label = unicode(a[0].text).strip()
+ href = a[0].get('href')
+ m = match(r"javascript:releve\((.*),'(.*)','(.*)'\)",
+ href)
+ account.id = unicode(m.group(1) + m.group(2) + m.group(3))
+ account.cmbvaleur = m.group(1)
+ account.cmbvaleur2 = m.group(2)
+ account.cmbtype = m.group(3)
+
+
+ balance = td[1].text
+ balance = balance.replace(',','.').replace(u"\xa0",'')
+ account.balance = float(balance)
+
+ span = td[3].xpath('a/span')
+ if len(span):
+ coming = span[0].text.replace(' ','').replace(',','.')
+ coming = coming.replace(u"\xa0",'')
+ account.coming = float(coming)
+ else:
+ account.coming = NotAvailable
+
+ yield account
+
+ def get_account(self, _id):
+ for account in self.iter_accounts():
+ if account.id == _id:
+ return account
+
+ raise AccountNotFound()
+
+ def iter_history(self, account):
+ page = "/domiweb/prive/particulier/releve/"
+ if account.cmbtype == 'D':
+ page += "10-releve.act"
+ else:
+ page += "2-releve.act"
+ page +="?noPageReleve=1&indiceCompte="
+ page += account.cmbvaleur
+ page += "&typeCompte="
+ page += account.cmbvaleur2
+ page += "&deviseOrigineEcran=EUR"
+
+ def do_http():
+ conn = HTTPSConnection("www.cmb.fr")
+ headers = self.headers
+ headers['Cookie'] = self.cookie
+ conn.request("GET", page, {}, headers)
+ response = conn.getresponse()
+ data = response.read()
+ conn.close
+ return data
+
+ data = do_http()
+ parser = etree.HTMLParser()
+ tree = etree.parse(StringIO(data), parser)
+
+
+ tables = tree.xpath('/html/body/table')
+ if len(tables) == 0:
+ title = tree.xpath('/html/head/title')[0].text
+ if title == u"Utilisateur non identifié":
+ self.login()
+ data = do_http()
+
+ parser = etree.HTMLParser()
+ tree = etree.parse(StringIO(data), parser)
+ tables = tree.xpath('/html/body/table')
+ if len(tables) == 0:
+ raise BrokenPageError
+ else:
+ raise BrokenPageError
+
+ i = 0
+
+ for table in tables:
+ if table.get('id') != "tableMouvements":
+ continue
+ for tr in table.getiterator('tr'):
+ if (tr.get('class') != 'LnTit' and
+ tr.get('class') != 'LnTot'):
+ operation = Operation(i)
+ td = tr.xpath('td')
+
+ div = td[1].xpath('div')
+ d = div[0].text.split('/')
+ operation.date = date(*reversed([int(x) for x in d]))
+
+ div = td[2].xpath('div')
+ label = div[0].xpath('a')[0].text.replace('\n','')
+ operation.label = unicode(' '.join(label.split()))
+
+ amount = td[3].text
+ if amount.count(',') != 1:
+ amount = td[4].text
+ amount = amount.replace(',','.').replace(u'\xa0','')
+ print repr(amount)
+ operation.amount = float(amount)
+ else:
+ amount = amount.replace(',','.').replace(u'\xa0','')
+ operation.amount = - float(amount)
+
+ i += 1
+ yield operation
+