Some checks failed
Build and publish / build (push) Failing after 19s
Source moved verbatim from mp/mail/ on 2026-04-29; mp was the first concrete adapter consuming the libreshop toolkit. Builds and publishes git.librete.ch/libreshop/mail on every main / v* push via the standard .gitea/workflows/build.yml shared across libreshop components.
240 lines
7.2 KiB
Python
240 lines
7.2 KiB
Python
from flask import Flask, request, jsonify
|
|
from prometheus_flask_exporter import PrometheusMetrics
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.application import MIMEApplication
|
|
import os
|
|
import time
|
|
import html
|
|
import re
|
|
import logging
|
|
|
|
|
|
class FilterRemoveDate(logging.Filter):
|
|
# '192.168.0.102 - - [30/Jun/2024 01:14:03] "%s" %s %s' -> '192.168.0.102 - "%s" %s %s'
|
|
pattern: re.Pattern = re.compile(r' - \[.+?]')
|
|
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
record.msg = self.pattern.sub('', record.msg)
|
|
return True
|
|
|
|
class FilterReplaceWerkzeug(logging.Filter):
|
|
# 'werkzeug:' -> 'app:flask:'
|
|
pattern: re.Pattern = re.compile(r'werkzeug:')
|
|
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
record.msg = self.pattern.sub('app:flask:', record.msg)
|
|
return True
|
|
|
|
class FilterReplaceLowercaseI(logging.Filter):
|
|
# 'I:' -> 'i:'
|
|
pattern: re.Pattern = re.compile(r'I:')
|
|
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
record.msg = self.pattern.sub('i:', record.msg)
|
|
return True
|
|
|
|
# Setup logger
|
|
logging.basicConfig(
|
|
level=logging.DEBUG,
|
|
format='%(asctime)s.%(msecs)03dZ %(name)s:%(levelname).1s: %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
|
|
logger = logging.getLogger('app')
|
|
logger_werkzeug = logging.getLogger('werkzeug')
|
|
logger_werkzeug.addFilter(FilterRemoveDate())
|
|
logger_werkzeug.addFilter(FilterReplaceWerkzeug())
|
|
logger_werkzeug.addFilter(FilterReplaceLowercaseI())
|
|
|
|
app = Flask(__name__, static_folder=None)
|
|
|
|
# Wrap the Flask app with PrometheusMetrics
|
|
metrics = PrometheusMetrics(app, defaults_prefix="mail")
|
|
|
|
email_sent_counter = metrics.counter(
|
|
'mail_text_sent_total',
|
|
'Total number of text messages sent',
|
|
labels={'endpoint': lambda: request.endpoint}
|
|
)
|
|
|
|
pdf_sent_counter = metrics.counter(
|
|
'mail_pdf_sent_total',
|
|
'Total number of pdfs sent',
|
|
labels={'endpoint': lambda: request.endpoint}
|
|
)
|
|
|
|
|
|
@app.route('/v1')
|
|
@metrics.do_not_track()
|
|
def info():
|
|
today = time.strftime("%Y-%m-%d")
|
|
url_map = app.url_map
|
|
return f"<h1>mail v1.0.0 - {today}</h1><pre>" + html.escape(str(url_map), False) + "</pre>"
|
|
|
|
|
|
@app.route('/v1/send/message', methods=['POST'])
|
|
@email_sent_counter
|
|
def send_message():
|
|
logger = logging.getLogger('app:send-message')
|
|
data = request.json
|
|
subject = data.get('subject')
|
|
text_content = data.get('message')
|
|
to_email = data.get('to_email')
|
|
html_content = data.get('html')
|
|
|
|
logger.info(f"Sending text email to {to_email}")
|
|
logger.debug(f"Subject: {subject}")
|
|
|
|
# SMTP Configuration
|
|
smtp_server = os.environ.get('SMTP_RELAY_HOST')
|
|
smtp_port = int(os.environ.get('SMTP_RELAY_PORT'))
|
|
smtp_username = os.environ.get('SMTP_RELAY_USERNAME')
|
|
smtp_password = os.environ.get('SMTP_RELAY_PASSWORD')
|
|
|
|
try:
|
|
# Create a message
|
|
msg = MIMEMultipart('mixed')
|
|
msg['From'] = smtp_username
|
|
msg['To'] = to_email
|
|
msg['Subject'] = subject
|
|
|
|
# Create the multipart/alternative part
|
|
msgAlternative = MIMEMultipart('alternative')
|
|
msg.attach(msgAlternative)
|
|
|
|
# Attach plain text message first (lower priority)
|
|
msgAlternative.attach(MIMEText(text_content, 'plain', 'utf-8'))
|
|
|
|
# Attach HTML version if provided (higher priority)
|
|
if html_content:
|
|
logger.debug("HTML content included in email")
|
|
msgAlternative.attach(MIMEText(html_content, 'html', 'utf-8'))
|
|
|
|
# Send email
|
|
logger.debug(f"Connecting to SMTP server {smtp_server}:{smtp_port}")
|
|
with smtplib.SMTP_SSL(smtp_server, smtp_port) as smtp:
|
|
smtp.login(smtp_username, smtp_password)
|
|
smtp.sendmail(smtp_username, to_email, msg.as_string())
|
|
smtp.quit()
|
|
|
|
logger.info("Email sent successfully")
|
|
return jsonify({"message": "Email sent successfully"}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error sending email: {str(e)}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@app.route('/v1/send/pdf', methods=['POST'])
|
|
@pdf_sent_counter
|
|
def send_pdf():
|
|
logger = logging.getLogger('app:send-pdf')
|
|
data = request.form
|
|
subject = data.get('subject')
|
|
text_content = data.get('message')
|
|
html_content = data.get('html')
|
|
to_email = data.get('to_email')
|
|
pdf_blob = request.files.get('pdf_blob')
|
|
|
|
logger.info(f"Sending PDF email to {to_email}")
|
|
logger.debug(f"Subject: {subject}")
|
|
|
|
# SMTP Configuration
|
|
smtp_server = os.environ.get('SMTP_RELAY_HOST')
|
|
smtp_port = int(os.environ.get('SMTP_RELAY_PORT'))
|
|
smtp_username = os.environ.get('SMTP_RELAY_USERNAME')
|
|
smtp_password = os.environ.get('SMTP_RELAY_PASSWORD')
|
|
|
|
try:
|
|
# Create a message with mixed content (attachments + text)
|
|
msg = MIMEMultipart('mixed')
|
|
msg['From'] = smtp_username
|
|
msg['To'] = to_email
|
|
msg['Subject'] = subject
|
|
|
|
# Create the multipart/alternative part for text and HTML
|
|
msgAlternative = MIMEMultipart('alternative')
|
|
msg.attach(msgAlternative)
|
|
|
|
# Attach plain text message first (lower priority)
|
|
msgAlternative.attach(MIMEText(text_content, 'plain', 'utf-8'))
|
|
|
|
# Attach HTML version if provided (higher priority)
|
|
if html_content:
|
|
logger.debug("HTML content included in email")
|
|
msgAlternative.attach(MIMEText(html_content, 'html', 'utf-8'))
|
|
|
|
# Attach PDF if provided
|
|
if pdf_blob:
|
|
logger.debug("Attaching PDF to email")
|
|
pdf_attachment = MIMEApplication(pdf_blob.read(), _subtype="pdf")
|
|
pdf_attachment.add_header('Content-Disposition', 'attachment', filename='document.pdf')
|
|
msg.attach(pdf_attachment)
|
|
else:
|
|
logger.warning("No PDF attachment provided")
|
|
|
|
# Send email
|
|
logger.debug(f"Connecting to SMTP server {smtp_server}:{smtp_port}")
|
|
with smtplib.SMTP(smtp_server, smtp_port) as smtp:
|
|
logger.debug("SMTP connection established")
|
|
try:
|
|
smtp.starttls()
|
|
logger.debug("STARTTLS successful")
|
|
except Exception as e:
|
|
logger.error(f"STARTTLS failed: {str(e)}")
|
|
|
|
smtp.login(smtp_username, smtp_password)
|
|
smtp.sendmail(smtp_username, to_email, msg.as_string())
|
|
logger.debug("Email sent via SMTP")
|
|
|
|
logger.info("PDF email sent successfully")
|
|
return jsonify({"message": "Email sent successfully"}), 200
|
|
except Exception as e:
|
|
logger.error(f"Error sending PDF email: {str(e)}")
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@app.route('/health', methods=['GET'])
|
|
def health_check():
|
|
logger = logging.getLogger('app:health-check')
|
|
health_status = {'status': 'ok'}
|
|
|
|
# Check SMTP configuration
|
|
smtp_server = os.environ.get('SMTP_RELAY_HOST')
|
|
smtp_port = os.environ.get('SMTP_RELAY_PORT')
|
|
smtp_username = os.environ.get('SMTP_RELAY_USERNAME')
|
|
|
|
if not all([smtp_server, smtp_port, smtp_username]):
|
|
logger.error("SMTP configuration incomplete")
|
|
health_status['smtp_config'] = False
|
|
health_status['status'] = 'error'
|
|
else:
|
|
health_status['smtp_config'] = True
|
|
|
|
# Try to connect to SMTP server
|
|
try:
|
|
smtp_port_int = int(smtp_port)
|
|
use_ssl = smtp_port_int == 465
|
|
|
|
if use_ssl:
|
|
with smtplib.SMTP_SSL(smtp_server, smtp_port_int, timeout=5) as smtp:
|
|
smtp.ehlo()
|
|
health_status['smtp_responding'] = True
|
|
else:
|
|
with smtplib.SMTP(smtp_server, smtp_port_int, timeout=5) as smtp:
|
|
smtp.ehlo()
|
|
health_status['smtp_responding'] = True
|
|
|
|
except Exception as e:
|
|
logger.error(f"SMTP connection test failed: {str(e)}")
|
|
health_status['smtp_responding'] = False
|
|
health_status['status'] = 'error'
|
|
|
|
logger.debug(f"Health check performed: {health_status}")
|
|
return jsonify(health_status)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=2222, debug=False)
|