Envois de mail:

- réglage de l'adresse origine From au niveau global
 et systémtisation de son utilisation.
 - ajout de logs, réglage du log par défaut.
 - modernisation de code.
This commit is contained in:
Emmanuel Viennet 2023-02-28 19:43:48 +01:00
parent 50efadf421
commit 7fc3108886
14 changed files with 128 additions and 96 deletions

View File

@ -247,6 +247,7 @@ def create_app(config_class=DevConfig):
migrate.init_app(app, db)
login.init_app(app)
mail.init_app(app)
app.extensions["mail"].debug = 0 # disable copy of mails to stderr
bootstrap.init_app(app)
moment.init_app(app)
cache.init_app(app)
@ -545,10 +546,9 @@ def log_call_stack():
# Alarms by email:
def send_scodoc_alarm(subject, txt):
from app.scodoc import sco_preferences
from app import email
sender = sco_preferences.get_preference("email_from_addr")
sender = email.get_from_addr()
email.send_email(subject, sender, ["exception@scodoc.org"], txt)

View File

@ -1,14 +1,14 @@
# -*- coding: UTF-8 -*
from flask import render_template, current_app
from flask_babel import _
from app.email import send_email
from app.email import get_from_addr, send_email
def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email(
"[ScoDoc] Réinitialisation de votre mot de passe",
sender=current_app.config["SCODOC_MAIL_FROM"],
sender=get_from_addr(),
recipients=[user.email],
text_body=render_template("email/reset_password.txt", user=user, token=token),
html_body=render_template("email/reset_password.j2", user=user, token=token),

View File

@ -11,6 +11,8 @@ from flask import current_app, g
from flask_mail import Message
from app import mail
from app.models.departements import Departement
from app.models.config import ScoDocSiteConfig
from app.scodoc import sco_preferences
@ -56,6 +58,7 @@ def send_message(msg: Message):
In mail debug mode, addresses are discarded and all mails are sent to the
specified debugging address.
"""
email_test_mode_address = False
if hasattr(g, "scodoc_dept"):
# on est dans un département, on peut accéder aux préférences
email_test_mode_address = sco_preferences.get_preference(
@ -81,6 +84,35 @@ Adresses d'origine:
+ msg.body
)
current_app.logger.info(
f"""email sent to{' (mode test)' if email_test_mode_address else ''}: {msg.recipients}
from sender {msg.sender}
"""
)
Thread(
target=send_async_email, args=(current_app._get_current_object(), msg)
).start()
def get_from_addr(dept_acronym: str = None):
"""L'adresse "from" à utiliser pour envoyer un mail
Si le departement est spécifié, ou si l'attribut `g.scodoc_dept`existe,
prend le `email_from_addr` des préférences de ce département si ce champ est non vide.
Sinon, utilise le paramètre global `email_from_addr`.
Sinon, la variable de config `SCODOC_MAIL_FROM`.
"""
dept_acronym = dept_acronym or getattr(g, "scodoc_dept", None)
if dept_acronym:
dept = Departement.query.filter_by(acronym=dept_acronym).first()
if dept:
from_addr = (
sco_preferences.get_preference("email_from_addr", dept_id=dept.id) or ""
).strip()
if from_addr:
return from_addr
return (
ScoDocSiteConfig.get("email_from_addr")
or current_app.config["SCODOC_MAIL_FROM"]
or "none"
)

View File

@ -216,7 +216,7 @@ def send_email_notifications_entreprise(subject: str, entreprise: Entreprise):
txt = "\n".join(txt)
email.send_email(
subject,
sco_preferences.get_preference("email_from_addr"),
email.get_from_addr(),
[EntreprisePreferences.get_email_notifications],
txt,
)

View File

@ -31,8 +31,8 @@ Formulaires configuration Exports Apogée (codes)
from flask import flash, url_for, redirect, request, render_template
from flask_wtf import FlaskForm
from wtforms import BooleanField, SelectField, SubmitField
from wtforms import BooleanField, SelectField, StringField, SubmitField
from wtforms.validators import Email, Optional
import app
from app.models import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
@ -70,6 +70,12 @@ class ScoDocConfigurationForm(FlaskForm):
(i, name.capitalize()) for (i, name) in enumerate(scu.MONTH_NAMES, start=1)
],
)
email_from_addr = StringField(
label="Adresse source des mails",
description="""adresse email source (from) des mails émis par ScoDoc.
Attention: si ce champ peut aussi être défini dans chaque département.""",
validators=[Optional(), Email()],
)
submit_scodoc = SubmitField("Valider")
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
@ -87,6 +93,7 @@ def configuration():
"enable_entreprises": ScoDocSiteConfig.is_entreprises_enabled(),
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
}
)
if request.method == "POST" and (
@ -130,6 +137,8 @@ def configuration():
scu.MONTH_NAMES[ScoDocSiteConfig.get_month_debut_periode2()-1]
}"""
)
if ScoDocSiteConfig.set("email_from_addr", form_scodoc.data["email_from_addr"]):
flash("Adresse email origine enregistrée")
return redirect(url_for("scodoc.index"))
return render_template(

View File

@ -233,8 +233,7 @@ class ScolarNews(db.Model):
txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
subject = "[ScoDoc] " + self.NEWS_MAP.get(self.type, "?")
sender = prefs["email_from_addr"]
sender = email.get_from_addr()
email.send_email(subject, sender, destinations, txt)
@classmethod

View File

@ -32,20 +32,21 @@
Il suffit d'appeler abs_notify() après chaque ajout d'absence.
"""
import datetime
from typing import Optional
from flask import g, url_for
from flask_mail import Message
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import db
from app import email
from app import log
from app.scodoc.scolog import logdb
from app.models.absences import AbsenceNotification
from app.models.events import Scolog
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
from app.scodoc import sco_users
from app import email
def abs_notify(etudid, date):
@ -106,32 +107,24 @@ def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust):
def abs_notify_send(destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id):
"""Actually send the notification by email, and register it in database"""
cnx = ndb.GetDBConnexion()
log("abs_notify: sending notification to %s" % destinations)
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
log(f"abs_notify: sending notification to {destinations}")
for dest_addr in destinations:
msg.recipients = [dest_addr]
email.send_message(msg)
ndb.SimpleQuery(
"""INSERT into absences_notifications
(etudid, email, nbabs, nbabsjust, formsemestre_id)
VALUES (%(etudid)s, %(email)s, %(nbabs)s, %(nbabsjust)s, %(formsemestre_id)s)
""",
{
"etudid": etudid,
"email": dest_addr,
"nbabs": nbabs,
"nbabsjust": nbabsjust,
"formsemestre_id": formsemestre_id,
},
cursor=cursor,
notification = AbsenceNotification(
etudid=etudid,
email=dest_addr,
nbabs=nbabs,
nbabsjust=nbabsjust,
formsemestre_id=formsemestre_id,
)
db.session.add(notification)
logdb(
cnx=cnx,
Scolog.logdb(
method="abs_notify",
etudid=etudid,
msg="sent to %s (nbabs=%d)" % (destinations, nbabs),
msg=f"sent to {destinations} (nbabs={nbabs})",
commit=True,
)
@ -201,39 +194,32 @@ def abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id):
return False
def etud_nbabs_last_notified(etudid, formsemestre_id=None):
def etud_nbabs_last_notified(etudid: int, formsemestre_id: int = None):
"""nbabs lors de la dernière notification envoyée pour cet étudiant dans ce semestre
ou sans semestre (ce dernier cas est nécessaire pour la transition au nouveau code)"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""select * from absences_notifications where etudid = %(etudid)s and (formsemestre_id = %(formsemestre_id)s or formsemestre_id is NULL) order by notification_date desc""",
vars(),
ou sans semestre (ce dernier cas est nécessaire pour la transition au nouveau code)
"""
notifications = (
AbsenceNotification.query.filter_by(etudid=etudid)
.filter(
(AbsenceNotification.formsemestre_id == formsemestre_id)
| (AbsenceNotification.formsemestre_id.is_(None))
)
.order_by(AbsenceNotification.notification_date.desc())
)
res = cursor.dictfetchone()
if res:
return res["nbabs"]
else:
return 0
last_notif = notifications.first()
return last_notif.nbabs if last_notif else 0
def user_nbdays_since_last_notif(email_addr, etudid):
def user_nbdays_since_last_notif(email_addr, etudid) -> Optional[int]:
"""nb days since last notification to this email, or None if no previous notification"""
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT * FROM absences_notifications
WHERE email = %(email_addr)s and etudid=%(etudid)s
ORDER BY notification_date DESC
""",
{"email_addr": email_addr, "etudid": etudid},
)
res = cursor.dictfetchone()
if res:
now = datetime.datetime.now(res["notification_date"].tzinfo)
return (now - res["notification_date"]).days
else:
return None
notifications = AbsenceNotification.query.filter_by(
etudid=etudid, email=email_addr
).order_by(AbsenceNotification.notification_date.desc())
last_notif = notifications.first()
if last_notif:
now = datetime.datetime.now(last_notif.notification_date.tzinfo)
return (now - last_notif.notification_date).days
return None
def abs_notification_message(
@ -264,13 +250,13 @@ def abs_notification_message(
log("abs_notification_message: empty template, not sending message")
return None
subject = """[ScoDoc] Trop d'absences pour %(nomprenom)s""" % etud
msg = Message(subject, sender=prefs["email_from_addr"])
subject = f"""[ScoDoc] Trop d'absences pour {etud["nomprenom"]}"""
msg = Message(subject, sender=email.get_from_addr(formsemestre.departement.acronym))
msg.body = txt
return msg
def retreive_current_formsemestre(etudid: int, cur_date) -> FormSemestre:
def retreive_current_formsemestre(etudid: int, cur_date) -> Optional[FormSemestre]:
"""Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée
date est une chaine au format ISO (yyyy-mm-dd)
@ -296,5 +282,4 @@ def mod_with_evals_at_date(date_abs, etudid):
FROM notes_moduleimpl m, notes_evaluation e, notes_moduleimpl_inscription i
WHERE m.id = e.moduleimpl_id AND e.moduleimpl_id = i.moduleimpl_id
AND i.etudid = %(etudid)s AND e.jour = %(date_abs)s"""
r = ndb.SimpleDictFetch(req, {"etudid": etudid, "date_abs": date_abs})
return r
return ndb.SimpleDictFetch(req, {"etudid": etudid, "date_abs": date_abs})

View File

@ -1080,7 +1080,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
subject = f"""Relevé de notes de {etud["nomprenom"]}"""
recipients = [recipient_addr]
sender = sco_preferences.get_preference("email_from_addr", formsemestre_id)
sender = email.get_from_addr()
if copy_addr:
bcc = copy_addr.strip().split(",")
else:

View File

@ -438,9 +438,7 @@ def notify_etud_change(email_addr, etud, before, after, subject):
log("notify_etud_change: sending notification to %s" % email_addr)
log("notify_etud_change: subject: %s" % subject)
log(txt)
email.send_email(
subject, sco_preferences.get_preference("email_from_addr"), [email_addr], txt
)
email.send_email(subject, email.get_from_addr(), [email_addr], txt)
return txt

View File

@ -308,5 +308,4 @@ Pour plus d'informations sur ce logiciel, voir %s
subject = "Mot de passe ScoDoc"
else:
subject = "Votre accès ScoDoc"
sender = sco_preferences.get_preference("email_from_addr")
email.send_email(subject, sender, [user["email"]], txt)
email.send_email(subject, email.get_from_addr(), [user["email"]], txt)

View File

@ -360,12 +360,31 @@ class BasePreferences(object):
},
),
# ------------------ MISC
(
"email_from_addr",
{
"initvalue": "",
"title": "Adresse mail origine",
"size": 40,
"explanation": """adresse expéditeur pour tous les envois par mails (bulletins,
comptes, etc.).
Si vide, utilise la config globale.""",
"category": "misc",
"only_global": True,
},
),
(
"use_ue_coefs",
{
"initvalue": 0,
"title": "Utiliser les coefficients d'UE pour calculer la moyenne générale (hors BUT)",
"explanation": """Calcule les moyennes dans chaque UE, puis pondère ces résultats pour obtenir la moyenne générale. Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules dans lesquels l'étudiant a des notes. <b>Attention: changer ce réglage va modifier toutes les moyennes du semestre !</b>. Aucun effet en BUT.""",
"title": """Utiliser les coefficients d'UE pour calculer la moyenne générale
(hors BUT)""",
"explanation": """Calcule les moyennes dans chaque UE, puis pondère ces
résultats pour obtenir la moyenne générale.
Par défaut, le coefficient d'une UE est simplement la somme des coefficients des modules
dans lesquels l'étudiant a des notes. <b>Attention: changer ce réglage va modifier toutes
les moyennes du semestre !</b>. Aucun effet en BUT.
""",
"input_type": "boolcheckbox",
"category": "misc",
"labels": ["non", "oui"],
@ -505,7 +524,7 @@ class BasePreferences(object):
{
"initvalue": 7,
"title": "Fréquence maximale de notification",
"explanation": "en jours (pas plus de X envois de mail pour chaque étudiant/destinataire)",
"explanation": "nb de jours minimum entre deux mails envoyés au même destinataire à propos d'un même étudiant ",
"size": 4,
"type": "int",
"convert_numbers": True,
@ -1569,17 +1588,6 @@ class BasePreferences(object):
"category": "bul_mail",
},
),
(
"email_from_addr",
{
"initvalue": current_app.config["SCODOC_MAIL_FROM"],
"title": "adresse mail origine",
"size": 40,
"explanation": "adresse expéditeur pour les envois par mails (bulletins)",
"category": "bul_mail",
"only_global": True,
},
),
(
"bul_intro_mail",
{
@ -2073,7 +2081,7 @@ class BasePreferences(object):
page_title="Préférences",
javascripts=["js/detail_summary_persistence.js"],
),
f"<h2>Préférences globales pour {scu.ScoURL()}</h2>",
f"<h2>Préférences globales pour le département {g.scodoc_dept}</h2>",
# f"""<p><a href="{url_for("scodoc.configure_logos", scodoc_dept=g.scodoc_dept)
# }">modification des logos du département (pour documents pdf)</a></p>"""
# if current_user.is_administrator()

View File

@ -65,7 +65,7 @@
<form id="configuration_form_scodoc" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
{{ form_scodoc.hidden_tag() }}
<div class="row">
<div class="col-md-4">
<div class="col-md-8">
{{ wtf.quick_form(form_scodoc) }}
</div>
</div>

View File

@ -48,13 +48,13 @@ from wtforms import HiddenField, PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Email, ValidationError, EqualTo
from app import db
from app import email
from app.auth.forms import DeactivateUserForm
from app.auth.models import Permission
from app.auth.models import User
from app.auth.models import Role
from app.auth.models import UserRole
from app.auth.models import is_valid_password
from app.email import send_email
from app.models import Departement
from app.models.config import ScoDocSiteConfig
@ -212,7 +212,6 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
user_name = str(user_name)
Role.ensure_standard_roles() # assure la présence des rôles en base
auth_dept = current_user.dept
from_mail = current_app.config["SCODOC_MAIL_FROM"] # current_user.email
initvalues = {}
edit = int(edit)
all_roles = int(all_roles)
@ -699,9 +698,10 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
token = the_user.get_reset_password_token()
else:
token = None
send_email(
# Le from doit utiliser la préférence du département de l'utilisateur
email.send_email(
"[ScoDoc] Création de votre compte",
sender=from_mail, # current_app.config["ADMINS"][0],
sender=email.get_from_addr(),
recipients=[the_user.email],
text_body=render_template(
"email/welcome.txt", user=the_user, token=token

View File

@ -32,7 +32,9 @@ def login():
the user's attributes are saved under the key
'CAS_USERNAME_ATTRIBUTE_KEY'
"""
if not "CAS_SERVER" in current_app.config:
current_app.logger.info("cas_login: no configuration")
return "CAS configuration missing"
cas_token_session_key = current_app.config["CAS_TOKEN_SESSION_KEY"]
redirect_url = create_cas_login_url(