se responde al remitente con las plantillas de aceptado/rechazado
El sistema contesta a quien envió el correo con la plantilla que corresponda. Por defecto (NOTIFICACION_MODO=prueba) la respuesta va solo al equipo con el asunto "[PRUEBA → remitente]"; en modo real va al remitente con copia oculta al equipo. El correo técnico (tablas de SAP, JSON) pasa a ir solo al equipo. - respuesta_proveedor.py: contextos de las plantillas, envío y protecciones (remitente desconocido o sin dominio, propio buzón, no-reply, mensajes automáticos, respuestas a notificaciones, límite de 3 por remitente/24 h). - observaciones.py: motivos de rechazo con título, filas y cómo corregir (borrador de textos); también para correos sin comprobante válido. - Un error de SAP/HANA o un fallo inesperado no se le comunica al proveedor y ya no tumba el ciclo; "guía no encontrada" exige que HANA responda. - outlook_utilities: es_mensaje_automatico y copia oculta en send_email. - seguimiento: notificacion_destinatario (con migración) y contar_respuestas_correo. - config_validator anuncia el modo activo; documentación y pruebas. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
190fcc8f9e
commit
108d52da33
22 files changed
+1660
-146
No files matched your search
+29
-4
@@ -52,11 +52,30 @@ def get_access_token() -> str:
|
||||
correlation_id = result.get("correlation_id")
|
||||
raise Exception(f"Error al obtener token: {error} - {error_description} (ID: {correlation_id})")
|
||||
|
||||
# ============================================================================
|
||||
def es_mensaje_automatico(headers: list[dict] | None) -> bool:
|
||||
"""
|
||||
Indica si un mensaje fue generado automáticamente (respuesta automática,
|
||||
rebote, lista de distribución…) a partir de ``internetMessageHeaders``.
|
||||
|
||||
No se debe responder a estos mensajes: se generarían bucles de correos.
|
||||
"""
|
||||
h = {(x.get("name") or "").lower(): (x.get("value") or "").strip().lower() for x in headers or []}
|
||||
if h.get("auto-submitted", "no") not in ("", "no"):
|
||||
return True
|
||||
if "x-auto-response-suppress" in h or "x-autoreply" in h or "x-autorespond" in h:
|
||||
return True
|
||||
if h.get("precedence") in ("bulk", "junk", "list", "auto_reply"):
|
||||
return True
|
||||
return "report-type=" in h.get("content-type", "") # multipart/report: acuse o rebote de entrega
|
||||
|
||||
|
||||
# ============================================================================
|
||||
def get_emails(user_email: str, token: str, fecha_inicio: str, fecha_fin: str) -> list[dict]:
|
||||
"""
|
||||
Obtiene los correos de una cuenta específica en un rango de fechas.
|
||||
Retorna una lista de JSON con: id, fecha (ajustada -5h), sender, subject y has_attach.
|
||||
Retorna una lista de JSON con: id, fecha (ajustada -5h), sender, subject, has_attach
|
||||
y es_automatico (True si es una respuesta automática o un rebote).
|
||||
"""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
@@ -73,7 +92,7 @@ def get_emails(user_email: str, token: str, fecha_inicio: str, fecha_fin: str) -
|
||||
|
||||
params = {
|
||||
"$filter": filter_query,
|
||||
"$select": "id,receivedDateTime,hasAttachments,subject,from",
|
||||
"$select": "id,receivedDateTime,hasAttachments,subject,from,internetMessageHeaders",
|
||||
"$top": 50
|
||||
}
|
||||
|
||||
@@ -107,7 +126,8 @@ def get_emails(user_email: str, token: str, fecha_inicio: str, fecha_fin: str) -
|
||||
"fecha": dt_local.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"sender": sender,
|
||||
"subject": msg.get("subject", ""),
|
||||
"has_attach": msg.get("hasAttachments")
|
||||
"has_attach": msg.get("hasAttachments"),
|
||||
"es_automatico": es_mensaje_automatico(msg.get("internetMessageHeaders")),
|
||||
})
|
||||
|
||||
url = data.get("@odata.nextLink")
|
||||
@@ -153,7 +173,7 @@ def download_attachments(user_email: str, message_id: str, token: str, download_
|
||||
return downloaded_files
|
||||
|
||||
# ============================================================================
|
||||
def send_email(access_token: str, from_email: str, to_recipients: list[str], subject: str, html_body: str, attachment_paths: list[str] | None = None, cc_recipients: list[str] | None = None) -> bool:
|
||||
def send_email(access_token: str, from_email: str, to_recipients: list[str], subject: str, html_body: str, attachment_paths: list[str] | None = None, cc_recipients: list[str] | None = None, bcc_recipients: list[str] | None = None) -> bool:
|
||||
"""
|
||||
Envía un correo electrónico con cuerpo HTML y múltiples adjuntos PDF.
|
||||
|
||||
@@ -164,6 +184,7 @@ def send_email(access_token: str, from_email: str, to_recipients: list[str], sub
|
||||
:param html_body: Contenido del correo en formato HTML.
|
||||
:param attachment_paths: Lista de rutas a los archivos PDF que se adjuntarán.
|
||||
:param cc_recipients: Lista de correos electrónicos en copia (CC).
|
||||
:param bcc_recipients: Lista de correos electrónicos en copia oculta (BCC): no los ve el resto de destinatarios.
|
||||
:return: True si se envió correctamente, False en caso contrario.
|
||||
"""
|
||||
url = f"https://graph.microsoft.com/v1.0/users/{from_email}/sendMail"
|
||||
@@ -192,6 +213,9 @@ def send_email(access_token: str, from_email: str, to_recipients: list[str], sub
|
||||
}
|
||||
})
|
||||
|
||||
# Construir lista de destinatarios en copia oculta (BCC) para Graph API
|
||||
bcc_payload = [{"emailAddress": {"address": email}} for email in (bcc_recipients or [])]
|
||||
|
||||
# Construir lista de adjuntos
|
||||
attachments_payload = []
|
||||
if attachment_paths:
|
||||
@@ -229,6 +253,7 @@ def send_email(access_token: str, from_email: str, to_recipients: list[str], sub
|
||||
},
|
||||
"toRecipients": recipients_payload,
|
||||
"ccRecipients": cc_payload,
|
||||
"bccRecipients": bcc_payload,
|
||||
"attachments": attachments_payload
|
||||
},
|
||||
"saveToSentItems": "true"
|
||||
|
||||
Reference in new issue
Block a user