Esempio n. 1
0
class Client(object):
    """Class whose methods perform various SMTP commands on a given socket. The
    return value from each command is a |Reply| object. Commands that are
    pipelined may not have their replies filled until subsequent commands are
    executed.

    The ``extensions`` attribute contains the |Extensions| object that are made
    available by the server.

    :param socket: Connected socket to use for the client.
    :param tls_wrapper: Optional function that takes a socket and the ``tls``
                        dictionary, creates a new encrypted socket, performs
                        the TLS handshake, and returns it. The default uses
                        :class:`~gevent.ssl.SSLSocket`.

    """

    def __init__(self, socket, tls_wrapper=None):
        self.io = IO(socket, tls_wrapper)
        self.reply_queue = []

        #: :class:`Extensions` object of the client, populated once the EHLO
        #: command returns its response.
        self.extensions = Extensions()

    def _flush_pipeline(self):
        self.io.flush_send()
        while True:
            try:
                reply = self.reply_queue.pop(0)
            except IndexError:
                return None
            reply.recv(self.io)

    def has_reply_waiting(self):
        """Checks if the underlying socket has data waiting to be received,
        which means a reply is waiting to be read.

        :rtype: True or False

        """
        sock_fd = self.io.socket.fileno()
        try:
            wait_read(sock_fd, 0.1, Timeout())
        except Timeout:
            return False
        else:
            return True

    def get_reply(self, command='[TIMEOUT]'):
        """Gets a reply from the server that was not triggered by the client
        sending a command. This is most useful for receiving timeout
        notifications.

        :param command: Optional command name to associate with the reply.
        :returns: |Reply| object populated with the response.

        """
        reply = Reply(command=command)
        self.reply_queue.append(reply)

        self._flush_pipeline()

        return reply

    def get_banner(self):
        """Waits for the SMTP banner at the beginning of the connection.

        :returns: |Reply| object populated with the response.

        """
        banner = Reply(command='[BANNER]')
        banner.enhanced_status_code = False
        self.reply_queue.append(banner)

        self._flush_pipeline()

        return banner

    def custom_command(self, command, arg=None):
        """Sends a custom command to the SMTP server and waits for the reply.

        :param command: The command to send.
        :param arg: Optonal argument string to send with the command.
        :returns: |Reply| object populated with the response.

        """
        custom = Reply(command=command.upper())
        self.reply_queue.append(custom)

        if arg:
            command = ' '.join((command, arg))
        self.io.send_command(command)

        self._flush_pipeline()

        return custom

    def ehlo(self, ehlo_as):
        """Sends the EHLO command with identifier string and waits for the
        reply. When this method returns, the ``self.extensions`` object will
        also be populated with the SMTP extensions the server supports.

        :param ehlo_as: EHLO identifier string, usually an FQDN.
        :returns: |Reply| object populated with the response.

        """
        ehlo = Reply(command='EHLO')
        ehlo.enhanced_status_code = False
        self.reply_queue.append(ehlo)

        command = 'EHLO '+ehlo_as
        self.io.send_command(command)

        self._flush_pipeline()
        if ehlo.code == '250':
            self.extensions.reset()
            ehlo.message = self.extensions.parse_string(ehlo.message)

        return ehlo

    def helo(self, helo_as):
        """Sends the HELO command with identifier string and waits for the
        reply.

        :param helo_as: HELO identifier string, usually an FQDN.
        :returns: |Reply| object populated with the response.

        """
        helo = Reply(command='HELO')
        helo.enhanced_status_code = False
        self.reply_queue.append(helo)

        command = 'HELO '+helo_as
        self.io.send_command(command)

        self._flush_pipeline()

        return helo

    def encrypt(self, tls):
        """Encrypts the underlying socket with the information given by ``tls``.
        This call should only be used directly against servers that expect to be
        immediately encrypted. If encryption is negotiated with
        :meth:`starttls()` there is no need to call this method.

        :param tls: Dictionary of keyword arguments for
                    :class:`~gevent.ssl.SSLSocket`.

        """
        self.io.encrypt_socket(tls)

    def starttls(self, tls):
        """Sends the STARTTLS command with identifier string and waits for the
        reply. When the reply is received and the code is 220, the socket is
        encrypted with the parameters in ``tls``. This should be followed by a
        another call to :meth:`ehlo()`.

        :param tls: Dictionary of keyword arguments for
                    :class:`~gevent.ssl.SSLSocket`.
        :returns: |Reply| object populated with the response.

        """
        reply = self.custom_command('STARTTLS')
        if reply.code == '220':
            self.encrypt(tls)
        return reply

    def mailfrom(self, address, data_size=None):
        """Sends the MAIL command with the ``address`` and possibly the message
        size. The message size is sent if the server supports the SIZE
        extension. If the server does *not* support PIPELINING, the returned
        reply object is populated immediately.

        :param address: The sender address to send.
        :param data_size: Optional size of the message body.
        :returns: |Reply| object that will be populated with the response
                  once a non-pipelined command is called, or if the server does
                  not support PIPELINING.

        """
        mailfrom = Reply(command='MAIL')
        self.reply_queue.append(mailfrom)

        command = 'MAIL FROM:<{0}>'.format(address)
        if data_size is not None and 'SIZE' in self.extensions:
            command += ' SIZE='+str(data_size)
        self.io.send_command(command)

        if 'PIPELINING' not in self.extensions:
            self._flush_pipeline()

        return mailfrom

    def rcptto(self, address):
        """Sends the RCPT command with the ``address``. If the server
        does *not* support PIPELINING, the returned reply object is
        populated immediately.

        :param address: The sender address to send.
        :param data_size: Optional size of the message body.
        :returns: |Reply| object that will be populated with the response
                  once a non-pipelined command is called, or if the server does
                  not support PIPELINING.

        """
        rcptto = Reply(command='RCPT')
        self.reply_queue.append(rcptto)

        command = 'RCPT TO:<{0}>'.format(address)
        self.io.send_command(command)

        if 'PIPELINING' not in self.extensions:
            self._flush_pipeline()

        return rcptto

    def data(self):
        """Sends the DATA command and waits for the response. If the response
        from the server is a 354, the server is respecting message data and
        should be sent :meth:`send_data` or :meth:`send_empty_data`.

        :returns: |Reply| object populated with the response.

        """
        return self.custom_command('DATA')

    def send_data(self, *data):
        """Processes and sends message data. At the end of the message data,
        the client will send a line with a single ``.`` to indicate the end of
        the message.  If the server does *not* support PIPELINING, the returned
        reply object is populated immediately.

        :param data: The message data parts.
        :type data: string or unicode
        :returns: |Reply| object that will be populated with the response
                  once a non-pipelined command is called, or if the server does
                  not support PIPELINING.

        """
        send_data = Reply(command='[SEND_DATA]')
        self.reply_queue.append(send_data)

        data_sender = DataSender(*data)
        data_sender.send(self.io)

        if 'PIPELINING' not in self.extensions:
            self._flush_pipeline()

        return send_data

    def send_empty_data(self):
        """Sends a line with a single ``.`` to indicate an empty message. If
        the server does *not* support PIPELINING, the returned reply object is
        populated immediately.

        :param data: The message data.
        :type data: string or unicode
        :returns: |Reply| object that will be populated with the response
                  once a non-pipelined command is called, or if the server does
                  not support PIPELINING.

        """
        send_data = Reply(command='[SEND_DATA]')
        self.reply_queue.append(send_data)

        self.io.send_command('.')

        if 'PIPELINING' not in self.extensions:
            self._flush_pipeline()

        return send_data

    def rset(self):
        """Sends a RSET command and waits for the response. The intent of the
        RSET command is to reset any :meth:`mail` or :meth:`rcpt` commands that
        are pending.

        :returns: |Reply| object populated with the response.

        """
        return self.custom_command('RSET')

    def quit(self):
        """Sends the QUIT command and waits for the response. After the response
        is received (should be 221) the socket should be closed.

        :returns: |Reply| object populated with the response.

        """
        return self.custom_command('QUIT')