699 lines
32 KiB
Python
699 lines
32 KiB
Python
# ════════════════════════════════════════════════════════════════════════
|
|
# APLICACIÓN DE CORREO WEB - SERVIDOR FLASK + IMAP + SMTP
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
# IMPORTAR LIBRERÍAS NECESARIAS
|
|
import os # Para operaciones del sistema
|
|
import email # Para procesar mensajes de correo
|
|
import smtplib # Para enviar correos
|
|
import imaplib # Para recibir/gestionar correos
|
|
import poplib # Para descargar correos (alternativa a IMAP)
|
|
import base64 # Para codificar datos (adjuntos)
|
|
import json # Para trabajar con JSON
|
|
from email.mime.multipart import MIMEMultipart # Para correos con múltiples partes
|
|
from email.mime.text import MIMEText # Para cuerpo de texto en correos
|
|
from email.mime.base import MIMEBase # Para adjuntos
|
|
from email import encoders # Para codificar adjuntos en base64
|
|
from email.header import decode_header # Para decodificar cabeceras especiales
|
|
from datetime import datetime # Para fecha/hora
|
|
from flask import Flask, render_template, request, jsonify, send_file, session # Framework web
|
|
import io # Para manejar datos en memoria
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# CREAR APLICACIÓN FLASK Y CONFIGURAR PARÁMETROS
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
app = Flask(__name__) # Crear la aplicación web
|
|
app.secret_key = 'secret_mail_key_2024' # Clave para encriptar sesiones
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# CONFIGURACIÓN DEL SERVIDOR DE CORREO (PSP)
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
MAIL_SERVER = '10.10.0.101' # Dirección IP del servidor de correo corporativo
|
|
MAIL_USER = 'javi@psp.es' # Email del usuario
|
|
MAIL_PASS = '1234' # Contraseña de acceso
|
|
SMTP_PORT = 25 # Puerto para ENVIAR correos (SMTP)
|
|
IMAP_PORT = 143 # Puerto para RECIBIR/GESTIONAR correos (IMAP)
|
|
POP_PORT = 110 # Puerto alternativo para descargar (POP3) - no usado aquí
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# FUNCIONES AUXILIARES (HELPERS)
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
def decode_str(s):
|
|
"""
|
|
Decodificar cabeceras de correo que pueden estar en diferentes codificaciones.
|
|
Algunos correos tienen asuntos o remitentes en ISO-8859-1, UTF-8, etc.
|
|
Esta función los convierte todos a texto normal.
|
|
"""
|
|
if s is None:
|
|
return ''
|
|
|
|
# Descomponer la cabecera codificada en partes
|
|
parts = decode_header(s)
|
|
result = []
|
|
|
|
# Procesar cada parte
|
|
for part, enc in parts:
|
|
if isinstance(part, bytes):
|
|
# Si está en bytes, decodificar usando la codificación detectada
|
|
result.append(part.decode(enc or 'utf-8', errors='replace'))
|
|
else:
|
|
# Si ya es texto, usarlo tal cual
|
|
result.append(str(part))
|
|
|
|
return ' '.join(result)
|
|
|
|
|
|
def get_imap():
|
|
"""
|
|
CONECTAR CON EL SERVIDOR IMAP
|
|
Abre una conexión segura con el servidor de correo para leer/gestionar mensajes.
|
|
Retorna: (conexión, error) - Si hay error, conexión=None
|
|
"""
|
|
try:
|
|
# Crear conexión IMAP al servidor
|
|
M = imaplib.IMAP4(MAIL_SERVER, IMAP_PORT)
|
|
|
|
# Autentificarse con usuario y contraseña
|
|
M.login(MAIL_USER, MAIL_PASS)
|
|
|
|
# Retornar conexión exitosa y sin error
|
|
return M, None
|
|
|
|
except Exception as e:
|
|
# Si hay problema, retornar error
|
|
return None, str(e)
|
|
|
|
def parse_email_message(raw):
|
|
"""
|
|
EXTRAER INFORMACIÓN DEL CORREO COMPLETO
|
|
Recibe el contenido RAW (sin procesar) del correo completo.
|
|
Extrae: remitente, destinatario, asunto, cuerpo (texto/HTML), adjuntos, imágenes inline.
|
|
Retorna: diccionario con toda la información organizada.
|
|
"""
|
|
|
|
# Convertir el contenido RAW en un objeto de mensaje procesable
|
|
msg = email.message_from_bytes(raw)
|
|
|
|
# Inicializar variables para el contenido
|
|
body_text = '' # Texto plano del correo
|
|
body_html = '' # Versión HTML del correo
|
|
attachments = [] # Lista de archivos adjuntos
|
|
|
|
def walk(part):
|
|
"""
|
|
FUNCIÓN INTERNA: Recorrer todas las partes del correo
|
|
Los correos modernos pueden tener texto, HTML, imágenes, adjuntos, todo mezclado.
|
|
Esta función recursiva procesa cada parte.
|
|
"""
|
|
nonlocal body_text, body_html # Poder modificar variables externas
|
|
|
|
# Obtener el tipo de contenido (image/jpeg, text/plain, application/pdf, etc.)
|
|
ct = part.get_content_type()
|
|
|
|
# Obtener la disposición (attachment, inline, etc.)
|
|
cd = str(part.get('Content-Disposition', ''))
|
|
|
|
# Obtener el ID único si es una imagen inline (para mostrarla en el HTML)
|
|
cid = part.get('Content-ID')
|
|
if cid:
|
|
cid = cid.strip('<>') # Limpiar formato <ID> a ID
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# 1. PROCESAR ADJUNTOS E IMÁGENES INLINE
|
|
# ══════════════════════════════════════════════════════════════
|
|
if 'attachment' in cd or part.get_filename() or (cid and ct.startswith('image/')):
|
|
# Es un archivo adjunto o imagen
|
|
fn = decode_str(part.get_filename() or 'file') # Nombre del archivo
|
|
data = part.get_payload(decode=True) # Contenido binario del archivo
|
|
|
|
# Guardar en la lista de adjuntos
|
|
attachments.append({
|
|
'filename': fn, # Nombre
|
|
'data': base64.b64encode(data).decode() if data else '', # Contenido en BASE64
|
|
'mime': ct, # Tipo (image/jpeg, etc)
|
|
'size': len(data) if data else 0, # Tamaño en bytes
|
|
'content_id': cid # ID para imágenes inline
|
|
})
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# 2. PROCESAR CUERPO DE TEXTO PLANO
|
|
# ══════════════════════════════════════════════════════════════
|
|
elif ct == 'text/plain' and not body_text:
|
|
# Es texto plano y aún no hemos capturado uno
|
|
payload = part.get_payload(decode=True)
|
|
body_text = payload.decode(part.get_content_charset() or 'utf-8', errors='replace')
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# 3. PROCESAR CUERPO HTML
|
|
# ══════════════════════════════════════════════════════════════
|
|
elif ct == 'text/html' and not body_html:
|
|
# Es HTML y aún no hemos capturado uno
|
|
payload = part.get_payload(decode=True)
|
|
body_html = payload.decode(part.get_content_charset() or 'utf-8', errors='replace')
|
|
|
|
# ══════════════════════════════════════════════════════════════
|
|
# 4. RECURSIÓN: Si la parte contiene más partes, procesarlas
|
|
# ══════════════════════════════════════════════════════════════
|
|
if part.is_multipart():
|
|
# Procesar cada sub-parte
|
|
for p in part.get_payload():
|
|
walk(p)
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# EJECUTAR EL PROCESAMIENTO
|
|
# ══════════════════════════════════════════════════════════════════
|
|
|
|
if msg.is_multipart():
|
|
# El correo tiene múltiples partes (lo más común)
|
|
for part in msg.get_payload():
|
|
walk(part)
|
|
else:
|
|
# El correo es simple (solo texto o solo HTML)
|
|
ct = msg.get_content_type()
|
|
payload = msg.get_payload(decode=True)
|
|
text = payload.decode(msg.get_content_charset() or 'utf-8', errors='replace') if payload else ''
|
|
|
|
if ct == 'text/html':
|
|
body_html = text
|
|
else:
|
|
body_text = text
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# REEMPLAZAR IMÁGENES INLINE (cid:xxx) POR DATA URLs
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# Los correos HTML puede referenciar imágenes así: <img src="cid:image123">
|
|
# Necesitamos convertirlas a data URLs para que se vean en el navegador
|
|
|
|
if body_html and attachments:
|
|
for att in attachments:
|
|
if att.get('content_id'):
|
|
# Encontrar la imagen en los adjuntos
|
|
cid = att['content_id']
|
|
# Crear URL de datos: data:image/jpeg;base64,XXXXX...
|
|
dataurl = f"data:{att['mime']};base64,{att['data']}"
|
|
|
|
# Reemplazar en el HTML (comillas dobles y simples)
|
|
body_html = body_html.replace(f'src="cid:{cid}"', f'src="{dataurl}"')
|
|
body_html = body_html.replace(f"src='cid:{cid}'", f"src='{dataurl}'")
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# RETORNAR TODA LA INFORMACIÓN ORGANIZADA
|
|
# ══════════════════════════════════════════════════════════════════
|
|
return {
|
|
'from': decode_str(msg.get('From', '')), # Quién envió
|
|
'to': decode_str(msg.get('To', '')), # A quién fue
|
|
'subject': decode_str(msg.get('Subject', '(Sin asunto)')), # Asunto
|
|
'date': decode_str(msg.get('Date', '')), # Cuándo
|
|
'body_text': body_text, # Cuerpo texto plano
|
|
'body_html': body_html, # Cuerpo HTML (con imágenes procesadas)
|
|
'attachments': attachments, # Lista de archivos
|
|
}
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# RUTAS API (Endpoints que el navegador web llamará)
|
|
# Cada @app.route es una URL que responde con JSON
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 1. RUTA PRINCIPAL: Servir la página HTML
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/')
|
|
def index():
|
|
"""
|
|
GET /
|
|
Cuando el usuario accede a http://localhost:5000/,
|
|
servir el archivo HTML (interfaz gráfica).
|
|
"""
|
|
return render_template('index.html')
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 2. OBTENER LISTA DE CARPETAS (Inbox, Enviados, Papelera, etc)
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/folders')
|
|
def get_folders():
|
|
"""
|
|
GET /api/folders
|
|
Conectar con IMAP y obtener la lista de carpetas del correo.
|
|
Responde con: {"folders": ["INBOX", "Sent", "Drafts", "Trash", ...]}
|
|
"""
|
|
M, err = get_imap() # Conectar
|
|
if err:
|
|
return jsonify({'error': err}), 500 # Si hay error, retornar
|
|
|
|
try:
|
|
# Listar carpetas del servidor
|
|
_, folders_raw = M.list()
|
|
folders = []
|
|
|
|
# Procesar cada carpeta (parsear el formato IMAP)
|
|
for f in folders_raw:
|
|
parts = f.decode().split('"."')
|
|
name = parts[-1].strip().strip('"')
|
|
folders.append(name)
|
|
|
|
M.logout() # Cerrar conexión
|
|
return jsonify({'folders': folders})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 3. OBTENER LISTA DE MENSAJES CON PAGINACIÓN
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/messages')
|
|
def get_messages():
|
|
"""
|
|
GET /api/messages?folder=INBOX&page=1&per_page=25
|
|
|
|
Obtener los mensajes de una carpeta, con paginación.
|
|
Parámetros:
|
|
- folder: nombre de la carpeta (INBOX, Sent, etc)
|
|
- page: número de página (por defecto 1)
|
|
- per_page: cuántos mensajes por página (por defecto 20)
|
|
|
|
Retorna:
|
|
{
|
|
"messages": [
|
|
{"uid": "123", "from": "user@example.com", "subject": "Asunto", "date": "...", "seen": false},
|
|
...
|
|
],
|
|
"total": 150,
|
|
"page": 1,
|
|
"per_page": 25
|
|
}
|
|
"""
|
|
folder = request.args.get('folder', 'INBOX') # Carpeta solicitada
|
|
page = int(request.args.get('page', 1)) # Número de página
|
|
per_page = int(request.args.get('per_page', 20)) # Mensajes por página
|
|
|
|
M, err = get_imap()
|
|
if err:
|
|
return jsonify({'error': err}), 500
|
|
|
|
try:
|
|
# Seleccionar la carpeta
|
|
M.select(folder)
|
|
|
|
# Buscar TODOS los mensajes
|
|
_, data = M.search(None, 'ALL')
|
|
ids = data[0].split() # Obtener lista de IDs
|
|
ids = list(reversed(ids)) # Invertir para mostrar más nuevos primero
|
|
|
|
total = len(ids) # Total de mensajes en la carpeta
|
|
|
|
# Calcular qué mensajes mostrar (paginación)
|
|
start = (page - 1) * per_page
|
|
end = start + per_page
|
|
page_ids = ids[start:end]
|
|
|
|
messages = []
|
|
|
|
# Procesar cada mensaje de esta página
|
|
for uid in page_ids:
|
|
# Obtener solo las cabeceras (más rápido que todo el mensaje)
|
|
_, header_data = M.fetch(uid, '(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])')
|
|
raw_header = header_data[0][1]
|
|
msg = email.message_from_bytes(raw_header)
|
|
|
|
# Verificar si el mensaje ya fue leído (flag \Seen)
|
|
_, flags_data = M.fetch(uid, '(FLAGS)')
|
|
flags = flags_data[0].decode() if flags_data[0] else ''
|
|
seen = '\\Seen' in flags
|
|
|
|
# Guardar información del mensaje
|
|
messages.append({
|
|
'uid': uid.decode(),
|
|
'from': decode_str(msg.get('From', '')),
|
|
'subject': decode_str(msg.get('Subject', '(Sin asunto)')),
|
|
'date': decode_str(msg.get('Date', '')),
|
|
'seen': seen,
|
|
})
|
|
|
|
M.logout()
|
|
return jsonify({
|
|
'messages': messages,
|
|
'total': total,
|
|
'page': page,
|
|
'per_page': per_page
|
|
})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 4. OBTENER UN MENSAJE COMPLETO (con cuerpo, adjuntos, etc)
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/message/<uid>')
|
|
def get_message(uid):
|
|
"""
|
|
GET /api/message/123?folder=INBOX
|
|
|
|
Descargar un correo completo con su contenido, imágenes, adjuntos, etc.
|
|
También marca el correo como leído (flag \Seen).
|
|
|
|
Retorna el JSON del correo procesado por parse_email_message()
|
|
"""
|
|
folder = request.args.get('folder', 'INBOX')
|
|
|
|
M, err = get_imap()
|
|
if err:
|
|
return jsonify({'error': err}), 500
|
|
|
|
try:
|
|
M.select(folder)
|
|
|
|
# Descargar el correo completo (RFC822)
|
|
_, data = M.fetch(uid.encode(), '(RFC822)')
|
|
raw = data[0][1]
|
|
|
|
# Marcar el correo como leído
|
|
M.store(uid.encode(), '+FLAGS', '\\Seen')
|
|
M.logout()
|
|
|
|
# Procesar el correo con la función parse_email_message()
|
|
parsed = parse_email_message(raw)
|
|
parsed['uid'] = uid # Agregar el UID
|
|
|
|
return jsonify(parsed)
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 5. DESCARGAR UN ADJUNTO ESPECÍFICO
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/attachment/<uid>/<int:idx>')
|
|
def get_attachment(uid, idx):
|
|
"""
|
|
GET /api/attachment/123/0?folder=INBOX
|
|
|
|
Descargar un archivo adjunto específico.
|
|
Parámetros:
|
|
- uid: ID del correo
|
|
- idx: índice del adjunto (0 = primer adjunto, 1 = segundo, etc)
|
|
- folder: carpeta donde está el correo
|
|
|
|
Retorna el archivo con su nombre y tipo MIME para descargar.
|
|
"""
|
|
folder = request.args.get('folder', 'INBOX')
|
|
|
|
M, err = get_imap()
|
|
if err:
|
|
return jsonify({'error': err}), 500
|
|
|
|
try:
|
|
M.select(folder)
|
|
|
|
# Descargar el correo completo
|
|
_, data = M.fetch(uid.encode(), '(RFC822)')
|
|
raw = data[0][1]
|
|
M.logout()
|
|
|
|
# Procesar el correo
|
|
parsed = parse_email_message(raw)
|
|
att = parsed['attachments'][idx] # Obtener el adjunto por índice
|
|
|
|
# Decodificar el contenido de base64 a bytes
|
|
file_data = base64.b64decode(att['data'])
|
|
|
|
# Enviar el archivo para descargar
|
|
return send_file(
|
|
io.BytesIO(file_data),
|
|
download_name=att['filename'], # Nombre del archivo
|
|
as_attachment=True, # Descargar (no mostrar en navegador)
|
|
mimetype=att['mime'] # Tipo (image/jpeg, application/pdf, etc)
|
|
)
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 6. ENVIAR UN CORREO (con o sin adjuntos)
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/send', methods=['POST'])
|
|
def send_email():
|
|
"""
|
|
POST /api/send
|
|
|
|
Enviar un correo electrónico con opcionalmente:
|
|
- cuerpo de texto plano
|
|
- cuerpo HTML (formato enriquecido)
|
|
- múltiples archivos adjuntos
|
|
|
|
Parámetros (formulario):
|
|
- to: dirección de correo destino
|
|
- subject: asunto del correo
|
|
- body: contenido del correo
|
|
- body_type: 'plain' (texto) o 'html' (formato enriquecido)
|
|
- attachments: lista de archivos (opcional)
|
|
|
|
Usa SMTP para conectarse al servidor y enviar.
|
|
"""
|
|
data = request.form
|
|
to_addr = data.get('to', '')
|
|
subject = data.get('subject', '')
|
|
body = data.get('body', '')
|
|
body_type = data.get('body_type', 'plain')
|
|
|
|
# Crear estructura MIME del correo
|
|
msg = MIMEMultipart('alternative' if body_type == 'html' else 'mixed')
|
|
msg['From'] = MAIL_USER
|
|
msg['To'] = to_addr
|
|
msg['Subject'] = subject
|
|
|
|
# Agregar el cuerpo del correo (texto HTML o plano)
|
|
if body_type == 'html':
|
|
msg.attach(MIMEText(body, 'html', 'utf-8'))
|
|
else:
|
|
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
|
|
|
# Procesar adjuntos si los hay
|
|
files = request.files.getlist('attachments')
|
|
for f in files:
|
|
if f.filename:
|
|
# Crear una parte MIME para el archivo
|
|
part = MIMEBase('application', 'octet-stream')
|
|
part.set_payload(f.read()) # Leer contenido del archivo
|
|
encoders.encode_base64(part) # Codificar en base64
|
|
part.add_header('Content-Disposition', f'attachment; filename="{f.filename}"')
|
|
msg.attach(part)
|
|
|
|
# Enviar el correo vía SMTP
|
|
try:
|
|
with smtplib.SMTP(MAIL_SERVER, SMTP_PORT, timeout=10) as server:
|
|
server.sendmail(MAIL_USER, [to_addr], msg.as_string())
|
|
return jsonify({'ok': True})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 7. ELIMINAR UN MENSAJE
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/delete/<uid>', methods=['DELETE'])
|
|
def delete_message(uid):
|
|
"""
|
|
DELETE /api/delete/123?folder=INBOX
|
|
|
|
Marcar un correo como eliminado en IMAP.
|
|
Parámetros:
|
|
- uid: ID del correo a eliminar
|
|
- folder: carpeta donde está el correo
|
|
|
|
Usa dos comandos IMAP:
|
|
1. store(..., '+FLAGS', '\\Deleted') - marca con bandera \Deleted
|
|
2. expunge() - elimina permanentemente los marcados
|
|
"""
|
|
folder = request.args.get('folder', 'INBOX')
|
|
M, err = get_imap()
|
|
if err:
|
|
return jsonify({'error': err}), 500
|
|
try:
|
|
M.select(folder)
|
|
M.store(uid.encode(), '+FLAGS', '\\Deleted') # Marcar como eliminado
|
|
M.expunge() # Expulsar definitivamente
|
|
M.logout()
|
|
return jsonify({'ok': True})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 8. MOVER UN MENSAJE A OTRA CARPETA
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/move/<uid>', methods=['POST'])
|
|
def move_message(uid):
|
|
"""
|
|
POST /api/move/123
|
|
|
|
Copiar un correo a otra carpeta y eliminar del original.
|
|
Parámetros JSON:
|
|
- from: carpeta origen (ej. "INBOX")
|
|
- to: carpeta destino (ej. "Trash")
|
|
|
|
Proceso:
|
|
1. copy() - copiar el correo a la carpeta destino
|
|
2. store()+FLAGS - marcar \Deleted en la carpeta origen
|
|
3. expunge() - eliminar de la carpeta origen
|
|
"""
|
|
src = request.json.get('from', 'INBOX')
|
|
dst = request.json.get('to', 'Trash')
|
|
M, err = get_imap()
|
|
if err:
|
|
return jsonify({'error': err}), 500
|
|
try:
|
|
M.select(src)
|
|
M.copy(uid.encode(), dst) # Copiar a carpeta destino
|
|
M.store(uid.encode(), '+FLAGS', '\\Deleted') # Marcar para eliminar
|
|
M.expunge() # Eliminar del origen
|
|
M.logout()
|
|
return jsonify({'ok': True})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 9. BUSCAR MENSAJES
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/search')
|
|
def search_messages():
|
|
"""
|
|
GET /api/search?q=palabra&folder=INBOX
|
|
|
|
Buscar correos por asunto o remitente.
|
|
Parámetros:
|
|
- q: término de búsqueda
|
|
- folder: carpeta donde buscar
|
|
|
|
Usa el comando IMAP:
|
|
- (OR SUBJECT "palabra" FROM "palabra")
|
|
|
|
Retorna máximo 50 resultados.
|
|
"""
|
|
query = request.args.get('q', '')
|
|
folder = request.args.get('folder', 'INBOX')
|
|
M, err = get_imap()
|
|
if err:
|
|
return jsonify({'error': err}), 500
|
|
try:
|
|
M.select(folder)
|
|
|
|
# Criterio búsqueda: coincida en ASUNTO o REMITENTE
|
|
criteria = f'(OR SUBJECT "{query}" FROM "{query}")'
|
|
_, data = M.search(None, criteria)
|
|
|
|
# Obtener máximo 50 resultados, más nuevos primero
|
|
ids = list(reversed(data[0].split()))[:50]
|
|
messages = []
|
|
|
|
# Procesar cada resultado encontrado
|
|
for uid in ids:
|
|
_, header_data = M.fetch(uid, '(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)])')
|
|
raw_header = header_data[0][1]
|
|
msg = email.message_from_bytes(raw_header)
|
|
_, flags_data = M.fetch(uid, '(FLAGS)')
|
|
flags = flags_data[0].decode() if flags_data[0] else ''
|
|
seen = '\\Seen' in flags
|
|
|
|
messages.append({
|
|
'uid': uid.decode(),
|
|
'from': decode_str(msg.get('From', '')),
|
|
'subject': decode_str(msg.get('Subject', '(Sin asunto)')),
|
|
'date': decode_str(msg.get('Date', '')),
|
|
'seen': seen,
|
|
})
|
|
|
|
M.logout()
|
|
return jsonify({'messages': messages, 'total': len(messages)})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 10. MARCAR COMO LEÍDO O DESTACAR CON ESTRELLA
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/mark/<uid>', methods=['POST'])
|
|
def mark_message(uid):
|
|
"""
|
|
POST /api/mark/123?folder=INBOX
|
|
|
|
Marcar o desmarcar un correo como leído, o agregarlo/quitarlo de favoritos.
|
|
Parámetros JSON:
|
|
- flag: qué marcar ('seen' = leído, 'flagged' = estrella)
|
|
- action: 'add' (marcar) o 'remove' (desmarcar)
|
|
|
|
Usa comando IMAP store:
|
|
- '+FLAGS' para agregar bandera
|
|
- '-FLAGS' para quitar bandera
|
|
|
|
Banderas IMAP:
|
|
- \\Seen : el correo fue leído
|
|
- \\Flagged : el correo tiene estrella/favorito
|
|
"""
|
|
folder = request.args.get('folder', 'INBOX')
|
|
flag = request.json.get('flag', 'seen') # 'seen' o 'flagged'
|
|
action = request.json.get('action', 'add') # 'add' o 'remove'
|
|
|
|
M, err = get_imap()
|
|
if err:
|
|
return jsonify({'error': err}), 500
|
|
try:
|
|
M.select(folder)
|
|
|
|
# Convertir flag a bandera IMAP
|
|
imap_flag = {'seen': '\\Seen', 'flagged': '\\Flagged'}.get(flag, '\\Seen')
|
|
|
|
# Operación: agregar (+FLAGS) o quitar (-FLAGS)
|
|
op = '+FLAGS' if action == 'add' else '-FLAGS'
|
|
|
|
# Ejecutar en IMAP
|
|
M.store(uid.encode(), op, imap_flag)
|
|
M.logout()
|
|
return jsonify({'ok': True})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
# 11. CREAR UNA NUEVA CARPETA
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@app.route('/api/create_folder', methods=['POST'])
|
|
def create_folder():
|
|
"""
|
|
POST /api/create_folder
|
|
|
|
Crear una carpeta nueva en el servidor IMAP.
|
|
Parámetro JSON:
|
|
- name: nombre de la nueva carpeta
|
|
|
|
Usa el comando IMAP create().
|
|
"""
|
|
name = request.json.get('name', '')
|
|
M, err = get_imap()
|
|
if err:
|
|
return jsonify({'error': err}), 500
|
|
try:
|
|
M.create(name) # Crear la carpeta en el servidor
|
|
M.logout()
|
|
return jsonify({'ok': True})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
# INICIO DEL SERVIDOR
|
|
# ════════════════════════════════════════════════════════════════════════
|
|
if __name__ == '__main__':
|
|
# Iniciar el servidor Flask
|
|
# debug=True: recarga automática al cambiar código
|
|
# host='0.0.0.0': aceptar conexiones desde cualquier dirección IP
|
|
# port=5000: puerto donde escuchar
|
|
app.run(debug=True, host='0.0.0.0', port=5000)
|