Restore email notifications (#621)

* remove email verification related code

* remove email verification related code

* remove sendgrid callback

* remove sendgrid related code

* remove sendgrid related code

* rename sendgrid app to email

* remove email from built-in channels

* remove email from built-in channels

* remove email from built-in channels

* add email backend: https://github.com/grafana/oncall/pull/50

* add email templater

* add email templater

* convert md to html

* add email settings to live settings

* use task to send email, handle some exceptions to create logs

* remove ERROR_NOTIFICATION_MAIL_DELIVERY_FAILED usage

* add email limit logic

* fix tests

* add docs

* remove old email templates

* remove old email templates

* add template_fields to messaging backend

* add messaging backends templates to public api

* add comment for deprecated fields

* fix test

* fix tests

* disable email by default

* don't retry on SMTPException and TimeoutError

* add tests

* bring email back to public api docs

* return ERROR_NOTIFICATION_MAIL_LIMIT_EXCEEDED

* make template_fields tuple

* build_subject_and_title -> build_subject_and_message

* add one more comment about template deprecation

* use 8 as backend id

* add comment about gaierror and BadHeaderError

* add comment on importing in notify_user_async

* edit oss docs
This commit is contained in:
Vadim Stepanov 2022-10-19 12:32:56 +01:00 committed by GitHub
parent e36757b293
commit e67d3519fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 590 additions and 1068 deletions

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -59,8 +59,6 @@ class IntegrationOptionsMixin:
"web_title",
"web_message",
"web_image_url",
"email_title",
"email_message",
"sms_title",
"phone_call_title",
"telegram_title",

View file

@ -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",

View file

@ -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.

View file

@ -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,
}

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

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

View file

@ -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 <a href='"
"https://grafana.com/docs/grafana-cloud/oncall/open-source/#slack-setup"
@ -98,16 +108,6 @@ class LiveSetting(models.Model):
"You can create a service in Twilio web interface. "
"twilio.com -> verify -> create new service."
),
"SENDGRID_API_KEY": (
"Sendgrid api key to send emails, "
"<a href='https://sendgrid.com/docs/ui/account-and-settings/api-keys/' target='_blank'>more info</a>."
),
"SENDGRID_FROM_EMAIL": (
"Address to send emails, <a href='https://sendgrid.com/docs/ui/sending-email/senders/' target='_blank'>"
"more info</a>."
),
"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 <a href='https://t.me/BotFather' target='_blank'>BotFather</a>."
),
@ -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",

View file

@ -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",
}

View file

@ -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:

View file

@ -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

View file

@ -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)}"

View file

@ -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

View file

@ -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
)

View file

@ -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')),
],
),
]

View file

@ -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)

View file

@ -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

View file

@ -0,0 +1,28 @@
<!DOCTYPE html>
You are invited to check an alert group in Grafana OnCall!
<br><br>
Organization: {{ organization }}
<br>
Integration: {{ integration }}
<br>
Title: {{ title }}
{% if message %}
<br>
Message:
<br>
{% autoescape off %}
{{ message }}
{% endautoescape %}
{% endif %}
<br>
<strong><a href="{{ url }}">Go to the alert group</a></strong>
<br>
Your Grafana OnCall
{% if limit_notification %}
<br><br>
<span style="color: #333333;"><em>{{ emails_left }} emails left for the organization today. Contact your admin.</em></span>
{% endif %}
<!-- this ensures Gmail doesn't trim the email -->
<span style="display:none;">{% now "H:i.u e"%}</span>
<!---->

View file

@ -0,0 +1,8 @@
import factory
from apps.email.models import EmailMessage
class EmailMessageFactory(factory.DjangoModelFactory):
class Meta:
model = EmailMessage

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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,

View file

@ -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,
}

View file

@ -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)

View file

@ -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", "<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

View file

@ -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

View file

@ -1,26 +0,0 @@
<!DOCTYPE html>
<!-- this ensures Gmail doesn't trim the email. Add this line at the beginning and end of email content. -->
<span style="display:none;">{% now "H:i.u e"%}</span>
<!---->
You are invited to check Incident
<br><br>
<strong><a href="{{ url }}">{{ title }}</a></strong>
{% if message %}
{{ message|linebreaks }}
{% endif %}
{#<br>#}
{#<img src="{{ image_url }}" alt="image" />#}
Amixr team: {{ amixr_team }}
<br>
Alert channel: {{ alert_channel }}
<br><br>
<strong><a href="{{ url }}">Check Incident</a></strong>
<br><br>
Your Amixr.IO
{% if limit_notification %}
<br><br>
<span style="color: #333333;"><em>{{ emails_left }} mail(s) left for this week. Contact your admin.</em></span>
{% endif %}
<!-- this ensures Gmail doesn't trim the email. Add this line at the beginning and end of email content. -->
<span style="display:none;">{% now "H:i.u e"%}</span>
<!---->

View file

@ -1,15 +0,0 @@
<!-- this ensures Gmail doesn't trim the email. Add this line at the beginning and end of email content. -->
<span style="display:none;">{% now "H:i.u e"%}</span>
<!---->
<strong>Welcome to OnCall!</strong>
<br><br>
To verify your email address, please click the button below. If you did not sign up for OnCall, please ignore this email.
<br>
<a href='{{ url }}' target='_blank'>Confirm email</a>
<br><br>
Thanks,
<br>
OnCall Team
<!-- this ensures Gmail doesn't trim the email. Add this line at the beginning and end of email content. -->
<span style="display:none;">{% now "H:i.u e"%}</span>
<!---->

View file

@ -1,8 +0,0 @@
# import factory
#
# from apps.sendgridapp.models import EmailMessage
#
#
# class EmailMessageFactory(factory.DjangoModelFactory):
# class Meta:
# model = EmailMessage

View file

@ -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]

View file

@ -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"),
]

View file

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

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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

View file

@ -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,
}

View file

@ -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": (

View file

@ -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 = "<code>{{ payload|tojson_pretty }}</code>"

View file

@ -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

View file

@ -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": (

View file

@ -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": (

View file

@ -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

View file

@ -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 = "<code>{{ payload|tojson_pretty }}</code>"

View file

@ -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

View file

@ -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

View file

@ -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 = "<code>{{ payload|tojson_pretty }}</code>"

View file

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

View file

@ -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)

View file

@ -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

View file

@ -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",

View file

@ -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"