mirror of
https://github.com/seerr-team/seerr.git
synced 2026-06-04 14:52:47 -04:00
413 lines
13 KiB
TypeScript
413 lines
13 KiB
TypeScript
import { IssueType, IssueTypeName } from '@server/constants/issue';
|
|
import { MediaType } from '@server/constants/media';
|
|
import { getRepository } from '@server/datasource';
|
|
import { User } from '@server/entity/User';
|
|
import { defineMessages, getIntl } from '@server/i18n';
|
|
import globalMessages from '@server/i18n/globalMessages';
|
|
import PreparedEmail from '@server/lib/email';
|
|
import type { NotificationAgentEmail } from '@server/lib/settings';
|
|
import { NotificationAgentKey, getSettings } from '@server/lib/settings';
|
|
import logger from '@server/logger';
|
|
import type { AvailableLocale } from '@server/types/languages';
|
|
import type { EmailOptions } from 'email-templates';
|
|
import path from 'path';
|
|
import validator from 'validator';
|
|
import { Notification, shouldSendAdminNotification } from '..';
|
|
import type { NotificationAgent, NotificationPayload } from './agent';
|
|
import { BaseAgent } from './agent';
|
|
|
|
const PUBLIC_LOGO_URL =
|
|
'https://raw.githubusercontent.com/seerr-team/seerr/refs/heads/develop/public/logo_full.svg';
|
|
|
|
const messages = defineMessages('notifications.agents.email', {
|
|
issueType: '{type} issue',
|
|
issue: 'issue',
|
|
pendingRequest:
|
|
'A new request for the following {mediaType} is pending approval:',
|
|
pendingRequest4k:
|
|
'A new request for the following {mediaType} in 4K is pending approval:',
|
|
autoRequested:
|
|
'A new request for the following {mediaType} was automatically submitted:',
|
|
autoRequested4k:
|
|
'A new request for the following {mediaType} in 4K was automatically submitted:',
|
|
approvedRequest:
|
|
'Your request for the following {mediaType} has been approved:',
|
|
approvedRequest4k:
|
|
'Your request for the following {mediaType} in 4K has been approved:',
|
|
autoApproved:
|
|
'A new request for the following {mediaType} has been automatically approved:',
|
|
autoApproved4k:
|
|
'A new request for the following {mediaType} in 4K has been automatically approved:',
|
|
availableRequest:
|
|
'Your request for the following {mediaType} is now available:',
|
|
availableRequest4k:
|
|
'Your request for the following {mediaType} in 4K is now available:',
|
|
declinedRequest: 'Your request for the following {mediaType} was declined:',
|
|
declinedRequest4k:
|
|
'Your request for the following {mediaType} in 4K was declined:',
|
|
failedRequest:
|
|
'A request for the following {mediaType} failed to be added to {service}:',
|
|
failedRequest4k:
|
|
'A request for the following {mediaType} in 4K failed to be added to {service}:',
|
|
issueCreated:
|
|
'A new {issueType} has been reported by {userName} for the {mediaType} {subject}:',
|
|
issueComment:
|
|
'{userName} commented on the {issueType} for the {mediaType} {subject}:',
|
|
issueResolved:
|
|
'The {issueType} for the {mediaType} {subject} was marked as resolved by {userName}!',
|
|
issueReopened:
|
|
'The {issueType} for the {mediaType} {subject} was reopened by {userName}.',
|
|
});
|
|
|
|
class EmailAgent
|
|
extends BaseAgent<NotificationAgentEmail>
|
|
implements NotificationAgent
|
|
{
|
|
protected getSettings(): NotificationAgentEmail {
|
|
if (this.settings) {
|
|
return this.settings;
|
|
}
|
|
|
|
const settings = getSettings();
|
|
|
|
return settings.notifications.agents.email;
|
|
}
|
|
|
|
public shouldSend(): boolean {
|
|
const settings = this.getSettings();
|
|
|
|
if (
|
|
settings.enabled &&
|
|
settings.options.emailFrom &&
|
|
settings.options.smtpHost &&
|
|
settings.options.smtpPort
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private buildMessage(
|
|
type: Notification,
|
|
payload: NotificationPayload,
|
|
recipientEmail: string,
|
|
recipientName?: string,
|
|
locale?: AvailableLocale
|
|
): EmailOptions | undefined {
|
|
const intl = getIntl(locale);
|
|
const settings = getSettings();
|
|
const { applicationUrl, applicationTitle } = settings.main;
|
|
const { embedPoster } = settings.notifications.agents.email;
|
|
const { usePublicLogo } = settings.notifications.agents.email.options;
|
|
const logoUrl = usePublicLogo
|
|
? PUBLIC_LOGO_URL
|
|
: applicationUrl
|
|
? `${applicationUrl}/logo_full.svg`
|
|
: undefined;
|
|
|
|
if (type === Notification.TEST_NOTIFICATION) {
|
|
return {
|
|
template: path.join(__dirname, '../../../templates/email/test-email'),
|
|
message: {
|
|
to: recipientEmail,
|
|
},
|
|
locals: {
|
|
body: payload.message,
|
|
applicationUrl,
|
|
applicationTitle,
|
|
logoUrl,
|
|
recipientName,
|
|
recipientEmail,
|
|
},
|
|
};
|
|
}
|
|
|
|
const mediaType = payload.media
|
|
? payload.media.mediaType === MediaType.MOVIE
|
|
? intl.formatMessage(globalMessages.movie)
|
|
: intl.formatMessage(globalMessages.series)
|
|
: undefined;
|
|
const is4k = payload.request?.is4k;
|
|
|
|
if (payload.request) {
|
|
let body = '';
|
|
|
|
switch (type) {
|
|
case Notification.MEDIA_PENDING:
|
|
body = intl.formatMessage(
|
|
is4k ? messages.pendingRequest4k : messages.pendingRequest,
|
|
{ mediaType }
|
|
);
|
|
break;
|
|
case Notification.MEDIA_AUTO_REQUESTED:
|
|
body = intl.formatMessage(
|
|
is4k ? messages.autoRequested4k : messages.autoRequested,
|
|
{ mediaType }
|
|
);
|
|
break;
|
|
case Notification.MEDIA_APPROVED:
|
|
body = intl.formatMessage(
|
|
is4k ? messages.approvedRequest4k : messages.approvedRequest,
|
|
{ mediaType }
|
|
);
|
|
break;
|
|
case Notification.MEDIA_AUTO_APPROVED:
|
|
body = intl.formatMessage(
|
|
is4k ? messages.autoApproved4k : messages.autoApproved,
|
|
{ mediaType }
|
|
);
|
|
break;
|
|
case Notification.MEDIA_AVAILABLE:
|
|
body = intl.formatMessage(
|
|
is4k ? messages.availableRequest4k : messages.availableRequest,
|
|
{ mediaType }
|
|
);
|
|
break;
|
|
case Notification.MEDIA_DECLINED:
|
|
body = intl.formatMessage(
|
|
is4k ? messages.declinedRequest4k : messages.declinedRequest,
|
|
{ mediaType }
|
|
);
|
|
break;
|
|
case Notification.MEDIA_FAILED:
|
|
body = intl.formatMessage(
|
|
is4k ? messages.failedRequest4k : messages.failedRequest,
|
|
{
|
|
mediaType,
|
|
service:
|
|
payload.media?.mediaType === MediaType.MOVIE
|
|
? 'Radarr'
|
|
: 'Sonarr',
|
|
}
|
|
);
|
|
break;
|
|
}
|
|
|
|
return {
|
|
template: path.join(
|
|
__dirname,
|
|
'../../../templates/email/media-request'
|
|
),
|
|
message: {
|
|
to: recipientEmail,
|
|
},
|
|
locals: {
|
|
event: payload.event,
|
|
body,
|
|
mediaName: payload.subject,
|
|
mediaExtra: payload.extra ?? [],
|
|
imageUrl: embedPoster ? payload.image : undefined,
|
|
timestamp: new Date().toTimeString(),
|
|
requestedBy: payload.request.requestedBy.displayName,
|
|
actionUrl: applicationUrl
|
|
? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}`
|
|
: undefined,
|
|
applicationUrl,
|
|
applicationTitle,
|
|
logoUrl,
|
|
recipientName,
|
|
recipientEmail,
|
|
},
|
|
};
|
|
} else if (payload.issue) {
|
|
const issueType =
|
|
payload.issue && payload.issue.issueType !== IssueType.OTHER
|
|
? intl.formatMessage(messages.issueType, {
|
|
type: IssueTypeName[payload.issue.issueType].toLowerCase(),
|
|
})
|
|
: intl.formatMessage(messages.issue);
|
|
|
|
let body = '';
|
|
|
|
switch (type) {
|
|
case Notification.ISSUE_CREATED:
|
|
body = intl.formatMessage(messages.issueCreated, {
|
|
issueType,
|
|
userName: payload.issue.createdBy.displayName,
|
|
mediaType,
|
|
subject: payload.subject,
|
|
});
|
|
break;
|
|
case Notification.ISSUE_COMMENT:
|
|
body = intl.formatMessage(messages.issueComment, {
|
|
userName: payload.comment?.user.displayName,
|
|
issueType,
|
|
mediaType,
|
|
subject: payload.subject,
|
|
});
|
|
break;
|
|
case Notification.ISSUE_RESOLVED:
|
|
body = intl.formatMessage(messages.issueResolved, {
|
|
issueType,
|
|
userName: payload.issue.modifiedBy?.displayName,
|
|
mediaType,
|
|
subject: payload.subject,
|
|
});
|
|
break;
|
|
case Notification.ISSUE_REOPENED:
|
|
body = intl.formatMessage(messages.issueReopened, {
|
|
issueType,
|
|
userName: payload.issue.modifiedBy?.displayName,
|
|
mediaType,
|
|
subject: payload.subject,
|
|
});
|
|
break;
|
|
}
|
|
|
|
return {
|
|
template: path.join(__dirname, '../../../templates/email/media-issue'),
|
|
message: {
|
|
to: recipientEmail,
|
|
},
|
|
locals: {
|
|
event: payload.event,
|
|
body,
|
|
issueDescription: payload.message,
|
|
issueComment: payload.comment?.message,
|
|
mediaName: payload.subject,
|
|
extra: payload.extra ?? [],
|
|
imageUrl: embedPoster ? payload.image : undefined,
|
|
timestamp: new Date().toTimeString(),
|
|
actionUrl: applicationUrl
|
|
? `${applicationUrl}/issues/${payload.issue.id}`
|
|
: undefined,
|
|
applicationUrl,
|
|
applicationTitle,
|
|
logoUrl,
|
|
recipientName,
|
|
recipientEmail,
|
|
},
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
public async send(
|
|
type: Notification,
|
|
payload: NotificationPayload
|
|
): Promise<boolean> {
|
|
if (payload.notifyUser) {
|
|
if (
|
|
!payload.notifyUser.settings ||
|
|
(payload.notifyUser.settings.hasNotificationType(
|
|
NotificationAgentKey.EMAIL,
|
|
type
|
|
) ??
|
|
true)
|
|
) {
|
|
logger.debug('Sending email notification', {
|
|
label: 'Notifications',
|
|
recipient: payload.notifyUser.displayName,
|
|
type: Notification[type],
|
|
subject: payload.subject,
|
|
});
|
|
|
|
try {
|
|
const email = new PreparedEmail(
|
|
this.getSettings(),
|
|
payload.notifyUser.settings?.pgpKey
|
|
);
|
|
if (
|
|
validator.isEmail(payload.notifyUser.email, { require_tld: false })
|
|
) {
|
|
await email.send(
|
|
this.buildMessage(
|
|
type,
|
|
payload,
|
|
payload.notifyUser.email,
|
|
payload.notifyUser.displayName,
|
|
payload.notifyUser.settings?.locale as AvailableLocale
|
|
)
|
|
);
|
|
} else {
|
|
logger.warn('Invalid email address provided for user', {
|
|
label: 'Notifications',
|
|
recipient: payload.notifyUser.displayName,
|
|
type: Notification[type],
|
|
subject: payload.subject,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
logger.error('Error sending email notification', {
|
|
label: 'Notifications',
|
|
recipient: payload.notifyUser.displayName,
|
|
type: Notification[type],
|
|
subject: payload.subject,
|
|
errorMessage: e.message,
|
|
});
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (payload.notifyAdmin) {
|
|
const userRepository = getRepository(User);
|
|
const users = await userRepository.find();
|
|
|
|
await Promise.all(
|
|
users
|
|
.filter(
|
|
(user) =>
|
|
(!user.settings ||
|
|
(user.settings.hasNotificationType(
|
|
NotificationAgentKey.EMAIL,
|
|
type
|
|
) ??
|
|
true)) &&
|
|
shouldSendAdminNotification(type, user, payload)
|
|
)
|
|
.map(async (user) => {
|
|
logger.debug('Sending email notification', {
|
|
label: 'Notifications',
|
|
recipient: user.displayName,
|
|
type: Notification[type],
|
|
subject: payload.subject,
|
|
});
|
|
|
|
try {
|
|
const email = new PreparedEmail(
|
|
this.getSettings(),
|
|
user.settings?.pgpKey
|
|
);
|
|
if (validator.isEmail(user.email, { require_tld: false })) {
|
|
await email.send(
|
|
this.buildMessage(
|
|
type,
|
|
payload,
|
|
user.email,
|
|
user.displayName,
|
|
user.settings?.locale as AvailableLocale
|
|
)
|
|
);
|
|
} else {
|
|
logger.warn('Invalid email address provided for user', {
|
|
label: 'Notifications',
|
|
recipient: user.displayName,
|
|
type: Notification[type],
|
|
subject: payload.subject,
|
|
});
|
|
}
|
|
} catch (e) {
|
|
logger.error('Error sending email notification', {
|
|
label: 'Notifications',
|
|
recipient: user.displayName,
|
|
type: Notification[type],
|
|
subject: payload.subject,
|
|
errorMessage: e.message,
|
|
});
|
|
|
|
return false;
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export default EmailAgent;
|