Exemple #1
0
    def _upgrade_connection(self):
        """
        Upgrade the connection if STARTTLS is supported.
        If it's not/ it fails and SSL is not required, do nothing. Otherwise,
        raise an exception.

        """
        # If STARTTLS is available, always use it -- irrespective of the
        # `self.ssl_required`. If it's not or it fails, use `self.ssl_required`
        # to determine whether to fail or continue with plaintext
        # authentication.
        self.connection.ehlo()
        if self.connection.has_extn('starttls'):
            try:
                self.connection.starttls()
            except ssl.SSLError as e:
                if not self.ssl_required:
                    log.warning(
                        'STARTTLS supported but failed for SSL NOT '
                        'required authentication',
                        exc_info=True)
                else:
                    msg = _transform_ssl_error(e.strerror)
                    raise SendMailException(msg, 503)
        elif self.ssl_required:
            raise SendMailException('Required SMTP STARTTLS not supported.',
                                    403)
Exemple #2
0
    def _handle_sending_exception(self, err):
        if isinstance(err, smtplib.SMTPServerDisconnected):
            raise SendMailException(
                'The server unexpectedly closed the connection', 503)

        elif isinstance(err, smtplib.SMTPRecipientsRefused):
            raise SendMailException('Sending to all recipients failed', 402)

        elif isinstance(err, smtplib.SMTPResponseException):
            # Distinguish between permanent failures due to message
            # content or recipients, and temporary failures for other reasons.
            # In particular, see https://support.google.com/a/answer/3726730
            if err.smtp_code == 550 and err.smtp_error.startswith('5.4.5'):
                message = 'Daily sending quota exceeded'
                http_code = 429
            elif (err.smtp_code == 552
                  and (err.smtp_error.startswith('5.2.3')
                       or err.smtp_error.startswith('5.3.4'))):
                message = 'Message too large'
                http_code = 402
            elif err.smtp_code == 552 and err.smtp_error.startswith('5.7.0'):
                message = 'Message content rejected for security reasons'
                http_code = 402
            else:
                message = 'Sending failed'
                http_code = 503

            server_error = '{} : {}'.format(err.smtp_code, err.smtp_error)
            raise SendMailException(message,
                                    http_code=http_code,
                                    server_error=server_error)
        else:
            raise SendMailException('Sending failed',
                                    http_code=503,
                                    server_error=str(err))
Exemple #3
0
def _send(account_id, draft_id, db_session):
    """Send the draft with id = `draft_id`."""
    account = db_session.query(Account).get(account_id)

    try:
        sendmail_client = get_sendmail_client(account)
    except SendMailException:
        log.error('Send Error', message="Failed to create sendmail client.",
                  account_id=account_id)
        raise
    try:
        draft = db_session.query(Message).filter(
            Message.id == draft_id).one()

    except NoResultFound:
        log.info('Send Error',
                 message='NoResultFound for draft_id {0}'.format(draft_id),
                 account_id=account_id)
        raise SendMailException('No draft with id {0}'.format(draft_id))

    except MultipleResultsFound:
        log.info('Send Error',
                 message='MultipleResultsFound for draft_id'
                         '{0}'.format(draft_id),
                 account_id=account_id)
        raise SendMailException('Multiple drafts with id {0}'.format(
            draft_id))

    if not draft.is_draft or draft.is_sent:
        return

    recipients = Recipients(draft.to_addr, draft.cc_addr, draft.bcc_addr)

    if not draft.is_reply:
        sendmail_client.send_new(db_session, draft, recipients)
    else:
        sendmail_client.send_reply(db_session, draft, recipients)

    if account.provider == 'icloud':
        # Special case because iCloud doesn't save
        # sent messages.
        schedule_action('save_sent_email', draft, draft.namespace.id,
                        db_session)

    # Update message
    draft.is_sent = True
    draft.is_draft = False
    draft.state = 'sent'

    # Update thread
    sent_tag = account.namespace.tags['sent']
    draft_tag = account.namespace.tags['drafts']
    draft.thread.apply_tag(sent_tag)
    # Remove the drafts tag from the thread if there are no more drafts.
    if not draft.thread.drafts:
        draft.thread.remove_tag(draft_tag)

    return draft
Exemple #4
0
 def _handle_sending_exception(self, err):
     self.log.error("Error sending", error=err, exc_info=True)
     if isinstance(err, smtplib.SMTPServerDisconnected):
         raise SendMailException(
             'The server unexpectedly closed the '
             'connection', 503)
     elif (isinstance(err, smtplib.SMTPDataError) and err.smtp_code == 550
           and err.smtp_error.startswith('5.4.5')):
         # Gmail-specific quota exceeded error.
         raise SendMailException('Daily sending quota exceeded', 429)
     elif isinstance(err, smtplib.SMTPRecipientsRefused):
         raise SendMailException('Sending to all recipients failed', 402)
     else:
         raise SendMailException('Sending failed: {}'.format(err), 503)
Exemple #5
0
 def smtp_oauth2(self):
     code, resp = self._try_xoauth2()
     # If auth failed, try to refresh the access token and try again.
     if code != SMTP_AUTH_SUCCESS:
         self._smtp_oauth2_try_refresh()
         code, resp = self._try_xoauth2()
         # Propagate known temporary authentication issues as such.
         if code in SMTP_TEMP_AUTH_FAIL_CODES and resp.startswith('4.7.0'):
             raise SendMailException('Temporary provider send throttling',
                                     429)
     if code != SMTP_AUTH_SUCCESS:
         raise SendMailException(
             'Could not authenticate with the SMTP server.', 403)
     self.log.info('SMTP Auth(OAuth2) success',
                   email_address=self.email_address)
Exemple #6
0
    def __init__(self, account_id):
        self.account_id = account_id
        self.log = get_logger()
        self.log.bind(account_id=account_id)

        with session_scope() as db_session:
            account = db_session.query(ImapAccount).get(self.account_id)

            self.email_address = account.email_address
            self.provider_name = account.provider
            self.sender_name = account.name
            self.smtp_endpoint = account.smtp_endpoint

            if account.sent_folder is None:
                # account has no detected sent folder - create one.
                sent_folder = Folder.find_or_create(db_session, account,
                                                    'sent', 'sent')
                account.sent_folder = sent_folder

            self.sent_folder = account.sent_folder.name

            self.auth_type = provider_info(self.provider_name,
                                           self.email_address)['auth']

            if self.auth_type == 'oauth2':
                try:
                    self.auth_token = account.access_token
                except OAuthError:
                    raise SendMailException('Error logging in.')
            else:
                assert self.auth_type == 'password'
                self.auth_token = account.password
Exemple #7
0
    def smtp_oauth2(self):
        code, resp = self._try_xoauth2()
        if code in SMTP_TEMP_AUTH_FAIL_CODES and resp.startswith('4.7.0'):
            # If we're getting 'too many login attempt errors', tell the client
            # they are being rate-limited.
            raise SendMailException('Temporary provider send throttling', 429)

        if code != SMTP_AUTH_SUCCESS:
            # If auth failed for any other reason, try to refresh the access
            # token and try again.
            self._smtp_oauth2_try_refresh()
            code, resp = self._try_xoauth2()
            if code != SMTP_AUTH_SUCCESS:
                raise SendMailException(
                    'Could not authenticate with the SMTP server.', 403)
        self.log.info('SMTP Auth(OAuth2) success', account_id=self.account_id)
Exemple #8
0
    def __init__(self, account):
        self.account_id = account.id
        self.log = get_logger()
        self.log.bind(account_id=account.id)
        if isinstance(account, GenericAccount):
            self.smtp_username = account.smtp_username
            self.ssl_required = account.ssl_required
        else:
            # Non-generic accounts have no smtp username, ssl_required
            self.smtp_username = account.email_address
            self.ssl_required = True
        self.email_address = account.email_address
        self.provider_name = account.provider
        self.sender_name = account.name
        self.smtp_endpoint = account.smtp_endpoint
        self.auth_type = provider_info(self.provider_name)['auth']

        if self.auth_type == 'oauth2':
            try:
                self.auth_token = token_manager.get_token(account)
            except OAuthError:
                raise SendMailException(
                    'Could not authenticate with the SMTP server.', 403)
        else:
            assert self.auth_type == 'password'
            if isinstance(account, GenericAccount):
                self.auth_token = account.smtp_password
            else:
                # non-generic accounts have no smtp password
                self.auth_token = account.password
Exemple #9
0
    def auth_connection(self):
        c = self.connection

        # Auth mechanisms supported by the server
        if not c.has_extn('auth'):
            raise SendMailException('Required SMTP AUTH not supported.')

        supported_types = c.esmtp_features['auth'].strip().split()

        # Auth mechanism needed for this account
        if AUTH_EXTNS.get(self.auth_type) not in supported_types:
            raise SendMailException(
                'Required SMTP Auth mechanism not supported.')

        auth_handler = self.auth_handlers.get(self.auth_type)
        auth_handler()
Exemple #10
0
    def smtp_password(self):
        c = self.connection

        try:
            c.login(self.smtp_username, self.auth_token)
        except smtplib.SMTPAuthenticationError as e:
            self.log.error("SMTP login refused", exc=e)
            raise SendMailException("Could not authenticate with the SMTP server.", 403)
        except smtplib.SMTPException as e:
            # Raised by smtplib if the server doesn't support the AUTH
            # extension or doesn't support any of the implemented mechanisms.
            # Shouldn't really happen normally.
            self.log.error("SMTP auth failed due to unsupported mechanism", exc=e)
            raise SendMailException(str(e), 403)

        self.log.info("SMTP Auth(Password) success")
Exemple #11
0
 def _connect(self, host, port):
     """ Connect, with error-handling """
     try:
         self.connection.connect(host, port)
     except socket.error as e:
         # 'Connection refused', SSL errors for non-TLS connections, etc.
         msg = _transform_ssl_error(e.strerror)
         raise SendMailException(msg, 503)
Exemple #12
0
 def sendmail(self, recipients, msg):
     try:
         return self.connection.sendmail(
             self.email_address, recipients, msg)
     except UnicodeEncodeError:
         self.log.error('Unicode error when trying to decode email',
                        logstash_tag='sendmail_encode_error',
                        email=self.email_address, recipients=recipients)
         raise SendMailException(
             'Invalid character in recipient address', 402)
Exemple #13
0
 def sendmail(self, recipients, msg):
     try:
         return self.connection.sendmail(self.email_address, recipients, msg)
     except UnicodeEncodeError:
         self.log.error(
             "Unicode error when trying to decode email",
             logstash_tag="sendmail_encode_error",
             account_id=self.account_id,
             recipients=recipients,
         )
         raise SendMailException("Invalid character in recipient address", 402)
Exemple #14
0
    def _upgrade_connection(self):
        """
        Upgrade the connection if STARTTLS is supported.
        If it's not/ it fails and SSL is not required, do nothing. Otherwise,
        raise an exception.

        """
        self.connection.ehlo()
        # Always use STARTTLS if we're using a non-SSL port.
        if self.connection.has_extn("starttls"):
            try:
                self.connection.starttls()
            except ssl.SSLError as e:
                log.warning(
                    "STARTTLS supported but failed.", exc_info=True,
                )
                msg = _transform_ssl_error(e.strerror)
                raise SendMailException(msg, 503)
        else:
            raise SendMailException("Required SMTP STARTTLS not supported.", 403)
Exemple #15
0
    def _handle_sending_exception(self, err):
        if isinstance(err, smtplib.SMTPServerDisconnected):
            raise SendMailException(
                "The server unexpectedly closed the connection", 503
            )

        elif isinstance(err, smtplib.SMTPRecipientsRefused):
            raise SendMailException("Sending to all recipients failed", 402)

        elif isinstance(err, smtplib.SMTPResponseException):
            # Distinguish between permanent failures due to message
            # content or recipients, and temporary failures for other reasons.
            # In particular, see https://support.google.com/a/answer/3726730

            message = "Sending failed"
            http_code = 503

            if err.smtp_code in SMTP_ERRORS:
                for stem in SMTP_ERRORS[err.smtp_code]:
                    if stem in err.smtp_error:
                        res = SMTP_ERRORS[err.smtp_code][stem]
                        http_code = res[0]
                        message = res[1]
                        break

            server_error = "{} : {}".format(err.smtp_code, err.smtp_error)

            self.log.error(
                "Sending failed",
                message=message,
                http_code=http_code,
                server_error=server_error,
            )

            raise SendMailException(
                message, http_code=http_code, server_error=server_error
            )
        else:
            raise SendMailException(
                "Sending failed", http_code=503, server_error=str(err)
            )
Exemple #16
0
def _send(account_id, draft_id, db_session):
    """Send the draft with id = `draft_id`."""
    account = db_session.query(Account).get(account_id)

    log = get_logger()
    sendmail_client = get_sendmail_client(account)
    try:
        draft = db_session.query(Message).filter(Message.id == draft_id).one()

    except NoResultFound:
        log.info('NoResultFound for draft_id {0}'.format(draft_id))
        raise SendMailException('No draft with id {0}'.format(draft_id))

    except MultipleResultsFound:
        log.info('MultipleResultsFound for draft_id {0}'.format(draft_id))
        raise SendMailException('Multiple drafts with id {0}'.format(draft_id))

    if not draft.is_draft or draft.is_sent:
        return

    recipients = Recipients(draft.to_addr, draft.cc_addr, draft.bcc_addr)
    if not draft.is_reply:
        sendmail_client.send_new(db_session, draft, recipients)
    else:
        sendmail_client.send_reply(db_session, draft, recipients)

    # Update message
    draft.is_sent = True
    draft.is_draft = False
    draft.state = 'sent'

    # Update thread
    sent_tag = account.namespace.tags['sent']
    draft_tag = account.namespace.tags['drafts']
    draft.thread.apply_tag(sent_tag)
    # Remove the drafts tag from the thread if there are no more drafts.
    if not draft.thread.drafts:
        draft.thread.remove_tag(draft_tag)

    return draft
Exemple #17
0
 def _connect(self, host, port):
     """ Connect, with error-handling """
     try:
         self.connection.connect(host, port)
     except socket.error as e:
         # clean up errors like:
         # _ssl.c:510: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
         if e.strerror.endswith('certificate verify failed'):
             msg = 'SSL certificate verify failed'
         else:
             # 'Connection refused', etc.
             msg = e.strerror
         raise SendMailException(msg, 503)
Exemple #18
0
    def setup(self):
        connection = self.connection

        # Put the SMTP connection in TLS mode
        connection.ehlo()

        if not connection.has_extn('starttls'):
            raise SendMailException('Required SMTP STARTTLS not supported.')

        connection.starttls()
        connection.ehlo()

        # Auth the connection
        self.auth_connection()
Exemple #19
0
    def setup(self):
        host, port = self.smtp_endpoint
        if port in (SMTP_OVER_SSL_PORT, SMTP_OVER_SSL_TEST_PORT):
            self.connection = SMTP_SSL_VerifyCerts(timeout=SMTP_TIMEOUT)
            self._connect(host, port)
        else:
            self.connection = SMTP_VerifyCerts(timeout=SMTP_TIMEOUT)
            self._connect(host, port)
            # Put the SMTP connection in TLS mode
            self.connection.ehlo()
            if not self.connection.has_extn('starttls'):
                raise SendMailException(
                    'Required SMTP STARTTLS not '
                    'supported.', 403)
            try:
                self.connection.starttls()
            except ssl.SSLError as e:
                msg = _transform_ssl_error(e.strerror)
                raise SendMailException(msg, 503)

        # Auth the connection
        self.connection.ehlo()
        auth_handler = self.auth_handlers.get(self.auth_type)
        auth_handler()
Exemple #20
0
    def setup(self):
        host, port = self.smtp_endpoint
        if port == SMTP_OVER_SSL_PORT:
            self.connection = smtplib.SMTP_SSL()
            self.connection.connect(host, port)
        else:
            self.connection = smtplib.SMTP()
            self.connection.connect(host, port)
            # Put the SMTP connection in TLS mode
            self.connection.ehlo()
            if not self.connection.has_extn('starttls'):
                raise SendMailException('Required SMTP STARTTLS not '
                                        'supported.')
            self.connection.starttls()

        # Auth the connection
        self.connection.ehlo()
        self.auth_connection()
    def setup(self):
        host, port = self.smtp_endpoint
        if port in (SMTP_OVER_SSL_PORT, SMTP_OVER_SSL_TEST_PORT):
            self.connection = SMTP_SSL_VerifyCerts(timeout=SMTP_TIMEOUT)
            self._connect(host, port)
        else:
            self.connection = SMTP_VerifyCerts(timeout=SMTP_TIMEOUT)
            self._connect(host, port)
            # Put the SMTP connection in TLS mode
            self.connection.ehlo()
            if not self.connection.has_extn('starttls'):
                raise SendMailException(
                    'Required SMTP STARTTLS not '
                    'supported.', 403)
            self.connection.starttls()

        # Auth the connection
        self.connection.ehlo()
        self.auth_connection()
Exemple #22
0
    def _send(self, recipients, msg):
        """ Send the email message over the network. """
        try:
            with self._get_connection() as smtpconn:
                failures = smtpconn.sendmail(recipients, msg)
        except smtplib.SMTPException as err:
            self._handle_sending_exception(err)
        if failures:
            # At least one recipient was rejected by the server.
            raise SendMailException(
                'Sending to at least one recipent '
                'failed',
                402,
                failures=failures)

        # Sent to all successfully
        self.log.info('Sending successful',
                      sender=self.email_address,
                      recipients=recipients)
Exemple #23
0
    def __init__(self, account):
        self.account_id = account.id
        self.log = get_logger()
        self.log.bind(account_id=account.id)
        self.email_address = account.email_address
        self.provider_name = account.provider
        self.sender_name = account.name
        self.smtp_endpoint = account.smtp_endpoint
        self.auth_type = provider_info(self.provider_name,
                                       self.email_address)['auth']

        if self.auth_type == 'oauth2':
            try:
                self.auth_token = token_manager.get_token(account)
            except OAuthError:
                raise SendMailException(
                    'Could not authenticate with the SMTP server.', 403)
        else:
            assert self.auth_type == 'password'
            self.auth_token = account.password
Exemple #24
0
    def _send(self, recipients, msg):
        """Send the email message. Retries up to SMTP_MAX_RETRIES times if the
        message couldn't be submitted to any recipient.

        Parameters
        ----------
        recipients: list
            list of recipient email addresses.
        msg: string
            byte-encoded MIME message.

        Raises
        ------
        SendMailException
            If the message couldn't be sent to all recipients successfully.
        """
        last_error = None
        for _ in range(SMTP_MAX_RETRIES + 1):
            try:
                with self._get_connection() as smtpconn:
                    failures = smtpconn.sendmail(recipients, msg)
                    if not failures:
                        # Sending successful!
                        return
                    else:
                        # At least one recipient was rejected by the server,
                        # but at least one recipient got it. Don't retry; raise
                        # exception so that we fail to client.
                        raise SendMailException(
                            "Sending to at least one recipent failed",
                            http_code=200,
                            failures=failures,
                        )
            except smtplib.SMTPException as err:
                last_error = err
                self.log.error("Error sending", error=err, exc_info=True)

        assert last_error is not None
        self.log.error("Max retries reached; failing to client", error=last_error)
        self._handle_sending_exception(last_error)
Exemple #25
0
def send_rsvp(ical_data, event, body_text, status, account):
    from inbox.sendmail.base import SendMailException, get_sendmail_client

    ical_file = ical_data["cal"]
    ical_txt = ical_file.to_ical()
    rsvp_to = rsvp_recipient(event)

    if rsvp_to is None:
        raise SendMailException("Couldn't find an organizer to RSVP to.")

    sendmail_client = get_sendmail_client(account)

    msg = mime.create.multipart("mixed")

    body = mime.create.multipart("alternative")
    body.append(
        mime.create.text("plain", ""),
        mime.create.text("calendar;method=REPLY", ical_txt),
    )

    msg.append(body)

    msg.headers["Reply-To"] = account.email_address
    msg.headers["From"] = account.email_address
    msg.headers["To"] = rsvp_to

    assert status in ["yes", "no", "maybe"]

    if status == "yes":
        msg.headers["Subject"] = u"Accepted: {}".format(event.message.subject)
    elif status == "maybe":
        msg.headers["Subject"] = u"Tentatively accepted: {}".format(
            event.message.subject)
    elif status == "no":
        msg.headers["Subject"] = u"Declined: {}".format(event.message.subject)

    final_message = msg.to_string()

    sendmail_client = get_sendmail_client(account)
    sendmail_client.send_generated_email([rsvp_to], final_message)
Exemple #26
0
def send_rsvp(ical_data, event, body_text, status, account):
    from inbox.sendmail.base import get_sendmail_client, SendMailException

    ical_file = ical_data["cal"]
    ical_txt = ical_file.to_ical()
    rsvp_to = rsvp_recipient(event)

    if rsvp_to is None:
        raise SendMailException("Couldn't find an organizer to RSVP to.")

    sendmail_client = get_sendmail_client(account)

    msg = mime.create.multipart('mixed')

    body = mime.create.multipart('alternative')
    body.append(
        mime.create.text('plain', ''),
        mime.create.text('calendar;method=REPLY', ical_txt))

    msg.append(body)

    msg.headers['Reply-To'] = account.email_address
    msg.headers['From'] = account.email_address
    msg.headers['To'] = rsvp_to

    assert status in ['yes', 'no', 'maybe']

    if status == 'yes':
        msg.headers['Subject'] = u'Accepted: {}'.format(event.message.subject)
    elif status == 'maybe':
        msg.headers['Subject'] = u'Tentatively accepted: {}'.format(
            event.message.subject)
    elif status == 'no':
        msg.headers['Subject'] = u'Declined: {}'.format(event.message.subject)

    final_message = msg.to_string()

    sendmail_client = get_sendmail_client(account)
    sendmail_client.send_generated_email([rsvp_to], final_message)