Dox Newsletter self-updates
Newsletters and email campaigns from your own WordPress (Dox Studio)
by Dox Studio · github.com/davidzoque/dox-newsletter · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/davidzoque/dox-newsletter/archive/refs/heads/main.zipShips its own WordPress updater (Plugin Update Checker), so new versions show up under Dashboard → Updates.
Boletines y campañas de correo desde el propio WordPress, "tipo Mailchimp pero
local". Lo pidió David el 04/10/2026 como plugin propio, igual que hizo Dox Mail
en Perfex, y con una interfaz al estilo de pixfort. Gran parte de la lógica viene
de Dox Mail (Projects/Perfex CRM/Perfex Modules/dox_mail): el renderizador del
correo, el envío por tandas, el seguimiento y el informe.
Qué hay en cada archivo
| Archivo | Qué hace |
|---|---|
dox-newsletter.php |
Arranque: constantes, dox-core, carga de clases, Plugin Update Checker |
includes/class-dxn-install.php |
Las 7 tablas wp_dxn_* con dbDelta. Se ponen al día solas al cambiar DB_VERSION |
includes/class-dxn-settings.php |
Ajustes (opción dxn_settings), marca del correo, cifrado de la contraseña de SES y utilidades de fecha y número |
includes/class-dxn-renderer.php |
De los bloques al HTML del correo. PHP puro: lo prueba tests/run.php sin WordPress |
includes/class-dxn-subscribers.php |
Suscriptores y listas: alta con doble confirmación, baja, interés, importar CSV |
includes/class-dxn-campaigns.php |
Campañas: guardar, lanzar o programar, audiencia, reenviar, bienvenida, números del informe |
includes/class-dxn-mailer.php |
Envía por wp_mail o por Amazon SES (PHPMailer propio) |
includes/class-dxn-sender.php |
La cola: tandas por WP-Cron con tope por hora y candado |
includes/class-dxn-public.php |
Lo que llega de fuera: suscribirse, confirmar, baja, pixel, clic, ver en el navegador |
includes/class-dxn-forms.php |
Formularios (opción dxn_forms): shortcode, ventana emergente y barra |
includes/class-dxn-stats.php |
Los números del Resumen |
includes/class-dxn-admin.php |
El panel: menú, recursos, iconos, gráfica y todas las acciones AJAX dxn_* |
includes/class-dxn-elementor.php |
Widget de Elementor (pinta el shortcode) |
admin/views/*.php |
Las pantallas: Resumen, Campañas, Editor, Suscriptores, Formularios, Informe, Ajustes |
assets/admin.* |
CSS y JS del panel |
assets/public.* |
CSS y JS de los formularios en la web |
tools/i18n.py, tools/es.py |
Saca los textos y genera el .pot, .po y .mo en español. No viaja en el zip |
tests/run.php |
Pruebas del renderizador. No viaja en el zip; el workflow las corre antes de cada release |
maqueta/panel.html |
La maqueta que aprobó David el 04/10/2026. No viaja en el zip |
Cómo sale un correo
- Lanzar congela la audiencia en
wp_dxn_recipients(una fila por persona, cada una con su token) y registra los enlaces del correo enwp_dxn_links. - Cada minuto (WP-Cron, hook
dxn_send) sale una tanda: como mucho 25 correos y 20 segundos, y nunca más de lo que deje el tope por hora de Ajustes contando los enviados en los últimos 60 minutos (no por pasadas: aunque el cron se salte minutos, nunca salen más de los que tocan). - Antes de cada correo se mira que la persona siga activa: si se dio de baja entre medias, se salta.
- Con el panel abierto, la franja de estado pregunta cada 20 segundos y de
paso ejecuta una pasada (
ajax_status). Así el envío no depende de que alguien visite la web para que corra WP-Cron.
Por qué el tope: en un hosting compartido el servidor deja salir un número de correos por hora por dominio (en el de Dox Studio, 200). El boletín sale a 150 por defecto para dejar sitio a pedidos, contraseñas y formularios. Y la IP del servidor la comparten todas las webs: una lista sucia puede llevar el correo de todas a spam, por eso la doble confirmación viene encendida.
Si la web tiene un plugin de SMTP (GoSMTP, WP Mail SMTP...), wp_mail sale
por él, y si ese plugin fuerza el remitente, manda el suyo sobre el de Ajustes.
Lo que llega de fuera
Todo por la portada con ?dxn=<acción>&t=<token>, no por la API REST: Hide My WP
puede cerrar wp-json a los visitantes. Se atiende en init y se marca como no
cacheable (DONOTCACHEPAGE, litespeed_control_set_nocache, nocache_headers).
Nunca dentro de wp-admin ni de admin-ajax (is_admin()).
subscribe(POST): sin nonce a propósito, porque la página del formulario puede estar en la caché y el nonce caducaría. Lo frenan un campo trampa (dxn_hp), un mínimo de 2 segundos desde que se pinta el formulario y 5 intentos cada 10 minutos por IP. Condxn_ajax=1responde JSON; sin JS redirige a la página con?dxn_s=<estado>&dxn_f=<formulario>.confirm: pasa dependingaactivey pone en cola la bienvenida si está encendida.unsubscribe: por GET enseña un botón (los antivirus del correo abren todos los enlaces solos y darían de baja a la gente); por POST da de baja. Gmail y Yahoo mandan un POST conList-Unsubscribe=One-Click(RFC 8058) sin cookies.open: el pixel. Gmail y Mail del iPhone lo piden solos al llegar el correo, así que la apertura es una cifra al alza; el informe lo dice.click: redirige a la URL guardada al lanzar la campaña, nunca a una que venga en la petición. Un clic sin apertura cuenta también como apertura.view: el correo de esa persona en el navegador, sin seguimiento.
Trampas que ya conté
- Un
<form>dentro de otro cierra el de fuera antes de tiempo. La vista previa del formulario en el panel va dentro del formulario de edición: por eso en modopreviewse pinta con<div>y sinnameen los campos. Conname, el campo ocultodxn=subscribeviajaba con cada guardado. - Los scripts del pie se imprimen en
wp_footercon prioridad 20. La ventana emergente y la barra se pintan enwp_footercon prioridad 5; a la 20, el JS se encolaba tarde y la ventana no salía nunca. .dxn-app svg { width: 16px }es para los iconos: la gráfica necesita su regla con más peso (.dxn-app svg.dxn-chart).- Un
display: gridgana al atributohidden. El panel lleva.dxn-app [hidden] { display: none !important }. - Las reglas de párrafo del tema (o de wp-admin) ganan a
.dxn-title. Enpublic.csslos textos del formulario van con dos clases (.dxn-box .dxn-title). - El editor redibuja los bloques al abrir uno: el campo al que dar el foco está en el elemento nuevo, no en el que recibió el clic.
- Las fechas de la gráfica van en HTML debajo del SVG: el SVG se estira a lo
ancho (
preserveAspectRatio="none") y el texto dentro salía deformado en el móvil. __( '%s person' )no encuentra la traducción si ese texto existe también como plural (_n): el.molo guarda junto a su plural. Para esos, siempre_n().
Traducción
El código está en inglés y trae el español (languages/dox-newsletter-es_ES.*).
WordPress no pasa de es_CO o es_MX a es_ES solo: el filtro de
load_textdomain_mofile del archivo principal lo hace. Para un texto nuevo,
añadir su traducción en tools/es.py y correr python3 tools/i18n.py.
Probar en local
Funciona en un WordPress con SQLite (el plugin oficial SQLite Database
Integration) y php -S, sin MySQL. Con un mu-plugin que enganche pre_wp_mail
y guarde cada correo en un archivo se ve exactamente lo que recibiría la gente sin
enviar nada. El servidor de PHP atiende de una en una, así que el editor va algo
lento ahí.
Desinstalar
Borrar el plugin no borra la lista ni las campañas (como Dox POS): solo quita
la tarea programada. Las tablas wp_dxn_* y las opciones dxn_* se borran a mano.
Pendiente
- Casilla "Quiero recibir novedades" en el checkout de WooCommerce.
- Rebotes y quejas de Amazon SES por SNS (como
acg_mail). - Filtro de bots en los clics (los antivirus de Outlook abren todos los enlaces).