# -*- perl -*-
=pod
=head1 SUMMARY
mailman_mimedefang_fix_footer - mimedefang filter code for moving a
mailman footer from a separate attachment into the text and HTML body
parts of the message.
=head1 DESCRIPTION
GNU Mailman L is the most popular free software
package for managing electronic mailing lists. While it is quite good
at what it does, there is one functional deficiency that people
stumble over frequently: it doesn't handle message footers well for
multipart MIME messages.
Nowadays, most mail programs generate email messages containing both
text and HTML versions of their content. When a Mailman list is
configured to append a footer to outgoing messages and it encounters a
message with both text and HTML content versions, it adds the footer
as a separate section of the message entirely. Some mail programs
display the resulting footer properly, but others do not, with the
result that users may not see the footer as desired by the list
administrators. This problem is discussed in the Mailman FAQ at
L, which
essentially says that the maintainers of Mailman consider this problem
to be unsolveable.
Not everyone is convinced, and some of the dissenters have put their
money where their mouth is. For example, see
L,
which describes enhancements to Mailman implemented at one site to
allow separate text and HTML headers to be configured and appended
directly to the text and HTML content versions of outgoing messages.
Unfortunately, the maintainers of Mailman rejected these enhancements,
and they are obsolete, i.e., incompatible with the current stable
version of Mailman.
Others have pointed out that the popular mimedefang
L free software package also knows how to add
footers to text and HTML message parts, so if mimedefang can do it,
then Mailman should be able to as well.
It is also frequently mentioned that mimedefang can be used in
conjunction with Mailman to implement a workaround for this problem,
but it is not straightforward, and it seems like no one who mentions
this solution ever explains exactly how to do it. Until now, that
is... This script can be used to embed Mailman message footers
directly in the text and HTML content versions of your outgoing
messages. It does not require any code changes to Mailman; it can
used with standard versions of Mailman and mimedefang. Read on to
find out exactly how.
=head1 USAGE
Usage is simple:
=over
=item 1. Install and configure Mailman (consult Mailman documentation
for details).
=item 2. Install and configure mimedefang normally (consult mimedefang
documentation for details). This script is tested with mimedefang
version 2.70.
=item 3. Add the code below (everything after "=cut") at the end of
/etc/mail/mimedefang-filter, or wherever the mimedefang filter script
lives on your system, right before the final "1;".
=item 4. Add "&mailman_footer_munge($entity);" to the filter_end
function, right after "return if message_rejected();".
=item 5. For the Mailman lists for which you want to use this, change
the "msg_footer" setting on the "Non-digest options" configuration
page so that it starts and ends with a line containing only
"MAILMAN_MIMEDEFANG". That is, add lines containing that string to the
beginning and end of the setting, in addition to the footer text
that's already there. If you don't want any footer at all, just put
two "MAILMAN_MIMEDEFANG" lines, one right after the other, in the
header.
=item 6. Reload your mimedefang configuration.
=item 7. Send an email message and check your mail syslog to make sure
you didn't introduce a syntax error which is breaking mail delivery.
=back
That's it! The footers generated by Mailman for outgoing messages to
the lists you've reconfigured will now be detected by mimedefang and
moved into the text and HTML body parts of the messages.
=head2 Word wrapping
If you want the text version of the footer to be word-wrapped
automatically, put a space and then "WRAP" on the first line of the
footer, after "MAILMAN_MIMEDEFANG".
=head2 Fixed-width HTML
If you want the HTML version of the footer to be displayed in a
fixed-width font, put a space and then "FIXED_WIDTH" on the first line
of the footer, after "MAILMAN_MIMEDEFANG".
You can do "MAILMAN_MIMEDEFANG WRAP FIXED_WIDTH" to get both of these
features.
=head1 COPYRIGHT
Copyright (c) 2010-2011 Jonathan Kamens.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or (at
your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
See L.
=head1 CONTACT
I love to hear from people who are using my software, so please email
me and let me know! Please also email me if you have
any questions, suggestions, bug reports, fixes or enhancements to
share.
This is version $Revision: 1.16 $ of this script. The current version can
always be downloaded from
L.
This Documentation is available on-line at
L.
=head1 CHANGES
=over
=item December 14, 2011 -- Clarify how to format the footer setting in
the mailman list configuration. Update the copyright to include
2011. Update the link to my email address in the documentation.
=item December 6, 2011 -- Clarify in the documentation where exactly
to add the code to filter_end.
=item November 4, 2011 -- If the message is multipart and has only one
part, then work with that one part rather than with the entire
message. This is necessary became mimedefang has a nasty habit of
turning text/plain messages into multipart/mixed messages.
=item June 14, 2010 -- Fixed to properly handle mailing list messages
with attachments.
=item May 24, 2010 -- Initial release
=back
=cut
use MIME::Body;
use MIME::Parser;
use Text::Wrap;
# Uncomment the next line for debugging, in which case you use this
# file as a standalone filter script as follows: feed a message on
# STDIN and it will print the possibly reformatted message on stdout.
# Note that in this case the message may not look *exactly* like it
# will look when mimedefang is done with it, but it'll be pretty
# close.
#$standalone_debug = 1;
if ($standalone_debug) {
$parser = new MIME::Parser;
$parser->output_under("/tmp");
$entity = $parser->parse(\*STDIN);
&mailman_footer_munge($entity);
print $entity->as_string, "\n";
}
sub mailman_footer_munge {
my($top) = @_;
my $entity = $top;
md_syslog('debug', 'mailman_footer_munge: start');
if ($entity->is_multipart and $entity->parts == 1) {
$entity = $entity->parts(0);
md_syslog('debug', 'mailman_footer_munge: multipart with one part');
}
md_syslog('debug', 'mailman_footer_munge: type=' . $entity->effective_type);
if ($entity->effective_type eq 'text/plain') {
my $body = $entity->bodyhandle->as_string;
return if ($body !~ s/\n[ \t]*MAILMAN_MIMEDEFANG\s*$/\n/);
return if ($body !~ s/\n[ \t]*MAILMAN_MIMEDEFANG((?:\s+(?:FIXED_WIDTH|WRAP))+)?[ \t]*\n((?:.*\n)*)$/\n/);
my $footer = $2;
$entity->bodyhandle(new MIME::Body::InCore([$body . $footer]));
if (! $standalone_debug) {
replace_entire_message($top);
}
return;
}
return if ($entity->effective_type ne 'multipart/mixed');
return if ($entity->parts < 2);
my $last = $entity->parts($entity->parts-1);
return if ($last->effective_type ne 'text/plain');
my $footer_bodyhandle = $last->bodyhandle;
return if (! $footer_bodyhandle);
my $footer = $footer_bodyhandle->as_string;
return if ($footer !~ s/^[ \t]*MAILMAN_MIMEDEFANG((?:[ \t]+(?:FIXED_WIDTH|WRAP))+)?[ \t]*\n//);
my $args = $1;
return if ($footer !~ s/\n[ \t]*MAILMAN_MIMEDEFANG\s*$//);
$args =~ s/^\s+//;
$args = lc $args;
my %args;
map($args{$_}++, split(' ', $args));
my $html_footer = $footer;
$html_footer =~ s/\&/\&/g;
$html_footer =~ s/\\</g;
$html_footer =~ s/\>/\>/g;
$html_footer =~ s,\n,
,g;
if ($args{'fixed_width'}) {
$html_footer = '' . $html_footer . '';
}
$html_footer = "" . $html_footer . "
";
if ($args{'wrap'}) {
$footer = wrap('', '', $footer);
}
if ($entity->parts == 2) {
my $inner_head = $entity->parts(0)->head;
my %mime_tags;
foreach my $tag ($inner_head->tags) {
if ($tag =~ /^content-/i) {
$mime_tags{$tag} = $inner_head->get($tag);
}
}
if ($entity->parts(0)->is_multipart) {
$entity->preamble($entity->parts(0)->preamble);
$entity->epilogue($entity->parts(0)->epilogue);
$entity->parts([$entity->parts(0)->parts]);
map {
$entity->head->delete($_);
$entity->head->add($_, $mime_tags{$_});
} keys %mime_tags;
}
else {
my $inner_bodyhandle = $entity->parts(0)->bodyhandle;
$entity->parts([]);
map {
$entity->head->delete($_);
$entity->head->add($_, $mime_tags{$_});
} keys %mime_tags;
$entity->bodyhandle($inner_bodyhandle);
}
}
else {
my(@parts) = $entity->parts;
pop @parts;
$entity->parts(\@parts);
}
if ($standalone_debug) {
my($did_text, $did_html);
my(@parts) = $entity->parts;
while (my $part = shift @parts) {
if ($part->is_multipart) {
unshift(@parts, $part->parts);
next;
}
if (! $did_text and $part->effective_type eq 'text/plain') {
$part->bodyhandle(new MIME::Body::InCore([$part->bodyhandle->as_string . $footer]));
last if ($did_html);
$did_text++;
}
if (! $did_html and $part->effective_type eq 'text/html') {
$part->bodyhandle(new MIME::Body::InCore([$part->bodyhandle->as_string . $html_footer]));
last if ($did_text);
$did_html++;
}
}
}
else {
append_text_boilerplate($entity, $footer, 0);
append_html_boilerplate($entity, $html_footer, 0);
replace_entire_message($top);
}
}