mirror of
https://github.com/grafana/grafana.git
synced 2025-12-23 05:04:29 +08:00
Compare commits
4 Commits
docs/add-t
...
fix/placeh
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de1c2ef28e | ||
|
|
e51fb87cc2 | ||
|
|
7ba340f4a2 | ||
|
|
08f58b2346 |
@@ -2,19 +2,52 @@ package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/alerting/receivers"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
)
|
||||
|
||||
const (
|
||||
// placeholderEmailAddress is the default placeholder email address used when no real email is configured
|
||||
placeholderEmailAddress = "<example@email.com>"
|
||||
)
|
||||
|
||||
var logger = log.New("ngalert.notifier.sender")
|
||||
|
||||
type emailSender struct {
|
||||
ns notifications.Service
|
||||
}
|
||||
|
||||
// isPlaceholderEmail checks if the given email address is a placeholder that should not be sent
|
||||
func isPlaceholderEmail(email string) bool {
|
||||
trimmed := strings.TrimSpace(email)
|
||||
return trimmed == placeholderEmailAddress
|
||||
}
|
||||
|
||||
func (s emailSender) SendEmail(ctx context.Context, cmd *receivers.SendEmailSettings) error {
|
||||
// Filter out placeholder addresses from the recipient list (single loop)
|
||||
validRecipients := make([]string, 0, len(cmd.To))
|
||||
for _, addr := range cmd.To {
|
||||
if !isPlaceholderEmail(addr) {
|
||||
validRecipients = append(validRecipients, addr)
|
||||
} else {
|
||||
logger.Warn("Filtering out placeholder email address from recipients", "address", addr)
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid recipients remain, skip sending
|
||||
if len(validRecipients) == 0 {
|
||||
if len(cmd.To) > 0 {
|
||||
logger.Info("Skipping email notification to placeholder address(es). Please configure a valid email address in your contact point to receive alerts.", "addresses", cmd.To)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
sendEmailCommand := notifications.SendEmailCommand{
|
||||
To: cmd.To,
|
||||
To: validRecipients,
|
||||
SingleEmail: cmd.SingleEmail,
|
||||
Template: cmd.Template,
|
||||
Subject: cmd.Subject,
|
||||
|
||||
194
pkg/services/ngalert/notifier/sender_test.go
Normal file
194
pkg/services/ngalert/notifier/sender_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/alerting/receivers"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/notifications"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
func TestIsPlaceholderEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "placeholder with angle brackets",
|
||||
email: "<example@email.com>",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "not a placeholder - example@email.com without angle brackets",
|
||||
email: "example@email.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "placeholder with spaces",
|
||||
email: " <example@email.com> ",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "valid email",
|
||||
email: "user@example.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "another valid email",
|
||||
email: "admin@grafana.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
email: "",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isPlaceholderEmail(tt.email)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockNotificationService struct {
|
||||
sendEmailCalled bool
|
||||
lastCommand *notifications.SendEmailCommandSync
|
||||
}
|
||||
|
||||
func (m *mockNotificationService) SendEmailCommandHandlerSync(ctx context.Context, cmd *notifications.SendEmailCommandSync) error {
|
||||
m.sendEmailCalled = true
|
||||
m.lastCommand = cmd
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockNotificationService) SendEmailCommandHandler(ctx context.Context, cmd *notifications.SendEmailCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockNotificationService) SendWebhookSync(ctx context.Context, cmd *notifications.SendWebhookSync) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockNotificationService) SendResetPasswordEmail(ctx context.Context, cmd *notifications.SendResetPasswordEmailCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockNotificationService) ValidateResetPasswordCode(ctx context.Context, query *notifications.ValidateResetPasswordCodeQuery, userByLogin notifications.GetUserByLoginFunc) (*user.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockNotificationService) SendVerificationEmail(ctx context.Context, cmd *notifications.SendVerifyEmailCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestEmailSender_SendEmail_PlaceholderHandling(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
recipients []string
|
||||
expectSend bool
|
||||
expectedRecips []string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "all placeholder addresses - skip gracefully",
|
||||
recipients: []string{"<example@email.com>"},
|
||||
expectSend: false,
|
||||
expectedRecips: nil,
|
||||
description: "Should skip sending when all recipients are placeholders",
|
||||
},
|
||||
{
|
||||
name: "mixed valid and placeholder addresses",
|
||||
recipients: []string{"<example@email.com>", "user@example.com"},
|
||||
expectSend: true,
|
||||
expectedRecips: []string{"user@example.com"},
|
||||
description: "Should filter out placeholders and send to valid addresses",
|
||||
},
|
||||
{
|
||||
name: "all valid addresses",
|
||||
recipients: []string{"user@example.com", "admin@grafana.com"},
|
||||
expectSend: true,
|
||||
expectedRecips: []string{"user@example.com", "admin@grafana.com"},
|
||||
description: "Should send to all valid addresses",
|
||||
},
|
||||
{
|
||||
name: "placeholder with angle brackets filtered, others sent",
|
||||
recipients: []string{"example@email.com", "<example@email.com>", "valid@example.com"},
|
||||
expectSend: true,
|
||||
expectedRecips: []string{"example@email.com", "valid@example.com"},
|
||||
description: "Should filter out only placeholder with angle brackets and send to other addresses",
|
||||
},
|
||||
{
|
||||
name: "example@email.com without angle brackets is valid",
|
||||
recipients: []string{"example@email.com"},
|
||||
expectSend: true,
|
||||
expectedRecips: []string{"example@email.com"},
|
||||
description: "Should send to example@email.com when it doesn't have angle brackets",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockNS := &mockNotificationService{}
|
||||
sender := emailSender{ns: mockNS}
|
||||
|
||||
cmd := &receivers.SendEmailSettings{
|
||||
To: tt.recipients,
|
||||
Subject: "Test Subject",
|
||||
SingleEmail: true,
|
||||
}
|
||||
|
||||
err := sender.SendEmail(context.Background(), cmd)
|
||||
require.NoError(t, err, tt.description)
|
||||
|
||||
if tt.expectSend {
|
||||
assert.True(t, mockNS.sendEmailCalled, "Expected email to be sent but it was not")
|
||||
assert.Equal(t, tt.expectedRecips, mockNS.lastCommand.SendEmailCommand.To, "Recipients mismatch")
|
||||
} else {
|
||||
assert.False(t, mockNS.sendEmailCalled, "Expected email not to be sent but it was")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailSender_SendEmail_EmptyRecipients(t *testing.T) {
|
||||
mockNS := &mockNotificationService{}
|
||||
sender := emailSender{ns: mockNS}
|
||||
|
||||
cmd := &receivers.SendEmailSettings{
|
||||
To: []string{},
|
||||
Subject: "Test Subject",
|
||||
SingleEmail: true,
|
||||
}
|
||||
|
||||
err := sender.SendEmail(context.Background(), cmd)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, mockNS.sendEmailCalled, "Should not send email with empty recipient list")
|
||||
}
|
||||
|
||||
func TestEmailSender_SendEmail_EmbeddedContents(t *testing.T) {
|
||||
mockNS := &mockNotificationService{}
|
||||
sender := emailSender{ns: mockNS}
|
||||
|
||||
cmd := &receivers.SendEmailSettings{
|
||||
To: []string{"user@example.com"},
|
||||
Subject: "Test with embedded content",
|
||||
SingleEmail: true,
|
||||
EmbeddedContents: []receivers.EmbeddedContent{
|
||||
{Name: "image.png", Content: []byte("fake image data")},
|
||||
},
|
||||
}
|
||||
|
||||
err := sender.SendEmail(context.Background(), cmd)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, mockNS.sendEmailCalled, "Email should be sent")
|
||||
assert.Len(t, mockNS.lastCommand.SendEmailCommand.EmbeddedContents, 1, "Embedded content should be passed through")
|
||||
assert.Equal(t, "image.png", mockNS.lastCommand.SendEmailCommand.EmbeddedContents[0].Name)
|
||||
}
|
||||
@@ -74,6 +74,11 @@ func (sc *SmtpClient) sendMessage(ctx context.Context, dialer *gomail.Dialer, ms
|
||||
))
|
||||
defer span.End()
|
||||
|
||||
// Skip sending if there are no recipients
|
||||
if len(msg.To) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := sc.buildEmail(ctx, msg)
|
||||
|
||||
err := dialer.DialAndSend(m)
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
GrafanaChannelValues,
|
||||
ReceiverFormValues,
|
||||
} from '../../../types/receiver-form';
|
||||
import { hasPlaceholderEmail } from '../../../utils/receiver-form';
|
||||
import { OnCallIntegrationType } from '../grafanaAppReceivers/onCall/useOnCallIntegration';
|
||||
|
||||
import { ChannelOptions } from './ChannelOptions';
|
||||
@@ -70,6 +71,10 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
const onCallIntegrationType = watch(`${settingsFieldPath}.integration_type`);
|
||||
const isTestAvailable = onCallIntegrationType !== OnCallIntegrationType.NewIntegration;
|
||||
|
||||
// Check if email integration has placeholder addresses
|
||||
const channelValues = getValues(channelFieldPath) as GrafanaChannelValues | undefined;
|
||||
const isPlaceholderEmail = hasPlaceholderEmail(channelValues);
|
||||
|
||||
useEffect(() => {
|
||||
register(`${channelFieldPath}.__id`);
|
||||
/* Need to manually register secureFields or else they'll
|
||||
@@ -230,7 +235,22 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
{isTestable && onTest && isTestAvailable && (
|
||||
<Button size="xs" variant="secondary" type="button" onClick={() => handleTest()} icon="message">
|
||||
<Button
|
||||
disabled={isPlaceholderEmail}
|
||||
size="xs"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={() => handleTest()}
|
||||
icon="message"
|
||||
tooltip={
|
||||
isPlaceholderEmail
|
||||
? t(
|
||||
'alerting.channel-sub-form.test-disabled-placeholder',
|
||||
'Please configure a valid email address before testing'
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Trans i18nKey="alerting.channel-sub-form.test">Test</Trans>
|
||||
</Button>
|
||||
)}
|
||||
@@ -257,6 +277,20 @@ export function ChannelSubForm<R extends ChannelValues>({
|
||||
</div>
|
||||
{notifier && (
|
||||
<div className={styles.innerContent}>
|
||||
{isPlaceholderEmail && (
|
||||
<Alert
|
||||
title={t(
|
||||
'alerting.contact-points.email.placeholder-warning-title',
|
||||
'Configure a valid email address'
|
||||
)}
|
||||
severity="info"
|
||||
>
|
||||
<Trans i18nKey="alerting.contact-points.email.placeholder-warning-body">
|
||||
This contact point is using a placeholder email address (<Text variant="code">example@email.com</Text>
|
||||
). Please update it with a valid email address to receive alerts and enable testing.
|
||||
</Trans>
|
||||
</Alert>
|
||||
)}
|
||||
{showTelegramWarning && (
|
||||
<Alert
|
||||
title={t(
|
||||
|
||||
@@ -185,6 +185,7 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
|
||||
isOpen={!!testReceivers}
|
||||
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||
receivers={testReceivers}
|
||||
channelValues={testReceivers[0]?.grafana_managed_receiver_configs?.[0]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -4,13 +4,15 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Alert, Button, Label, Modal, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Button, Label, Modal, RadioButtonGroup, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Receiver, TestReceiversAlert } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { Annotations, Labels } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { useTestIntegrationMutation } from '../../../api/receiversApi';
|
||||
import { GrafanaChannelValues } from '../../../types/receiver-form';
|
||||
import { defaultAnnotations } from '../../../utils/constants';
|
||||
import { stringifyErrorLike } from '../../../utils/misc';
|
||||
import { hasPlaceholderEmail } from '../../../utils/receiver-form';
|
||||
import AnnotationsStep from '../../rule-editor/AnnotationsStep';
|
||||
import LabelsField from '../../rule-editor/labels/LabelsField';
|
||||
|
||||
@@ -19,6 +21,7 @@ interface Props {
|
||||
onDismiss: () => void;
|
||||
alertManagerSourceName: string;
|
||||
receivers: Receiver[];
|
||||
channelValues?: GrafanaChannelValues;
|
||||
}
|
||||
|
||||
type AnnoField = {
|
||||
@@ -43,15 +46,23 @@ const defaultValues: FormFields = {
|
||||
labels: [{ key: '', value: '' }],
|
||||
};
|
||||
|
||||
export const TestContactPointModal = ({ isOpen, onDismiss, alertManagerSourceName, receivers }: Props) => {
|
||||
export const TestContactPointModal = ({
|
||||
isOpen,
|
||||
onDismiss,
|
||||
alertManagerSourceName,
|
||||
receivers,
|
||||
channelValues,
|
||||
}: Props) => {
|
||||
const [notificationType, setNotificationType] = useState<NotificationType>(NotificationType.predefined);
|
||||
const styles = useStyles2(getStyles);
|
||||
const formMethods = useForm<FormFields>({ defaultValues, mode: 'onBlur' });
|
||||
const [testIntegration, { isLoading, error, isSuccess }] = useTestIntegrationMutation();
|
||||
|
||||
// Check if email integration has placeholder addresses
|
||||
const isPlaceholderEmail = hasPlaceholderEmail(channelValues);
|
||||
|
||||
const onSubmit = async (data: FormFields) => {
|
||||
let alert: TestReceiversAlert | undefined;
|
||||
|
||||
if (notificationType === NotificationType.custom) {
|
||||
alert = {
|
||||
annotations: data.annotations
|
||||
@@ -93,6 +104,22 @@ export const TestContactPointModal = ({ isOpen, onDismiss, alertManagerSourceNam
|
||||
/>
|
||||
)}
|
||||
|
||||
{isPlaceholderEmail && (
|
||||
<div className={styles.section}>
|
||||
<Alert
|
||||
title={t(
|
||||
'alerting.test-contact-point-modal.placeholder-warning-title',
|
||||
'Configure a valid email address'
|
||||
)}
|
||||
severity="info"
|
||||
>
|
||||
<Trans i18nKey="alerting.test-contact-point-modal.placeholder-warning-body">
|
||||
This contact point is using a placeholder email address (<Text variant="code">example@email.com</Text>).
|
||||
Please update it with a valid email address before testing.
|
||||
</Trans>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.section}>
|
||||
<Label>
|
||||
<Trans i18nKey="alerting.test-contact-point-modal.notification-message">Notification message</Trans>
|
||||
@@ -132,7 +159,18 @@ export const TestContactPointModal = ({ isOpen, onDismiss, alertManagerSourceNam
|
||||
)}
|
||||
|
||||
<Modal.ButtonRow>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isLoading || isPlaceholderEmail}
|
||||
tooltip={
|
||||
isPlaceholderEmail
|
||||
? t(
|
||||
'alerting.test-contact-point-modal.test-disabled-placeholder',
|
||||
'Please configure a valid email address before testing'
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Trans i18nKey="alerting.test-contact-point-modal.send-test-notification">Send test notification</Trans>
|
||||
</Button>
|
||||
</Modal.ButtonRow>
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { locationService, logMeasurement } from '@grafana/runtime';
|
||||
import { AlertManagerCortexConfig, AlertmanagerGroup, Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { appEvents } from 'app/core/core';
|
||||
import {
|
||||
AlertManagerCortexConfig,
|
||||
AlertmanagerGroup,
|
||||
Matcher,
|
||||
Receiver,
|
||||
TestReceiversAlert,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ThunkResult } from 'app/types/store';
|
||||
import { RuleIdentifier, RuleNamespace, StateHistoryItem } from 'app/types/unified-alerting';
|
||||
import { RulerRuleDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { withPromRulesMetadataLogging, withRulerRulesMetadataLogging } from '../Analytics';
|
||||
import { deleteAlertManagerConfig, fetchAlertGroups, updateAlertManagerConfig } from '../api/alertmanager';
|
||||
import {
|
||||
deleteAlertManagerConfig,
|
||||
fetchAlertGroups,
|
||||
testReceivers,
|
||||
updateAlertManagerConfig,
|
||||
} from '../api/alertmanager';
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { fetchAnnotations } from '../api/annotations';
|
||||
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
||||
@@ -17,6 +30,7 @@ import { FetchRulerRulesFilter, fetchRulerRules } from '../api/ruler';
|
||||
import { addDefaultsToAlertmanagerConfig } from '../utils/alertmanager';
|
||||
import { getAllRulesSourceNames } from '../utils/datasource';
|
||||
import { makeAMLink } from '../utils/misc';
|
||||
import { receiverHasPlaceholderEmail } from '../utils/receiver-form';
|
||||
import { withAppEvents, withSerializedError } from '../utils/redux';
|
||||
import { getAlertInfo } from '../utils/rules';
|
||||
import { safeParsePrometheusDuration } from '../utils/time';
|
||||
@@ -253,6 +267,39 @@ export const deleteAlertManagerConfigAction = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
interface TestReceiversOptions {
|
||||
alertManagerSourceName: string;
|
||||
receivers: Receiver[];
|
||||
alert?: TestReceiversAlert;
|
||||
}
|
||||
|
||||
export const testReceiversAction = createAsyncThunk(
|
||||
'unifiedalerting/testReceivers',
|
||||
async ({ alertManagerSourceName, receivers, alert }: TestReceiversOptions): Promise<void> => {
|
||||
const usesPlaceholder = receiverHasPlaceholderEmail(receivers);
|
||||
|
||||
if (usesPlaceholder) {
|
||||
// Handle placeholder email case with custom warning message
|
||||
try {
|
||||
await withSerializedError(testReceivers(alertManagerSourceName, receivers, alert));
|
||||
appEvents.emit(AppEvents.alertWarning, [
|
||||
'Test completed, but no email was sent because a placeholder email address is configured. Please update your contact point with a valid email address to receive alerts.',
|
||||
]);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : 'Unknown error';
|
||||
appEvents.emit(AppEvents.alertError, [`Failed to send test alert: ${msg}`]);
|
||||
throw e;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return withAppEvents(withSerializedError(testReceivers(alertManagerSourceName, receivers, alert)), {
|
||||
errorMessage: 'Failed to send test alert.',
|
||||
successMessage: 'Test alert sent.',
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const rulesInSameGroupHaveInvalidFor = (rules: RulerRuleDTO[], everyDuration: string) => {
|
||||
return rules.filter((rule: RulerRuleDTO) => {
|
||||
const { forDuration } = getAlertInfo(rule, everyDuration);
|
||||
|
||||
@@ -304,3 +304,42 @@ export function omitTemporaryIdentifiers<T>(object: Readonly<T>): T {
|
||||
|
||||
return objectCopy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder emails that ship with the default grafana-default-email contact point.
|
||||
* These should not trigger actual email sends or throw errors.
|
||||
*/
|
||||
const PLACEHOLDER_EMAILS = ['<example@email.com>'];
|
||||
|
||||
/**
|
||||
* Check if a single channel/integration has placeholder email addresses.
|
||||
* Used in UI components to disable test buttons and show warnings.
|
||||
*/
|
||||
export function hasPlaceholderEmail(channelValues?: GrafanaChannelValues): boolean {
|
||||
if (!channelValues || channelValues.type !== 'email') {
|
||||
return false;
|
||||
}
|
||||
const addresses = channelValues.settings?.addresses;
|
||||
if (!addresses) {
|
||||
return false;
|
||||
}
|
||||
const addressList = typeof addresses === 'string' ? [addresses] : addresses;
|
||||
return addressList.some((addr: string) => PLACEHOLDER_EMAILS.includes(addr?.trim()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any receiver in a list has placeholder email addresses.
|
||||
* Used in actions to determine if warning messages should be shown.
|
||||
*/
|
||||
export function receiverHasPlaceholderEmail(receivers: Receiver[]): boolean {
|
||||
return receivers.some((receiver) =>
|
||||
receiver.grafana_managed_receiver_configs?.some((config) => {
|
||||
if (config.type === 'email' && config.settings?.addresses) {
|
||||
const addresses = config.settings.addresses;
|
||||
const addressList = typeof addresses === 'string' ? [addresses] : addresses;
|
||||
return addressList.some((addr: string) => PLACEHOLDER_EMAILS.includes(addr.trim()));
|
||||
}
|
||||
return false;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user