255 lines
9.2 KiB
Python
255 lines
9.2 KiB
Python
|
#!/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")
|