#!/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; 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); } use Getopt::Long; our $verbose = 0; GetOptions("verbose" => \$verbose, "help" => sub {usage}) or exit 1; use FP::List; use FP::IOStream qw(xfile_replace_lines xfile_chars); use FP::Ops qw(regex_match); use FP::Predicates qw(complement either); use Chj::chompspace; use Chj::TEST; # 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: \t\n" } ''; 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: To: Heiri Hunten Bcc: some@where.com message-id: 1235@meh user-agent: tester Hi There! All good? ' )->strings_join("") } 'Return-Path: BCC: , some@where.com from: To: Heiri Hunten 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 }; };