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)
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))
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
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)
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)
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
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)
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
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()
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")
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)
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)
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)
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)
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) )
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
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)
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()
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()
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()
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)
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
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)
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)
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)