- Convert mail messaging from SMTP to Scaleway TEM mails

This commit is contained in:
Josako
2025-05-10 10:49:15 +02:00
parent a421977918
commit 12a53ebc1c
14 changed files with 90 additions and 71 deletions

View File

@@ -135,15 +135,6 @@ class Config(object):
"LLM": {"name": "LLM", "description": "Algorithm using information integrated in the used LLM"} "LLM": {"name": "LLM", "description": "Algorithm using information integrated in the used LLM"}
} }
# flask-mailman settings
MAIL_SERVER = environ.get('MAIL_SERVER')
MAIL_PORT = int(environ.get('MAIL_PORT', 465))
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_USERNAME = environ.get('MAIL_USERNAME')
MAIL_PASSWORD = environ.get('MAIL_PASSWORD')
MAIL_DEFAULT_SENDER = ('Evie', MAIL_USERNAME)
# Email settings for API key notifications # Email settings for API key notifications
PROMOTIONAL_IMAGE_URL = 'https://askeveai.com/wp-content/uploads/2024/07/Evie-Call-scaled.jpg' # Replace with your actual URL PROMOTIONAL_IMAGE_URL = 'https://askeveai.com/wp-content/uploads/2024/07/Evie-Call-scaled.jpg' # Replace with your actual URL
@@ -175,6 +166,13 @@ class Config(object):
PUSH_GATEWAY_PORT = environ.get('PUSH_GATEWAY_PORT', '9091') PUSH_GATEWAY_PORT = environ.get('PUSH_GATEWAY_PORT', '9091')
PUSH_GATEWAY_URL = f"{PUSH_GATEWAY_HOST}:{PUSH_GATEWAY_PORT}" PUSH_GATEWAY_URL = f"{PUSH_GATEWAY_HOST}:{PUSH_GATEWAY_PORT}"
# Scaleway parameters
SW_EMAIL_ACCESS_KEY = environ.get('SW_EMAIL_ACCESS_KEY')
SW_EMAIL_SECRET_KEY = environ.get('SW_EMAIL_SECRET_KEY')
SW_EMAIL_SENDER = environ.get('SW_EMAIL_SENDER')
SW_EMAIL_NAME = environ.get('SW_EMAIL_NAME')
SW_PROJECT = environ.get('SW_PROJECT')
class DevConfig(Config): class DevConfig(Config):
DEVELOPMENT = True DEVELOPMENT = True
@@ -252,15 +250,6 @@ class ProdConfig(Config):
WTF_CSRF_SSL_STRICT = True # Set to True if using HTTPS WTF_CSRF_SSL_STRICT = True # Set to True if using HTTPS
# flask-mailman settings
MAIL_SERVER = 'mail.askeveai.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USE_SSL = False
MAIL_DEFAULT_SENDER = ('Evie Admin', 'evie_admin@askeveai.com')
MAIL_USERNAME = environ.get('MAIL_USERNAME')
MAIL_PASSWORD = environ.get('MAIL_PASSWORD')
# Define the nginx prefix used for the specific apps # Define the nginx prefix used for the specific apps
EVEAI_APP_LOCATION_PREFIX = '/admin' EVEAI_APP_LOCATION_PREFIX = '/admin'
EVEAI_CHAT_LOCATION_PREFIX = '/chat' EVEAI_CHAT_LOCATION_PREFIX = '/chat'

View File

@@ -270,14 +270,6 @@ LOGGING = {
'backupCount': 2, 'backupCount': 2,
'formatter': 'standard', 'formatter': 'standard',
}, },
'file_mailman': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
'filename': 'logs/mailman.log',
'maxBytes': 1024 * 1024 * 1, # 1MB
'backupCount': 2,
'formatter': 'standard',
},
'file_security': { 'file_security': {
'level': 'DEBUG', 'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler', 'class': 'logging.handlers.RotatingFileHandler',
@@ -389,11 +381,6 @@ LOGGING = {
'level': 'DEBUG', 'level': 'DEBUG',
'propagate': False 'propagate': False
}, },
'mailman': { # logger for the mailman
'handlers': ['file_mailman', 'graylog', ] if env == 'production' else ['file_mailman', ],
'level': 'DEBUG',
'propagate': False
},
'security': { # logger for the security 'security': { # logger for the security
'handlers': ['file_security', 'graylog', ] if env == 'production' else ['file_security', ], 'handlers': ['file_security', 'graylog', ] if env == 'production' else ['file_security', ],
'level': 'DEBUG', 'level': 'DEBUG',

View File

@@ -18,10 +18,6 @@ x-common-variables: &common-variables
FLASK_DEBUG: true FLASK_DEBUG: true
SECRET_KEY: '97867c1491bea5ee6a8e8436eb11bf2ba6a69ff53ab1b17ecba450d0f2e572e1' SECRET_KEY: '97867c1491bea5ee6a8e8436eb11bf2ba6a69ff53ab1b17ecba450d0f2e572e1'
SECURITY_PASSWORD_SALT: '228614859439123264035565568761433607235' SECURITY_PASSWORD_SALT: '228614859439123264035565568761433607235'
MAIL_USERNAME: evie@askeveai.com
MAIL_PASSWORD: 'D**0z@UGfJOI@yv3eC5'
MAIL_SERVER: mail.flow-it.net
MAIL_PORT: 465
REDIS_URL: redis REDIS_URL: redis
REDIS_PORT: '6379' REDIS_PORT: '6379'
FLOWER_USER: 'Felucia' FLOWER_USER: 'Felucia'
@@ -41,6 +37,11 @@ x-common-variables: &common-variables
CREWAI_STORAGE_DIR: "/app/crewai_storage" CREWAI_STORAGE_DIR: "/app/crewai_storage"
PUSH_GATEWAY_HOST: "pushgateway" PUSH_GATEWAY_HOST: "pushgateway"
PUSH_GATEWAY_PORT: "9091" PUSH_GATEWAY_PORT: "9091"
SW_EMAIL_ACCESS_KEY: "SCWFMQ871RE4XGF04SW0"
SW_EMAIL_SECRET_KEY: "ec84604c-e2d4-4b0d-a120-40420693f42a"
SW_EMAIL_SENDER: "admin_dev@mail.askeveai.be"
SW_EMAIL_NAME: "Evie Admin (dev)"
SW_PROJECT: "f282f55a-ea52-4538-a979-5bcb890717ab"
services: services:
nginx: nginx:

View File

@@ -6,7 +6,7 @@ from flask_security.signals import user_authenticated
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
import logging.config import logging.config
from common.extensions import (db, migrate, bootstrap, security, mail, login_manager, cors, csrf, session, from common.extensions import (db, migrate, bootstrap, security, login_manager, cors, csrf, session,
minio_client, simple_encryption, metrics, cache_manager) minio_client, simple_encryption, metrics, cache_manager)
from common.models.user import User, Role, Tenant, TenantDomain from common.models.user import User, Role, Tenant, TenantDomain
import common.models.interaction import common.models.interaction
@@ -19,7 +19,7 @@ from .errors import register_error_handlers
from common.utils.celery_utils import make_celery, init_celery from common.utils.celery_utils import make_celery, init_celery
from common.utils.template_filters import register_filters from common.utils.template_filters import register_filters
from config.config import get_config from config.config import get_config
from eveai_app.views.security_forms import ResetPasswordForm from eveai_app.views.security_forms import ResetPasswordForm, ForgotPasswordForm
def create_app(config_file=None): def create_app(config_file=None):
@@ -40,6 +40,7 @@ def create_app(config_file=None):
app.config['SESSION_KEY_PREFIX'] = 'eveai_app_' app.config['SESSION_KEY_PREFIX'] = 'eveai_app_'
app.config['SECURITY_RESET_PASSWORD_FORM'] = ResetPasswordForm app.config['SECURITY_RESET_PASSWORD_FORM'] = ResetPasswordForm
app.config['SECURITY_FORGOT_PASSWORD_FORM'] = ForgotPasswordForm
try: try:
os.makedirs(app.instance_path) os.makedirs(app.instance_path)
@@ -79,8 +80,6 @@ def create_app(config_file=None):
# Debugging settings # Debugging settings
if app.config['DEBUG'] is True: if app.config['DEBUG'] is True:
app.logger.setLevel(logging.DEBUG) app.logger.setLevel(logging.DEBUG)
mail_logger = logging.getLogger('flask_mailman')
mail_logger.setLevel(logging.DEBUG)
security_logger = logging.getLogger('flask_security') security_logger = logging.getLogger('flask_security')
security_logger.setLevel(logging.DEBUG) security_logger.setLevel(logging.DEBUG)
sqlalchemy_logger = logging.getLogger('sqlalchemy.engine') sqlalchemy_logger = logging.getLogger('sqlalchemy.engine')
@@ -117,7 +116,6 @@ def register_extensions(app):
db.init_app(app) db.init_app(app)
migrate.init_app(app, db) migrate.init_app(app, db)
bootstrap.init_app(app) bootstrap.init_app(app)
mail.init_app(app)
csrf.init_app(app) csrf.init_app(app)
login_manager.init_app(app) login_manager.init_app(app)
cors.init_app(app) cors.init_app(app)

View File

@@ -38,21 +38,21 @@
<div class="tab-content tab-space"> <div class="tab-content tab-space">
<!-- Storage Tab --> <!-- Storage Tab -->
<div class="tab-pane fade show active" id="storage-tab" role="tabpanel"> <div class="tab-pane fade show active" id="storage-tab" role="tabpanel">
{% set storage_fields = ['max_storage_mb', 'additional_storage_price', 'additional_storage_bucket'] %} {% set storage_fields = ['max_storage_mb', 'additional_storage_allowed', 'additional_storage_price', 'additional_storage_bucket'] %}
{% for field in form %} {% for field in form %}
{{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=storage_fields) }} {{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=storage_fields) }}
{% endfor %} {% endfor %}
</div> </div>
<!-- Embedding Tab --> <!-- Embedding Tab -->
<div class="tab-pane fade" id="embedding-tab" role="tabpanel"> <div class="tab-pane fade" id="embedding-tab" role="tabpanel">
{% set embedding_fields = ['included_embedding_mb', 'additional_embedding_price', 'additional_embedding_bucket', 'overage_embedding'] %} {% set embedding_fields = ['included_embedding_mb', 'additional_embedding_allowed', 'additional_embedding_price', 'additional_embedding_bucket', 'overage_embedding'] %}
{% for field in form %} {% for field in form %}
{{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=embedding_fields) }} {{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=embedding_fields) }}
{% endfor %} {% endfor %}
</div> </div>
<!-- Interaction Tab --> <!-- Interaction Tab -->
<div class="tab-pane fade" id="interaction-tab" role="tabpanel"> <div class="tab-pane fade" id="interaction-tab" role="tabpanel">
{% set interaction_fields = ['included_interaction_tokens', 'additional_interaction_token_price', 'additional_interaction_bucket', 'overage_interaction'] %} {% set interaction_fields = ['included_interaction_tokens', 'additional_interaction_allowed', 'additional_interaction_token_price', 'additional_interaction_bucket', 'overage_interaction'] %}
{% for field in form %} {% for field in form %}
{{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=interaction_fields) }} {{ render_included_field(field, readonly_fields=ext_readonly_fields, include_fields=interaction_fields) }}
{% endfor %} {% endfor %}

View File

@@ -7,7 +7,7 @@
{% block content %} {% block content %}
{% include "security/_messages.html" %} {% include "security/_messages.html" %}
<form action="{{ url_for_security('forgot_password') }}" method="post" name="forgot_password_form"> <form action="{{ url_for('security_bp.forgot_password') }}" method="post" novalidate>
{{ forgot_password_form.hidden_tag() }} {{ forgot_password_form.hidden_tag() }}
<p> <p>

View File

@@ -26,8 +26,7 @@
{# </div>#} {# </div>#}
{% endblock %} {% endblock %}
{% block content_footer %} {% block content_footer %}
First time here? Forgot your password? Forgot your password? Contact your administrator to send you a password reset link.
<a href="/reset" class="text-success text-gradient font-weight-bold">Request new password</a>
{% endblock %} {% endblock %}
{#{{ url_for_security('reset_password', token=reset_password_token) }}#} {#{{ url_for_security('reset_password', token=reset_password_token) }}#}

View File

@@ -51,10 +51,12 @@ class LicenseForm(FlaskForm):
yearly_payment = BooleanField('Yearly Payment', default=False) yearly_payment = BooleanField('Yearly Payment', default=False)
basic_fee = FloatField('Basic Fee', validators=[InputRequired(), NumberRange(min=0)]) basic_fee = FloatField('Basic Fee', validators=[InputRequired(), NumberRange(min=0)])
max_storage_mb = IntegerField('Max Storage (MiB)', validators=[DataRequired(), NumberRange(min=1)]) max_storage_mb = IntegerField('Max Storage (MiB)', validators=[DataRequired(), NumberRange(min=1)])
additional_storage_allowed = BooleanField('Additional Storage Allowed', default=True)
additional_storage_price = FloatField('Additional Storage Token Fee', additional_storage_price = FloatField('Additional Storage Token Fee',
validators=[InputRequired(), NumberRange(min=0)]) validators=[InputRequired(), NumberRange(min=0)])
additional_storage_bucket = IntegerField('Additional Storage Bucket Size (MiB)', additional_storage_bucket = IntegerField('Additional Storage Bucket Size (MiB)',
validators=[DataRequired(), NumberRange(min=1)]) validators=[DataRequired(), NumberRange(min=1)])
additional_embedding_allowed = BooleanField('Additional Embedding Allowed', default=True)
included_embedding_mb = IntegerField('Included Embedding Tokens (MiB)', included_embedding_mb = IntegerField('Included Embedding Tokens (MiB)',
validators=[DataRequired(), NumberRange(min=1)]) validators=[DataRequired(), NumberRange(min=1)])
additional_embedding_price = FloatField('Additional Embedding Token Fee', additional_embedding_price = FloatField('Additional Embedding Token Fee',
@@ -63,6 +65,7 @@ class LicenseForm(FlaskForm):
validators=[DataRequired(), NumberRange(min=1)]) validators=[DataRequired(), NumberRange(min=1)])
included_interaction_tokens = IntegerField('Included Interaction Tokens (M Tokens)', included_interaction_tokens = IntegerField('Included Interaction Tokens (M Tokens)',
validators=[DataRequired(), NumberRange(min=1)]) validators=[DataRequired(), NumberRange(min=1)])
additional_interaction_allowed = BooleanField('Additional Interaction Allowed', default=True)
additional_interaction_token_price = FloatField('Additional Interaction Token Fee', additional_interaction_token_price = FloatField('Additional Interaction Token Fee',
validators=[InputRequired(), NumberRange(min=0)]) validators=[InputRequired(), NumberRange(min=0)])
additional_interaction_bucket = IntegerField('Additional Interaction Bucket Size (M Tokens)', additional_interaction_bucket = IntegerField('Additional Interaction Bucket Size (M Tokens)',

View File

@@ -152,8 +152,9 @@ def create_license(license_tier_id):
tenant_id = session.get('tenant').get('id') tenant_id = session.get('tenant').get('id')
currency = session.get('tenant').get('currency') currency = session.get('tenant').get('currency')
if current_user_has_role("Partner Admin"): # The Partner Admin can only set the end date if current_user_has_role("Partner Admin"): # The Partner Admin can only set start & end dates, and allowed fields
readonly_fields = [field.name for field in form if (field.name != 'end_date' and field.name != 'start_date')] readonly_fields = [field.name for field in form if (field.name != 'end_date' and field.name != 'start_date' and
not field.name.endswith('allowed'))]
if request.method == 'GET': if request.method == 'GET':
# Fetch the LicenseTier # Fetch the LicenseTier

View File

@@ -2,10 +2,6 @@ from flask import current_app
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import PasswordField, SubmitField, StringField from wtforms import PasswordField, SubmitField, StringField
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, EqualTo from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, EqualTo
from flask_security.forms import ForgotPasswordForm
from flask_security.utils import send_mail, config_value
from common.utils.nginx_utils import prefixed_url_for
class SetPasswordForm(FlaskForm): class SetPasswordForm(FlaskForm):
@@ -14,7 +10,7 @@ class SetPasswordForm(FlaskForm):
submit = SubmitField('Set Password') submit = SubmitField('Set Password')
class RequestResetForm(FlaskForm): class ForgotPasswordForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()]) email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Request Password Reset') submit = SubmitField('Request Password Reset')

View File

@@ -13,7 +13,7 @@ from sqlalchemy.exc import SQLAlchemyError
from common.models.user import User from common.models.user import User
from common.utils.eveai_exceptions import EveAIException, EveAINoActiveLicense from common.utils.eveai_exceptions import EveAIException, EveAINoActiveLicense
from common.utils.nginx_utils import prefixed_url_for from common.utils.nginx_utils import prefixed_url_for
from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, RequestResetForm from eveai_app.views.security_forms import SetPasswordForm, ResetPasswordForm, ForgotPasswordForm
from common.extensions import db from common.extensions import db
from common.utils.security_utils import confirm_token, send_confirmation_email, send_reset_email from common.utils.security_utils import confirm_token, send_confirmation_email, send_reset_email
from common.utils.security import set_tenant_session_data, is_valid_tenant from common.utils.security import set_tenant_session_data, is_valid_tenant
@@ -111,16 +111,16 @@ def confirm_email(token):
return redirect(prefixed_url_for('basic_bp.confirm_email_ok')) return redirect(prefixed_url_for('basic_bp.confirm_email_ok'))
@security_bp.route('/reset_password_request', methods=['GET', 'POST']) @security_bp.route('/forgot_password', methods=['GET', 'POST'])
def reset_password_request(): def forgot_password():
form = RequestResetForm() form = ForgotPasswordForm()
if form.validate_on_submit(): if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first() user = User.query.filter_by(email=form.email.data).first()
if user: if user:
send_reset_email(user) send_reset_email(user)
flash('An email with instructions to reset your password has been sent.', 'info') flash('An email with instructions to reset your password has been sent.', 'info')
return redirect(prefixed_url_for('security_bp.login')) return redirect(prefixed_url_for('security_bp.login'))
return render_template('security/reset_password_request.html', form=form) return render_template('security/forgot_password.html', form=form)
@security_bp.route('/reset_password/<token>', methods=['GET', 'POST']) @security_bp.route('/reset_password/<token>', methods=['GET', 'POST'])

View File

@@ -1,7 +1,6 @@
import uuid import uuid
from datetime import datetime as dt, timezone as tz from datetime import datetime as dt, timezone as tz
from flask import request, redirect, flash, render_template, Blueprint, session, current_app from flask import request, redirect, flash, render_template, Blueprint, session, current_app
from flask_mailman import EmailMessage
from flask_security import roles_accepted, current_user from flask_security import roles_accepted, current_user
from sqlalchemy.exc import SQLAlchemyError, IntegrityError from sqlalchemy.exc import SQLAlchemyError, IntegrityError
import ast import ast
@@ -21,6 +20,7 @@ from common.utils.eveai_exceptions import EveAIException
from common.utils.document_utils import set_logging_information, update_logging_information from common.utils.document_utils import set_logging_information, update_logging_information
from common.services.tenant_services import TenantServices from common.services.tenant_services import TenantServices
from common.services.user_services import UserServices from common.services.user_services import UserServices
from common.utils.mail_utils import send_email
user_bp = Blueprint('user_bp', __name__, url_prefix='/user') user_bp = Blueprint('user_bp', __name__, url_prefix='/user')
@@ -670,20 +670,15 @@ def send_api_key_notification(tenant_id, tenant_name, project_name, api_key, ser
} }
try: try:
# Create email message # Create email message
msg = EmailMessage( msg = send_email(
subject='Your new API-key from Ask Eve AI (Evie)', subject='Your new API-key from Ask Eve AI (Evie)',
body=render_template('email/api_key_notification.html', **context), html=render_template('email/api_key_notification.html', **context),
from_email=current_app.config['MAIL_DEFAULT_SENDER'], to_email=recipient_email,
to=[recipient_email] to_name=recipient_email,
) )
# Set HTML content type
msg.content_subtype = "html"
# Send email
msg.send()
current_app.logger.info(f"API key notification sent to {recipient_email} for tenant {tenant_id}") current_app.logger.info(f"API key notification sent to {recipient_email} for tenant {tenant_id}")
return True return True

View File

@@ -0,0 +1,48 @@
"""Add Allowed fields for overrun in License
Revision ID: fa6113ce4306
Revises: 9ed466e9756b
Create Date: 2025-05-08 16:05:55.612416
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fa6113ce4306'
down_revision = '9ed466e9756b'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('license', schema=None) as batch_op:
batch_op.add_column(
sa.Column('additional_storage_allowed', sa.Boolean(), nullable=True))
batch_op.add_column(
sa.Column('additional_embedding_allowed', sa.Boolean(), nullable=True))
batch_op.add_column(
sa.Column('additional_interaction_allowed', sa.Boolean(), nullable=True))
op.execute("""
UPDATE license
SET additional_storage_allowed = TRUE,
additional_embedding_allowed = TRUE,
additional_interaction_allowed = TRUE
""")
with op.batch_alter_table('license', schema=None) as batch_op:
batch_op.alter_column('additional_storage_allowed', nullable=False)
batch_op.alter_column('additional_embedding_allowed', nullable=False)
batch_op.alter_column('additional_interaction_allowed', nullable=False)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('license', schema=None) as batch_op:
batch_op.drop_column('additional_interaction_allowed')
batch_op.drop_column('additional_embedding_allowed')
batch_op.drop_column('additional_storage_allowed')
# ### end Alembic commands ###

View File

@@ -12,7 +12,6 @@ Flask-Bootstrap~=3.3.7.1
Flask-Cors~=5.0.1 Flask-Cors~=5.0.1
Flask-JWT-Extended~=4.7.1 Flask-JWT-Extended~=4.7.1
Flask-Login~=0.6.3 Flask-Login~=0.6.3
flask-mailman~=1.1.1
Flask-Migrate~=4.1.0 Flask-Migrate~=4.1.0
Flask-Principal~=0.4.0 Flask-Principal~=0.4.0
Flask-Security-Too~=5.6.2 Flask-Security-Too~=5.6.2
@@ -90,3 +89,6 @@ mistral-common~=1.5.3
mistralai~=1.6.0 mistralai~=1.6.0
contextvars~=2.4 contextvars~=2.4
pandas~=2.2.3 pandas~=2.2.3
prometheus_client~=0.21.1
scaleway~=2.9.0
html2text~=2025.4.15