diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc index 92562ba..704ae3b 100644 Binary files a/__pycache__/models.cpython-313.pyc and b/__pycache__/models.cpython-313.pyc differ diff --git a/migrations/versions/0a8006bd1732_add_key_value_settings_table.py b/migrations/versions/0a8006bd1732_add_key_value_settings_table.py new file mode 100644 index 0000000..eecf644 --- /dev/null +++ b/migrations/versions/0a8006bd1732_add_key_value_settings_table.py @@ -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 ### diff --git a/migrations/versions/__pycache__/0a8006bd1732_add_key_value_settings_table.cpython-313.pyc b/migrations/versions/__pycache__/0a8006bd1732_add_key_value_settings_table.cpython-313.pyc new file mode 100644 index 0000000..db91cf9 Binary files /dev/null and b/migrations/versions/__pycache__/0a8006bd1732_add_key_value_settings_table.cpython-313.pyc differ diff --git a/models.py b/models.py index c84db27..5b69336 100644 --- a/models.py +++ b/models.py @@ -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', @@ -158,7 +159,38 @@ class SiteSettings(db.Model): settings = cls() db.session.add(settings) db.session.commit() - return settings + 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) diff --git a/routes/__pycache__/__init__.cpython-313.pyc b/routes/__pycache__/__init__.cpython-313.pyc index 7c496fc..3cb40c7 100644 Binary files a/routes/__pycache__/__init__.cpython-313.pyc and b/routes/__pycache__/__init__.cpython-313.pyc differ diff --git a/routes/__pycache__/main.cpython-313.pyc b/routes/__pycache__/main.cpython-313.pyc index 4bf8c80..2ecb22f 100644 Binary files a/routes/__pycache__/main.cpython-313.pyc and b/routes/__pycache__/main.cpython-313.pyc differ diff --git a/routes/main.py b/routes/main.py index 62bd3a6..02e419b 100644 --- a/routes/main.py +++ b/routes/main.py @@ -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(): diff --git a/templates/settings/settings.html b/templates/settings/settings.html index c57a6e7..ded4261 100644 --- a/templates/settings/settings.html +++ b/templates/settings/settings.html @@ -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 @@ Debugging +
@@ -102,6 +108,11 @@
{{ debugging_tab() }}
+ + +
+ {{ smtp_settings_tab(smtp_settings, csrf_token) }} +
diff --git a/templates/settings/tabs/smtp_settings.html b/templates/settings/tabs/smtp_settings.html new file mode 100644 index 0000000..0d343dd --- /dev/null +++ b/templates/settings/tabs/smtp_settings.html @@ -0,0 +1,311 @@ +{% macro smtp_settings_tab(smtp_settings, csrf_token) %} +
+
+
+
+
+ + +
+ +
+
SMTP Server Settings
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
Authentication
+
+ + +
+
+ + +
+
+ + +
+
Sender Information
+
+ + +
+
+ + +
+
+ + +
+
+
+ Email Deliverability Best Practices +
+

To prevent your emails from being marked as spam, ensure you have:

+
    +
  • SPF Record: Add a TXT record to your domain's DNS settings: + v=spf1 include:_spf.your-email-provider.com ~all +
  • +
  • DKIM: Add the DKIM record provided by your email provider to your domain's DNS settings
  • +
  • DMARC: Add a TXT record named "_dmarc" with: + v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com +
  • +
  • Reverse DNS (PTR): Ensure your sending IP has a valid reverse DNS record
  • +
  • Domain Age: Use a domain that's at least 30 days old
  • +
  • Email Volume: Start with a small volume and gradually increase
  • +
+
+
+
+ + +
+ +
+
+ + +
+ +
+
+
+
+
+
+ + +{% endmacro %} \ No newline at end of file