diff --git a/.env.dev.example b/.env.dev.example index ad6128a2..8f7f1ad6 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -14,11 +14,6 @@ TWILIO_VERIFY_SERVICE_SID= TWILIO_AUTH_TOKEN= TWILIO_NUMBER= -SENDGRID_SECRET_KEY= -SENDGRID_INBOUND_EMAIL_DOMAIN= -SENDGRID_API_KEY= -SENDGRID_FROM_EMAIL= - DJANGO_SETTINGS_MODULE=settings.dev SECRET_KEY=jkashdkjashdkjh BASE_URL=http://localhost:8080 diff --git a/docs/sources/open-source/_index.md b/docs/sources/open-source/_index.md index b2e6c1df..fdc3a1a8 100644 --- a/docs/sources/open-source/_index.md +++ b/docs/sources/open-source/_index.md @@ -187,3 +187,14 @@ Grafana OnCall supports Twilio SMS and phone call notifications delivery. If you 1. Set `GRAFANA_CLOUD_NOTIFICATIONS_ENABLED` as **False** to ensure the Grafana OSS <-> Cloud connector is disabled. 1. From your **OnCall** environment, select **Env Variables** and configure all variables starting with `TWILIO_`. + +## Email Setup +Grafana OnCall is capable of sending emails using SMTP as a user notification step. To setup email notifications, populate the following env variables with your SMTP server credentials: + +- `EMAIL_HOST` - SMTP server host +- `EMAIL_HOST_USER` - SMTP server user +- `EMAIL_HOST_PASSWORD` - SMTP server password +- `EMAIL_PORT` (default is `587`) - SMTP server port +- `EMAIL_USE_TLS` (default is `True`) - to enable/disable TLS + +After enabling the email integration, it will be possible to use the `Notify by email` notification step in user settings. diff --git a/engine/apps/alerts/incident_appearance/renderers/email_renderer.py b/engine/apps/alerts/incident_appearance/renderers/email_renderer.py deleted file mode 100644 index eb18e190..00000000 --- a/engine/apps/alerts/incident_appearance/renderers/email_renderer.py +++ /dev/null @@ -1,42 +0,0 @@ -from django.template.loader import render_to_string - -from apps.alerts.incident_appearance.renderers.base_renderer import AlertBaseRenderer, AlertGroupBaseRenderer -from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_TITLE -from apps.alerts.incident_appearance.templaters import AlertEmailTemplater -from common.utils import str_or_backup - - -class AlertEmailRenderer(AlertBaseRenderer): - @property - def templater_class(self): - return AlertEmailTemplater - - -class AlertGroupEmailRenderer(AlertGroupBaseRenderer): - @property - def alert_renderer_class(self): - return AlertEmailRenderer - - def render(self, limit_notification=False): - subject = "You are invited to check an incident from Grafana OnCall" - templated_alert = self.alert_renderer.templated_alert - - title_fallback = ( - f"#{self.alert_group.inside_organization_number} " - f"{DEFAULT_BACKUP_TITLE} via {self.alert_group.channel.verbal_name}" - ) - - content = render_to_string( - "email_notification.html", - { - "url": self.alert_group.slack_permalink or self.alert_group.web_link, - "title": str_or_backup(templated_alert.title, title_fallback), - "message": str_or_backup(templated_alert.message, ""), # not render message it all if smth go wrong - "amixr_team": self.alert_group.channel.organization, - "alert_channel": self.alert_group.channel.short_name, - "limit_notification": limit_notification, - "emails_left": self.alert_group.channel.organization.emails_left, - }, - ) - - return subject, content diff --git a/engine/apps/alerts/incident_appearance/templaters/__init__.py b/engine/apps/alerts/incident_appearance/templaters/__init__.py index c34fc370..bff515d4 100644 --- a/engine/apps/alerts/incident_appearance/templaters/__init__.py +++ b/engine/apps/alerts/incident_appearance/templaters/__init__.py @@ -1,6 +1,5 @@ from .alert_templater import TemplateLoader # noqa: F401 from .classic_markdown_templater import AlertClassicMarkdownTemplater # noqa: F401 -from .email_templater import AlertEmailTemplater # noqa: F401 from .phone_call_templater import AlertPhoneCallTemplater # noqa: F401 from .slack_templater import AlertSlackTemplater # noqa: F401 from .sms_templater import AlertSmsTemplater # noqa: F401 diff --git a/engine/apps/alerts/incident_appearance/templaters/email_templater.py b/engine/apps/alerts/incident_appearance/templaters/email_templater.py deleted file mode 100644 index 48870848..00000000 --- a/engine/apps/alerts/incident_appearance/templaters/email_templater.py +++ /dev/null @@ -1,18 +0,0 @@ -from apps.alerts.incident_appearance.templaters.alert_templater import AlertTemplater - - -class AlertEmailTemplater(AlertTemplater): - RENDER_FOR_EMAIL = "email" - - def _render_for(self): - return self.RENDER_FOR_EMAIL - - def _postformat(self, templated_alert): - templated_alert.title = self._slack_format_for_email(templated_alert.title) - templated_alert.message = self._slack_format_for_email(templated_alert.message) - return templated_alert - - def _slack_format_for_email(self, data): - sf = self.slack_formatter - sf.hyperlink_mention_format = "{title} - {url}" - return sf.format(data) diff --git a/engine/apps/alerts/incident_log_builder/incident_log_builder.py b/engine/apps/alerts/incident_log_builder/incident_log_builder.py index ca5ae047..11285ad1 100644 --- a/engine/apps/alerts/incident_log_builder/incident_log_builder.py +++ b/engine/apps/alerts/incident_log_builder/incident_log_builder.py @@ -589,9 +589,6 @@ class IncidentLogBuilder: result += f"call {user_verbal} by phone" elif notification_policy.notify_by == UserNotificationPolicy.NotificationChannel.TELEGRAM: result += f"send telegram message to {user_verbal}" - # TODO: restore email notifications - # elif notification_policy.notify_by == UserNotificationPolicy.NotificationChannel.EMAIL: - # result += f"send email to {user_verbal}" else: try: backend_id = UserNotificationPolicy.NotificationChannel(notification_policy.notify_by).name diff --git a/engine/apps/alerts/integration_options_mixin.py b/engine/apps/alerts/integration_options_mixin.py index a0a81bab..a747d899 100644 --- a/engine/apps/alerts/integration_options_mixin.py +++ b/engine/apps/alerts/integration_options_mixin.py @@ -59,8 +59,6 @@ class IntegrationOptionsMixin: "web_title", "web_message", "web_image_url", - "email_title", - "email_message", "sms_title", "phone_call_title", "telegram_title", diff --git a/engine/apps/alerts/models/alert_receive_channel.py b/engine/apps/alerts/models/alert_receive_channel.py index 60704773..2666ade7 100644 --- a/engine/apps/alerts/models/alert_receive_channel.py +++ b/engine/apps/alerts/models/alert_receive_channel.py @@ -21,7 +21,6 @@ from apps.alerts.integration_options_mixin import IntegrationOptionsMixin from apps.alerts.models.maintainable_object import MaintainableObject from apps.alerts.tasks import disable_maintenance, sync_grafana_alerting_contact_points from apps.base.messaging import get_messaging_backend_from_id -from apps.base.utils import live_settings from apps.integrations.metadata import heartbeat from apps.integrations.tasks import create_alert, create_alertmanager_alerts from apps.slack.constants import SLACK_RATE_LIMIT_DELAY, SLACK_RATE_LIMIT_TIMEOUT @@ -162,8 +161,10 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): web_message_template = models.TextField(null=True, default=None) web_image_url_template = models.TextField(null=True, default=None) - email_title_template = models.TextField(null=True, default=None) - email_message_template = models.TextField(null=True, default=None) + # email related fields are deprecated in favour of messaging backend based templates + # these templates are stored in the messaging_backends_templates field + email_title_template = models.TextField(null=True, default=None) # deprecated + email_message_template = models.TextField(null=True, default=None) # deprecated telegram_title_template = models.TextField(null=True, default=None) telegram_message_template = models.TextField(null=True, default=None) @@ -194,10 +195,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): "phone_call": { "title": "phone_call_title_template", }, - "email": { - "title": "email_title_template", - "message": "email_message_template", - }, "telegram": { "title": "telegram_title_template", "message": "telegram_message_template", @@ -438,7 +435,8 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): @property def inbound_email(self): - return f"{self.token}@{live_settings.SENDGRID_INBOUND_EMAIL_DOMAIN}" + # todo: implement inbound emails + pass @property def default_channel_filter(self): @@ -461,10 +459,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): "message": self.web_message_template, "image_url": self.web_image_url_template, }, - "email": { - "title": self.email_title_template, - "message": self.email_message_template, - }, "sms": { "title": self.sms_title_template, }, @@ -620,8 +614,6 @@ class AlertReceiveChannel(IntegrationOptionsMixin, MaintainableObject): "web_title": self.web_title_template or "default", "web_message": self.web_message_template or "default", "web_image_url_template": self.web_image_url_template or "default", - "email_title_template": self.email_title_template or "default", - "email_message": self.email_message_template or "default", "telegram_title": self.telegram_title_template or "default", "telegram_message": self.telegram_message_template or "default", "telegram_image_url": self.telegram_image_url_template or "default", diff --git a/engine/apps/alerts/tasks/notify_user.py b/engine/apps/alerts/tasks/notify_user.py index 425eea16..7bd2f03d 100644 --- a/engine/apps/alerts/tasks/notify_user.py +++ b/engine/apps/alerts/tasks/notify_user.py @@ -228,7 +228,6 @@ def notify_user_task( def perform_notification(log_record_pk): SMSMessage = apps.get_model("twilioapp", "SMSMessage") PhoneCall = apps.get_model("twilioapp", "PhoneCall") - # EmailMessage = apps.get_model("sendgridapp", "EmailMessage") TODO: restore email notifications UserNotificationPolicy = apps.get_model("base", "UserNotificationPolicy") TelegramToUserConnector = apps.get_model("telegram", "TelegramToUserConnector") UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord") @@ -280,10 +279,6 @@ def perform_notification(log_record_pk): elif notification_channel == UserNotificationPolicy.NotificationChannel.TELEGRAM: TelegramToUserConnector.notify_user(user, alert_group, notification_policy) - # TODO: restore email notifications - # elif notification_channel == UserNotificationPolicy.NotificationChannel.EMAIL: - # EmailMessage.send_incident_mail(user, alert_group, notification_policy) - elif notification_channel == UserNotificationPolicy.NotificationChannel.SLACK: # TODO: refactor checking the possibility of sending a notification in slack # Code below is not consistent. diff --git a/engine/apps/alerts/tests/test_default_templates.py b/engine/apps/alerts/tests/test_default_templates.py index 259aa051..50c1ecd6 100644 --- a/engine/apps/alerts/tests/test_default_templates.py +++ b/engine/apps/alerts/tests/test_default_templates.py @@ -2,7 +2,6 @@ import pytest from jinja2 import TemplateSyntaxError from apps.alerts.incident_appearance.templaters import ( - AlertEmailTemplater, AlertPhoneCallTemplater, AlertSlackTemplater, AlertSmsTemplater, @@ -45,14 +44,12 @@ def test_default_templates( slack_templater = AlertSlackTemplater(alert) web_templater = AlertWebTemplater(alert) sms_templater = AlertSmsTemplater(alert) - email_templater = AlertEmailTemplater(alert) telegram_templater = AlertTelegramTemplater(alert) phone_call_templater = AlertPhoneCallTemplater(alert) templaters = { "slack": slack_templater, "web": web_templater, "sms": sms_templater, - "email": email_templater, "telegram": telegram_templater, "phone_call": phone_call_templater, } diff --git a/engine/apps/api/serializers/alert_receive_channel.py b/engine/apps/api/serializers/alert_receive_channel.py index b04f2283..58d6f349 100644 --- a/engine/apps/api/serializers/alert_receive_channel.py +++ b/engine/apps/api/serializers/alert_receive_channel.py @@ -254,18 +254,6 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode validators=[valid_jinja_template_for_serializer_method_field], required=False, ) - email_title_template = WritableSerializerMethodField( - allow_null=True, - deserializer_field=serializers.CharField(), - validators=[valid_jinja_template_for_serializer_method_field], - required=False, - ) - email_message_template = WritableSerializerMethodField( - allow_null=True, - deserializer_field=serializers.CharField(), - validators=[valid_jinja_template_for_serializer_method_field], - required=False, - ) source_link_template = WritableSerializerMethodField( allow_null=True, deserializer_field=serializers.CharField(), @@ -306,8 +294,6 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode "web_title_template", "web_message_template", "web_image_url_template", - "email_title_template", - "email_message_template", "telegram_title_template", "telegram_message_template", "telegram_image_url_template", @@ -391,17 +377,6 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode elif default_template is not None and default_template.strip() == value.strip(): self.instance.web_title_template = None - def get_email_title_template(self, obj): - default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_EMAIL_TITLE_TEMPLATE[obj.integration] - return obj.email_title_template or default_template - - def set_email_title_template(self, value): - default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_EMAIL_TITLE_TEMPLATE[self.instance.integration] - if default_template is None or default_template.strip() != value.strip(): - self.instance.email_title_template = value.strip() - elif default_template is not None and default_template.strip() == value.strip(): - self.instance.email_title_template = None - def get_web_message_template(self, obj): default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_WEB_MESSAGE_TEMPLATE[obj.integration] return obj.web_message_template or default_template @@ -424,17 +399,6 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode elif default_template is not None and default_template.strip() == value.strip(): self.instance.web_image_url_template = None - def get_email_message_template(self, obj): - default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_EMAIL_MESSAGE_TEMPLATE[obj.integration] - return obj.email_message_template or default_template - - def set_email_message_template(self, value): - default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_EMAIL_MESSAGE_TEMPLATE[self.instance.integration] - if default_template is None or default_template.strip() != value.strip(): - self.instance.email_message_template = value.strip() - elif default_template is not None and default_template.strip() == value.strip(): - self.instance.email_message_template = None - def get_telegram_title_template(self, obj): default_template = AlertReceiveChannel.INTEGRATION_TO_DEFAULT_TELEGRAM_TITLE_TEMPLATE[obj.integration] return obj.telegram_title_template or default_template @@ -644,8 +608,8 @@ class AlertReceiveChannelTemplatesSerializer(EagerLoadingMixin, serializers.Mode def _get_messaging_backend_templates(self, obj): """Return additional messaging backend templates if any.""" templates = {} - for backend_id, _ in get_messaging_backends(): - for field in ("title", "message", "image_url"): + for backend_id, backend in get_messaging_backends(): + for field in backend.template_fields: value = None if obj.messaging_backends_templates: value = obj.messaging_backends_templates.get(backend_id, {}).get(field) diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index 6d4a0b9e..4ef75494 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -1443,7 +1443,7 @@ def test_alert_group_preview_body_non_existent_template_var( client = APIClient() url = reverse("api-internal:alertgroup-preview-template", kwargs={"pk": alert_group.public_primary_key}) - data = {"template_name": "email_title_template", "template_body": "foobar: {{ foobar.does_not_exist }}"} + data = {"template_name": "testonly_title_template", "template_body": "foobar: {{ foobar.does_not_exist }}"} response = client.post(url, data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK @@ -1465,7 +1465,7 @@ def test_alert_group_preview_body_invalid_template_syntax( client = APIClient() url = reverse("api-internal:alertgroup-preview-template", kwargs={"pk": alert_group.public_primary_key}) - data = {"template_name": "email_title_template", "template_body": "{{'' if foo is None else foo}}"} + data = {"template_name": "testonly_title_template", "template_body": "{{'' if foo is None else foo}}"} response = client.post(url, data, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/engine/apps/api/tests/test_alert_receive_channel_template.py b/engine/apps/api/tests/test_alert_receive_channel_template.py index 08340646..a4a10ccf 100644 --- a/engine/apps/api/tests/test_alert_receive_channel_template.py +++ b/engine/apps/api/tests/test_alert_receive_channel_template.py @@ -6,6 +6,7 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient +from apps.base.messaging import BaseMessagingBackend from common.constants.role import Role @@ -224,7 +225,7 @@ def test_update_alert_receive_channel_backend_template_update_values( # patch messaging backends to add OTHER as a valid backend with patch( "apps.api.serializers.alert_receive_channel.get_messaging_backends", - return_value=[("TESTONLY", None), ("OTHER", None)], + return_value=[("TESTONLY", BaseMessagingBackend), ("OTHER", BaseMessagingBackend)], ): response = client.put( url, format="json", data={"testonly_title_template": "updated-title"}, **make_user_auth_headers(user, token) diff --git a/engine/apps/api/views/user_notification_policy.py b/engine/apps/api/views/user_notification_policy.py index 7231bcc5..5cc6399e 100644 --- a/engine/apps/api/views/user_notification_policy.py +++ b/engine/apps/api/views/user_notification_policy.py @@ -160,9 +160,6 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet): notification_channel in NotificationChannelAPIOptions.TELEGRAM_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS ) - email_integration_required = ( - notification_channel in NotificationChannelAPIOptions.EMAIL_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS - ) mobile_app_integration_required = ( notification_channel in NotificationChannelAPIOptions.MOBILE_APP_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS @@ -171,8 +168,6 @@ class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet): continue if telegram_integration_required and not settings.FEATURE_TELEGRAM_INTEGRATION_ENABLED: continue - if email_integration_required and not settings.FEATURE_EMAIL_INTEGRATION_ENABLED: - continue if mobile_app_integration_required and not settings.MOBILE_APP_PUSH_NOTIFICATIONS_ENABLED: continue diff --git a/engine/apps/base/messaging.py b/engine/apps/base/messaging.py index 64d9682d..7d2b2e88 100644 --- a/engine/apps/base/messaging.py +++ b/engine/apps/base/messaging.py @@ -7,7 +7,9 @@ class BaseMessagingBackend: label = "The Backend" short_label = "Backend" available_for_use = False + templater = None + template_fields = ("title", "message", "image_url") def __init__(self, *args, **kwargs): self.notification_channel_id = kwargs.get("notification_channel_id") diff --git a/engine/apps/base/models/live_setting.py b/engine/apps/base/models/live_setting.py index abd5cf1e..4e2664d7 100644 --- a/engine/apps/base/models/live_setting.py +++ b/engine/apps/base/models/live_setting.py @@ -33,6 +33,11 @@ class LiveSetting(models.Model): error = models.TextField(null=True, default=None) AVAILABLE_NAMES = ( + "EMAIL_HOST", + "EMAIL_PORT", + "EMAIL_HOST_USER", + "EMAIL_HOST_PASSWORD", + "EMAIL_USE_TLS", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_NUMBER", @@ -51,6 +56,11 @@ class LiveSetting(models.Model): ) DESCRIPTIONS = { + "EMAIL_HOST": "SMTP server host. This email server will be used to notify users via email.", + "EMAIL_PORT": "SMTP server port", + "EMAIL_HOST_USER": "SMTP server user", + "EMAIL_HOST_PASSWORD": "SMTP server password", + "EMAIL_USE_TLS": "SMTP enable/disable TLS", "SLACK_SIGNING_SECRET": ( "Check more info." - ), - "SENDGRID_FROM_EMAIL": ( - "Address to send emails, " - "more info." - ), - "SENDGRID_SECRET_KEY": "It is the secret key to secure receiving inbound emails.", - "SENDGRID_INBOUND_EMAIL_DOMAIN": "Domain to receive emails for inbound emails integration.", "TELEGRAM_TOKEN": ( "Secret token for Telegram bot, you can get one via BotFather." ), @@ -126,11 +126,10 @@ class LiveSetting(models.Model): } SECRET_SETTING_NAMES = ( + "EMAIL_HOST_PASSWORD", "TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN", "TWILIO_VERIFY_SERVICE_SID", - "SENDGRID_API_KEY", - "SENDGRID_SECRET_KEY", "SLACK_CLIENT_OAUTH_ID", "SLACK_CLIENT_OAUTH_SECRET", "SLACK_SIGNING_SECRET", diff --git a/engine/apps/base/models/user_notification_policy.py b/engine/apps/base/models/user_notification_policy.py index b6444995..a4c4b876 100644 --- a/engine/apps/base/models/user_notification_policy.py +++ b/engine/apps/base/models/user_notification_policy.py @@ -35,7 +35,6 @@ BUILT_IN_BACKENDS = ( ("SMS", 1), ("PHONE_CALL", 2), ("TELEGRAM", 3), - ("EMAIL", 4), ("MOBILE_PUSH_GENERAL", 5), ("MOBILE_PUSH_CRITICAL", 6), ) @@ -214,7 +213,6 @@ class NotificationChannelOptions: SLACK_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS = [UserNotificationPolicy.NotificationChannel.SLACK] TELEGRAM_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS = [UserNotificationPolicy.NotificationChannel.TELEGRAM] - EMAIL_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS = [UserNotificationPolicy.NotificationChannel.EMAIL] MOBILE_APP_INTEGRATION_REQUIRED_NOTIFICATION_CHANNELS = [ UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL, UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL, @@ -227,7 +225,6 @@ class NotificationChannelAPIOptions(NotificationChannelOptions): UserNotificationPolicy.NotificationChannel.SMS: "SMS \U00002709\U0001F4F2", UserNotificationPolicy.NotificationChannel.PHONE_CALL: "Phone call \U0000260E", UserNotificationPolicy.NotificationChannel.TELEGRAM: "Telegram \U0001F916", - UserNotificationPolicy.NotificationChannel.EMAIL: "Email \U0001F4E8", UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL: "Mobile App", UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL: "Mobile App Critical", } @@ -243,7 +240,6 @@ class NotificationChannelAPIOptions(NotificationChannelOptions): UserNotificationPolicy.NotificationChannel.SMS: "SMS", UserNotificationPolicy.NotificationChannel.PHONE_CALL: "\U0000260E", UserNotificationPolicy.NotificationChannel.TELEGRAM: "Telegram", - UserNotificationPolicy.NotificationChannel.EMAIL: "Email", UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL: "Mobile App", UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL: "Mobile App Critical", } @@ -261,7 +257,6 @@ class NotificationChannelPublicAPIOptions(NotificationChannelAPIOptions): UserNotificationPolicy.NotificationChannel.SMS: "notify_by_sms", UserNotificationPolicy.NotificationChannel.PHONE_CALL: "notify_by_phone_call", UserNotificationPolicy.NotificationChannel.TELEGRAM: "notify_by_telegram", - UserNotificationPolicy.NotificationChannel.EMAIL: "notify_by_email", UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL: "notify_by_mobile_app", UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL: "notify_by_mobile_app_critical", } diff --git a/engine/apps/base/models/user_notification_policy_log_record.py b/engine/apps/base/models/user_notification_policy_log_record.py index e29a9ec4..4256faef 100644 --- a/engine/apps/base/models/user_notification_policy_log_record.py +++ b/engine/apps/base/models/user_notification_policy_log_record.py @@ -49,14 +49,14 @@ class UserNotificationPolicyLogRecord(models.Model): ERROR_NOTIFICATION_PHONE_CALLS_LIMIT_EXCEEDED, ERROR_NOTIFICATION_PHONE_NUMBER_IS_NOT_VERIFIED, ERROR_NOTIFICATION_NOT_ABLE_TO_SEND_MAIL, - ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED, - ERROR_NOTIFICATION_EMAIL_IS_NOT_VERIFIED, + ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED, # todo: manage backend specific limits in messaging backend + ERROR_NOTIFICATION_EMAIL_IS_NOT_VERIFIED, # deprecated ERROR_NOTIFICATION_TELEGRAM_IS_NOT_LINKED_TO_SLACK_ACC, ERROR_NOTIFICATION_PHONE_CALL_LINE_BUSY, ERROR_NOTIFICATION_PHONE_CALL_FAILED, ERROR_NOTIFICATION_PHONE_CALL_NO_ANSWER, ERROR_NOTIFICATION_SMS_DELIVERY_FAILED, - ERROR_NOTIFICATION_MAIL_DELIVERY_FAILED, + ERROR_NOTIFICATION_MAIL_DELIVERY_FAILED, # deprecated ERROR_NOTIFICATION_TELEGRAM_BOT_IS_DELETED, ERROR_NOTIFICATION_POSTING_TO_SLACK_IS_DISABLED, ERROR_NOTIFICATION_POSTING_TO_TELEGRAM_IS_DISABLED, # deprecated @@ -78,7 +78,6 @@ class UserNotificationPolicyLogRecord(models.Model): ERROR_NOTIFICATION_PHONE_CALLS_LIMIT_EXCEEDED, ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED, ERROR_NOTIFICATION_PHONE_NUMBER_IS_NOT_VERIFIED, - ERROR_NOTIFICATION_EMAIL_IS_NOT_VERIFIED, ] type = models.IntegerField(choices=TYPE_CHOICES) @@ -172,9 +171,6 @@ class UserNotificationPolicyLogRecord(models.Model): result += f"SMS to {user_verbal} was delivered successfully" elif notification_channel == UserNotificationPolicy.NotificationChannel.PHONE_CALL: result += f"phone call to {user_verbal} was successful" - # TODO: restore email notifications - # elif notification_channel == UserNotificationPolicy.NotificationChannel.EMAIL: - # result += f"email to {user_verbal} was delivered successfully" elif notification_channel is None: result += f"notification to {user_verbal} was delivered successfully" elif self.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED: @@ -185,6 +181,7 @@ class UserNotificationPolicyLogRecord(models.Model): == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_PHONE_CALLS_LIMIT_EXCEEDED ): result += f"attempt to call to {user_verbal} has been failed due to a plan limit" + # todo: manage backend specific limits in messaging backend elif self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED: result += f"failed to send email to {user_verbal}. Exceeded limit for mails" elif ( @@ -201,10 +198,6 @@ class UserNotificationPolicyLogRecord(models.Model): result += f"OnCall was not able to send an SMS to {user_verbal}" elif self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ABLE_TO_CALL: result += f"OnCall was not able to call to {user_verbal}" - elif ( - self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ABLE_TO_SEND_MAIL - ): - result += f"OnCall was not able to send an email to {user_verbal}" elif ( self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_POSTING_TO_SLACK_IS_DISABLED @@ -242,10 +235,6 @@ class UserNotificationPolicyLogRecord(models.Model): result += f"phone call to {user_verbal} ended without being answered" elif self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_SMS_DELIVERY_FAILED: result += f"SMS {user_verbal} was not delivered" - elif ( - self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_MAIL_DELIVERY_FAILED - ): - result += f"email to {user_verbal} was not delivered" elif self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_IN_SLACK: result += f"failed to notify {user_verbal} in Slack" elif ( @@ -286,7 +275,7 @@ class UserNotificationPolicyLogRecord(models.Model): except ValueError: backend = None result += ( - f"failed to notify {user_verbal} in {backend.label.lower() if backend else 'disabled backend'}" + f"failed to notify {user_verbal} by {backend.label.lower() if backend else 'disabled backend'}" ) elif self.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_TRIGGERED: if notification_step == UserNotificationPolicy.Step.NOTIFY: @@ -298,9 +287,6 @@ class UserNotificationPolicyLogRecord(models.Model): result += f"called {user_verbal} by phone" elif notification_channel == UserNotificationPolicy.NotificationChannel.TELEGRAM: result += f"sent telegram message to {user_verbal}" - # TODO: restore email notifications - # elif notification_channel == UserNotificationPolicy.NotificationChannel.EMAIL: - # result += f"sent email to {user_verbal}" elif notification_channel == UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_GENERAL: result += f"sent push notifications to {user_verbal}" elif notification_channel == UserNotificationPolicy.NotificationChannel.MOBILE_PUSH_CRITICAL: diff --git a/engine/apps/base/tests/test_user_notification_policy_log_record.py b/engine/apps/base/tests/test_user_notification_policy_log_record.py index 7c8c8939..c51b892b 100644 --- a/engine/apps/base/tests/test_user_notification_policy_log_record.py +++ b/engine/apps/base/tests/test_user_notification_policy_log_record.py @@ -31,7 +31,7 @@ def test_extra_messaging_backends_error_log( ) output = log_record.render_log_line_action() - assert output == f"failed to notify {user_1.username} in {TestOnlyBackend.label.lower()}" + assert output == f"failed to notify {user_1.username} by {TestOnlyBackend.label.lower()}" @pytest.mark.django_db diff --git a/engine/apps/base/utils.py b/engine/apps/base/utils.py index 0fe0d8a9..f8ad633f 100644 --- a/engine/apps/base/utils.py +++ b/engine/apps/base/utils.py @@ -5,8 +5,6 @@ from urllib.parse import urlparse import phonenumbers from django.apps import apps from phonenumbers import NumberParseException -from python_http_client import UnauthorizedError -from sendgrid import SendGridAPIClient from telegram import Bot from twilio.base.exceptions import TwilioException from twilio.rest import Client @@ -77,20 +75,6 @@ class LiveSettingValidator: if not cls._is_phone_number_valid(twilio_number): return "Please specify a valid phone number in the following format: +XXXXXXXXXXX" - @classmethod - def _check_sendgrid_api_key(cls, sendgrid_api_key): - sendgrid_client = SendGridAPIClient(sendgrid_api_key) - - try: - sendgrid_client.client.mail_settings.get() - except Exception as e: - return cls._prettify_sendgrid_error(e) - - @classmethod - def _check_sendgrid_from_email(cls, sendgrid_from_email): - if not cls._is_email_valid(sendgrid_from_email): - return "Please specify a valid email" - @classmethod def _check_slack_install_return_redirect_host(cls, slack_install_return_redirect_host): scheme = urlparse(slack_install_return_redirect_host).scheme @@ -147,10 +131,3 @@ class LiveSettingValidator: return f"Twilio error: {exc.args[0]}" else: return f"Twilio error: {str(exc)}" - - @staticmethod - def _prettify_sendgrid_error(exc): - if isinstance(exc, UnauthorizedError): - return "Sendgrid error: couldn't authorize with given credentials" - else: - return f"Sendgrid error: {str(exc)}" diff --git a/engine/apps/sendgridapp/__init__.py b/engine/apps/email/__init__.py similarity index 100% rename from engine/apps/sendgridapp/__init__.py rename to engine/apps/email/__init__.py diff --git a/engine/apps/email/alert_rendering.py b/engine/apps/email/alert_rendering.py new file mode 100644 index 00000000..b3802d83 --- /dev/null +++ b/engine/apps/email/alert_rendering.py @@ -0,0 +1,55 @@ +from django.template.loader import render_to_string +from emoji.core import emojize + +from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_TITLE +from apps.alerts.incident_appearance.templaters.alert_templater import AlertTemplater +from common.utils import convert_md_to_html, str_or_backup + + +class AlertEmailTemplater(AlertTemplater): + RENDER_FOR_EMAIL = "email" + + def _render_for(self): + return self.RENDER_FOR_EMAIL + + def _postformat(self, templated_alert): + templated_alert.title = self._slack_format_for_email(templated_alert.title) + templated_alert.message = self._slack_format_for_email(templated_alert.message) + return templated_alert + + def _slack_format_for_email(self, data): + sf = self.slack_formatter + sf.hyperlink_mention_format = "{title} - {url}" + return sf.format(data) + + +def build_subject_and_message(alert_group, emails_left): + alert = alert_group.alerts.first() + templated_alert = AlertEmailTemplater(alert).render() + + title_fallback = ( + f"#{alert_group.inside_organization_number} " f"{DEFAULT_BACKUP_TITLE} via {alert_group.channel.verbal_name}" + ) + + # default templates are the same as web templates, which are in Markdown format + message = templated_alert.message + if message: + message = convert_md_to_html(templated_alert.message) if templated_alert.message else "" + + content = render_to_string( + "email_notification.html", + { + "url": alert_group.slack_permalink or alert_group.web_link, + "title": str_or_backup(templated_alert.title, title_fallback), + "message": str_or_backup(message, ""), # not render message at all if smth goes wrong + "organization": alert_group.channel.organization.org_title, + "integration": emojize(alert_group.channel.short_name, use_aliases=True), + "limit_notification": emails_left <= 20, + "emails_left": emails_left, + }, + ) + + title = str_or_backup(templated_alert.title, title_fallback) + subject = f"[{title}] You are invited to check an alert group" + + return subject, content diff --git a/engine/apps/email/backend.py b/engine/apps/email/backend.py new file mode 100644 index 00000000..164f0cb8 --- /dev/null +++ b/engine/apps/email/backend.py @@ -0,0 +1,20 @@ +from apps.base.messaging import BaseMessagingBackend +from apps.email.tasks import notify_user_async + + +class EmailBackend(BaseMessagingBackend): + backend_id = "EMAIL" + label = "Email" + short_label = "Email" + available_for_use = True + + templater = "apps.email.alert_rendering.AlertEmailTemplater" + template_fields = ("title", "message") + + def serialize_user(self, user): + return {"email": user.email} + + def notify_user(self, user, alert_group, notification_policy): + notify_user_async.delay( + user_pk=user.pk, alert_group_pk=alert_group.pk, notification_policy_pk=notification_policy.pk + ) diff --git a/engine/apps/email/migrations/0001_initial.py b/engine/apps/email/migrations/0001_initial.py new file mode 100644 index 00000000..3fe9537a --- /dev/null +++ b/engine/apps/email/migrations/0001_initial.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.15 on 2022-10-10 12:06 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('user_management', '0003_user_hide_phone_number'), + ('alerts', '0007_populate_web_title_cache'), + ('base', '0003_delete_organizationlogrecord'), + ] + + operations = [ + migrations.CreateModel( + name='EmailMessage', + fields=[ + ('message_uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('exceeded_limit', models.BooleanField(default=None, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('notification_policy', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='base.usernotificationpolicy')), + ('receiver', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='user_management.user')), + ('represents_alert', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='alerts.alert')), + ('represents_alert_group', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='alerts.alertgroup')), + ], + ), + ] diff --git a/engine/apps/sendgridapp/migrations/__init__.py b/engine/apps/email/migrations/__init__.py similarity index 100% rename from engine/apps/sendgridapp/migrations/__init__.py rename to engine/apps/email/migrations/__init__.py diff --git a/engine/apps/email/models.py b/engine/apps/email/models.py new file mode 100644 index 00000000..0db26986 --- /dev/null +++ b/engine/apps/email/models.py @@ -0,0 +1,20 @@ +import logging +import uuid + +from django.db import models + +logger = logging.getLogger(__name__) + + +class EmailMessage(models.Model): + message_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + exceeded_limit = models.BooleanField(null=True, default=None) + represents_alert = models.ForeignKey("alerts.Alert", on_delete=models.SET_NULL, null=True, default=None) + represents_alert_group = models.ForeignKey("alerts.AlertGroup", on_delete=models.SET_NULL, null=True, default=None) + notification_policy = models.ForeignKey( + "base.UserNotificationPolicy", on_delete=models.SET_NULL, null=True, default=None + ) + + receiver = models.ForeignKey("user_management.User", on_delete=models.PROTECT, null=True, default=None) + created_at = models.DateTimeField(auto_now_add=True) diff --git a/engine/apps/email/tasks.py b/engine/apps/email/tasks.py new file mode 100644 index 00000000..3fdf4d4e --- /dev/null +++ b/engine/apps/email/tasks.py @@ -0,0 +1,99 @@ +from socket import gaierror + +from celery.utils.log import get_task_logger +from django.conf import settings +from django.core.mail import BadHeaderError, get_connection, send_mail +from django.utils.html import strip_tags + +from apps.alerts.models import AlertGroup +from apps.base.utils import live_settings +from apps.email.alert_rendering import build_subject_and_message +from apps.email.models import EmailMessage +from apps.user_management.models import User +from common.custom_celery_tasks import shared_dedicated_queue_retry_task + +MAX_RETRIES = 1 if settings.DEBUG else 10 +logger = get_task_logger(__name__) + + +@shared_dedicated_queue_retry_task(autoretry_for=(Exception,), retry_backoff=True, max_retries=MAX_RETRIES) +def notify_user_async(user_pk, alert_group_pk, notification_policy_pk): + # imported here to avoid circular import error + from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord + + try: + user = User.objects.get(pk=user_pk) + except User.DoesNotExist: + logger.warning(f"User {user_pk} does not exist") + return + + try: + alert_group = AlertGroup.all_objects.get(pk=alert_group_pk) + except AlertGroup.DoesNotExist: + logger.warning(f"Alert group {alert_group_pk} does not exist") + return + + try: + notification_policy = UserNotificationPolicy.objects.get(pk=notification_policy_pk) + except UserNotificationPolicy.DoesNotExist: + logger.warning(f"User notification policy {notification_policy_pk} does not exist") + return + + emails_left = user.organization.emails_left(user) + if emails_left <= 0: + UserNotificationPolicyLogRecord.objects.create( + author=user, + type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, + notification_policy=notification_policy, + alert_group=alert_group, + reason="Error while sending email", + notification_step=notification_policy.step, + notification_channel=notification_policy.notify_by, + notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED, + ) + EmailMessage.objects.create( + represents_alert_group=alert_group, + notification_policy=notification_policy, + receiver=user, + exceeded_limit=True, + ) + return + + subject, html_message = build_subject_and_message(alert_group, emails_left) + + message = strip_tags(html_message) + email_from = settings.EMAIL_HOST_USER + recipient_list = [user.email] + + connection = get_connection( + host=live_settings.EMAIL_HOST, + port=live_settings.EMAIL_PORT, + username=live_settings.EMAIL_HOST_USER, + password=live_settings.EMAIL_HOST_PASSWORD, + use_tls=live_settings.EMAIL_USE_TLS, + fail_silently=False, + timeout=5, + ) + + try: + send_mail(subject, message, email_from, recipient_list, html_message=html_message, connection=connection) + EmailMessage.objects.create( + represents_alert_group=alert_group, + notification_policy=notification_policy, + receiver=user, + exceeded_limit=False, + ) + except (gaierror, BadHeaderError) as e: + # gaierror is raised when EMAIL_HOST is invalid + # BadHeaderError is raised when there's newlines in the subject + UserNotificationPolicyLogRecord.objects.create( + author=user, + type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, + notification_policy=notification_policy, + alert_group=alert_group, + reason="Error while sending email", + notification_step=notification_policy.step, + notification_channel=notification_policy.notify_by, + ) + logger.error(f"Error while sending email: {e}") + return diff --git a/engine/apps/email/templates/email_notification.html b/engine/apps/email/templates/email_notification.html new file mode 100644 index 00000000..86ade2bb --- /dev/null +++ b/engine/apps/email/templates/email_notification.html @@ -0,0 +1,28 @@ + +You are invited to check an alert group in Grafana OnCall! +

+Organization: {{ organization }} +
+Integration: {{ integration }} +
+Title: {{ title }} +{% if message %} +
+Message: +
+{% autoescape off %} + {{ message }} +{% endautoescape %} +{% endif %} +
+Go to the alert group +
+Your Grafana OnCall +{% if limit_notification %} +

+ {{ emails_left }} emails left for the organization today. Contact your admin. +{% endif %} + + +{% now "H:i.u e"%} + \ No newline at end of file diff --git a/engine/apps/sendgridapp/tests/__init__.py b/engine/apps/email/tests/__init__.py similarity index 100% rename from engine/apps/sendgridapp/tests/__init__.py rename to engine/apps/email/tests/__init__.py diff --git a/engine/apps/email/tests/factories.py b/engine/apps/email/tests/factories.py new file mode 100644 index 00000000..db86c94e --- /dev/null +++ b/engine/apps/email/tests/factories.py @@ -0,0 +1,8 @@ +import factory + +from apps.email.models import EmailMessage + + +class EmailMessageFactory(factory.DjangoModelFactory): + class Meta: + model = EmailMessage diff --git a/engine/apps/email/tests/test_notify_user.py b/engine/apps/email/tests/test_notify_user.py new file mode 100644 index 00000000..a6b1c356 --- /dev/null +++ b/engine/apps/email/tests/test_notify_user.py @@ -0,0 +1,118 @@ +import socket +from unittest.mock import patch + +import pytest +from django.core import mail +from django.core.mail.backends.locmem import EmailBackend + +from apps.base.models import UserNotificationPolicy, UserNotificationPolicyLogRecord +from apps.email.tasks import notify_user_async +from apps.user_management.subscription_strategy.free_public_beta_subscription_strategy import ( + FreePublicBetaSubscriptionStrategy, +) + + +@pytest.mark.django_db +def test_notify_user( + settings, + make_organization, + make_user_for_organization, + make_token_for_organization, + make_alert_receive_channel, + make_alert_group, + make_alert, + make_user_notification_policy, +): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + + organization = make_organization() + user = make_user_for_organization(organization) + + alert_receive_channel = make_alert_receive_channel(organization) + alert_group = make_alert_group(alert_receive_channel) + + make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload) + + notification_policy = make_user_notification_policy( + user, + UserNotificationPolicy.Step.NOTIFY, + notify_by=8, + important=False, + ) + + notify_user_async(user.pk, alert_group.pk, notification_policy.pk) + assert len(mail.outbox) == 1 + + +@pytest.mark.django_db +def test_notify_user_bad_smtp_host( + settings, + make_organization, + make_user_for_organization, + make_token_for_organization, + make_alert_receive_channel, + make_alert_group, + make_alert, + make_user_notification_policy, +): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + + organization = make_organization() + user = make_user_for_organization(organization) + + alert_receive_channel = make_alert_receive_channel(organization) + alert_group = make_alert_group(alert_receive_channel) + + make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload) + + notification_policy = make_user_notification_policy( + user, + UserNotificationPolicy.Step.NOTIFY, + notify_by=8, + important=False, + ) + + with patch.object(EmailBackend, "send_messages", side_effect=socket.gaierror): + notify_user_async(user.pk, alert_group.pk, notification_policy.pk) + + assert len(mail.outbox) == 0 + + log_record = notification_policy.personal_log_records.last() + assert log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED + + +@pytest.mark.django_db +def test_notify_user_no_emails_left( + settings, + make_organization, + make_user_for_organization, + make_token_for_organization, + make_alert_receive_channel, + make_alert_group, + make_alert, + make_user_notification_policy, +): + settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + + organization = make_organization() + user = make_user_for_organization(organization) + + alert_receive_channel = make_alert_receive_channel(organization) + alert_group = make_alert_group(alert_receive_channel) + + make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload) + + notification_policy = make_user_notification_policy( + user, + UserNotificationPolicy.Step.NOTIFY, + notify_by=8, + important=False, + ) + + with patch.object(FreePublicBetaSubscriptionStrategy, "emails_left", return_value=0): + notify_user_async(user.pk, alert_group.pk, notification_policy.pk) + + assert len(mail.outbox) == 0 + log_record = notification_policy.personal_log_records.last() + assert log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED + assert log_record.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED diff --git a/engine/apps/integrations/views.py b/engine/apps/integrations/views.py index 4c975b15..b0f81c02 100644 --- a/engine/apps/integrations/views.py +++ b/engine/apps/integrations/views.py @@ -25,8 +25,6 @@ from apps.integrations.mixins import ( is_ratelimit_ignored, ) from apps.integrations.tasks import create_alert, create_alertmanager_alerts -from apps.sendgridapp.parse import Parse -from apps.sendgridapp.permissions import AllowOnlySendgrid from common.api_helpers.utils import create_engine_url logger = logging.getLogger(__name__) @@ -384,72 +382,8 @@ class HeartBeatAPIView(AlertChannelDefiningMixin, APIView): class InboundWebhookEmailView(AlertChannelDefiningMixin, APIView): - permission_classes = [AllowOnlySendgrid] - - def dispatch(self, *args, **kwargs): - parse = Parse(self.request) - self.email_data = parse.key_values() - # When email is forwarded recipient field can be stored both in "to" and in "envelope" fields. - token_from_to = self._parse_token_from_to(self.email_data) - try: - kwargs["alert_channel_key"] = token_from_to - return super().dispatch(*args, **kwargs) - except KeyError as e: - logger.warning(f"InboundWebhookEmailView: {e}") - except PermissionDenied as e: - self._log_permission_denied(token_from_to, e) - kwargs.pop("alert_channel_key") - - token_from_envelope = self._parse_token_from_envelope(self.email_data) - try: - kwargs["alert_channel_key"] = token_from_envelope - return super().dispatch(*args, **kwargs) - except KeyError as e: - logger.warning(f"InboundWebhookEmailView: {e}") - except PermissionDenied as e: - self._log_permission_denied(token_from_to, e) - kwargs.pop("alert_channel_key") - - raise PermissionDenied("Integration key was not found. Permission denied.") - - def _log_permission_denied(self, token, e): - logger.info( - f"InboundWebhookEmailView: Permission denied. token {token}. " - f"To {self.email_data.get('to')}. " - f"Envelope {self.email_data.get('envelope')}." - f"Exception: {e}" - ) - - def _parse_token_from_envelope(self, email_data): - envelope = email_data["envelope"] - envelope = json.loads(envelope) - token = envelope.get("to")[0].split("@")[0] - return token - - def _parse_token_from_to(self, email_data): - return email_data["to"].split("@")[0] - - def post(self, request, alert_receive_channel=None): - title = self.email_data["subject"] - message = self.email_data.get("text", "").strip() - - payload = {"title": title, "message": message} - - if alert_receive_channel: - create_alert.apply_async( - [], - { - "title": title, - "message": message, - "alert_receive_channel_pk": alert_receive_channel.pk, - "image_url": None, - "link_to_upstream_details": payload.get("link_to_upstream_details"), - "integration_unique_data": payload, - "raw_request_data": request.data, - }, - ) - - return Response("OK") + # todo: implement inbound emails + pass class IntegrationHeartBeatAPIView(AlertChannelDefiningMixin, IntegrationHeartBeatRateLimitMixin, APIView): diff --git a/engine/apps/public_api/serializers/integrations.py b/engine/apps/public_api/serializers/integrations.py index 899f6260..b1860b9a 100644 --- a/engine/apps/public_api/serializers/integrations.py +++ b/engine/apps/public_api/serializers/integrations.py @@ -5,9 +5,10 @@ from rest_framework import fields, serializers from apps.alerts.grafana_alerting_sync_manager.grafana_alerting_sync import GrafanaAlertingSyncManager from apps.alerts.models import AlertReceiveChannel +from apps.base.messaging import get_messaging_backends from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField from common.api_helpers.exceptions import BadRequest -from common.api_helpers.mixins import EagerLoadingMixin +from common.api_helpers.mixins import NOTIFICATION_CHANNEL_OPTIONS, EagerLoadingMixin from common.jinja_templater import jinja_template_env from common.utils import timed_lru_cache @@ -62,6 +63,9 @@ class IntegrationSerializer(EagerLoadingMixin, serializers.ModelSerializer, Main serializer = DefaultChannelFilterSerializer(default_route, context=self.context) result["default_route"] = serializer.data + # add additional templates for messaging backends + result["templates"].update(self._get_messaging_backend_templates(instance)) + return result def create(self, validated_data): @@ -102,6 +106,8 @@ class IntegrationSerializer(EagerLoadingMixin, serializers.ModelSerializer, Main raise BadRequest(detail="Integration with this name already exists") def _correct_validated_data(self, validated_data): + validated_data = self._correct_validated_data_for_messaging_backends(validated_data) + templates = validated_data.pop("templates", {}) for template_name, templates_for_notification_channel in templates.items(): if type(templates_for_notification_channel) is dict: @@ -134,7 +140,7 @@ class IntegrationSerializer(EagerLoadingMixin, serializers.ModelSerializer, Main if not isinstance(templates, dict): raise BadRequest(detail="Invalid template data") - for notification_channel in ["slack", "web", "sms", "phone_call", "email", "telegram"]: + for notification_channel in NOTIFICATION_CHANNEL_OPTIONS: template_data = templates.get(notification_channel, {}) if template_data is None: continue @@ -160,6 +166,49 @@ class IntegrationSerializer(EagerLoadingMixin, serializers.ModelSerializer, Main raise BadRequest(detail=f"Invalid {common_template} template data") return templates + def _correct_validated_data_for_messaging_backends(self, validated_data): + templates = validated_data.get("templates", {}) + + messaging_backends_templates = self.instance.messaging_backends_templates if self.instance else None + + for backend_id, backend in get_messaging_backends(): + backend_templates = {} + if messaging_backends_templates is not None: + backend_templates = messaging_backends_templates.get(backend_id, {}) + + for field in backend.template_fields: + try: + template = templates[backend_id.lower()][field] + except KeyError: + continue + + backend_templates[field] = template + + # remove backend-specific template from payload + templates.pop(backend_id.lower(), None) + + if backend_templates: + validated_data["messaging_backends_templates"] = messaging_backends_templates or {} | { + backend_id: backend_templates + } + + return validated_data + + @staticmethod + def _get_messaging_backend_templates(instance): + result = {} + messaging_backends_templates = instance.messaging_backends_templates or {} + + for backend_id, backend in get_messaging_backends(): + if not backend.template_fields: + continue + + result[backend_id.lower()] = { + field: messaging_backends_templates.get(backend_id, {}).get(field) for field in backend.template_fields + } + + return result + def get_heartbeat(self, obj): try: heartbeat = obj.integration_heartbeat diff --git a/engine/apps/public_api/tests/test_integrations.py b/engine/apps/public_api/tests/test_integrations.py index de992a16..22756022 100644 --- a/engine/apps/public_api/tests/test_integrations.py +++ b/engine/apps/public_api/tests/test_integrations.py @@ -54,11 +54,12 @@ def test_get_list_integrations( "phone_call": { "title": None, }, - "email": { + "telegram": { "title": None, "message": None, + "image_url": None, }, - "telegram": { + TEST_MESSAGING_BACKEND_FIELD: { "title": None, "message": None, "image_url": None, @@ -117,7 +118,6 @@ def test_create_integrations_with_none_templates( "web": None, "sms": None, "phone_call": None, - "email": None, "telegram": None, }, } @@ -184,15 +184,76 @@ def test_update_integration_template( "phone_call": { "title": None, }, - "email": { + "telegram": { "title": None, "message": None, + "image_url": None, + }, + TEST_MESSAGING_BACKEND_FIELD: { + "title": None, + "message": None, + "image_url": None, + }, + }, + "maintenance_mode": None, + "maintenance_started_at": None, + "maintenance_end_at": None, + } + url = reverse("api-public:integrations-detail", args=[integration.public_primary_key]) + response = client.put(url, data=data_for_update, format="json", HTTP_AUTHORIZATION=f"{token}") + assert response.status_code == status.HTTP_200_OK + assert response.data == expected_response + + +@pytest.mark.django_db +def test_update_integration_template_messaging_backend( + make_organization_and_user_with_token, make_alert_receive_channel, make_channel_filter, make_integration_heartbeat +): + organization, user, token = make_organization_and_user_with_token() + integration = make_alert_receive_channel(organization, verbal_name="grafana") + default_channel_filter = make_channel_filter(integration, is_default=True) + make_integration_heartbeat(integration) + + client = APIClient() + data_for_update = {"templates": {"grouping_key": "ip_addr", TEST_MESSAGING_BACKEND_FIELD: {"title": "Incident"}}} + expected_response = { + "id": integration.public_primary_key, + "team_id": None, + "name": "grafana", + "link": integration.integration_url, + "type": "grafana", + "default_route": { + "escalation_chain_id": None, + "id": default_channel_filter.public_primary_key, + "slack": {"channel_id": None, "enabled": True}, + "telegram": {"id": None, "enabled": False}, + TEST_MESSAGING_BACKEND_FIELD: {"id": None, "enabled": False}, + }, + "heartbeat": { + "link": f"{integration.integration_url}heartbeat/", + }, + "templates": { + "grouping_key": "ip_addr", + "resolve_signal": None, + "acknowledge_signal": None, + "slack": {"title": None, "message": None, "image_url": None}, + "web": {"title": None, "message": None, "image_url": None}, + "sms": { + "title": None, + }, + "phone_call": { + "title": None, }, "telegram": { "title": None, "message": None, "image_url": None, }, + TEST_MESSAGING_BACKEND_FIELD: { + "title": "Incident", + "message": None, + "image_url": None, + }, }, "maintenance_mode": None, "maintenance_started_at": None, @@ -259,11 +320,12 @@ def test_update_resolve_signal_template( "phone_call": { "title": None, }, - "email": { + "telegram": { "title": None, "message": None, + "image_url": None, }, - "telegram": { + TEST_MESSAGING_BACKEND_FIELD: { "title": None, "message": None, "image_url": None, @@ -366,11 +428,12 @@ def test_update_sms_template_with_empty_dict( "phone_call": { "title": None, }, - "email": { + "telegram": { "title": None, "message": None, + "image_url": None, }, - "telegram": { + TEST_MESSAGING_BACKEND_FIELD: { "title": None, "message": None, "image_url": None, @@ -425,11 +488,12 @@ def test_update_integration_name( "phone_call": { "title": None, }, - "email": { + "telegram": { "title": None, "message": None, + "image_url": None, }, - "telegram": { + TEST_MESSAGING_BACKEND_FIELD: { "title": None, "message": None, "image_url": None, @@ -487,11 +551,12 @@ def test_set_default_template( "phone_call": { "title": None, }, - "email": { + "telegram": { "title": None, "message": None, + "image_url": None, }, - "telegram": { + TEST_MESSAGING_BACKEND_FIELD: { "title": None, "message": None, "image_url": None, diff --git a/engine/apps/sendgridapp/constants.py b/engine/apps/sendgridapp/constants.py deleted file mode 100644 index b6d84793..00000000 --- a/engine/apps/sendgridapp/constants.py +++ /dev/null @@ -1,49 +0,0 @@ -class SendgridEmailMessageStatuses(object): - """ - https://sendgrid.com/docs/for-developers/tracking-events/event/#delivery-events - """ - - # Delivery events - ACCEPTED = 10 - PROCESSED = 20 - DEFERRED = 30 - DELIVERED = 40 - DROPPED = 50 - BOUNCE = 60 # "event": "bounce", "type: "bounce" - BLOCKED = 70 # "event": "bounce", "type: "blocked" - - # Engagement events - OPEN = 80 - CLICK = 90 - UNSUBSCRIBE = 100 - SPAMREPORT = 110 - # Group Unsubscribe - ? - # Group Resubscribe - ? - - CHOICES = ( - (ACCEPTED, "accepted"), - (PROCESSED, "processed"), - (DEFERRED, "deferred"), - (DELIVERED, "delivered"), - (DROPPED, "dropped"), - (BOUNCE, "bounce"), - (BLOCKED, "blocked"), - (OPEN, "open"), - (CLICK, "click"), - (UNSUBSCRIBE, "unsubscribe"), - (SPAMREPORT, "spamreport"), - ) - - DETERMINANT = { - "accepted": ACCEPTED, - "processed": PROCESSED, - "deferred": DEFERRED, - "delivered": DELIVERED, - "dropped": DROPPED, - "bounce": BOUNCE, - "blocked": BLOCKED, - "open": OPEN, - "click": CLICK, - "unsubscribe": UNSUBSCRIBE, - "spamreport": SPAMREPORT, - } diff --git a/engine/apps/sendgridapp/models.py b/engine/apps/sendgridapp/models.py deleted file mode 100644 index cd717165..00000000 --- a/engine/apps/sendgridapp/models.py +++ /dev/null @@ -1,185 +0,0 @@ -import logging -import uuid - -from django.apps import apps -from django.db import models -from python_http_client.exceptions import BadRequestsError, ForbiddenError, UnauthorizedError -from sendgrid import SendGridAPIClient -from sendgrid.helpers.mail import CustomArg, Mail - -from apps.alerts.incident_appearance.renderers.email_renderer import AlertGroupEmailRenderer -from apps.alerts.signals import user_notification_action_triggered_signal -from apps.base.utils import live_settings -from apps.sendgridapp.constants import SendgridEmailMessageStatuses - -logger = logging.getLogger(__name__) - - -class EmailMessageManager(models.Manager): - def update_status(self, message_uuid, message_status): - """The function checks existence of EmailMessage - instance according to message_uuid and updates status on - message_status - - Args: - message_uuid (str): uuid of Email message - message_status (str): new status - - Returns: - - """ - UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord") - - if message_uuid and message_status: - email_message_qs = self.filter(message_uuid=message_uuid) - status = SendgridEmailMessageStatuses.DETERMINANT.get(message_status) - - if email_message_qs.exists() and status: - email_message_qs.update(status=status) - - email_message = email_message_qs.first() - log_record = None - - if status == SendgridEmailMessageStatuses.DELIVERED: - log_record = UserNotificationPolicyLogRecord( - author=email_message.receiver, - type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_SUCCESS, - notification_policy=email_message.notification_policy, - alert_group=email_message.represents_alert_group, - notification_step=email_message.notification_policy.step - if email_message.notification_policy - else None, - notification_channel=email_message.notification_policy.notify_by - if email_message.notification_policy - else None, - ) - elif status in [ - SendgridEmailMessageStatuses.BOUNCE, - SendgridEmailMessageStatuses.BLOCKED, - SendgridEmailMessageStatuses.DROPPED, - ]: - log_record = UserNotificationPolicyLogRecord( - author=email_message.receiver, - type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, - notification_policy=email_message.notification_policy, - alert_group=email_message.represents_alert_group, - notification_error_code=email_message.get_error_code_by_sendgrid_status(status), - notification_step=email_message.notification_policy.step - if email_message.notification_policy - else None, - notification_channel=email_message.notification_policy.notify_by - if email_message.notification_policy - else None, - ) - if log_record is not None: - log_record.save() - user_notification_action_triggered_signal.send( - sender=EmailMessage.objects.update_status, log_record=log_record - ) - - -class EmailMessage(models.Model): - objects = EmailMessageManager() - - message_uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - - exceeded_limit = models.BooleanField(null=True, default=None) - represents_alert = models.ForeignKey("alerts.Alert", on_delete=models.SET_NULL, null=True, default=None) - represents_alert_group = models.ForeignKey("alerts.AlertGroup", on_delete=models.SET_NULL, null=True, default=None) - notification_policy = models.ForeignKey( - "base.UserNotificationPolicy", on_delete=models.SET_NULL, null=True, default=None - ) - - receiver = models.ForeignKey("user_management.User", on_delete=models.PROTECT, null=True, default=None) - - status = models.PositiveSmallIntegerField(blank=True, null=True, choices=SendgridEmailMessageStatuses.CHOICES) - - created_at = models.DateTimeField(auto_now_add=True) - - @staticmethod - def send_incident_mail(user, alert_group, notification_policy): - UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord") - - log_record = None - alert = alert_group.alerts.first() - - email_message = EmailMessage( - represents_alert_group=alert_group, - represents_alert=alert, - receiver=user, - notification_policy=notification_policy, - ) - emails_left = alert_group.channel.organization.emails_left(user) - if emails_left > 0: - email_message.exceeded_limit = False - - limit_notification = False - if emails_left < 5: - limit_notification = True - - subject, html_content = AlertGroupEmailRenderer(alert_group).render(limit_notification) - - message = Mail( - from_email=live_settings.SENDGRID_FROM_EMAIL, - to_emails=user.email, - subject=subject, - html_content=html_content, - ) - custom_arg = CustomArg("message_uuid", str(email_message.message_uuid)) - message.add_custom_arg(custom_arg) - - sendgrid_client = SendGridAPIClient(live_settings.SENDGRID_API_KEY) - try: - response = sendgrid_client.send(message) - sending_status = True - except (BadRequestsError, UnauthorizedError, ForbiddenError) as e: - logger.error(f"Error email sending: {e}") - sending_status = False - else: - if response.status_code == 202: - email_message.status = SendgridEmailMessageStatuses.ACCEPTED - email_message.save() - else: - logger.error(f"Error email sending: status code: {response.status_code}") - sending_status = False - - if not sending_status: - log_record = UserNotificationPolicyLogRecord( - author=user, - type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, - notification_policy=notification_policy, - alert_group=alert_group, - notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ABLE_TO_SEND_MAIL, - notification_step=notification_policy.step if notification_policy else None, - notification_channel=notification_policy.notify_by if notification_policy else None, - ) - else: - log_record = UserNotificationPolicyLogRecord( - author=user, - type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, - notification_policy=notification_policy, - alert_group=alert_group, - notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED, - notification_step=notification_policy.step if notification_policy else None, - notification_channel=notification_policy.notify_by if notification_policy else None, - ) - email_message.exceeded_limit = True - email_message.save() - - if log_record is not None: - log_record.save() - user_notification_action_triggered_signal.send( - sender=EmailMessage.send_incident_mail, log_record=log_record - ) - - @staticmethod - def get_error_code_by_sendgrid_status(status): - UserNotificationPolicyLogRecord = apps.get_model("base", "UserNotificationPolicyLogRecord") - - SENDGRID_ERRORS_TO_ERROR_CODES_MAP = { - SendgridEmailMessageStatuses.BOUNCE: UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_MAIL_DELIVERY_FAILED, - SendgridEmailMessageStatuses.BLOCKED: UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_MAIL_DELIVERY_FAILED, - SendgridEmailMessageStatuses.DROPPED: UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_MAIL_DELIVERY_FAILED, - } - - return SENDGRID_ERRORS_TO_ERROR_CODES_MAP.get(status, None) diff --git a/engine/apps/sendgridapp/parse.py b/engine/apps/sendgridapp/parse.py deleted file mode 100644 index 55cc939c..00000000 --- a/engine/apps/sendgridapp/parse.py +++ /dev/null @@ -1,119 +0,0 @@ -import base64 -import email -import mimetypes - -from six import iteritems -from werkzeug.utils import secure_filename - - -class Parse(object): - """Parse data received from the SendGrid Inbound Parse webhook. - It's based on https://github.com/sendgrid/sendgrid-python/blob/master/sendgrid/helpers/inbound/parse.py - """ - - def __init__(self, request): - self._keys = [ - "attachments", - "headers", - "text", - "envelope", - "to", - "html", - "sender_ip", - "attachment-info", - "subject", - "dkim", - "SPF", - "charsets", - "content-ids", - "spam_report", - "spam_score", - "email", - ] - self._request = request - self._payload = request.POST.dict() - self._raw_payload = request.POST - - def key_values(self): - """ - Return a dictionary of key/values in the payload received from - the webhook - """ - key_values = {} - for key in self.keys: - if key in self.payload: - key_values[key] = self.payload[key] - return key_values - - def get_raw_email(self): - """ - This only applies to raw payloads: - https://sendgrid.com/docs/Classroom/Basics/Inbound_Parse_Webhook/setting_up_the_inbound_parse_webhook.html#-Raw-Parameters - """ - if "email" in self.payload: - raw_email = email.message_from_string(self.payload["email"]) - return raw_email - else: - return None - - def attachments(self): - """Returns an object with: - type = file content type - file_name = the name of the file - contents = base64 encoded file contents""" - attachments = None - if "attachment-info" in self.payload: - attachments = self._get_attachments(self.request) - # Check if we have a raw message - raw_email = self.get_raw_email() - if raw_email is not None: - attachments = self._get_attachments_raw(raw_email) - return attachments - - def _get_attachments(self, request): - attachments = [] - for _, filestorage in iteritems(request.files): - attachment = {} - if filestorage.filename not in (None, "fdopen", ""): - filename = secure_filename(filestorage.filename) - attachment["type"] = filestorage.content_type - attachment["file_name"] = filename - attachment["contents"] = base64.b64encode(filestorage.read()) - attachments.append(attachment) - return attachments - - def _get_attachments_raw(self, raw_email): - attachments = [] - counter = 1 - for part in raw_email.walk(): - attachment = {} - if part.get_content_maintype() == "multipart": - continue - filename = part.get_filename() - if not filename: - ext = mimetypes.guess_extension(part.get_content_type()) - if not ext: - ext = ".bin" - filename = "part-%03d%s" % (counter, ext) - counter += 1 - attachment["type"] = part.get_content_type() - attachment["file_name"] = filename - attachment["contents"] = part.get_payload(decode=False) - attachments.append(attachment) - return attachments - - @property - def keys(self): - return self._keys - - @property - def request(self): - return self._request - - @property - def payload(self): - return self._payload - - @property - def raw_payload(self): - return self._raw_payload diff --git a/engine/apps/sendgridapp/permissions.py b/engine/apps/sendgridapp/permissions.py deleted file mode 100644 index 7c2206e3..00000000 --- a/engine/apps/sendgridapp/permissions.py +++ /dev/null @@ -1,14 +0,0 @@ -from rest_framework.permissions import BasePermission - -from apps.base.utils import live_settings - - -class AllowOnlySendgrid(BasePermission): - def has_permission(self, request, view): - # https://stackoverflow.com/questions/20865673/sendgrid-incoming-mail-webhook-how-do-i-secure-my-endpoint - sendgrid_key = request.query_params.get("key") - - if sendgrid_key is None: - return False - - return live_settings.SENDGRID_SECRET_KEY == sendgrid_key diff --git a/engine/apps/sendgridapp/templates/email_notification.html b/engine/apps/sendgridapp/templates/email_notification.html deleted file mode 100644 index e591944d..00000000 --- a/engine/apps/sendgridapp/templates/email_notification.html +++ /dev/null @@ -1,26 +0,0 @@ - - -{% now "H:i.u e"%} - -You are invited to check Incident -

-{{ title }} -{% if message %} - {{ message|linebreaks }} -{% endif %} -{#
#} -{#image#} -Amixr team: {{ amixr_team }} -
-Alert channel: {{ alert_channel }} -

-Check Incident -

-Your Amixr.IO -{% if limit_notification %} -

- {{ emails_left }} mail(s) left for this week. Contact your admin. -{% endif %} - -{% now "H:i.u e"%} - \ No newline at end of file diff --git a/engine/apps/sendgridapp/templates/email_verification.html b/engine/apps/sendgridapp/templates/email_verification.html deleted file mode 100644 index 468daf41..00000000 --- a/engine/apps/sendgridapp/templates/email_verification.html +++ /dev/null @@ -1,15 +0,0 @@ - -{% now "H:i.u e"%} - -Welcome to OnCall! -

-To verify your email address, please click the button below. If you did not sign up for OnCall, please ignore this email. -
-Confirm email -

-Thanks, -
-OnCall Team - -{% now "H:i.u e"%} - \ No newline at end of file diff --git a/engine/apps/sendgridapp/tests/factories.py b/engine/apps/sendgridapp/tests/factories.py deleted file mode 100644 index e27fd458..00000000 --- a/engine/apps/sendgridapp/tests/factories.py +++ /dev/null @@ -1,8 +0,0 @@ -# import factory -# -# from apps.sendgridapp.models import EmailMessage -# -# -# class EmailMessageFactory(factory.DjangoModelFactory): -# class Meta: -# model = EmailMessage diff --git a/engine/apps/sendgridapp/tests/test_emails.py b/engine/apps/sendgridapp/tests/test_emails.py deleted file mode 100644 index 7d29c428..00000000 --- a/engine/apps/sendgridapp/tests/test_emails.py +++ /dev/null @@ -1,135 +0,0 @@ -# from unittest.mock import patch -# -# import pytest -# from django.urls import reverse -# from django.utils import timezone -# from rest_framework.test import APIClient -# -# from apps.sendgridapp.constants import SendgridEmailMessageStatuses -# from apps.sendgridapp.verification_token import email_verification_token_generator -# -# -# @pytest.mark.skip(reason="email disabled") -# @pytest.mark.django_db -# def test_email_verification( -# make_team, -# make_user_for_team, -# make_email_message, -# make_alert_receive_channel, -# make_alert_group, -# ): -# amixr_team = make_team() -# admin = make_user_for_team(amixr_team, role=ROLE_ADMIN) -# alert_receive_channel = make_alert_receive_channel(amixr_team) -# alert_group = make_alert_group(alert_receive_channel) -# make_email_message( -# receiver=admin, status=SendgridEmailMessageStatuses.ACCEPTED, represents_alert_group=alert_group -# ), -# client = APIClient() -# correct_token = email_verification_token_generator.make_token(admin) -# url = reverse("sendgridapp:verify_email", kwargs={"token": correct_token, "uid": admin.pk, "slackteam": None}) -# response = client.get(url, content_type="application/json") -# assert response.status_code == 200 -# admin.refresh_from_db() -# assert admin.email_verified is True -# -# -# @pytest.mark.skip(reason="email disabled") -# @pytest.mark.django_db -# def test_email_verification_incorrect_token( -# make_team, -# make_user_for_team, -# make_email_message, -# make_alert_receive_channel, -# make_alert_group, -# ): -# amixr_team = make_team() -# admin = make_user_for_team(amixr_team, role=ROLE_ADMIN) -# alert_receive_channel = make_alert_receive_channel(amixr_team) -# alert_group = make_alert_group(alert_receive_channel) -# make_email_message( -# receiver=admin, status=SendgridEmailMessageStatuses.ACCEPTED, represents_alert_group=alert_group -# ), -# -# client = APIClient() -# url = reverse("sendgridapp:verify_email", kwargs={"token": "incorrect_token", "uid": admin.pk, "slackteam": None}) -# -# response = client.get(path=url, content_type="application/json") -# assert response.status_code == 403 -# admin.refresh_from_db() -# assert admin.email_verified is False -# -# -# @pytest.mark.skip(reason="email disabled") -# @pytest.mark.django_db -# def test_email_verification_incorrect_uid( -# make_team, -# make_user_for_team, -# make_email_message, -# make_alert_receive_channel, -# make_alert_group, -# ): -# amixr_team = make_team() -# admin = make_user_for_team(amixr_team, role=ROLE_ADMIN) -# alert_receive_channel = make_alert_receive_channel(amixr_team) -# alert_group = make_alert_group(alert_receive_channel) -# make_email_message( -# receiver=admin, status=SendgridEmailMessageStatuses.ACCEPTED, represents_alert_group=alert_group -# ), -# client = APIClient() -# -# correct_token = email_verification_token_generator.make_token(admin) -# url = reverse( -# "sendgridapp:verify_email", kwargs={"token": correct_token, "uid": 100, "slackteam": None} # incorrect user uid -# ) -# response = client.get(path=url, content_type="application/json") -# assert response.status_code == 403 -# admin.refresh_from_db() -# assert admin.email_verified is False -# -# -# @pytest.mark.skip(reason="email disabled") -# @patch("apps.integrations.helpers.inbound_emails.AllowOnlySendgrid.has_permission", return_value=True) -# @patch( -# "apps.slack.helpers.slack_client.SlackClientWithErrorHandling.api_call", -# return_value={"ok": True, "ts": timezone.now().timestamp()}, -# ) -# @pytest.mark.django_db -# @pytest.mark.parametrize("status", ["delivered", "bounce", "dropped"]) -# def test_update_email_status( -# mocked_slack_api_call, -# mocked_sendgrid_permission, -# make_team, -# make_user_for_team, -# make_email_message, -# make_alert_receive_channel, -# make_alert_group, -# status, -# ): -# """The test for Email message status update via api""" -# amixr_team = make_team() -# admin = make_user_for_team(amixr_team, role=ROLE_ADMIN) -# alert_receive_channel = make_alert_receive_channel(amixr_team) -# alert_group = make_alert_group(alert_receive_channel) -# email_message = make_email_message( -# receiver=admin, status=SendgridEmailMessageStatuses.ACCEPTED, represents_alert_group=alert_group -# ) -# client = APIClient() -# url = reverse("sendgridapp:email_status_event") -# -# data = [ -# { -# "message_uuid": str(email_message.message_uuid), -# "event": status, -# } -# ] -# response = client.post( -# url, -# data, -# format="json", -# ) -# -# assert response.status_code == 204 -# assert response.data == "" -# email_message.refresh_from_db() -# assert email_message.status == SendgridEmailMessageStatuses.DETERMINANT[status] diff --git a/engine/apps/sendgridapp/urls.py b/engine/apps/sendgridapp/urls.py deleted file mode 100644 index 1419df32..00000000 --- a/engine/apps/sendgridapp/urls.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.urls import path - -from apps.sendgridapp.views import EmailStatusCallback - -app_name = "sendgridapp" - -urlpatterns = [ - path(r"email_status_event/", EmailStatusCallback.as_view(), name="email_status_event"), -] diff --git a/engine/apps/sendgridapp/verification_token.py b/engine/apps/sendgridapp/verification_token.py deleted file mode 100644 index 3efc97c7..00000000 --- a/engine/apps/sendgridapp/verification_token.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Based on example https://simpleisbetterthancomplex.com/tutorial/2016/08/24/how-to-create-one-time-link.html""" - -from django.conf import settings -from django.contrib.auth.tokens import PasswordResetTokenGenerator - - -class EmailVerificationTokenGenerator(PasswordResetTokenGenerator): - # There are the default setting of PASSWORD_RESET_TIMEOUT_DAYS = 3 (days) - - key_salt = "EmailVerificationTokenGenerator" + settings.TOKEN_SALT - secret = settings.TOKEN_SECRET - - def _make_hash_value(self, user, timestamp): - team_datetime_timestamp = ( - "" if user.teams.first() is None else user.teams.first().datetime.replace(microsecond=0, tzinfo=None) - ) - return str(user.pk) + str(timestamp) + str(team_datetime_timestamp) + str(user.email_verified) - - -email_verification_token_generator = EmailVerificationTokenGenerator() diff --git a/engine/apps/sendgridapp/views.py b/engine/apps/sendgridapp/views.py deleted file mode 100644 index 538df2d8..00000000 --- a/engine/apps/sendgridapp/views.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging - -from django.apps import apps -from rest_framework import status -from rest_framework.response import Response -from rest_framework.views import APIView - -from apps.sendgridapp.permissions import AllowOnlySendgrid - -logger = logging.getLogger(__name__) - - -# Receive Email Status Update from Sendgrid -class EmailStatusCallback(APIView): - # https://sendgrid.com/docs/for-developers/tracking-events/event/#delivery-events - permission_classes = [AllowOnlySendgrid] - - def post(self, request): - for data in request.data: - message_uuid = data.get("message_uuid") - message_status = data.get("event") - if message_status is not None and "type" in message_status: - message_status = message_status["type"] - logger.info(f"UUID: {message_uuid}, Status: {message_status}") - - EmailMessage = apps.get_model("sendgridapp", "EmailMessage") - EmailMessage.objects.update_status(message_uuid=message_uuid, message_status=message_status) - - return Response(data="", status=status.HTTP_204_NO_CONTENT) diff --git a/engine/apps/slack/scenarios/alertgroup_appearance.py b/engine/apps/slack/scenarios/alertgroup_appearance.py index 8a335fcd..7526843f 100644 --- a/engine/apps/slack/scenarios/alertgroup_appearance.py +++ b/engine/apps/slack/scenarios/alertgroup_appearance.py @@ -90,7 +90,7 @@ class OpenAlertAppearanceDialogStep( blocks.append(block) blocks.append({"type": "divider"}) - for notification_channel in ["slack", "web", "sms", "phone_call", "email", "telegram"]: + for notification_channel in ["slack", "web", "sms", "phone_call", "telegram"]: blocks.append( { "type": "header", @@ -236,7 +236,7 @@ class UpdateAppearanceStep(scenario_step.ScenarioStep): prev_state = alert_receive_channel.insight_logs_serialized for templatizable_attr in ["title", "message", "image_url"]: - for notification_channel in ["slack", "web", "sms", "phone_call", "email", "telegram"]: + for notification_channel in ["slack", "web", "sms", "phone_call", "telegram"]: attr_name = f"{notification_channel}_{templatizable_attr}_template" try: old_value = getattr(alert_receive_channel, attr_name) diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index fd37ba81..a6f76102 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -218,6 +218,7 @@ class Organization(MaintainableObject): def sms_left(self, user): return self.subscription_strategy.sms_left(user) + # todo: manage backend specific limits in messaging backend def emails_left(self, user): return self.subscription_strategy.emails_left(user) diff --git a/engine/apps/user_management/subscription_strategy/free_public_beta_subscription_strategy.py b/engine/apps/user_management/subscription_strategy/free_public_beta_subscription_strategy.py index 32f9fa69..dcd0be9f 100644 --- a/engine/apps/user_management/subscription_strategy/free_public_beta_subscription_strategy.py +++ b/engine/apps/user_management/subscription_strategy/free_public_beta_subscription_strategy.py @@ -1,6 +1,7 @@ -from datetime import datetime - from django.apps import apps +from django.utils import timezone + +from apps.email.models import EmailMessage from .base_subsription_strategy import BaseSubscriptionStrategy @@ -21,17 +22,16 @@ class FreePublicBetaSubscriptionStrategy(BaseSubscriptionStrategy): def sms_left(self, user): return self._calculate_phone_notifications_left(user) + # todo: manage backend specific limits in messaging backend def emails_left(self, user): - # Email notifications are disabled now. - EmailMessage = apps.get_model("sendgridapp", "EmailMessage") - now = datetime.now() + now = timezone.now() day_start = now.replace(hour=0, minute=0, second=0, microsecond=0) - emails_this_week = EmailMessage.objects.filter( + emails_today = EmailMessage.objects.filter( created_at__gte=day_start, represents_alert_group__channel__organization=self.organization, receiver=user, ).count() - return self._emails_limit - emails_this_week + return self._emails_limit - emails_today def notifications_limit_web_report(self, user): limits_to_show = [] @@ -59,7 +59,7 @@ class FreePublicBetaSubscriptionStrategy(BaseSubscriptionStrategy): """ PhoneCall = apps.get_model("twilioapp", "PhoneCall") SMSMessage = apps.get_model("twilioapp", "SMSMessage") - now = datetime.now() + now = timezone.now() day_start = now.replace(hour=0, minute=0, second=0, microsecond=0) calls_today = PhoneCall.objects.filter( created_at__gte=day_start, diff --git a/engine/apps/user_management/tests/test_free_public_beta_subcription_strategy.py b/engine/apps/user_management/tests/test_free_public_beta_subcription_strategy.py index 0f309570..c26b4216 100644 --- a/engine/apps/user_management/tests/test_free_public_beta_subcription_strategy.py +++ b/engine/apps/user_management/tests/test_free_public_beta_subcription_strategy.py @@ -1,8 +1,5 @@ -import sys - import pytest -from apps.sendgridapp.constants import SendgridEmailMessageStatuses from apps.twilioapp.constants import TwilioCallStatuses, TwilioMessageStatuses from common.constants.role import Role @@ -65,7 +62,6 @@ def test_phone_calls_and_sms_counts_together( assert organization.sms_left(user) == organization.subscription_strategy._phone_notifications_limit -@pytest.mark.skip(reason="email disabled") @pytest.mark.django_db def test_emails_left( make_organization, @@ -75,10 +71,11 @@ def test_emails_left( make_alert_group, ): organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) + user = make_user_for_organization(organization) + alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) - make_email_message( - receiver=admin, status=SendgridEmailMessageStatuses.DELIVERED, represents_alert_group=alert_group - ), - assert organization.emails_left(admin) == sys.maxsize + + make_email_message(receiver=user, represents_alert_group=alert_group) + + assert organization.emails_left(user) == organization.subscription_strategy._emails_limit - 1 diff --git a/engine/common/api_helpers/mixins.py b/engine/common/api_helpers/mixins.py index cddecdc7..3af06bcc 100644 --- a/engine/common/api_helpers/mixins.py +++ b/engine/common/api_helpers/mixins.py @@ -11,7 +11,6 @@ from rest_framework.exceptions import NotFound, Throttled from rest_framework.response import Response from apps.alerts.incident_appearance.templaters import ( - AlertEmailTemplater, AlertPhoneCallTemplater, AlertSlackTemplater, AlertSmsTemplater, @@ -245,9 +244,8 @@ SLACK = "slack" WEB = "web" PHONE_CALL = "phone_call" SMS = "sms" -EMAIL = "email" TELEGRAM = "telegram" -NOTIFICATION_CHANNEL_OPTIONS = [SLACK, WEB, PHONE_CALL, SMS, EMAIL, TELEGRAM] +NOTIFICATION_CHANNEL_OPTIONS = [SLACK, WEB, PHONE_CALL, SMS, TELEGRAM] TITLE = "title" MESSAGE = "message" IMAGE_URL = "image_url" @@ -261,7 +259,6 @@ NOTIFICATION_CHANNEL_TO_TEMPLATER_MAP = { WEB: AlertWebTemplater, PHONE_CALL: AlertPhoneCallTemplater, SMS: AlertSmsTemplater, - EMAIL: AlertEmailTemplater, TELEGRAM: AlertTelegramTemplater, } diff --git a/engine/config_integrations/alertmanager.py b/engine/config_integrations/alertmanager.py index cc356e26..b2ff3de0 100644 --- a/engine/config_integrations/alertmanager.py +++ b/engine/config_integrations/alertmanager.py @@ -73,22 +73,6 @@ web_image_url = slack_image_url sms_title = '{{ payload.get("labels", {}).get("alertname", "Title undefined") }}' phone_call_title = sms_title -email_title = web_title - -email_message = """\ -{{- payload.messsage }} -{%- if "status" in payload -%} -**Status**: {{ payload.status }} -{% endif -%} -**Labels:** {% for k, v in payload["labels"].items() %} -{{ k }}: {{ v }}{% endfor %} -**Annotations:** -{%- for k, v in payload.get("annotations", {}).items() %} -{#- render annotation as markdown url if it starts with http #} -{{ k }}: {{v}} -{% endfor %} -""" # noqa: W291 - telegram_title = sms_title telegram_message = """\ @@ -188,23 +172,6 @@ tests = { "phone_call": { "title": "KubeJobCompletion", }, - "email": { - "title": "KubeJobCompletion", - "message": ( - "**Status**: firing\n" - "**Labels:** \n" - "job: kube-state-metrics\n" - "instance: 10.143.139.7:8443\n" - "job_name: email-tracking-perform-initialization-1.0.50\n" - "severity: warning\n" - "alertname: KubeJobCompletion\n" - "namespace: default\n" - "prometheus: monitoring/k8s\n" - "**Annotations:**\n" - "message: Job default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete.\n\n" - "runbook_url: https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-kubejobcompletion\n" - ), - }, "telegram": { "title": "KubeJobCompletion", "message": ( diff --git a/engine/config_integrations/elastalert.py b/engine/config_integrations/elastalert.py index 73320d53..bb5dc8db 100644 --- a/engine/config_integrations/elastalert.py +++ b/engine/config_integrations/elastalert.py @@ -36,10 +36,6 @@ sms_title = web_title phone_call_title = sms_title -email_title = web_title - -email_message = "{{ payload|tojson_pretty }}" - telegram_title = sms_title telegram_message = "{{ payload|tojson_pretty }}" diff --git a/engine/config_integrations/formatted_webhook.py b/engine/config_integrations/formatted_webhook.py index 6847639f..fb6e3061 100644 --- a/engine/config_integrations/formatted_webhook.py +++ b/engine/config_integrations/formatted_webhook.py @@ -32,10 +32,6 @@ sms_title = web_title phone_call_title = sms_title -email_title = web_title - -email_message = slack_message - telegram_title = sms_title telegram_message = slack_message diff --git a/engine/config_integrations/grafana.py b/engine/config_integrations/grafana.py index 4feefd61..cc35a778 100644 --- a/engine/config_integrations/grafana.py +++ b/engine/config_integrations/grafana.py @@ -81,29 +81,6 @@ sms_title = """\ phone_call_title = sms_title -email_title = web_title - -email_message = """\ -{{- payload.message }} -{%- for value in payload.get("evalMatches", []) %} -**{{ value.metric }}**: {{ value.value }} -{% endfor -%} -{%- if "status" in payload -%} -**Status**: {{ payload.status }} -{% endif -%} -{%- if "labels" in payload -%} -**Labels:** {% for k, v in payload["labels"].items() %} -{{ k }}: {{ v }}{% endfor %} -{% endif -%} -{%- if "annotations" in payload -%} -**Annotations:** -{%- for k, v in payload.get("annotations", {}).items() %} -{#- render annotation as markdown url if it starts with http #} -{{ k }}: {{v}} -{% endfor %} -{%- endif -%} -""" - telegram_title = sms_title telegram_message = """\ @@ -215,23 +192,6 @@ tests = { "phone_call": { "title": "KubeJobCompletion", }, - "email": { - "title": "KubeJobCompletion", - "message": ( - "**Status**: firing\n" - "**Labels:** \n" - "job: kube-state-metrics\n" - "instance: 10.143.139.7:8443\n" - "job_name: email-tracking-perform-initialization-1.0.50\n" - "severity: warning\n" - "alertname: KubeJobCompletion\n" - "namespace: default\n" - "prometheus: monitoring/k8s\n" - "**Annotations:**\n" - "message: Job default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete.\n\n" - "runbook_url: https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-kubejobcompletion\n" - ), - }, "telegram": { "title": "KubeJobCompletion", "message": ( diff --git a/engine/config_integrations/grafana_alerting.py b/engine/config_integrations/grafana_alerting.py index 4eac0135..e44aaaae 100644 --- a/engine/config_integrations/grafana_alerting.py +++ b/engine/config_integrations/grafana_alerting.py @@ -77,22 +77,6 @@ web_image_url = slack_image_url sms_title = '{{ payload.get("labels", {}).get("alertname", "Title undefined") }}' phone_call_title = sms_title -email_title = web_title - -email_message = """\ -{{- payload.messsage }} -{%- if "status" in payload -%} -**Status**: {{ payload.status }} -{% endif -%} -**Labels:** {% for k, v in payload["labels"].items() %} -{{ k }}: {{ v }}{% endfor %} -**Annotations:** -{%- for k, v in payload.get("annotations", {}).items() %} -{#- render annotation as markdown url if it starts with http #} -{{ k }}: {{v}} -{% endfor %} -""" # noqa:W291 - telegram_title = sms_title telegram_message = """\ @@ -191,23 +175,6 @@ tests = { "phone_call": { "title": "KubeJobCompletion", }, - "email": { - "title": "KubeJobCompletion", - "message": ( - "**Status**: firing\n" - "**Labels:** \n" - "job: kube-state-metrics\n" - "instance: 10.143.139.7:8443\n" - "job_name: email-tracking-perform-initialization-1.0.50\n" - "severity: warning\n" - "alertname: KubeJobCompletion\n" - "namespace: default\n" - "prometheus: monitoring/k8s\n" - "**Annotations:**\n" - "message: Job default/email-tracking-perform-initialization-1.0.50 is taking more than one hour to complete.\n\n" - "runbook_url: https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-kubejobcompletion\n" - ), - }, "telegram": { "title": "KubeJobCompletion", "message": ( diff --git a/engine/config_integrations/inbound_email.py b/engine/config_integrations/inbound_email.py index 4ecac8e4..7ddfa0e7 100644 --- a/engine/config_integrations/inbound_email.py +++ b/engine/config_integrations/inbound_email.py @@ -32,10 +32,6 @@ sms_title = web_title phone_call_title = web_title -email_title = web_title - -email_message = slack_message - telegram_title = sms_title telegram_message = slack_message diff --git a/engine/config_integrations/kapacitor.py b/engine/config_integrations/kapacitor.py index 3d761766..fd76c695 100644 --- a/engine/config_integrations/kapacitor.py +++ b/engine/config_integrations/kapacitor.py @@ -38,10 +38,6 @@ sms_title = web_title phone_call_title = web_title -email_title = web_title - -email_message = slack_message - telegram_title = sms_title telegram_message = "{{ payload|tojson_pretty }}" diff --git a/engine/config_integrations/maintenance.py b/engine/config_integrations/maintenance.py index d27405ef..60b8710b 100644 --- a/engine/config_integrations/maintenance.py +++ b/engine/config_integrations/maintenance.py @@ -32,10 +32,6 @@ sms_title = web_title phone_call_title = sms_title -email_title = web_title - -email_message = slack_message - telegram_title = sms_title telegram_message = slack_message diff --git a/engine/config_integrations/manual.py b/engine/config_integrations/manual.py index fdcaadaa..416be3e6 100644 --- a/engine/config_integrations/manual.py +++ b/engine/config_integrations/manual.py @@ -41,10 +41,6 @@ sms_title = web_title phone_call_title = sms_title -email_title = web_title - -email_message = slack_message - telegram_title = sms_title telegram_message = slack_message diff --git a/engine/config_integrations/webhook.py b/engine/config_integrations/webhook.py index 4a3b0b73..823bc837 100644 --- a/engine/config_integrations/webhook.py +++ b/engine/config_integrations/webhook.py @@ -36,10 +36,6 @@ sms_title = web_title phone_call_title = sms_title -email_title = web_title - -email_message = "{{ payload|tojson_pretty }}" - telegram_title = sms_title telegram_message = "{{ payload|tojson_pretty }}" diff --git a/engine/conftest.py b/engine/conftest.py index 68ef50d5..8291d921 100644 --- a/engine/conftest.py +++ b/engine/conftest.py @@ -44,6 +44,7 @@ from apps.base.tests.factories import ( UserNotificationPolicyFactory, UserNotificationPolicyLogRecordFactory, ) +from apps.email.tests.factories import EmailMessageFactory from apps.heartbeat.tests.factories import IntegrationHeartBeatFactory from apps.schedules.tests.factories import ( CustomOnCallShiftFactory, @@ -105,7 +106,7 @@ register(ResolutionNoteSlackMessageFactory) register(PhoneCallFactory) register(SMSFactory) -# register(EmailMessageFactory) +register(EmailMessageFactory) register(IntegrationHeartBeatFactory) @@ -627,13 +628,12 @@ def make_sms(): return _make_sms -# TODO: restore email notifications -# @pytest.fixture() -# def make_email_message(): -# def _make_email_message(receiver, status, **kwargs): -# return EmailMessageFactory(receiver=receiver, status=status, **kwargs) -# -# return _make_email_message +@pytest.fixture() +def make_email_message(): + def _make_email_message(receiver, **kwargs): + return EmailMessageFactory(receiver=receiver, **kwargs) + + return _make_email_message @pytest.fixture() diff --git a/engine/engine/urls.py b/engine/engine/urls.py index 518c5608..e8080a7f 100644 --- a/engine/engine/urls.py +++ b/engine/engine/urls.py @@ -36,7 +36,6 @@ urlpatterns = [ path("api/internal/v1/", include("apps.social_auth.urls", namespace="social_auth")), path("integrations/v1/", include("apps.integrations.urls", namespace="integrations")), path("twilioapp/", include("apps.twilioapp.urls")), - # path('sendgridapp/', include('apps.sendgridapp.urls')), TODO: restore email notifications path("api/v1/", include("apps.public_api.urls", namespace="api-public")), path("api/internal/v1/", include("apps.migration_tool.urls", namespace="migration-tool")), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/engine/requirements.txt b/engine/requirements.txt index 950c1d1e..5232c646 100644 --- a/engine/requirements.txt +++ b/engine/requirements.txt @@ -23,7 +23,6 @@ recurring-ical-events==0.1.16b0 slack-export-viewer==1.0.0 beautifulsoup4==4.8.1 social-auth-app-django==3.1.0 -sendgrid==6.1.2 cryptography==3.3.2 pytest==5.4.3 pytest-django==3.9.0 diff --git a/engine/settings/base.py b/engine/settings/base.py index 2dfeaa30..92e759ed 100644 --- a/engine/settings/base.py +++ b/engine/settings/base.py @@ -64,14 +64,6 @@ TWILIO_VERIFY_SERVICE_SID = os.environ.get("TWILIO_VERIFY_SERVICE_SID") TELEGRAM_WEBHOOK_HOST = os.environ.get("TELEGRAM_WEBHOOK_HOST", BASE_URL) TELEGRAM_TOKEN = os.environ.get("TELEGRAM_TOKEN") -# For Sending email -SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY") -SENDGRID_FROM_EMAIL = os.environ.get("SENDGRID_FROM_EMAIL") - -# For Inbound email -SENDGRID_SECRET_KEY = os.environ.get("SENDGRID_SECRET_KEY") -SENDGRID_INBOUND_EMAIL_DOMAIN = os.environ.get("SENDGRID_INBOUND_EMAIL_DOMAIN") - # For Grafana Cloud integration GRAFANA_CLOUD_ONCALL_API_URL = os.environ.get( "GRAFANA_CLOUD_ONCALL_API_URL", "https://oncall-prod-us-central-0.grafana.net/oncall" @@ -198,13 +190,13 @@ INSTALLED_APPS = [ "apps.integrations", "apps.schedules", "apps.heartbeat", + "apps.email", "apps.slack", "apps.telegram", "apps.twilioapp", "apps.api", "apps.api_for_grafana_incident", "apps.base", - # "apps.sendgridapp", TODO: restore email notifications "apps.auth_token", "apps.public_api", "apps.grafana_plugin", @@ -570,6 +562,18 @@ SLOW_THRESHOLD_SECONDS = 2.0 EXTRA_MESSAGING_BACKENDS = [] +# Email messaging backend +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = os.getenv("EMAIL_HOST") +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") +EMAIL_PORT = getenv_integer("EMAIL_PORT", 587) +EMAIL_USE_TLS = getenv_boolean("EMAIL_USE_TLS", True) +DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL") + +if FEATURE_EMAIL_INTEGRATION_ENABLED: + EXTRA_MESSAGING_BACKENDS = [("apps.email.backend.EmailBackend", 8)] + INSTALLED_ONCALL_INTEGRATIONS = [ "config_integrations.alertmanager", "config_integrations.grafana", diff --git a/engine/settings/ci-test.py b/engine/settings/ci-test.py index 7af883d3..23439c13 100644 --- a/engine/settings/ci-test.py +++ b/engine/settings/ci-test.py @@ -36,8 +36,6 @@ if BROKER_TYPE != BrokerTypes.REDIS: # Dummy Telegram token (fake one) TELEGRAM_TOKEN = "0000000000:XXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXX" -SENDGRID_FROM_EMAIL = "dummy_sendgrid_from_email@test.ci-test" -SENDGRID_SECRET_KEY = "dummy_sendgrid_secret_key" TWILIO_ACCOUNT_SID = "dummy_twilio_account_sid" TWILIO_AUTH_TOKEN = "dummy_twilio_auth_token"