first
This commit is contained in:
45
.dockerignore
Normal file
45
.dockerignore
Normal file
@@ -0,0 +1,45 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dépendances
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# Données
|
||||
data
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
# Système
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# VCS
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Environnement
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
.dockerignore
|
||||
|
||||
# Tests
|
||||
coverage
|
||||
.nyc_output
|
||||
47
.gitignore
vendored
Normal file
47
.gitignore
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# Environment variables
|
||||
#.env
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Documentation
|
||||
docs
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
jspm_packages
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
[Uu]ploads/
|
||||
dist/
|
||||
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
# Installer PM2 globalement
|
||||
RUN npm install pm2 -g
|
||||
|
||||
# Créer le répertoire de l'application
|
||||
WORKDIR /app
|
||||
|
||||
# Copier les fichiers de dépendances
|
||||
COPY package*.json ./
|
||||
|
||||
# Installer les dépendances
|
||||
RUN npm install --production
|
||||
|
||||
# Copier les fichiers du projet
|
||||
COPY . .
|
||||
|
||||
# Créer le répertoire de données
|
||||
RUN mkdir -p /app/data && chmod 777 /app/data
|
||||
|
||||
# Exposer les ports (ajuster selon vos besoins)
|
||||
EXPOSE 3000 2525
|
||||
|
||||
# Configuration des variables d'environnement
|
||||
ENV NODE_ENV=production
|
||||
ENV SERVER_PORT=3000
|
||||
ENV SMTP_PORT=2525
|
||||
ENV SMTP_HOST=0.0.0.0
|
||||
|
||||
# Démarrer l'application avec PM2
|
||||
CMD ["pm2-runtime", "start", "ecosystem.config.js"]
|
||||
67
README.md
Normal file
67
README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Local Mail Notifier
|
||||
|
||||
Un serveur mail local qui envoie des notifications via Pushover quand des emails sont reçus.
|
||||
|
||||
## Structure du projet
|
||||
|
||||
```
|
||||
local-mail-notifier/
|
||||
├── config/
|
||||
│ ├── default.json # Configuration par défaut
|
||||
│ └── custom-environment-variables.json # Variables d'environnement
|
||||
├── public/ # Fichiers statiques pour l'interface web
|
||||
│ ├── css/
|
||||
│ │ └── style.css
|
||||
│ └── js/
|
||||
│ └── main.js
|
||||
├── src/
|
||||
│ ├── server.js # Point d'entrée principal
|
||||
│ ├── mail-server.js # Serveur SMTP
|
||||
│ ├── notifiers/
|
||||
│ │ └── pushover.js # Intégration Pushover
|
||||
│ ├── routes/
|
||||
│ │ ├── api.js # Routes API
|
||||
│ │ └── web.js # Routes web pour l'interface admin
|
||||
│ ├── models/
|
||||
│ │ ├── email.js # Modèle pour les emails
|
||||
│ │ └── settings.js # Modèle pour les paramètres
|
||||
│ └── db/
|
||||
│ └── database.js # Gestion de la base de données
|
||||
├── views/ # Templates EJS pour l'interface web
|
||||
│ ├── layouts/
|
||||
│ │ └── main.ejs
|
||||
│ ├── partials/
|
||||
│ │ ├── header.ejs
|
||||
│ │ └── footer.ejs
|
||||
│ ├── dashboard.ejs
|
||||
│ ├── emails.ejs
|
||||
│ └── settings.ejs
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Cloner le dépôt
|
||||
git clone https://github.com/votre-username/local-mail-notifier.git
|
||||
cd local-mail-notifier
|
||||
|
||||
# Installer les dépendances
|
||||
npm install
|
||||
|
||||
# Démarrer le serveur
|
||||
npm start
|
||||
```
|
||||
|
||||
## Utilisation
|
||||
|
||||
Ce serveur offre les fonctionnalités suivantes :
|
||||
- Réception d'emails sur un port SMTP configurable
|
||||
- Envoi automatique de notifications via Pushover à la réception d'emails
|
||||
- Interface web d'administration pour gérer les paramètres
|
||||
- Visualisation des emails reçus
|
||||
- Configuration des notifications (modèles, filtres, etc.)
|
||||
|
||||
## Configuration
|
||||
|
||||
Modifiez le fichier `config/default.json` selon vos besoins ou utilisez des variables d'environnement.
|
||||
19
config/custom-environment-variables.json
Normal file
19
config/custom-environment-variables.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"server": {
|
||||
"port": "SERVER_PORT",
|
||||
"sessionSecret": "SESSION_SECRET"
|
||||
},
|
||||
"smtp": {
|
||||
"port": "SMTP_PORT",
|
||||
"host": "SMTP_HOST"
|
||||
},
|
||||
"pushover": {
|
||||
"user": "PUSHOVER_USER",
|
||||
"token": "PUSHOVER_TOKEN"
|
||||
},
|
||||
"admin": {
|
||||
"username": "ADMIN_USERNAME",
|
||||
"password": "ADMIN_PASSWORD",
|
||||
"enableAuth": "ENABLE_AUTH"
|
||||
}
|
||||
}
|
||||
28
config/default.json
Normal file
28
config/default.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"server": {
|
||||
"port": 3000,
|
||||
"sessionSecret": "localMailNotifierSecret"
|
||||
},
|
||||
"smtp": {
|
||||
"port": 2525,
|
||||
"host": "0.0.0.0",
|
||||
"secure": false,
|
||||
"authOptional": true,
|
||||
"authRequired": false
|
||||
},
|
||||
"pushover": {
|
||||
"user": "",
|
||||
"token": "",
|
||||
"title": "Mail Notifier",
|
||||
"sound": "pushover",
|
||||
"priority": 0
|
||||
},
|
||||
"admin": {
|
||||
"username": "admin",
|
||||
"password": "admin",
|
||||
"enableAuth": true
|
||||
},
|
||||
"db": {
|
||||
"path": "./data/db.json"
|
||||
}
|
||||
}
|
||||
38
container_data/db.json
Normal file
38
container_data/db.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"emails": [
|
||||
{
|
||||
"id": "20BOdp4orU4XryKYBAubC",
|
||||
"from": "\"Test\" <test@example.com>",
|
||||
"to": "\"Destinataire\" <destinataire@example.com>",
|
||||
"subject": "Test de notification",
|
||||
"text": "Ceci est un test d'envoi d'email vers le serveur mail local.\n\n",
|
||||
"html": "",
|
||||
"receivedAt": "2025-05-07T14:14:35.107Z",
|
||||
"attachments": 0,
|
||||
"read": false
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"smtp": {
|
||||
"port": "2525",
|
||||
"host": "0.0.0.0",
|
||||
"secure": false,
|
||||
"authOptional": true
|
||||
},
|
||||
"pushover": {
|
||||
"user": "u1chkifjzxpv15fcp3h56qs2gj1dup",
|
||||
"token": "ah9uacr4mg5isbb86qeg2jyf35mxpk",
|
||||
"title": "Mail Notifier",
|
||||
"sound": "pushover",
|
||||
"priority": 0
|
||||
},
|
||||
"admin": {
|
||||
"username": "admin",
|
||||
"password": "motdepasse",
|
||||
"enableAuth": "true"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"createdAt": "2025-05-07T14:13:43.742Z",
|
||||
"updatedAt": "2025-05-07T14:13:43.742Z"
|
||||
}
|
||||
}
|
||||
37
data/db.json
Normal file
37
data/db.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"emails": [
|
||||
{
|
||||
"id": "kvXkk2kMjJntCMesl3yZq",
|
||||
"from": "\"Test\" <test@example.com>",
|
||||
"to": "\"Destinataire\" <destinataire@example.com>",
|
||||
"subject": "Test de notification",
|
||||
"text": "Ceci est un test d'envoi d'email vers le serveur mail local.\n\n",
|
||||
"html": "",
|
||||
"receivedAt": "2025-05-07T13:55:52.951Z",
|
||||
"attachments": 0,
|
||||
"read": true
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"smtp": {
|
||||
"port": 2525,
|
||||
"host": "0.0.0.0",
|
||||
"secure": false,
|
||||
"authOptional": true
|
||||
},
|
||||
"pushover": {
|
||||
"user": "u1chkifjzxpv15fcp3h56qs2gj1dup",
|
||||
"token": "ah9uacr4mg5isbb86qeg2jyf35mxpk",
|
||||
"title": "Mail Notifier",
|
||||
"sound": "pushover",
|
||||
"priority": 0
|
||||
},
|
||||
"admin": {
|
||||
"username": "admin",
|
||||
"enableAuth": true
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"createdAt": "2025-05-07T13:42:52.510Z",
|
||||
"updatedAt": "2025-05-07T13:54:24.307Z"
|
||||
}
|
||||
}
|
||||
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mail-notifier:
|
||||
image: bertomlab/smtp-to-pushover
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: mail-notifier
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000" # Port pour l'interface web
|
||||
- "2525:2525" # Port pour le serveur SMTP
|
||||
volumes:
|
||||
- ./container_data:/app/data # Persistance des données
|
||||
- ./logs:/app/logs # Logs
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- SERVER_PORT=3000
|
||||
- SMTP_PORT=2525
|
||||
- SMTP_HOST=0.0.0.0
|
||||
# Configurez ici vos variables d'environnement Pushover
|
||||
- PUSHOVER_USER=u1chkifjzxpv15fcp3h56qs2gj1dup
|
||||
- PUSHOVER_TOKEN=ah9uacr4mg5isbb86qeg2jyf35mxpk
|
||||
- ADMIN_USERNAME=admin
|
||||
- ADMIN_PASSWORD=motdepasse
|
||||
- ENABLE_AUTH=true
|
||||
22
ecosystem.config.js
Normal file
22
ecosystem.config.js
Normal file
@@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'mail-notifier',
|
||||
script: 'src/server.js',
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '256M',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
SERVER_PORT: 3000,
|
||||
SMTP_PORT: 2525,
|
||||
SMTP_HOST: '0.0.0.0'
|
||||
},
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss',
|
||||
error_file: '/app/logs/error.log',
|
||||
out_file: '/app/logs/out.log',
|
||||
merge_logs: true
|
||||
}
|
||||
]
|
||||
};
|
||||
5777
package-lock.json
generated
Normal file
5777
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
package.json
Normal file
42
package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "local-mail-notifier",
|
||||
"version": "1.0.0",
|
||||
"description": "Serveur mail local avec notifications Pushover et interface d'administration",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node src/server.js",
|
||||
"dev": "nodemon src/server.js",
|
||||
"test": "jest"
|
||||
},
|
||||
"keywords": [
|
||||
"email",
|
||||
"smtp",
|
||||
"pushover",
|
||||
"notifications",
|
||||
"mail-server"
|
||||
],
|
||||
"author": "Votre Nom",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"config": "^3.3.9",
|
||||
"connect-flash": "^0.1.1",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"express-session": "^1.17.3",
|
||||
"lowdb": "^1.0.0",
|
||||
"mailparser": "^3.6.5",
|
||||
"method-override": "^3.0.0",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "^5.1.5",
|
||||
"nodemailer": "^6.9.4",
|
||||
"pushover-notifications": "^1.2.2",
|
||||
"smtp-server": "^3.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.6.4",
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
119
public/css/style.css
Normal file
119
public/css/style.css
Normal file
@@ -0,0 +1,119 @@
|
||||
/* Styles personnalisés pour Mail Notifier */
|
||||
|
||||
/* Styles globaux */
|
||||
body {
|
||||
background-color: #f8f9fc;
|
||||
font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Styles pour les cartes */
|
||||
.card {
|
||||
margin-bottom: 24px;
|
||||
border: none;
|
||||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15);
|
||||
}
|
||||
|
||||
.card .card-header {
|
||||
background-color: #f8f9fc;
|
||||
border-bottom: 1px solid #e3e6f0;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
/* Styles pour les bordures colorées */
|
||||
.border-left-primary {
|
||||
border-left: 0.25rem solid #4e73df !important;
|
||||
}
|
||||
|
||||
.border-left-success {
|
||||
border-left: 0.25rem solid #1cc88a !important;
|
||||
}
|
||||
|
||||
.border-left-warning {
|
||||
border-left: 0.25rem solid #f6c23e !important;
|
||||
}
|
||||
|
||||
.border-left-danger {
|
||||
border-left: 0.25rem solid #e74a3b !important;
|
||||
}
|
||||
|
||||
/* Styles pour le tableau de bord */
|
||||
.dashboard-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
word-wrap: break-word;
|
||||
background-color: #fff;
|
||||
background-clip: border-box;
|
||||
border: 1px solid #e3e6f0;
|
||||
border-radius: 0.35rem;
|
||||
}
|
||||
|
||||
/* Styles pour la page de connexion */
|
||||
.login-page {
|
||||
background-color: #4e73df;
|
||||
background-image: linear-gradient(180deg, #4e73df 10%, #224abe 100%);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Styles pour les tableaux */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
/* Styles pour les boutons d'action */
|
||||
.btn-group .btn {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.btn-group form {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Styles pour le contenu des emails */
|
||||
.email-content-html {
|
||||
border: 1px solid #e3e6f0;
|
||||
border-radius: 0.35rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
background-color: #f8f9fc;
|
||||
border-radius: 0.35rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Styles pour les formulaires */
|
||||
.form-text {
|
||||
color: #858796;
|
||||
}
|
||||
|
||||
/* Styles responsive */
|
||||
@media (max-width: 768px) {
|
||||
.ms-auto {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
.col-lg-2 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.col-lg-10 {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
159
public/js/main.js
Normal file
159
public/js/main.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Fichier JavaScript principal pour l'application Mail Notifier
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Fermeture automatique des alertes après 5 secondes
|
||||
setTimeout(function() {
|
||||
const alerts = document.querySelectorAll('.alert');
|
||||
alerts.forEach(function(alert) {
|
||||
const bsAlert = new bootstrap.Alert(alert);
|
||||
bsAlert.close();
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Activer les tooltips Bootstrap
|
||||
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
|
||||
// Gestionnaire d'événements pour la confirmation de suppression
|
||||
const deleteForms = document.querySelectorAll('form[data-confirm]');
|
||||
deleteForms.forEach(function(form) {
|
||||
form.addEventListener('submit', function(event) {
|
||||
const message = form.getAttribute('data-confirm') || 'Êtes-vous sûr de vouloir effectuer cette action?';
|
||||
if (!confirm(message)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Affichage du temps écoulé depuis la réception des emails
|
||||
const timestampElements = document.querySelectorAll('.timestamp');
|
||||
timestampElements.forEach(function(element) {
|
||||
const timestamp = new Date(element.getAttribute('data-timestamp'));
|
||||
element.textContent = timeAgo(timestamp);
|
||||
});
|
||||
|
||||
// Mise à jour automatique du tableau de bord
|
||||
if (document.querySelector('#dashboard-content')) {
|
||||
setInterval(refreshDashboard, 60000); // Actualiser toutes les minutes
|
||||
}
|
||||
|
||||
// Fonction pour tester la configuration Pushover
|
||||
const testPushoverBtn = document.getElementById('testPushover');
|
||||
if (testPushoverBtn) {
|
||||
testPushoverBtn.addEventListener('click', function() {
|
||||
const user = document.getElementById('pushoverUser').value;
|
||||
const token = document.getElementById('pushoverToken').value;
|
||||
|
||||
if (!user || !token) {
|
||||
alert('Veuillez d\'abord configurer votre clé utilisateur et votre token d\'application Pushover.');
|
||||
return;
|
||||
}
|
||||
|
||||
testPushoverBtn.disabled = true;
|
||||
testPushoverBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Envoi en cours...';
|
||||
|
||||
// Le formulaire est soumis par un événement onclick dans le HTML
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Convertit une date en texte "il y a X temps"
|
||||
* @param {Date} date - La date à convertir
|
||||
* @returns {string} - Le texte formaté
|
||||
*/
|
||||
function timeAgo(date) {
|
||||
const seconds = Math.floor((new Date() - date) / 1000);
|
||||
|
||||
let interval = Math.floor(seconds / 31536000);
|
||||
if (interval > 1) {
|
||||
return 'il y a ' + interval + ' ans';
|
||||
}
|
||||
if (interval === 1) {
|
||||
return 'il y a 1 an';
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 2592000);
|
||||
if (interval > 1) {
|
||||
return 'il y a ' + interval + ' mois';
|
||||
}
|
||||
if (interval === 1) {
|
||||
return 'il y a 1 mois';
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 86400);
|
||||
if (interval > 1) {
|
||||
return 'il y a ' + interval + ' jours';
|
||||
}
|
||||
if (interval === 1) {
|
||||
return 'hier';
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 3600);
|
||||
if (interval > 1) {
|
||||
return 'il y a ' + interval + ' heures';
|
||||
}
|
||||
if (interval === 1) {
|
||||
return 'il y a 1 heure';
|
||||
}
|
||||
|
||||
interval = Math.floor(seconds / 60);
|
||||
if (interval > 1) {
|
||||
return 'il y a ' + interval + ' minutes';
|
||||
}
|
||||
if (interval === 1) {
|
||||
return 'il y a 1 minute';
|
||||
}
|
||||
|
||||
return 'à l\'instant';
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualise le contenu du tableau de bord
|
||||
*/
|
||||
function refreshDashboard() {
|
||||
fetch('/api/emails?limit=5')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Mettre à jour les compteurs
|
||||
const totalCount = data.length;
|
||||
const unreadCount = data.filter(email => !email.read).length;
|
||||
|
||||
document.getElementById('total-count').textContent = totalCount;
|
||||
document.getElementById('unread-count').textContent = unreadCount;
|
||||
|
||||
// Mettre à jour la liste des derniers emails
|
||||
const emailsList = document.getElementById('latest-emails');
|
||||
if (emailsList && data.length > 0) {
|
||||
let html = '';
|
||||
|
||||
data.forEach(email => {
|
||||
const date = new Date(email.receivedAt).toLocaleString();
|
||||
const status = email.read
|
||||
? '<span class="badge bg-success">Lu</span>'
|
||||
: '<span class="badge bg-warning">Non lu</span>';
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td>${date}</td>
|
||||
<td>${email.from}</td>
|
||||
<td>${email.subject}</td>
|
||||
<td>${status}</td>
|
||||
<td>
|
||||
<a href="/emails/${email.id}" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
emailsList.innerHTML = html;
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Erreur lors de l\'actualisation du tableau de bord:', error));
|
||||
}
|
||||
77
src/db/database.js
Normal file
77
src/db/database.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const low = require('lowdb');
|
||||
const FileSync = require('lowdb/adapters/FileSync');
|
||||
const config = require('config');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Chemin vers le fichier de base de données
|
||||
const dbPath = config.get('db.path');
|
||||
|
||||
// S'assurer que le répertoire de la base de données existe
|
||||
function ensureDbDirectory() {
|
||||
const dir = path.dirname(dbPath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
console.log(`Répertoire créé: ${dir}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialiser la base de données
|
||||
function init() {
|
||||
try {
|
||||
ensureDbDirectory();
|
||||
|
||||
const adapter = new FileSync(dbPath);
|
||||
const db = low(adapter);
|
||||
|
||||
// Structure par défaut de la base de données
|
||||
db.defaults({
|
||||
emails: [],
|
||||
settings: {
|
||||
smtp: {
|
||||
port: config.get('smtp.port'),
|
||||
host: config.get('smtp.host'),
|
||||
secure: config.get('smtp.secure'),
|
||||
authOptional: config.get('smtp.authOptional')
|
||||
},
|
||||
pushover: {
|
||||
user: config.get('pushover.user'),
|
||||
token: config.get('pushover.token'),
|
||||
title: config.get('pushover.title'),
|
||||
sound: config.get('pushover.sound'),
|
||||
priority: config.get('pushover.priority')
|
||||
},
|
||||
admin: {
|
||||
username: config.get('admin.username'),
|
||||
password: config.get('admin.password'),
|
||||
enableAuth: config.get('admin.enableAuth')
|
||||
},
|
||||
version: '1.0.0',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}).write();
|
||||
|
||||
console.log('Base de données initialisée avec succès');
|
||||
|
||||
// Exposer l'instance de la base de données
|
||||
module.exports.instance = db;
|
||||
return db;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'initialisation de la base de données:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Si le module est déjà initialisé, retourner l'instance existante
|
||||
if (!module.exports.instance) {
|
||||
module.exports = {
|
||||
init,
|
||||
instance: null,
|
||||
get: (collection) => module.exports.instance.get(collection),
|
||||
set: (collection, value) => module.exports.instance.set(collection, value)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = module.exports;
|
||||
117
src/mail-server.js
Normal file
117
src/mail-server.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const SMTPServer = require('smtp-server').SMTPServer;
|
||||
const simpleParser = require('mailparser').simpleParser;
|
||||
const config = require('config');
|
||||
const pushover = require('./notifiers/pushover');
|
||||
const Email = require('./models/email');
|
||||
|
||||
// Options du serveur SMTP
|
||||
const smtpOptions = {
|
||||
name: 'Local Mail Notifier',
|
||||
banner: 'Local Mail Notifier - SMTP Server',
|
||||
size: 1024 * 1024, // Taille maximale des messages: 1MB
|
||||
authOptional: config.get('smtp.authOptional'),
|
||||
disableAuth: !config.get('smtp.authRequired'),
|
||||
onAuth(auth, session, callback) {
|
||||
// Pour la simplicité, nous acceptons toute authentification
|
||||
// En production, vous voudriez vérifier les identifiants
|
||||
return callback(null, { user: auth.username });
|
||||
},
|
||||
onData(stream, session, callback) {
|
||||
let mailData = '';
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
mailData += chunk;
|
||||
});
|
||||
|
||||
stream.on('end', async () => {
|
||||
try {
|
||||
// Parser l'email
|
||||
const parsed = await simpleParser(mailData);
|
||||
|
||||
// Créer un objet email pour la BD
|
||||
const email = {
|
||||
from: parsed.from ? parsed.from.text : '',
|
||||
to: parsed.to ? parsed.to.text : '',
|
||||
subject: parsed.subject || '(Sans objet)',
|
||||
text: parsed.text || '',
|
||||
html: parsed.html || '',
|
||||
receivedAt: new Date(),
|
||||
attachments: parsed.attachments ? parsed.attachments.length : 0
|
||||
};
|
||||
|
||||
// Sauvegarder l'email dans la BD
|
||||
const savedEmail = await Email.create(email);
|
||||
|
||||
// Envoyer une notification
|
||||
await sendNotification(savedEmail);
|
||||
|
||||
console.log(`Email reçu de ${email.from} - "${email.subject}"`);
|
||||
callback();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du traitement de l\'email:', error);
|
||||
callback(new Error('Erreur de traitement de l\'email'));
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
console.error('Erreur de flux:', error);
|
||||
callback(new Error('Erreur de lecture du flux'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Instance du serveur SMTP
|
||||
let smtpServer = null;
|
||||
|
||||
// Fonction pour envoyer une notification
|
||||
async function sendNotification(email) {
|
||||
try {
|
||||
// Construction du message
|
||||
const message = {
|
||||
title: `Nouvel email: ${email.subject}`,
|
||||
message: `De: ${email.from}\n${email.text.substring(0, 500)}${email.text.length > 500 ? '...' : ''}`,
|
||||
priority: config.get('pushover.priority'),
|
||||
sound: config.get('pushover.sound'),
|
||||
url: `http://localhost:${config.get('server.port')}/emails/${email.id}`,
|
||||
url_title: 'Voir l\'email complet'
|
||||
};
|
||||
|
||||
// Envoi via Pushover
|
||||
await pushover.send(message);
|
||||
console.log('Notification envoyée avec succès');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'envoi de la notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fonctions pour démarrer et arrêter le serveur
|
||||
function start() {
|
||||
const port = config.get('smtp.port');
|
||||
const host = config.get('smtp.host');
|
||||
|
||||
smtpServer = new SMTPServer(smtpOptions);
|
||||
|
||||
smtpServer.on('error', err => {
|
||||
console.error('Erreur du serveur SMTP:', err);
|
||||
});
|
||||
|
||||
smtpServer.listen(port, host, () => {
|
||||
console.log(`Serveur SMTP démarré sur ${host}:${port}`);
|
||||
});
|
||||
|
||||
return smtpServer;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (smtpServer) {
|
||||
smtpServer.close();
|
||||
console.log('Serveur SMTP arrêté');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
stop
|
||||
};
|
||||
116
src/models/email.js
Normal file
116
src/models/email.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// src/models/email.js
|
||||
const db = require('../db/database');
|
||||
const { nanoid } = require('nanoid');
|
||||
|
||||
/**
|
||||
* Modèle pour gérer les emails
|
||||
*/
|
||||
class Email {
|
||||
/**
|
||||
* Crée un nouvel email dans la base de données
|
||||
* @param {Object} emailData - Données de l'email
|
||||
* @returns {Object} - L'email créé avec son ID
|
||||
*/
|
||||
static async create(emailData) {
|
||||
try {
|
||||
const email = {
|
||||
id: nanoid(),
|
||||
...emailData,
|
||||
read: false
|
||||
};
|
||||
|
||||
await db.get('emails')
|
||||
.push(email)
|
||||
.write();
|
||||
|
||||
return email;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création de l\'email:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère tous les emails
|
||||
* @param {Object} options - Options de filtrage
|
||||
* @returns {Array} - Liste des emails
|
||||
*/
|
||||
static async getAll(options = {}) {
|
||||
try {
|
||||
let query = db.get('emails');
|
||||
|
||||
// Filtres
|
||||
if (options.read !== undefined) {
|
||||
query = query.filter({ read: options.read });
|
||||
}
|
||||
|
||||
// Tri
|
||||
query = query.orderBy(['receivedAt'], ['desc']);
|
||||
|
||||
// Pagination
|
||||
if (options.limit) {
|
||||
query = query.take(options.limit);
|
||||
}
|
||||
|
||||
return query.value();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des emails:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un email par son ID
|
||||
* @param {string} id - ID de l'email
|
||||
* @returns {Object} - L'email trouvé
|
||||
*/
|
||||
static async getById(id) {
|
||||
try {
|
||||
return db.get('emails')
|
||||
.find({ id })
|
||||
.value();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération de l\'email:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque un email comme lu
|
||||
* @param {string} id - ID de l'email
|
||||
* @returns {Object} - L'email mis à jour
|
||||
*/
|
||||
static async markAsRead(id) {
|
||||
try {
|
||||
await db.get('emails')
|
||||
.find({ id })
|
||||
.assign({ read: true })
|
||||
.write();
|
||||
|
||||
return this.getById(id);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du marquage de l\'email comme lu:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime un email
|
||||
* @param {string} id - ID de l'email
|
||||
* @returns {boolean} - true si l'email a été supprimé
|
||||
*/
|
||||
static async delete(id) {
|
||||
try {
|
||||
await db.get('emails')
|
||||
.remove({ id })
|
||||
.write();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la suppression de l\'email:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Email;
|
||||
85
src/models/settings.js
Normal file
85
src/models/settings.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const db = require('../db/database');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
/**
|
||||
* Modèle pour gérer les paramètres
|
||||
*/
|
||||
class Settings {
|
||||
/**
|
||||
* Récupère tous les paramètres
|
||||
* @returns {Object} - Les paramètres
|
||||
*/
|
||||
static async get() {
|
||||
try {
|
||||
return db.get('settings').value() || {};
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la récupération des paramètres:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les paramètres
|
||||
* @param {Object} settings - Les nouveaux paramètres
|
||||
* @returns {Object} - Les paramètres mis à jour
|
||||
*/
|
||||
static async update(settings) {
|
||||
try {
|
||||
// Si le mot de passe est fourni, le hasher
|
||||
if (settings.admin && settings.admin.password) {
|
||||
settings.admin.password = await bcrypt.hash(settings.admin.password, 10);
|
||||
}
|
||||
|
||||
// Fusion avec les paramètres existants
|
||||
const currentSettings = await this.get();
|
||||
const updatedSettings = {
|
||||
...currentSettings,
|
||||
...settings,
|
||||
updatedAt: new Date()
|
||||
};
|
||||
|
||||
await db.set('settings', updatedSettings).write();
|
||||
|
||||
return updatedSettings;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour des paramètres:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie les identifiants de l'administrateur
|
||||
* @param {string} username - Nom d'utilisateur
|
||||
* @param {string} password - Mot de passe
|
||||
* @returns {boolean} - true si les identifiants sont valides
|
||||
*/
|
||||
static async verifyAdminCredentials(username, password) {
|
||||
try {
|
||||
const settings = await this.get();
|
||||
|
||||
// Si l'authentification est désactivée
|
||||
if (!settings.admin || !settings.admin.enableAuth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vérifier le nom d'utilisateur
|
||||
if (!settings.admin.username || settings.admin.username !== username) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier si le mot de passe est hashé
|
||||
if (settings.admin.password && settings.admin.password.startsWith('$2')) {
|
||||
// Mot de passe hashé
|
||||
return await bcrypt.compare(password, settings.admin.password);
|
||||
} else {
|
||||
// Mot de passe en clair (pour la compatibilité avec les anciennes versions)
|
||||
return settings.admin.password === password;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la vérification des identifiants:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Settings;
|
||||
70
src/notifiers/pushover.js
Normal file
70
src/notifiers/pushover.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const Push = require('pushover-notifications');
|
||||
const config = require('config');
|
||||
const Settings = require('../models/settings');
|
||||
|
||||
/**
|
||||
* Envoie une notification via l'API Pushover
|
||||
* @param {Object} message - Le message à envoyer
|
||||
* @returns {Promise<Object>} - La réponse de l'API Pushover
|
||||
*/
|
||||
async function send(message) {
|
||||
try {
|
||||
// Obtenir les paramètres Pushover
|
||||
const settings = await Settings.get();
|
||||
const userKey = settings.pushover?.user || config.get('pushover.user');
|
||||
const appToken = settings.pushover?.token || config.get('pushover.token');
|
||||
|
||||
// Vérifier si les identifiants sont configurés
|
||||
if (!userKey || !appToken) {
|
||||
throw new Error('Pushover n\'est pas configuré. Veuillez configurer la clé utilisateur et le token d\'application.');
|
||||
}
|
||||
|
||||
// Initialiser le client Pushover
|
||||
const push = new Push({
|
||||
user: userKey,
|
||||
token: appToken
|
||||
});
|
||||
|
||||
// Préparer le message
|
||||
const notification = {
|
||||
title: message.title || settings.pushover?.title || config.get('pushover.title'),
|
||||
message: message.message || 'Notification du serveur mail local',
|
||||
priority: message.priority !== undefined ? message.priority : (settings.pushover?.priority || config.get('pushover.priority')),
|
||||
sound: message.sound || settings.pushover?.sound || config.get('pushover.sound'),
|
||||
url: message.url,
|
||||
url_title: message.url_title
|
||||
};
|
||||
|
||||
// Envoyer la notification
|
||||
return new Promise((resolve, reject) => {
|
||||
push.send(notification, (err, result) => {
|
||||
if (err) {
|
||||
console.error('Erreur Pushover:', err);
|
||||
return reject(err);
|
||||
}
|
||||
console.log('Notification Pushover envoyée:', result);
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'envoi de la notification Pushover:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une notification de test pour vérifier la configuration
|
||||
* @returns {Promise<Object>} - La réponse de l'API Pushover
|
||||
*/
|
||||
async function sendTest() {
|
||||
return send({
|
||||
title: 'Test de notification',
|
||||
message: 'Ceci est un test de notification du serveur mail local. Si vous recevez cette notification, la configuration est correcte.'
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
send,
|
||||
sendTest
|
||||
};
|
||||
139
src/routes/api.js
Normal file
139
src/routes/api.js
Normal file
@@ -0,0 +1,139 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Email = require('../models/email');
|
||||
const Settings = require('../models/settings');
|
||||
const pushover = require('../notifiers/pushover');
|
||||
|
||||
// Middleware d'authentification pour l'API
|
||||
const authMiddleware = async (req, res, next) => {
|
||||
// Pour simplifier, on utilise une authentification basique
|
||||
// En production, utilisez un système d'authentification plus robuste
|
||||
const settings = await Settings.get();
|
||||
|
||||
if (!settings.admin || !settings.admin.enableAuth) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
return res.status(401).json({ error: 'Authentification requise' });
|
||||
}
|
||||
|
||||
const base64Credentials = authHeader.split(' ')[1];
|
||||
const credentials = Buffer.from(base64Credentials, 'base64').toString('utf8');
|
||||
const [username, password] = credentials.split(':');
|
||||
|
||||
const isValid = await Settings.verifyAdminCredentials(username, password);
|
||||
if (!isValid) {
|
||||
return res.status(401).json({ error: 'Identifiants invalides' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
// Appliquer le middleware d'authentification à toutes les routes API
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Liste des emails
|
||||
router.get('/emails', async (req, res) => {
|
||||
try {
|
||||
const options = {
|
||||
read: req.query.read === 'true' ? true : (req.query.read === 'false' ? false : undefined),
|
||||
limit: req.query.limit ? parseInt(req.query.limit) : undefined
|
||||
};
|
||||
|
||||
const emails = await Email.getAll(options);
|
||||
res.json(emails);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Détails d'un email
|
||||
router.get('/emails/:id', async (req, res) => {
|
||||
try {
|
||||
const email = await Email.getById(req.params.id);
|
||||
|
||||
if (!email) {
|
||||
return res.status(404).json({ error: 'Email non trouvé' });
|
||||
}
|
||||
|
||||
res.json(email);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Marquer un email comme lu
|
||||
router.put('/emails/:id/read', async (req, res) => {
|
||||
try {
|
||||
const email = await Email.markAsRead(req.params.id);
|
||||
|
||||
if (!email) {
|
||||
return res.status(404).json({ error: 'Email non trouvé' });
|
||||
}
|
||||
|
||||
res.json(email);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Supprimer un email
|
||||
router.delete('/emails/:id', async (req, res) => {
|
||||
try {
|
||||
const success = await Email.delete(req.params.id);
|
||||
|
||||
if (!success) {
|
||||
return res.status(404).json({ error: 'Email non trouvé' });
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Récupérer les paramètres
|
||||
router.get('/settings', async (req, res) => {
|
||||
try {
|
||||
const settings = await Settings.get();
|
||||
|
||||
// Ne pas exposer le mot de passe
|
||||
if (settings.admin && settings.admin.password) {
|
||||
settings.admin.password = undefined;
|
||||
}
|
||||
|
||||
res.json(settings);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Mettre à jour les paramètres
|
||||
router.put('/settings', async (req, res) => {
|
||||
try {
|
||||
const updatedSettings = await Settings.update(req.body);
|
||||
|
||||
// Ne pas exposer le mot de passe
|
||||
if (updatedSettings.admin && updatedSettings.admin.password) {
|
||||
updatedSettings.admin.password = undefined;
|
||||
}
|
||||
|
||||
res.json(updatedSettings);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Test de notification Pushover
|
||||
router.post('/test-pushover', async (req, res) => {
|
||||
try {
|
||||
const result = await pushover.sendTest();
|
||||
res.json({ success: true, result });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
240
src/routes/web.js
Normal file
240
src/routes/web.js
Normal file
@@ -0,0 +1,240 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Email = require('../models/email');
|
||||
const Settings = require('../models/settings');
|
||||
const pushover = require('../notifiers/pushover');
|
||||
|
||||
// Middleware d'authentification
|
||||
const authMiddleware = async (req, res, next) => {
|
||||
try {
|
||||
const settings = await Settings.get();
|
||||
|
||||
// Si l'authentification est désactivée
|
||||
if (!settings.admin || !settings.admin.enableAuth) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Si l'utilisateur est déjà authentifié
|
||||
if (req.session.isAuthenticated) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Rediriger vers la page de connexion
|
||||
res.redirect('/login');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur d\'authentification:', error);
|
||||
res.status(500).render('error', {
|
||||
message: 'Erreur d\'authentification',
|
||||
error: error
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Page de connexion
|
||||
router.get('/login', async (req, res) => {
|
||||
try {
|
||||
const settings = await Settings.get();
|
||||
|
||||
// Si l'authentification est désactivée, rediriger vers la page d'accueil
|
||||
if (!settings.admin || !settings.admin.enableAuth) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
res.render('login');
|
||||
} catch (error) {
|
||||
res.status(500).render('error', {
|
||||
message: 'Erreur lors du chargement de la page de connexion',
|
||||
error: error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Traitement de la connexion
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
const isValid = await Settings.verifyAdminCredentials(username, password);
|
||||
if (!isValid) {
|
||||
req.flash('error_msg', 'Identifiants invalides');
|
||||
return res.redirect('/login');
|
||||
}
|
||||
|
||||
// Authentification réussie
|
||||
req.session.isAuthenticated = true;
|
||||
req.flash('success_msg', 'Connexion réussie');
|
||||
res.redirect('/');
|
||||
|
||||
} catch (error) {
|
||||
res.status(500).render('error', {
|
||||
message: 'Erreur lors de la connexion',
|
||||
error: error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Déconnexion
|
||||
router.get('/logout', (req, res) => {
|
||||
req.session.destroy(() => {
|
||||
res.redirect('/login');
|
||||
});
|
||||
});
|
||||
|
||||
// Appliquer le middleware d'authentification aux routes protégées
|
||||
router.use(authMiddleware);
|
||||
|
||||
// Page d'accueil - Tableau de bord
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
// Récupérer des statistiques
|
||||
const emails = await Email.getAll();
|
||||
const unreadCount = emails.filter(email => !email.read).length;
|
||||
const totalCount = emails.length;
|
||||
const latestEmails = await Email.getAll({ limit: 5 });
|
||||
|
||||
res.render('dashboard', {
|
||||
unreadCount,
|
||||
totalCount,
|
||||
latestEmails
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).render('error', {
|
||||
message: 'Erreur lors du chargement du tableau de bord',
|
||||
error: error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Liste des emails
|
||||
router.get('/emails', async (req, res) => {
|
||||
try {
|
||||
const options = {
|
||||
read: req.query.read === 'true' ? true : (req.query.read === 'false' ? false : undefined)
|
||||
};
|
||||
|
||||
const emails = await Email.getAll(options);
|
||||
|
||||
res.render('emails', {
|
||||
emails,
|
||||
filter: req.query.read
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).render('error', {
|
||||
message: 'Erreur lors du chargement des emails',
|
||||
error: error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Détails d'un email
|
||||
router.get('/emails/:id', async (req, res) => {
|
||||
try {
|
||||
const email = await Email.getById(req.params.id);
|
||||
|
||||
if (!email) {
|
||||
req.flash('error_msg', 'Email non trouvé');
|
||||
return res.redirect('/emails');
|
||||
}
|
||||
|
||||
// Marquer l'email comme lu s'il ne l'est pas déjà
|
||||
if (!email.read) {
|
||||
await Email.markAsRead(email.id);
|
||||
}
|
||||
|
||||
res.render('email-detail', {
|
||||
email
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).render('error', {
|
||||
message: 'Erreur lors du chargement de l\'email',
|
||||
error: error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Supprimer un email (via formulaire)
|
||||
router.delete('/emails/:id', async (req, res) => {
|
||||
try {
|
||||
await Email.delete(req.params.id);
|
||||
req.flash('success_msg', 'Email supprimé avec succès');
|
||||
res.redirect('/emails');
|
||||
} catch (error) {
|
||||
req.flash('error_msg', 'Erreur lors de la suppression de l\'email');
|
||||
res.redirect(`/emails/${req.params.id}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Page des paramètres
|
||||
router.get('/settings', async (req, res) => {
|
||||
try {
|
||||
const settings = await Settings.get();
|
||||
|
||||
// Ne pas afficher le mot de passe
|
||||
if (settings.admin && settings.admin.password) {
|
||||
settings.admin.password = '';
|
||||
}
|
||||
|
||||
res.render('settings', {
|
||||
settings
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).render('error', {
|
||||
message: 'Erreur lors du chargement des paramètres',
|
||||
error: error
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mettre à jour les paramètres
|
||||
router.post('/settings', async (req, res) => {
|
||||
try {
|
||||
// Formater les données
|
||||
const settings = {
|
||||
smtp: {
|
||||
port: parseInt(req.body.smtpPort),
|
||||
host: req.body.smtpHost,
|
||||
secure: req.body.smtpSecure === 'true',
|
||||
authOptional: req.body.smtpAuthOptional === 'true'
|
||||
},
|
||||
pushover: {
|
||||
user: req.body.pushoverUser,
|
||||
token: req.body.pushoverToken,
|
||||
title: req.body.pushoverTitle,
|
||||
sound: req.body.pushoverSound,
|
||||
priority: parseInt(req.body.pushoverPriority)
|
||||
},
|
||||
admin: {
|
||||
username: req.body.adminUsername,
|
||||
enableAuth: req.body.enableAuth === 'true'
|
||||
}
|
||||
};
|
||||
|
||||
// Ajouter le mot de passe seulement s'il est fourni
|
||||
if (req.body.adminPassword) {
|
||||
settings.admin.password = req.body.adminPassword;
|
||||
}
|
||||
|
||||
await Settings.update(settings);
|
||||
|
||||
req.flash('success_msg', 'Paramètres mis à jour avec succès');
|
||||
res.redirect('/settings');
|
||||
} catch (error) {
|
||||
req.flash('error_msg', 'Erreur lors de la mise à jour des paramètres');
|
||||
res.redirect('/settings');
|
||||
}
|
||||
});
|
||||
|
||||
// Test de notification Pushover
|
||||
router.post('/test-pushover', async (req, res) => {
|
||||
try {
|
||||
await pushover.sendTest();
|
||||
req.flash('success_msg', 'Notification de test envoyée avec succès');
|
||||
} catch (error) {
|
||||
req.flash('error_msg', `Erreur lors de l'envoi de la notification: ${error.message}`);
|
||||
}
|
||||
|
||||
res.redirect('/settings');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
80
src/server.js
Normal file
80
src/server.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const config = require('config');
|
||||
const bodyParser = require('body-parser');
|
||||
const session = require('express-session');
|
||||
const flash = require('connect-flash');
|
||||
const methodOverride = require('method-override');
|
||||
const expressLayouts = require('express-ejs-layouts');
|
||||
|
||||
// Importation des modules personnalisés
|
||||
const mailServer = require('./mail-server');
|
||||
const apiRoutes = require('./routes/api');
|
||||
const webRoutes = require('./routes/web');
|
||||
const database = require('./db/database');
|
||||
|
||||
// Initialisation de la base de données
|
||||
database.init();
|
||||
|
||||
// Création de l'application Express
|
||||
const app = express();
|
||||
const port = config.get('server.port') || 3000;
|
||||
|
||||
// Configuration des vues
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('views', path.join(__dirname, '../views'));
|
||||
app.use(expressLayouts);
|
||||
app.set('layout', 'layouts/main');
|
||||
app.set('layout extractScripts', true);
|
||||
app.set('layout extractStyles', true);
|
||||
|
||||
// Middleware
|
||||
app.use(express.static(path.join(__dirname, '../public')));
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(bodyParser.json());
|
||||
app.use(methodOverride('_method'));
|
||||
app.use(session({
|
||||
secret: config.get('server.sessionSecret'),
|
||||
resave: false,
|
||||
saveUninitialized: false
|
||||
}));
|
||||
app.use(flash());
|
||||
|
||||
// Variables globales pour les templates
|
||||
app.use((req, res, next) => {
|
||||
res.locals.success_msg = req.flash('success_msg');
|
||||
res.locals.error_msg = req.flash('error_msg');
|
||||
res.locals.error = req.flash('error');
|
||||
next();
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api', apiRoutes);
|
||||
app.use('/', webRoutes);
|
||||
|
||||
// Démarrage du serveur HTTP
|
||||
app.listen(port, () => {
|
||||
console.log(`Serveur web démarré sur le port ${port}`);
|
||||
});
|
||||
|
||||
// Démarrage du serveur SMTP
|
||||
mailServer.start();
|
||||
|
||||
// Gestion des erreurs non capturées
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('Uncaught Exception:', err);
|
||||
|
||||
// En production, on pourrait vouloir redémarrer proprement le serveur ici
|
||||
// mais pour le développement, on laisse le processus se terminer
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
// Fermeture propre des serveurs
|
||||
mailServer.stop();
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
148
views/dashboard.ejs
Normal file
148
views/dashboard.ejs
Normal file
@@ -0,0 +1,148 @@
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4">Tableau de bord</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-primary shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
||||
Emails totaux</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="total-count"><%= totalCount %></div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-envelope fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xl-3 col-md-6 mb-4">
|
||||
<div class="card border-left-warning shadow h-100 py-2">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
||||
Emails non lus</div>
|
||||
<div class="h5 mb-0 font-weight-bold text-gray-800" id="unread-count"><%= unreadCount %></div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-envelope-open fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Derniers emails reçus</h6>
|
||||
<a href="/emails" class="btn btn-sm btn-primary">Voir tous les emails</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if(latestEmails && latestEmails.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">De</th>
|
||||
<th scope="col">Sujet</th>
|
||||
<th scope="col">Statut</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="latest-emails">
|
||||
<% latestEmails.forEach(email => { %>
|
||||
<tr>
|
||||
<td><%= new Date(email.receivedAt).toLocaleString() %></td>
|
||||
<td><%= email.from %></td>
|
||||
<td><%= email.subject %></td>
|
||||
<td>
|
||||
<% if(email.read) { %>
|
||||
<span class="badge bg-success">Lu</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-warning">Non lu</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/emails/<%= email.id %>" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||
<p>Aucun email reçu pour le moment.</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Statut du serveur</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Serveur SMTP :</strong> <span class="text-success">Actif sur <%= settings.smtp?.host || '0.0.0.0' %>:<%= settings.smtp?.port || '2525' %></span></p>
|
||||
<p><strong>Mode TLS :</strong>
|
||||
<% if(settings.smtp?.secure) { %>
|
||||
<span class="text-success">Activé</span>
|
||||
<% } else { %>
|
||||
<span class="text-warning">Désactivé</span>
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Notifications Pushover :</strong>
|
||||
<% if(settings.pushover?.user && settings.pushover?.token) { %>
|
||||
<span class="text-success">Configurées</span>
|
||||
<% } else { %>
|
||||
<span class="text-danger">Non configurées</span>
|
||||
<% } %>
|
||||
</p>
|
||||
<p><strong>Priorité des notifications :</strong>
|
||||
<%
|
||||
let priorityText = "Normale";
|
||||
let priorityClass = "text-info";
|
||||
|
||||
if(settings.pushover?.priority === -2) {
|
||||
priorityText = "Très basse";
|
||||
priorityClass = "text-muted";
|
||||
} else if(settings.pushover?.priority === -1) {
|
||||
priorityText = "Basse";
|
||||
priorityClass = "text-secondary";
|
||||
} else if(settings.pushover?.priority === 1) {
|
||||
priorityText = "Haute";
|
||||
priorityClass = "text-warning";
|
||||
} else if(settings.pushover?.priority === 2) {
|
||||
priorityText = "Urgente";
|
||||
priorityClass = "text-danger";
|
||||
}
|
||||
%>
|
||||
<span class="<%= priorityClass %>"><%= priorityText %></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
83
views/email-detail.ejs
Normal file
83
views/email-detail.ejs
Normal file
@@ -0,0 +1,83 @@
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<a href="/emails" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Retour
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form action="/emails/<%= email.id %>?_method=DELETE" method="POST" class="d-inline" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet email?');">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash"></i> Supprimer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary"><%= email.subject %></h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-4">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>De:</strong> <%= email.from %></p>
|
||||
</div>
|
||||
<div class="col-md-6 text-md-end">
|
||||
<p><strong>Reçu le:</strong> <%= new Date(email.receivedAt).toLocaleString() %></p>
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>À:</strong> <%= email.to %></p>
|
||||
<% if(email.attachments && email.attachments > 0) { %>
|
||||
<p><strong>Pièces jointes:</strong> <%= email.attachments %></p>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<% if(email.html) { %>
|
||||
<div class="email-content-html mb-3">
|
||||
<iframe srcdoc="<%= email.html %>" style="width: 100%; height: 500px; border: 1px solid #ddd; border-radius: 0.25rem;"></iframe>
|
||||
</div>
|
||||
|
||||
<div class="accordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseText">
|
||||
Voir en texte brut
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseText" class="accordion-collapse collapse">
|
||||
<div class="accordion-body">
|
||||
<pre class="text-content" style="white-space: pre-wrap;"><%= email.text %></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<pre class="text-content" style="white-space: pre-wrap;"><%= email.text %></pre>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Informations techniques</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>ID:</strong> <%= email.id %></p>
|
||||
<p><strong>Taille:</strong> <%= email.text ? email.text.length : 0 %> caractères (texte brut)</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Format HTML:</strong> <%= email.html ? 'Oui' : 'Non' %></p>
|
||||
<p><strong>Statut:</strong> <%= email.read ? 'Lu' : 'Non lu' %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
78
views/emails.ejs
Normal file
78
views/emails.ejs
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3">Emails reçus</h1>
|
||||
|
||||
<div class="btn-group">
|
||||
<a href="/emails" class="btn btn-outline-primary <%= !filter ? 'active' : '' %>">Tous</a>
|
||||
<a href="/emails?read=false" class="btn btn-outline-primary <%= filter === 'false' ? 'active' : '' %>">Non lus</a>
|
||||
<a href="/emails?read=true" class="btn btn-outline-primary <%= filter === 'true' ? 'active' : '' %>">Lus</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<% if(emails && emails.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">De</th>
|
||||
<th scope="col">Sujet</th>
|
||||
<th scope="col">Statut</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% emails.forEach(email => { %>
|
||||
<tr class="<%= !email.read ? 'table-warning' : '' %>">
|
||||
<td>
|
||||
<span class="timestamp" data-timestamp="<%= email.receivedAt %>">
|
||||
<%= new Date(email.receivedAt).toLocaleString() %>
|
||||
</span>
|
||||
</td>
|
||||
<td><%= email.from %></td>
|
||||
<td><%= email.subject %></td>
|
||||
<td>
|
||||
<% if(email.read) { %>
|
||||
<span class="badge bg-success">Lu</span>
|
||||
<% } else { %>
|
||||
<span class="badge bg-warning">Non lu</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="/emails/<%= email.id %>" class="btn btn-sm btn-info">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<form action="/emails/<%= email.id %>?_method=DELETE" method="POST" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet email?');" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-inbox fa-4x text-muted mb-3"></i>
|
||||
<h5>Aucun email trouvé</h5>
|
||||
<p class="text-muted">
|
||||
<% if (filter === 'true') { %>
|
||||
Il n'y a pas d'emails lus.
|
||||
<% } else if (filter === 'false') { %>
|
||||
Il n'y a pas d'emails non lus.
|
||||
<% } else { %>
|
||||
Les emails reçus apparaîtront ici.
|
||||
<% } %>
|
||||
</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
22
views/error.ejs
Normal file
22
views/error.ejs
Normal file
@@ -0,0 +1,22 @@
|
||||
<%- include('layouts/main', { title: 'Erreur' }) %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="text-center mt-5">
|
||||
<div class="error mx-auto" data-text="Error">
|
||||
<i class="fas fa-exclamation-triangle fa-3x text-danger mb-3"></i>
|
||||
</div>
|
||||
<h3 class="text-danger">Une erreur est survenue</h3>
|
||||
<p class="lead text-gray-800 mb-4"><%= message %></p>
|
||||
<% if (process.env.NODE_ENV !== 'production' && error) { %>
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Détails de l'erreur</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre class="text-danger"><%= error.stack || error.message || error %></pre>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
<a href="/">← Retour à l'accueil</a>
|
||||
</div>
|
||||
</div>
|
||||
16
views/index.ejs
Normal file
16
views/index.ejs
Normal file
@@ -0,0 +1,16 @@
|
||||
<!-- Page d'accueil - redirige vers le tableau de bord -->
|
||||
<%- include('layouts/main', { title: 'Accueil' }) %>
|
||||
|
||||
<script>
|
||||
window.location.href = "/dashboard";
|
||||
</script>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="text-center mt-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Redirection...</span>
|
||||
</div>
|
||||
<p class="mt-3">Redirection vers le tableau de bord...</p>
|
||||
<p>Si vous n'êtes pas redirigé automatiquement, <a href="/dashboard">cliquez ici</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
97
views/layouts/main.ejs
Normal file
97
views/layouts/main.ejs
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mail Notifier - <%= typeof title !== 'undefined' ? title : 'Serveur de mail avec notifications' %></title>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<% if (typeof hideNavbar === 'undefined' || !hideNavbar) { %>
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-3 col-lg-2 px-0 bg-dark position-fixed" style="min-height:100vh">
|
||||
<div class="d-flex flex-column p-3 text-white bg-dark" style="height:100%">
|
||||
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
|
||||
<i class="fas fa-envelope me-2"></i>
|
||||
<span class="fs-4">Mail Notifier</span>
|
||||
</a>
|
||||
<hr>
|
||||
<ul class="nav nav-pills flex-column mb-auto">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link text-white <%= (typeof activePage !== 'undefined' && activePage === 'dashboard') ? 'active' : '' %>">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>
|
||||
Tableau de bord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/emails" class="nav-link text-white <%= (typeof activePage !== 'undefined' && activePage === 'emails') ? 'active' : '' %>">
|
||||
<i class="fas fa-envelope-open-text me-2"></i>
|
||||
Emails reçus
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings" class="nav-link text-white <%= (typeof activePage !== 'undefined' && activePage === 'settings') ? 'active' : '' %>">
|
||||
<i class="fas fa-cog me-2"></i>
|
||||
Paramètres
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<div>
|
||||
<a href="/logout" class="nav-link text-white">
|
||||
<i class="fas fa-sign-out-alt me-2"></i>
|
||||
Déconnexion
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="col-md-9 col-lg-10 ms-auto p-4">
|
||||
<% } else { %>
|
||||
<!-- Full width content (for login page) -->
|
||||
<div class="col-12 p-0">
|
||||
<% } %>
|
||||
|
||||
<!-- Flash messages -->
|
||||
<% if(typeof success_msg !== 'undefined' && success_msg.length > 0) { %>
|
||||
<div class="alert alert-success alert-dismissible fade show">
|
||||
<%= success_msg %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if(typeof error_msg !== 'undefined' && error_msg.length > 0) { %>
|
||||
<div class="alert alert-danger alert-dismissible fade show">
|
||||
<%= error_msg %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if(typeof error !== 'undefined' && error.length > 0) { %>
|
||||
<div class="alert alert-danger alert-dismissible fade show">
|
||||
<%= error %>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Content -->
|
||||
<%- typeof body !== 'undefined' ? body : '' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- Custom JS -->
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
views/login.ejs
Normal file
27
views/login.ejs
Normal file
@@ -0,0 +1,27 @@
|
||||
<%- include('layouts/main', { hideNavbar: true, title: 'Connexion' }) %>
|
||||
|
||||
<div class="min-vh-100 d-flex align-items-center justify-content-center bg-light">
|
||||
<div class="card shadow-sm" style="width: 400px;">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-center mb-4">
|
||||
<i class="fas fa-envelope fa-3x text-primary mb-3"></i>
|
||||
<h3>Mail Notifier</h3>
|
||||
<p class="text-muted">Connexion à l'interface d'administration</p>
|
||||
</div>
|
||||
|
||||
<form action="/login" method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Nom d'utilisateur</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Mot de passe</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-primary">Se connecter</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
13
views/not-found.ejs
Normal file
13
views/not-found.ejs
Normal file
@@ -0,0 +1,13 @@
|
||||
<%- include('layouts/main', { title: 'Page non trouvée' }) %>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="text-center mt-5">
|
||||
<div class="error mx-auto" data-text="404">
|
||||
<i class="fas fa-search fa-3x text-warning mb-3"></i>
|
||||
</div>
|
||||
<h3 class="text-warning">Page non trouvée</h3>
|
||||
<p class="lead text-gray-800 mb-5">La page que vous recherchez n'existe pas</p>
|
||||
<p class="text-gray-500 mb-0">Il semble que vous ayez trouvé un bug dans la matrice...</p>
|
||||
<a href="/">← Retour à l'accueil</a>
|
||||
</div>
|
||||
</div>
|
||||
6
views/partials/footer.ejs
Normal file
6
views/partials/footer.ejs
Normal file
@@ -0,0 +1,6 @@
|
||||
<!-- Footer partial - inclus dans certaines vues -->
|
||||
<footer class="mt-auto py-3 bg-light">
|
||||
<div class="container-fluid text-center">
|
||||
<span class="text-muted">Mail Notifier © <%= new Date().getFullYear() %> | Serveur mail local avec notifications Pushover</span>
|
||||
</div>
|
||||
</footer>
|
||||
21
views/partials/header.ejs
Normal file
21
views/partials/header.ejs
Normal file
@@ -0,0 +1,21 @@
|
||||
<!-- Header partial - inclus dans certaines vues -->
|
||||
<header class="bg-white shadow-sm mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center p-3">
|
||||
<h1 class="h4 mb-0"><%= typeof pageTitle !== 'undefined' ? pageTitle : 'Mail Notifier' %></h1>
|
||||
|
||||
<% if (typeof showActions !== 'undefined' && showActions) { %>
|
||||
<div class="btn-group">
|
||||
<% if (typeof actionButtons !== 'undefined') { %>
|
||||
<% actionButtons.forEach(function(button) { %>
|
||||
<a href="<%= button.href %>" class="btn <%= button.class || 'btn-outline-primary' %>">
|
||||
<% if (button.icon) { %>
|
||||
<i class="fas fa-<%= button.icon %> me-1"></i>
|
||||
<% } %>
|
||||
<%= button.text %>
|
||||
</a>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</header>
|
||||
173
views/settings.ejs
Normal file
173
views/settings.ejs
Normal file
@@ -0,0 +1,173 @@
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="h3 mb-4">Paramètres</h1>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Configuration</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/settings" method="POST">
|
||||
<h5 class="mb-3">Serveur SMTP</h5>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="smtpHost" class="form-label">Hôte</label>
|
||||
<input type="text" class="form-control" id="smtpHost" name="smtpHost" value="<%= settings.smtp?.host || '0.0.0.0' %>">
|
||||
<div class="form-text">Adresse IP à laquelle le serveur SMTP sera lié</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="smtpPort" class="form-label">Port</label>
|
||||
<input type="number" class="form-control" id="smtpPort" name="smtpPort" value="<%= settings.smtp?.port || 2525 %>">
|
||||
<div class="form-text">Port sur lequel le serveur SMTP écoutera</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="smtpSecure" name="smtpSecure" value="true" <%= settings.smtp?.secure ? 'checked' : '' %>>
|
||||
<label class="form-check-label" for="smtpSecure">
|
||||
Connexion sécurisée (TLS)
|
||||
</label>
|
||||
<div class="form-text">Activer TLS pour les connexions SMTP</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="smtpAuthOptional" name="smtpAuthOptional" value="true" <%= settings.smtp?.authOptional ? 'checked' : '' %>>
|
||||
<label class="form-check-label" for="smtpAuthOptional">
|
||||
Authentification optionnelle
|
||||
</label>
|
||||
<div class="form-text">Permettre les connexions sans authentification</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5 class="mb-3">Notifications Pushover</h5>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="pushoverUser" class="form-label">Clé utilisateur</label>
|
||||
<input type="text" class="form-control" id="pushoverUser" name="pushoverUser" value="<%= settings.pushover?.user || '' %>">
|
||||
<div class="form-text">Votre clé utilisateur Pushover</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="pushoverToken" class="form-label">Token d'application</label>
|
||||
<input type="text" class="form-control" id="pushoverToken" name="pushoverToken" value="<%= settings.pushover?.token || '' %>">
|
||||
<div class="form-text">Token API de votre application Pushover</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="pushoverTitle" class="form-label">Titre des notifications</label>
|
||||
<input type="text" class="form-control" id="pushoverTitle" name="pushoverTitle" value="<%= settings.pushover?.title || 'Mail Notifier' %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="pushoverSound" class="form-label">Son</label>
|
||||
<select class="form-select" id="pushoverSound" name="pushoverSound">
|
||||
<option value="pushover" <%= settings.pushover?.sound === 'pushover' ? 'selected' : '' %>>Pushover (défaut)</option>
|
||||
<option value="bike" <%= settings.pushover?.sound === 'bike' ? 'selected' : '' %>>Bike</option>
|
||||
<option value="bugle" <%= settings.pushover?.sound === 'bugle' ? 'selected' : '' %>>Bugle</option>
|
||||
<option value="cashregister" <%= settings.pushover?.sound === 'cashregister' ? 'selected' : '' %>>Cash Register</option>
|
||||
<option value="classical" <%= settings.pushover?.sound === 'classical' ? 'selected' : '' %>>Classical</option>
|
||||
<option value="cosmic" <%= settings.pushover?.sound === 'cosmic' ? 'selected' : '' %>>Cosmic</option>
|
||||
<option value="falling" <%= settings.pushover?.sound === 'falling' ? 'selected' : '' %>>Falling</option>
|
||||
<option value="gamelan" <%= settings.pushover?.sound === 'gamelan' ? 'selected' : '' %>>Gamelan</option>
|
||||
<option value="incoming" <%= settings.pushover?.sound === 'incoming' ? 'selected' : '' %>>Incoming</option>
|
||||
<option value="magic" <%= settings.pushover?.sound === 'magic' ? 'selected' : '' %>>Magic</option>
|
||||
<option value="none" <%= settings.pushover?.sound === 'none' ? 'selected' : '' %>>Aucun son</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="pushoverPriority" class="form-label">Priorité</label>
|
||||
<select class="form-select" id="pushoverPriority" name="pushoverPriority">
|
||||
<option value="-2" <%= settings.pushover?.priority === -2 ? 'selected' : '' %>>Très basse (-2)</option>
|
||||
<option value="-1" <%= settings.pushover?.priority === -1 ? 'selected' : '' %>>Basse (-1)</option>
|
||||
<option value="0" <%= settings.pushover?.priority === 0 || !settings.pushover?.priority ? 'selected' : '' %>>Normale (0)</option>
|
||||
<option value="1" <%= settings.pushover?.priority === 1 ? 'selected' : '' %>>Haute (1)</option>
|
||||
<option value="2" <%= settings.pushover?.priority === 2 ? 'selected' : '' %>>Urgente (2)</option>
|
||||
</select>
|
||||
<div class="form-text">Les notifications urgentes (2) contournent le mode silencieux et nécessitent une confirmation</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button type="button" class="btn btn-info" id="testPushover" onclick="document.getElementById('testPushoverForm').submit();">
|
||||
<i class="fas fa-bell"></i> Tester les notifications
|
||||
</button>
|
||||
<div class="form-text">Envoie une notification de test pour vérifier votre configuration</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h5 class="mb-3">Administration</h5>
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="adminUsername" class="form-label">Nom d'utilisateur</label>
|
||||
<input type="text" class="form-control" id="adminUsername" name="adminUsername" value="<%= settings.admin?.username || 'admin' %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="adminPassword" class="form-label">Mot de passe</label>
|
||||
<input type="password" class="form-control" id="adminPassword" name="adminPassword" placeholder="Laisser vide pour ne pas changer">
|
||||
<div class="form-text">Laisser vide pour conserver le mot de passe actuel</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-4">
|
||||
<input class="form-check-input" type="checkbox" id="enableAuth" name="enableAuth" value="true" <%= settings.admin?.enableAuth ? 'checked' : '' %>>
|
||||
<label class="form-check-label" for="enableAuth">
|
||||
Activer l'authentification
|
||||
</label>
|
||||
<div class="form-text">Si désactivé, aucune connexion ne sera requise pour accéder à l'interface d'administration</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> Enregistrer les paramètres
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Formulaire de test Pushover (soumis via JS) -->
|
||||
<form id="testPushoverForm" action="/test-pushover" method="POST" class="d-none"></form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Informations système</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Version:</strong> <%= settings.version || '1.0.0' %></p>
|
||||
<p><strong>Mode:</strong> <%= process.env.NODE_ENV || 'development' %></p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Démarré le:</strong> <%= settings.createdAt ? new Date(settings.createdAt).toLocaleString() : new Date().toLocaleString() %></p>
|
||||
<p><strong>Modifié le:</strong> <%= settings.updatedAt ? new Date(settings.updatedAt).toLocaleString() : new Date().toLocaleString() %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user