The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

#!/usr/bin/env perl
# Copyright (c) 2015-2021 Christian Jaeger, copying@christianjaeger.ch
# This is free software. See the file COPYING.md that came bundled
# with this file.
use strict;
use warnings FATAL => 'uninitialized';
use experimental "signatures";
# find modules from functional-perl working directory (not installed)
use Cwd 'abs_path';
our ($mydir, $myname);
BEGIN {
my $location = (-l $0) ? abs_path($0) : $0;
$location =~ /(.*?)([^\/]+?)_?\z/s or die "?";
($mydir, $myname) = ($1, $2);
}
use lib "$mydir/../lib";
sub usage {
print STDERR map {"$_\n"} @_ if @_;
print "$myname file(s)
Hack to prepare an outgoing mail file (as saved in a Maildir) for
sending through 'sendmail -t'. It simply removes and adds some mail
headers. It replaces the given file(s) with the modified one(s).
";
exit(@_ ? 1 : 0);
}
our $verbose = 0;
GetOptions("verbose" => \$verbose, "help" => sub {usage}) or exit 1;
use FP::IOStream qw(xfile_replace_lines xfile_chars);
use FP::Ops qw(regex_match);
use FP::Predicates qw(complement either);
# do not warn about failures to keep owner, or backups, due to
# different user than owner
local $Chj::IO::Tempfile::warn_all_failures = 0;
# Split lines into head and body (at the first empty line)
sub lines_head_and_body($lines) {
$lines->take_while_and_rest(complement regex_match qr/^\n\z/s)
}
# Get headers of given name; this ad-hoc mail "parser" unsafely assumes
# that the headers we're interested in consist of one line only
sub head_get ($head, $name) {
$head->filter(regex_match qr/^\Q$name:/i)
}
# Get one of the headers
sub head_perhaps_get_one ($head, $name) {
# XX: what if there are multiple?
head_get($head, $name)->perhaps_one
}
sub head_xgetone ($head, $name) {
my ($header) = head_perhaps_get_one($head, $name)
or die "missing header '$name'";
$header
}
# Extract the value of a header
sub header_value($header) {
$header =~ s/^[^:]+:// or die "header line missing a key: '$header'";
chompspace $header
}
TEST { header_value "From: <foo\@br>\t\n" } '<foo@br>';
sub regex_from_strings {
join("|", map { quotemeta $_ } @_)
}
my $drop_header_name_regex = regex_from_strings(
qw(
Return-Path
BCC
X-K9mail-Identity
User-Agent
Date
Message-ID)
);
sub fixlines($lines) {
my ($head, $body) = lines_head_and_body $lines;
my $from = head_xgetone $head, "From";
my ($fromaddr) = $from =~ m@<([^<>]+)>@s
or die "missing from address in '$from'";
my ($fromwhole) = $from =~ m@.?: *([^\n]*)@s or die "?";
my ($fromdomain) = $fromaddr =~ m{\@(.*)}s or die "?";
my $messageid_uuid = do {
if (my ($messageid) = head_perhaps_get_one($head, "Message-ID")) {
my ($str) = header_value($messageid) =~ m{([^<>@]+)\@}s
or die "no match in messageid '$messageid'";
$str
} else {
xfile_chars("/dev/urandom")->take(16)->map (
sub ($c) {
sprintf '%02x', ord $c
}
)->string
}
};
my $bcc_values = cons $fromwhole,
head_get($head, "BCC")->map(\&header_value);
(
cons "Return-Path: <$fromaddr>\n",
cons "BCC: " . $bcc_values->strings_join(", ") . "\n",
$head->filter_with_tail(
complement(regex_match("(?^i:^$drop_header_name_regex:)")),
cons "Message-ID: <$messageid_uuid\@$fromdomain>\n",
$body
)
)
}
sub string2lines($str) {
list map {"$_\n"} split /\n/, $str
}
TEST {
fixlines(
string2lines 'return-path: Hello
from: <foo@bar.com>
To: Heiri Hunten <heiri.hunten@example.com>
Bcc: some@where.com
message-id: 1235@meh
user-agent: tester
Hi There!
All good?
'
)->strings_join("")
}
'Return-Path: <foo@bar.com>
BCC: <foo@bar.com>, some@where.com
from: <foo@bar.com>
To: Heiri Hunten <heiri.hunten@example.com>
Message-ID: <1235@bar.com>
Hi There!
All good?
';
sub sendprepare($path) {
xfile_replace_lines $path, \&fixlines;
}
$ENV{DEBUG}
? do {
require FP::Repl::Trap;
FP::Repl::repl();
}
: do {
perhaps_run_tests __PACKAGE__ or do { sendprepare $_ for @ARGV };
};