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 %}
-{#
#}
-{#
#}
-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"