added on WebSTLs (webstls.com) at 2026-07-01 22:27:09
| 1 | #!/usr/bin/perl | |
| 2 | #====================================================================== | |
| 3 | # WebSTLs -- /unsubscribe.cgi?t=TOKEN | |
| 4 | # | |
| 5 | # One-click unsubscribe from email footer. Resolves the token to a user, | |
| 6 | # either unsubscribes a single category (?cat=lifecycle) or all | |
| 7 | # non-essential (?unsub_all=1). | |
| 8 | # | |
| 9 | # Always responds with a public-facing confirmation page, even if the | |
| 10 | # token is invalid (don't leak whether the token was real). | |
| 11 | #====================================================================== | |
| 12 | use strict; | |
| 13 | use warnings; | |
| 14 | ||
| 15 | use lib '/var/www/vhosts/webstls.com/httpdocs'; | |
| 16 | use CGI; | |
| 17 | use MODS::DBConnect; | |
| 18 | use MODS::WebSTLs::Config; | |
| 19 | ||
| 20 | my $q = CGI->new; | |
| 21 | my $form = $q->Vars; | |
| 22 | my $db = MODS::DBConnect->new; | |
| 23 | my $cfg = MODS::WebSTLs::Config->new; | |
| 24 | my $DB = $cfg->settings('database_name'); | |
| 25 | ||
| 26 | my $tok = $form->{t} || ''; | |
| 27 | $tok =~ s/[^a-zA-Z0-9]//g; | |
| 28 | my $cat = $form->{cat} || ''; | |
| 29 | $cat =~ s/[^a-z]//g; | |
| 30 | my $all = ($form->{unsub_all} || '') eq '1' ? 1 : 0; | |
| 31 | ||
| 32 | my $dbh = $db->db_connect; | |
| 33 | my $user_id = 0; | |
| 34 | my $email = ''; | |
| 35 | if (length($tok) >= 12) { | |
| 36 | my $r = $db->db_readwrite($dbh, qq~ | |
| 37 | SELECT t.user_id, u.email | |
| 38 | FROM ${DB}.email_unsubscribe_tokens t | |
| 39 | JOIN ${DB}.users u ON u.id = t.user_id | |
| 40 | WHERE t.token='$tok' LIMIT 1 | |
| 41 | ~, $0, __LINE__); | |
| 42 | if ($r && $r->{user_id}) { | |
| 43 | $user_id = $r->{user_id} + 0; | |
| 44 | $email = $r->{email} || ''; | |
| 45 | } | |
| 46 | } | |
| 47 | ||
| 48 | my $changed = 0; | |
| 49 | if ($user_id) { | |
| 50 | $db->db_readwrite($dbh, qq~ | |
| 51 | UPDATE ${DB}.email_unsubscribe_tokens SET last_used_at=NOW() WHERE token='$tok' | |
| 52 | ~, $0, __LINE__); | |
| 53 | ||
| 54 | my @cats_to_off; | |
| 55 | if ($all) { | |
| 56 | @cats_to_off = qw(lifecycle product marketing); | |
| 57 | } elsif ($cat =~ /^(lifecycle|product|marketing)$/) { | |
| 58 | @cats_to_off = ($cat); | |
| 59 | } | |
| 60 | foreach my $c (@cats_to_off) { | |
| 61 | $db->db_readwrite($dbh, qq~ | |
| 62 | INSERT INTO ${DB}.user_email_preferences (user_id, category, is_subscribed) | |
| 63 | VALUES ('$user_id', '$c', 0) | |
| 64 | ON DUPLICATE KEY UPDATE is_subscribed=0 | |
| 65 | ~, $0, __LINE__); | |
| 66 | $changed++; | |
| 67 | } | |
| 68 | } | |
| 69 | ||
| 70 | $db->db_disconnect($dbh); | |
| 71 | ||
| 72 | print "Content-Type: text/html; charset=utf-8\r\nCache-Control: no-store\r\n\r\n"; | |
| 73 | ||
| 74 | my $email_h = $email; $email_h =~ s/&/&/g; $email_h =~ s/</</g; $email_h =~ s/>/>/g; | |
| 75 | ||
| 76 | my $msg; | |
| 77 | if (!$user_id) { | |
| 78 | $msg = qq~<h1 style="font-size:24px;color:#fff;margin:0 0 14px">Unsubscribe link expired</h1> | |
| 79 | <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> | |
| 80 | <p style="margin-top:18px"><a href="/email_preferences.cgi" style="display:inline-block;background:#5aa9ff;color:#fff;padding:10px 18px;border-radius:6px;text-decoration:none;font-weight:600">Manage preferences</a></p>~; | |
| 81 | } elsif ($all) { | |
| 82 | $msg = qq~<h1 style="font-size:24px;color:#fff;margin:0 0 14px">You're unsubscribed</h1> | |
| 83 | <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> | |
| 84 | <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> | |
| 85 | <p style="margin-top:18px"><a href="/email_preferences.cgi" style="color:#5aa9ff">Change preferences instead</a></p>~; | |
| 86 | } elsif ($changed) { | |
| 87 | $msg = qq~<h1 style="font-size:24px;color:#fff;margin:0 0 14px">Unsubscribed from $cat</h1> | |
| 88 | <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> | |
| 89 | <p style="margin-top:18px"><a href="/email_preferences.cgi" style="color:#5aa9ff">Manage all preferences</a></p>~; | |
| 90 | } else { | |
| 91 | $msg = qq~<h1 style="font-size:24px;color:#fff;margin:0 0 14px">Manage email preferences</h1> | |
| 92 | <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> | |
| 93 | <p style="margin-top:18px"><a href="/email_preferences.cgi" style="display:inline-block;background:#5aa9ff;color:#fff;padding:10px 18px;border-radius:6px;text-decoration:none;font-weight:600">Manage preferences</a></p>~; | |
| 94 | } | |
| 95 | ||
| 96 | print qq~<!doctype html><html><head><meta charset="utf-8"><title>Unsubscribe · WebSTLs</title> | |
| 97 | <style> | |
| 98 | body{background:#0b0d10;color:#cbd5e1;font-family:system-ui,-apple-system,sans-serif;margin:0;padding:80px 24px;display:flex;justify-content:center} | |
| 99 | .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} | |
| 100 | .brand{display:flex;align-items:center;gap:10px;margin-bottom:24px} | |
| 101 | .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} | |
| 102 | </style></head><body><div class="card"> | |
| 103 | <div class="brand"><div class="brand-logo">PT</div><div style="font-weight:700;color:#fff">WebSTLs</div></div> | |
| 104 | $msg | |
| 105 | </div></body></html>~; |