Compare commits

...

7 Commits

7 changed files with 135 additions and 119 deletions

View File

@ -5,6 +5,7 @@ Simple mailing script.
**Table of content** **Table of content**
- [Installation](#installation) - [Installation](#installation)
- [Input format](#input-format)
- [Licence](#licence) - [Licence](#licence)
## Installation ## Installation
@ -14,17 +15,44 @@ On a Debian-based host running at least Debian Stretch, you will need the
following packages: following packages:
- git (recommended for getting the source) - git (recommended for getting the source)
- python3 - python3
- python3-jinja2
### Manual installation ### Manual installation
1. Clone repo 1. Clone repo
$ git clone https://forge.april.org/adminsys/spamotron $ git clone https://forge.april.org/adminsys/spamotron
$ chmod a+x spamotron/mailing.py $ cd spamotron
$ chmod a+x mailing.py
2. Try it 2. Try it
$ ./spamotron/mailing.py --help $ ./mailing.py --help
$ ./mailing.py -t test-recipients-data -b test-emailling --dry-run --verbose
## Input format
### CSV
The « to-file » is expected to be csv with the [default
dialect](https://docs.python.org/3/library/csv.html?highlight=csv#dialects-and-formatting-parameters)
(delimiter = `,` and char delimiter =`"`).
The first line declare columns headers. The `from` and `to` headers are
expected the be found. Headers are lower cased.
To avoid boring repetition it is possible to declare a default column value
using `:` in header. The default value is considered when the column is lacking,
not when the column is empty.
### Templates
The templates files are expected to be [jinja2
templates](https://jinja.palletsprojects.com/en/2.11.x/templates/). The csv
values are accessible using the column name, i.e. one can insert the recipient
address anywhere in template using `{{ to }}`.
Don't worry about escaping: HTML templates are auto escaped.
## License ## License

View File

@ -1,7 +1,7 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
# (C) Olivier Berger 2001-2002 # (C) Olivier Berger 2001-2002
# (C) François Poulain 2019 # (C) François Poulain 2019-2020
# #
# This program is free software; you can redistribute it and/or modify # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -22,10 +22,10 @@
# This program is used to mail a message to several recipients, thus # This program is used to mail a message to several recipients, thus
# providing customization for each recipient. # providing customization for each recipient.
import argparse, sys, re import argparse, csv, sys
import smtplib, time, mimetypes import smtplib, time, mimetypes
from email.header import Header
from email.message import EmailMessage from email.message import EmailMessage
from jinja2 import Environment, FileSystemLoader, select_autoescape
def parse_args(): def parse_args():
@ -34,18 +34,18 @@ def parse_args():
ma.add_argument( ma.add_argument(
"-t", "-t",
"--tofile", "--tofile",
metavar="TO.FILE",
type=str, type=str,
required=True, required=True,
help="Sort of CSV file containing addresses of recipients.", help="CSV file containing recipients data.",
) )
ma.add_argument( ma.add_argument(
"-b", "-b",
"--bodyfile", "--basename",
metavar="BODY.FILE",
type=str, type=str,
required=True, required=True,
help="Template of the mail to be sent", help="Templates basename to be used. The known extensions are: "
".subject for subject, .txt for plain body "
"and optionally .html for html alternative.",
) )
parser.add_argument( parser.add_argument(
"-d", "-d",
@ -56,30 +56,54 @@ def parse_args():
parser.add_argument( parser.add_argument(
"-v", "--verbose", action="store_true", "-v", "--verbose", action="store_true",
) )
parser.add_argument(
"-vv", "--very-verbose", action="store_true",
)
parser.add_argument( parser.add_argument(
"-a", "--attachement", type=str, nargs="+", help="Optionnal attachments.", "-a", "--attachement", type=str, nargs="+", help="Optionnal attachments.",
) )
return parser.parse_args() return parser.parse_args()
# read the recipients file where values are separated by | characters def read_recipients_datas(args):
def read_recipients(args): with open(args.tofile) as csv_file:
recipientfile = open(args.tofile) csv_soup = csv.reader(csv_file)
lines = recipientfile.readlines() first = [(s.split(":", 1)) for s in csv_soup.__next__()]
return [line[:-1].split("|") for line in lines] header = [
(h[0].strip().lower(), h[1].strip() if len(h) == 2 else None) for h in first
]
datas = [[s.strip() for s in d] for d in csv_soup]
full_datas = []
for data in datas:
try:
full_data = {
h[0]: data[i] if i < len(data) or not h[1] else h[1]
for i, h in enumerate(header)
}
full_datas.append(full_data)
except Exception:
print("Ligne mal renseignée:", data, file=sys.stderr)
return full_datas
def read_body(args): def read_templates(args):
bodyfile = open(args.bodyfile) env = Environment(
body = "" loader=FileSystemLoader("."), autoescape=select_autoescape(["html"])
for line in bodyfile.readlines(): )
body = body + line templates = [
return body env.get_template("{}.subject".format(args.basename)),
env.get_template("{}.txt".format(args.basename)),
]
try:
templates.append(env.get_template("{}.html".format(args.basename)))
except Exception:
pass
return templates
def read_join(args): def read_attachments(args):
r = [] r = []
for attachement in args.attachement: for attachement in args.attachement or []:
try: try:
with open(attachement, "rb") as fp: with open(attachement, "rb") as fp:
ctype, encoding = mimetypes.guess_type(attachement) ctype, encoding = mimetypes.guess_type(attachement)
@ -98,86 +122,28 @@ def read_join(args):
return r return r
def replace_values(bodytemplate, values): def send_message(templates, data, attachments, args):
body = bodytemplate dests = [data["to"]]
for i in range(len(values)):
i = i + 1
pattern = "${{%02d}}" % i
body = body.replace(pattern, values[i - 1])
return body
# patterns for header of the mail
FROMPATTERN = re.compile(r"^From: *(.*)")
SUBJECTPATTERN = re.compile(r"^Subject: *(.*)")
CCPATTERN = re.compile(r"^Cc: *(.*)")
BCCPATTERN = re.compile(r"^Bcc: *(.*)")
REPLYTOPATTERN = re.compile(r"^Reply-To: *(.*)")
def qencode_subject(message):
subjectmatches = re.search(r"^Subject: *(.*)$", message, re.MULTILINE)
if subjectmatches == None:
return message
subjectstr = subjectmatches.group(1)
h = Header(subjectstr, "utf-8")
qsubjectstr = h.encode()
return message.replace(subjectstr, qsubjectstr, 1)
def send_message(message, to, attachments, args):
lines = message.split("\n")
# identify headers in the template
fromvalue = None
subjectvalue = None
ccvalue = None
bccvalue = None
replytovalue = None
for index, line in enumerate(lines):
if not line:
body = "\n".join(lines[index + 1 :])
break
frommatches = FROMPATTERN.match(line)
subjectmatches = SUBJECTPATTERN.match(line)
ccmatches = CCPATTERN.match(line)
bccmatches = BCCPATTERN.match(line)
replytomatches = REPLYTOPATTERN.match(line)
if frommatches:
fromvalue = frommatches.group(1)
if subjectmatches:
subjectvalue = subjectmatches.group(1)
if ccmatches:
ccvalue = ccmatches.group(1)
if bccmatches:
bccvalue = bccmatches.group(1)
if replytomatches:
replytovalue = replytomatches.group(1)
# lists all addresses to which the mail will be sent
dests = [to]
if ccvalue:
dests.append(ccvalue)
if bccvalue:
dests.append(bccvalue)
msg = EmailMessage() msg = EmailMessage()
msg["Subject"] = subjectvalue msg["Subject"] = templates[0].render(**data).strip()
msg["From"] = fromvalue msg["From"] = data["from"]
msg["To"] = to msg["To"] = data["to"]
if replytovalue: if "reply-to" in data:
msg["Reply-To"] = replytovalue msg["Reply-To"] = data["reply-to"]
if ccvalue: if "cc" in data:
msg["Cc"] = ccvalue msg["Cc"] = data["cc"]
msg.set_content(body) dests.append(data["cc"])
if "bcc" in data:
msg["Bcc"] = data["bcc"]
dests.append(data["bcc"])
msg.set_content(templates[1].render(**data))
if len(templates) > 2:
alt = templates[2].render(**data)
msg.add_alternative(alt)
if attachments: if attachments:
for attachment in attachments: for attachment in attachments:
msg.add_attachment(attachment[0], **attachment[1]) msg.add_attachment(attachment[0], **attachment[1])
@ -188,13 +154,14 @@ def send_message(message, to, attachments, args):
else: else:
print( print(
"Sending : %s, from %s to %s, dests : %s" "Sending : %s, from %s to %s, dests : %s"
% (subjectvalue, fromvalue, dests[0], repr(dests)) % (msg["Subject"], data["from"], dests[0], repr(dests))
) )
if not args.dry_run: if not args.dry_run:
# sending the mail to its recipients using the local mailer via SMTP # sending the mail to its recipients using the local mailer via SMTP
server = smtplib.SMTP("localhost", "25") server = smtplib.SMTP("localhost", "25")
server.set_debuglevel(1) if args.very_verbose:
server.set_debuglevel(1)
server.send_message(msg) server.send_message(msg)
server.quit() server.quit()
@ -204,32 +171,22 @@ if __name__ == "__main__":
args = parse_args() args = parse_args()
# read the recipients file # read the recipients file
sets = read_recipients(args) datas = read_recipients_datas(args)
# read the template of the mail # read the templates of the mail
bodytemplate = read_body(args) templates = read_templates(args)
# optionnaly read the attachment of the mail # optionnaly read the attachment of the mail
attachment = None attachments = read_attachments(args)
if args.attachement:
attachments = read_join(args)
# counter to be able to pause each 10 mails send # counter to be able to pause each 10 mails send
counter = 0 counter = 0
# send mail to each recipient # send mail to each recipient
for set in sets: for data in datas:
values = set
# the recipient is always in first column
recipient = values[0]
# replace substitution variables in the template for each recipient
body = replace_values(bodytemplate, values)
# send message # send message
send_message(body, recipient, attachments, args) send_message(templates, data, attachments, args)
# pause every 10 mails for 5 secs so that the MTA doesn't explode # pause every 10 mails for 5 secs so that the MTA doesn't explode
counter = counter + 1 counter = counter + 1

7
test-emailling.footer Normal file
View File

@ -0,0 +1,7 @@
--
Vous disposez d'un droit d'accès, de modification, de rectification et de
suppression des données vous concernant (loi « Informatique et Libertés » du 6
janvier 1978). Pour toute demande, merci de vous adressez à contact@april.org.
Si vous souhaitez ne plus recevoir nos communiqués de presse merci de vous
adressez à contact@april.org.

15
test-emailling.html Normal file
View File

@ -0,0 +1,15 @@
<html>
<header>
<title>
Vive le libre en fête
</title>
</header>
<body>
<p>
Chère {{ nom }},
</p><p>
Venez découvrir le logiciel libre autour du printemps. Déjà 42 événements pour
le Libre en Fête à venir.
</p>
</body>
</html>

1
test-emailling.subject Normal file
View File

@ -0,0 +1 @@
Vive le libre en fête

4
test-emailling.txt Normal file
View File

@ -0,0 +1,4 @@
Chère {{ nom }},
Venez découvrir le logiciel libre autour du printemps. Déjà 42 événements pour
le Libre en Fête à venir.

4
test-recipients-data Normal file
View File

@ -0,0 +1,4 @@
to,nom,from:president@april.org,cc:contact@april.org,reply-to:contact@april.org
noc@april.org,network operating center
secretaire@april.org,l'secrétaire,president@paril.org,
prez@april.org,présidente,prez@elysee.fr