controle_don/main.py: change sendmail to use actual mail system

This commit is contained in:
Adrien Bourmault 2024-09-16 16:30:30 +02:00
parent e7442c89aa
commit bf39e8a8b9
Signed by: neox
GPG Key ID: 57BC26A3687116F6
3 changed files with 126 additions and 139 deletions

View File

@ -1,7 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
# Libre en Communs's adhesion control program # Libre en Communs's adhesion control program
# Copyright (C) 2022 Libre en Communs # Copyright (C) 2022-2024 Libre en Communs
# 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 Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
# Libre en Communs's cotisation control program # Libre en Communs's cotisation control program
# Copyright (C) 2022 Libre en Communs # Copyright (C) 2022-2024 Libre en Communs
# 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 Affero General Public License as # it under the terms of the GNU Affero General Public License as

View File

@ -1,7 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
# Libre en Communs's mecene control program # Libre en Communs's mecene control program
# Copyright (C) 2022 Libre en Communs # Copyright (C) 2022-2024 Libre en Communs
# 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 Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -16,33 +16,42 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
import os, requests, json, datetime, shutil, quopri, subprocess, base64 import os
import requests
import datetime
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from requests.auth import HTTPDigestAuth
VERSION = "1.0.0"
GESTION_SECRET_FILE = "/home/tresorier/.secret/gestion_api_password"
WORKDIR = "/srv/validation_don.d"
MODALITY_MAIL = "mail_instructions.txt"
MODALITY_MAIL_HEADERS = "mail_instructions_headers.txt"
RECEPT_MAIL = "mail_recu.txt"
RECEPT_MAIL_HEADERS = "mail_recu_headers.txt"
RECEPT_MAIL_ATTACHMENT = "mail_recu_attachment.txt"
SUMMARY_MAIL = "mail_recap_header.txt"
VERSION="1.0.0" # SMTP settings (adjust as necessary)
GESTION_SECRET_FILE="/home/tresorier/.secret/gestion_api_password" SMTP_SERVER = 'mail.a-lec.org'
WORKDIR="/srv/validation_don.d" SMTP_PORT = 587
MODALITY_MAIL="mail_instructions.txt" SMTP_USER = 'tresorier@a-lec.org'
MODALITY_MAIL_HEADERS="mail_instructions_headers.txt" SMTP_SECRET_FILE = "/home/tresorier/.secret/smtp_api_password"
RECEPT_MAIL="mail_recu.txt"
RECEPT_MAIL_HEADERS="mail_recu_headers.txt"
RECEPT_MAIL_ATTACHMENT="mail_recu_attachment.txt"
SUMMARY_MAIL="mail_recap_header.txt"
SENDMAIL_LOCATION = "/usr/sbin/sendmail" # sendmail location
BUF=[]
# gestion_read("SELECT * FROM services_users su \ BUF = []
# INNER JOIN membres m ON m.id = su.id_user \
# INNER JOIN services_fees sf ON sf.id = su.id_fee \
# LEFT JOIN acc_transactions_users tu ON tu.id_service_user = su.id \
# LEFT JOIN acc_transactions_lines l ON l.id_transaction = tu.id_transaction \
# WHERE m.id = 3 AND l.id_account = 481;")
# Utility functions
def gestion_get_secret(): def gestion_get_secret():
with open(GESTION_SECRET_FILE) as sfile: with open(GESTION_SECRET_FILE) as sfile:
return sfile.readline().replace("\n", "") return sfile.readline().strip()
def smtp_get_secret():
with open(SMTP_SECRET_FILE) as sfile:
return sfile.readline().strip()
def get_file_content(filename): def get_file_content(filename):
with open(filename) as sfile: with open(filename) as sfile:
@ -58,90 +67,95 @@ def set_file_content(filename, lines):
def gestion_read(req): def gestion_read(req):
response = requests.post('https://gestion.a-lec.org/api/sql/', response = requests.post('https://gestion.a-lec.org/api/sql/',
auth = HTTPBasicAuth('api666', gestion_get_secret()), auth=HTTPBasicAuth('api666', gestion_get_secret()),
data = req) data=req)
return response.json() return response.json()
def setup_workdir(): def setup_workdir():
if not os.path.isdir(WORKDIR): for subdir in ['transition', 'nouveau', 'validé']:
os.mkdir(WORKDIR) path = os.path.join(WORKDIR, subdir)
if not "transition" in os.listdir(WORKDIR): if not os.path.exists(path):
os.mkdir(WORKDIR+"/transition") os.makedirs(path)
if not "nouveau" in os.listdir(WORKDIR):
os.mkdir(WORKDIR+"/nouveau")
if not "validé" in os.listdir(WORKDIR):
os.mkdir(WORKDIR+"/validé")
def sendmail(headers, data): # Send email with attachment via SMTP, password fetched from file
msg = bytes(headers + "\n", 'utf-8') + quopri.encodestring(bytes(data, 'utf-8')) def sendmail_with_attachment(headers, body, attachment_path, attachment_filename):
subprocess.run([SENDMAIL_LOCATION, "-t", "-oi"], input=msg) msg = MIMEMultipart()
def sendmail_with_attachment(headers, data, attachment_header, attachment, ending): # Add headers
msg = bytes(headers + "\n", 'utf-8') \ for header in headers.split("\n"):
+ quopri.encodestring(bytes(data, 'utf-8')) \ if ": " in header:
+ bytes(attachment_header + "\n", 'utf-8') \ key, value = header.split(": ", 1)
+ base64.b64encode(attachment) \ msg[key] = value.strip()
+ bytes(ending, 'utf-8')
subprocess.run([SENDMAIL_LOCATION, "-t", "-oi"], input=msg) # Email body
msg.attach(MIMEText(body, 'plain', 'utf-8'))
# Attach PDF file
if attachment_path:
with open(attachment_path, "rb") as attachment:
part = MIMEBase('application', 'octet-stream')
part.set_payload(attachment.read())
encoders.encode_base64(part)
part.add_header('Content-Disposition', f'attachment; filename={attachment_filename}')
msg.attach(part)
# Send email
try:
with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
server.starttls() # Secure the connection
server.login(SMTP_USER, smtp_get_secret()) # Fetch SMTP password from file
server.sendmail(SMTP_USER, msg['To'], msg.as_string())
#print(f"Email sent to {msg['To']}")
except Exception as e:
print(f"Error sending email: {e}")
# Notify unpaid donors
def notify_unpaid(record): def notify_unpaid(record):
# Check if notified content = get_file_content_all(os.path.join(WORKDIR, "transition", record)).split("\n")
content = get_file_content_all(WORKDIR+"/transition/"+record).split("\n")
if len(content) > 8 and "notified" in content[8]: if len(content) > 8 and "notified" in content[8]:
return return # Already notified
# Get infos # Parse donor information
name, surname, address, postal_code, city, email, amount, mode = \ name, surname, address, postal_code, city, email, amount, mode = content[:8]
get_file_content_all(WORKDIR+"/transition/"+record).split("\n")[:8]
BUF.append("* {} {}, {}".format(record, name, amount)) BUF.append(f"* {record} {name}, {amount}")
BUF.append(" NOTIFICATION DONATEUR") BUF.append(" NOTIFICATION DONATEUR")
BUF.append("") BUF.append("")
mailtext = get_file_content_all(MODALITY_MAIL) + "\n" mailtext = get_file_content_all(MODALITY_MAIL)
mailtext = mailtext.replace("NOM_DONNEUR", surname+" "+name) mailtext = mailtext.replace("NOM_DONNEUR", f"{surname} {name}")
mailtext = mailtext.replace("NUMERO_DON", str(record)) mailtext = mailtext.replace("NUMERO_DON", str(record))
mailtext = mailtext.replace("MONTANT_DON", amount) mailtext = mailtext.replace("MONTANT_DON", amount)
mailtext = mailtext.replace("MODE_DON", mode) mailtext = mailtext.replace("MODE_DON", mode)
mailheaders = get_file_content_all(MODALITY_MAIL_HEADERS) + "\n"
mailheaders = get_file_content_all(MODALITY_MAIL_HEADERS)
mailheaders = mailheaders.replace("COURRIEL_DONNEUR", email) mailheaders = mailheaders.replace("COURRIEL_DONNEUR", email)
# Notify # Notify
sendmail(mailheaders, mailtext) sendmail_with_attachment(mailheaders, mailtext, None, None)
# Indicate as notified # Mark as notified
with open(WORKDIR+"/transition/"+record, "a") as sfile: with open(os.path.join(WORKDIR, "transition", record), "a") as sfile:
sfile.write("notified") sfile.write("notified\n")
# Validate donation records
def validate(record): def validate(record):
request = f"SELECT * FROM acc_transactions tr INNER JOIN acc_transactions_lines l " \
# Get infos f"ON tr.id = l.id_transaction WHERE tr.notes LIKE '%{record}%' AND id_account = 469"
request = "SELECT * FROM acc_transactions tr " +\
"INNER JOIN acc_transactions_lines l " +\
" ON tr.id = l.id_transaction " +\
"WHERE tr.notes LIKE '%{}%' and id_account = 469".format(record)
# Note: su.id_service = 1 parceque la cotisation correspond au service 1
try: try:
answer = gestion_read(request)["results"][-1] answer = gestion_read(request)["results"][-1]
except: except IndexError:
return False return False # If no result, donation not validated
# Parse donor information
name, surname, address, postal_code, city, email, amount, mode = \ name, surname, address, postal_code, city, email, amount, mode = \
get_file_content_all(WORKDIR+"/transition/"+record).split("\n")[:8] get_file_content_all(os.path.join(WORKDIR, "transition", record)).split("\n")[:8]
date = datetime.datetime.strptime( date = datetime.datetime.strptime(answer["date"], '%Y-%m-%d').strftime("%d/%m/%Y")
answer["date"],'%Y-%m-%d').strftime("%d/%m/%Y") filename = f"{name}_reçu_{record}_{date.replace('/', '.')}"
filename = "{}_reçu_{}_{}".format( BUF.append(f"* {record} {name}, {amount}")
name,
record,
date.replace("/", "."))
BUF.append("* {} {}, {}".format(record, name, "{},{}".format(
str(answer["credit"])[:-2],
str(answer["credit"])[-2:])))
BUF.append(" VALIDATION DON") BUF.append(" VALIDATION DON")
BUF.append("") BUF.append("")
@ -150,95 +164,68 @@ def validate(record):
latexfile = latexfile.replace("NUMERO-DON", record) latexfile = latexfile.replace("NUMERO-DON", record)
latexfile = latexfile.replace("ANNEE-CIVILE", answer["date"][:4]) latexfile = latexfile.replace("ANNEE-CIVILE", answer["date"][:4])
latexfile = latexfile.replace("NOM-DONATEUR", name) latexfile = latexfile.replace("NOM-DONATEUR", name)
latexfile = latexfile.replace("STATUT-DONATEUR", "Personne physique") # XXX latexfile = latexfile.replace("STATUT-DONATEUR", "Personne physique")
latexfile = latexfile.replace("ADRESSE-DONATEUR", "{}, {} {}".format( latexfile = latexfile.replace("ADRESSE-DONATEUR", f"{address}, {postal_code} {city}")
address, latexfile = latexfile.replace("SOMME", f"{str(answer['credit'])[:-2]},{str(answer['credit'])[-2:]}")
postal_code,
city))
latexfile = latexfile.replace("SOMME", "{},{}".format(
str(answer["credit"])[:-2],
str(answer["credit"])[-2:]))
latexfile = latexfile.replace("DATE-VERSEMENT", date) latexfile = latexfile.replace("DATE-VERSEMENT", date)
latexfile = latexfile.replace("MODE-VERSEMENT", answer["reference"]) latexfile = latexfile.replace("MODE-VERSEMENT", answer["reference"])
latexfile = latexfile.replace("FORME-DON", "Déclaration de don manuel") # XXX latexfile = latexfile.replace("FORME-DON", "Déclaration de don manuel")
latexfile = latexfile.replace("NATURE-DON", "Numéraire") # XXX latexfile = latexfile.replace("NATURE-DON", "Numéraire")
try: # Save the LaTeX file and generate the PDF
os.remove(WORKDIR+"/validé/"+filename+".tex") tex_file_path = os.path.join(WORKDIR, "validé", f"{filename}.tex")
except: set_file_content(tex_file_path, latexfile)
pass os.system(f"cd {WORKDIR}/validé/ && pdflatex {filename}.tex")
set_file_content(WORKDIR+"/validé/"+filename+".tex", latexfile)
os.system("cd {} && pdflatex {}".format(WORKDIR+"/validé/", filename+".tex"))
# Preparing mail # Prepare and send email with attachment
mailheaders = get_file_content_all(RECEPT_MAIL_HEADERS).replace( mailheaders = get_file_content_all(RECEPT_MAIL_HEADERS).replace("COURRIEL-DON", email)
"COURRIEL-DON", mailtext = get_file_content_all(RECEPT_MAIL).replace("DATE-DON", date)
email) + "\n" mailtattach = get_file_content_all(RECEPT_MAIL_ATTACHMENT).replace("DATE-DON", f"{record}_{date.replace('/', '.')}")
mailtext = get_file_content_all(RECEPT_MAIL).replace("DATE-DON", date) + "\n" pdf_path = os.path.join(WORKDIR, "validé", f"{filename}.pdf")
mailtattach = get_file_content_all(RECEPT_MAIL_ATTACHMENT).replace(
"DATE-DON",
record + "_" + date.replace("/", ".")) + "\n"
# Opening PDF file as binary sendmail_with_attachment(mailheaders, mailtext, pdf_path, f"{filename}.pdf")
attachment = open(WORKDIR+"/validé/"+filename+".pdf", "rb")
data = attachment.read()
attachment.close()
ending = "--------------3yxkFgv0AINs5nd0i6BJrWaV--"
sendmail_with_attachment(mailheaders, mailtext, mailtattach, data, ending)
# The end
os.remove(WORKDIR+"/transition/"+record)
# Clean up
os.remove(os.path.join(WORKDIR, "transition", record))
return True return True
# Process new donation records
def check_record(intent): def check_record(intent):
numero, content = get_file_content_all(intent).split("|") numero, content = get_file_content_all(intent).split("|")
name, surname, address, postal_code, city, email, amount, mode = \ name, surname, address, postal_code, city, email, amount, mode = content.split(";")[:8]
content.split(";")[:8]
BUF.append("* {} {}, {}".format(numero, name+" "+surname, amount)) BUF.append(f"* {numero} {name} {surname}, {amount}")
BUF.append(" NOUVEAU DON") BUF.append(" NOUVEAU DON")
BUF.append("") BUF.append("")
lines = [ "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n".format( lines = [f"{name}\n{surname}\n{address}\n{postal_code}\n{city}\n{email}\n{amount}\n{mode}\n"]
name, surname, address, postal_code, city, email, amount, mode set_file_content(os.path.join(WORKDIR, "transition", numero), lines)
) ] os.remove(intent) # Clean up processed intent file
set_file_content(WORKDIR+"/transition/"+numero, lines)
if numero in os.listdir(WORKDIR+"/transition/"):
os.remove(intent)
# Main donation validation workflow
def validate_donors(): def validate_donors():
# Process new donation intents
for new_intent in os.listdir(os.path.join(WORKDIR, "nouveau")):
check_record(os.path.join(WORKDIR, "nouveau", new_intent))
# Get new # Validate existing records or notify unpaid
for new_intent in os.listdir(WORKDIR+"/nouveau"): for record in os.listdir(os.path.join(WORKDIR, "transition")):
check_record(WORKDIR+"/nouveau/"+new_intent)
# Validate record
for record in os.listdir(WORKDIR+"/transition"):
if not validate(record): if not validate(record):
notify_unpaid(record) notify_unpaid(record)
# Main function
def main(): def main():
setup_workdir() setup_workdir()
#GET DATA()
validate_donors() validate_donors()
# End of work # Send summary email
# Launch summary mail
mailheaders = get_file_content_all(SUMMARY_MAIL) + "\n" mailheaders = get_file_content_all(SUMMARY_MAIL) + "\n"
mailtext = "" mailtext = ""
is_sendable = False; if BUF:
for line in BUF: mailtext = "\n".join(BUF)
mailtext += line + "\n" sendmail_with_attachment(mailheaders, mailtext, None, None)
is_sendable = True;
print(line)
if is_sendable:
sendmail(mailheaders, mailtext)
## Bootstrap
if __name__ == '__main__': if __name__ == '__main__':
main() main()