[Rt-commit] rt branch, 4.4/munge-more-attachments, created. rt-4.4.3-203-ge644561d8

? sunnavy sunnavy at bestpractical.com
Fri Jan 25 16:26:33 EST 2019


The branch, 4.4/munge-more-attachments has been created
        at  e644561d85fe1216ca3ede6fd279c3ee29508c01 (commit)

- Log -----------------------------------------------------------------
commit 67d87737c9924065a9a1c2e64aef526c56eeaeed
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jan 24 01:22:24 2019 +0800

    Abstract method to return the list of mime types that indicate email messages

diff --git a/lib/RT/Util.pm b/lib/RT/Util.pm
index 3538c00aa..aaa9d1530 100644
--- a/lib/RT/Util.pm
+++ b/lib/RT/Util.pm
@@ -52,7 +52,7 @@ use warnings;
 
 
 use base 'Exporter';
-our @EXPORT = qw/safe_run_child mime_recommended_filename EntityLooksLikeEmailMessage/;
+our @EXPORT = qw/safe_run_child mime_recommended_filename EntityLooksLikeEmailMessage EmailContentTypes/;
 
 use Encode qw/encode/;
 
@@ -230,16 +230,26 @@ sub EntityLooksLikeEmailMessage {
     # MIME::Parser used.
     my $mime_type = $entity->mime_type();
 
-    # This is the same list of MIME types MIME::Parser uses. The partial and
-    # external-body types are unlikely to produce usable attachments, but they
-    # are still recognized as email for the purposes of this function.
-
-    my @email_types = ('message/rfc822', 'message/partial', 'message/external-body');
+    my @email_types = EmailContentTypes();
 
     return 1 if grep { $mime_type eq $_ } @email_types;
     return 0;
 }
 
+=head2 EmailContentTypes
+
+Return MIME types that indicate email messages.
+
+=cut
+
+sub EmailContentTypes {
+
+    # This is the same list of MIME types MIME::Parser uses. The partial and
+    # external-body types are unlikely to produce usable attachments, but they
+    # are still recognized as email for the purposes of this function.
+    return ( 'message/rfc822', 'message/partial', 'message/external-body' );
+}
+
 RT::Base->_ImportOverlays();
 
 1;

commit a7ac92d730a796037db0207bdbea3189248ad491
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jan 24 01:30:09 2019 +0800

    Refactor ReplaceAttachments method mainly to cover more attachments
    
    Previously we only replaced attachments of "text/plain" and "text/html",
    which is good enough for "Content" replacement but sadly not for
    "Headers", as we missed quite common types like "multipart/mixed" and
    more. This commit imporves "Headers" replacement by covering all the
    types of "multiplart/*" and also embeded email ones like
    "message/rfc822", 'message/partial' and 'message/external-body'.
    
    Code is also refactored to create Munge txns as soon as possible for
    better consistency, and the memory usage is also reduced as we don't
    need to keep all the ticket objects.

diff --git a/lib/RT/Attachments.pm b/lib/RT/Attachments.pm
index 474a369d8..fbbf3fc70 100644
--- a/lib/RT/Attachments.pm
+++ b/lib/RT/Attachments.pm
@@ -271,60 +271,101 @@ sub ReplaceAttachments {
 
     return ( 0, $self->loc('Provide a search string to search on') ) unless $args{Search};
 
-    $self->Limit(
-        ENTRYAGGREGATOR => 'OR',
-        FIELD           => 'Headers',
-        OPERATOR        => 'LIKE',
-        VALUE           => $args{Search},
-        SUBCLAUSE       => 'Attachments',
-    ) if $args{Headers};
-    $self->Limit(
-        ENTRYAGGREGATOR => 'OR',
-        FIELD           => 'Content',
-        OPERATOR        => 'LIKE',
-        VALUE           => $args{Search},
-        SUBCLAUSE       => 'Attachments',
-    ) if $args{Content};
-    $self->Limit(
-        FIELD           => 'ContentType',
-        OPERATOR        => 'IN',
-        VALUE           => ['text/plain', 'text/html'],
+
+    my %munged;
+    my $create_munge_txn = sub {
+        my $ticket = shift;
+        if ( !$munged{ $ticket->id } ) {
+            my ( $ret, $msg ) = $ticket->_NewTransaction( Type => "Munge" );
+            if ($ret) {
+                $munged{ $ticket->id } = 1;
+            }
+            else {
+                RT::Logger->error($msg);
+            }
+        }
+    };
+
+    my $attachments = $self->Clone;
+    $attachments->Limit(
+        FIELD     => 'ContentEncoding',
+        VALUE     => 'none',
+        SUBCLAUSE => 'Encoding',
     );
-    $self->Limit(
+    $attachments->Limit(
         FIELD           => 'ContentEncoding',
-        VALUE           => 'none',
+        OPERATOR        => 'IS',
+        VALUE           => 'NULL',
+        SUBCLAUSE       => 'Encoding',
     );
 
-    my %tickets;
-    my ($ret, $msg);
-    while (my $attachment = $self->Next) {
-        my $content_replaced;
-        if ( $args{Headers} ) {
-            ($ret, $msg) = $attachment->ReplaceHeaders(Search => $args{Search}, Replacement => $args{Replacement});
-            $content_replaced ||= $ret;
+    if ( $args{Headers} ) {
+        my $atts = $attachments->Clone;
+        $atts->Limit(
+            FIELD    => 'Headers',
+            OPERATOR => 'LIKE',
+            VALUE    => $args{Search},
+        );
+        $atts->Limit(
+            FIELD     => 'ContentType',
+            OPERATOR  => 'IN',
+            VALUE     => [ RT::Util::EmailContentTypes(), 'text/plain', 'text/html' ],
+            SUBCLAUSE => 'Types',
+        );
+        $atts->Limit(
+            FIELD           => 'ContentType',
+            OPERATOR        => 'STARTSWITH',
+            VALUE           => 'multipart/',
+            SUBCLAUSE       => 'Types',
+            ENTRYAGGREGATOR => 'OR',
+        );
 
-            RT::Logger->error($msg) unless $ret;
+        while ( my $att = $atts->Next ) {
+            my ( $ret, $msg ) = $att->ReplaceHeaders(
+                Search      => $args{Search},
+                Replacement => $args{Replacement},
+            );
+
+            if ( $ret ) {
+                $create_munge_txn->( $att->TransactionObj->TicketObj );
+            }
+            else {
+                RT::Logger->error($msg);
+            }
         }
+    }
 
-        if ( $args{Content} ) {
-            ($ret, $msg) = $attachment->ReplaceContent(Search => $args{Search}, Replacement => $args{Replacement});
-            $content_replaced ||= $ret;
+    if ( $args{Content} ) {
+        my $atts = $attachments->Clone;
+        $atts->Limit(
+            FIELD     => 'Content',
+            OPERATOR  => 'LIKE',
+            VALUE     => $args{Search},
+            SUBCLAUSE => 'Content',
+        );
+        $atts->Limit(
+            FIELD    => 'ContentType',
+            OPERATOR => 'IN',
+            VALUE    => [ 'text/plain', 'text/html' ],
+        );
 
-            RT::Logger->error($msg) unless $ret;
+        while ( my $att = $atts->Next ) {
+            my ( $ret, $msg ) = $att->ReplaceContent(
+                Search      => $args{Search},
+                Replacement => $args{Replacement},
+            );
+
+            if ( $ret ) {
+                $create_munge_txn->( $att->TransactionObj->TicketObj );
+            }
+            else {
+                RT::Logger->error($msg);
+            }
         }
-
-        my $ticket = $attachment->TransactionObj->TicketObj;
-        $tickets{$ticket->Id} ||= $ticket if $content_replaced;
     }
 
-    foreach my $id ( sort { $a <=> $b } keys %tickets ) {
-        (my $transaction, $msg, my $trans) = $tickets{$id}->_NewTransaction (
-            Type     => "Munge",
-        );
-        RT::Logger->error($msg) unless $transaction;
-    }
-    my $count = scalar keys %tickets;
-    return ( 1, $self->loc( "Updated [_1] ticket's attachment content", $count ) );
+    my $count = scalar keys %munged;
+    return ( 1, $self->loc( "Updated [quant,_1,ticket's,tickets'] attachment content", $count ) );
 }
 
 RT::Base->_ImportOverlays();

commit e65d6dfa4bdd5d6f010f6ba80335590534315e79
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jan 24 03:29:47 2019 +0800

    Munge quoted-printable attachments if the searched string keeps intact in QP

diff --git a/lib/RT/Attachments.pm b/lib/RT/Attachments.pm
index fbbf3fc70..438e4ca34 100644
--- a/lib/RT/Attachments.pm
+++ b/lib/RT/Attachments.pm
@@ -299,6 +299,18 @@ sub ReplaceAttachments {
         SUBCLAUSE       => 'Encoding',
     );
 
+    # For QP encoding, if encoded string is equal to the decoded
+    # version, then SQL search will also work.
+    #
+    # Adding "\n" is to avoid trailing "=" in QP encoding
+    if ( MIME::QuotedPrint::encode("$args{Search}\n") eq "$args{Search}\n" ) {
+        $attachments->Limit(
+            FIELD     => 'ContentEncoding',
+            VALUE     => 'quoted-printable',
+            SUBCLAUSE => 'Encoding',
+        );
+    }
+
     if ( $args{Headers} ) {
         my $atts = $attachments->Clone;
         $atts->Limit(

commit 7eb065e87823d62082f637dfdd229b133f7b2638
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Jan 24 05:12:13 2019 +0800

    Re-encode decoded attachment content on update for content replacement
    
    As we substitute the decoded content, to store it back to db, we need to
    encode it first accordingly.

diff --git a/lib/RT/Attachment.pm b/lib/RT/Attachment.pm
index ed9771e1c..e443020d2 100644
--- a/lib/RT/Attachment.pm
+++ b/lib/RT/Attachment.pm
@@ -800,7 +800,28 @@ sub ReplaceContent {
     my $content = $self->Content;
 
     if ( $content && $content =~ s/\Q$args{Search}\E/$args{Replacement}/ig ) {
-        my ( $ret, $msg ) = $self->_Set( Field => 'Content', Value => $content );
+        my ( $encoding, $encoded_content, undef, undef, $note_args )
+          = $self->_EncodeLOB( Encode::encode( 'UTF-8', $content ) );
+
+        $RT::Handle->BeginTransaction;
+        if ($note_args) {
+            $self->TransactionObj->Object->_NewTransaction(%$note_args);
+        }
+
+        my ( $ret, $msg ) = $self->_Set( Field => 'Content', Value => $encoded_content );
+        unless ($ret) {
+            $RT::Handle->Rollback;
+            return ( $ret, $msg );
+        }
+
+        if ( ( $self->ContentEncoding // '' ) ne $encoding ) {
+            my ( $ret, $msg ) = $self->_Set( Field => 'ContentEncoding', Value => $encoding );
+            unless ($ret) {
+                $RT::Handle->Rollback;
+                return ( $ret, $msg );
+            }
+        }
+        $RT::Handle->Commit;
         return ( $ret, 'Content replaced' );
     }
     return ( 1, $self->loc('No content matches found') );

commit fe491abaa0f8e8ec95bbe76f8adf61548803d30a
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 25 03:35:13 2019 +0800

    Make ReplaceHeaders/ReplaceContent return true only if replacement happens
    
    We filter attachments in RT::Attachments::ReplaceAttachments by the
    search string in advance, so replacement is supposed to always happen in
    the inner calls of ReplaceHeaders/ReplaceContent, which makes the
    statement of
    
            return ( 1, $self->loc('Headers cleared') );
    
    always be called and the statement of
    
            return ( 1, $self->loc('No content matches found') );
    
    never be called.
    
    Logically both statements above are not quite correct considering
    attachments that replacement methods are called on might not have
    matches.
    
    This commit fixes this, so we can always use the return values to
    indicate if there are replacements or not.

diff --git a/lib/RT/Attachment.pm b/lib/RT/Attachment.pm
index e443020d2..66a0e4e6d 100644
--- a/lib/RT/Attachment.pm
+++ b/lib/RT/Attachment.pm
@@ -769,14 +769,27 @@ sub ReplaceHeaders {
 
     return ( 0, $self->loc('No Search string provided') ) unless $args{Search};
 
+    my $updated;
     foreach my $header ( $self->SplitHeaders ) {
         my ( $tag, $value ) = split /:/, $header, 2;
         if ( $value =~ s/\Q$args{Search}\E/$args{Replacement}/ig ) {
-            my $ret = $self->SetHeader( $tag, $value );
-            RT::Logger->error("Could not set header: $tag to $value") unless $ret;
+            my ( $ret, $msg ) = $self->SetHeader( $tag, $value );
+            if ( $ret ) {
+                $updated ||= 1;
+            }
+            else {
+                RT::Logger->error("Could not set header: $tag to $value: $msg");
+                return ( $ret, $msg );
+            }
         }
     }
-    return ( 1, $self->loc('Headers cleared') );
+
+    if ( $updated ) {
+        return ( 1, $self->loc('Headers cleared') );
+    }
+    else {
+        return ( 0, $self->loc('No header matches found') );
+    }
 }
 
 =head2 ReplaceContent ( Search => 'SEARCH', Replacement => 'Replacement' )
@@ -824,7 +837,7 @@ sub ReplaceContent {
         $RT::Handle->Commit;
         return ( $ret, 'Content replaced' );
     }
-    return ( 1, $self->loc('No content matches found') );
+    return ( 0, $self->loc('No content matches found') );
 }
 
 

commit 05d2bacab54cd9fc696e15d259e8d3c9deb45de2
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 25 05:52:24 2019 +0800

    Add --tickets to munge more attachments that have encoded content
    
    Previously attachments were pre-filtered by the search string at SQL
    level, to limit the to-be-processed attachments to a reasonable small
    set. The drawback is it bypassed attachments that have encodings like
    base64, etc. even if the decoded content matches and should be replaced.
    Because of this, we couldn't munge non-ascii strings as they are quite
    probably encoded in the database.
    
    To get around the pre-filter behavior and still keep to-be-processed
    attachments being a small set, --tickets is added. Besides, in real
    life, we believe it'll be the most common usage to munge attachments for
    only a few tickets.

diff --git a/lib/RT/Attachments.pm b/lib/RT/Attachments.pm
index 438e4ca34..f93b413d7 100644
--- a/lib/RT/Attachments.pm
+++ b/lib/RT/Attachments.pm
@@ -266,6 +266,7 @@ sub ReplaceAttachments {
         Replacement => '',
         Headers     => 1,
         Content     => 1,
+        FilterBySearchString => 1,
         @_,
     );
 
@@ -287,37 +288,41 @@ sub ReplaceAttachments {
     };
 
     my $attachments = $self->Clone;
-    $attachments->Limit(
-        FIELD     => 'ContentEncoding',
-        VALUE     => 'none',
-        SUBCLAUSE => 'Encoding',
-    );
-    $attachments->Limit(
-        FIELD           => 'ContentEncoding',
-        OPERATOR        => 'IS',
-        VALUE           => 'NULL',
-        SUBCLAUSE       => 'Encoding',
-    );
-
-    # For QP encoding, if encoded string is equal to the decoded
-    # version, then SQL search will also work.
-    #
-    # Adding "\n" is to avoid trailing "=" in QP encoding
-    if ( MIME::QuotedPrint::encode("$args{Search}\n") eq "$args{Search}\n" ) {
+    if ( $args{FilterBySearchString} ) {
         $attachments->Limit(
             FIELD     => 'ContentEncoding',
-            VALUE     => 'quoted-printable',
+            VALUE     => 'none',
             SUBCLAUSE => 'Encoding',
         );
+        $attachments->Limit(
+            FIELD     => 'ContentEncoding',
+            OPERATOR  => 'IS',
+            VALUE     => 'NULL',
+            SUBCLAUSE => 'Encoding',
+        );
+
+        # For QP encoding, if encoded string is equal to the decoded
+        # version, then SQL search will also work.
+        #
+        # Adding "\n" is to avoid trailing "=" in QP encoding
+        if ( MIME::QuotedPrint::encode("$args{Search}\n") eq "$args{Search}\n" ) {
+            $attachments->Limit(
+                FIELD     => 'ContentEncoding',
+                VALUE     => 'quoted-printable',
+                SUBCLAUSE => 'Encoding',
+            );
+        }
     }
 
     if ( $args{Headers} ) {
         my $atts = $attachments->Clone;
-        $atts->Limit(
-            FIELD    => 'Headers',
-            OPERATOR => 'LIKE',
-            VALUE    => $args{Search},
-        );
+        if ( $args{FilterBySearchString} ) {
+            $atts->Limit(
+                FIELD    => 'Headers',
+                OPERATOR => 'LIKE',
+                VALUE    => $args{Search},
+            );
+        }
         $atts->Limit(
             FIELD     => 'ContentType',
             OPERATOR  => 'IN',
@@ -342,19 +347,21 @@ sub ReplaceAttachments {
                 $create_munge_txn->( $att->TransactionObj->TicketObj );
             }
             else {
-                RT::Logger->error($msg);
+                RT::Logger->debug($msg);
             }
         }
     }
 
     if ( $args{Content} ) {
         my $atts = $attachments->Clone;
-        $atts->Limit(
-            FIELD     => 'Content',
-            OPERATOR  => 'LIKE',
-            VALUE     => $args{Search},
-            SUBCLAUSE => 'Content',
-        );
+        if ( $args{FilterBySearchString} ) {
+            $atts->Limit(
+                FIELD     => 'Content',
+                OPERATOR  => 'LIKE',
+                VALUE     => $args{Search},
+                SUBCLAUSE => 'Content',
+            );
+        }
         $atts->Limit(
             FIELD    => 'ContentType',
             OPERATOR => 'IN',
@@ -371,13 +378,13 @@ sub ReplaceAttachments {
                 $create_munge_txn->( $att->TransactionObj->TicketObj );
             }
             else {
-                RT::Logger->error($msg);
+                RT::Logger->debug($msg);
             }
         }
     }
 
     my $count = scalar keys %munged;
-    return ( 1, $self->loc( "Updated [quant,_1,ticket's,tickets'] attachment content", $count ) );
+    return wantarray ? ( 1, $self->loc( "Updated [quant,_1,ticket's,tickets'] attachment content", $count ) ) : $count;
 }
 
 RT::Base->_ImportOverlays();
diff --git a/sbin/rt-munge-attachments.in b/sbin/rt-munge-attachments.in
index 3b4717b2f..137db957e 100644
--- a/sbin/rt-munge-attachments.in
+++ b/sbin/rt-munge-attachments.in
@@ -70,7 +70,7 @@ BEGIN {    # BEGIN RT CMD BOILERPLATE
 # Read in the options
 my %opts;
 use Getopt::Long;
-GetOptions( \%opts, "help|h", "search=s", "replacement=s", );
+GetOptions( \%opts, "help|h", "search=s", "replacement=s", 'tickets=s' );
 
 if ( $opts{'help'} || !$opts{'search'} ) {
     require Pod::Usage;
@@ -84,9 +84,39 @@ my $replacement = $opts{'replacement'} || '';
 
 my $search = $opts{'search'};
 
-my $attachments = RT::Attachments->new( RT->SystemUser );
-my ($ret, $msg) = $attachments->ReplaceAttachments(Search => $search, Replacement => $replacement);
+my $attachments;
+if ( $opts{tickets} ) {
+    my @tickets = split /\s*,\s*/, $opts{tickets};
+
+    $attachments = RT::Attachments->new( RT->SystemUser );
+    my $txn_alias   = $attachments->TransactionAlias;
+    $attachments->Limit(
+        ALIAS => $txn_alias,
+        FIELD => 'ObjectType',
+        VALUE => 'RT::Ticket',
+    );
+    my $ticket_alias = $attachments->Join(
+        ALIAS1 => $txn_alias,
+        FIELD1 => 'ObjectId',
+        TABLE2 => 'Tickets',
+        FIELD2 => 'id',
+    );
+    $attachments->Limit(
+        ALIAS    => $ticket_alias,
+        FIELD    => 'EffectiveId',
+        VALUE    => \@tickets,
+        OPERATOR => 'IN',
+    );
+}
+else {
+    $attachments = RT::Attachments->new( RT->SystemUser );
+}
 
+my ( $ret, $msg ) = $attachments->ReplaceAttachments(
+    Search      => $search,
+    Replacement => $replacement,
+    $opts{tickets} ? ( FilterBySearchString => 0 ) : (),
+);
 print STDERR $msg . "\n";
 
 
@@ -130,4 +160,11 @@ If a match is found the content will be removed unless a replacement is provided
 
 Provide a string to replace the value matched by the search.
 
+=item --tickets=1,2,3
+
+Limit attachments to the specified tickets. Note that if tickets are not
+specified, RT will pre-filter attachments by the search string use SQL,
+which might bypass attachments of which contents are encoded(like
+base64). Use this option to prevent the pre-filter behavior.
+
 =back

commit 2cad9976763eb34407a2f15385fc34da15bf51c8
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 25 20:31:13 2019 +0800

    Make sure search/replacement strings to munge are decoded ones
    
    We use decoded characters in RT code consistently, and this commit makes
    non-ascii search strings work too.

diff --git a/lib/RT/Attachments.pm b/lib/RT/Attachments.pm
index f93b413d7..71905919a 100644
--- a/lib/RT/Attachments.pm
+++ b/lib/RT/Attachments.pm
@@ -305,7 +305,9 @@ sub ReplaceAttachments {
         # version, then SQL search will also work.
         #
         # Adding "\n" is to avoid trailing "=" in QP encoding
-        if ( MIME::QuotedPrint::encode("$args{Search}\n") eq "$args{Search}\n" ) {
+        if ( MIME::QuotedPrint::encode( Encode::encode( 'UTF-8', "$args{Search}\n" ) ) eq
+            Encode::encode( 'UTF-8', "$args{Search}\n" ) )
+        {
             $attachments->Limit(
                 FIELD     => 'ContentEncoding',
                 VALUE     => 'quoted-printable',
diff --git a/sbin/rt-munge-attachments.in b/sbin/rt-munge-attachments.in
index 137db957e..667672341 100644
--- a/sbin/rt-munge-attachments.in
+++ b/sbin/rt-munge-attachments.in
@@ -113,8 +113,8 @@ else {
 }
 
 my ( $ret, $msg ) = $attachments->ReplaceAttachments(
-    Search      => $search,
-    Replacement => $replacement,
+    Search      => Encode::decode( 'UTF-8', $search ),
+    Replacement => Encode::decode( 'UTF-8', $replacement ),
     $opts{tickets} ? ( FilterBySearchString => 0 ) : (),
 );
 print STDERR $msg . "\n";

commit e644561d85fe1216ca3ede6fd279c3ee29508c01
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Fri Jan 25 20:42:38 2019 +0800

    Add tests for rt-munge-attachments with --tickets

diff --git a/t/api/attachment.t b/t/api/attachment.t
index 25d736b8b..35906bbc0 100644
--- a/t/api/attachment.t
+++ b/t/api/attachment.t
@@ -155,4 +155,56 @@ diag 'Test clearing and replacing header and content in attachments table';
     is $attachments->Count, 1, 'Headers are not replaced when flagged as false';
 }
 
+diag 'Test clearing and replacing header and content in attachments from example emails';
+{
+    my $email_file =
+      RT::Test::get_relocatable_file( 'multipart-alternative-with-umlaut',
+        ( File::Spec->updir(), 'data', 'emails' ) );
+    my $content = RT::Test->file_content($email_file);
+
+    my $parser = RT::EmailParser->new;
+    $parser->ParseMIMEEntityFromScalar($content);
+    my $ticket = RT::Test->create_ticket( Queue => 'General', Subject => 'test munge', MIMEObj => $parser->Entity );
+    my $decoded_umlaut = Encode::decode( 'UTF-8', 'Grüßen' );
+
+    my $attachments = $ticket->Attachments( WithHeaders => 1, WithContent => 1 );
+    while ( my $att = $attachments->Next ) {
+        if ( $att->Content ) {
+            like( $att->Content, qr/$decoded_umlaut/, "Content contains $decoded_umlaut" );
+            unlike( $att->Content, qr/anonymous/, 'Content lacks anonymous' );
+        }
+        else {
+            like( $att->Headers, qr/"Stever, Gregor" <gst\@example.com>/, 'Headers contain gst at example.com' );
+            unlike( $att->Headers, qr/anon\@example.com/, 'Headers lack anon at example.com' );
+        }
+    }
+
+    RT::Test->run_and_capture(
+        command     => $RT::SbinPath . '/rt-munge-attachments',
+        tickets     => $ticket->id,
+        search      => 'Grüßen',
+        replacement => 'anonymous',
+    );
+
+    RT::Test->run_and_capture(
+        command     => $RT::SbinPath . '/rt-munge-attachments',
+        tickets     => $ticket->id,
+        search      => '"Stever, Gregor" <gst at example.com>',
+        replacement => 'anon at example.com',
+    );
+
+    $attachments = $ticket->Attachments( WithHeaders => 1, WithContent => 1 );
+    while ( my $att = $attachments->Next ) {
+        my $decoded_umlaut = Encode::decode( 'UTF-8', 'Grüßen' );
+        if ( $att->Content ) {
+            unlike( $att->Content, qr/$decoded_umlaut/, "Content lacks $decoded_umlaut" );
+            like( $att->Content, qr/anonymous/, 'Content contains anonymous' );
+        }
+        else {
+            unlike( $att->Headers, qr/"Stever, Gregor" <gst\@example.com>/, 'Headers lack gst at example.com' );
+            like( $att->Headers, qr/anon\@example.com/, 'Headers contain anon at example.com' );
+        }
+    }
+}
+
 done_testing();

-----------------------------------------------------------------------


More information about the rt-commit mailing list