Send LMTP DATA command and process the server response.

        The server response is stored as a list of dicts in
        <DspamClient>.results, keyed on the recipient name(s). Depending
        on the server return data, different formats are available:
        * LMTP mode    -- Dict containing 'accepted', a bool indicating
                          that the message was handed to the server.
        * Summary mode -- Dict containing 'username', 'result',
                          'classification', 'probability', 'confidence'
                          and 'signature'.
        * Stdout mode  -- Dict containing 'result' and 'message', the
                          complete message payload including added headers.

        The return data is always parsed and stored, independent of its format.
        If you requested a regular LMTP response, but the server
        responded with an DLMTP summary, the summary is still stored in
        <DspamClient>.results, and you will need to check the result format
        yourself and decide whether that was acceptable for your use case.
        This is due to the fact that it's possible to configure the server to
        return non LMTP responses, even when in LMTP mode (see dspam.conf:
        ServerParameters).

        Note: while processing response data in stdout mode, it's not possible
        to relate the returned messages to a specific recipient, when multiple
        recipients were specified in rcptto(). There is no guarantee
        that the message stored in <DspamClient>.results['foo'] actually
        belongs to the recipient 'foo'. If this relationship needs to be
        guaranteed, send each message with a single recipient in rcptto().

        args:
        message -- The full message payload to pass to the server.

def data(self, message):
        """
        Send LMTP DATA command and process the server response.

        The server response is stored as a list of dicts in
        <DspamClient>.results, keyed on the recipient name(s). Depending
        on the server return data, different formats are available:
        * LMTP mode    -- Dict containing 'accepted', a bool indicating
                          that the message was handed to the server.
        * Summary mode -- Dict containing 'username', 'result',
                          'classification', 'probability', 'confidence'
                          and 'signature'.
        * Stdout mode  -- Dict containing 'result' and 'message', the
                          complete message payload including added headers.

        The return data is always parsed and stored, independent of its format.
        If you requested a regular LMTP response, but the server
        responded with an DLMTP summary, the summary is still stored in
        <DspamClient>.results, and you will need to check the result format
        yourself and decide whether that was acceptable for your use case.
        This is due to the fact that it's possible to configure the server to
        return non LMTP responses, even when in LMTP mode (see dspam.conf:
        ServerParameters).

        Note: while processing response data in stdout mode, it's not possible
        to relate the returned messages to a specific recipient, when multiple
        recipients were specified in rcptto(). There is no guarantee
        that the message stored in <DspamClient>.results['foo'] actually
        belongs to the recipient 'foo'. If this relationship needs to be
        guaranteed, send each message with a single recipient in rcptto().

        args:
        message -- The full message payload to pass to the server.

        """
        self._send('DATA\r\n')
        resp = self._read()
        if not resp.startswith('354'):
            raise DspamClientError(
                'Unexpected server response at DATA: ' + resp)

        # Send message payload
        for line in message.split('\n'):
            if line == '.':
                # Dot stuffing
                line = '..'
            self._send(line)

        # Send end-of-data
        self._send('.\r\n')

        # Depending on server configuration, several responses are possible:
        # * Standard LMTP response code, once for each recipient:
        #   250 2.6.0 <bar> Message accepted for delivery
        # * Summary response (--deliver=summary), once for each recipient:
        #   X-DSPAM-Result: bar; result="Spam"; class="Spam"; \
        #     probability=1.0000; confidence=0.85; \
        #     signature=50c50c0f315636261418125
        #   (after the last summary line, a single dot is sent)
        # * Stdout response (--delivery=stdout), once for each recipient:
        #   X-Daemon-Classification: INNOCENT
        #   <complete mail body>
        #
        # Note that when an unknown recipient is passed in, DSPAM will simply
        #   deliver the message (dspam.conf: (Un)TrustedDeliveryAgent,
        #   DeliveryHost) unaltered and unfiltered. The response for unknown
        #   recipients will still be something indicating 'accepted'.

        peek = self._peek(24)
        if peek.startswith('250'):
            # Response is LTMP formatted
            regex = re.compile('250 \d\.\d\.\d <([^>]+)>')
            finished = False
            while not finished:
                resp = self._read()
                match = regex.match(resp)
                if not match:
                    raise DspamClientError(
                        'Unexpected server response at END-OF-DATA: ' + resp)
                rcpt = match.group(1)
                self.results[rcpt] = {'accepted': True}
                logger.debug(
                    'Message accepted for recipient {} in LMTP mode'.format(
                        rcpt))
                if len(self.results) == len(self._recipients):
                    finished = True

        elif peek.startswith('X-DSPAM-Result:'):
            # Response is in summary format
            regex = re.compile('X-DSPAM-Result: ([^;]+); result="(\w+)"; '
                               'class="(\w+)"; probability=([\d\.]+); '
                               'confidence=([\d\.]+); signature=([\w,/]+)')
            finished = False
            while not finished:
                resp = self._read()
                match = regex.match(resp)
                if not match:
                    raise DspamClientError(
                        'Unexpected server response at END-OF-DATA: ' + resp)
                rcpt = match.group(1)

                # map results to their DSPAM classification result names
                fields = ('user', 'result', 'class',
                          'probability', 'confidence', 'signature')
                self.results[rcpt] = dict(zip(fields, match.groups()))
                if self.results[rcpt]['signature'] == 'N/A':
                    del(self.results[rcpt]['signature'])

                logger.debug(
                    'Message handled for recipient {} in DLMTP summary mode, '
                    'result is {}'.format(rcpt, match.group(2)))
                if len(self.results) == len(self._recipients):
                    # we received responses for all accepted recipients
                    finished = True
            # read final dot
            resp = self._read()
            if resp != '.':
                raise DspamClientError(
                    'Unexpected server response at END-OF-DATA: ' + resp)

        elif peek.startswith('X-Daemon-Classification:'):
            # Response is in stdout format
            finished = False
            message = ''
            while not finished:
                resp = self._read()
                if resp.startswith('X-Daemon-Classification:'):
                    if message != '':
                        # A new message body starts, store the previous one
                        rcpt = self._recipients.pop(0)
                        self.results[rcpt] = {
                            'result': result,
                            'message': message
                        }
                        logger.debug(
                            'Message handled for recipient {} in DLMTP '
                            'stdout mode, result is {}, message body '
                            'is {} chars'.format(rcpt, result, len(message)))
                        message = ''
                    # Remember next result
                    result = resp[25:]

                elif resp == '.':
                    # A single dot can signal end-of-data, or might be just
                    #   regular mail data.
                    self._socket.setblocking(False)
                    try:
                        # If _peek() succeeds, we did not reach end-of-data yet
                        #   so it was message content.
                        peek = self._peek(1)
                        message = message + '\r\n' + resp
                    except socket.error:
                        # reached end-of-data, store message and finish
                        finished = True
                        rcpt = self._recipients.pop(0)
                        # strip final newline
                        message = message[0:-2]
                        self.results[rcpt] = {
                            'result': result,
                            'message': message
                        }
                        logger.debug(
                            'Message accepted for recipient {} in DLMTP '
                            'stdout mode, result is {}, message body '
                            'is {} chars'.format(rcpt, result, len(message)))

                    self._socket.setblocking(True)

                else:
                    # regular message data
                    if message == '':
                        message = resp
                    else:
                        message = message + '\r\n' + resp

        else:
            raise DspamClientError(
                'Unexpected server response at END-OF-DATA: ' + resp)