Ejemplo n.º 1
0
    def send_email(self,
                   source,
                   to_addresses,
                   subject,
                   body,
                   html_body='',
                   reply_to_address=None):
        try:
            if isinstance(to_addresses, str):
                to_addresses = [to_addresses]

            reply_to_addresses = [reply_to_address] if reply_to_address else []

            body = {
                'Text': {'Data': body}
            }

            if html_body:
                body.update({
                    'Html': {'Data': html_body}
                })

            start_time = monotonic()
            response = self._client.send_email(
                Source=source,
                Destination={
                    'ToAddresses': [punycode_encode_email(addr) for addr in to_addresses],
                    'CcAddresses': [],
                    'BccAddresses': []
                },
                Message={
                    'Subject': {
                        'Data': subject,
                    },
                    'Body': body
                },
                ReplyToAddresses=[punycode_encode_email(addr) for addr in reply_to_addresses]
            )
        except botocore.exceptions.ClientError as e:
            self.statsd_client.incr("clients.ses.error")

            # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html
            if e.response['Error']['Code'] == 'InvalidParameterValue':
                raise InvalidEmailError('email: "{}" message: "{}"'.format(
                    to_addresses[0],
                    e.response['Error']['Message']
                ))
            else:
                self.statsd_client.incr("clients.ses.error")
                raise AwsSesClientException(str(e))
        except Exception as e:
            self.statsd_client.incr("clients.ses.error")
            raise AwsSesClientException(str(e))
        else:
            elapsed_time = monotonic() - start_time
            current_app.logger.info("AWS SES request finished in {}".format(elapsed_time))
            self.statsd_client.timing("clients.ses.request-time", elapsed_time)
            self.statsd_client.incr("clients.ses.success")
            return response['MessageId']
Ejemplo n.º 2
0
def test_should_technical_error_and_not_retry_if_invalid_email(
        sample_notification, mocker):
    mocker.patch('app.delivery.send_to_providers.send_email_to_provider',
                 side_effect=InvalidEmailError('bad email'))
    mocker.patch('app.celery.provider_tasks.deliver_email.retry')

    deliver_email(sample_notification.id)

    assert provider_tasks.deliver_email.retry.called is False
    assert sample_notification.status == 'technical-failure'
Ejemplo n.º 3
0
    def send_email(self,
                   source,
                   to_addresses,
                   subject,
                   body,
                   html_body,
                   reply_to_address=None,
                   attachments=[]):
        try:
            if isinstance(to_addresses, str):
                to_addresses = [to_addresses]

            # Sometimes the source is "Foo <*****@*****.**> vs just [email protected]"
            # TODO: Possibly revisit this to take in sender name and sender email address separately
            if "<" in source:
                source = source.split("<")[1].split(">")[0]

            recipients = [
                {"email": to_address} for to_address in to_addresses
            ]

            payload = {
                "subject": subject,
                "body": html_body,
                "recipients": recipients,
                "from_email": source,
                "click_tracking_enabled": False
            }

            start_time = monotonic()
            response = requests.post(
                self.govdelivery_url,
                json=payload,
                headers={
                    "X-AUTH-TOKEN": self.token
                }
            )
            response.raise_for_status()

        except HTTPError as e:
            self.statsd_client.incr("clients.govdelivery.error")
            if e.response.status_code == 422:
                raise InvalidEmailError(str(e))
            else:
                raise GovdeliveryClientException(str(e))
        except Exception as e:
            self.statsd_client.incr("clients.govdelivery.error")
            raise GovdeliveryClientException(str(e))
        else:
            elapsed_time = monotonic() - start_time
            current_app.logger.info("Govdelivery request finished in {}".format(elapsed_time))
            self.statsd_client.timing("clients.govdelivery.request-time", elapsed_time)
            self.statsd_client.incr("clients.govdelivery.success")
            return response.json()["id"]
Ejemplo n.º 4
0
 def _check_error_code(self, e, to_addresses):
     # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html
     if e.response['Error']['Code'] == 'InvalidParameterValue':
         self.statsd_client.incr("clients.ses.error.invalid-email")
         raise InvalidEmailError('message: "{}"'.format(
             e.response['Error']['Message']))
     elif (e.response['Error']['Code'] == 'Throttling'
           and e.response['Error']['Message']
           == 'Maximum sending rate exceeded.'):
         self.statsd_client.incr("clients.ses.error.throttling")
         raise AwsSesClientThrottlingSendRateException(str(e))
     else:
         self.statsd_client.incr("clients.ses.error")
         raise AwsSesClientException(str(e))
def test_should_technical_error_and_not_retry_if_invalid_email(
        sample_notification, mocker):
    mocker.patch(
        "app.delivery.send_to_providers.send_email_to_provider",
        side_effect=InvalidEmailError("bad email"),
    )
    mocker.patch("app.celery.provider_tasks.deliver_email.retry")
    logger = mocker.patch("app.celery.provider_tasks.current_app.logger.info")
    queued_callback = mocker.patch(
        "app.celery.provider_tasks._check_and_queue_callback_task")

    deliver_email(sample_notification.id)

    assert provider_tasks.deliver_email.retry.called is False
    assert sample_notification.status == "technical-failure"
    assert (call(
        f"Cannot send notification {sample_notification.id}, got an invalid email address: bad email."
    ) in logger.call_args_list)
    queued_callback.assert_called_once_with(sample_notification)
Ejemplo n.º 6
0
    def send_email(self,
                   source,
                   to_addresses,
                   subject,
                   body,
                   html_body='',
                   reply_to_address=None,
                   attachments=[]):
        try:
            if isinstance(to_addresses, str):
                to_addresses = [to_addresses]

            source = unidecode(source)

            reply_to_addresses = [reply_to_address] if reply_to_address else []

            multipart_content_subtype = 'alternative' if html_body else 'mixed'
            msg = MIMEMultipart(multipart_content_subtype)
            msg['Subject'] = subject
            msg['From'] = source
            msg['To'] = ",".join(
                [punycode_encode_email(addr) for addr in to_addresses])
            if reply_to_addresses != []:
                msg.add_header(
                    'reply-to', ",".join([
                        punycode_encode_email(addr)
                        for addr in reply_to_addresses
                    ]))
            part = MIMEText(body, 'plain')
            msg.attach(part)

            if html_body:
                part = MIMEText(html_body, 'html')
                msg.attach(part)

            for attachment in attachments or []:
                part = MIMEApplication(attachment["data"])
                part.add_header('Content-Disposition',
                                'attachment',
                                filename=attachment["name"])
                msg.attach(part)

            start_time = monotonic()
            response = self._client.send_raw_email(
                Source=source, RawMessage={'Data': msg.as_string()})
        except botocore.exceptions.ClientError as e:
            self.statsd_client.incr("clients.ses.error")

            # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html
            if e.response['Error']['Code'] == 'InvalidParameterValue':
                raise InvalidEmailError('email: "{}" message: "{}"'.format(
                    to_addresses[0], e.response['Error']['Message']))
            else:
                self.statsd_client.incr("clients.ses.error")
                raise AwsSesClientException(str(e))
        except Exception as e:
            self.statsd_client.incr("clients.ses.error")
            raise AwsSesClientException(str(e))
        else:
            elapsed_time = monotonic() - start_time
            current_app.logger.info(
                "AWS SES request finished in {}".format(elapsed_time))
            self.statsd_client.timing("clients.ses.request-time", elapsed_time)
            self.statsd_client.incr("clients.ses.success")
            return response['MessageId']
Ejemplo n.º 7
0
    def send_email(self,
                   source,
                   sending_domain,
                   to_addresses,
                   subject,
                   body,
                   html_body='',
                   reply_to_address=None,
                   attachments=[],
                   importance=None,
                   cc_addresses=None):
        try:
            aws_ses_owner_account = current_app.config['AWS_SES_OWNER_ACCOUNT']
            aws_ses_arn = 'arn:aws:ses:{}:{}:identity/{}'.format(
                current_app.config['AWS_SES_REGION'], aws_ses_owner_account,
                sending_domain) if aws_ses_owner_account else None
            if isinstance(to_addresses, str):
                to_addresses = [to_addresses]
            if isinstance(cc_addresses, str):
                cc_addresses = [cc_addresses]

            source = unicodedata.normalize('NFKD', source)
            friendly_name, match_string, from_address = source.partition("<")
            friendly_name = friendly_name.replace('"', '')
            h = Header(friendly_name, 'utf-8')
            encoded_friendly_name = h.encode()
            encoded_source = '{} {}{}'.format(encoded_friendly_name, match_string, from_address)

            reply_to_addresses = [reply_to_address] if reply_to_address else []

            multipart_content_subtype = 'alternative' if html_body else 'mixed'
            msg = MIMEMultipart(multipart_content_subtype)
            msg['Subject'] = subject
            msg['From'] = encoded_source
            msg['To'] = ",".join([punycode_encode_email(addr) for addr in to_addresses])
            if aws_ses_arn:
                msg.add_header('X-SES-SOURCE-ARN', aws_ses_arn)
                msg.add_header('X-SES-FROM-ARN', aws_ses_arn)
            if importance:
                msg.add_header('importance', importance)
            if cc_addresses:
                msg['CC'] = ",".join([punycode_encode_email(addr) for addr in cc_addresses])
            if reply_to_addresses != []:
                msg.add_header('reply-to', ",".join([punycode_encode_email(addr) for addr in reply_to_addresses]))
            part = MIMEText(body.encode(self.charset), 'plain', self.charset)
            msg.attach(part)

            if html_body:
                part = MIMEText(html_body.encode(self.charset), 'html', self.charset)
                msg.attach(part)

            for attachment in attachments or []:
                part = MIMEApplication(attachment["data"])
                part.add_header('Content-Disposition', 'attachment', filename=attachment["name"])
                msg.attach(part)

            start_time = monotonic()
            response = self._client.send_raw_email(
                Source=source,
                RawMessage={'Data': msg.as_string()}
            )
        except botocore.exceptions.ClientError as e:
            self.statsd_client.incr("clients.ses.error")

            # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html
            if e.response['Error']['Code'] == 'InvalidParameterValue':
                raise InvalidEmailError('email: "{}" message: "{}"'.format(
                    to_addresses[0],
                    e.response['Error']['Message']
                ))
            else:
                self.statsd_client.incr("clients.ses.error")
                raise AwsSesClientException(str(e))
        except Exception as e:
            self.statsd_client.incr("clients.ses.error")
            raise AwsSesClientException(str(e))
        else:
            elapsed_time = monotonic() - start_time
            current_app.logger.info("AWS SES request finished in {}".format(elapsed_time))
            self.statsd_client.timing("clients.ses.request-time", elapsed_time)
            self.statsd_client.incr("clients.ses.success")
            return response['MessageId']
Ejemplo n.º 8
0
    def send_email(
        self,
        source,
        to_addresses,
        subject,
        body,
        html_body="",
        reply_to_address=None,
        attachments=None,
    ):
        def create_mime_base(attachments, html):
            msg_type = "mixed" if attachments or (not attachments and not html) else "alternative"
            ret = MIMEMultipart(msg_type)
            ret["Subject"] = subject
            ret["From"] = source
            ret["To"] = ",".join([punycode_encode_email(addr) for addr in to_addresses])
            if reply_to_addresses:
                ret.add_header(
                    "reply-to",
                    ",".join([punycode_encode_email(addr) for addr in reply_to_addresses]),
                )
            return ret

        def attach_html(m, content):
            if content:
                parts = MIMEText(content, "html")
                m.attach(parts)

        attachments = attachments or []
        if isinstance(to_addresses, str):
            to_addresses = [to_addresses]
        source = unidecode(source)
        reply_to_addresses = [reply_to_address] if reply_to_address else []

        # - If sending a TXT email without attachments:
        #   => Multipart mixed
        #
        # - If sending a TXT + HTML email without attachments:
        #   => Multipart alternative
        #
        # - If sending a TXT + HTML email with attachments
        # =>  Multipart Mixed (enclosing)
        #       - Multipart alternative
        #         - TXT
        #         - HTML
        #       - Attachment(s)

        try:
            msg = create_mime_base(attachments, html_body)
            txt_part = MIMEText(body, "plain")

            if attachments and html_body:
                msg_alternative = MIMEMultipart("alternative")
                msg_alternative.attach(txt_part)
                attach_html(msg_alternative, html_body)
                msg.attach(msg_alternative)
            else:
                msg.attach(txt_part)
                attach_html(msg, html_body)

            for attachment in attachments:
                # See https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-raw.html#send-email-raw-mime
                attachment_part = MIMEApplication(attachment["data"])
                if attachment.get("mime_type"):
                    attachment_part.add_header("Content-Type", attachment["mime_type"], name=attachment["name"])
                attachment_part.add_header("Content-Disposition", "attachment", filename=attachment["name"])
                msg.attach(attachment_part)

            start_time = monotonic()
            response = self._client.send_raw_email(Source=source, RawMessage={"Data": msg.as_string()})
        except botocore.exceptions.ClientError as e:
            self.statsd_client.incr("clients.ses.error")

            # http://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html
            if e.response["Error"]["Code"] == "InvalidParameterValue":
                raise InvalidEmailError('email: "{}" message: "{}"'.format(to_addresses[0], e.response["Error"]["Message"]))
            else:
                self.statsd_client.incr("clients.ses.error")
                raise AwsSesClientException(str(e))
        except Exception as e:
            self.statsd_client.incr("clients.ses.error")
            raise AwsSesClientException(str(e))
        else:
            elapsed_time = monotonic() - start_time
            current_app.logger.info("AWS SES request finished in {}".format(elapsed_time))
            self.statsd_client.timing("clients.ses.request-time", elapsed_time)
            self.statsd_client.incr("clients.ses.success")
            return response["MessageId"]