Last active
December 7, 2025 12:17
-
-
Save ernstki/c1408d1f938276f36b77047a4835a3de to your computer and use it in GitHub Desktop.
Convert Microsoft Outlook "safelinks" URL back into human-readable URLs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env perl | |
| ## | |
| ## Unwrap Outlook "safelinks" URLs passed on stdin or as arguments | |
| ## | |
| ## Author: Kevin Ernst <[email protected]> | |
| ## Date: 20 December 2024; updated 6 December 2025 | |
| ## License: ISC or WTFPL, at your option | |
| ## Homepage: https://gist.github.com/ernstki/c1408d1f938276f36b77047a4835a3de | |
| ## Bugs: Won't handle quoted printable mails or links broken across lines | |
| ## | |
| ## Usage: symlink to `unsafelinks` and make executable, then pass | |
| ## "safe" links in on standard in, or as arguments, —or— | |
| ## `use` as a module from your own code | |
| ## | |
| use v5.24; | |
| use strict; | |
| use warnings; | |
| package Unsafelinks; | |
| use URI; | |
| use URI::Escape; | |
| use Data::Dumper; | |
| use Exporter qw(import); | |
| our $VERSION = 1.00; | |
| our @EXPORT = qw( unsafeurl unsafe ); | |
| # surely this'll break some day, but for now they all end with 'reserved=0' | |
| our $URLRE = qr{https://[^\s]+safelinks[^\s]+\d}; | |
| sub debug(@) { | |
| my ($package, $script, $lineno) = caller; | |
| print STDERR "[DEBUG:$package:$lineno] ", @_, "\n" if $ENV{DEBUG}; | |
| } | |
| sub unsafeurl($) { | |
| my $url = shift or die "Expected a (non-empty) URL"; | |
| my $qs = URI->new($url)->query; | |
| debug "parsing query string '$qs'"; | |
| my $qsargs = {map { split /=/ } split /&/, $qs}; | |
| debug "query string args: ", Dumper $qsargs; | |
| return exists $qsargs->{url} ? uri_unescape($qsargs->{url}) : $url; | |
| } | |
| # runs unsafeurl on things that look like URLs, passes the rest | |
| sub unsafe(@) { | |
| foreach (@_) { | |
| #s/(^<|>\$)//g; | |
| if (/($URLRE)/) { | |
| debug "URL regex match: $1"; | |
| s/($URLRE)/unsafeurl($1)/ge; | |
| } | |
| print; | |
| } | |
| } | |
| package main; | |
| use IO::Interactive qw(is_interactive); | |
| sub main { | |
| my @urls; | |
| if (!is_interactive) { | |
| local $/; | |
| @ARGV = <STDIN>; | |
| } | |
| foreach (@ARGV) { | |
| chomp; | |
| say Unsafelinks::unsafeurl $_ for @ARGV; | |
| } | |
| } | |
| main::main unless caller; | |
| 1; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env perl | |
| use v5.24; | |
| use utf8; | |
| use strict; | |
| use warnings; | |
| # ref: https://stackoverflow.com/a/47946606 | |
| use open qw(:std :utf8); | |
| use lib '.'; | |
| use Unsafelinks; | |
| my $url = 'https://nam11.safelinks.protection.outlook.com/?url=https%3A%2F%2Fresearch.uc.edu&data=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&sdata=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&reserved=0'; | |
| say unsafeurl($url); | |
| # *only* prints the URL | |
| say unsafeurl "Visit the Office of Research at: $url"; | |
| # preserves other inline text | |
| say unsafe "Visit the Office of Research at $url!"; | |
| say unsafe "Visit the Office of Research ($url) today."; | |
| say unsafe "Visit the Office of Research at $url--today!"; | |
| say unsafe "Visit the Office of Research at $url… today!"; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment