-
Notifications
You must be signed in to change notification settings - Fork 301
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Inbound email integration #837
Changes from 13 commits
13c3d5b
008c093
7d62445
e78f382
3245ca9
73035a0
8f1479f
b2dd277
d3465d8
7b6e7a8
18e2076
b712b83
5a6aab2
b8fa1d4
ef3f363
2d462b9
68293e6
2b1d613
f6a81ff
fcaf7df
d1dd54e
002e0a6
99ec063
c795810
2b52b63
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
--- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you also please add OSS configuration instruction here? https://grafana.com/docs/oncall/latest/open-source/#email-setup |
||
aliases: | ||
- add-inbound-email/ | ||
- /docs/oncall/latest/integrations/available-integrations/configure-inbound-email/ | ||
canonical: https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-inbound-email/ | ||
keywords: | ||
- Grafana Cloud | ||
- Alerts | ||
- Notifications | ||
- on-call | ||
title: Inbound Email integration for Grafana OnCall | ||
weight: 500 | ||
--- | ||
|
||
# Inbound Email integration for Grafana OnCall | ||
|
||
Inbound Email integration will consume emails from dedicated email address and make alert groups from them. | ||
|
||
## Configure Inbound Email integration for Grafana OnCall | ||
|
||
You must have an Admin role to create integrations in Grafana OnCall. | ||
|
||
1. In the **Integrations** tab, click **+ New integration to receive alerts**. | ||
2. Select **Inbound Email** from the list of available integrations. | ||
3. Get your dedicated email address in the **How to connect** window. | ||
|
||
## Grouping and auto-resolve | ||
|
||
Alert groups will be grouped by email subject and auto-resolved if email message text equals "OK". | ||
This behaviour can be modified via custom templates. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alert groups will be grouped by email subject and auto-resolved if the email message text equals "OK". |
||
|
||
Alerts from Inbound Email integration have followng payload: | ||
|
||
```json | ||
{ | ||
"subject": "<your_email_subject>", | ||
"message": "<your_email_message>", | ||
"sender": "<your_email_sender_address>" | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import logging | ||
|
||
from anymail.exceptions import AnymailWebhookValidationFailure | ||
from anymail.inbound import AnymailInboundMessage | ||
from anymail.signals import AnymailInboundEvent | ||
from anymail.webhooks import amazon_ses, mailgun, mailjet, mandrill, postal, postmark, sendgrid, sparkpost | ||
from django.http import HttpResponse, HttpResponseNotAllowed | ||
from rest_framework import status | ||
from rest_framework.request import Request | ||
from rest_framework.response import Response | ||
from rest_framework.views import APIView | ||
|
||
from apps.base.utils import live_settings | ||
from apps.integrations.mixins import AlertChannelDefiningMixin | ||
from apps.integrations.tasks import create_alert | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
# {<ESP name>: (<django-anymail inbound webhook view class>, <webhook secret argument name to pass to the view>), ...} | ||
INBOUND_EMAIL_ESP_OPTIONS = { | ||
"amazon_ses": (amazon_ses.AmazonSESInboundWebhookView, None), | ||
"mailgun": (mailgun.MailgunInboundWebhookView, "webhook_signing_key"), | ||
"mailjet": (mailjet.MailjetInboundWebhookView, "webhook_secret"), | ||
"mandrill": (mandrill.MandrillCombinedWebhookView, "webhook_key"), | ||
"postal": (postal.PostalInboundWebhookView, "webhook_key"), | ||
"postmark": (postmark.PostmarkInboundWebhookView, "webhook_secret"), | ||
"sendgrid": (sendgrid.SendGridInboundWebhookView, "webhook_secret"), | ||
"sparkpost": (sparkpost.SparkPostInboundWebhookView, "webhook_secret"), | ||
} | ||
|
||
|
||
class InboundEmailWebhookView(AlertChannelDefiningMixin, APIView): | ||
def dispatch(self, request): | ||
# http_method_names can't be used due to how AlertChannelDefiningMixin is implemented | ||
# todo: refactor AlertChannelDefiningMixin | ||
if not request.method.lower() in ["head", "post"]: | ||
return HttpResponseNotAllowed(permitted_methods=["head", "post"]) | ||
|
||
self._check_inbound_email_settings_set() | ||
|
||
# Some ESPs verify the webhook with a HEAD request at configuration time | ||
if request.method.lower() == "head": | ||
return HttpResponse(status=status.HTTP_200_OK) | ||
|
||
messages = get_messages_from_esp_request(request) | ||
if not messages: | ||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST) | ||
|
||
message = messages[0] | ||
integration_token = message.to[0].address.split("@")[0] | ||
return super().dispatch(request, alert_channel_key=integration_token) | ||
|
||
def _check_inbound_email_settings_set(self): | ||
""" | ||
Checks if INBOUND_EMAIL settings set. | ||
Returns BadRequest if not. | ||
""" | ||
# TODO: These settings should be checked before app start. | ||
if not live_settings.INBOUND_EMAIL_ESP: | ||
logger.error(f"InboundEmailWebhookView: INBOUND_EMAIL_ESP env variable must be set.") | ||
return HttpResponse( | ||
status=status.HTTP_400_BAD_REQUEST, | ||
) | ||
|
||
if not live_settings.INBOUND_EMAIL_DOMAIN: | ||
logger.error("InboundEmailWebhookView: INBOUND_EMAIL_DOMAIN env variable must be set.") | ||
return HttpResponse(status=status.HTTP_400_BAD_REQUEST) | ||
|
||
def post(self, request, alert_receive_channel): | ||
for email in get_messages_from_esp_request(request): | ||
subject = email.subject | ||
message = email.text.strip() | ||
sender = email.from_email.addr_spec | ||
|
||
payload = {"subject": subject, "message": message, "sender": sender} | ||
|
||
create_alert.delay( | ||
title=subject, | ||
message=message, | ||
alert_receive_channel_pk=alert_receive_channel.pk, | ||
image_url=None, | ||
link_to_upstream_details=None, | ||
integration_unique_data=request.data, | ||
raw_request_data=payload, | ||
) | ||
|
||
return Response("OK", status=status.HTTP_200_OK) | ||
|
||
|
||
def get_messages_from_esp_request(request: Request) -> list[AnymailInboundMessage]: | ||
view_class, secret_name = INBOUND_EMAIL_ESP_OPTIONS[live_settings.INBOUND_EMAIL_ESP] | ||
|
||
kwargs = {secret_name: live_settings.INBOUND_EMAIL_WEBHOOK_SECRET} if secret_name else {} | ||
view = view_class(**kwargs) | ||
|
||
try: | ||
view.run_validators(request) | ||
events = view.parse_events(request) | ||
except AnymailWebhookValidationFailure: | ||
return [] | ||
|
||
return [event.message for event in events if isinstance(event, AnymailInboundEvent)] | ||
|
||
|
||
def get_integration_token_from_email_message(message: AnymailInboundMessage): | ||
return message.to[0].address.split("@")[0] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import pytest | ||
from django.urls import reverse | ||
from rest_framework import status | ||
from rest_framework.test import APIClient | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_inbound_email_webhook_head( | ||
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.FEATURE_INBOUND_EMAIL_ENABLED = True | ||
settings.INBOUND_EMAIL_ESP = "mailgun" | ||
settings.INBOUND_EMAIL_DOMAIN = "test.test" | ||
client = APIClient() | ||
|
||
url = reverse("integrations:inbound_email_webhook") | ||
response = client.head(url) | ||
|
||
assert response.status_code == status.HTTP_200_OK |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,5 @@ | ||
<p>This integration will consume emails from dedicated email address and make incidents from them.</p> | ||
|
||
<p>It’s useful for:</p> | ||
<ol> | ||
<li>Service desk.</li> | ||
<li>Consuming alerts from other systems using emails as a message bus.</li> | ||
</ol> | ||
<p>Dedicated email address for incidents:</p> | ||
<h4>This is the dedicated email address to create alert groups:</h4> | ||
<pre><code class='code-multiline'>{{ alert_receive_channel.inbound_email }}</code></pre> | ||
|
||
<p><i>Fields:</i></p> | ||
<ul> | ||
<li><i>email_subject</i> - alert title;</li> | ||
<li><i>email_body</i> - alert details;</li> | ||
</ul> | ||
|
||
<a href="https://grafana.com/docs/oncall/latest/integrations/available-integrations/configure-inbound-email/">Docs</a> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,3 +50,4 @@ opentelemetry-exporter-otlp-proto-grpc==1.15.0 | |
pyroscope-io==0.8.1 | ||
django-dbconn-retry==0.1.7 | ||
django-ipware==4.0.2 | ||
django-anymail==8.6 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. License ok 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be refer to this PR? :)