IRON - UNSUBSCRIBE SYSTEM
=========================
Database-backed. Tokenised links. Automatic status updates. Staff
notification. No third-party service anywhere in the chain.


WHAT GOES WHERE
---------------
  /home/<cpanel-user>/private/          <- ABOVE public_html, not web-readable
      config.php                        passwords live here
      PHPMailer/                        you add this (step 2)
      unsubscribes.csv                  created automatically

  /home/<cpanel-user>/public_html/
      unsubscribe.html                  the page
      unsubscribe.php                   receives the submission
      token.php                         resolves a token to an address
      lib.php                           shared config/db/mail
      admin/
          .htaccess
          import.php                    load subscribers, mint tokens
          export-links.php              mail-merge CSV with personal links


=====================================================================
STEP 1 - DATABASE
=====================================================================
  1. cPanel -> MySQL Databases
       - Create database:  newsletter
       - Create user + strong password
       - Add user to database, tick ALL PRIVILEGES
       cPanel prefixes both with your account name, e.g. ironafr_newsletter

  2. cPanel -> phpMyAdmin -> select that database -> SQL tab
       Paste the whole of SETUP.sql, press Go.

     Creates: subscribers, unsubscribe_events, and two convenience views
     (v_active, v_suppression).


=====================================================================
STEP 2 - PHPMAILER
=====================================================================
  Option A - cPanel Terminal (if your host enables it):
      cd ~/private
      composer require phpmailer/phpmailer

  Option B - manual, works everywhere:
      1. Download  github.com/PHPMailer/PHPMailer  ->  Code -> Download ZIP
      2. Unzip on your PC.
      3. In cPanel create the folder:  private/PHPMailer/src/
      4. Upload just these three files from the ZIP's src/ folder into it:
             PHPMailer.php
             SMTP.php
             Exception.php

  That is all the code needs. If PHPMailer is missing entirely the system
  falls back to PHP mail() - deliverability drops, but no unsubscribe
  request is ever lost.


=====================================================================
STEP 3 - CONFIG
=====================================================================
  Upload config.php to  /home/<cpanel-user>/private/  and edit:

    db.name / db.user / db.pass    from step 1
    mail.notify_to                 your shared mailbox or staff addresses
    mail.from                      a REAL mailbox on iron.africa
    admin_key                      long random string - see the warning below

  ** admin_key is the only thing protecting your entire client list.
     Generate something long and random. Do not leave it as CHANGE_ME. **


=====================================================================
STEP 4 - MICROSOFT 365 / MAIL TRANSPORT
=====================================================================
  Read this before choosing. It affects whether the system still works
  in 2027.

  ---------------------------------------------------------------
  RECOMMENDED:  transport = 'cpanel'   (the shipped default)
  ---------------------------------------------------------------
  Sends through this server's own mail system, then out to Microsoft
  365 like any normal internet mail. No credentials in any file, and
  nothing that Microsoft is deprecating.

  Two things to set:

  a) cPanel -> Email Routing -> iron.africa -> REMOTE MAIL EXCHANGER
     Your MX points at Microsoft. If cPanel is left on "Local", it
     decides mail for iron.africa is its own, delivers it to a local
     mailbox on the web server, and it NEVER reaches Outlook. The
     script reports success and the email silently vanishes. This is
     the single most common cause of "the form works but no email
     arrives".

  b) Add the server to SPF so Microsoft does not treat it as a forgery:
     cPanel -> Zone Editor -> TXT record for iron.africa

         v=spf1 include:spf.protection.outlook.com +a +mx ~all

     (+a +mx authorises this web server. Keep the Microsoft include.)

  ---------------------------------------------------------------
  ALTERNATIVE:  transport = 'm365'
  ---------------------------------------------------------------
  Authenticates straight to Microsoft. Works today, but has a
  deadline - see the warning at the end of this file.

  a) Enable SMTP AUTH for the sending mailbox:
       M365 Admin -> Users -> Active users -> website@iron.africa
       -> Mail tab -> Manage email apps -> tick "Authenticated SMTP" -> Save

  b) Create an app password (requires MFA on that account):
       myaccount.microsoft.com -> Security info -> Add sign-in method
       -> App password.  Copy it once; it is never shown again.

  c) In config.php:
       'transport' => 'm365',
       'm365_user' => 'website@iron.africa',
       'm365_pass' => '<the app password>',

  If it fails with "5.7.139 Authentication unsuccessful", SMTP AUTH is
  still disabled at tenant level - Security Defaults usually blocks it.


=====================================================================
STEP 5 - LOAD YOUR LIST
=====================================================================
  Visit:
      https://www.iron.africa/admin/import.php?key=YOUR_ADMIN_KEY

  Paste one per line:
      ada@clientfirm.com, Ada Obi, Client Firm Ltd
      tunde@another.com, Tunde Bello, Another Ltd

  Each new address gets a unique 64-character token. Re-running the
  import later is safe: existing people keep their token and status,
  and anyone who already unsubscribed STAYS unsubscribed.


=====================================================================
STEP 6 - SEND THE NEWSLETTER
=====================================================================
  Download the merge file:
      https://www.iron.africa/admin/export-links.php?key=YOUR_ADMIN_KEY

  Columns: Email, Name, Company, UnsubscribeLink
  Only currently-subscribed people are included, so nobody who has
  opted out can be mailed again by accident.

  In Word: Mailings -> Select Recipients -> Use an Existing List ->
  pick that CSV. Insert the link with Ctrl+F9 to make the braces:

      { HYPERLINK "{ MERGEFIELD UnsubscribeLink }" }

  Footer:
      <p style="font-size:12px;color:#777;text-align:center;line-height:1.6;">
        You are receiving this because you are a client or contact of IRON.<br>
        <a href="{ MERGEFIELD UnsubscribeLink }" style="color:#777;">Unsubscribe</a>
        &nbsp;|&nbsp; IRON, [Street Address], Lagos, Nigeria
      </p>


=====================================================================
STEP 7 - OUTLOOK
=====================================================================
  Outlook on the web (Business Basic is web-only):
      Settings (gear) -> Mail -> Rules -> + Add new rule
        Name:      Unsubscribe requests
        Condition: Subject includes -> UNSUBSCRIBE
        Action:    Move to -> Unsubscribe Requests
        Action:    Mark as important
        Save

  A shared mailbox is better than a distribution list here: staff can
  see who has already dealt with a request, and - importantly -
  distribution lists REJECT unauthenticated external senders by
  default, which is exactly what your web server looks like. If you do
  use a list, untick "Require senders to be authenticated" on it.

  The notification tells you whether the database was updated:

      Matched: yes (id 42) - database updated
      Matched: NO - not found in subscribers, remove manually

  "NO" means someone reached the page without a valid token. Handle
  those by hand.


=====================================================================
WHAT HAPPENS ON CLICK
=====================================================================
  1. Client opens  unsubscribe.html?t=<token>
  2. Page asks token.php who that is, shows their address
  3. They choose "reduce to monthly" or "unsubscribe", optionally a reason
  4. POST to unsubscribe.php
  5. subscribers.status  ->  'monthly' or 'unsubscribed'
  6. A row is written to unsubscribe_events (permanent audit trail)
  7. Staff notification goes out
  8. A line is appended to private/unsubscribes.csv as a backup
  9. Client sees the confirmation

  If the database or mail server is down the client STILL sees success
  and the request is still logged. Never dead-end someone trying to
  leave a mailing list.


=====================================================================
WHY TOKENS INSTEAD OF ?email=
=====================================================================
  - Nobody can unsubscribe anyone else by guessing addresses. With
    ?email= links, one person with your client list could opt out your
    entire readership.
  - The address never appears in a URL, so it stays out of server
    access logs, proxies and Referer headers.
  - You know exactly who acted without asking them to type anything.


=====================================================================
CHECKING THE RESULTS
=====================================================================
  phpMyAdmin -> SQL:

    SELECT * FROM v_suppression ORDER BY unsubscribed_at DESC;
    SELECT * FROM v_active;
    SELECT reason, COUNT(*) n FROM subscribers
      WHERE status <> 'subscribed' GROUP BY reason ORDER BY n DESC;

  The last one tells you WHY people leave, which is the whole point of
  asking.


=====================================================================
SECURITY NOTES
=====================================================================
  - config.php and PHPMailer sit above public_html and cannot be
    fetched from the web.
  - private/unsubscribes.csv is above public_html too. Never move it
    inside - that publishes your client list at a guessable URL, which
    is a breach under NDPR.
  - All database access uses real prepared statements
    (PDO::ATTR_EMULATE_PREPARES => false).
  - CR/LF is stripped before anything reaches a mail header, so the
    script cannot be turned into an open relay.
  - PHP notices are logged, never printed, so nothing leaks server
    paths into a response.
  - admin/ pages need ?key=<admin_key>, compared with hash_equals().
    For a second lock, use cPanel -> Directory Privacy on that folder
    and uncomment the block in admin/.htaccess.


=====================================================================
DEADLINE YOU SHOULD KNOW ABOUT
=====================================================================
  Microsoft is retiring SMTP AUTH basic authentication in Exchange
  Online:

     End of Dec 2026  disabled by default on existing tenants
                      (an admin can still switch it back on)
     After Dec 2026   unavailable by default on new tenants
     H2 2027          final removal date to be announced

  App passwords depend on basic auth, so transport = 'm365' as
  configured here stops working on that timeline.

  transport = 'cpanel' - the default - is not affected, which is why
  it is the recommendation. If you do move to 'm365', plan on
  migrating to OAuth2 before the end of 2026.
