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.comvia media-server cloudflared;/sgated 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.comDNS/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://defaultin compose env (added 2026-07-01 after the worker crash-looped consuming the defaultsync://). Sends queue tomessenger_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 isdocroot, wherebin/consoledoesn't exist). - Editing email HTML by hand: update BOTH
emails.custom_htmlANDbundle_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' => truetomautic/config/local.php,cache:clear, callhttp://localhost:8090with headersHost: mautic.justinsforge.com+X-Forwarded-Proto: https(else 301 to https://localhost), then remove keys +cache:clear. - Users:
admin(creds in/opt/mautic/.envand~/.forge-secrets/mautic.env);krystalrole "Sip Editor" (emails/assets/contacts/segments/categories full, no config/user admin), passwordMAUTIC_KRYSTAL_PASSWORDin~/.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.aiverified in Resend 2026-07-02 (domain idc3f34aa4-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: TXTresend._domainkey.creatortrack.ai(DKIM key), TXTsend.creatortrack.ai(v=spf1 include:amazonses.com ~all), MXsend.creatortrack.ai->feedback-smtp.us-east-1.amazonses.comprio 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/+ repoforge/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', templatecreatortrack, from[email protected]/ from-name "CreatorTrack" / reply-to same, footer carries{unsubscribe_text}+{webview_text}. Segment: id 3 "CreatorTrack Updates" (aliascreatortrack-updates). Bothemails.custom_htmlandbundle_grapesjsbuilder.custom_mjmlpopulated (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:sendsilently skips list emails whosepublish_upis NULL (getPublishedByDateExpression(..., allowNullForPublishedUp: false)). Hand-inserted segment emails MUST setpublish_upto a past datetime; the command prints an empty results table with no error otherwise. - Verified 2026-07-02: test send to [email protected],
email_statsclean (0 failed), Resendlast_event: delivered(email iddeac2ed0-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 TXTresend._domainkey.<domain>, MX + SPF TXT onsend.<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.comalias; passwords in~/.forge-secrets/mautic.envasMAUTIC_<USERNAME>_PASSWORD):gusoutdoorco→role 3,gusthebass→5,shopnovadesign→4,justinwieb→6. Each owns a starter template email (ids 5-8) with fromhello@<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 (grantedfull). 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>/editwithplainPasswordreturns 200 but does NOT change the password (silent no-op). To set/reset a password, create the user viaPOST /api/users/new(works) or reset in the UI.gusoutdoorcohad 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 athttps://mautic.justinsforge.com/media/images/{gusoutdoor,gusbass,nova,justinwieb}-logo.png+sipnserve-icon-cream.png(repoinfra/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 copiesinfra/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]); allemailType='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 foremailType='template', sends from the email's own from_address via Resend). Verified 2026-07-02: all 5 brand welcomes sent to [email protected],email_statsis_failed=0, no SMTP errors. Self-check design first by rendering the HTML withgoogle-chrome --headless=new --screenshotand 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/sAccess 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/sAccess app policy (id3f3f3068-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_authinlocal.php, cache cleared) for the CreatorTrack signup sync (ct-emailbranch,lib/mautic.ts). The app calls the LAN originhttp://192.168.86.87:8090/api, never the public hostname. - Public
mautic.justinsforge.com/apiis gated by CF Access app "Mautic API (forge-mail)" (id3a5b1c41-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]