Compare commits

...

14 Commits

8 changed files with 200 additions and 167 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# Editors
*~
*.sw[po]
\#*\#
.\#*
# Python
*.py[cod]
__pycache__

34
README.md Normal file
View File

@ -0,0 +1,34 @@
# Spamotron
Simple mailing script.
**Table of content**
- [Installation](#installation)
- [Licence](#licence)
## Installation
### Requirements
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
$ cd spamotron
$ chmod a+x mailing.py
2. Try it
$ ./mailing.py --help
$ ./mailing.py -t test-recipients-data -b test-emailling --dry-run --verbose
## License
Spamotron is developed by April and licensed under the [GPLv2+](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,200 +22,160 @@
# 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 sys, getopt, 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
#
# displays usage of the program, then quit
#
def usage(returncode):
print("""SYNTAX:
""", sys.argv[0], """ -t tofile -b bodyfile [-p attachedfile]
tofile : sort of CSV file containing addresses of recipients
bodyfile : template of the mail to be sent
attachedfile : optionnal attachment
""")
sys.exit(returncode)
TOFILE = None def parse_args():
BODYFILE = None parser = argparse.ArgumentParser(description="Simple mailing script.")
JOINFILE = None ma = parser.add_argument_group(title="mandatory arguments", description=None)
ma.add_argument(
"-t",
"--tofile",
type=str,
required=True,
help="CSV file containing recipients data.",
)
ma.add_argument(
"-b",
"--basename",
type=str,
required=True,
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",
"--dry-run",
action="store_true",
help="Load data but don't send anything.",
)
parser.add_argument(
"-v", "--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_datas(args):
def read_recipients() : with open(args.tofile) as csv_file:
recipientfile = open(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
]
def read_body() : datas = [[s.strip() for s in d] for d in csv_soup]
bodyfile = open(BODYFILE) full_datas = []
body = "" for data in datas:
for line in bodyfile.readlines() :
body = body + line
return body
def read_join() :
try: try:
with open(JOINFILE, 'rb') as fp: full_data = {
ctype, encoding = mimetypes.guess_type(JOINFILE) h[0]: data[i] if i < len(data) or not h[1] else h[1]
if ctype is None or encoding is not None: for i, h in enumerate(header)
ctype = 'application/octet-stream'
maintype, subtype = ctype.split('/', 1)
metadata = {
'filename': JOINFILE,
'maintype': maintype,
'subtype': subtype,
} }
return (fp.read(), metadata) except Exception:
print("Ligne mal renseignée:", data, file=sys.stderr)
full_datas.append(full_data)
return full_datas
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_attachments(args):
r = []
for attachement in args.attachement or []:
try:
with open(attachement, "rb") as fp:
ctype, encoding = mimetypes.guess_type(attachement)
if ctype is None or encoding is not None:
ctype = "application/octet-stream"
maintype, subtype = ctype.split("/", 1)
metadata = {
"filename": attachement,
"maintype": maintype,
"subtype": subtype,
}
r.append((fp.read(), metadata))
except Exception as e: except Exception as e:
print("read error: %s" % e) print("read error: %s" % e)
exit(1) exit(1)
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 def send_message(templates, data, attachments, args):
FROMPATTERN = re.compile(r"^From: *(.*)") dests = [data["to"]]
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, attachment) :
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 attachment: if "bcc" in data:
dests.append(data["bcc"])
msg.set_content(templates[1].render(**data))
if attachments:
for attachment in attachments:
msg.add_attachment(attachment[0], **attachment[1]) msg.add_attachment(attachment[0], **attachment[1])
print("Sending : %s, from %s to %s, dests : %s" % (subjectvalue, fromvalue, dests[0], repr(dests))) if args.verbose:
print(msg)
print("-" * 80)
else:
print(
"Sending : %s, from %s to %s, dests : %s"
% (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 # 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) server.set_debuglevel(1)
server.send_message(msg) server.send_message(msg)
server.quit() server.quit()
if __name__ == "__main__":
args = parse_args()
if __name__ == '__main__' :
# handle options of the program
try:
opts, args = getopt.getopt(sys.argv[1:], 't:b:p:')
except getopt.error as msg:
print("getopt error: %s" % msg)
usage(1)
for name, value in opts:
if name == '-t': TOFILE = value
elif name == '-b': BODYFILE = value
elif name == '-p': JOINFILE = value
else:
print("argument: ", name, " invalid")
usage(1)
if not TOFILE or not BODYFILE:
usage(1)
# read the recipients file # read the recipients file
sets = read_recipients() datas = read_recipients_datas(args)
# read the template of the mail # read the templates of the mail
bodytemplate = read_body() 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 JOINFILE:
attachment = read_join()
# 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, attachment) 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
@ -223,4 +183,3 @@ if __name__ == '__main__' :
counter = 0 counter = 0
print("suspending execution for 5 secs") print("suspending execution for 5 secs")
time.sleep(5) time.sleep(5)

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 @@
Vivle 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,secrétaire,president@paril.org,
prez@april.org,présidente,prez@elysee.fr