+
+
+
+
+
@@ -22,13 +32,36 @@ export default {
handler: this.clickedOutside,
events: ['mousedown'],
isActive: true
- }
+ },
+ mouseoverItemIndex: null,
+ isOverSubItemMenu: false
}
},
computed: {},
methods: {
- clickAction(func) {
- this.$emit('action', func)
+ mouseoverSubItemMenu(index) {
+ this.isOverSubItemMenu = true
+ },
+ mouseleaveSubItemMenu(index) {
+ setTimeout(() => {
+ if (this.isOverSubItemMenu && this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
+ }, 1)
+ },
+ mouseoverItem(index) {
+ this.isOverSubItemMenu = false
+ this.mouseoverItemIndex = index
+ },
+ mouseleaveItem(index) {
+ setTimeout(() => {
+ if (this.isOverSubItemMenu) return
+ if (this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
+ }, 1)
+ },
+ clickAction(func, data) {
+ this.$emit('action', {
+ func,
+ data
+ })
this.close()
},
clickedOutside(e) {
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index 83e7e5c2..7734a7ee 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -380,6 +380,11 @@ export default {
adminMessageEvt(message) {
this.$toast.info(message)
},
+ ereaderDevicesUpdated(data) {
+ if (!data?.ereaderDevices) return
+
+ this.$store.commit('libraries/setEReaderDevices', data.ereaderDevices)
+ },
initializeSocket() {
this.socket = this.$nuxtSocket({
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
@@ -452,6 +457,9 @@ export default {
this.socket.on('task_finished', this.taskFinished)
this.socket.on('metadata_embed_queue_update', this.metadataEmbedQueueUpdate)
+ // EReader Device Listeners
+ this.socket.on('ereader-devices-updated', this.ereaderDevicesUpdated)
+
this.socket.on('backup_applied', this.backupApplied)
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
diff --git a/client/pages/collection/_id.vue b/client/pages/collection/_id.vue
index e6bfbb82..8964b6c4 100644
--- a/client/pages/collection/_id.vue
+++ b/client/pages/collection/_id.vue
@@ -145,7 +145,7 @@ export default {
feed: this.rssFeed
})
},
- contextMenuAction(action) {
+ contextMenuAction({ action }) {
if (action === 'delete') {
this.removeClick()
} else if (action === 'create-playlist') {
diff --git a/client/pages/config.vue b/client/pages/config.vue
index 21a55aa4..2b63d83c 100644
--- a/client/pages/config.vue
+++ b/client/pages/config.vue
@@ -4,7 +4,7 @@
arrow_forward
-
{{ $strings.HeaderSettings }}
+
{{ currentPage }}
@@ -55,6 +55,7 @@ export default {
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
else if (pageName === 'users') return this.$strings.HeaderUsers
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
+ else if (pageName === 'email') return this.$strings.HeaderEmail
}
return this.$strings.HeaderSettings
}
@@ -79,14 +80,6 @@ export default {
width: 900px;
max-width: calc(100% - 176px);
}
-.configContent.page-library-stats {
- width: 1200px;
-}
-@media (max-width: 1550px) {
- .configContent.page-library-stats {
- margin-left: 176px;
- }
-}
@media (max-width: 1240px) {
.configContent {
margin-left: 176px;
@@ -98,8 +91,5 @@ export default {
width: 100%;
max-width: 100%;
}
- .configContent.page-library-stats {
- margin-left: 0px;
- }
}
\ No newline at end of file
diff --git a/client/pages/config/email.vue b/client/pages/config/email.vue
new file mode 100644
index 00000000..1b5da829
--- /dev/null
+++ b/client/pages/config/email.vue
@@ -0,0 +1,244 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $strings.LabelName }} |
+ {{ $strings.LabelEmail }} |
+ |
+
+
+
+ {{ device.name }}
+ |
+
+ {{ device.email }}
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue
index f4e39523..254e2553 100644
--- a/client/pages/item/_id/index.vue
+++ b/client/pages/item/_id/index.vue
@@ -431,6 +431,19 @@ export default {
})
}
+ if (this.ebookFile && this.$store.state.libraries.ereaderDevices?.length) {
+ items.push({
+ text: this.$strings.LabelSendEbookToDevice,
+ subitems: this.$store.state.libraries.ereaderDevices.map((d) => {
+ return {
+ text: d.name,
+ action: 'sendToDevice',
+ data: d.name
+ }
+ })
+ })
+ }
+
if (this.userCanDelete) {
items.push({
text: this.$strings.ButtonDelete,
@@ -704,7 +717,35 @@ export default {
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
- contextMenuAction(action) {
+ sendToDevice(deviceName) {
+ const payload = {
+ message: this.$getString('MessageConfirmSendEbookToDevice', [this.ebookFile.ebookFormat, this.title, deviceName]),
+ callback: (confirmed) => {
+ if (confirmed) {
+ const payload = {
+ libraryItemId: this.libraryItemId,
+ deviceName
+ }
+ this.processing = true
+ this.$axios
+ .$post(`/api/emails/send-ebook-to-device`, payload)
+ .then(() => {
+ this.$toast.success(this.$getString('ToastSendEbookToDeviceSuccess', [deviceName]))
+ })
+ .catch((error) => {
+ console.error('Failed to send e-book to device', error)
+ this.$toast.error(this.$strings.ToastSendEbookToDeviceFailed)
+ })
+ .finally(() => {
+ this.processing = false
+ })
+ }
+ },
+ type: 'yesNo'
+ }
+ this.$store.commit('globals/setConfirmPrompt', payload)
+ },
+ contextMenuAction({ action, data }) {
if (action === 'collections') {
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setShowCollectionsModal', true)
@@ -719,6 +760,8 @@ export default {
this.downloadLibraryItem()
} else if (action === 'delete') {
this.deleteLibraryItem()
+ } else if (action === 'sendToDevice') {
+ this.sendToDevice(data)
}
}
},
diff --git a/client/pages/login.vue b/client/pages/login.vue
index a94045ae..359af700 100644
--- a/client/pages/login.vue
+++ b/client/pages/login.vue
@@ -107,7 +107,7 @@ export default {
const payload = {
newRoot: { ...this.newRoot }
}
- var success = await this.$axios
+ const success = await this.$axios
.$post('/init', payload)
.then(() => true)
.catch((error) => {
@@ -124,9 +124,10 @@ export default {
location.reload()
},
- setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
+ setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) {
this.$store.commit('setServerSettings', serverSettings)
this.$store.commit('setSource', Source)
+ this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
this.$setServerLanguageCode(serverSettings.language)
if (serverSettings.chromecastEnabled) {
diff --git a/client/store/libraries.js b/client/store/libraries.js
index e9385483..5901347b 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -11,7 +11,8 @@ export const state = () => ({
filterData: null,
numUserPlaylists: 0,
collections: [],
- userPlaylists: []
+ userPlaylists: [],
+ ereaderDevices: []
})
export const getters = {
@@ -339,5 +340,8 @@ export const mutations = {
removeUserPlaylist(state, playlist) {
state.userPlaylists = state.userPlaylists.filter(p => p.id !== playlist.id)
state.numUserPlaylists = state.userPlaylists.length
+ },
+ setEReaderDevices(state, ereaderDevices) {
+ state.ereaderDevices = ereaderDevices
}
}
\ No newline at end of file
diff --git a/client/strings/de.json b/client/strings/de.json
index 0a0e7f08..1d6d5f51 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "M4B-Kodierung starten",
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
"ButtonSubmit": "Ok",
+ "ButtonTest": "Test",
"ButtonUpload": "Hochladen",
"ButtonUploadBackup": "Sicherung hochladen",
"ButtonUploadCover": "Titelbild hochladen",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Aktuelle Downloads",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Warteschlange",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episoden",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Dateien",
"HeaderFindChapters": "Kapitel suchen",
"HeaderIgnoredFiles": "Ignorierte Dateien",
@@ -219,6 +223,10 @@
"LabelDuration": "Laufzeit",
"LabelDurationFound": "Gefundene Laufzeit:",
"LabelEdit": "Bearbeiten",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Eingebettetes Cover",
"LabelEnable": "Aktivieren",
"LabelEnd": "Ende",
@@ -241,6 +249,7 @@
"LabelGenre": "Kategorie",
"LabelGenres": "Kategorien",
"LabelHardDeleteFile": "Datei dauerhaft löschen",
+ "LabelHost": "Host",
"LabelHour": "Stunde",
"LabelIcon": "Symbol",
"LabelIncludeInTracklist": "In die Titelliste aufnehmen",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Typ",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
"LabelProgress": "Fortschritt",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Titel",
"LabelSearchTitleOrASIN": "Titel oder ASIN",
"LabelSeason": "Staffel",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Reihenfolge",
"LabelSeries": "Serien",
"LabelSeriesName": "Serienname",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Episode herunterladen",
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
"MessageEmbedFinished": "Einbettung abgeschlossen!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index e9c9e9f1..e92695ee 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Start M4B Encode",
"ButtonStartMetadataEmbed": "Start Metadata Embed",
"ButtonSubmit": "Submit",
+ "ButtonTest": "Test",
"ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload Backup",
"ButtonUploadCover": "Upload Cover",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodes",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Files",
"HeaderFindChapters": "Find Chapters",
"HeaderIgnoredFiles": "Ignored Files",
@@ -219,6 +223,10 @@
"LabelDuration": "Duration",
"LabelDurationFound": "Duration found:",
"LabelEdit": "Edit",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Enable",
"LabelEnd": "End",
@@ -241,6 +249,7 @@
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard delete file",
+ "LabelHost": "Host",
"LabelHour": "Hour",
"LabelIcon": "Icon",
"LabelIncludeInTracklist": "Include in Tracklist",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
"LabelProgress": "Progress",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN",
"LabelSeason": "Season",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Sequence",
"LabelSeries": "Series",
"LabelSeriesName": "Series Name",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFinished": "Embed Finished!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Failed to delete session",
diff --git a/client/strings/es.json b/client/strings/es.json
index bf7f9594..5d142d1f 100644
--- a/client/strings/es.json
+++ b/client/strings/es.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
"ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata",
"ButtonSubmit": "Enviar",
+ "ButtonTest": "Test",
"ButtonUpload": "Subir",
"ButtonUploadBackup": "Subir Respaldo",
"ButtonUploadCover": "Subir Portada",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Descargando Actualmente",
"HeaderDetails": "Detalles",
"HeaderDownloadQueue": "Lista de Descarga",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodios",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Elemento",
"HeaderFindChapters": "Buscar Capitulo",
"HeaderIgnoredFiles": "Ignorar Elemento",
@@ -219,6 +223,10 @@
"LabelDuration": "Duración",
"LabelDurationFound": "Duración Comprobada:",
"LabelEdit": "Editar",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Portada Integrada",
"LabelEnable": "Habilitar",
"LabelEnd": "Fin",
@@ -241,6 +249,7 @@
"LabelGenre": "Genero",
"LabelGenres": "Géneros",
"LabelHardDeleteFile": "Eliminar Definitivamente",
+ "LabelHost": "Host",
"LabelHour": "Hora",
"LabelIcon": "Icono",
"LabelIncludeInTracklist": "Incluir en Tracklist",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Tipo Podcast",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
"LabelPreventIndexing": "Evite que su fuente sea indexado por iTunes y Google podcast directories",
"LabelProgress": "Progreso",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Buscar Titulo",
"LabelSearchTitleOrASIN": "Buscar Titulo o ASIN",
"LabelSeason": "Temporada",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Secuencia",
"LabelSeries": "Series",
"LabelSeriesName": "Nombre de la Serie",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Esta seguro que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe por lo que se fusionarán.",
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Descargando Capitulo",
"MessageDragFilesIntoTrackOrder": "Arrastras los archivos en el orden correcto de la pista.",
"MessageEmbedFinished": "Incorporación Terminada!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección.",
"ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS",
"ToastRSSFeedCloseSuccess": "Fuente RSS cerrada",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Error al actualizar la serie",
"ToastSeriesUpdateSuccess": "Series actualizada",
"ToastSessionDeleteFailed": "Error al eliminar sesión",
diff --git a/client/strings/fr.json b/client/strings/fr.json
index cbb6d2da..31474eec 100644
--- a/client/strings/fr.json
+++ b/client/strings/fr.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Démarrer l’encodage M4B",
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées intégrées",
"ButtonSubmit": "Soumettre",
+ "ButtonTest": "Test",
"ButtonUpload": "Téléverser",
"ButtonUploadBackup": "Téléverser une sauvegarde",
"ButtonUploadCover": "Téléverser une couverture",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "File d’attente de téléchargement",
"HeaderDetails": "Détails",
"HeaderDownloadQueue": "Queue de téléchargement",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Épisodes",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Fichiers",
"HeaderFindChapters": "Trouver les chapitres",
"HeaderIgnoredFiles": "Fichiers Ignorés",
@@ -219,6 +223,10 @@
"LabelDuration": "Durée",
"LabelDurationFound": "Durée trouvée :",
"LabelEdit": "Modifier",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Couverture du livre intégrée",
"LabelEnable": "Activer",
"LabelEnd": "Fin",
@@ -241,6 +249,7 @@
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelHardDeleteFile": "Suppression du fichier",
+ "LabelHost": "Host",
"LabelHour": "Heure",
"LabelIcon": "Icone",
"LabelIncludeInTracklist": "Inclure dans la liste des pistes",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Type de Podcast",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
"LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de donénes iTunes et Google podcast",
"LabelProgress": "Progression",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Titre de recherche",
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
"LabelSeason": "Saison",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Séquence",
"LabelSeries": "Séries",
"LabelSeriesName": "Nom de la série",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » vers « {1} » pour tous les articles ?",
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
"MessageEmbedFinished": "Intégration Terminée !",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
"ToastSessionDeleteFailed": "Échec de la suppression de session",
diff --git a/client/strings/gu.json b/client/strings/gu.json
index 6f3049bb..da8e8d92 100644
--- a/client/strings/gu.json
+++ b/client/strings/gu.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
"ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો",
"ButtonSubmit": "સબમિટ કરો",
+ "ButtonTest": "Test",
"ButtonUpload": "અપલોડ કરો",
"ButtonUploadBackup": "બેકઅપ અપલોડ કરો",
"ButtonUploadCover": "કવર અપલોડ કરો",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodes",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Files",
"HeaderFindChapters": "Find Chapters",
"HeaderIgnoredFiles": "Ignored Files",
@@ -219,6 +223,10 @@
"LabelDuration": "Duration",
"LabelDurationFound": "Duration found:",
"LabelEdit": "Edit",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Enable",
"LabelEnd": "End",
@@ -241,6 +249,7 @@
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard delete file",
+ "LabelHost": "Host",
"LabelHour": "Hour",
"LabelIcon": "Icon",
"LabelIncludeInTracklist": "Include in Tracklist",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
"LabelProgress": "Progress",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN",
"LabelSeason": "Season",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Sequence",
"LabelSeries": "Series",
"LabelSeriesName": "Series Name",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFinished": "Embed Finished!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Failed to delete session",
diff --git a/client/strings/hi.json b/client/strings/hi.json
index 0b94ff05..81bacfa9 100644
--- a/client/strings/hi.json
+++ b/client/strings/hi.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें",
"ButtonStartMetadataEmbed": "मेटाडेटा एम्बेडिंग शुरू करें",
"ButtonSubmit": "जमा करें",
+ "ButtonTest": "Test",
"ButtonUpload": "अपलोड करें",
"ButtonUploadBackup": "बैकअप अपलोड करें",
"ButtonUploadCover": "कवर अपलोड करें",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodes",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Files",
"HeaderFindChapters": "Find Chapters",
"HeaderIgnoredFiles": "Ignored Files",
@@ -219,6 +223,10 @@
"LabelDuration": "Duration",
"LabelDurationFound": "Duration found:",
"LabelEdit": "Edit",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Enable",
"LabelEnd": "End",
@@ -241,6 +249,7 @@
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard delete file",
+ "LabelHost": "Host",
"LabelHour": "Hour",
"LabelIcon": "Icon",
"LabelIncludeInTracklist": "Include in Tracklist",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
"LabelProgress": "Progress",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN",
"LabelSeason": "Season",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Sequence",
"LabelSeries": "Series",
"LabelSeriesName": "Series Name",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Downloading episode",
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFinished": "Embed Finished!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed closed",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Failed to delete session",
diff --git a/client/strings/hr.json b/client/strings/hr.json
index 5ab28217..736312c4 100644
--- a/client/strings/hr.json
+++ b/client/strings/hr.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
"ButtonStartMetadataEmbed": "Pokreni ugradnju metapodataka",
"ButtonSubmit": "Submit",
+ "ButtonTest": "Test",
"ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload backup",
"ButtonUploadCover": "Upload Cover",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Detalji",
"HeaderDownloadQueue": "Download Queue",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Epizode",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Datoteke",
"HeaderFindChapters": "Pronađi poglavlja",
"HeaderIgnoredFiles": "Zanemarene datoteke",
@@ -219,6 +223,10 @@
"LabelDuration": "Trajanje",
"LabelDurationFound": "Pronađeno trajanje:",
"LabelEdit": "Uredi",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Uključi",
"LabelEnd": "Kraj",
@@ -241,6 +249,7 @@
"LabelGenre": "Genre",
"LabelGenres": "Žanrovi",
"LabelHardDeleteFile": "Obriši datoteku zauvijek",
+ "LabelHost": "Host",
"LabelHour": "Sat",
"LabelIcon": "Ikona",
"LabelIncludeInTracklist": "Dodaj u Tracklist",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcast Type",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
"LabelProgress": "Napredak",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Traži naslov",
"LabelSearchTitleOrASIN": "Traži naslov ili ASIN",
"LabelSeason": "Sezona",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Sekvenca",
"LabelSeries": "Serije",
"LabelSeriesName": "Ime serije",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Preuzimam epizodu",
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
"MessageEmbedFinished": "Embed završen!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije",
"ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda",
"ToastRSSFeedCloseSuccess": "RSS Feed zatvoren",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
diff --git a/client/strings/it.json b/client/strings/it.json
index 5d8ec9d6..40c6da1c 100644
--- a/client/strings/it.json
+++ b/client/strings/it.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B",
"ButtonStartMetadataEmbed": "Inizia Incorporo Metadata",
"ButtonSubmit": "Invia",
+ "ButtonTest": "Test",
"ButtonUpload": "Carica",
"ButtonUploadBackup": "Carica Backup",
"ButtonUploadCover": "Carica Cover",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Dettagli",
"HeaderDownloadQueue": "Download Queue",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodi",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "File",
"HeaderFindChapters": "Trova Capitoli",
"HeaderIgnoredFiles": "File Ignorati",
@@ -219,6 +223,10 @@
"LabelDuration": "Durata",
"LabelDurationFound": "Durata Trovata:",
"LabelEdit": "Modifica",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Abilita",
"LabelEnd": "Fine",
@@ -241,6 +249,7 @@
"LabelGenre": "Genere",
"LabelGenres": "Generi",
"LabelHardDeleteFile": "Elimina Definitivamente",
+ "LabelHost": "Host",
"LabelHour": "Ora",
"LabelIcon": "Icona",
"LabelIncludeInTracklist": "Includi nella Tracklist",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Timo di Podcast",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
"LabelProgress": "Cominciati",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Cerca Titolo",
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
"LabelSeason": "Stagione",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Sequenza",
"LabelSeries": "Serie",
"LabelSeriesName": "Nome Serie",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Download episodio in corso",
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
"MessageEmbedFinished": "Incorporamento finito!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito",
"ToastSeriesUpdateSuccess": "Serie Aggornate",
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
diff --git a/client/strings/nl.json b/client/strings/nl.json
index b043f56e..aaa5d965 100644
--- a/client/strings/nl.json
+++ b/client/strings/nl.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Start M4B-encoding",
"ButtonStartMetadataEmbed": "Start insluiten metadata",
"ButtonSubmit": "Indienen",
+ "ButtonTest": "Test",
"ButtonUpload": "Upload",
"ButtonUploadBackup": "Upload back-up",
"ButtonUploadCover": "Upload cover",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Huidige downloads",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download-wachtrij",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Afleveringen",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Bestanden",
"HeaderFindChapters": "Zoek hoofdstukken",
"HeaderIgnoredFiles": "Genegeerde bestanden",
@@ -219,6 +223,10 @@
"LabelDuration": "Duur",
"LabelDurationFound": "Gevonden duur:",
"LabelEdit": "Wijzig",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Ingesloten cover",
"LabelEnable": "Inschakelen",
"LabelEnd": "Einde",
@@ -241,6 +249,7 @@
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard-delete bestand",
+ "LabelHost": "Host",
"LabelHour": "Uur",
"LabelIcon": "Icoon",
"LabelIncludeInTracklist": "Includeer in tracklijst",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastType": "Podcasttype",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)",
"LabelPreventIndexing": "Voorkom indexering van je feed door iTunes- en Google podcastmappen",
"LabelProgress": "Voortgang",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Zoek titel",
"LabelSearchTitleOrASIN": "Zoek titel of ASIN",
"LabelSeason": "Seizoen",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Sequentie",
"LabelSeries": "Serie",
"LabelSeriesName": "Naam serie",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?",
"MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.",
"MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hoofdlettergebruik bestaat al: \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Aflevering aan het dowloaden",
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
"MessageEmbedFinished": "Insluiting voltooid!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Onderdeel verwijderd uit collectie",
"ToastRSSFeedCloseFailed": "Sluiten RSS-feed mislukt",
"ToastRSSFeedCloseSuccess": "RSS-feed gesloten",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Bijwerken serie mislukt",
"ToastSeriesUpdateSuccess": "Bijwerken serie gelukt",
"ToastSessionDeleteFailed": "Verwijderen sessie mislukt",
diff --git a/client/strings/pl.json b/client/strings/pl.json
index fa2f1a10..fe581b6d 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
"ButtonStartMetadataEmbed": "Osadź metadane",
"ButtonSubmit": "Zaloguj",
+ "ButtonTest": "Test",
"ButtonUpload": "Wgraj",
"ButtonUploadBackup": "Wgraj kopię zapasową",
"ButtonUploadCover": "Wgraj okładkę",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Szczegóły",
"HeaderDownloadQueue": "Download Queue",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Rozdziały",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Pliki",
"HeaderFindChapters": "Wyszukaj rozdziały",
"HeaderIgnoredFiles": "Zignoruj pliki",
@@ -219,6 +223,10 @@
"LabelDuration": "Czas trwania",
"LabelDurationFound": "Znaleziona długość:",
"LabelEdit": "Edytuj",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Włącz",
"LabelEnd": "Zakończ",
@@ -241,6 +249,7 @@
"LabelGenre": "Gatunek",
"LabelGenres": "Gatunki",
"LabelHardDeleteFile": "Usuń trwale plik",
+ "LabelHost": "Host",
"LabelHour": "Godzina",
"LabelIcon": "Ikona",
"LabelIncludeInTracklist": "Dołącz do listy odtwarzania",
@@ -326,6 +335,7 @@
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasty",
"LabelPodcastType": "Podcast Type",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
"LabelProgress": "Postęp",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Wyszukaj tytuł",
"LabelSearchTitleOrASIN": "Szukaj tytuł lub ASIN",
"LabelSeason": "Sezon",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Kolejność",
"LabelSeries": "Serie",
"LabelSeriesName": "Nazwy serii",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Pobieranie odcinka",
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
"MessageEmbedFinished": "Osadzanie zakończone!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji",
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success",
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji",
diff --git a/client/strings/ru.json b/client/strings/ru.json
index 3b7e7fa4..65caafb6 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "Начать кодирование M4B",
"ButtonStartMetadataEmbed": "Начать встраивание метаданных",
"ButtonSubmit": "Применить",
+ "ButtonTest": "Test",
"ButtonUpload": "Загрузить",
"ButtonUploadBackup": "Загрузить бэкап",
"ButtonUploadCover": "Загрузить обложку",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "Текущие закачки",
"HeaderDetails": "Подробности",
"HeaderDownloadQueue": "Очередь скачивания",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Эпизоды",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "Файлы",
"HeaderFindChapters": "Найти главы",
"HeaderIgnoredFiles": "Игнорируемые Файлы",
@@ -219,6 +223,10 @@
"LabelDuration": "Длина",
"LabelDurationFound": "Найденная длина:",
"LabelEdit": "Редактировать",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Включить",
"LabelEnd": "Конец",
@@ -241,6 +249,7 @@
"LabelGenre": "Жанр",
"LabelGenres": "Жанры",
"LabelHardDeleteFile": "Жесткое удаление файла",
+ "LabelHost": "Host",
"LabelHour": "Часы",
"LabelIcon": "Иконка",
"LabelIncludeInTracklist": "Включать в список воспроизведения",
@@ -326,6 +335,7 @@
"LabelPodcast": "Подкаст",
"LabelPodcasts": "Подкасты",
"LabelPodcastType": "Тип подкаста",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
"LabelPreventIndexing": "Запретить индексацию фида каталогами подкастов iTunes и Google",
"LabelProgress": "Прогресс",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "Поиск по названию",
"LabelSearchTitleOrASIN": "Поиск по названию или ASIN",
"LabelSeason": "Сезон",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "Последовательность",
"LabelSeries": "Серия",
"LabelSeriesName": "Имя серии",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?",
"MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.",
"MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "Эпизод скачивается",
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
"MessageEmbedFinished": "Встраивание завершено!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "Элемент удален из коллекции",
"ToastRSSFeedCloseFailed": "Не удалось закрыть RSS-канал",
"ToastRSSFeedCloseSuccess": "RSS-канал закрыт",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Не удалось обновить серию",
"ToastSeriesUpdateSuccess": "Успешное обновление серии",
"ToastSessionDeleteFailed": "Не удалось удалить сеанс",
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index 425f8ae1..b767018f 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.json
@@ -74,6 +74,7 @@
"ButtonStartM4BEncode": "开始 M4B 编码",
"ButtonStartMetadataEmbed": "开始嵌入元数据",
"ButtonSubmit": "提交",
+ "ButtonTest": "Test",
"ButtonUpload": "上传",
"ButtonUploadBackup": "上传备份",
"ButtonUploadCover": "上传封面",
@@ -97,7 +98,10 @@
"HeaderCurrentDownloads": "当前下载",
"HeaderDetails": "详情",
"HeaderDownloadQueue": "下载队列",
+ "HeaderEmail": "Email",
+ "HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "剧集",
+ "HeaderEReaderDevices": "E-Reader Devices",
"HeaderFiles": "文件",
"HeaderFindChapters": "查找章节",
"HeaderIgnoredFiles": "忽略的文件",
@@ -219,6 +223,10 @@
"LabelDuration": "持续时间",
"LabelDurationFound": "找到持续时间:",
"LabelEdit": "编辑",
+ "LabelEmail": "Email",
+ "LabelEmailSettingsFromAddress": "From Address",
+ "LabelEmailSettingsSecure": "Secure",
+ "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
"LabelEmbeddedCover": "嵌入封面",
"LabelEnable": "启用",
"LabelEnd": "结束",
@@ -241,6 +249,7 @@
"LabelGenre": "流派",
"LabelGenres": "流派",
"LabelHardDeleteFile": "完全删除文件",
+ "LabelHost": "Host",
"LabelHour": "小时",
"LabelIcon": "图标",
"LabelIncludeInTracklist": "包含在音轨列表中",
@@ -326,6 +335,7 @@
"LabelPodcast": "播客",
"LabelPodcasts": "播客",
"LabelPodcastType": "播客类型",
+ "LabelPort": "Port",
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
"LabelPreventIndexing": "防止 iTunes 和 Google 播客目录对你的源进行索引",
"LabelProgress": "进度",
@@ -350,6 +360,7 @@
"LabelSearchTitle": "搜索标题",
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
"LabelSeason": "季",
+ "LabelSendEbookToDevice": "Send E-Book to...",
"LabelSequence": "序列",
"LabelSeries": "系列",
"LabelSeriesName": "系列名称",
@@ -494,6 +505,7 @@
"MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?",
"MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.",
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
+ "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
"MessageDownloadingEpisode": "正在下载剧集",
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
"MessageEmbedFinished": "嵌入完成!",
@@ -648,6 +660,8 @@
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
+ "ToastSendEbookToDeviceFailed": "Failed to send e-book to device",
+ "ToastSendEbookToDeviceSuccess": "E-book sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "更新系列失败",
"ToastSeriesUpdateSuccess": "系列已更新",
"ToastSessionDeleteFailed": "删除会话失败",
diff --git a/package-lock.json b/package-lock.json
index bec1d038..f0652c1e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"graceful-fs": "^4.2.10",
"htmlparser2": "^8.0.1",
"node-tone": "^1.0.1",
+ "nodemailer": "^6.9.2",
"socket.io": "^4.5.4",
"xml2js": "^0.5.0"
},
@@ -835,6 +836,14 @@
"resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
"integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
},
+ "node_modules/nodemailer": {
+ "version": "6.9.2",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.2.tgz",
+ "integrity": "sha512-4+TYaa/e1nIxQfyw/WzNPYTEZ5OvHIDEnmjs4LPmIfccPQN+2CYKmGHjWixn/chzD3bmUTu5FMfpltizMxqzdg==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/nodemon": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
@@ -1946,6 +1955,11 @@
"resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
"integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
},
+ "nodemailer": {
+ "version": "6.9.2",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.2.tgz",
+ "integrity": "sha512-4+TYaa/e1nIxQfyw/WzNPYTEZ5OvHIDEnmjs4LPmIfccPQN+2CYKmGHjWixn/chzD3bmUTu5FMfpltizMxqzdg=="
+ },
"nodemon": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
@@ -2314,4 +2328,4 @@
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
}
}
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index 49ec4a6e..ec27bbd6 100644
--- a/package.json
+++ b/package.json
@@ -35,10 +35,11 @@
"graceful-fs": "^4.2.10",
"htmlparser2": "^8.0.1",
"node-tone": "^1.0.1",
+ "nodemailer": "^6.9.2",
"socket.io": "^4.5.4",
"xml2js": "^0.5.0"
},
"devDependencies": {
"nodemon": "^2.0.20"
}
-}
\ No newline at end of file
+}
diff --git a/server/Auth.js b/server/Auth.js
index 7fc8ccc6..c8e03606 100644
--- a/server/Auth.js
+++ b/server/Auth.js
@@ -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
}
}
diff --git a/server/Db.js b/server/Db.js
index b6bff04f..185ce131 100644
--- a/server/Db.js
+++ b/server/Db.js
@@ -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) => {
diff --git a/server/Server.js b/server/Server.js
index 2b5b6cf0..a33b509f 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -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()
diff --git a/server/controllers/EmailController.js b/server/controllers/EmailController.js
new file mode 100644
index 00000000..c9dd6879
--- /dev/null
+++ b/server/controllers/EmailController.js
@@ -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()
\ No newline at end of file
diff --git a/server/managers/EmailManager.js b/server/managers/EmailManager.js
new file mode 100644
index 00000000..3c26d249
--- /dev/null
+++ b/server/managers/EmailManager.js
@@ -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: '
',
+ 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
diff --git a/server/objects/settings/EmailSettings.js b/server/objects/settings/EmailSettings.js
new file mode 100644
index 00000000..31dcc886
--- /dev/null
+++ b/server/objects/settings/EmailSettings.js
@@ -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
\ No newline at end of file
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index a7cae46f..fbaf0d48 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -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
//