Repository: Bitmessage/PyBitmessage

Issue:
API not HTTP conform
# Issue

The API does not responds correctly to failed login attempts.
# Current result

In case invalid or no credentials are provided, the API returns an XML with an error message. While this is not incorrect, the HTTP headers themselves are.
# Expected result

The API should return with a 401 Header in case no login credentials are provided and with a 401 or 403 code if invalid credentials are provided.
401: Authentication required (specify username and password please)
403: Forbidden (do not try again with this user. It is invalid)



Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -11,12 +11,14 @@
 
 EXTRAS_REQUIRE = {
     'gir': ['pygobject'],
+    'json': ['jsonrpclib'],
     'notify2': ['notify2'],
     'opencl': ['pyopencl', 'numpy'],
     'prctl': ['python_prctl'],  # Named threads
     'qrcode': ['qrcode'],
     'sound;platform_system=="Windows"': ['winsound'],
     'tor': ['stem'],
+    'xml': ['defusedxml'],
     'docs': ['sphinx', 'sphinxcontrib-apidoc', 'm2r']
 }
 
diff --git a/src/api.py b/src/api.py
--- a/src/api.py
+++ b/src/api.py
@@ -1,19 +1,73 @@
-"""
-This is not what you run to run the Bitmessage API. Instead, enable the API
-( https://bitmessage.org/wiki/API ) and optionally enable daemon mode
-( https://bitmessage.org/wiki/Daemon ) then run bitmessagemain.py.
-"""
 # Copyright (c) 2012-2016 Jonathan Warren
 # Copyright (c) 2012-2020 The Bitmessage developers
 # pylint: disable=too-many-lines,no-self-use,unused-variable,unused-argument
+
+"""
+This is not what you run to start the Bitmessage API.
+Instead, `enable the API <https://bitmessage.org/wiki/API>`_
+and optionally `enable daemon mode <https://bitmessage.org/wiki/Daemon>`_
+then run the PyBitmessage.
+
+The PyBitmessage API is provided either as
+`XML-RPC <http://xmlrpc.scripting.com/spec.html>`_ or
+`JSON-RPC <https://www.jsonrpc.org/specification>`_ like in bitcoin.
+It's selected according to 'apivariant' setting in config file.
+
+Special value ``apivariant=legacy`` is to mimic the old pre 0.6.3
+behaviour when any results are returned as strings of json.
+
+.. list-table:: All config settings related to API:
+  :header-rows: 0
+
+  * - apienabled = true
+    - if 'false' the `singleAPI` wont start
+  * - apiinterface = 127.0.0.1
+    - this is the recommended default
+  * - apiport = 8442
+    - the API listens apiinterface:apiport if apiport is not used,
+      random in range (32767, 65535) otherwice
+  * - apivariant = xml
+    - current default for backward compatibility, 'json' is recommended
+  * - apiusername = username
+    - set the username
+  * - apipassword = password
+    - and the password
+  * - apinotifypath =
+    - not really the API setting, this sets a path for the executable to be ran
+      when certain internal event happens
+
+To use the API concider such simple example:
+
+.. code-block:: python
+
+    import jsonrpclib
+
+    from pybitmessage import bmconfigparser, helper_startup
+
+    helper_startup.loadConfig()  # find and load local config file
+    conf = bmconfigparser.BMConfigParser()
+    api_uri = "http://%s:%s@127.0.0.1:8442/" % (
+        conf.safeGet('bitmessagesettings', 'apiusername'),
+        conf.safeGet('bitmessagesettings', 'apipassword')
+    )
+    api = jsonrpclib.ServerProxy(api_uri)
+    print(api.clientStatus())
+
+
+For further examples please reference `.tests.test_api`.
+"""
+
 import base64
+import ConfigParser
 import errno
 import hashlib
+import httplib
 import json
 import random  # nosec
 import socket
 import subprocess
 import time
+import xmlrpclib
 from binascii import hexlify, unhexlify
 from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
 from struct import pack
@@ -27,7 +81,6 @@
 import shared
 import shutdown
 import state
-import threads
 from addresses import (
     addBMIfNotPresent,
     calculateInventoryHash,
@@ -43,30 +96,88 @@
 from network.threads import StoppableThread
 from version import softwareVersion
 
-str_chan = '[chan]'
+try:  # TODO: write tests for XML vulnerabilities
+    from defusedxml.xmlrpc import monkey_patch
+except ImportError:
+    logger.warning(
+        'defusedxml not available, only use API on a secure, closed network.')
+else:
+    monkey_patch()
 
 
-class APIError(Exception):
-    """APIError exception class"""
+str_chan = '[chan]'
+str_broadcast_subscribers = '[Broadcast subscribers]'
+
+
+class ErrorCodes(type):
+    """Metaclass for :class:`APIError` documenting error codes."""
+    _CODES = {
+        0: 'Invalid command parameters number',
+        1: 'The specified passphrase is blank.',
+        2: 'The address version number currently must be 3, 4, or 0'
+        ' (which means auto-select).',
+        3: 'The stream number must be 1 (or 0 which means'
+        ' auto-select). Others aren\'t supported.',
+        4: 'Why would you ask me to generate 0 addresses for you?',
+        5: 'You have (accidentally?) specified too many addresses to'
+        ' make. Maximum 999. This check only exists to prevent'
+        ' mischief; if you really want to create more addresses than'
+        ' this, contact the Bitmessage developers and we can modify'
+        ' the check or you can do it yourself by searching the source'
+        ' code for this message.',
+        6: 'The encoding type must be 2 or 3.',
+        7: 'Could not decode address',
+        8: 'Checksum failed for address',
+        9: 'Invalid characters in address',
+        10: 'Address version number too high (or zero)',
+        11: 'The address version number currently must be 2, 3 or 4.'
+        ' Others aren\'t supported. Check the address.',
+        12: 'The stream number must be 1. Others aren\'t supported.'
+        ' Check the address.',
+        13: 'Could not find this address in your keys.dat file.',
+        14: 'Your fromAddress is disabled. Cannot send.',
+        15: 'Invalid ackData object size.',
+        16: 'You are already subscribed to that address.',
+        17: 'Label is not valid UTF-8 data.',
+        18: 'Chan name does not match address.',
+        19: 'The length of hash should be 32 bytes (encoded in hex'
+        ' thus 64 characters).',
+        20: 'Invalid method:',
+        21: 'Unexpected API Failure',
+        22: 'Decode error',
+        23: 'Bool expected in eighteenByteRipe',
+        24: 'Chan address is already present.',
+        25: 'Specified address is not a chan address.'
+        ' Use deleteAddress API call instead.',
+        26: 'Malformed varint in address: ',
+        27: 'Message is too long.'
+    }
+
+    def __new__(mcs, name, bases, namespace):
+        result = super(ErrorCodes, mcs).__new__(mcs, name, bases, namespace)
+        for code in mcs._CODES.iteritems():
+            # beware: the formatting is adjusted for list-table
+            result.__doc__ += """   * - %04i
+         - %s
+    """ % code
+        return result
 
-    def __init__(self, error_number, error_message):
-        super(APIError, self).__init__()
-        self.error_number = error_number
-        self.error_message = error_message
 
-    def __str__(self):
-        return "API Error %04i: %s" % (self.error_number, self.error_message)
+class APIError(xmlrpclib.Fault):
+    """
+    APIError exception class
 
+    .. list-table:: Possible error values
+       :header-rows: 1
+       :widths: auto
 
-class StoppableXMLRPCServer(SimpleXMLRPCServer):
-    """A SimpleXMLRPCServer that honours state.shutdown"""
-    allow_reuse_address = True
+       * - Error Number
+         - Message
+    """
+    __metaclass__ = ErrorCodes
 
-    def serve_forever(self):
-        """Start the SimpleXMLRPCServer"""
-        # pylint: disable=arguments-differ
-        while state.shutdown == 0:
-            self.handle_request()
+    def __str__(self):
+        return "API Error %04i: %s" % (self.faultCode, self.faultString)
 
 
 # This thread, of which there is only one, runs the API.
@@ -89,22 +200,52 @@ def stopThread(self):
             pass
 
     def run(self):
+        """
+        The instance of `SimpleXMLRPCServer.SimpleXMLRPCServer` or
+        :class:`jsonrpclib.SimpleJSONRPCServer` is created and started here
+        with `BMRPCDispatcher` dispatcher.
+        """
         port = BMConfigParser().getint('bitmessagesettings', 'apiport')
         try:
             getattr(errno, 'WSAEADDRINUSE')
         except AttributeError:
             errno.WSAEADDRINUSE = errno.EADDRINUSE
+
+        RPCServerBase = SimpleXMLRPCServer
+        ct = 'text/xml'
+        if BMConfigParser().safeGet(
+                'bitmessagesettings', 'apivariant') == 'json':
+            try:
+                from jsonrpclib.SimpleJSONRPCServer import (
+                    SimpleJSONRPCServer as RPCServerBase)
+            except ImportError:
+                logger.warning(
+                    'jsonrpclib not available, failing back to XML-RPC')
+            else:
+                ct = 'application/json-rpc'
+
+        # Nested class. FIXME not found a better solution.
+        class StoppableRPCServer(RPCServerBase):
+            """A SimpleXMLRPCServer that honours state.shutdown"""
+            allow_reuse_address = True
+            content_type = ct
+
+            def serve_forever(self, poll_interval=None):
+                """Start the RPCServer"""
+                while state.shutdown == 0:
+                    self.handle_request()
+
         for attempt in range(50):
             try:
                 if attempt > 0:
                     logger.warning(
                         'Failed to start API listener on port %s', port)
                     port = random.randint(32767, 65535)
-                se = StoppableXMLRPCServer(
+                se = StoppableRPCServer(
                     (BMConfigParser().get(
                         'bitmessagesettings', 'apiinterface'),
                      port),
-                    MySimpleXMLRPCRequestHandler, True, True)
+                    BMXMLRPCRequestHandler, True, encoding='UTF-8')
             except socket.error as e:
                 if e.errno in (errno.EADDRINUSE, errno.WSAEADDRINUSE):
                     continue
@@ -115,6 +256,8 @@ def run(self):
                         'bitmessagesettings', 'apiport', str(port))
                     BMConfigParser().save()
                 break
+
+        se.register_instance(BMRPCDispatcher())
         se.register_introspection_functions()
 
         apiNotifyPath = BMConfigParser().safeGet(
@@ -134,16 +277,69 @@ def run(self):
         se.serve_forever()
 
 
-class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
+class CommandHandler(type):
     """
-    This is one of several classes that constitute the API
-
-    This class was written by Vaibhav Bhatia.
-    Modified by Jonathan Warren (Atheros).
-    http://code.activestate.com/recipes/501148-xmlrpc-serverclient-which-does-cookie-handling-and/
+    The metaclass for `BMRPCDispatcher` which fills _handlers dict by
+    methods decorated with @command
     """
-    # pylint: disable=too-many-public-methods
+    def __new__(mcs, name, bases, namespace):
+        # pylint: disable=protected-access
+        result = super(CommandHandler, mcs).__new__(
+            mcs, name, bases, namespace)
+        result.config = BMConfigParser()
+        result._handlers = {}
+        apivariant = result.config.safeGet('bitmessagesettings', 'apivariant')
+        for func in namespace.values():
+            try:
+                for alias in getattr(func, '_cmd'):
+                    try:
+                        prefix, alias = alias.split(':')
+                        if apivariant != prefix:
+                            continue
+                    except ValueError:
+                        pass
+                    result._handlers[alias] = func
+            except AttributeError:
+                pass
+        return result
+
 
+class command(object):  # pylint: disable=too-few-public-methods
+    """Decorator for API command method"""
+    def __init__(self, *aliases):
+        self.aliases = aliases
+
+    def __call__(self, func):
+        if BMConfigParser().safeGet(
+                'bitmessagesettings', 'apivariant') == 'legacy':
+            def wrapper(*args):
+                """
+                A wrapper for legacy apivariant which dumps the result
+                into string of json
+                """
+                result = func(*args)
+                return result if isinstance(result, (int, str)) \
+                    else json.dumps(result, indent=4)
+            wrapper.__doc__ = func.__doc__
+        else:
+            wrapper = func
+        # pylint: disable=protected-access
+        wrapper._cmd = self.aliases
+        wrapper.__doc__ = """Commands: *%s*
+
+        """ % ', '.join(self.aliases) + wrapper.__doc__.lstrip()
+        return wrapper
+
+
+# This is one of several classes that constitute the API
+# This class was written by Vaibhav Bhatia.
+# Modified by Jonathan Warren (Atheros).
+# Further modified by the Bitmessage developers
+# http://code.activestate.com/recipes/501148
+class BMXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
+    """The main API handler"""
+
+    # pylint: disable=protected-access
     def do_POST(self):
         """
         Handles the HTTP POST request.
@@ -151,8 +347,9 @@ def do_POST(self):
         Attempts to interpret all HTTP POST requests as XML-RPC calls,
         which are forwarded to the server's _dispatch method for handling.
 
-        Note: this method is the same as in SimpleXMLRPCRequestHandler,
-        just hacked to handle cookies
+        .. note:: this method is the same as in
+          `SimpleXMLRPCServer.SimpleXMLRPCRequestHandler`,
+          just hacked to handle cookies
         """
 
         # Check that the path is legal
@@ -174,23 +371,35 @@ def do_POST(self):
                 size_remaining -= len(L[-1])
             data = ''.join(L)
 
-            # In previous versions of SimpleXMLRPCServer, _dispatch
-            # could be overridden in this class, instead of in
-            # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
-            # check to see if a subclass implements _dispatch and dispatch
-            # using that method if present.
-            # pylint: disable=protected-access
-            response = self.server._marshaled_dispatch(
-                data, getattr(self, '_dispatch', None)
-            )
-        except BaseException:  # This should only happen if the module is buggy
+            # pylint: disable=attribute-defined-outside-init
+            self.cookies = []
+
+            validuser = self.APIAuthenticateClient()
+            if not validuser:
+                time.sleep(2)
+                self.send_response(httplib.UNAUTHORIZED)
+                self.end_headers()
+                return
+                # "RPC Username or password incorrect or HTTP header"
+                # " lacks authentication at all."
+            else:
+                # In previous versions of SimpleXMLRPCServer, _dispatch
+                # could be overridden in this class, instead of in
+                # SimpleXMLRPCDispatcher. To maintain backwards compatibility,
+                # check to see if a subclass implements _dispatch and dispatch
+                # using that method if present.
+
+                response = self.server._marshaled_dispatch(
+                    data, getattr(self, '_dispatch', None)
+                )
+        except Exception:  # This should only happen if the module is buggy
             # internal error, report as HTTP server error
-            self.send_response(500)
+            self.send_response(httplib.INTERNAL_SERVER_ERROR)
             self.end_headers()
         else:
             # got a valid XML RPC response
-            self.send_response(200)
-            self.send_header("Content-type", "text/xml")
+            self.send_response(httplib.OK)
+            self.send_header("Content-type", self.server.content_type)
             self.send_header("Content-length", str(len(response)))
 
             # HACK :start -> sends cookies here
@@ -211,18 +420,19 @@ def do_POST(self):
                 shutdown.doCleanShutdown()
 
     def APIAuthenticateClient(self):
-        """Predicate to check for valid API credentials in the request header"""
+        """
+        Predicate to check for valid API credentials in the request header
+        """
 
         if 'Authorization' in self.headers:
             # handle Basic authentication
-            _, encstr = self.headers.get('Authorization').split()
+            encstr = self.headers.get('Authorization').split()[1]
             emailid, password = encstr.decode('base64').split(':')
             return (
                 emailid == BMConfigParser().get(
-                    'bitmessagesettings', 'apiusername') and
-                password == BMConfigParser().get(
-                    'bitmessagesettings', 'apipassword')
-            )
+                    'bitmessagesettings', 'apiusername'
+                ) and password == BMConfigParser().get(
+                    'bitmessagesettings', 'apipassword'))
         else:
             logger.warning(
                 'Authentication failed because header lacks'
@@ -231,7 +441,14 @@ def APIAuthenticateClient(self):
 
         return False
 
-    def _decode(self, text, decode_type):
+
+# pylint: disable=no-self-use,no-member,too-many-public-methods
+class BMRPCDispatcher(object):
+    """This class is used to dispatch API commands"""
+    __metaclass__ = CommandHandler
+
+    @staticmethod
+    def _decode(text, decode_type):
         try:
             if decode_type == 'hex':
                 return unhexlify(text)
@@ -239,29 +456,22 @@ def _decode(self, text, decode_type):
                 return base64.b64decode(text)
         except Exception as e:
             raise APIError(
-                22, "Decode error - %s. Had trouble while decoding string: %r"
+                22, 'Decode error - %s. Had trouble while decoding string: %r'
                 % (e, text)
             )
-        return None
 
     def _verifyAddress(self, address):
         status, addressVersionNumber, streamNumber, ripe = \
             decodeAddress(address)
         if status != 'success':
-            logger.warning(
-                'API Error 0007: Could not decode address %s. Status: %s.',
-                address, status
-            )
-
             if status == 'checksumfailed':
                 raise APIError(8, 'Checksum failed for address: ' + address)
             if status == 'invalidcharacters':
                 raise APIError(9, 'Invalid characters in address: ' + address)
             if status == 'versiontoohigh':
                 raise APIError(
-                    10,
-                    'Address version number too high (or zero) in address: ' +
-                    address)
+                    10, 'Address version number too high (or zero) in address: '
+                    + address)
             if status == 'varintmalformed':
                 raise APIError(26, 'Malformed varint in address: ' + address)
             raise APIError(
@@ -277,70 +487,108 @@ def _verifyAddress(self, address):
                 ' Check the address.'
             )
 
-        return (status, addressVersionNumber, streamNumber, ripe)
+        return {
+            'status': status,
+            'addressVersion': addressVersionNumber,
+            'streamNumber': streamNumber,
+            'ripe': base64.b64encode(ripe)
+        } if self._method == 'decodeAddress' else (
+            status, addressVersionNumber, streamNumber, ripe)
+
+    @staticmethod
+    def _dump_inbox_message(  # pylint: disable=too-many-arguments
+            msgid, toAddress, fromAddress, subject, received,
+            message, encodingtype, read):
+        subject = shared.fixPotentiallyInvalidUTF8Data(subject)
+        message = shared.fixPotentiallyInvalidUTF8Data(message)
+        return {
+            'msgid': hexlify(msgid),
+            'toAddress': toAddress,
+            'fromAddress': fromAddress,
+            'subject': base64.b64encode(subject),
+            'message': base64.b64encode(message),
+            'encodingType': encodingtype,
+            'receivedTime': received,
+            'read': read
+        }
+
+    @staticmethod
+    def _dump_sent_message(  # pylint: disable=too-many-arguments
+            msgid, toAddress, fromAddress, subject, lastactiontime,
+            message, encodingtype, status, ackdata):
+        subject = shared.fixPotentiallyInvalidUTF8Data(subject)
+        message = shared.fixPotentiallyInvalidUTF8Data(message)
+        return {
+            'msgid': hexlify(msgid),
+            'toAddress': toAddress,
+            'fromAddress': fromAddress,
+            'subject': base64.b64encode(subject),
+            'message': base64.b64encode(message),
+            'encodingType': encodingtype,
+            'lastActionTime': lastactiontime,
+            'status': status,
+            'ackData': hexlify(ackdata)
+        }
 
     # Request Handlers
 
-    def HandleListAddresses(self, method):
-        """Handle a request to list addresses"""
-        data = '{"addresses":['
-        for addressInKeysFile in BMConfigParser().addresses():
-            status, addressVersionNumber, streamNumber, hash01 = decodeAddress(
-                addressInKeysFile)
-            if len(data) > 20:
-                data += ','
-            if BMConfigParser().has_option(addressInKeysFile, 'chan'):
-                chan = BMConfigParser().getboolean(addressInKeysFile, 'chan')
-            else:
-                chan = False
-            label = BMConfigParser().get(addressInKeysFile, 'label')
-            if method == 'listAddresses2':
+    @command('decodeAddress')
+    def HandleDecodeAddress(self, address):
+        """
+        Decode given address and return dict with
+        status, addressVersion, streamNumber and ripe keys
+        """
+        return self._verifyAddress(address)
+
+    @command('listAddresses', 'listAddresses2')
+    def HandleListAddresses(self):
+        """
+        Returns dict with a list of all used addresses with their properties
+        in the *addresses* key.
+        """
+        data = []
+        for address in self.config.addresses():
+            streamNumber = decodeAddress(address)[2]
+            label = self.config.get(address, 'label')
+            if self._method == 'listAddresses2':
                 label = base64.b64encode(label)
-            data += json.dumps({
+            data.append({
                 'label': label,
-                'address': addressInKeysFile,
+                'address': address,
                 'stream': streamNumber,
-                'enabled':
-                BMConfigParser().getboolean(addressInKeysFile, 'enabled'),
-                'chan': chan
-            }, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleListAddressBookEntries(self, params):
-        """Handle a request to list address book entries"""
-
-        if len(params) == 1:
-            label, = params
-            label = self._decode(label, "base64")
-            queryreturn = sqlQuery(
-                "SELECT label, address from addressbook WHERE label = ?",
-                label)
-        elif len(params) > 1:
-            raise APIError(0, "Too many paremeters, max 1")
-        else:
-            queryreturn = sqlQuery("SELECT label, address from addressbook")
-        data = '{"addresses":['
-        for row in queryreturn:
-            label, address = row
+                'enabled': self.config.safeGetBoolean(address, 'enabled'),
+                'chan': self.config.safeGetBoolean(address, 'chan')
+            })
+        return {'addresses': data}
+
+    # the listAddressbook alias should be removed eventually.
+    @command('listAddressBookEntries', 'legacy:listAddressbook')
+    def HandleListAddressBookEntries(self, label=None):
+        """
+        Returns dict with a list of all address book entries (address and label)
+        in the *addresses* key.
+        """
+        queryreturn = sqlQuery(
+            "SELECT label, address from addressbook WHERE label = ?",
+            label
+        ) if label else sqlQuery("SELECT label, address from addressbook")
+        data = []
+        for label, address in queryreturn:
             label = shared.fixPotentiallyInvalidUTF8Data(label)
-            if len(data) > 20:
-                data += ','
-            data += json.dumps({
+            data.append({
                 'label': base64.b64encode(label),
-                'address': address}, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleAddAddressBookEntry(self, params):
-        """Handle a request to add an address book entry"""
+                'address': address
+            })
+        return {'addresses': data}
 
-        if len(params) != 2:
-            raise APIError(0, "I need label and address")
-        address, label = params
+    # the addAddressbook alias should be deleted eventually.
+    @command('addAddressBookEntry', 'legacy:addAddressbook')
+    def HandleAddAddressBookEntry(self, address, label):
+        """Add an entry to address book. label must be base64 encoded."""
         label = self._decode(label, "base64")
         address = addBMIfNotPresent(address)
         self._verifyAddress(address)
+        # TODO: add unique together constraint in the table
         queryreturn = sqlQuery(
             "SELECT address FROM addressbook WHERE address=?", address)
         if queryreturn != []:
@@ -353,12 +601,10 @@ def HandleAddAddressBookEntry(self, params):
         queues.UISignalQueue.put(('rerenderAddressBook', ''))
         return "Added address %s to address book" % address
 
-    def HandleDeleteAddressBookEntry(self, params):
-        """Handle a request to delete an address book entry"""
-
-        if len(params) != 1:
-            raise APIError(0, "I need an address")
-        address, = params
+    # the deleteAddressbook alias should be deleted eventually.
+    @command('deleteAddressBookEntry', 'legacy:deleteAddressbook')
+    def HandleDeleteAddressBookEntry(self, address):
+        """Delete an entry from address book."""
         address = addBMIfNotPresent(address)
         self._verifyAddress(address)
         sqlExecute('DELETE FROM addressbook WHERE address=?', address)
@@ -367,49 +613,42 @@ def HandleDeleteAddressBookEntry(self, params):
         queues.UISignalQueue.put(('rerenderAddressBook', ''))
         return "Deleted address book entry for %s if it existed" % address
 
-    def HandleCreateRandomAddress(self, params):
-        """Handle a request to create a random address"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-
-        elif len(params) == 1:
-            label, = params
-            eighteenByteRipe = False
-            nonceTrialsPerByte = BMConfigParser().get(
-                'bitmessagesettings', 'defaultnoncetrialsperbyte')
-            payloadLengthExtraBytes = BMConfigParser().get(
-                'bitmessagesettings', 'defaultpayloadlengthextrabytes')
-        elif len(params) == 2:
-            label, eighteenByteRipe = params
-            nonceTrialsPerByte = BMConfigParser().get(
-                'bitmessagesettings', 'defaultnoncetrialsperbyte')
-            payloadLengthExtraBytes = BMConfigParser().get(
-                'bitmessagesettings', 'defaultpayloadlengthextrabytes')
-        elif len(params) == 3:
-            label, eighteenByteRipe, totalDifficulty = params
-            nonceTrialsPerByte = int(
-                defaults.networkDefaultProofOfWorkNonceTrialsPerByte *
-                totalDifficulty)
-            payloadLengthExtraBytes = BMConfigParser().get(
-                'bitmessagesettings', 'defaultpayloadlengthextrabytes')
-        elif len(params) == 4:
-            label, eighteenByteRipe, totalDifficulty, \
-                smallMessageDifficulty = params
-            nonceTrialsPerByte = int(
-                defaults.networkDefaultProofOfWorkNonceTrialsPerByte *
-                totalDifficulty)
-            payloadLengthExtraBytes = int(
-                defaults.networkDefaultPayloadLengthExtraBytes *
-                smallMessageDifficulty)
-        else:
-            raise APIError(0, 'Too many parameters!')
+    @command('createRandomAddress')
+    def HandleCreateRandomAddress(
+        self, label, eighteenByteRipe=False, totalDifficulty=0,
+        smallMessageDifficulty=0
+    ):
+        """
+        Create one address using the random number generator.
+
+        :param str label: base64 encoded label for the address
+        :param bool eighteenByteRipe: is telling Bitmessage whether to
+          generate an address with an 18 byte RIPE hash
+          (as opposed to a 19 byte hash).
+        """
+
+        nonceTrialsPerByte = self.config.get(
+            'bitmessagesettings', 'defaultnoncetrialsperbyte'
+        ) if not totalDifficulty else int(
+            defaults.networkDefaultProofOfWorkNonceTrialsPerByte
+            * totalDifficulty)
+        payloadLengthExtraBytes = self.config.get(
+            'bitmessagesettings', 'defaultpayloadlengthextrabytes'
+        ) if not smallMessageDifficulty else int(
+            defaults.networkDefaultPayloadLengthExtraBytes
+            * smallMessageDifficulty)
+
+        if not isinstance(eighteenByteRipe, bool):
+            raise APIError(
+                23, 'Bool expected in eighteenByteRipe, saw %s instead'
+                % type(eighteenByteRipe))
         label = self._decode(label, "base64")
         try:
             unicode(label, 'utf-8')
-        except BaseException:
+        except UnicodeDecodeError:
             raise APIError(17, 'Label is not valid UTF-8 data.')
         queues.apiAddressGeneratorReturnQueue.queue.clear()
+        # FIXME hard coded stream no
         streamNumberForAddress = 1
         queues.addressGeneratorQueue.put((
             'createRandomAddress', 4, streamNumberForAddress, label, 1, "",
@@ -417,98 +656,53 @@ def HandleCreateRandomAddress(self, params):
         ))
         return queues.apiAddressGeneratorReturnQueue.get()
 
-    def HandleCreateDeterministicAddresses(self, params):
-        """Handle a request to create a deterministic address"""
-        # pylint: disable=too-many-branches, too-many-statements
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-
-        elif len(params) == 1:
-            passphrase, = params
-            numberOfAddresses = 1
-            addressVersionNumber = 0
-            streamNumber = 0
-            eighteenByteRipe = False
-            nonceTrialsPerByte = BMConfigParser().get(
-                'bitmessagesettings', 'defaultnoncetrialsperbyte')
-            payloadLengthExtraBytes = BMConfigParser().get(
-                'bitmessagesettings', 'defaultpayloadlengthextrabytes')
-
-        elif len(params) == 2:
-            passphrase, numberOfAddresses = params
-            addressVersionNumber = 0
-            streamNumber = 0
-            eighteenByteRipe = False
-            nonceTrialsPerByte = BMConfigParser().get(
-                'bitmessagesettings', 'defaultnoncetrialsperbyte')
-            payloadLengthExtraBytes = BMConfigParser().get(
-                'bitmessagesettings', 'defaultpayloadlengthextrabytes')
-
-        elif len(params) == 3:
-            passphrase, numberOfAddresses, addressVersionNumber = params
-            streamNumber = 0
-            eighteenByteRipe = False
-            nonceTrialsPerByte = BMConfigParser().get(
-                'bitmessagesettings', 'defaultnoncetrialsperbyte')
-            payloadLengthExtraBytes = BMConfigParser().get(
-                'bitmessagesettings', 'defaultpayloadlengthextrabytes')
-
-        elif len(params) == 4:
-            passphrase, numberOfAddresses, addressVersionNumber, \
-                streamNumber = params
-            eighteenByteRipe = False
-            nonceTrialsPerByte = BMConfigParser().get(
-                'bitmessagesettings', 'defaultnoncetrialsperbyte')
-            payloadLengthExtraBytes = BMConfigParser().get(
-                'bitmessagesettings', 'defaultpayloadlengthextrabytes')
-
-        elif len(params) == 5:
-            passphrase, numberOfAddresses, addressVersionNumber, \
-                streamNumber, eighteenByteRipe = params
-            nonceTrialsPerByte = BMConfigParser().get(
-                'bitmessagesettings', 'defaultnoncetrialsperbyte')
-            payloadLengthExtraBytes = BMConfigParser().get(
-                'bitmessagesettings', 'defaultpayloadlengthextrabytes')
-
-        elif len(params) == 6:
-            passphrase, numberOfAddresses, addressVersionNumber, \
-                streamNumber, eighteenByteRipe, totalDifficulty = params
-            nonceTrialsPerByte = int(
-                defaults.networkDefaultProofOfWorkNonceTrialsPerByte *
-                totalDifficulty)
-            payloadLengthExtraBytes = BMConfigParser().get(
-                'bitmessagesettings', 'defaultpayloadlengthextrabytes')
-
-        elif len(params) == 7:
-            passphrase, numberOfAddresses, addressVersionNumber, \
-                streamNumber, eighteenByteRipe, totalDifficulty, \
-                smallMessageDifficulty = params
-            nonceTrialsPerByte = int(
-                defaults.networkDefaultProofOfWorkNonceTrialsPerByte *
-                totalDifficulty)
-            payloadLengthExtraBytes = int(
-                defaults.networkDefaultPayloadLengthExtraBytes *
-                smallMessageDifficulty)
-        else:
-            raise APIError(0, 'Too many parameters!')
+    # pylint: disable=too-many-arguments
+    @command('createDeterministicAddresses')
+    def HandleCreateDeterministicAddresses(
+        self, passphrase, numberOfAddresses=1, addressVersionNumber=0,
+        streamNumber=0, eighteenByteRipe=False, totalDifficulty=0,
+        smallMessageDifficulty=0
+    ):
+        """
+        Create many addresses deterministically using the passphrase.
+
+        :param str passphrase: base64 encoded passphrase
+        :param int numberOfAddresses: number of addresses to create,
+          up to 999
+
+        *addressVersionNumber* and *streamNumber* may be set to 0
+        which will tell Bitmessage to use the most up-to-date
+        address version and the most available stream.
+        """
+
+        nonceTrialsPerByte = self.config.get(
+            'bitmessagesettings', 'defaultnoncetrialsperbyte'
+        ) if not totalDifficulty else int(
+            defaults.networkDefaultProofOfWorkNonceTrialsPerByte
+            * totalDifficulty)
+        payloadLengthExtraBytes = self.config.get(
+            'bitmessagesettings', 'defaultpayloadlengthextrabytes'
+        ) if not smallMessageDifficulty else int(
+            defaults.networkDefaultPayloadLengthExtraBytes
+            * smallMessageDifficulty)
+
         if not passphrase:
             raise APIError(1, 'The specified passphrase is blank.')
         if not isinstance(eighteenByteRipe, bool):
             raise APIError(
-                23, 'Bool expected in eighteenByteRipe, saw %s instead' %
-                type(eighteenByteRipe))
+                23, 'Bool expected in eighteenByteRipe, saw %s instead'
+                % type(eighteenByteRipe))
         passphrase = self._decode(passphrase, "base64")
         # 0 means "just use the proper addressVersionNumber"
         if addressVersionNumber == 0:
             addressVersionNumber = 4
-        if addressVersionNumber != 3 and addressVersionNumber != 4:
+        if addressVersionNumber not in (3, 4):
             raise APIError(
                 2, 'The address version number currently must be 3, 4, or 0'
-                ' (which means auto-select). %i isn\'t supported.' %
-                addressVersionNumber)
+                ' (which means auto-select). %i isn\'t supported.'
+                % addressVersionNumber)
         if streamNumber == 0:  # 0 means "just use the most available stream"
-            streamNumber = 1
+            streamNumber = 1  # FIXME hard coded stream no
         if streamNumber != 1:
             raise APIError(
                 3, 'The stream number must be 1 (or 0 which means'
@@ -533,27 +727,24 @@ def HandleCreateDeterministicAddresses(self, params):
             'unused API address', numberOfAddresses, passphrase,
             eighteenByteRipe, nonceTrialsPerByte, payloadLengthExtraBytes
         ))
-        data = '{"addresses":['
-        queueReturn = queues.apiAddressGeneratorReturnQueue.get()
-        for item in queueReturn:
-            if len(data) > 20:
-                data += ','
-            data += "\"" + item + "\""
-        data += ']}'
-        return data
-
-    def HandleGetDeterministicAddress(self, params):
-        """Handle a request to get a deterministic address"""
-
-        if len(params) != 3:
-            raise APIError(0, 'I need exactly 3 parameters.')
-        passphrase, addressVersionNumber, streamNumber = params
+
+        return {'addresses': queues.apiAddressGeneratorReturnQueue.get()}
+
+    @command('getDeterministicAddress')
+    def HandleGetDeterministicAddress(
+            self, passphrase, addressVersionNumber, streamNumber):
+        """
+        Similar to *createDeterministicAddresses* except that the one
+        address that is returned will not be added to the Bitmessage
+        user interface or the keys.dat file.
+        """
+
         numberOfAddresses = 1
         eighteenByteRipe = False
         if not passphrase:
             raise APIError(1, 'The specified passphrase is blank.')
         passphrase = self._decode(passphrase, "base64")
-        if addressVersionNumber != 3 and addressVersionNumber != 4:
+        if addressVersionNumber not in (3, 4):
             raise APIError(
                 2, 'The address version number currently must be 3 or 4. %i'
                 ' isn\'t supported.' % addressVersionNumber)
@@ -571,16 +762,14 @@ def HandleGetDeterministicAddress(self, params):
         ))
         return queues.apiAddressGeneratorReturnQueue.get()
 
-    def HandleCreateChan(self, params):
-        """Handle a request to create a chan"""
-
-        if not params:
-            raise APIError(0, 'I need parameters.')
+    @command('createChan')
+    def HandleCreateChan(self, passphrase):
+        """
+        Creates a new chan. passphrase must be base64 encoded.
+        Returns the corresponding Bitmessage address.
+        """
 
-        elif len(params) == 1:
-            passphrase, = params
         passphrase = self._decode(passphrase, "base64")
-
         if not passphrase:
             raise APIError(1, 'The specified passphrase is blank.')
         # It would be nice to make the label the passphrase but it is
@@ -588,7 +777,7 @@ def HandleCreateChan(self, params):
         try:
             unicode(passphrase, 'utf-8')
             label = str_chan + ' ' + passphrase
-        except BaseException:
+        except UnicodeDecodeError:
             label = str_chan + ' ' + repr(passphrase)
 
         addressVersionNumber = 4
@@ -601,18 +790,17 @@ def HandleCreateChan(self, params):
             passphrase, True
         ))
         queueReturn = queues.apiAddressGeneratorReturnQueue.get()
-        if not queueReturn:
+        try:
+            return queueReturn[0]
+        except IndexError:
             raise APIError(24, 'Chan address is already present.')
-        address = queueReturn[0]
-        return address
 
-    def HandleJoinChan(self, params):
-        """Handle a request to join a chan"""
+    @command('joinChan')
+    def HandleJoinChan(self, passphrase, suppliedAddress):
+        """
+        Join a chan. passphrase must be base64 encoded. Returns 'success'.
+        """
 
-        if len(params) < 2:
-            raise APIError(0, 'I need two parameters.')
-        elif len(params) == 2:
-            passphrase, suppliedAddress = params
         passphrase = self._decode(passphrase, "base64")
         if not passphrase:
             raise APIError(1, 'The specified passphrase is blank.')
@@ -621,373 +809,287 @@ def HandleJoinChan(self, params):
         try:
             unicode(passphrase, 'utf-8')
             label = str_chan + ' ' + passphrase
-        except BaseException:
+        except UnicodeDecodeError:
             label = str_chan + ' ' + repr(passphrase)
-        status, addressVersionNumber, streamNumber, toRipe = (
-            self._verifyAddress(suppliedAddress))
+
+        self._verifyAddress(suppliedAddress)
         suppliedAddress = addBMIfNotPresent(suppliedAddress)
         queues.apiAddressGeneratorReturnQueue.queue.clear()
         queues.addressGeneratorQueue.put((
             'joinChan', suppliedAddress, label, passphrase, True
         ))
-        addressGeneratorReturnValue = \
-            queues.apiAddressGeneratorReturnQueue.get()
-
-        if addressGeneratorReturnValue[0] == \
-                'chan name does not match address':
-            raise APIError(18, 'Chan name does not match address.')
-        if not addressGeneratorReturnValue:
+        queueReturn = queues.apiAddressGeneratorReturnQueue.get()
+        try:
+            if queueReturn[0] == 'chan name does not match address':
+                raise APIError(18, 'Chan name does not match address.')
+        except IndexError:
             raise APIError(24, 'Chan address is already present.')
+
         return "success"
 
-    def HandleLeaveChan(self, params):
-        """Handle a request to leave a chan"""
+    @command('leaveChan')
+    def HandleLeaveChan(self, address):
+        """
+        Leave a chan. Returns 'success'.
 
-        if not params:
-            raise APIError(0, 'I need parameters.')
-        elif len(params) == 1:
-            address, = params
-            status, addressVersionNumber, streamNumber, toRipe = (
-                self._verifyAddress(address))
+        .. note:: at this time, the address is still shown in the UI
+          until a restart.
+        """
+        self._verifyAddress(address)
         address = addBMIfNotPresent(address)
-        if not BMConfigParser().has_section(address):
-            raise APIError(
-                13, 'Could not find this address in your keys.dat file.')
-        if not BMConfigParser().safeGetBoolean(address, 'chan'):
+        if not self.config.safeGetBoolean(address, 'chan'):
             raise APIError(
                 25, 'Specified address is not a chan address.'
                 ' Use deleteAddress API call instead.')
-        BMConfigParser().remove_section(address)
-        with open(state.appdata + 'keys.dat', 'wb') as configfile:
-            BMConfigParser().write(configfile)
-        return 'success'
-
-    def HandleDeleteAddress(self, params):
-        """Handle a request to delete an address"""
-
-        if not params:
-            raise APIError(0, 'I need parameters.')
-        elif len(params) == 1:
-            address, = params
-        status, addressVersionNumber, streamNumber, toRipe = (
-            self._verifyAddress(address))
+        try:
+            self.config.remove_section(address)
+        except ConfigParser.NoSectionError:
+            raise APIError(
+                13, 'Could not find this address in your keys.dat file.')
+        self.config.save()
+        queues.UISignalQueue.put(('rerenderMessagelistFromLabels', ''))
+        queues.UISignalQueue.put(('rerenderMessagelistToLabels', ''))
+        return "success"
+
+    @command('deleteAddress')
+    def HandleDeleteAddress(self, address):
+        """
+        Permanently delete the address from keys.dat file. Returns 'success'.
+        """
+        self._verifyAddress(address)
         address = addBMIfNotPresent(address)
-        if not BMConfigParser().has_section(address):
+        try:
+            self.config.remove_section(address)
+        except ConfigParser.NoSectionError:
             raise APIError(
                 13, 'Could not find this address in your keys.dat file.')
-        BMConfigParser().remove_section(address)
-        with open(state.appdata + 'keys.dat', 'wb') as configfile:
-            BMConfigParser().write(configfile)
+        self.config.save()
         queues.UISignalQueue.put(('writeNewAddressToTable', ('', '', '')))
         shared.reloadMyAddressHashes()
-        return 'success'
+        return "success"
 
-    def HandleGetAllInboxMessages(self, params):
-        """Handle a request to get all inbox messages"""
+    @command('getAllInboxMessages')
+    def HandleGetAllInboxMessages(self):
+        """
+        Returns a dict with all inbox messages in the *inboxMessages* key.
+        The message is a dict with such keys:
+        *msgid*, *toAddress*, *fromAddress*, *subject*, *message*,
+        *encodingType*, *receivedTime*, *read*.
+        *msgid* is hex encoded string.
+        *subject* and *message* are base64 encoded.
+        """
 
         queryreturn = sqlQuery(
             "SELECT msgid, toaddress, fromaddress, subject, received, message,"
-            " encodingtype, read FROM inbox where folder='inbox'"
+            " encodingtype, read FROM inbox WHERE folder='inbox'"
             " ORDER BY received"
         )
-        data = '{"inboxMessages":['
-        for row in queryreturn:
-            msgid, toAddress, fromAddress, subject, received, message, \
-                encodingtype, read = row
-            subject = shared.fixPotentiallyInvalidUTF8Data(subject)
-            message = shared.fixPotentiallyInvalidUTF8Data(message)
-            if len(data) > 25:
-                data += ','
-            data += json.dumps({
-                'msgid': hexlify(msgid),
-                'toAddress': toAddress,
-                'fromAddress': fromAddress,
-                'subject': base64.b64encode(subject),
-                'message': base64.b64encode(message),
-                'encodingType': encodingtype,
-                'receivedTime': received,
-                'read': read}, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleGetAllInboxMessageIds(self, params):
-        """Handle a request to get all inbox message IDs"""
+        return {"inboxMessages": [
+            self._dump_inbox_message(*data) for data in queryreturn
+        ]}
+
+    @command('getAllInboxMessageIds', 'getAllInboxMessageIDs')
+    def HandleGetAllInboxMessageIds(self):
+        """
+        The same as *getAllInboxMessages* but returns only *msgid*s,
+        result key - *inboxMessageIds*.
+        """
 
         queryreturn = sqlQuery(
             "SELECT msgid FROM inbox where folder='inbox' ORDER BY received")
-        data = '{"inboxMessageIds":['
-        for row in queryreturn:
-            msgid = row[0]
-            if len(data) > 25:
-                data += ','
-            data += json.dumps(
-                {'msgid': hexlify(msgid)}, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleGetInboxMessageById(self, params):
-        """Handle a request to get an inbox messsage by ID"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        elif len(params) == 1:
-            msgid = self._decode(params[0], "hex")
-        elif len(params) >= 2:
-            msgid = self._decode(params[0], "hex")
-            readStatus = params[1]
+
+        return {"inboxMessageIds": [
+            {'msgid': hexlify(msgid)} for msgid, in queryreturn
+        ]}
+
+    @command('getInboxMessageById', 'getInboxMessageByID')
+    def HandleGetInboxMessageById(self, hid, readStatus=None):
+        """
+        Returns a dict with list containing single message in the result
+        key *inboxMessage*. May also return None if message was not found.
+
+        :param str hid: hex encoded msgid
+        :param bool readStatus: sets the message's read status if present
+        """
+
+        msgid = self._decode(hid, "hex")
+        if readStatus is not None:
             if not isinstance(readStatus, bool):
                 raise APIError(
-                    23, 'Bool expected in readStatus, saw %s instead.' %
-                    type(readStatus))
+                    23, 'Bool expected in readStatus, saw %s instead.'
+                    % type(readStatus))
             queryreturn = sqlQuery(
                 "SELECT read FROM inbox WHERE msgid=?", msgid)
             # UPDATE is slow, only update if status is different
-            if queryreturn != [] and (queryreturn[0][0] == 1) != readStatus:
-                sqlExecute(
-                    "UPDATE inbox set read = ? WHERE msgid=?",
-                    readStatus, msgid)
-                queues.UISignalQueue.put(('changedInboxUnread', None))
+            try:
+                if (queryreturn[0][0] == 1) != readStatus:
+                    sqlExecute(
+                        "UPDATE inbox set read = ? WHERE msgid=?",
+                        readStatus, msgid)
+                    queues.UISignalQueue.put(('changedInboxUnread', None))
+            except IndexError:
+                pass
         queryreturn = sqlQuery(
-            "SELECT msgid, toaddress, fromaddress, subject, received, message,"
+            "SELECT toaddress, fromaddress, subject, received, message,"
             " encodingtype, read FROM inbox WHERE msgid=?", msgid
         )
-        data = '{"inboxMessage":['
-        for row in queryreturn:
-            msgid, toAddress, fromAddress, subject, received, message, \
-                encodingtype, read = row
-            subject = shared.fixPotentiallyInvalidUTF8Data(subject)
-            message = shared.fixPotentiallyInvalidUTF8Data(message)
-            data += json.dumps({
-                'msgid': hexlify(msgid),
-                'toAddress': toAddress,
-                'fromAddress': fromAddress,
-                'subject': base64.b64encode(subject),
-                'message': base64.b64encode(message),
-                'encodingType': encodingtype,
-                'receivedTime': received,
-                'read': read}, indent=4, separators=(',', ': '))
-            data += ']}'
-            return data
-
-    def HandleGetAllSentMessages(self, params):
-        """Handle a request to get all sent messages"""
+        try:
+            return {"inboxMessage": [
+                self._dump_inbox_message(*queryreturn[0])]}
+        except IndexError:
+            pass  # FIXME inconsistent
+
+    @command('getAllSentMessages')
+    def HandleGetAllSentMessages(self):
+        """
+        The same as *getAllInboxMessages* but for sent,
+        result key - *sentMessages*. Message dict keys are:
+        *msgid*, *toAddress*, *fromAddress*, *subject*, *message*,
+        *encodingType*, *lastActionTime*, *status*, *ackData*.
+        *ackData* is also a hex encoded string.
+        """
 
         queryreturn = sqlQuery(
             "SELECT msgid, toaddress, fromaddress, subject, lastactiontime,"
             " message, encodingtype, status, ackdata FROM sent"
             " WHERE folder='sent' ORDER BY lastactiontime"
         )
-        data = '{"sentMessages":['
-        for row in queryreturn:
-            msgid, toAddress, fromAddress, subject, lastactiontime, message, \
-                encodingtype, status, ackdata = row
-            subject = shared.fixPotentiallyInvalidUTF8Data(subject)
-            message = shared.fixPotentiallyInvalidUTF8Data(message)
-            if len(data) > 25:
-                data += ','
-            data += json.dumps({
-                'msgid': hexlify(msgid),
-                'toAddress': toAddress,
-                'fromAddress': fromAddress,
-                'subject': base64.b64encode(subject),
-                'message': base64.b64encode(message),
-                'encodingType': encodingtype,
-                'lastActionTime': lastactiontime,
-                'status': status,
-                'ackData': hexlify(ackdata)}, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleGetAllSentMessageIds(self, params):
-        """Handle a request to get all sent message IDs"""
+        return {"sentMessages": [
+            self._dump_sent_message(*data) for data in queryreturn
+        ]}
+
+    @command('getAllSentMessageIds', 'getAllSentMessageIDs')
+    def HandleGetAllSentMessageIds(self):
+        """
+        The same as *getAllInboxMessageIds* but for sent,
+        result key - *sentMessageIds*.
+        """
 
         queryreturn = sqlQuery(
-            "SELECT msgid FROM sent where folder='sent'"
+            "SELECT msgid FROM sent WHERE folder='sent'"
             " ORDER BY lastactiontime"
         )
-        data = '{"sentMessageIds":['
-        for row in queryreturn:
-            msgid = row[0]
-            if len(data) > 25:
-                data += ','
-            data += json.dumps(
-                {'msgid': hexlify(msgid)}, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleInboxMessagesByReceiver(self, params):
-        """Handle a request to get inbox messages by receiver"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        toAddress = params[0]
+        return {"sentMessageIds": [
+            {'msgid': hexlify(msgid)} for msgid, in queryreturn
+        ]}
+
+    # after some time getInboxMessagesByAddress should be removed
+    @command('getInboxMessagesByReceiver', 'legacy:getInboxMessagesByAddress')
+    def HandleInboxMessagesByReceiver(self, toAddress):
+        """
+        The same as *getAllInboxMessages* but returns only messages
+        for toAddress.
+        """
+
         queryreturn = sqlQuery(
-            "SELECT msgid, toaddress, fromaddress, subject, received, message,"
-            " encodingtype FROM inbox WHERE folder='inbox' AND toAddress=?",
-            toAddress)
-        data = '{"inboxMessages":['
-        for row in queryreturn:
-            msgid, toAddress, fromAddress, subject, received, message, \
-                encodingtype = row
-            subject = shared.fixPotentiallyInvalidUTF8Data(subject)
-            message = shared.fixPotentiallyInvalidUTF8Data(message)
-            if len(data) > 25:
-                data += ','
-            data += json.dumps({
-                'msgid': hexlify(msgid),
-                'toAddress': toAddress,
-                'fromAddress': fromAddress,
-                'subject': base64.b64encode(subject),
-                'message': base64.b64encode(message),
-                'encodingType': encodingtype,
-                'receivedTime': received}, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleGetSentMessageById(self, params):
-        """Handle a request to get a sent message by ID"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        msgid = self._decode(params[0], "hex")
+            "SELECT msgid, toaddress, fromaddress, subject, received,"
+            " message, encodingtype, read FROM inbox WHERE folder='inbox'"
+            " AND toAddress=?", toAddress)
+        return {"inboxMessages": [
+            self._dump_inbox_message(*data) for data in queryreturn
+        ]}
+
+    @command('getSentMessageById', 'getSentMessageByID')
+    def HandleGetSentMessageById(self, hid):
+        """
+        Similiar to *getInboxMessageById* but doesn't change message's
+        read status (sent messages have no such field).
+        Result key is *sentMessage*
+        """
+
+        msgid = self._decode(hid, "hex")
         queryreturn = sqlQuery(
             "SELECT msgid, toaddress, fromaddress, subject, lastactiontime,"
             " message, encodingtype, status, ackdata FROM sent WHERE msgid=?",
             msgid
         )
-        data = '{"sentMessage":['
-        for row in queryreturn:
-            msgid, toAddress, fromAddress, subject, lastactiontime, message, \
-                encodingtype, status, ackdata = row
-            subject = shared.fixPotentiallyInvalidUTF8Data(subject)
-            message = shared.fixPotentiallyInvalidUTF8Data(message)
-            data += json.dumps({
-                'msgid': hexlify(msgid),
-                'toAddress': toAddress,
-                'fromAddress': fromAddress,
-                'subject': base64.b64encode(subject),
-                'message': base64.b64encode(message),
-                'encodingType': encodingtype,
-                'lastActionTime': lastactiontime,
-                'status': status,
-                'ackData': hexlify(ackdata)}, indent=4, separators=(',', ': '))
-            data += ']}'
-            return data
-
-    def HandleGetSentMessagesByAddress(self, params):
-        """Handle a request to get sent messages by address"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        fromAddress = params[0]
+        try:
+            return {"sentMessage": [
+                self._dump_sent_message(*queryreturn[0])
+            ]}
+        except IndexError:
+            pass  # FIXME inconsistent
+
+    @command('getSentMessagesByAddress', 'getSentMessagesBySender')
+    def HandleGetSentMessagesByAddress(self, fromAddress):
+        """
+        The same as *getAllSentMessages* but returns only messages
+        from fromAddress.
+        """
+
         queryreturn = sqlQuery(
             "SELECT msgid, toaddress, fromaddress, subject, lastactiontime,"
             " message, encodingtype, status, ackdata FROM sent"
             " WHERE folder='sent' AND fromAddress=? ORDER BY lastactiontime",
             fromAddress
         )
-        data = '{"sentMessages":['
-        for row in queryreturn:
-            msgid, toAddress, fromAddress, subject, lastactiontime, message, \
-                encodingtype, status, ackdata = row
-            subject = shared.fixPotentiallyInvalidUTF8Data(subject)
-            message = shared.fixPotentiallyInvalidUTF8Data(message)
-            if len(data) > 25:
-                data += ','
-            data += json.dumps({
-                'msgid': hexlify(msgid),
-                'toAddress': toAddress,
-                'fromAddress': fromAddress,
-                'subject': base64.b64encode(subject),
-                'message': base64.b64encode(message),
-                'encodingType': encodingtype,
-                'lastActionTime': lastactiontime,
-                'status': status,
-                'ackData': hexlify(ackdata)}, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleGetSentMessagesByAckData(self, params):
-        """Handle a request to get sent messages by ack data"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        ackData = self._decode(params[0], "hex")
+        return {"sentMessages": [
+            self._dump_sent_message(*data) for data in queryreturn
+        ]}
+
+    @command('getSentMessageByAckData')
+    def HandleGetSentMessagesByAckData(self, ackData):
+        """
+        Similiar to *getSentMessageById* but searches by ackdata
+        (also hex encoded).
+        """
+
+        ackData = self._decode(ackData, "hex")
         queryreturn = sqlQuery(
             "SELECT msgid, toaddress, fromaddress, subject, lastactiontime,"
             " message, encodingtype, status, ackdata FROM sent"
             " WHERE ackdata=?", ackData
         )
-        data = '{"sentMessage":['
-        for row in queryreturn:
-            msgid, toAddress, fromAddress, subject, lastactiontime, message, \
-                encodingtype, status, ackdata = row
-            subject = shared.fixPotentiallyInvalidUTF8Data(subject)
-            message = shared.fixPotentiallyInvalidUTF8Data(message)
-            data += json.dumps({
-                'msgid': hexlify(msgid),
-                'toAddress': toAddress,
-                'fromAddress': fromAddress,
-                'subject': base64.b64encode(subject),
-                'message': base64.b64encode(message),
-                'encodingType': encodingtype,
-                'lastActionTime': lastactiontime,
-                'status': status,
-                'ackData': hexlify(ackdata)}, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleTrashMessage(self, params):
-        """Handle a request to trash a message by ID"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        msgid = self._decode(params[0], "hex")
 
+        try:
+            return {"sentMessage": [
+                self._dump_sent_message(*queryreturn[0])
+            ]}
+        except IndexError:
+            pass  # FIXME inconsistent
+
+    @command('trashMessage')
+    def HandleTrashMessage(self, msgid):
+        """
+        Trash message by msgid (encoded in hex). Returns a simple message
+        saying that the message was trashed assuming it ever even existed.
+        Prior existence is not checked.
+        """
+        msgid = self._decode(msgid, "hex")
         # Trash if in inbox table
         helper_inbox.trash(msgid)
         # Trash if in sent table
-        sqlExecute('''UPDATE sent SET folder='trash' WHERE msgid=?''', msgid)
+        sqlExecute("UPDATE sent SET folder='trash' WHERE msgid=?", msgid)
         return 'Trashed message (assuming message existed).'
 
-    def HandleTrashInboxMessage(self, params):
-        """Handle a request to trash an inbox message by ID"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        msgid = self._decode(params[0], "hex")
+    @command('trashInboxMessage')
+    def HandleTrashInboxMessage(self, msgid):
+        """Trash inbox message by msgid (encoded in hex)."""
+        msgid = self._decode(msgid, "hex")
         helper_inbox.trash(msgid)
         return 'Trashed inbox message (assuming message existed).'
 
-    def HandleTrashSentMessage(self, params):
-        """Handle a request to trash a sent message by ID"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        msgid = self._decode(params[0], "hex")
+    @command('trashSentMessage')
+    def HandleTrashSentMessage(self, msgid):
+        """Trash sent message by msgid (encoded in hex)."""
+        msgid = self._decode(msgid, "hex")
         sqlExecute('''UPDATE sent SET folder='trash' WHERE msgid=?''', msgid)
         return 'Trashed sent message (assuming message existed).'
 
-    def HandleSendMessage(self, params):  # pylint: disable=too-many-locals
-        """Handle a request to send a message"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-
-        elif len(params) == 4:
-            toAddress, fromAddress, subject, message = params
-            encodingType = 2
-            TTL = 4 * 24 * 60 * 60
-
-        elif len(params) == 5:
-            toAddress, fromAddress, subject, message, encodingType = params
-            TTL = 4 * 24 * 60 * 60
-
-        elif len(params) == 6:
-            toAddress, fromAddress, subject, message, encodingType, TTL = \
-                params
-
-        if encodingType not in [2, 3]:
+    @command('sendMessage')
+    def HandleSendMessage(
+        self, toAddress, fromAddress, subject, message,
+        encodingType=2, TTL=4 * 24 * 60 * 60
+    ):
+        """
+        Send the message and return ackdata (hex encoded string).
+        subject and message must be encoded in base64 which may optionally
+        include line breaks. TTL is specified in seconds; values outside
+        the bounds of 3600 to 2419200 will be moved to be within those
+        bounds. TTL defaults to 4 days.
+        """
+        # pylint: disable=too-many-locals
+        if encodingType not in (2, 3):
             raise APIError(6, 'The encoding type must be 2 or 3.')
         subject = self._decode(subject, "base64")
         message = self._decode(message, "base64")
@@ -999,11 +1101,10 @@ def HandleSendMessage(self, params):  # pylint: disable=too-many-locals
             TTL = 28 * 24 * 60 * 60
         toAddress = addBMIfNotPresent(toAddress)
         fromAddress = addBMIfNotPresent(fromAddress)
-        status, addressVersionNumber, streamNumber, toRipe = \
-            self._verifyAddress(toAddress)
+        streamNumber, toRipe = self._verifyAddress(toAddress)[2:]
         self._verifyAddress(fromAddress)
         try:
-            fromAddressEnabled = BMConfigParser().getboolean(
+            fromAddressEnabled = self.config.getboolean(
                 fromAddress, 'enabled')
         except BaseException:
             raise APIError(
@@ -1011,7 +1112,7 @@ def HandleSendMessage(self, params):  # pylint: disable=too-many-locals
         if not fromAddressEnabled:
             raise APIError(14, 'Your fromAddress is disabled. Cannot send.')
 
-        stealthLevel = BMConfigParser().safeGetInt(
+        stealthLevel = self.config.safeGetInt(
             'bitmessagesettings', 'ackstealthlevel')
         ackdata = genAckPayload(streamNumber, stealthLevel)
 
@@ -1028,41 +1129,31 @@ def HandleSendMessage(self, params):  # pylint: disable=too-many-locals
              'msgqueued',
              0,
              'sent',
-             2,
+             encodingType,
              TTL)
         helper_sent.insert(t)
 
         toLabel = ''
         queryreturn = sqlQuery(
             "SELECT label FROM addressbook WHERE address=?", toAddress)
-        if queryreturn != []:
-            for row in queryreturn:
-                toLabel, = row
+        try:
+            toLabel, = queryreturn[0][0]
+        except IndexError:
+            pass
+
         queues.UISignalQueue.put(('displayNewSentMessage', (
             toAddress, toLabel, fromAddress, subject, message, ackdata)))
-
         queues.workerQueue.put(('sendmessage', toAddress))
 
         return hexlify(ackdata)
 
-    def HandleSendBroadcast(self, params):
-        """Handle a request to send a broadcast message"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-
-        if len(params) == 3:
-            fromAddress, subject, message = params
-            encodingType = 2
-            TTL = 4 * 24 * 60 * 60
+    @command('sendBroadcast')
+    def HandleSendBroadcast(
+        self, fromAddress, subject, message, encodingType=2,
+            TTL=4 * 24 * 60 * 60):
+        """Send the broadcast message. Similiar to *sendMessage*."""
 
-        elif len(params) == 4:
-            fromAddress, subject, message, encodingType = params
-            TTL = 4 * 24 * 60 * 60
-        elif len(params) == 5:
-            fromAddress, subject, message, encodingType, TTL = params
-
-        if encodingType not in [2, 3]:
+        if encodingType not in (2, 3):
             raise APIError(6, 'The encoding type must be 2 or 3.')
 
         subject = self._decode(subject, "base64")
@@ -1076,13 +1167,13 @@ def HandleSendBroadcast(self, params):
         fromAddress = addBMIfNotPresent(fromAddress)
         self._verifyAddress(fromAddress)
         try:
-            BMConfigParser().getboolean(fromAddress, 'enabled')
+            self.config.getboolean(fromAddress, 'enabled')
         except BaseException:
             raise APIError(
-                13, 'could not find your fromAddress in the keys.dat file.')
+                13, 'Could not find your fromAddress in the keys.dat file.')
         streamNumber = decodeAddress(fromAddress)[2]
         ackdata = genAckPayload(streamNumber, 0)
-        toAddress = '[Broadcast subscribers]'
+        toAddress = str_broadcast_subscribers
         ripe = ''
 
         t = ('',
@@ -1098,59 +1189,54 @@ def HandleSendBroadcast(self, params):
              'broadcastqueued',
              0,
              'sent',
-             2,
+             encodingType,
              TTL)
         helper_sent.insert(t)
 
-        toLabel = '[Broadcast subscribers]'
+        toLabel = str_broadcast_subscribers
         queues.UISignalQueue.put(('displayNewSentMessage', (
             toAddress, toLabel, fromAddress, subject, message, ackdata)))
         queues.workerQueue.put(('sendbroadcast', ''))
 
         return hexlify(ackdata)
 
-    def HandleGetStatus(self, params):
-        """Handle a request to get the status of a sent message"""
+    @command('getStatus')
+    def HandleGetStatus(self, ackdata):
+        """
+        Get the status of sent message by its ackdata (hex encoded).
+        Returns one of these strings: notfound, msgqueued,
+        broadcastqueued, broadcastsent, doingpubkeypow, awaitingpubkey,
+        doingmsgpow, forcepow, msgsent, msgsentnoackexpected or ackreceived.
+        """
 
-        if len(params) != 1:
-            raise APIError(0, 'I need one parameter!')
-        ackdata, = params
         if len(ackdata) < 76:
             # The length of ackData should be at least 38 bytes (76 hex digits)
             raise APIError(15, 'Invalid ackData object size.')
         ackdata = self._decode(ackdata, "hex")
         queryreturn = sqlQuery(
             "SELECT status FROM sent where ackdata=?", ackdata)
-        if queryreturn == []:
+        try:
+            return queryreturn[0][0]
+        except IndexError:
             return 'notfound'
-        for row in queryreturn:
-            status, = row
-            return status
-
-    def HandleAddSubscription(self, params):
-        """Handle a request to add a subscription"""
-
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        if len(params) == 1:
-            address, = params
-            label = ''
-        if len(params) == 2:
-            address, label = params
+
+    @command('addSubscription')
+    def HandleAddSubscription(self, address, label=''):
+        """Subscribe to the address. label must be base64 encoded."""
+
+        if label:
             label = self._decode(label, "base64")
             try:
                 unicode(label, 'utf-8')
-            except BaseException:
+            except UnicodeDecodeError:
                 raise APIError(17, 'Label is not valid UTF-8 data.')
-        if len(params) > 2:
-            raise APIError(0, 'I need either 1 or 2 parameters!')
-        address = addBMIfNotPresent(address)
         self._verifyAddress(address)
+        address = addBMIfNotPresent(address)
         # First we must check to see if the address is already in the
         # subscriptions list.
         queryreturn = sqlQuery(
             "SELECT * FROM subscriptions WHERE address=?", address)
-        if queryreturn != []:
+        if queryreturn:
             raise APIError(16, 'You are already subscribed to that address.')
         sqlExecute(
             "INSERT INTO subscriptions VALUES (?,?,?)", label, address, True)
@@ -1159,36 +1245,43 @@ def HandleAddSubscription(self, params):
         queues.UISignalQueue.put(('rerenderSubscriptions', ''))
         return 'Added subscription.'
 
-    def HandleDeleteSubscription(self, params):
-        """Handle a request to delete a subscription"""
+    @command('deleteSubscription')
+    def HandleDeleteSubscription(self, address):
+        """
+        Unsubscribe from the address. The program does not check whether
+        you were subscribed in the first place.
+        """
 
-        if len(params) != 1:
-            raise APIError(0, 'I need 1 parameter!')
-        address, = params
         address = addBMIfNotPresent(address)
-        sqlExecute('''DELETE FROM subscriptions WHERE address=?''', address)
+        sqlExecute("DELETE FROM subscriptions WHERE address=?", address)
         shared.reloadBroadcastSendersForWhichImWatching()
         queues.UISignalQueue.put(('rerenderMessagelistFromLabels', ''))
         queues.UISignalQueue.put(('rerenderSubscriptions', ''))
         return 'Deleted subscription if it existed.'
 
-    def ListSubscriptions(self, params):
-        """Handle a request to list susbcriptions"""
+    @command('listSubscriptions')
+    def ListSubscriptions(self):
+        """
+        Returns dict with a list of all subscriptions
+        in the *subscriptions* key.
+        """
 
         queryreturn = sqlQuery(
             "SELECT label, address, enabled FROM subscriptions")
-        data = {'subscriptions': []}
-        for row in queryreturn:
-            label, address, enabled = row
+        data = []
+        for label, address, enabled in queryreturn:
             label = shared.fixPotentiallyInvalidUTF8Data(label)
-            data['subscriptions'].append({
+            data.append({
                 'label': base64.b64encode(label),
                 'address': address,
                 'enabled': enabled == 1
             })
-        return json.dumps(data, indent=4, separators=(',', ': '))
+        return {'subscriptions': data}
 
-    def HandleDisseminatePreEncryptedMsg(self, params):
+    @command('disseminatePreEncryptedMsg')
+    def HandleDisseminatePreEncryptedMsg(
+        self, encryptedPayload, requiredAverageProofOfWorkNonceTrialsPerByte,
+            requiredPayloadLengthExtraBytes):
         """Handle a request to disseminate an encrypted message"""
 
         # The device issuing this command to PyBitmessage supplies a msg
@@ -1196,42 +1289,28 @@ def HandleDisseminatePreEncryptedMsg(self, params):
         # to be done. PyBitmessage accepts this msg object and sends it out
         # to the rest of the Bitmessage network as if it had generated
         # the message itself. Please do not yet add this to the api doc.
-        if len(params) != 3:
-            raise APIError(0, 'I need 3 parameter!')
-        encryptedPayload, requiredAverageProofOfWorkNonceTrialsPerByte, \
-            requiredPayloadLengthExtraBytes = params
         encryptedPayload = self._decode(encryptedPayload, "hex")
         # Let us do the POW and attach it to the front
-        target = 2**64 / (
-            (
-                len(encryptedPayload) + requiredPayloadLengthExtraBytes + 8
-            ) * requiredAverageProofOfWorkNonceTrialsPerByte
+        target = 2**64 / ((
+            len(encryptedPayload) + requiredPayloadLengthExtraBytes + 8) *
+            requiredAverageProofOfWorkNonceTrialsPerByte)
+        logger.info(
+            '(For msg message via API) Doing proof of work. Total  required'
+            ' difficulty: %s\nRequired small message difficulty: %s',
+            float(requiredAverageProofOfWorkNonceTrialsPerByte) /
+            defaults.networkDefaultProofOfWorkNonceTrialsPerByte,
+            float(requiredPayloadLengthExtraBytes) /
+            defaults.networkDefaultPayloadLengthExtraBytes,
         )
-        with threads.printLock:
-            print(
-                '(For msg message via API) Doing proof of work.'
-                'Total required difficulty:',
-                float(
-                    requiredAverageProofOfWorkNonceTrialsPerByte
-                ) / defaults.networkDefaultProofOfWorkNonceTrialsPerByte,
-                'Required small message difficulty:',
-                float(
-                    requiredPayloadLengthExtraBytes
-                ) / defaults.networkDefaultPayloadLengthExtraBytes,
-            )
         powStartTime = time.time()
         initialHash = hashlib.sha512(encryptedPayload).digest()
         trialValue, nonce = proofofwork.run(target, initialHash)
-        with threads.printLock:
-            print '(For msg message via API) Found proof of work', trialValue, 'Nonce:', nonce
-            try:
-                print(
-                    'POW took', int(time.time() - powStartTime),
-                    'seconds.', nonce / (time.time() - powStartTime),
-                    'nonce trials per second.',
-                )
-            except BaseException:
-                pass
+        logger.info(
+            '(For msg message via API) Found proof of work %s\nNonce: %s\n'
+            'POW took %s seconds. %s nonce trials per second.',
+            trialValue, nonce, int(time.time() - powStartTime),
+            nonce / (time.time() - powStartTime)
+        )
         encryptedPayload = pack('>Q', nonce) + encryptedPayload
         toStreamNumber = decodeVarint(encryptedPayload[16:26])[0]
         inventoryHash = calculateInventoryHash(encryptedPayload)
@@ -1241,21 +1320,21 @@ def HandleDisseminatePreEncryptedMsg(self, params):
             objectType, toStreamNumber, encryptedPayload,
             int(time.time()) + TTL, ''
         )
-        with threads.printLock:
-            print 'Broadcasting inv for msg(API disseminatePreEncryptedMsg command):', hexlify(inventoryHash)
+        logger.info(
+            'Broadcasting inv for msg(API disseminatePreEncryptedMsg'
+            ' command): %s', hexlify(inventoryHash))
         queues.invQueue.put((toStreamNumber, inventoryHash))
 
-    def HandleTrashSentMessageByAckDAta(self, params):
-        """Handle a request to trash a sent message by ackdata"""
-
+    @command('trashSentMessageByAckData')
+    def HandleTrashSentMessageByAckDAta(self, ackdata):
+        """Trash a sent message by ackdata (hex encoded)"""
         # This API method should only be used when msgid is not available
-        if not params:
-            raise APIError(0, 'I need parameters!')
-        ackdata = self._decode(params[0], "hex")
+        ackdata = self._decode(ackdata, "hex")
         sqlExecute("UPDATE sent SET folder='trash' WHERE ackdata=?", ackdata)
         return 'Trashed sent message (assuming message existed).'
 
-    def HandleDissimatePubKey(self, params):
+    @command('disseminatePubkey')
+    def HandleDissimatePubKey(self, payload):
         """Handle a request to disseminate a public key"""
 
         # The device issuing this command to PyBitmessage supplies a pubkey
@@ -1263,19 +1342,19 @@ def HandleDissimatePubKey(self, params):
         # PyBitmessage accepts this pubkey object and sends it out to the rest
         # of the Bitmessage network as if it had generated the pubkey object
         # itself. Please do not yet add this to the api doc.
-        if len(params) != 1:
-            raise APIError(0, 'I need 1 parameter!')
-        payload, = params
         payload = self._decode(payload, "hex")
 
         # Let us do the POW
         target = 2 ** 64 / ((
             len(payload) + defaults.networkDefaultPayloadLengthExtraBytes + 8
         ) * defaults.networkDefaultProofOfWorkNonceTrialsPerByte)
-        print '(For pubkey message via API) Doing proof of work...'
+        logger.info('(For pubkey message via API) Doing proof of work...')
         initialHash = hashlib.sha512(payload).digest()
         trialValue, nonce = proofofwork.run(target, initialHash)
-        print '(For pubkey message via API) Found proof of work', trialValue, 'Nonce:', nonce
+        logger.info(
+            '(For pubkey message via API) Found proof of work %s Nonce: %s',
+            trialValue, nonce
+        )
         payload = pack('>Q', nonce) + payload
 
         pubkeyReadPosition = 8  # bypass the nonce
@@ -1284,8 +1363,8 @@ def HandleDissimatePubKey(self, params):
             pubkeyReadPosition += 8
         else:
             pubkeyReadPosition += 4
-        addressVersion, addressVersionLength = decodeVarint(
-            payload[pubkeyReadPosition:pubkeyReadPosition + 10])
+        addressVersionLength = decodeVarint(
+            payload[pubkeyReadPosition:pubkeyReadPosition + 10])[1]
         pubkeyReadPosition += addressVersionLength
         pubkeyStreamNumber = decodeVarint(
             payload[pubkeyReadPosition:pubkeyReadPosition + 10])[0]
@@ -1295,19 +1374,19 @@ def HandleDissimatePubKey(self, params):
         Inventory()[inventoryHash] = (
             objectType, pubkeyStreamNumber, payload, int(time.time()) + TTL, ''
         )
-        with threads.printLock:
-            print 'broadcasting inv within API command disseminatePubkey with hash:', hexlify(inventoryHash)
+        logger.info(
+            'broadcasting inv within API command disseminatePubkey with'
+            ' hash: %s', hexlify(inventoryHash))
         queues.invQueue.put((pubkeyStreamNumber, inventoryHash))
 
-    def HandleGetMessageDataByDestinationHash(self, params):
+    @command(
+        'getMessageDataByDestinationHash', 'getMessageDataByDestinationTag')
+    def HandleGetMessageDataByDestinationHash(self, requestedHash):
         """Handle a request to get message data by destination hash"""
 
         # Method will eventually be used by a particular Android app to
         # select relevant messages. Do not yet add this to the api
         # doc.
-        if len(params) != 1:
-            raise APIError(0, 'I need 1 parameter!')
-        requestedHash, = params
         if len(requestedHash) != 32:
             raise APIError(
                 19, 'The length of hash should be 32 bytes (encoded in hex'
@@ -1321,8 +1400,7 @@ def HandleGetMessageDataByDestinationHash(self, params):
             "SELECT hash, payload FROM inventory WHERE tag = ''"
             " and objecttype = 2")
         with SqlBulkExecute() as sql:
-            for row in queryreturn:
-                hash01, payload = row
+            for hash01, payload in queryreturn:
                 readPosition = 16  # Nonce length + time length
                 # Stream Number length
                 readPosition += decodeVarint(
@@ -1332,18 +1410,21 @@ def HandleGetMessageDataByDestinationHash(self, params):
 
         queryreturn = sqlQuery(
             "SELECT payload FROM inventory WHERE tag = ?", requestedHash)
-        data = '{"receivedMessageDatas":['
-        for row in queryreturn:
-            payload, = row
-            if len(data) > 25:
-                data += ','
-            data += json.dumps(
-                {'data': hexlify(payload)}, indent=4, separators=(',', ': '))
-        data += ']}'
-        return data
-
-    def HandleClientStatus(self, params):
-        """Handle a request to get the status of the client"""
+        return {"receivedMessageDatas": [
+            {'data': hexlify(payload)} for payload, in queryreturn
+        ]}
+
+    @command('clientStatus')
+    def HandleClientStatus(self):
+        """
+        Returns the bitmessage status as dict with keys *networkConnections*,
+        *numberOfMessagesProcessed*, *numberOfBroadcastsProcessed*,
+        *numberOfPubkeysProcessed*, *networkStatus*,
+        *softwareName*, *softwareVersion*. *networkStatus* will be one
+        of these strings: "notConnected",
+        "connectedButHaveNotReceivedIncomingConnections",
+        or "connectedAndReceivingIncomingConnections".
+        """
 
         connections_num = len(network.stats.connectedHostsList())
         if connections_num == 0:
@@ -1352,7 +1433,7 @@ def HandleClientStatus(self, params):
             networkStatus = 'connectedAndReceivingIncomingConnections'
         else:
             networkStatus = 'connectedButHaveNotReceivedIncomingConnections'
-        return json.dumps({
+        return {
             'networkConnections': connections_num,
             'numberOfMessagesProcessed': state.numberOfMessagesProcessed,
             'numberOfBroadcastsProcessed': state.numberOfBroadcastsProcessed,
@@ -1360,141 +1441,89 @@ def HandleClientStatus(self, params):
             'networkStatus': networkStatus,
             'softwareName': 'PyBitmessage',
             'softwareVersion': softwareVersion
-        }, indent=4, separators=(',', ': '))
-
-    def HandleDecodeAddress(self, params):
-        """Handle a request to decode an address"""
+        }
 
-        # Return a meaningful decoding of an address.
-        if len(params) != 1:
-            raise APIError(0, 'I need 1 parameter!')
-        address, = params
-        status, addressVersion, streamNumber, ripe = decodeAddress(address)
-        return json.dumps({
-            'status': status,
-            'addressVersion': addressVersion,
-            'streamNumber': streamNumber,
-            'ripe': base64.b64encode(ripe)
-        }, indent=4, separators=(',', ': '))
-
-    def HandleHelloWorld(self, params):
+    @command('helloWorld')
+    def HandleHelloWorld(self, a, b):
         """Test two string params"""
-
-        a, b = params
         return a + '-' + b
 
-    def HandleAdd(self, params):
+    @command('add')
+    def HandleAdd(self, a, b):
         """Test two numeric params"""
-
-        a, b = params
         return a + b
 
-    def HandleStatusBar(self, params):
-        """Handle a request to update the status bar"""
-
-        message, = params
+    @command('statusBar')
+    def HandleStatusBar(self, message):
+        """Update GUI statusbar message"""
         queues.UISignalQueue.put(('updateStatusBar', message))
 
-    def HandleDeleteAndVacuum(self, params):
-        """Handle a request to run the deleteandvacuum stored procedure"""
-
-        if not params:
-            sqlStoredProcedure('deleteandvacuume')
-            return 'done'
-        return None
-
-    def HandleShutdown(self, params):
-        """Handle a request to shutdown the node"""
-
-        if not params:
-            # backward compatible trick because False == 0 is True
-            state.shutdown = False
-            return 'done'
-        return None
-
-    handlers = {}
-    handlers['helloWorld'] = HandleHelloWorld
-    handlers['add'] = HandleAdd
-    handlers['statusBar'] = HandleStatusBar
-    handlers['listAddresses'] = HandleListAddresses
-    handlers['listAddressBookEntries'] = HandleListAddressBookEntries
-    # the listAddressbook alias should be removed eventually.
-    handlers['listAddressbook'] = HandleListAddressBookEntries
-    handlers['addAddressBookEntry'] = HandleAddAddressBookEntry
-    # the addAddressbook alias should be deleted eventually.
-    handlers['addAddressbook'] = HandleAddAddressBookEntry
-    handlers['deleteAddressBookEntry'] = HandleDeleteAddressBookEntry
-    # The deleteAddressbook alias should be deleted eventually.
-    handlers['deleteAddressbook'] = HandleDeleteAddressBookEntry
-    handlers['createRandomAddress'] = HandleCreateRandomAddress
-    handlers['createDeterministicAddresses'] = \
-        HandleCreateDeterministicAddresses
-    handlers['getDeterministicAddress'] = HandleGetDeterministicAddress
-    handlers['createChan'] = HandleCreateChan
-    handlers['joinChan'] = HandleJoinChan
-    handlers['leaveChan'] = HandleLeaveChan
-    handlers['deleteAddress'] = HandleDeleteAddress
-    handlers['getAllInboxMessages'] = HandleGetAllInboxMessages
-    handlers['getAllInboxMessageIds'] = HandleGetAllInboxMessageIds
-    handlers['getAllInboxMessageIDs'] = HandleGetAllInboxMessageIds
-    handlers['getInboxMessageById'] = HandleGetInboxMessageById
-    handlers['getInboxMessageByID'] = HandleGetInboxMessageById
-    handlers['getAllSentMessages'] = HandleGetAllSentMessages
-    handlers['getAllSentMessageIds'] = HandleGetAllSentMessageIds
-    handlers['getAllSentMessageIDs'] = HandleGetAllSentMessageIds
-    handlers['getInboxMessagesByReceiver'] = HandleInboxMessagesByReceiver
-    # after some time getInboxMessagesByAddress should be removed
-    handlers['getInboxMessagesByAddress'] = HandleInboxMessagesByReceiver
-    handlers['getSentMessageById'] = HandleGetSentMessageById
-    handlers['getSentMessageByID'] = HandleGetSentMessageById
-    handlers['getSentMessagesByAddress'] = HandleGetSentMessagesByAddress
-    handlers['getSentMessagesBySender'] = HandleGetSentMessagesByAddress
-    handlers['getSentMessageByAckData'] = HandleGetSentMessagesByAckData
-    handlers['trashMessage'] = HandleTrashMessage
-    handlers['trashInboxMessage'] = HandleTrashInboxMessage
-    handlers['trashSentMessage'] = HandleTrashSentMessage
-    handlers['trashSentMessageByAckData'] = HandleTrashSentMessageByAckDAta
-    handlers['sendMessage'] = HandleSendMessage
-    handlers['sendBroadcast'] = HandleSendBroadcast
-    handlers['getStatus'] = HandleGetStatus
-    handlers['addSubscription'] = HandleAddSubscription
-    handlers['deleteSubscription'] = HandleDeleteSubscription
-    handlers['listSubscriptions'] = ListSubscriptions
-    handlers['disseminatePreEncryptedMsg'] = HandleDisseminatePreEncryptedMsg
-    handlers['disseminatePubkey'] = HandleDissimatePubKey
-    handlers['getMessageDataByDestinationHash'] = \
-        HandleGetMessageDataByDestinationHash
-    handlers['getMessageDataByDestinationTag'] = \
-        HandleGetMessageDataByDestinationHash
-    handlers['clientStatus'] = HandleClientStatus
-    handlers['decodeAddress'] = HandleDecodeAddress
-    handlers['deleteAndVacuum'] = HandleDeleteAndVacuum
-    handlers['shutdown'] = HandleShutdown
+    @command('deleteAndVacuum')
+    def HandleDeleteAndVacuum(self):
+        """Cleanup trashes and vacuum messages database"""
+        sqlStoredProcedure('deleteandvacuume')
+        return 'done'
+
+    @command('shutdown')
+    def HandleShutdown(self):
+        """Shutdown the bitmessage. Returns 'done'."""
+        # backward compatible trick because False == 0 is True
+        state.shutdown = False
+        return 'done'
 
     def _handle_request(self, method, params):
-        if method not in self.handlers:
+        try:
+            # pylint: disable=attribute-defined-outside-init
+            self._method = method
+            func = self._handlers[method]
+            return func(self, *params)
+        except KeyError:
             raise APIError(20, 'Invalid method: %s' % method)
-        result = self.handlers[method](self, params)
-        state.last_api_response = time.time()
-        return result
+        except TypeError as e:
+            msg = 'Unexpected API Failure - %s' % e
+            if 'argument' not in str(e):
+                raise APIError(21, msg)
+            argcount = len(params)
+            maxcount = func.func_code.co_argcount
+            if argcount > maxcount:
+                msg = (
+                    'Command %s takes at most %s parameters (%s given)'
+                    % (method, maxcount, argcount))
+            else:
+                mincount = maxcount - len(func.func_defaults or [])
+                if argcount < mincount:
+                    msg = (
+                        'Command %s takes at least %s parameters (%s given)'
+                        % (method, mincount, argcount))
+            raise APIError(0, msg)
+        finally:
+            state.last_api_response = time.time()
 
     def _dispatch(self, method, params):
-        # pylint: disable=attribute-defined-outside-init
-        self.cookies = []
-
-        validuser = self.APIAuthenticateClient()
-        if not validuser:
-            time.sleep(2)
-            return "RPC Username or password incorrect or HTTP header lacks authentication at all."
+        _fault = None
 
         try:
             return self._handle_request(method, params)
         except APIError as e:
-            return str(e)
+            _fault = e
         except varintDecodeError as e:
             logger.error(e)
-            return "API Error 0026: Data contains a malformed varint. Some details: %s" % e
+            _fault = APIError(
+                26, 'Data contains a malformed varint. Some details: %s' % e)
         except Exception as e:
             logger.exception(e)
+            _fault = APIError(21, 'Unexpected API Failure - %s' % e)
+
+        if _fault:
+            if self.config.safeGet(
+                    'bitmessagesettings', 'apivariant') == 'legacy':
+                return str(_fault)
+            else:
+                raise _fault  # pylint: disable=raising-bad-type
+
+    def _listMethods(self):
+        """List all API commands"""
+        return self._handlers.keys()
 
-            return "API Error 0021: Unexpected API Failure - %s" % e
+    def _methodHelp(self, method):
+        return self._handlers[method].__doc__
diff --git a/src/bitmessagemain.py b/src/bitmessagemain.py
--- a/src/bitmessagemain.py
+++ b/src/bitmessagemain.py
@@ -188,6 +188,8 @@ def start(self):
                     'bitmessagesettings', 'apiusername', 'username')
                 config.set(
                     'bitmessagesettings', 'apipassword', 'password')
+                config.set(
+                    'bitmessagesettings', 'apivariant', 'legacy')
                 config.set(
                     'bitmessagesettings', 'apinotifypath',
                     os.path.join(app_dir, 'tests', 'apinotify_handler.py')


Test patch:
diff --git a/src/tests/test_api.py b/src/tests/test_api.py
--- a/src/tests/test_api.py
+++ b/src/tests/test_api.py
@@ -41,6 +41,19 @@ def test_shutdown(self):
                 '%s has not stopped in 10 sec' % ' '.join(self._process_cmd))
 
 
+# TODO: uncovered API commands
+# getAllInboxMessages
+# getAllInboxMessageIds
+# getInboxMessageById
+# getInboxMessagesByReceiver
+# trashMessage
+# trashInboxMessage
+# addSubscription
+# disseminatePreEncryptedMsg
+# disseminatePubkey
+# getMessageDataByDestinationHash
+# statusBar
+
 class TestAPI(TestAPIProto):
     """Main API test case"""
     _seed = base64.encodestring(
@@ -53,11 +66,8 @@ def _add_random_address(self, label):
     def test_user_password(self):
         """Trying to connect with wrong username/password"""
         api_wrong = xmlrpclib.ServerProxy("http://test:wrong@127.0.0.1:8442/")
-        self.assertEqual(
-            api_wrong.clientStatus(),
-            'RPC Username or password incorrect or HTTP header lacks'
-            ' authentication at all.'
-        )
+        with self.assertRaises(xmlrpclib.ProtocolError):
+            api_wrong.clientStatus()
 
     def test_connection(self):
         """API command 'helloWorld'"""
@@ -101,24 +111,45 @@ def test_decode_address(self):
         self.assertEqual(result['streamNumber'], 1)
 
     def test_create_deterministic_addresses(self):
-        """API command 'getDeterministicAddress': with various params"""
+        """Test creation of deterministic addresses"""
         self.assertEqual(
             self.api.getDeterministicAddress(self._seed, 4, 1),
-            'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK'
-        )
+            'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK')
         self.assertEqual(
             self.api.getDeterministicAddress(self._seed, 3, 1),
-            'BM-2DBPTgeSawWYZceFD69AbDT5q4iUWtj1ZN'
-        )
+            'BM-2DBPTgeSawWYZceFD69AbDT5q4iUWtj1ZN')
         self.assertRegexpMatches(
             self.api.getDeterministicAddress(self._seed, 2, 1),
-            r'^API Error 0002:'
-        )
+            r'^API Error 0002:')
+
         # This is here until the streams will be implemented
         self.assertRegexpMatches(
             self.api.getDeterministicAddress(self._seed, 3, 2),
-            r'API Error 0003:'
-        )
+            r'API Error 0003:')
+        self.assertRegexpMatches(
+            self.api.createDeterministicAddresses(self._seed, 1, 4, 2),
+            r'API Error 0003:')
+
+        self.assertRegexpMatches(
+            self.api.createDeterministicAddresses('', 1),
+            r'API Error 0001:')
+        self.assertRegexpMatches(
+            self.api.createDeterministicAddresses(self._seed, 1, 2),
+            r'API Error 0002:')
+        self.assertRegexpMatches(
+            self.api.createDeterministicAddresses(self._seed, 0),
+            r'API Error 0004:')
+        self.assertRegexpMatches(
+            self.api.createDeterministicAddresses(self._seed, 1000),
+            r'API Error 0005:')
+
+        addresses = json.loads(
+            self.api.createDeterministicAddresses(self._seed, 2, 4)
+        )['addresses']
+        self.assertEqual(len(addresses), 2)
+        self.assertEqual(addresses[0], 'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK')
+        for addr in addresses:
+            self.assertEqual(self.api.deleteAddress(addr), 'success')
 
     def test_create_random_address(self):
         """API command 'createRandomAddress': basic BM-address validation"""
@@ -157,23 +188,136 @@ def test_addressbook(self):
             []
         )
 
+    def test_subscriptions(self):
+        """Testing the API commands related to subscriptions"""
+        for s in json.loads(self.api.listSubscriptions())['subscriptions']:
+            # special address, added when sqlThread starts
+            if s['address'] == 'BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw':
+                self.assertEqual(
+                    base64.decodestring(s['label']),
+                    'Bitmessage new releases/announcements')
+                self.assertTrue(s['enabled'])
+                break
+        else:
+            self.fail(
+                'Could not find Bitmessage new releases/announcements'
+                ' in subscriptions')
+        self.assertEqual(
+            self.api.deleteSubscription('BM-GtovgYdgs7qXPkoYaRgrLFuFKz1SFpsw'),
+            'Deleted subscription if it existed.')
+        self.assertEqual(
+            json.loads(self.api.listSubscriptions())['subscriptions'], [])
+
+    def test_send(self):
+        """Test message sending"""
+        # self.api.createDeterministicAddresses(self._seed, 1, 4)
+        addr = self._add_random_address('random_2')
+        msg = base64.encodestring('test message')
+        msg_subject = base64.encodestring('test_subject')
+        ackdata = self.api.sendMessage(
+            'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK', addr, msg_subject, msg)
+        try:
+            # Check ackdata and message status
+            int(ackdata, 16)
+            status = self.api.getStatus(ackdata)
+            if status == 'notfound':
+                raise KeyError
+            self.assertIn(
+                status, (
+                    'msgqueued', 'awaitingpubkey', 'msgsent', 'ackreceived',
+                    'doingpubkeypow', 'doingmsgpow', 'msgsentnoackexpected'
+                ))
+            # Find the message in sent
+            for m in json.loads(
+                    self.api.getSentMessagesByAddress(addr))['sentMessages']:
+                if m['ackData'] == ackdata:
+                    sent_msg = m['message']
+                    break
+            else:
+                raise KeyError
+            # Find the message in inbox
+            # for m in json.loads(
+            #     self.api.getInboxMessagesByReceiver(
+            #         'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK'))['inboxMessages']:
+            #     if m['subject'] == msg_subject:
+            #         inbox_msg = m['message']
+            #         break
+        except ValueError:
+            self.fail('sendMessage returned error or ackData is not hex')
+        except KeyError:
+            self.fail('Could not find sent message in sent messages')
+        else:
+            # Check found message
+            try:
+                self.assertEqual(sent_msg, msg.strip())
+            except UnboundLocalError:
+                self.fail('Could not find sent message in sent messages')
+            # self.assertEqual(inbox_msg, msg.strip())
+            self.assertEqual(json.loads(
+                self.api.getSentMessageByAckData(ackdata)
+            )['sentMessage'][0]['message'], sent_msg)
+            # Trash the message
+            self.assertEqual(
+                self.api.trashSentMessageByAckData(ackdata),
+                'Trashed sent message (assuming message existed).')
+            # Empty trash
+            self.assertEqual(self.api.deleteAndVacuum(), 'done')
+            # The message should disappear
+            self.assertIsNone(json.loads(
+                self.api.getSentMessageByAckData(ackdata)))
+        finally:
+            self.assertEqual(self.api.deleteAddress(addr), 'success')
+
     def test_send_broadcast(self):
-        """API command 'sendBroadcast': ensure it returns ackData"""
+        """Test broadcast sending"""
         addr = self._add_random_address('random_2')
-        ack = self.api.sendBroadcast(
-            addr, base64.encodestring('test_subject'),
-            base64.encodestring('test message')
-        )
+        msg = base64.encodestring('test broadcast')
+        ackdata = self.api.sendBroadcast(
+            addr, base64.encodestring('test_subject'), msg)
         try:
-            int(ack, 16)
+            int(ackdata, 16)
+            status = self.api.getStatus(ackdata)
+            if status == 'notfound':
+                raise KeyError
+            self.assertIn(
+                status, ('broadcastqueued', 'broadcastsent', 'doingmsgpow'))
+            # Find the message and its ID in sent
+            for m in json.loads(self.api.getAllSentMessages())['sentMessages']:
+                if m['ackData'] == ackdata:
+                    sent_msg = m['message']
+                    sent_msgid = m['msgid']
+                    break
+            else:
+                raise KeyError
         except ValueError:
             self.fail('sendBroadcast returned error or ackData is not hex')
+        except KeyError:
+            self.fail('Could not find sent broadcast in sent messages')
+        else:
+            # Check found message and its ID
+            try:
+                self.assertEqual(sent_msg, msg.strip())
+            except UnboundLocalError:
+                self.fail('Could not find sent message in sent messages')
+            self.assertEqual(json.loads(
+                self.api.getSentMessageById(sent_msgid)
+            )['sentMessage'][0]['message'], sent_msg)
+            self.assertIn(
+                {'msgid': sent_msgid}, json.loads(
+                    self.api.getAllSentMessageIds())['sentMessageIds'])
+            # Trash the message by ID
+            self.assertEqual(
+                self.api.trashSentMessage(sent_msgid),
+                'Trashed sent message (assuming message existed).')
+            self.assertEqual(self.api.deleteAndVacuum(), 'done')
+            self.assertIsNone(json.loads(
+                self.api.getSentMessageById(sent_msgid)))
         finally:
             self.assertEqual(self.api.deleteAddress(addr), 'success')
 
     def test_chan(self):
         """Testing chan creation/joining"""
-        # Cheate chan with known address
+        # Create chan with known address
         self.assertEqual(
             self.api.createChan(self._seed),
             'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK'
@@ -185,8 +329,8 @@ def test_chan(self):
         )
         # Join chan with addresses of version 3 or 4
         for addr in (
-                'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK',
-                'BM-2DBPTgeSawWYZceFD69AbDT5q4iUWtj1ZN'
+            'BM-2cWzSnwjJ7yRP3nLEWUV5LisTZyREWSzUK',
+            'BM-2DBPTgeSawWYZceFD69AbDT5q4iUWtj1ZN'
         ):
             self.assertEqual(self.api.joinChan(self._seed, addr), 'success')
             self.assertEqual(self.api.leaveChan(addr), 'success')