Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow "custom_subject" to be at the beginning of the subject (#1811) #1817

Merged
merged 3 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 103 additions & 96 deletions src/lib/Sympa/Spindle/TransformIncoming.pm
Original file line number Diff line number Diff line change
Expand Up @@ -95,108 +95,115 @@ sub _twist {
}

# Add Custom Subject

my $parsed_tag;
if ($list->{'admin'}{'custom_subject'}) {
if (($list->{'admin'}{'custom_subject'} // '') =~ /\S/) {
my $custom_subject = $list->{'admin'}{'custom_subject'};
my $tag;

# Check if custom_subject parameter is parsable.
my $data = {
list => {
name => $list->{'name'},
sequence => $message->{xsequence},
},
};
# Check if custom_subject parameter is parsable and parsed tag contains
# non-space character.
my $template = Sympa::Template->new(undef);
unless ($template->parse($data, [$custom_subject], \$parsed_tag)) {
unless (
$template->parse(
{ list => {
name => $list->{'name'},
sequence => $message->{xsequence},
},
},
[$custom_subject],
\$tag
)
) {
$log->syslog('err', 'Can\'t parse custom_subject of list %s: %s',
$list, $template->{last_error});

undef $parsed_tag;
}
}
if ($list->{'admin'}{'custom_subject'} and defined $parsed_tag) {
my $subject_field = $message->{'decoded_subject'};
$subject_field = '' unless defined $subject_field;
## Remove leading and trailing blanks
$subject_field =~ s/^\s*(.*)\s*$/$1/;

## Search previous subject tagging in Subject
my $custom_subject = $list->{'admin'}{'custom_subject'};

## tag_regexp will be used to remove the custom subject if it is
## already present in the message subject.
## Remember that the value of custom_subject can be
## "dude number [%list.sequence"%]" whereas the actual subject will
## contain "dude number 42".
my $list_name_escaped = $list->{'name'};
$list_name_escaped =~ s/(\W)/\\$1/g;
my $tag_regexp = $custom_subject;
## cleanup, just in case dangerous chars were left
$tag_regexp =~ s/([^\w\s\x80-\xFF])/\\$1/g;
## Replaces "[%list.sequence%]" by "\d+"
$tag_regexp =~ s/\\\[\\\%\s*list\\\.sequence\s*\\\%\\\]/\\d+/g;
## Replace "[%list.name%]" by escaped list name
$tag_regexp =~
s/\\\[\\\%\s*list\\\.name\s*\\\%\\\]/$list_name_escaped/g;
## Replaces variables declarations by "[^\]]+"
$tag_regexp =~ s/\\\[\\\%\s*[^]]+\s*\\\%\\\]/[^]]+/g;
## Takes spaces into account
$tag_regexp =~ s/\s+/\\s+/g;

# Add subject tag

## If subject is tagged, replace it with new tag
## Splitting the subject in two parts :
## - what will be before the custom subject (probably some "Re:")
## - what will be after it : the original subject sent to the list.
## The custom subject is not kept.
my $before_tag;
my $after_tag;
if ($custom_subject =~ /\S/) {
$subject_field =~ s/\s*\[$tag_regexp\]\s*/ /;
}
$subject_field =~ s/\s+$//;

# Truncate multiple "Re:" and equivalents.
# Note that Unicode case-ignore match is performed.
my $re_regexp = Sympa::Regexps::re();
$subject_field = Encode::decode_utf8($subject_field);
if ($subject_field =~ s/\A\s*($re_regexp\s*)($re_regexp\s*)*//i) {
$before_tag = Encode::encode_utf8($1);
} else {
$before_tag = '';
} elsif (($tag // '') =~ /\S/) {
my $decoded_subject = $message->{'decoded_subject'} // '';

# Remove the custom subject if it is already present in the
# subject field. The new tag will be added in below.
# Remember that the value of custom_subject can be
# "dude number [%list.sequence%]" whereas the actual subject will
# contain "dude number 42".
# Regexp is derived from the custom_subject parameter:
# - Replaces "[%list.sequence%]" by /\d+/ and remember it.
# - Replaces "[%list.name%]" by escaped list name.
# - Replaces variable interpolation "[% ... %]" by /[^]]+/. FIXME
# - Cleanup, just in case dangerous chars were left.
# - Takes spaces into account.
my $has_sequence;
my $custom_subject_regexp = $custom_subject =~ s{
( \[ % \s* list\.sequence \s* % \] )
| ( \[ % \s* list\.name \s* % \] )
| ( \[ % \s* [^]]+ \s* % \] )
| ( [^\w\s\x80-\xFF] )
| \s+
}{
if ($1) {
$has_sequence = 1;
"\\d+";
} elsif ($2) {
$list->{'name'} =~ s/(\W)/\\$1/gr;
} elsif ($3) {
'[^]]+';
} elsif ($4) {
"\\" . $4;
} else {
"\\s+";
}
}egrx;
$decoded_subject =~ s/\s*\[$custom_subject_regexp\]\s*/ /;

$decoded_subject =~ s/\A\s+//;
$decoded_subject =~ s/\s+\z//;

# Add subject tag.
# Splitting the subject in two parts :
# - what will be prepended to the subject (probably some "Re:").
# - what will be after it : the original subject.
# Multiple "Re:" and equivalents will be truncated.
# Then, they are rejoined as:
# - if the tag has seq number, "[tag] Re: Stem" (6.2.74 and later)
# - otherwise, "Re: [tag] Stem".
my $subject_field;
my $re_regexp = Sympa::Regexps::re();

if ($message->{'subject_charset'}) {
# Note that Unicode case-ignore match is performed.
my $uStem =
Encode::decode_utf8($decoded_subject) =~
s/\A\s*($re_regexp\s*)($re_regexp\s*)*//ir;
my $uRe = $1 // '';

# Encode subject using initial charset.
my $uTag = Encode::decode_utf8(sprintf '[%s]', $tag);
$subject_field = MIME::EncWords::encode_mimewords(
$has_sequence
? join(' ', $uTag, $uRe . $uStem)
: join(' ', $uRe . $uTag, $uStem),
Charset => $message->{'subject_charset'},
Field => 'Subject',
Replacement => 'FALLBACK'
);
} else {
my $stem = $decoded_subject =~
s/\A\s*($re_regexp\s*)($re_regexp\s*)*//ir;
my $re = ($1 // '') =~ s/\s+\z//r;

# Don't try to encode the original subject if it was not
# originally encoded.
my $eTag = MIME::EncWords::encode_mimewords(
Encode::decode_utf8(sprintf '[%s]', $tag),
Charset => Conf::lang2charset($language->get_lang),
Field => 'Subject'
);
$subject_field =
$has_sequence
? join(' ', grep {length} $eTag, $re, $stem)
: join(' ', grep {length} $re, $eTag, $stem);
}

$message->delete_header('Subject');
$message->add_header('Subject', $subject_field);
}
$after_tag = Encode::encode_utf8($subject_field);

## Encode subject using initial charset

## Don't try to encode the subject if it was not originally encoded.
if ($message->{'subject_charset'}) {
$subject_field = MIME::EncWords::encode_mimewords(
Encode::decode_utf8(
$before_tag . '[' . $parsed_tag . '] ' . $after_tag
),
Charset => $message->{'subject_charset'},
Encoding => 'A',
Field => 'Subject',
Replacement => 'FALLBACK'
);
} else {
$subject_field =
$before_tag . ' '
. MIME::EncWords::encode_mimewords(
Encode::decode_utf8('[' . $parsed_tag . ']'),
Charset => Conf::lang2charset($language->get_lang),
Encoding => 'A',
Field => 'Subject'
)
. ' '
. $after_tag;
}

$message->delete_header('Subject');
$message->add_header('Subject', $subject_field);
}

## Prepare tracking if list config allow it
Expand Down
98 changes: 89 additions & 9 deletions t/Spindle_TransformIncoming.t
Original file line number Diff line number Diff line change
Expand Up @@ -27,31 +27,84 @@ foreach my $pinfo (grep { $_->{name} and exists $_->{default} }
unless exists $Conf::Conf{$pinfo->{name}};
}

my $list = bless {
my $list;
my $spindle;
my $message;

# custom_subject and subject prefixes (with [%list.sequence%])

$list = bless {
name => 'list',
domain => $Conf::Conf{'domain'},
admin => {custom_subject => '[%list.name%]:[%list.sequence%]',},
} => 'Sympa::List';

my $spindle = Sympa::Spindle::TransformIncoming->new(
$spindle = Sympa::Spindle::TransformIncoming->new(
context => $list,
splicing_to => 'Sympa::Spindle',
);

my $message;

# custom_subject and subject prefixes

$message = Sympa::Message->new("Subject: Re: Re: Test\n\n", context => $list);
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: Re: [list:1] Test\n\n", 'Re: Re:';
is $message->as_string, "Subject: [list:1] Re: Test\n\n",
'Re: Re: (with list.sequence)';

$message = Sympa::Message->new("Subject: Re:[list:979] Re: Test\n\n",
context => $list);
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: Re: [list:1] Test\n\n", 'Re:[tag] Re:';
is $message->as_string, "Subject: [list:1] Re: Test\n\n",
'Re:[tag] Re: (with list.sequence)';

$message =
Sympa::Message->new("Subject: [list:979] Re: Test\n\n", context => $list);
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: [list:1] Re: Test\n\n",
'[tag] Re: (with list.sequence)';

$message = Sympa::Message->new(<<'EOF', context => $list);
Subject: =?UTF-8?B?U1Y6IEFudHc6IFZTOiBSRUYgOiBSRTogUkVbMl06IEFXOiDOkc6gOiA=?=
=?UTF-8?B?zpHPgDogzqPOp86VzqQ6IM6jz4fOtc+EOiDQndCwOiDQvdCwOiBWw6E6IFI6IFJJ?=
=?UTF-8?B?RjogQXRiLjogUkVTOiBPZHA6IFludDogQVRCOiDlm57lpI06IOWbnuimhu+8mlZT?=
=?UTF-8?Q?=3a_Fwd=3a_Re=3a_Something?=

EOF
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: [list:1] SV: Fwd: Re: Something\n\n",
'Multilingual "Re:" (with list.sequence)';

# custom_subject and subject prefixes (without [%list.sequence%])

$list = bless {
name => 'list',
domain => $Conf::Conf{'domain'},
admin => {custom_subject => '[%list.name%]',},
} => 'Sympa::List';

$spindle = Sympa::Spindle::TransformIncoming->new(
context => $list,
splicing_to => 'Sympa::Spindle',
);

$message = Sympa::Message->new("Subject: Re: Re: Test\n\n", context => $list);
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: Re: [list] Test\n\n", 'Re: Re:';

$message =
Sympa::Message->new("Subject: Re:[list] Re: Test\n\n", context => $list);
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: Re: [list] Test\n\n", 'Re:[tag] Re:';

$message =
Sympa::Message->new("Subject: [list] Re: Test\n\n", context => $list);
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: Re: [list] Test\n\n", '[tag] Re:';

$message = Sympa::Message->new(<<'EOF', context => $list);
Subject: =?UTF-8?B?U1Y6IEFudHc6IFZTOiBSRUYgOiBSRTogUkVbMl06IEFXOiDOkc6gOiA=?=
Expand All @@ -62,9 +115,36 @@ Subject: =?UTF-8?B?U1Y6IEFudHc6IFZTOiBSRUYgOiBSRTogUkVbMl06IEFXOiDOkc6gOiA=?=
EOF
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: SV: [list:1] Fwd: Re: Something\n\n",
is $message->as_string, "Subject: SV: [list] Fwd: Re: Something\n\n",
'Multilingual "Re:"';

# Empty tmplate

$list->{admin}{custom_subject} = ' ';

$message = Sympa::Message->new("Subject: Re: Re: Test\n\n", context => $list);
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: Re: Re: Test\n\n", 'empty template';

# Error in tmplate

$list->{admin}{custom_subject} = '[%list.name%%]';

$message = Sympa::Message->new("Subject: Re: Re: Test\n\n", context => $list);
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: Re: Re: Test\n\n", 'error in template';

# Empty tag

$list->{admin}{custom_subject} = ' [%# Nothing %] ';

$message = Sympa::Message->new("Subject: Re: Re: Test\n\n", context => $list);
$spindle->{distaff} = Sympa::Spool::Mock->new(message => $message);
$spindle->spin;
is $message->as_string, "Subject: Re: Re: Test\n\n", 'tag parsed to be empty';

done_testing;

package Sympa::Spool::Mock;
Expand Down
Loading