|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- import { arrayify } from '@peertube/peertube-core-utils'
- import { EmailPayload, SendEmailDefaultOptions, UserExportState, UserRegistrationState } from '@peertube/peertube-models'
- import { isTestOrDevInstance, root } from '@peertube/peertube-node-utils'
- import { readFileSync } from 'fs'
- import merge from 'lodash-es/merge.js'
- import { Transporter, createTransport } from 'nodemailer'
- import { join } from 'path'
- import { bunyanLogger, logger } from '../helpers/logger.js'
- import { CONFIG, isEmailEnabled } from '../initializers/config.js'
- import { WEBSERVER } from '../initializers/constants.js'
- import { MRegistration, MUser, MUserExport, MUserImport } from '../types/models/index.js'
- import { JobQueue } from './job-queue/index.js'
- import { UserModel } from '@server/models/user/user.js'
-
- class Emailer {
-
- private static instance: Emailer
- private initialized = false
- private transporter: Transporter
-
- private constructor () {
- }
-
- init () {
- // Already initialized
- if (this.initialized === true) return
- this.initialized = true
-
- if (!isEmailEnabled()) {
- if (!isTestOrDevInstance()) {
- logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
- }
-
- return
- }
-
- if (CONFIG.SMTP.TRANSPORT === 'smtp') this.initSMTPTransport()
- else if (CONFIG.SMTP.TRANSPORT === 'sendmail') this.initSendmailTransport()
- }
-
- async checkConnection () {
- if (!this.transporter || CONFIG.SMTP.TRANSPORT !== 'smtp') return
-
- logger.info('Testing SMTP server...')
-
- try {
- const success = await this.transporter.verify()
- if (success !== true) this.warnOnConnectionFailure()
-
- logger.info('Successfully connected to SMTP server.')
- } catch (err) {
- this.warnOnConnectionFailure(err)
- }
- }
-
- // ---------------------------------------------------------------------------
-
- addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
- const emailPayload: EmailPayload = {
- template: 'password-reset',
- to: [ to ],
- subject: 'Reset your account password',
- locals: {
- username,
- resetPasswordUrl,
-
- hideNotificationPreferencesLink: true
- }
- }
-
- return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
- }
-
- addPasswordCreateEmailJob (username: string, to: string, createPasswordUrl: string) {
- const emailPayload: EmailPayload = {
- template: 'password-create',
- to: [ to ],
- subject: 'Create your account password',
- locals: {
- username,
- createPasswordUrl,
-
- hideNotificationPreferencesLink: true
- }
- }
-
- return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
- }
-
- addVerifyEmailJob (options: {
- username: string
- isRegistrationRequest: boolean
- to: string
- verifyEmailUrl: string
- }) {
- const { username, isRegistrationRequest, to, verifyEmailUrl } = options
-
- const emailPayload: EmailPayload = {
- template: 'verify-email',
- to: [ to ],
- subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`,
- locals: {
- username,
- verifyEmailUrl,
- isRegistrationRequest,
-
- hideNotificationPreferencesLink: true
- }
- }
-
- return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
- }
-
- addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
- const reasonString = reason ? ` for the following reason: ${reason}` : ''
- const blockedWord = blocked ? 'blocked' : 'unblocked'
-
- const to = user.email
- const emailPayload: EmailPayload = {
- to: [ to ],
- subject: 'Account ' + blockedWord,
- text: `Your account ${user.username} on ${CONFIG.INSTANCE.NAME} has been ${blockedWord}${reasonString}.`
- }
-
- return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
- }
-
- addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) {
- const emailPayload: EmailPayload = {
- template: 'contact-form',
- to: [ CONFIG.ADMIN.EMAIL ],
- replyTo: `"${fromName}" <${fromEmail}>`,
- subject: `(contact form) ${subject}`,
- locals: {
- fromName,
- fromEmail,
- body,
-
- // There are not notification preferences for the contact form
- hideNotificationPreferencesLink: true
- }
- }
-
- return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
- }
-
- addUserRegistrationRequestProcessedJob (registration: MRegistration) {
- let template: string
- let subject: string
- if (registration.state === UserRegistrationState.ACCEPTED) {
- template = 'user-registration-request-accepted'
- subject = `Your registration request for ${registration.username} has been accepted`
- } else {
- template = 'user-registration-request-rejected'
- subject = `Your registration request for ${registration.username} has been rejected`
- }
-
- const to = registration.email
- const emailPayload: EmailPayload = {
- to: [ to ],
- template,
- subject,
- locals: {
- username: registration.username,
- moderationResponse: registration.moderationResponse,
- loginLink: WEBSERVER.URL + '/login',
-
- hideNotificationPreferencesLink: true
- }
- }
-
- return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
- }
-
- // ---------------------------------------------------------------------------
-
- async addUserExportCompletedOrErroredJob (userExport: MUserExport) {
- let template: string
- let subject: string
-
- if (userExport.state === UserExportState.COMPLETED) {
- template = 'user-export-completed'
- subject = `Your export archive has been created`
- } else {
- template = 'user-export-errored'
- subject = `Failed to create your export archive`
- }
-
- const user = await UserModel.loadById(userExport.userId)
-
- const emailPayload: EmailPayload = {
- to: [ user.email ],
- template,
- subject,
- locals: {
- exportsUrl: WEBSERVER.URL + '/my-account/import-export',
- errorMessage: userExport.error,
-
- hideNotificationPreferencesLink: true
- }
- }
-
- return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
- }
-
- async addUserImportErroredJob (userImport: MUserImport) {
- const user = await UserModel.loadById(userImport.userId)
-
- const emailPayload: EmailPayload = {
- to: [ user.email ],
- template: 'user-import-errored',
- subject: 'Failed to import your archive',
- locals: {
- errorMessage: userImport.error,
-
- hideNotificationPreferencesLink: true
- }
- }
-
- return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
- }
-
- async addUserImportSuccessJob (userImport: MUserImport) {
- const user = await UserModel.loadById(userImport.userId)
-
- const emailPayload: EmailPayload = {
- to: [ user.email ],
- template: 'user-import-completed',
- subject: 'Your archive import has finished',
- locals: {
- resultStats: userImport.resultSummary.stats,
-
- hideNotificationPreferencesLink: true
- }
- }
-
- return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
- }
-
- // ---------------------------------------------------------------------------
-
- async sendMail (options: EmailPayload) {
- if (!isEmailEnabled()) {
- logger.info('Cannot send mail because SMTP is not configured.')
- return
- }
-
- const fromDisplayName = options.from
- ? options.from
- : CONFIG.INSTANCE.NAME
-
- const EmailTemplates = (await import('email-templates')).default
-
- const email = new EmailTemplates({
- send: true,
- htmlToText: {
- selectors: [
- { selector: 'img', format: 'skip' },
- { selector: 'a', options: { hideLinkHrefIfSameAsText: true } }
- ]
- },
- message: {
- from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`
- },
- transport: this.transporter,
- views: {
- root: join(root(), 'dist', 'core', 'assets', 'email-templates')
- },
- subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX
- })
-
- const toEmails = arrayify(options.to)
-
- for (const to of toEmails) {
- const baseOptions: SendEmailDefaultOptions = {
- template: 'common',
- message: {
- to,
- from: options.from,
- subject: options.subject,
- replyTo: options.replyTo
- },
- locals: { // default variables available in all templates
- WEBSERVER,
- EMAIL: CONFIG.EMAIL,
- instanceName: CONFIG.INSTANCE.NAME,
- text: options.text,
- subject: options.subject
- }
- }
-
- // overridden/new variables given for a specific template in the payload
- const sendOptions = merge(baseOptions, options)
-
- await email.send(sendOptions)
- .then(res => logger.debug('Sent email.', { res }))
- .catch(err => logger.error('Error in email sender.', { err }))
- }
- }
-
- private warnOnConnectionFailure (err?: Error) {
- logger.error('Failed to connect to SMTP %s:%d.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT, { err })
- }
-
- private initSMTPTransport () {
- logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
-
- let tls: { ca: [ Buffer ] }
- if (CONFIG.SMTP.CA_FILE) {
- tls = {
- ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
- }
- }
-
- let auth: { user: string, pass: string }
- if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) {
- auth = {
- user: CONFIG.SMTP.USERNAME,
- pass: CONFIG.SMTP.PASSWORD
- }
- }
-
- this.transporter = createTransport({
- host: CONFIG.SMTP.HOSTNAME,
- port: CONFIG.SMTP.PORT,
- secure: CONFIG.SMTP.TLS,
- debug: CONFIG.LOG.LEVEL === 'debug',
- logger: bunyanLogger as any,
- ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS,
- tls,
- auth
- })
- }
-
- private initSendmailTransport () {
- logger.info('Using sendmail to send emails')
-
- this.transporter = createTransport({
- sendmail: true,
- newline: 'unix',
- path: CONFIG.SMTP.SENDMAIL,
- logger: bunyanLogger
- })
- }
-
- static get Instance () {
- return this.instance || (this.instance = new this())
- }
- }
-
- // ---------------------------------------------------------------------------
-
- export {
- Emailer
- }
|