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**
- [Installation](#installation)
- [Input format](#input-format)
- [Licence](#licence)
## Installation
@ -14,17 +15,44 @@ On a Debian-based host running at least Debian Stretch, you will need the
following packages:
- git (recommended for getting the source)
- python3
- python3-jinja2
### Manual installation
1. Clone repo
$ git clone https://forge.april.org/adminsys/spamotron
$ chmod a+x spamotron/mailing.py
$ cd spamotron
$ chmod a+x mailing.py
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

View File

@ -1,7 +1,7 @@
#! /usr/bin/env python3
# (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
# 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
# providing customization for each recipient.
import argparse, sys, re
import argparse, csv, sys
import smtplib, time, mimetypes
from email.header import Header
from email.message import EmailMessage
from jinja2 import Environment, FileSystemLoader, select_autoescape
def parse_args():
@ -34,18 +34,18 @@ def parse_args():
ma.add_argument(
"-t",
"--tofile",
metavar="TO.FILE",
type=str,
required=True,
help="Sort of CSV file containing addresses of recipients.",
help="CSV file containing recipients data.",
)
ma.add_argument(
"-b",
"--bodyfile",
metavar="BODY.FILE",
"--basename",
type=str,
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(
"-d",
@ -56,30 +56,54 @@ def parse_args():
parser.add_argument(
"-v", "--verbose", action="store_true",
)
parser.add_argument(
"-vv", "--very-verbose", action="store_true",
)
parser.add_argument(
"-a", "--attachement", type=str, nargs="+", help="Optionnal attachments.",
)
return parser.parse_args()
# read the recipients file where values are separated by | characters
def read_recipients(args):
recipientfile = open(args.tofile)
lines = recipientfile.readlines()
return [line[:-1].split("|") for line in lines]
def read_recipients_datas(args):
with open(args.tofile) as csv_file:
csv_soup = csv.reader(csv_file)
first = [(s.split(":", 1)) for s in csv_soup.__next__()]
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):
bodyfile = open(args.bodyfile)
body = ""
for line in bodyfile.readlines():
body = body + line
return body
def read_templates(args):
env = Environment(
loader=FileSystemLoader("."), autoescape=select_autoescape(["html"])
)
templates = [
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 = []
for attachement in args.attachement:
for attachement in args.attachement or []:
try:
with open(attachement, "rb") as fp:
ctype, encoding = mimetypes.guess_type(attachement)
@ -98,86 +122,28 @@ def read_join(args):
return r
def replace_values(bodytemplate, values):
body = bodytemplate
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)
def send_message(templates, data, attachments, args):
dests = [data["to"]]
msg = EmailMessage()
msg["Subject"] = subjectvalue
msg["From"] = fromvalue
msg["To"] = to
if replytovalue:
msg["Reply-To"] = replytovalue
if ccvalue:
msg["Cc"] = ccvalue
msg.set_content(body)
msg["Subject"] = templates[0].render(**data).strip()
msg["From"] = data["from"]
msg["To"] = data["to"]
if "reply-to" in data:
msg["Reply-To"] = data["reply-to"]
if "cc" in data:
msg["Cc"] = data["cc"]
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:
for attachment in attachments:
msg.add_attachment(attachment[0], **attachment[1])
@ -188,13 +154,14 @@ def send_message(message, to, attachments, args):
else:
print(
"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:
# sending the mail to its recipients using the local mailer via SMTP
server = smtplib.SMTP("localhost", "25")
server.set_debuglevel(1)
if args.very_verbose:
server.set_debuglevel(1)
server.send_message(msg)
server.quit()
@ -204,32 +171,22 @@ if __name__ == "__main__":
args = parse_args()
# read the recipients file
sets = read_recipients(args)
datas = read_recipients_datas(args)
# read the template of the mail
bodytemplate = read_body(args)
# read the templates of the mail
templates = read_templates(args)
# optionnaly read the attachment of the mail
attachment = None
if args.attachement:
attachments = read_join(args)
attachments = read_attachments(args)
# counter to be able to pause each 10 mails send
counter = 0
# send mail to each recipient
for set in sets:
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)
for data in datas:
# 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
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