ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

emailer.ts 10 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import { arrayify } from '@peertube/peertube-core-utils'
  2. import { EmailPayload, SendEmailDefaultOptions, UserExportState, UserRegistrationState } from '@peertube/peertube-models'
  3. import { isTestOrDevInstance, root } from '@peertube/peertube-node-utils'
  4. import { readFileSync } from 'fs'
  5. import merge from 'lodash-es/merge.js'
  6. import { Transporter, createTransport } from 'nodemailer'
  7. import { join } from 'path'
  8. import { bunyanLogger, logger } from '../helpers/logger.js'
  9. import { CONFIG, isEmailEnabled } from '../initializers/config.js'
  10. import { WEBSERVER } from '../initializers/constants.js'
  11. import { MRegistration, MUser, MUserExport, MUserImport } from '../types/models/index.js'
  12. import { JobQueue } from './job-queue/index.js'
  13. import { UserModel } from '@server/models/user/user.js'
  14. class Emailer {
  15. private static instance: Emailer
  16. private initialized = false
  17. private transporter: Transporter
  18. private constructor () {
  19. }
  20. init () {
  21. // Already initialized
  22. if (this.initialized === true) return
  23. this.initialized = true
  24. if (!isEmailEnabled()) {
  25. if (!isTestOrDevInstance()) {
  26. logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!')
  27. }
  28. return
  29. }
  30. if (CONFIG.SMTP.TRANSPORT === 'smtp') this.initSMTPTransport()
  31. else if (CONFIG.SMTP.TRANSPORT === 'sendmail') this.initSendmailTransport()
  32. }
  33. async checkConnection () {
  34. if (!this.transporter || CONFIG.SMTP.TRANSPORT !== 'smtp') return
  35. logger.info('Testing SMTP server...')
  36. try {
  37. const success = await this.transporter.verify()
  38. if (success !== true) this.warnOnConnectionFailure()
  39. logger.info('Successfully connected to SMTP server.')
  40. } catch (err) {
  41. this.warnOnConnectionFailure(err)
  42. }
  43. }
  44. // ---------------------------------------------------------------------------
  45. addPasswordResetEmailJob (username: string, to: string, resetPasswordUrl: string) {
  46. const emailPayload: EmailPayload = {
  47. template: 'password-reset',
  48. to: [ to ],
  49. subject: 'Reset your account password',
  50. locals: {
  51. username,
  52. resetPasswordUrl,
  53. hideNotificationPreferencesLink: true
  54. }
  55. }
  56. return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
  57. }
  58. addPasswordCreateEmailJob (username: string, to: string, createPasswordUrl: string) {
  59. const emailPayload: EmailPayload = {
  60. template: 'password-create',
  61. to: [ to ],
  62. subject: 'Create your account password',
  63. locals: {
  64. username,
  65. createPasswordUrl,
  66. hideNotificationPreferencesLink: true
  67. }
  68. }
  69. return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
  70. }
  71. addVerifyEmailJob (options: {
  72. username: string
  73. isRegistrationRequest: boolean
  74. to: string
  75. verifyEmailUrl: string
  76. }) {
  77. const { username, isRegistrationRequest, to, verifyEmailUrl } = options
  78. const emailPayload: EmailPayload = {
  79. template: 'verify-email',
  80. to: [ to ],
  81. subject: `Verify your email on ${CONFIG.INSTANCE.NAME}`,
  82. locals: {
  83. username,
  84. verifyEmailUrl,
  85. isRegistrationRequest,
  86. hideNotificationPreferencesLink: true
  87. }
  88. }
  89. return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
  90. }
  91. addUserBlockJob (user: MUser, blocked: boolean, reason?: string) {
  92. const reasonString = reason ? ` for the following reason: ${reason}` : ''
  93. const blockedWord = blocked ? 'blocked' : 'unblocked'
  94. const to = user.email
  95. const emailPayload: EmailPayload = {
  96. to: [ to ],
  97. subject: 'Account ' + blockedWord,
  98. text: `Your account ${user.username} on ${CONFIG.INSTANCE.NAME} has been ${blockedWord}${reasonString}.`
  99. }
  100. return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
  101. }
  102. addContactFormJob (fromEmail: string, fromName: string, subject: string, body: string) {
  103. const emailPayload: EmailPayload = {
  104. template: 'contact-form',
  105. to: [ CONFIG.ADMIN.EMAIL ],
  106. replyTo: `"${fromName}" <${fromEmail}>`,
  107. subject: `(contact form) ${subject}`,
  108. locals: {
  109. fromName,
  110. fromEmail,
  111. body,
  112. // There are not notification preferences for the contact form
  113. hideNotificationPreferencesLink: true
  114. }
  115. }
  116. return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
  117. }
  118. addUserRegistrationRequestProcessedJob (registration: MRegistration) {
  119. let template: string
  120. let subject: string
  121. if (registration.state === UserRegistrationState.ACCEPTED) {
  122. template = 'user-registration-request-accepted'
  123. subject = `Your registration request for ${registration.username} has been accepted`
  124. } else {
  125. template = 'user-registration-request-rejected'
  126. subject = `Your registration request for ${registration.username} has been rejected`
  127. }
  128. const to = registration.email
  129. const emailPayload: EmailPayload = {
  130. to: [ to ],
  131. template,
  132. subject,
  133. locals: {
  134. username: registration.username,
  135. moderationResponse: registration.moderationResponse,
  136. loginLink: WEBSERVER.URL + '/login',
  137. hideNotificationPreferencesLink: true
  138. }
  139. }
  140. return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
  141. }
  142. // ---------------------------------------------------------------------------
  143. async addUserExportCompletedOrErroredJob (userExport: MUserExport) {
  144. let template: string
  145. let subject: string
  146. if (userExport.state === UserExportState.COMPLETED) {
  147. template = 'user-export-completed'
  148. subject = `Your export archive has been created`
  149. } else {
  150. template = 'user-export-errored'
  151. subject = `Failed to create your export archive`
  152. }
  153. const user = await UserModel.loadById(userExport.userId)
  154. const emailPayload: EmailPayload = {
  155. to: [ user.email ],
  156. template,
  157. subject,
  158. locals: {
  159. exportsUrl: WEBSERVER.URL + '/my-account/import-export',
  160. errorMessage: userExport.error,
  161. hideNotificationPreferencesLink: true
  162. }
  163. }
  164. return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
  165. }
  166. async addUserImportErroredJob (userImport: MUserImport) {
  167. const user = await UserModel.loadById(userImport.userId)
  168. const emailPayload: EmailPayload = {
  169. to: [ user.email ],
  170. template: 'user-import-errored',
  171. subject: 'Failed to import your archive',
  172. locals: {
  173. errorMessage: userImport.error,
  174. hideNotificationPreferencesLink: true
  175. }
  176. }
  177. return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
  178. }
  179. async addUserImportSuccessJob (userImport: MUserImport) {
  180. const user = await UserModel.loadById(userImport.userId)
  181. const emailPayload: EmailPayload = {
  182. to: [ user.email ],
  183. template: 'user-import-completed',
  184. subject: 'Your archive import has finished',
  185. locals: {
  186. resultStats: userImport.resultSummary.stats,
  187. hideNotificationPreferencesLink: true
  188. }
  189. }
  190. return JobQueue.Instance.createJobAsync({ type: 'email', payload: emailPayload })
  191. }
  192. // ---------------------------------------------------------------------------
  193. async sendMail (options: EmailPayload) {
  194. if (!isEmailEnabled()) {
  195. logger.info('Cannot send mail because SMTP is not configured.')
  196. return
  197. }
  198. const fromDisplayName = options.from
  199. ? options.from
  200. : CONFIG.INSTANCE.NAME
  201. const EmailTemplates = (await import('email-templates')).default
  202. const email = new EmailTemplates({
  203. send: true,
  204. htmlToText: {
  205. selectors: [
  206. { selector: 'img', format: 'skip' },
  207. { selector: 'a', options: { hideLinkHrefIfSameAsText: true } }
  208. ]
  209. },
  210. message: {
  211. from: `"${fromDisplayName}" <${CONFIG.SMTP.FROM_ADDRESS}>`
  212. },
  213. transport: this.transporter,
  214. views: {
  215. root: join(root(), 'dist', 'core', 'assets', 'email-templates')
  216. },
  217. subjectPrefix: CONFIG.EMAIL.SUBJECT.PREFIX
  218. })
  219. const toEmails = arrayify(options.to)
  220. for (const to of toEmails) {
  221. const baseOptions: SendEmailDefaultOptions = {
  222. template: 'common',
  223. message: {
  224. to,
  225. from: options.from,
  226. subject: options.subject,
  227. replyTo: options.replyTo
  228. },
  229. locals: { // default variables available in all templates
  230. WEBSERVER,
  231. EMAIL: CONFIG.EMAIL,
  232. instanceName: CONFIG.INSTANCE.NAME,
  233. text: options.text,
  234. subject: options.subject
  235. }
  236. }
  237. // overridden/new variables given for a specific template in the payload
  238. const sendOptions = merge(baseOptions, options)
  239. await email.send(sendOptions)
  240. .then(res => logger.debug('Sent email.', { res }))
  241. .catch(err => logger.error('Error in email sender.', { err }))
  242. }
  243. }
  244. private warnOnConnectionFailure (err?: Error) {
  245. logger.error('Failed to connect to SMTP %s:%d.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT, { err })
  246. }
  247. private initSMTPTransport () {
  248. logger.info('Using %s:%s as SMTP server.', CONFIG.SMTP.HOSTNAME, CONFIG.SMTP.PORT)
  249. let tls: { ca: [ Buffer ] }
  250. if (CONFIG.SMTP.CA_FILE) {
  251. tls = {
  252. ca: [ readFileSync(CONFIG.SMTP.CA_FILE) ]
  253. }
  254. }
  255. let auth: { user: string, pass: string }
  256. if (CONFIG.SMTP.USERNAME && CONFIG.SMTP.PASSWORD) {
  257. auth = {
  258. user: CONFIG.SMTP.USERNAME,
  259. pass: CONFIG.SMTP.PASSWORD
  260. }
  261. }
  262. this.transporter = createTransport({
  263. host: CONFIG.SMTP.HOSTNAME,
  264. port: CONFIG.SMTP.PORT,
  265. secure: CONFIG.SMTP.TLS,
  266. debug: CONFIG.LOG.LEVEL === 'debug',
  267. logger: bunyanLogger as any,
  268. ignoreTLS: CONFIG.SMTP.DISABLE_STARTTLS,
  269. tls,
  270. auth
  271. })
  272. }
  273. private initSendmailTransport () {
  274. logger.info('Using sendmail to send emails')
  275. this.transporter = createTransport({
  276. sendmail: true,
  277. newline: 'unix',
  278. path: CONFIG.SMTP.SENDMAIL,
  279. logger: bunyanLogger
  280. })
  281. }
  282. static get Instance () {
  283. return this.instance || (this.instance = new this())
  284. }
  285. }
  286. // ---------------------------------------------------------------------------
  287. export {
  288. Emailer
  289. }