🎉 first commit

This commit is contained in:
otthorn 2021-03-04 02:38:30 +01:00 committed by otthorn
commit 7767674ae7
20 changed files with 671 additions and 0 deletions

24
README.md Normal file
View file

@ -0,0 +1,24 @@
# Script de massmail
Ce script est un fork (a peine modifié) du script de massmail utilsé au crans
qui se base la même architecture a base de re2o. Ce depo est fait pour être
pull et exectué sur `re2o-server.adm.auro.re`
## Utilisation
Tout est expliqué dans l'aide. Pour optenir de l'aide :
```
./mail_all.py --help
```
## Exemples
Envoyer uniquement sur des mails de test pour vous si tout fonctionne bien
```
./mail_all.py -s "Aurore <no-reply@auro.re>" -f list_test_mails.txt -t Innondations_Fev_2021 -p
```
On oublie pas de tester avec `p` avant tout envoie de mail, ne surtout pas
faire de `--doit` avant d'avoir fait une simulation !

41
cprint.py Normal file
View file

@ -0,0 +1,41 @@
#!/bin/python3
# -*- mode: python; coding: utf-8 -*-
def coul(txt, col=None):
"""
Retourne la chaine donnée encadrée des séquences qui
vont bien pour obtenir la couleur souhaitée
Les couleur sont celles de codecol
Il est possible de changer la couleur de fond grace aux couleur f_<couleur>
"""
if not col:
return txt
codecol = {'rouge': 31,
'vert': 32,
'jaune': 33,
'bleu': 34,
'violet': 35,
'cyan': 36,
'gris': 30,
'gras': 50}
codecol_dialog = {'rouge': 1,
'vert': 2,
'jaune': 3,
'bleu': 4,
'violet': 5,
'cyan': 6,
'gris': 0,
'gras': 'b'}
try:
if col[:2] == 'f_':
add = 10
col = col[2:]
else:
add = 0
txt = "\033[1;%sm%s\033[1;0m" % (codecol[col] + add, txt)
finally:
return txt
def cprint(txt, col='blanc'):
print(coul(txt, col))

27
locale_util.py Executable file
View file

@ -0,0 +1,27 @@
# -*- mode: python; coding: utf-8 -*-
# Source:
# http://stackoverflow.com/questions/18593661/how-do-i-strftime-a-date-object-in-a-different-locale
import locale
import threading
from datetime import datetime
from contextlib import contextmanager
LOCALE_LOCK = threading.Lock()
@contextmanager
def setlocale(name):
with LOCALE_LOCK:
saved = locale.setlocale(locale.LC_ALL)
try:
current_val = locale.setlocale(locale.LC_ALL, name)
except:
current_val = saved
print("Warning: Failed setting locale %r" % name)
try:
yield current_val
finally:
locale.setlocale(locale.LC_ALL, saved)

254
mail.py Executable file
View file

@ -0,0 +1,254 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
import jinja2
import sys
import json
import inspect
import locale
import smtplib
import traceback
from contextlib import contextmanager
from email.header import Header
from email.message import Message
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate, parseaddr, formataddr
try:
import misaka
def markdown(text):
return misaka.html(text, misaka.EXT_TABLES)
except ImportError:
from markdown import markdown
from locale_util import setlocale
if '/usr/scripts' not in sys.path:
sys.path.append('/usr/scripts')
default_language = 'fr'
template_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'template') + '/'
html_template = template_path + 'html'
html_mutilang_template = template_path + 'html_multilang'
text_mutilang_template = template_path + 'text_multilang'
templateLoader = jinja2.FileSystemLoader( searchpath=["/", template_path] )
templateEnv = jinja2.Environment( loader=templateLoader )
def format_date(d):
""" Renvoie une jolie représentation (unicode) d'un datetime"""
# L'encoding dépend de ce qu'on a choisi plus bas
lang, encoding = locale.getlocale()
if not encoding:
encoding = 'ascii'
if lang == 'fr_FR':
return d.strftime('%A %d %B %Y').decode(encoding)
else:
return d.strftime('%A, %B %d %Y').decode(encoding)
def given_name(adh):
"""Renvoie le joli nom d'un adhérent"""
if adh.is_class_club:
return "Club {}".format(adh.surname)
return adh.name + " " + adh.surname
templateEnv.filters['date'] = format_date
templateEnv.filters['name'] = given_name
# file extension to rendering function map
markup = {
'md' : markdown,
'html' : lambda x:x,
}
### For an example:
### print (generate('bienvenue', {'From':'respbats@crans.org', 'To':'admin@genua.fr', 'lang_info':'English version below'}).as_bytes())
### or from a shell : python -c "import mail; print(mail.generate('bienvenue', {'From':'respbats@crans.org', 'To':'admin@genua.fr', 'lang_info':'English version below'}))"
def submessage(payload, type, charset='utf-8'):
"""Renvois un sous message à mettre dans un message multipart"""
submsg = MIMEText('', type, charset)
del(submsg['Content-Transfer-Encoding'])
# submsg['Content-Transfer-Encoding'] = '8bit'
# submsg['Content-Disposition'] = 'inline'
submsg.set_payload(payload)
return submsg
def get_lang(mail, part, lang, lang_fallback):
"""Récupère le chemin vers le fichier à utiliser, en fonction de la
langue souhaitée"""
for l in [lang, lang_fallback]:
for ext in markup.keys():
if os.path.isfile(template_path + mail + '/' + part + '/' + l + '.' + ext):
return l, ext, template_path + mail + '/' + part + '/' + l + '.' + ext
if os.path.isfile(template_path + mail + '/' + part + '/' + l):
return l, None, template_path + mail + '/' + part + '/' + l
raise ValueError("Language %s nor %s found" % (lang, lang_fallback))
def gen_local_body(fname, params, lang):
"""Génère le texte localisé d'un body"""
locales = {
'fr': 'fr_FR.UTF-8',
'en': 'en_US.UTF-8',
}
with setlocale(locales.get(lang, 'C')):
return templateEnv.get_template(fname).render(params)
def body(mail, lang1, lang2, mk, params, charset):
"""Génère le texte du mail, en deux langues, avec une extension `mk` donnée
"""
ret = []
file1 = template_path + mail + '/body/' + lang1
file2 = template_path + mail + '/body/' + lang2
if mk:
file1 = file1 + '.' + mk
file2 = file2 + '.' + mk
if lang1 == lang2 or not os.path.isfile(file2): # No alt language
txt = gen_local_body(file1, params, lang1)
if mk != "html":
ret.append(submessage(txt.encode(charset), 'plain', charset))
if mk: # compute the html version
html = templateEnv.get_template(html_template).render({'body': markup[mk](txt)})
ret.append(submessage(html.encode(charset), 'html', charset))
else:
txt1 = gen_local_body(file1, params, lang1)
txt2 = gen_local_body(file2, params, lang2)
if mk != "html":
params_txt = dict(params)
params_txt.update({'body1': txt1, 'body2': txt2})
txt = templateEnv.get_template(text_mutilang_template).render(params_txt)
ret.append(submessage(txt.encode(charset), 'plain', charset))
if mk: # compute the html version
params_html = dict(params)
params_html.update({
'lang1':lang1,
'lang2':lang2,
'body1': markup[mk](txt1),
'body2': markup[mk](txt2),
})
html = templateEnv.get_template(html_mutilang_template).render(params_html)
ret.append(submessage(html.encode(charset), 'html', charset))
return ret
def generate(mail, params, lang=default_language, lang_fallback=default_language, lang_alt='en', charset='utf-8'):
"""Génère un message multipart"""
if 'mailer' not in params:
# Il y a vraiment des gens qui lisent ce champ ?
params['mailer'] = "Un MA de Aurore a personnalisé ce message à la main pour toi"
msg = MIMEMultipart('mixed')
inline_msg = MIMEMultipart('alternative')
if os.path.isdir(template_path + mail):
for filename in [dir for dir in os.listdir(template_path + mail) if os.path.isdir(template_path + mail + '/' + dir)]:
lang_tmp, mk, file = get_lang(mail, filename, lang, lang_fallback)
if filename == 'body':
for part in body(mail, lang_tmp, lang_alt, mk, params, charset):
inline_msg.attach(part)
else:
txt = templateEnv.get_template(file).render(params)
if filename in ['From', 'To', 'Cc', 'Bcc']:
msg[filename] = format_sender(txt, charset)
else:
msg[filename] = Header(txt.encode(charset), charset)
msg['Date'] = formatdate(localtime=True)
msg.attach(inline_msg)
return msg
def format_sender(sender, header_charset='utf-8'):
"""
Check and format sender for header.
"""
# Split real name (which is optional) and email address parts
sender_name, sender_addr = parseaddr(sender)
# We must always pass Unicode strings to Header, otherwise it will
# use RFC 2047 encoding even on plain ASCII strings.
sender_name = str(Header(str(sender_name), header_charset))
# # Make sure email addresses do not contain non-ASCII characters
# sender_addr = sender_addr.encode('ascii')
return formataddr((sender_name, sender_addr))
@contextmanager
def bugreport():
"""Context manager: Si erreur, renvoie un bugreport avec un traceback à
roots@."""
try:
yield
except Exception as exc:
From = 'root@auro.re'
to = From
tb = sys.exc_info()[2].tb_next
data = {
'from': From,
'to': to,
'exc': exc,
'lineno': tb.tb_frame.f_lineno,
'filename': os.path.basename(tb.tb_frame.f_code.co_filename),
'traceback': traceback.format_exc(),
}
mail = generate('bugreport', data)
with ServerConnection() as conn:
conn.sendmail(From, [to], mail.as_string())
raise
class ServerConnection(object):
"""Connexion au serveur smtp"""
_conn = None
def __enter__(self):
if os.getenv('DBG_MAIL') != 'print':
self._conn = smtplib.SMTP('localhost')
return self
def check_sender(self, mail):
"""Vérifie l'expéditeur, pour éviter certaines erreurs"""
if mail.split('@')[0].lower() in ['ca.aurore', 'tech.aurore', 'communication.aurore', 'events.aurore']:
raise Exception(u"Merci d'utiliser une autre adresse mail d'expédition, celle-ci est une ML publique sur laquelle des bounces seraient malvenus.")
def sendmail(self, From, to, mail):
"""Envoie un mail"""
self.check_sender(From)
if os.getenv('DBG_MAIL', False):
deb = os.getenv('DBG_MAIL')
if '@' in deb:
to = [deb]
else:
print(mail)
return
self._conn.sendmail(From, to, mail)
def send_template(self, tpl_name, data):
"""Envoie un mail à partir d'un template.
`data` est un dictionnaire contenant entre
"""
From = data.get('from', '')
adh = data.get('adh', data.get('proprio', ''))
to = data.get('to', None) or (adh.get_mail() if adh else None)
if to is None:
print("Pas de mail valide pour %r. Skipping..." % (adh, ))
return
# TODO: get lang toussa
body = generate(tpl_name, data).as_string()
self.sendmail(From, to, body)
def __exit__(self, type, value, traceback):
if os.getenv('DBG_MAIL') != 'print':
self._conn.quit()
# TODO: intégrer ceci dans le ServerConnection
def postconf(i):
"Fixe la fréquence d'envoi maximale par client (en msg/min)"
os.system("/usr/sbin/postconf -e smtpd_client_message_rate_limit=%s" % i)
os.system("/etc/init.d/postfix reload")
# opt = commands.getoutput("/usr/sbin/postconf smtpd_client_message_rate_limit")

197
mail_all.py Executable file
View file

@ -0,0 +1,197 @@
#!/usr/bin/python3
# -*- mode: python; coding: utf-8 -*-
"""
Script générique pour envoyer des mails en masse ~~au crans~~ à Aurore après un
(petit) fork
"""
import os
import sys
import django
import argparse
from base64 import b64decode
import json
from cprint import cprint
try:
import mail
except ImportError:
cprint("Vérifie bien d'avoir tous les paquets (par exemple python3-jinja2)", "rouge")
def mail_sender(template, From, recipients, PREV=True, SEND=False, cc=None, bcc=None):
"""
``template`` template du mail à envoyer.
``From`` Pour remplir le champ From du mail.
``recipients`` Liste des addresses mails recipiendaires.
``PREV`` Booléen specifiant s'il faut faire un dry-run avec prévisualisation.
Default = True.
``SEND`` Booléen spécifiant s'il faut effectivement envoyer le mail.
Default = False.
``cc`` Liste des addresses mails en copie.
``bcc`` Liste des addresses mails en copie cachée.
"""
echecs = []
if PREV:
print("Envoi simulé")
try:
print("{} destinataires (Ctrl + C pour annuler l'envoi)".format(len(recipients)))
input("Envoyer ? (Ret pour envoyer)\n")
except KeyboardInterrupt:
cprint("\nEnvoi annulé.", "rouge")
sys.exit(1)
with mail.ServerConnection() as conn_smtp:
for line in recipients:
extra_params = {}
if len(line.split(';')) == 3:
# Format Prénom Nom Email
Prenom, Nom, To = line.split(':')
elif len(line.split(';')) == 2:
# Format Prénom Email
Nom, Prenom, To = '', line.split(';')
elif len(line.split(';')) == 1:
# Juste un email
Prenom, Nom, To = '', '', line
else:
# Format Prénom Nom Email ExtraJson
Prenom, Nom, To, extra = line.split(';')
extra_params = json.loads(extra)
print("Envoi du mail à {}".format(To))
try:
params = {'To' : To,
'From' : From,
'lang_info' : 'English version below',
'Prenom' : Prenom,
'Nom': Nom,
}
params.update(extra_params)
mailtxt = mail.generate(
template,
params).as_bytes()
except KeyError: # Il faut corriger le fichier source pour que ça marche
cprint("Félicitation tu viens de tomber sur un bug python (cf {})".format(
"https://bugs.python.org/issue27321"),
"rouge")
raise
if PREV:
print(mailtxt)
try:
if SEND:
conn_smtp.sendmail(From, (To,), mailtxt)
print(" Envoyé !")
else:
print(" Simulé !")
except:
print(sys.exc_info()[:2])
cprint("Erreur lors de l'envoi à {} ".format(To), "rouge")
echecs.append(To)
if not SEND:
cprint("\n\
/!\ Avant d'envoyer réellement ce mail all, as-tu vérifié que:\n\
- Les lignes sont bien formatées à 75-80 caractères ?\n\
- Le texte a été lu et relu ?\n\
- Il existe une version en anglais ?\n\
- Les destinataires sont bien les bons ?\n\
- Il y a bien une signature ?\n",
'rouge'
)
if echecs:
print("\nIl y a eu des erreurs pour les addresses suivantes :")
for echec in echecs:
print(" - {}\n".format(echec))
sys.exit(1)
RE2O = '/var/www/re2o'
if __name__=="__main__":
if not RE2O in sys.path:
sys.path.append(RE2O)
try:
import re2o
except ImportError:
print("Nécessite une instance re2o")
sys.exit(42)
# Setup l'environnement django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 're2o.settings')
django.setup()
parser = argparse.ArgumentParser(
description="Mail all générique. Prend un template en argument.",
add_help=True
)
parser.add_argument(
"-f", "--recipientfile",
help="Un fichier contenant un destinataire par ligne. Les formats acceptés sont : `Prenom:Nom:Email`, `Prenom:Email`, ou tout simplement `Email`. Override tous les filtres. Dans le premier cas, on peut aussi rajouter des paramètres supplémentaires sous format json.",
action="store"
)
parser.add_argument(
"-t", "--template",
help="Un template de mail. Fournir le chemin du dossier principal du mail, relatif à "
+ mail.template_path,
action="store"
)
parser.add_argument(
"-s", "--sender",
help="Spécifier un expéditeur particulier. Par défaut no-reply@auro.re",
action="store",
default="no-reply@auro.re"
)
filtres = parser.add_argument_group('Filtres')
filtres.add_argument(
"-a", "--allaccess",
help="Envoie un mail à toutes les personnes bénéficiant d'une connexion valide.",
action="store_true"
)
filtres.add_argument(
"-A", "--alladh",
help="Envoie un mail à tous les adhérents.",
action="store_true"
)
exclusive = parser.add_mutually_exclusive_group(required=True)
exclusive.add_argument(
"--doit",
help="Lance effectivement le mail",
action="store_true"
)
exclusive.add_argument(
"-p", "--prev",
help="Prévisualise le mail à envoyer",
action="store_true"
)
args = parser.parse_args()
if args.recipientfile:
with open(args.recipientfile, 'r') as recipientfile:
recipients = recipientfile.readlines()
else:
if args.allaccess:
print("{} du mail à toutes les personnes bénéficiant d'une connexion valide (Ctrl + C pour annuler l'envoi)".format("Simulation" if args.prev else "Envoi"))
input("Envoyer ? (Ret pour envoyer)\n")
users = re2o.utils.all_has_access()
elif args.alladh:
print("{} du mail à tous les adhérents (Ctrl + C pour annuler l'envoi)".format("Simulation" if args.prev else "Envoi"))
input("Envoyer ? (Ret pour envoyer)\n")
users = re2o.utils.all_adherent()
else:
print("Spécifier au moins un destinataire")
sys.exit(2)
recipients = ['{}:{}:{}'.format(user.name,
user.surname,
user.get_mail)
for user in users]
mail_sender(args.template, args.sender, recipients, args.prev, args.doit)
sys.exit(0)

View file

@ -0,0 +1 @@
"L'équipe d'Aurore" <no-reply@auro.re>

View file

@ -0,0 +1 @@
Fin de la coupure d'internet globale -- End of the global internet outage

View file

@ -0,0 +1 @@
{{To}}

View file

@ -0,0 +1 @@
{{ mailer }}

View file

@ -0,0 +1,28 @@
Dear members,
As you probably noticed, Aurore's Internet connection went down from March 2nd
2:41 PM to March 3rd 3:49 PM.
This service interruption was due to an incident in a nearby construction site,
where an excavator severed our optical fiber. The break in the optical fiber
caused the technicians to pull a new one as a replacement.
Nonetheless, we would like to apologise for the inconvinence and thank you for
your patience.
We would also like to thank the members who reached us via
support.aurore@lists.crans.org and Matrix to inform us of the incident, as they
helped our technical team to confirm our diagnosis.
Since 3:50 PM, we have not noticed any issue on our network. All services are
working as intended. We do not declare any permanent loss in term of data or
quality of service, in particular, our bandwidth does not seem to be affected
by the new fiber.
If you are still experiencing any trouble, please contact us at once at our
support email address (support.aurore@lists.crans.org).
To conclude, "With a great excavator comes great responsibility".
Best regards,
Aurore's team

View file

@ -0,0 +1,29 @@
Bonjour,
Comme vous avez pu le constater, Aurore a subi une interruption de service
entre le mardi 2 mars à 14h41 et le mercredi 3 mars à 15h49.
Cet incident était dû à une erreur sur un chantier du plateau du Moulon, où une
fibre optique nous reliant au reste du réseau Internet a été coupée. La rupture
de la fibre optique a forcé les techniciens à tirer une nouvelle fibre en
remplacement.
Nous voulons nous excuser pour le désagrément lié à cette interruption de
service, et vous remercier pour votre patience.
Nous tenons aussi à remercier les adhérents qui nous ont contacté via
support.aurore@lists.crans.org et Matrix afin de signaler l'incident, ce qui a
permis de conforter le diagnostic effectué par nos équipes.
Depuis 15h50, nous ne constatons plus de défaillance sur le réseau. Tous les
services sont revenus en node nominal. Nous ne déplorons aucune perte de
données ou de qualité de service, en particulier le débit est revenu à la
normale.
Si vous subissez encore des dysfonctionnements, vous pouvez contacter notre
support (support.aurore@lists.crans.org).
En conclusion, « une grande pelleteuse implique de grandes responsabilités ».
Cordialement,
L'équipe d'Aurore

View file

@ -0,0 +1 @@
"L'équipe technique d'Aurore" <no-reply@auro.re>

View file

@ -0,0 +1 @@
Problème de connexion -- Internet trouble

View file

@ -0,0 +1 @@
{{To}}

View file

@ -0,0 +1 @@
{{ mailer }}

View file

@ -0,0 +1,11 @@
Hello everyone,
Due to flooding in the core network of Aurore today, our internet connection
has been experiencing some troubles, with many cuts. A temporary solution has
been etablished, in order to maintain a decent quality of service.
We apologize for any inconvenience caused
--
The technical members of Aurore

View file

@ -0,0 +1,13 @@
Bonsoir à tous,
Aujourd'hui, une inondation est survenue dans le coeur de réseau d'Aurore.
Suite à cela, nous rencontrons des problèmes d'alimentation électrique
entraînant une baisse de qualité de nos services, ainsi que de nombreuses
coupures. Le temps que nous soyons en mesure de régler le problème, une
solution temporaire a été mise en place.
Nous nous excusons pour les désagréments occasionnés.
--
L'équipe technique d'Aurore

15
template/html Executable file
View file

@ -0,0 +1,15 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta name="generator" content="pandoc" />
<title></title>
<style type="text/css">code{white-space: pre;}body{max-width: 55em;text-align:justify;}</style>
</head>
<body>
{% block body %}
{{body}}
{% endblock %}
</body>
</html>

13
template/html_multilang Executable file
View file

@ -0,0 +1,13 @@
{% extends "html" %}
{% block body %}
<p>{{lang_info}}</p>
<div lang="{{lang1}}">
{{body1}}
</div>
<hr/>
<div lang="{{lang2}}">
{{body2}}
</div>
{% endblock %}

11
template/text_multilang Executable file
View file

@ -0,0 +1,11 @@
{{lang_info}}
{{body1}}
================================================================================
{{body2}}