Codice iniziale
This commit is contained in:
+501
@@ -0,0 +1,501 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Genera il PDF di documentazione per connettore_metadataexporter."""
|
||||
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import cm
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
|
||||
HRFlowable, KeepTogether,
|
||||
)
|
||||
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
|
||||
from reportlab.platypus.tableofcontents import TableOfContents
|
||||
|
||||
OUTPUT = "connettore_metadataexporter.pdf"
|
||||
|
||||
W, H = A4
|
||||
MARGIN = 2 * cm
|
||||
|
||||
# ─── Palette ──────────────────────────────────────────────────────────────────
|
||||
BLUE = colors.HexColor("#1a4a7a")
|
||||
LBLUE = colors.HexColor("#2d7dd2")
|
||||
LGRAY = colors.HexColor("#f4f6f8")
|
||||
DGRAY = colors.HexColor("#4a4a4a")
|
||||
GREEN = colors.HexColor("#2e7d32")
|
||||
ORANGE = colors.HexColor("#e65100")
|
||||
WHITE = colors.white
|
||||
BLACK = colors.black
|
||||
|
||||
# ─── Stili ────────────────────────────────────────────────────────────────────
|
||||
base = getSampleStyleSheet()
|
||||
|
||||
def style(name, **kw):
|
||||
s = ParagraphStyle(name, **kw)
|
||||
return s
|
||||
|
||||
S = {
|
||||
"title": style("title",
|
||||
fontName="Helvetica-Bold", fontSize=26, textColor=WHITE,
|
||||
alignment=TA_CENTER, spaceAfter=6),
|
||||
"subtitle": style("subtitle",
|
||||
fontName="Helvetica", fontSize=13, textColor=colors.HexColor("#cce0ff"),
|
||||
alignment=TA_CENTER, spaceAfter=4),
|
||||
"version": style("version",
|
||||
fontName="Helvetica", fontSize=10, textColor=colors.HexColor("#aac8ff"),
|
||||
alignment=TA_CENTER),
|
||||
"h1": style("h1",
|
||||
fontName="Helvetica-Bold", fontSize=15, textColor=BLUE,
|
||||
spaceBefore=18, spaceAfter=6, borderPadding=(0, 0, 4, 0)),
|
||||
"h2": style("h2",
|
||||
fontName="Helvetica-Bold", fontSize=11, textColor=LBLUE,
|
||||
spaceBefore=12, spaceAfter=4),
|
||||
"body": style("body",
|
||||
fontName="Helvetica", fontSize=9.5, textColor=DGRAY,
|
||||
leading=15, spaceAfter=6, alignment=TA_JUSTIFY),
|
||||
"bullet": style("bullet",
|
||||
fontName="Helvetica", fontSize=9.5, textColor=DGRAY,
|
||||
leading=14, leftIndent=14, spaceAfter=3,
|
||||
bulletIndent=4, bulletFontName="Helvetica"),
|
||||
"code": style("code",
|
||||
fontName="Courier", fontSize=8.5, textColor=colors.HexColor("#1a1a1a"),
|
||||
backColor=LGRAY, leading=13, leftIndent=10, rightIndent=10,
|
||||
borderPadding=6, spaceAfter=6, spaceBefore=4),
|
||||
"note": style("note",
|
||||
fontName="Helvetica-Oblique", fontSize=9, textColor=colors.HexColor("#555"),
|
||||
leading=13, spaceAfter=4),
|
||||
"footer": style("footer",
|
||||
fontName="Helvetica", fontSize=8, textColor=colors.HexColor("#888"),
|
||||
alignment=TA_CENTER),
|
||||
}
|
||||
|
||||
def h1(text):
|
||||
return [
|
||||
HRFlowable(width="100%", thickness=1.5, color=BLUE, spaceAfter=3),
|
||||
Paragraph(text, S["h1"]),
|
||||
]
|
||||
|
||||
def h2(text):
|
||||
return [Paragraph(text, S["h2"])]
|
||||
|
||||
def p(text):
|
||||
return Paragraph(text, S["body"])
|
||||
|
||||
def code(text):
|
||||
escaped = text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return Paragraph(escaped, S["code"])
|
||||
|
||||
def bullets(items):
|
||||
return [Paragraph(f"• {i}", S["bullet"]) for i in items]
|
||||
|
||||
def note(text):
|
||||
return Paragraph(f"<i>ℹ {text}</i>", S["note"])
|
||||
|
||||
def spacer(h=0.3):
|
||||
return Spacer(1, h * cm)
|
||||
|
||||
def table(data, col_widths, header=True):
|
||||
t = Table(data, colWidths=col_widths)
|
||||
style_cmds = [
|
||||
("FONTNAME", (0, 0), (-1, -1), "Helvetica"),
|
||||
("FONTSIZE", (0, 0), (-1, -1), 9),
|
||||
("ROWBACKGROUNDS", (0, 1), (-1, -1), [WHITE, LGRAY]),
|
||||
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#cccccc")),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 5),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 7),
|
||||
]
|
||||
if header:
|
||||
style_cmds += [
|
||||
("BACKGROUND", (0, 0), (-1, 0), BLUE),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0, 0), (-1, 0), 9),
|
||||
]
|
||||
t.setStyle(TableStyle(style_cmds))
|
||||
return t
|
||||
|
||||
|
||||
# ─── Callback numerazione pagine ──────────────────────────────────────────────
|
||||
class NumberedCanvas:
|
||||
"""Aggiunge numero pagina in fondo, sovrascrivendo il canvas di reportlab."""
|
||||
pass # gestito con onLaterPages/onFirstPage
|
||||
|
||||
|
||||
def on_page(canvas, doc):
|
||||
canvas.saveState()
|
||||
canvas.setFont("Helvetica", 8)
|
||||
canvas.setFillColor(colors.HexColor("#888888"))
|
||||
canvas.drawCentredString(W / 2, 1.2 * cm, f"Pagina {doc.page}")
|
||||
canvas.restoreState()
|
||||
|
||||
|
||||
def on_first_page(canvas, doc):
|
||||
# Cover page: sfondo blu
|
||||
canvas.saveState()
|
||||
canvas.setFillColor(BLUE)
|
||||
canvas.rect(0, 0, W, H, fill=1, stroke=0)
|
||||
# Banda inferiore
|
||||
canvas.setFillColor(LBLUE)
|
||||
canvas.rect(0, 0, W, 3.5 * cm, fill=1, stroke=0)
|
||||
canvas.setFont("Helvetica", 8)
|
||||
canvas.setFillColor(colors.HexColor("#cce0ff"))
|
||||
canvas.drawCentredString(W / 2, 1.4 * cm, "connettore_metadataexporter • Documentazione tecnica")
|
||||
canvas.restoreState()
|
||||
|
||||
|
||||
# ─── Costruzione contenuto ────────────────────────────────────────────────────
|
||||
|
||||
def build_story():
|
||||
story = []
|
||||
|
||||
# ── Cover ──────────────────────────────────────────────────────────────
|
||||
story.append(Spacer(1, 5 * cm))
|
||||
story.append(Paragraph("connettore_metadataexporter", S["title"]))
|
||||
story.append(Spacer(1, 0.4 * cm))
|
||||
story.append(Paragraph("Guida all'installazione e all'utilizzo", S["subtitle"]))
|
||||
story.append(Spacer(1, 0.3 * cm))
|
||||
story.append(Paragraph("v1.0 • Maggio 2026", S["version"]))
|
||||
story.append(Spacer(1, 8 * cm))
|
||||
story.append(Paragraph(
|
||||
"Compatibile con <b>rqwatch</b> (bilias/rqwatch)",
|
||||
style("compat", fontName="Helvetica", fontSize=10,
|
||||
textColor=colors.HexColor("#aac8ff"), alignment=TA_CENTER)))
|
||||
|
||||
# forza nuova pagina
|
||||
from reportlab.platypus import PageBreak
|
||||
story.append(PageBreak())
|
||||
|
||||
# ── 1. Panoramica ──────────────────────────────────────────────────────
|
||||
story += h1("1. Panoramica")
|
||||
story.append(p(
|
||||
"Il connettore è un servizio HTTP scritto in Python (FastAPI) che funge da "
|
||||
"backend per il modulo <b>metadata_exporter</b> di Rspamd. Ogni messaggio "
|
||||
"analizzato da Rspamd viene inviato via POST al connettore, che provvede a:"
|
||||
))
|
||||
story += bullets([
|
||||
"Salvare il file <b>.eml</b> grezzo nella directory di quarantena sul filesystem.",
|
||||
"Inserire i metadati del messaggio nel database MySQL/MariaDB compatibile con <b>rqwatch</b>.",
|
||||
"Permettere a rqwatch di gestire, visualizzare e rilasciare i messaggi in quarantena.",
|
||||
])
|
||||
story.append(spacer())
|
||||
|
||||
# Schema flusso
|
||||
story += h2("Flusso dei dati")
|
||||
flow_data = [
|
||||
["Componente", "Ruolo"],
|
||||
["Rspamd", "Analizza le email e chiama il connettore via HTTP POST"],
|
||||
["connettore_metadataexporter", "Riceve il POST, salva .eml su disco, scrive metadati nel DB"],
|
||||
["MySQL / MariaDB", "Database condiviso con rqwatch (tabelle mail_logs, mail_log_recipients)"],
|
||||
["rqwatch", "Interfaccia web per visualizzare, cercare e rilasciare i messaggi"],
|
||||
["QUARANTINE_DIR", "Directory filesystem dove risiedono i file .eml"],
|
||||
]
|
||||
story.append(table(flow_data, [4.5*cm, 11.5*cm]))
|
||||
story.append(spacer())
|
||||
|
||||
# ── 2. Requisiti ───────────────────────────────────────────────────────
|
||||
story += h1("2. Requisiti")
|
||||
story += bullets([
|
||||
"Python 3.11 o superiore",
|
||||
"MySQL 8+ oppure MariaDB 10.6+ (schema rqwatch già inizializzato)",
|
||||
"Rspamd con il modulo metadata_exporter attivo",
|
||||
"Directory di quarantena scrivibile dal processo Python",
|
||||
"rqwatch installato e configurato sullo stesso database",
|
||||
])
|
||||
story.append(spacer())
|
||||
|
||||
# ── 3. Installazione ───────────────────────────────────────────────────
|
||||
story += h1("3. Installazione")
|
||||
|
||||
story += h2("3.1 Dipendenze Python")
|
||||
story.append(p("Installare le dipendenze dalla directory del progetto:"))
|
||||
story.append(code("pip install -r requirements.txt"))
|
||||
story.append(p("Il file <b>requirements.txt</b> contiene:"))
|
||||
story += bullets([
|
||||
"fastapi ≥ 0.115",
|
||||
"uvicorn[standard] ≥ 0.30",
|
||||
"python-multipart ≥ 0.0.9",
|
||||
"pymysql ≥ 1.1",
|
||||
"python-dotenv ≥ 1.0",
|
||||
])
|
||||
story.append(spacer(0.2))
|
||||
|
||||
story += h2("3.2 File di configurazione")
|
||||
story.append(p(
|
||||
"Copiare il template e personalizzare i valori:"
|
||||
))
|
||||
story.append(code("cp .env.example .env\nnano .env"))
|
||||
story.append(spacer(0.2))
|
||||
|
||||
story += h2("3.3 Avvio del servizio")
|
||||
story.append(p("Avvio diretto (sviluppo/test):"))
|
||||
story.append(code("python main.py"))
|
||||
story.append(p("Avvio con uvicorn (produzione):"))
|
||||
story.append(code(
|
||||
"uvicorn main:app --host 127.0.0.1 --port 8080 --workers 2"
|
||||
))
|
||||
story.append(note(
|
||||
"In produzione si consiglia di usare un process manager come systemd "
|
||||
"o supervisord per garantire il riavvio automatico."
|
||||
))
|
||||
story.append(spacer())
|
||||
|
||||
# ── 4. Configurazione (.env) ───────────────────────────────────────────
|
||||
story += h1("4. Variabili di configurazione (.env)")
|
||||
|
||||
story += h2("4.1 Server")
|
||||
env_server = [
|
||||
["Variabile", "Default", "Descrizione"],
|
||||
["LISTEN_HOST", "127.0.0.1", "Indirizzo IP su cui ascolta il connettore"],
|
||||
["LISTEN_PORT", "8080", "Porta TCP"],
|
||||
["MY_API_SERVER_ALIAS", "mx1", "Alias del server (deve corrispondere a ?server= nella URL di Rspamd)"],
|
||||
]
|
||||
story.append(table(env_server, [5*cm, 3*cm, 8*cm]))
|
||||
story.append(spacer(0.3))
|
||||
|
||||
story += h2("4.2 Autenticazione API")
|
||||
env_auth = [
|
||||
["Variabile", "Default", "Descrizione"],
|
||||
["RSPAMD_API_USER", "rspamd", "Username HTTP Basic Auth (deve corrispondere a metadata_exporter.conf)"],
|
||||
["RSPAMD_API_PASS", "—", "Password HTTP Basic Auth"],
|
||||
["RSPAMD_API_ACL", "127.0.0.1", "IP autorizzati a chiamare l'API (separati da virgola)"],
|
||||
]
|
||||
story.append(table(env_auth, [5*cm, 3*cm, 8*cm]))
|
||||
story.append(spacer(0.3))
|
||||
|
||||
story += h2("4.3 Quarantena")
|
||||
env_quar = [
|
||||
["Variabile", "Default", "Descrizione"],
|
||||
["QUARANTINE_DIR", "/quarantine", "Directory radice dove vengono salvati i file .eml"],
|
||||
["STORE_NO_ACTION", "false", "Salva su disco anche i messaggi puliti"],
|
||||
["STORE_ADD_HEADER", "true", "Salva messaggi con azione add header"],
|
||||
["STORE_REWRITE_SUBJECT", "true", "Salva messaggi con azione rewrite subject"],
|
||||
["STORE_GREYLIST", "false", "Salva messaggi in greylisting"],
|
||||
["STORE_DISCARD", "true", "Salva messaggi scartati (discard)"],
|
||||
["STORE_REJECT", "true", "Salva messaggi rifiutati (reject)"],
|
||||
]
|
||||
story.append(table(env_quar, [5.5*cm, 2.5*cm, 8*cm]))
|
||||
story.append(spacer(0.3))
|
||||
|
||||
story += h2("4.4 Database")
|
||||
env_db = [
|
||||
["Variabile", "Default", "Descrizione"],
|
||||
["DB_HOST", "127.0.0.1", "Host MySQL/MariaDB"],
|
||||
["DB_PORT", "3306", "Porta MySQL/MariaDB"],
|
||||
["DB_NAME", "rqwatch", "Nome del database"],
|
||||
["DB_USER", "rqwatch", "Utente database"],
|
||||
["DB_PASS", "—", "Password database"],
|
||||
["MAILLOGS_TABLE", "mail_logs", "Tabella principale dei log"],
|
||||
["MAIL_RECIPIENTS_TABLE", "mail_log_recipients", "Tabella destinatari"],
|
||||
]
|
||||
story.append(table(env_db, [5.5*cm, 2.5*cm, 8*cm]))
|
||||
story.append(spacer())
|
||||
|
||||
# ── 5. Configurazione Rspamd ───────────────────────────────────────────
|
||||
story += h1("5. Configurazione Rspamd")
|
||||
story.append(p(
|
||||
"Copiare (o creare) il file <b>/etc/rspamd/local.d/metadata_exporter.conf</b> "
|
||||
"con il contenuto del file <b>rspamd_metadata_exporter.conf</b> incluso nel progetto, "
|
||||
"adattando URL, user e password:"
|
||||
))
|
||||
story.append(code(
|
||||
"rules {\n"
|
||||
" CONNETTORE {\n"
|
||||
" backend = \"http\";\n"
|
||||
" url = \"http://127.0.0.1:8080/api/metadata_importer_multipart?server=mx1\";\n"
|
||||
" user = \"rspamd\"; # = RSPAMD_API_USER in .env\n"
|
||||
" password = \"la-tua-password\"; # = RSPAMD_API_PASS in .env\n"
|
||||
" selector = \"default\"; # invia tutti i messaggi\n"
|
||||
" formatter = \"multipart\"; # formato raccomandato\n"
|
||||
" timeout = 5;\n"
|
||||
" }\n"
|
||||
"}"
|
||||
))
|
||||
story.append(p("Dopo aver salvato il file, ricaricare la configurazione di Rspamd:"))
|
||||
story.append(code("rspamc reload\n# oppure\nsystemctl reload rspamd"))
|
||||
story.append(spacer())
|
||||
|
||||
# ── 6. Endpoint HTTP ───────────────────────────────────────────────────
|
||||
story += h1("6. Endpoint HTTP")
|
||||
|
||||
ep_data = [
|
||||
["Endpoint", "Formatter Rspamd", "Content-Type", "Note"],
|
||||
[
|
||||
"POST /api/metadata_importer_multipart",
|
||||
"multipart",
|
||||
"multipart/form-data",
|
||||
"Raccomandato. Invia JSON metadata + file .eml come form-data."
|
||||
],
|
||||
[
|
||||
"POST /api/metadata_importer",
|
||||
"default + meta_headers",
|
||||
"message/rfc822",
|
||||
"Email grezza nel body. Metadata negli header X-Rspamd-*. "
|
||||
"meta_headers è deprecato in Rspamd ≥ 3.14.2."
|
||||
],
|
||||
]
|
||||
story.append(table(ep_data, [5.5*cm, 2.8*cm, 3.2*cm, 5.5*cm]))
|
||||
story.append(spacer(0.3))
|
||||
|
||||
story += h2("Autenticazione")
|
||||
story.append(p(
|
||||
"Tutti gli endpoint richiedono <b>HTTP Basic Authentication</b> e verificano "
|
||||
"che l'IP del chiamante sia presente in <b>RSPAMD_API_ACL</b>. "
|
||||
"Se l'IP non è autorizzato viene restituito <b>HTTP 403</b>; "
|
||||
"se le credenziali sono errate viene restituito <b>HTTP 401</b>."
|
||||
))
|
||||
|
||||
story += h2("Risposta")
|
||||
resp_data = [
|
||||
["Codice", "Corpo", "Significato"],
|
||||
["200 OK", "Message saved", "Messaggio ricevuto, salvato e inserito nel DB"],
|
||||
["400 Bad Request", "Messaggio di errore", "Payload mancante o non valido"],
|
||||
["401 Unauthorized", "Unauthorized", "Credenziali errate"],
|
||||
["403 Forbidden", "Forbidden", "IP non autorizzato in RSPAMD_API_ACL"],
|
||||
["500 Internal Server Error", "Errore database", "Errore di scrittura su DB o filesystem"],
|
||||
]
|
||||
story.append(table(resp_data, [3*cm, 3.5*cm, 9.5*cm]))
|
||||
story.append(spacer())
|
||||
|
||||
# ── 7. Ciclo di vita dei messaggi ──────────────────────────────────────
|
||||
story += h1("7. Ciclo di vita dei messaggi")
|
||||
|
||||
lifecycle = [
|
||||
["Evento", "Filesystem", "Database"],
|
||||
[
|
||||
"Ricezione da Rspamd",
|
||||
"File .eml salvato in\nQUARANTINE_DIR/YYYY-MM-DD/<QID>/mail.eml\n(solo se l'azione è in STORE_*)",
|
||||
"Riga inserita in mail_logs\nmail_stored = 1 (o 0 se non salvato)\nDestinatari in mail_log_recipients"
|
||||
],
|
||||
[
|
||||
"Rilascio da rqwatch",
|
||||
"Il file rimane su disco\n(non viene cancellato)",
|
||||
"released = 1\nrelease_date = now()"
|
||||
],
|
||||
[
|
||||
"Pulizia cron (rqwatch)",
|
||||
"Directory <QID>/ cancellata\ndopo QUARANTINE_DAYS giorni",
|
||||
"mail_stored = 0\nIl record rimane per lo storico"
|
||||
],
|
||||
]
|
||||
story.append(table(lifecycle, [4*cm, 6*cm, 6*cm]))
|
||||
story.append(spacer(0.3))
|
||||
story.append(note(
|
||||
"La pulizia automatica dei file è eseguita esclusivamente da rqwatch "
|
||||
"tramite il comando bin/cli.php cron:quarantine -d (tipicamente via cron). "
|
||||
"Il connettore non elimina mai file dal filesystem."
|
||||
))
|
||||
story.append(spacer())
|
||||
|
||||
# ── 8. Struttura del filesystem ────────────────────────────────────────
|
||||
story += h1("8. Struttura directory quarantena")
|
||||
story.append(code(
|
||||
"/quarantine/ ← QUARANTINE_DIR\n"
|
||||
" 2026-05-07/\n"
|
||||
" ABC123DEF/ ← QID del messaggio\n"
|
||||
" mail.eml ← email grezza (RFC 822)\n"
|
||||
" XYZ789GHI/\n"
|
||||
" mail.eml\n"
|
||||
" 2026-05-06/\n"
|
||||
" ..."
|
||||
))
|
||||
story.append(p(
|
||||
"Se il QID non è disponibile o non è alfanumerico, il file viene salvato "
|
||||
"sotto una directory <b>unknown/<uuid>/</b>."
|
||||
))
|
||||
story.append(spacer())
|
||||
|
||||
# ── 9. Schema database ─────────────────────────────────────────────────
|
||||
story += h1("9. Colonne principali della tabella mail_logs")
|
||||
db_cols = [
|
||||
["Colonna", "Tipo", "Descrizione"],
|
||||
["id", "INT AUTO_INCREMENT", "Chiave primaria"],
|
||||
["qid", "VARCHAR(30)", "Queue-ID dell'MTA"],
|
||||
["server", "VARCHAR(10)", "Alias del server mittente"],
|
||||
["subject", "VARCHAR(1024)", "Oggetto del messaggio (MIME)"],
|
||||
["score", "FLOAT(8,2)", "Punteggio spam di Rspamd"],
|
||||
["action", "CHAR(20)", "Azione: reject, discard, add header, ecc."],
|
||||
["symbols", "JSON", "Simboli Rspamd con score e opzioni"],
|
||||
["has_virus", "TINYINT(1)", "1 se rilevato da antivirus"],
|
||||
["fuzzy_hashes", "JSON", "Hash fuzzy Rspamd"],
|
||||
["ip", "VARCHAR(50)", "IP del mittente SMTP"],
|
||||
["mail_from", "VARCHAR(255)", "Envelope From (SMTP)"],
|
||||
["mime_from", "VARCHAR(255)", "Header From (MIME)"],
|
||||
["rcpt_to", "VARCHAR(1024)", "Destinatari (comma-separated)"],
|
||||
["mime_to", "VARCHAR(1024)", "Header To (MIME)"],
|
||||
["size", "BIGINT", "Dimensione messaggio in byte"],
|
||||
["mail_stored", "TINYINT(1)", "1 se il file .eml è sul filesystem"],
|
||||
["mail_location", "VARCHAR(255)", "Path assoluto del file .eml"],
|
||||
["headers", "LONGTEXT", "Header MIME grezzi del messaggio"],
|
||||
["message_id", "VARCHAR(1024)", "Message-ID"],
|
||||
["released", "TINYINT(1)", "1 se il messaggio è stato rilasciato"],
|
||||
["release_date", "DATETIME", "Data/ora del rilascio"],
|
||||
["notified", "TINYINT(1)", "1 se è stata inviata notifica al destinatario"],
|
||||
]
|
||||
story.append(table(db_cols, [4.5*cm, 3.5*cm, 8*cm]))
|
||||
story.append(spacer())
|
||||
|
||||
# ── 10. Risoluzione problemi ────────────────────────────────────────────
|
||||
story += h1("10. Risoluzione problemi")
|
||||
|
||||
prob_data = [
|
||||
["Sintomo", "Causa probabile", "Soluzione"],
|
||||
[
|
||||
"HTTP 403 da Rspamd",
|
||||
"IP Rspamd non in RSPAMD_API_ACL",
|
||||
"Aggiungere l'IP in .env e riavviare il connettore"
|
||||
],
|
||||
[
|
||||
"HTTP 401 da Rspamd",
|
||||
"User/password non corrispondenti",
|
||||
"Verificare RSPAMD_API_USER/PASS in .env e in metadata_exporter.conf"
|
||||
],
|
||||
[
|
||||
"Messaggio nel DB ma file assente",
|
||||
"Azione non in STORE_* oppure errore di scrittura",
|
||||
"Verificare le variabili STORE_* e i permessi di QUARANTINE_DIR"
|
||||
],
|
||||
[
|
||||
"Errore 500 dal connettore",
|
||||
"DB non raggiungibile o credenziali errate",
|
||||
"Verificare DB_HOST, DB_USER, DB_PASS e che il servizio MySQL sia attivo"
|
||||
],
|
||||
[
|
||||
"rqwatch non vede i messaggi",
|
||||
"DB diverso o tabelle errate",
|
||||
"Verificare che .env punti allo stesso DB usato da rqwatch"
|
||||
],
|
||||
]
|
||||
story.append(table(prob_data, [4*cm, 4.5*cm, 7.5*cm]))
|
||||
story.append(spacer())
|
||||
|
||||
story.append(HRFlowable(width="100%", thickness=0.5, color=colors.lightgrey))
|
||||
story.append(spacer(0.3))
|
||||
story.append(Paragraph(
|
||||
"connettore_metadataexporter • Compatibile con rqwatch (github.com/bilias/rqwatch) • Maggio 2026",
|
||||
S["footer"]
|
||||
))
|
||||
|
||||
return story
|
||||
|
||||
|
||||
# ─── Generazione PDF ──────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
doc = SimpleDocTemplate(
|
||||
OUTPUT,
|
||||
pagesize=A4,
|
||||
leftMargin=MARGIN, rightMargin=MARGIN,
|
||||
topMargin=MARGIN, bottomMargin=2 * cm,
|
||||
title="connettore_metadataexporter – Guida",
|
||||
author="connettore_metadataexporter",
|
||||
subject="Documentazione tecnica",
|
||||
)
|
||||
story = build_story()
|
||||
doc.build(story, onFirstPage=on_first_page, onLaterPages=on_page)
|
||||
print(f"PDF generato: {OUTPUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user