script_mass_mail/mail.py

255 lines
9.2 KiB
Python
Raw Normal View History

2021-03-04 02:38:30 +01:00
#!/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")