Skip to content

forge-mail Mautic email platform

URL: https://mkdocs.justinsforge.com/memory/general/reference_forge_mail_mautic/

  • Host: forge-mail LXC CT 112, 192.168.86.87 (ssh [email protected]). Mautic 5.2 at /opt/mautic (compose: db/mautic_web/mautic_cron/mautic_worker), port 8090, mautic.justinsforge.com via media-server cloudflared; /s gated by CF Access. Repo mirror: forge/infra/mautic/ (keep both in sync). Mautic is the sole email platform: Listmonk was a false start (never used, only test data) and was fully removed 2026-07-08 (container+volume, /opt/listmonk, lists.justinsforge.com DNS/tunnel/Access, secret, infra dir all gone).
  • Send path (the clean one): segment email (email_type='list') sent to a segment. NEVER "contact → Send Email → import from existing template", that path re-renders through CKEditor and mangles the design.
  • Messenger: MAUTIC_MESSENGER_DSN_EMAIL/HIT=doctrine://default in compose env (added 2026-07-01 after the worker crash-looped consuming the default sync://). Sends queue to messenger_messages, worker delivers via Resend SMTP (smtp.resend.com:587, key in /opt/mautic/.env + ~/.forge-secrets/resend.env).
  • Manual send / test: docker compose exec -T -u www-data -w /var/www/html mautic_web php bin/console mautic:broadcasts:send --id=<email-id>. Console must run with -w /var/www/html (container default cwd is docroot, where bin/console doesn't exist).
  • Editing email HTML by hand: update BOTH emails.custom_html AND bundle_grapesjsbuilder.custom_mjml (GrapesJS builder source), plus theme twigs at /opt/mautic/mautic/themes/sipnserve/ if applicable. Images self-hosted at /opt/mautic/mautic/media/images/https://mautic.justinsforge.com/media/images/ (public, not Access-gated).
  • API: disabled by default. To use temporarily: add 'api_enabled' => true, 'api_enable_basic_auth' => true to mautic/config/local.php, cache:clear, call http://localhost:8090 with headers Host: mautic.justinsforge.com + X-Forwarded-Proto: https (else 301 to https://localhost), then remove keys + cache:clear.
  • Users: admin (creds in /opt/mautic/.env and ~/.forge-secrets/mautic.env); krystal role "Sip Editor" (emails/assets/contacts/segments/categories full, no config/user admin), password MAUTIC_KRYSTAL_PASSWORD in ~/.forge-secrets/mautic.env.
  • Sip inbound architecture (verified 2026-07-01): [email protected] is a Google Group ("Sip N Serve") on Justin's Workspace (sipnservesociety.com = secondary domain, MX -> Google, NOT Cloudflare Email Routing). Members [email protected] + [email protected], both ALL_MAIL: every hello@ message fans out to both. Do NOT propose MX flips or forwarding; inbound already reaches both. Krystal sends as hello@ via Gmail send-as over Resend SMTP (verified; SMTP auth = ownership proof, no code email). Her Gmail: "Sip N Serve" label + to/from-hello@ filters, and she is the third OAuth account in forge_google_user.py (google-oauth-krystal.env).
  • Full build story: sipnserve-email-platform handoff.

CreatorTrack identity ([email protected])

  • Domain: creatortrack.ai verified in Resend 2026-07-02 (domain id c3f34aa4-c814-45f5-841b-812a954d7708, region us-east-1, DKIM + SPF verified). Required Resend Pro (Justin upgraded 2026-07-02; free tier caps at 1 domain, held by sipnservesociety.com).
  • DNS (Cloudflare zone 5039254a768b949cffe09bf46fe9c6aa), 4 records added 2026-07-02: TXT resend._domainkey.creatortrack.ai (DKIM key), TXT send.creatortrack.ai (v=spf1 include:amazonses.com ~all), MX send.creatortrack.ai -> feedback-smtp.us-east-1.amazonses.com prio 10 (bounce feedback; subdomain only, zone had and has NO root MX), TXT _dmarc.creatortrack.ai (v=DMARC1; p=none;). Pre-change snapshot: forge/data/creatortrack-email/creatortrack-ai-dns-snapshot-2026-07-02.json.
  • Theme: creatortrack (MJML, grapesjsbuilder) at /opt/mautic/mautic/themes/creatortrack/ + repo forge/infra/mautic/themes/creatortrack/ (compiled reference: email-compiled.html). Dark layout: page #0d0d12, black header band with white wordmark logo, card #20202a, blue accent #3b82f6. Logo self-hosted: https://mautic.justinsforge.com/media/images/creatortrack-logo-white.png (also in repo media dir).
  • Email: id 4 "CreatorTrack Updates", email_type='list', template creatortrack, from [email protected] / from-name "CreatorTrack" / reply-to same, footer carries {unsubscribe_text} + {webview_text}. Segment: id 3 "CreatorTrack Updates" (alias creatortrack-updates). Both emails.custom_html and bundle_grapesjsbuilder.custom_mjml populated (id 4).
  • How to send as updates@: add contacts to segment 3, then docker compose exec -T -u www-data -w /var/www/html mautic_web php bin/console mautic:broadcasts:send --channel=email --id=4. Transport is the account-wide Resend SMTP; per-email from_address overrides the default sender, no extra mailer config needed once the domain is verified in Resend.
  • Gotcha (cost 2 debug rounds): mautic:broadcasts:send silently skips list emails whose publish_up is NULL (getPublishedByDateExpression(..., allowNullForPublishedUp: false)). Hand-inserted segment emails MUST set publish_up to a past datetime; the command prints an empty results table with no error otherwise.
  • Verified 2026-07-02: test send to [email protected], email_stats clean (0 failed), Resend last_event: delivered (email id deac2ed0-3f60-4268-a968-8de1ff3e0f06).

Multi-brand per-login isolation (2026-07-02)

  • 4 brand domains added to Resend + Cloudflare 2026-07-02 (same 3-record pattern as CreatorTrack, subdomain-only, no root MX touched): gusoutdoor.co, shopnovadesign.com, gusthebass.com, justinwieb.com. All status=verified. Records: DKIM TXT resend._domainkey.<domain>, MX + SPF TXT on send.<domain>. No DMARC yet (p=none pending). Account now holds 6 verified domains.
  • Per-brand logins, each isolated to its own data. Mautic is single-tenant, so isolation = per-brand role with own-scoped permissions (viewown/editown/create/publishown/deleteown on email+asset; viewown on lead+imports) + a per-brand global category. Roles: Gus Outdoor Editor (3), Nova Design Editor (4), Gus The Bass Editor (5), JustinWieb Editor (6). Categories (global bundle): Gus Outdoor (1), Nova Design (2), Gus The Bass (3), JustinWieb (4). Admin still sees all.
  • Users (login email = justinwieb+<username>@gmail.com alias; passwords in ~/.forge-secrets/mautic.env as MAUTIC_<USERNAME>_PASSWORD): gusoutdoorco→role 3, gusthebass→5, shopnovadesign→4, justinwieb→6. Each owns a starter template email (ids 5-8) with from hello@<domain> + brand category, so its login sees exactly one email.
  • Isolation gap (Mautic core limitation): segments (lead:lists) have NO per-owner scoping, so segment lists are visible account-wide regardless of role (granted full). Mitigate by brand-prefixing segment names + tagging with the brand category. Emails/assets/contacts/imports are hard-isolated by owner.
  • Password reset gotcha: PATCH /api/users/<id>/edit with plainPassword returns 200 but does NOT change the password (silent no-op). To set/reset a password, create the user via POST /api/users/new (works) or reset in the UI. gusoutdoorco had to be deleted + recreated for this reason.
  • Brand welcome templates + logos (2026-07-02): real logos live at /mnt/workspace/<Brand>/Assets/Logo/; Justin curates them there. Email-optimized copies self-hosted at https://mautic.justinsforge.com/media/images/{gusoutdoor,gusbass,nova,justinwieb}-logo.png + sipnserve-icon-cream.png (repo infra/mautic/media/images/). One shared responsive table-based template per brand (header band + logo, greeting/tagline/2 paras/CTA button, footer with {webview_text}+{unsubscribe_text}); repo copies infra/mautic/emails/<brand>-welcome.html. Emails: id 8 gusoutdoor ([email protected]), 5 gusbass ([email protected]), 6 nova ([email protected]), 7 justinwieb ([email protected]), 3 sipnserve ([email protected]); all emailType='template', own-brand category. Palettes: gusoutdoor dark#14181c/gold#c49a5e, gusbass near-black/water-blue#38B6FF, nova light#f4f2fb/violet#4f46f5, sipnserve cobalt#0137f4, justinwieb black/yellow#ffe600 (use a darker accent_text for on-white text/links, raw yellow is invisible on white).
  • Test-send a template to one address: POST /api/emails/{id}/contact/{contactId}/send (works for emailType='template', sends from the email's own from_address via Resend). Verified 2026-07-02: all 5 brand welcomes sent to [email protected], email_stats is_failed=0, no SMTP errors. Self-check design first by rendering the HTML with google-chrome --headless=new --screenshot and viewing before sending.
  • CF Access: brand logins are Justin's + aliases, not real Google identities, so they are NOT (and cannot be) added to the /s Access policy. To reach the Mautic login page the operator first passes CF Access as one of the 3 allowlisted Google accounts ([email protected], [email protected], [email protected]), THEN logs into Mautic with the brand username/password. For a genuinely separate operator (e.g. Michael on Gus), add their real email to the /s Access app policy (id 3f3f3068-4b5e-40ba-9f83-254a48b0bbdf).

[Claude Code, sipnserve-email worker]

REST API enablement + Access gate (2026-07-02)

  • Mautic REST API enabled on CT 112 (api_enabled + api_enable_basic_auth in local.php, cache cleared) for the CreatorTrack signup sync (ct-email branch, lib/mautic.ts). The app calls the LAN origin http://192.168.86.87:8090/api, never the public hostname.
  • Public mautic.justinsforge.com/api is gated by CF Access app "Mautic API (forge-mail)" (id 3a5b1c41-9aaa-4ff1-9b2d-1c295e257bd1, policy Justin + Krystal, created 2026-07-02; verified 403 without Access identity). Mirrors the existing "/s" admin gate. Tracking pixels/unsubscribe pages are unaffected (not under /api).

[Claude Code]