SMTP Settings

This commit is contained in:
2025-06-02 14:30:20 +02:00
parent 694c8df364
commit 765c07316a
9 changed files with 461 additions and 2 deletions

Binary file not shown.

View File

@@ -0,0 +1,35 @@
"""add key value settings table
Revision ID: 0a8006bd1732
Revises: 20519a2437c2
Create Date: 2025-06-02 14:10:54.033943
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0a8006bd1732'
down_revision = '20519a2437c2'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('key_value_settings',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('key', sa.String(length=100), nullable=False),
sa.Column('value', sa.Text(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('key')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('key_value_settings')
# ### end Alembic commands ###

View File

@@ -5,6 +5,7 @@ from datetime import datetime
from sqlalchemy.orm import relationship
from extensions import db
from enum import Enum
import json
# Association table for room members
room_members = db.Table('room_members',
@@ -160,6 +161,37 @@ class SiteSettings(db.Model):
db.session.commit()
return settings
class KeyValueSettings(db.Model):
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(100), unique=True, nullable=False)
value = db.Column(db.Text)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@classmethod
def get_value(cls, key, default=None):
setting = cls.query.filter_by(key=key).first()
if setting:
try:
return json.loads(setting.value)
except (json.JSONDecodeError, TypeError):
return setting.value
return default
@classmethod
def set_value(cls, key, value):
setting = cls.query.filter_by(key=key).first()
if not setting:
setting = cls(key=key)
if isinstance(value, (dict, list)):
setting.value = json.dumps(value)
else:
setting.value = str(value)
db.session.add(setting)
db.session.commit()
return setting
class Conversation(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)

View File

@@ -1,6 +1,6 @@
from flask import render_template, Blueprint, redirect, url_for, request, flash, Response, jsonify, session
from flask_login import current_user, login_required
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail
from models import User, db, Room, RoomFile, RoomMemberPermission, SiteSettings, Event, Conversation, Message, MessageAttachment, Notif, EmailTemplate, Mail, KeyValueSettings
from routes.auth import require_password_change
import os
from werkzeug.utils import secure_filename
@@ -14,6 +14,8 @@ from utils import log_event, create_notification, get_unread_count
from io import StringIO
import csv
from flask_wtf.csrf import generate_csrf
import json
import smtplib
# Set up logging to show in console
logging.basicConfig(
@@ -631,6 +633,11 @@ def init_routes(main_bp):
site_settings = SiteSettings.get_settings()
company_form = CompanySettingsForm()
# Get SMTP settings for the SMTP tab
smtp_settings = None
if active_tab == 'smtp':
smtp_settings = KeyValueSettings.get_value('smtp_settings')
# Get events for the events tab
events = None
total_pages = 0
@@ -691,8 +698,71 @@ def init_routes(main_bp):
users=users,
email_templates=email_templates,
form=company_form,
smtp_settings=smtp_settings,
csrf_token=generate_csrf())
@main_bp.route('/settings/update-smtp', methods=['POST'])
@login_required
def update_smtp_settings():
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
# Get SMTP settings from form
smtp_settings = {
'smtp_host': request.form.get('smtp_host'),
'smtp_port': int(request.form.get('smtp_port')),
'smtp_security': request.form.get('smtp_security'),
'smtp_username': request.form.get('smtp_username'),
'smtp_password': request.form.get('smtp_password'),
'smtp_from_email': request.form.get('smtp_from_email'),
'smtp_from_name': request.form.get('smtp_from_name')
}
# Save to database using KeyValueSettings
KeyValueSettings.set_value('smtp_settings', smtp_settings)
flash('SMTP settings updated successfully.', 'success')
return redirect(url_for('main.settings', tab='smtp'))
except Exception as e:
db.session.rollback()
flash(f'Error updating SMTP settings: {str(e)}', 'error')
return redirect(url_for('main.settings', tab='smtp'))
@main_bp.route('/settings/test-smtp', methods=['POST'])
@login_required
def test_smtp_connection():
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
data = request.get_json()
# Create SMTP connection
if data['smtp_security'] == 'ssl':
smtp = smtplib.SMTP_SSL(data['smtp_host'], int(data['smtp_port']))
else:
smtp = smtplib.SMTP(data['smtp_host'], int(data['smtp_port']))
# Start TLS if needed
if data['smtp_security'] == 'tls':
smtp.starttls()
# Login
smtp.login(data['smtp_username'], data['smtp_password'])
# Close connection
smtp.quit()
return jsonify({'success': True})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
})
@main_bp.route('/settings/colors', methods=['POST'])
@login_required
def update_colors():

View File

@@ -7,6 +7,7 @@
{% from "settings/tabs/events.html" import events_tab %}
{% from "settings/tabs/email_templates.html" import email_templates_tab %}
{% from "settings/tabs/mails.html" import mails_tab %}
{% from "settings/tabs/smtp_settings.html" import smtp_settings_tab %}
{% from "settings/components/reset_colors_modal.html" import reset_colors_modal %}
{% block title %}Settings - DocuPulse{% endblock %}
@@ -64,6 +65,11 @@
<i class="fas fa-bug me-2"></i>Debugging
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link {% if active_tab == 'smtp' %}active{% endif %}" id="smtp-tab" data-bs-toggle="tab" data-bs-target="#smtp" type="button" role="tab" aria-controls="smtp" aria-selected="{{ 'true' if active_tab == 'smtp' else 'false' }}">
<i class="fas fa-server me-2"></i>SMTP
</button>
</li>
</ul>
</div>
<div class="card-body">
@@ -102,6 +108,11 @@
<div class="tab-pane fade {% if active_tab == 'debugging' %}show active{% endif %}" id="debugging" role="tabpanel" aria-labelledby="debugging-tab">
{{ debugging_tab() }}
</div>
<!-- SMTP Settings Tab -->
<div class="tab-pane fade {% if active_tab == 'smtp' %}show active{% endif %}" id="smtp" role="tabpanel" aria-labelledby="smtp-tab">
{{ smtp_settings_tab(smtp_settings, csrf_token) }}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,311 @@
{% macro smtp_settings_tab(smtp_settings, csrf_token) %}
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<form id="smtpSettingsForm" method="POST" action="{{ url_for('main.update_smtp_settings') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="row">
<!-- SMTP Server Settings -->
<div class="col-md-6 mb-4">
<h6 class="mb-3">SMTP Server Settings</h6>
<div class="mb-3">
<label for="smtp_host" class="form-label">
SMTP Host
<i class="fas fa-info-circle ms-1"
style="color: var(--secondary-color); cursor: pointer;"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-title="SMTP Host Information"
data-bs-content="
<strong>Common SMTP Hosts:</strong><br>
• Gmail: smtp.gmail.com<br>
• Office 365: smtp.office365.com<br>
• Outlook: smtp-mail.outlook.com<br>
• Yahoo: smtp.mail.yahoo.com<br>
• Zoho: smtp.zoho.com<br>
• SendGrid: smtp.sendgrid.net<br>
• Amazon SES: email-smtp.[region].amazonaws.com<br>
• Custom Domain: mail.yourdomain.com or smtp.yourdomain.com<br><br>
<strong>Host Configuration Tips:</strong><br>
• Always use the official SMTP server of your email provider<br>
• For custom domains, check with your hosting provider<br>
• Some providers may require specific subdomains<br>
• Ensure the host is accessible from your server's network"
style="cursor: pointer;"></i>
</label>
<input type="text" class="form-control" id="smtp_host" name="smtp_host"
value="{{ smtp_settings.smtp_host if smtp_settings else '' }}" required>
</div>
<div class="mb-3">
<label for="smtp_port" class="form-label">
SMTP Port
<i class="fas fa-info-circle ms-1"
style="color: var(--secondary-color); cursor: pointer;"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-title="SMTP Port Information"
data-bs-content="
<strong>Common SMTP Ports:</strong><br>
<strong>Port 587 (Recommended):</strong><br>
• Default for TLS encryption<br>
• Most widely supported<br>
• Best for modern email systems<br><br>
<strong>Port 465:</strong><br>
• Used for SSL encryption<br>
• Older but still secure<br>
• Some providers prefer this port<br><br>
<strong>Port 25:</strong><br>
• Traditional SMTP port<br>
• Often blocked by ISPs<br>
• Not recommended for modern use<br><br>
<strong>Port 2525:</strong><br>
• Alternative to port 587<br>
• Used when port 587 is blocked<br>
• Common in cloud environments<br><br>
<strong>Port Selection Guidelines:</strong><br>
• Always use encrypted ports (587 or 465) in production<br>
• Check your email provider's documentation<br>
• Some networks may block certain ports<br>
• Port 25 is often blocked by ISPs"
style="cursor: pointer;"></i>
</label>
<input type="number" class="form-control" id="smtp_port" name="smtp_port"
value="{{ smtp_settings.smtp_port if smtp_settings else '587' }}" required>
</div>
<div class="mb-3">
<label for="smtp_security" class="form-label">
Security
<i class="fas fa-info-circle ms-1"
style="color: var(--secondary-color); cursor: pointer;"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-title="Security Best Practices"
data-bs-content="
<strong>Security Options:</strong><br>
<strong>TLS (Recommended):</strong><br>
• Uses port 587<br>
• Encrypts connection after establishing it<br>
• Most widely supported<br><br>
<strong>SSL:</strong><br>
• Uses port 465<br>
• Encrypts connection from the start<br>
• Older but still secure<br><br>
<strong>None:</strong><br>
• Not recommended for production use<br>
• May be blocked by many providers<br>
• Only use for testing"
style="cursor: pointer;"></i>
</label>
<select class="form-select" id="smtp_security" name="smtp_security">
<option value="tls" {% if smtp_settings and smtp_settings.smtp_security == 'tls' %}selected{% endif %}>TLS</option>
<option value="ssl" {% if smtp_settings and smtp_settings.smtp_security == 'ssl' %}selected{% endif %}>SSL</option>
<option value="none" {% if smtp_settings and smtp_settings.smtp_security == 'none' %}selected{% endif %}>None</option>
</select>
</div>
</div>
<!-- Authentication Settings -->
<div class="col-md-6 mb-4">
<h6 class="mb-3">Authentication</h6>
<div class="mb-3">
<label for="smtp_username" class="form-label">
Username
<i class="fas fa-info-circle ms-1"
style="color: var(--secondary-color); cursor: pointer;"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-title="Username Guidelines"
data-bs-content="
<strong>Username Requirements:</strong><br>
• For Gmail: Use your full email address<br>
• For Office 365: Usually your email address<br>
• For custom domains: Check with your email provider<br>
• If using 2FA, you may need to generate an App Password<br><br>
<strong>Best Practices:</strong><br>
• Always use the email address associated with your SMTP account<br>
• For shared accounts, use a dedicated service account<br>
• Keep track of which username is used for which purpose"
style="cursor: pointer;"></i>
</label>
<input type="text" class="form-control" id="smtp_username" name="smtp_username"
value="{{ smtp_settings.smtp_username if smtp_settings else '' }}" required>
</div>
<div class="mb-3">
<label for="smtp_password" class="form-label">
Password
<i class="fas fa-info-circle ms-1"
style="color: var(--secondary-color); cursor: pointer;"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-title="Password Security"
data-bs-content="
<strong>Password Requirements:</strong><br>
• For Gmail with 2FA: Use an App Password<br>
• For Office 365: May require an App Password if 2FA is enabled<br>
• Never use your main account password for SMTP<br>
• Regularly rotate your SMTP passwords<br><br>
<strong>Security Best Practices:</strong><br>
• Use strong, unique passwords<br>
• Store passwords securely<br>
• Monitor for unauthorized access<br>
• Set up alerts for failed login attempts"
style="cursor: pointer;"></i>
</label>
<input type="password" class="form-control" id="smtp_password" name="smtp_password"
value="{{ smtp_settings.smtp_password if smtp_settings else '' }}" required>
</div>
</div>
<!-- Sender Settings -->
<div class="col-md-6 mb-4">
<h6 class="mb-3">Sender Information</h6>
<div class="mb-3">
<label for="smtp_from_email" class="form-label">
From Email
<i class="fas fa-info-circle ms-1"
style="color: var(--secondary-color); cursor: pointer;"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-title="From Email Guidelines"
data-bs-content="
<strong>Email Requirements:</strong><br>
• Must match the domain of your SMTP server<br>
• Should be a valid, monitored email address<br>
• Use a dedicated sending address (e.g., no-reply@yourdomain.com)<br>
• Ensure the domain has proper SPF, DKIM, and DMARC records<br><br>
<strong>Best Practices:</strong><br>
• Use a consistent sending address<br>
• Set up proper email forwarding<br>
• Monitor bounce rates and spam reports<br>
• Keep the address active and monitored"
style="cursor: pointer;"></i>
</label>
<input type="email" class="form-control" id="smtp_from_email" name="smtp_from_email"
value="{{ smtp_settings.smtp_from_email if smtp_settings else '' }}" required>
</div>
<div class="mb-3">
<label for="smtp_from_name" class="form-label">
From Name
<i class="fas fa-info-circle ms-1"
style="color: var(--secondary-color); cursor: pointer;"
data-bs-toggle="popover"
data-bs-html="true"
data-bs-title="From Name Best Practices"
data-bs-content="
<strong>Name Guidelines:</strong><br>
• Use your company or application name<br>
• Keep it consistent across all emails<br>
• Include department name if relevant (e.g., 'DocuPulse Support')<br>
• Avoid using personal names unless sending personal communications<br>
• Keep it under 50 characters for best display<br><br>
<strong>Best Practices:</strong><br>
• Make it recognizable to recipients<br>
• Avoid special characters<br>
• Consider mobile display length<br>
• Test how it appears in different email clients"
style="cursor: pointer;"></i>
</label>
<input type="text" class="form-control" id="smtp_from_name" name="smtp_from_name"
value="{{ smtp_settings.smtp_from_name if smtp_settings else '' }}" required>
</div>
</div>
<!-- Email Deliverability Best Practices -->
<div class="col-12 mb-4">
<div class="alert alert-info">
<h6 class="alert-heading mb-2">
<i class="fas fa-info-circle me-2"></i>Email Deliverability Best Practices
</h6>
<p class="mb-2">To prevent your emails from being marked as spam, ensure you have:</p>
<ul class="mb-0">
<li><strong>SPF Record:</strong> Add a TXT record to your domain's DNS settings:
<code>v=spf1 include:_spf.your-email-provider.com ~all</code>
</li>
<li><strong>DKIM:</strong> Add the DKIM record provided by your email provider to your domain's DNS settings</li>
<li><strong>DMARC:</strong> Add a TXT record named "_dmarc" with:
<code>v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com</code>
</li>
<li><strong>Reverse DNS (PTR):</strong> Ensure your sending IP has a valid reverse DNS record</li>
<li><strong>Domain Age:</strong> Use a domain that's at least 30 days old</li>
<li><strong>Email Volume:</strong> Start with a small volume and gradually increase</li>
</ul>
</div>
</div>
</div>
<!-- Test Connection Button -->
<div class="mb-4">
<button type="button" class="btn btn-outline-primary" onclick="testSmtpConnection()">
<i class="fas fa-plug me-2"></i>Test Connection
</button>
<div id="testConnectionResult" class="mt-2"></div>
</div>
<!-- Save Button -->
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Save Settings
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
// Initialize all popovers
document.addEventListener('DOMContentLoaded', function() {
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'));
var popoverList = popoverTriggerList.map(function(popoverTriggerEl) {
return new bootstrap.Popover(popoverTriggerEl, {
html: true,
trigger: 'hover',
placement: 'right'
});
});
});
function testSmtpConnection() {
const form = document.getElementById('smtpSettingsForm');
const resultDiv = document.getElementById('testConnectionResult');
const button = event.target;
const originalText = button.innerHTML;
// Disable button and show loading state
button.disabled = true;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Testing...';
// Get form data
const formData = new FormData(form);
// Send test request
fetch('/settings/test-smtp', {
method: 'POST',
headers: {
'X-CSRF-Token': '{{ csrf_token }}',
'Content-Type': 'application/json',
},
body: JSON.stringify(Object.fromEntries(formData))
})
.then(response => response.json())
.then(data => {
if (data.success) {
resultDiv.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle me-2"></i>Connection successful!</div>';
} else {
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times-circle me-2"></i>Connection failed: ${data.error}</div>`;
}
})
.catch(error => {
resultDiv.innerHTML = `<div class="alert alert-danger"><i class="fas fa-times-circle me-2"></i>Error: ${error.message}</div>`;
})
.finally(() => {
// Restore button state
button.disabled = false;
button.innerHTML = originalText;
});
}
</script>
{% endmacro %}