added on local at 2026-07-01 12:35:16
| 1 | #!/usr/bin/perl | |
| 2 | ||
| 3 | ################################################################################################################################## | |
| 4 | # ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____ | |
| 5 | # / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \ | |
| 6 | # / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / / | |
| 7 | # / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ / | |
| 8 | # \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/ | |
| 9 | # | |
| 10 | # - Written by: Shawn Pickering | |
| 11 | # - shawn@shawnpickering.com | |
| 12 | ################################################################################################################################## | |
| 13 | use strict; | |
| 14 | use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com'; | |
| 15 | ||
| 16 | ||
| 17 | ############################################################# | |
| 18 | ## Variables | |
| 19 | ############################################################# | |
| 20 | ||
| 21 | ||
| 22 | ||
| 23 | ############################################################ | |
| 24 | # Modules | |
| 25 | ############################################################ | |
| 26 | use CGI; my $query = new CGI; my $form = $query->Vars; | |
| 27 | use MODS::PTMatrix::Urls; my $getlist = new MODS::PTMatrix::Urls; my $url = $getlist->urls(); | |
| 28 | use MODS::DBConnect; my $db = new MODS::DBConnect; | |
| 29 | use MODS::PTMatrix::Config; my $config = new MODS::PTMatrix::Config; my $database_name = $config->settings('database_name'); | |
| 30 | ||
| 31 | ||
| 32 | ||
| 33 | ############################################################# | |
| 34 | ## Get form data | |
| 35 | ############################################################# | |
| 36 | foreach my $line(keys %$form){ | |
| 37 | $form->{$line} =~ s/[^!-~\s]//g; | |
| 38 | $form->{$line} = quotemeta("$form->{$line}"); | |
| 39 | } | |
| 40 | ||
| 41 | ||
| 42 | ||
| 43 | ############################################################ | |
| 44 | # Program Controls | |
| 45 | ############################################################ | |
| 46 | $|=1; | |
| 47 | &unsub_handler; | |
| 48 | ||
| 49 | ||
| 50 | ############################################################ | |
| 51 | # Subroutines | |
| 52 | ############################################################ | |
| 53 | sub unsub_handler{ | |
| 54 | #One-click unsubscribe from email footer. Resolves the token to a user, | |
| 55 | #either unsubs a single category (?cat=lifecycle) or all (?unsub_all=1). | |
| 56 | #Always responds with a public confirmation page even on bad token (so we | |
| 57 | #don't leak whether the token was real). | |
| 58 | ||
| 59 | my $tok = $form->{t} || ''; $tok =~ s/[^a-zA-Z0-9]//g; | |
| 60 | my $cat = $form->{cat} || ''; $cat =~ s/[^a-z]//g; | |
| 61 | my $all = ($form->{unsub_all} || '') eq '1' ? 1 : 0; | |
| 62 | ||
| 63 | my $dbh = $db->db_connect; | |
| 64 | ||
| 65 | #Resolve token -> user | |
| 66 | my $user_id = 0; | |
| 67 | my $email = ''; | |
| 68 | if(length($tok) >= 12){ | |
| 69 | my $sql_r = qq~SELECT t.user_id, u.email FROM `${database_name}`.email_unsubscribe_tokens t JOIN `${database_name}`.users u ON u.id = t.user_id WHERE t.token='$tok' LIMIT 1~; | |
| 70 | my $r = $db->db_readwrite($dbh, $sql_r, $0, __LINE__); | |
| 71 | if($r && $r->{user_id}){ | |
| 72 | $user_id = $r->{user_id} + 0; | |
| 73 | $email = $r->{email} || ''; | |
| 74 | } | |
| 75 | } | |
| 76 | ||
| 77 | #Apply the unsubscribe(s) | |
| 78 | my $changed = 0; | |
| 79 | if($user_id){ | |
| 80 | $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.email_unsubscribe_tokens SET last_used_at=NOW() WHERE token='$tok'~, $0, __LINE__); | |
| 81 | ||
| 82 | my @cats_to_off; | |
| 83 | if($all){ | |
| 84 | @cats_to_off = qw(lifecycle product marketing); | |
| 85 | }elsif($cat =~ /^(lifecycle|product|marketing)$/){ | |
| 86 | @cats_to_off = ($cat); | |
| 87 | } | |
| 88 | foreach my $c(@cats_to_off){ | |
| 89 | my $sql_u = qq~INSERT INTO `${database_name}`.user_email_preferences (user_id, category, is_subscribed) VALUES ('$user_id', '$c', 0) ON DUPLICATE KEY UPDATE is_subscribed=0~; | |
| 90 | $db->db_readwrite($dbh, $sql_u, $0, __LINE__); | |
| 91 | $changed++; | |
| 92 | } | |
| 93 | } | |
| 94 | ||
| 95 | $db->db_disconnect($dbh); | |
| 96 | ||
| 97 | #Render the confirmation page | |
| 98 | print qq~Content-Type: text/html; charset=utf-8\r\nCache-Control: no-store\r\n\r\n~; | |
| 99 | ||
| 100 | my $email_h = $email; | |
| 101 | $email_h =~ s/&/&/g; $email_h =~ s/</</g; $email_h =~ s/>/>/g; | |
| 102 | ||
| 103 | my $msg; | |
| 104 | if(!$user_id){ | |
| 105 | $msg = qq~<h1 style="font-size:24px;color:#fff;margin:0 0 14px">Unsubscribe link expired</h1> | |
| 106 | <p style="color:#cbd5e1;font-size:15px;line-height:1.6">That link is no longer valid. If you're signed in, you can manage your email preferences directly:</p> | |
| 107 | <p style="margin-top:18px"><a href="$url->{email_preferences}" style="display:inline-block;background:#5aa9ff;color:#fff;padding:10px 18px;border-radius:6px;text-decoration:none;font-weight:600">Manage preferences</a></p>~; | |
| 108 | }elsif($all){ | |
| 109 | $msg = qq~<h1 style="font-size:24px;color:#fff;margin:0 0 14px">You're unsubscribed</h1> | |
| 110 | <p style="color:#cbd5e1;font-size:15px;line-height:1.6">We won't send <strong>$email_h</strong> any more lifecycle, product, or marketing emails.</p> | |
| 111 | <p style="color:#94a3b8;font-size:13px;line-height:1.6;margin-top:16px">You'll still get transactional emails (password resets, billing receipts, team invites you accept) because those are essential to using the account.</p> | |
| 112 | <p style="margin-top:18px"><a href="$url->{email_preferences}" style="color:#5aa9ff">Change preferences instead</a></p>~; | |
| 113 | }elsif($changed){ | |
| 114 | $msg = qq~<h1 style="font-size:24px;color:#fff;margin:0 0 14px">Unsubscribed from $cat</h1> | |
| 115 | <p style="color:#cbd5e1;font-size:15px;line-height:1.6">We won't send <strong>$email_h</strong> any more <strong>$cat</strong> emails.</p> | |
| 116 | <p style="margin-top:18px"><a href="$url->{email_preferences}" style="color:#5aa9ff">Manage all preferences</a></p>~; | |
| 117 | }else{ | |
| 118 | $msg = qq~<h1 style="font-size:24px;color:#fff;margin:0 0 14px">Manage email preferences</h1> | |
| 119 | <p style="color:#cbd5e1;font-size:15px;line-height:1.6">No category specified. Use the link below to pick which emails you'd like.</p> | |
| 120 | <p style="margin-top:18px"><a href="$url->{email_preferences}" style="display:inline-block;background:#5aa9ff;color:#fff;padding:10px 18px;border-radius:6px;text-decoration:none;font-weight:600">Manage preferences</a></p>~; | |
| 121 | } | |
| 122 | ||
| 123 | print qq~<!doctype html><html><head><meta charset="utf-8"><title>Unsubscribe · PTMatrix</title> | |
| 124 | <style> | |
| 125 | body{background:#0b0d10;color:#cbd5e1;font-family:system-ui,-apple-system,sans-serif;margin:0;padding:80px 24px;display:flex;justify-content:center} | |
| 126 | .card{max-width:520px;width:100%;background:rgba(20,20,30,.6);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:36px 32px} | |
| 127 | .brand{display:flex;align-items:center;gap:10px;margin-bottom:24px} | |
| 128 | .brand-logo{width:32px;height:32px;border-radius:8px;background:#2c0508;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;border:1.5px solid #fff;font-size:13px} | |
| 129 | </style></head><body><div class="card"> | |
| 130 | <div class="brand"><div class="brand-logo">PT</div><div style="font-weight:700;color:#fff">PTMatrix</div></div> | |
| 131 | $msg | |
| 132 | </div></body></html>~; | |
| 133 | } |