Add:Email smtp config & send ebooks to devices #1474

This commit is contained in:
advplyr 2023-05-29 17:38:38 -05:00
parent 15aaf2863c
commit 05ce9c6eda
40 changed files with 1077 additions and 99 deletions

View file

@ -120,6 +120,7 @@ class Auth {
user: user.toJSONForBrowser(),
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
serverSettings: this.db.serverSettings.toJSONForBrowser(),
ereaderDevices: this.db.emailSettings.getEReaderDevices(user),
Source: global.Source
}
}

View file

@ -12,6 +12,7 @@ const Author = require('./objects/entities/Author')
const Series = require('./objects/entities/Series')
const ServerSettings = require('./objects/settings/ServerSettings')
const NotificationSettings = require('./objects/settings/NotificationSettings')
const EmailSettings = require('./objects/settings/EmailSettings')
const PlaybackSession = require('./objects/PlaybackSession')
class Db {
@ -49,6 +50,7 @@ class Db {
this.serverSettings = null
this.notificationSettings = null
this.emailSettings = null
// Stores previous version only if upgraded
this.previousVersion = null
@ -156,6 +158,10 @@ class Db {
this.notificationSettings = new NotificationSettings()
await this.insertEntity('settings', this.notificationSettings)
}
if (!this.emailSettings) {
this.emailSettings = new EmailSettings()
await this.insertEntity('settings', this.emailSettings)
}
global.ServerSettings = this.serverSettings.toJSON()
}
@ -202,6 +208,11 @@ class Db {
if (notificationSettings) {
this.notificationSettings = new NotificationSettings(notificationSettings)
}
const emailSettings = this.settings.find(s => s.id === 'email-settings')
if (emailSettings) {
this.emailSettings = new EmailSettings(emailSettings)
}
}
})
const p5 = this.collectionsDb.select(() => true).then((results) => {

View file

@ -25,6 +25,7 @@ const HlsRouter = require('./routers/HlsRouter')
const StaticRouter = require('./routers/StaticRouter')
const NotificationManager = require('./managers/NotificationManager')
const EmailManager = require('./managers/EmailManager')
const CoverManager = require('./managers/CoverManager')
const AbMergeManager = require('./managers/AbMergeManager')
const CacheManager = require('./managers/CacheManager')
@ -66,6 +67,7 @@ class Server {
// Managers
this.taskManager = new TaskManager()
this.notificationManager = new NotificationManager(this.db)
this.emailManager = new EmailManager(this.db)
this.backupManager = new BackupManager(this.db)
this.logManager = new LogManager(this.db)
this.cacheManager = new CacheManager()

View file

@ -0,0 +1,86 @@
const Logger = require('../Logger')
const SocketAuthority = require('../SocketAuthority')
class EmailController {
constructor() { }
getSettings(req, res) {
res.json({
settings: this.db.emailSettings
})
}
async updateSettings(req, res) {
const updated = this.db.emailSettings.update(req.body)
if (updated) {
await this.db.updateEntity('settings', this.db.emailSettings)
}
res.json({
settings: this.db.emailSettings
})
}
async sendTest(req, res) {
this.emailManager.sendTest(res)
}
async updateEReaderDevices(req, res) {
if (!req.body.ereaderDevices || !Array.isArray(req.body.ereaderDevices)) {
return res.status(400).send('Invalid payload. ereaderDevices array required')
}
const ereaderDevices = req.body.ereaderDevices
for (const device of ereaderDevices) {
if (!device.name || !device.email) {
return res.status(400).send('Invalid payload. ereaderDevices array items must have name and email')
}
}
const updated = this.db.emailSettings.update({
ereaderDevices
})
if (updated) {
await this.db.updateEntity('settings', this.db.emailSettings)
SocketAuthority.adminEmitter('ereader-devices-updated', {
ereaderDevices: this.db.emailSettings.ereaderDevices
})
}
res.json({
ereaderDevices: this.db.emailSettings.ereaderDevices
})
}
async sendEBookToDevice(req, res) {
Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`)
const libraryItem = this.db.getLibraryItem(req.body.libraryItemId)
if (!libraryItem) {
return res.status(404).send('Library item not found')
}
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
return res.sendStatus(403)
}
const ebookFile = libraryItem.media.ebookFile
if (!ebookFile) {
return res.status(404).send('EBook file not found')
}
const device = this.db.emailSettings.getEReaderDevice(req.body.deviceName)
if (!device) {
return res.status(404).send('E-reader device not found')
}
this.emailManager.sendEBookToDevice(ebookFile, device, res)
}
middleware(req, res, next) {
if (!req.user.isAdminOrUp) {
return res.sendStatus(404)
}
next()
}
}
module.exports = new EmailController()

View file

@ -0,0 +1,73 @@
const nodemailer = require('nodemailer')
const Logger = require("../Logger")
const SocketAuthority = require('../SocketAuthority')
class EmailManager {
constructor(db) {
this.db = db
}
getTransporter() {
return nodemailer.createTransport(this.db.emailSettings.getTransportObject())
}
async sendTest(res) {
Logger.info(`[EmailManager] Sending test email`)
const transporter = this.getTransporter()
const success = await transporter.verify().catch((error) => {
Logger.error(`[EmailManager] Failed to verify SMTP connection config`, error)
return false
})
if (!success) {
return res.status(400).send('Failed to verify SMTP connection configuration')
}
transporter.sendMail({
from: this.db.emailSettings.fromAddress,
to: this.db.emailSettings.fromAddress,
subject: 'Test email from Audiobookshelf',
text: 'Success!'
}).then((result) => {
Logger.info(`[EmailManager] Test email sent successfully`, result)
res.sendStatus(200)
}).catch((error) => {
Logger.error(`[EmailManager] Failed to send test email`, error)
res.status(400).send(error.message || 'Failed to send test email')
})
}
async sendEBookToDevice(ebookFile, device, res) {
Logger.info(`[EmailManager] Sending ebook "${ebookFile.metadata.filename}" to device "${device.name}"/"${device.email}"`)
const transporter = this.getTransporter()
const success = await transporter.verify().catch((error) => {
Logger.error(`[EmailManager] Failed to verify SMTP connection config`, error)
return false
})
if (!success) {
return res.status(400).send('Failed to verify SMTP connection configuration')
}
transporter.sendMail({
from: this.db.emailSettings.fromAddress,
to: device.email,
html: '<div dir="auto"></div>',
attachments: [
{
filename: ebookFile.metadata.filename,
path: ebookFile.metadata.path,
}
]
}).then((result) => {
Logger.info(`[EmailManager] Ebook sent to device successfully`, result)
res.sendStatus(200)
}).catch((error) => {
Logger.error(`[EmailManager] Failed to send ebook to device`, error)
res.status(400).send(error.message || 'Failed to send ebook to device')
})
}
}
module.exports = EmailManager

View file

@ -0,0 +1,101 @@
const Logger = require('../../Logger')
const { areEquivalent, copyValue, isNullOrNaN } = require('../../utils')
// REF: https://nodemailer.com/smtp/
class EmailSettings {
constructor(settings = null) {
this.id = 'email-settings'
this.host = null
this.port = 465
this.secure = true
this.user = null
this.pass = null
this.fromAddress = null
// Array of { name:String, email:String }
this.ereaderDevices = []
if (settings) {
this.construct(settings)
}
}
construct(settings) {
this.host = settings.host
this.port = settings.port
this.secure = !!settings.secure
this.user = settings.user
this.pass = settings.pass
this.fromAddress = settings.fromAddress
this.ereaderDevices = settings.ereaderDevices?.map(d => ({ ...d })) || []
}
toJSON() {
return {
id: this.id,
host: this.host,
port: this.port,
secure: this.secure,
user: this.user,
pass: this.pass,
fromAddress: this.fromAddress,
ereaderDevices: this.ereaderDevices.map(d => ({ ...d }))
}
}
update(payload) {
if (!payload) return false
if (payload.port !== undefined) {
if (isNullOrNaN(payload.port)) payload.port = 465
else payload.port = Number(payload.port)
}
if (payload.secure !== undefined) payload.secure = !!payload.secure
if (payload.ereaderDevices !== undefined && !Array.isArray(payload.ereaderDevices)) payload.ereaderDevices = undefined
let hasUpdates = false
const json = this.toJSON()
for (const key in json) {
if (key === 'id') continue
if (payload[key] !== undefined && !areEquivalent(payload[key], json[key])) {
this[key] = copyValue(payload[key])
hasUpdates = true
}
}
return hasUpdates
}
getTransportObject() {
const payload = {
host: this.host,
secure: this.secure
}
if (this.port) payload.port = this.port
if (this.user && this.pass !== undefined) {
payload.auth = {
user: this.user,
pass: this.pass
}
}
return payload
}
getEReaderDevices(user) {
// Only accessible to admin or up
if (!user.isAdminOrUp) {
return []
}
return this.ereaderDevices.map(d => ({ ...d }))
}
getEReaderDevice(deviceName) {
return this.ereaderDevices.find(d => d.name === deviceName)
}
}
module.exports = EmailSettings

View file

@ -20,6 +20,7 @@ const AuthorController = require('../controllers/AuthorController')
const SessionController = require('../controllers/SessionController')
const PodcastController = require('../controllers/PodcastController')
const NotificationController = require('../controllers/NotificationController')
const EmailController = require('../controllers/EmailController')
const SearchController = require('../controllers/SearchController')
const CacheController = require('../controllers/CacheController')
const ToolsController = require('../controllers/ToolsController')
@ -50,6 +51,7 @@ class ApiRouter {
this.rssFeedManager = Server.rssFeedManager
this.cronManager = Server.cronManager
this.notificationManager = Server.notificationManager
this.emailManager = Server.emailManager
this.taskManager = Server.taskManager
this.bookFinder = new BookFinder()
@ -259,6 +261,15 @@ class ApiRouter {
this.router.patch('/notifications/:id', NotificationController.middleware.bind(this), NotificationController.updateNotification.bind(this))
this.router.get('/notifications/:id/test', NotificationController.middleware.bind(this), NotificationController.sendNotificationTest.bind(this))
//
// Email Routes (Admin and up)
//
this.router.get('/emails/settings', EmailController.middleware.bind(this), EmailController.getSettings.bind(this))
this.router.patch('/emails/settings', EmailController.middleware.bind(this), EmailController.updateSettings.bind(this))
this.router.post('/emails/test', EmailController.middleware.bind(this), EmailController.sendTest.bind(this))
this.router.post('/emails/ereader-devices', EmailController.middleware.bind(this), EmailController.updateEReaderDevices.bind(this))
this.router.post('/emails/send-ebook-to-device', EmailController.middleware.bind(this), EmailController.sendEBookToDevice.bind(this))
//
// Search Routes
//