1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
55 This module should never be instantiated directly by client code. it's an internal
56 module which should only be instantiated through exported APIs in Ticket, Queue and other
66 package RT::Attachment;
69 no warnings qw(redefine);
73 use MIME::QuotedPrint;
75 sub _OverlayAccessible {
77 TransactionId => { 'read'=>1, 'public'=>1, 'write' => 0 },
78 MessageId => { 'read'=>1, 'write' => 0 },
79 Parent => { 'read'=>1, 'write' => 0 },
80 ContentType => { 'read'=>1, 'write' => 0 },
81 Subject => { 'read'=>1, 'write' => 0 },
82 Content => { 'read'=>1, 'write' => 0 },
83 ContentEncoding => { 'read'=>1, 'write' => 0 },
84 Headers => { 'read'=>1, 'write' => 0 },
85 Filename => { 'read'=>1, 'write' => 0 },
86 Creator => { 'read'=>1, 'auto'=>1, },
87 Created => { 'read'=>1, 'auto'=>1, },
93 Create a new attachment. Takes a paramhash:
95 'Attachment' Should be a single MIME body with optional subparts
96 'Parent' is an optional id of the parent attachment
97 'TransactionId' is the mandatory id of the transaction this attachment is associated with.;
103 my %args = ( id => 0,
109 # For ease of reference
110 my $Attachment = $args{'Attachment'};
112 # if we didn't specify a ticket, we need to bail
113 unless ( $args{'TransactionId'} ) {
114 $RT::Logger->crit( "RT::Attachment->Create couldn't, as you didn't specify a transaction" );
118 # If we possibly can, collapse it to a singlepart
119 $Attachment->make_singlepart;
122 my $Subject = $Attachment->head->get( 'subject', 0 );
123 $Subject = '' unless defined $Subject;
125 utf8::decode( $Subject ) unless utf8::is_utf8( $Subject );
128 my $MessageId = $Attachment->head->get( 'Message-ID', 0 );
129 defined($MessageId) or $MessageId = '';
131 $MessageId =~ s/^<(.*?)>$/$1/o;
134 my $Filename = $Attachment->head->recommended_filename;
136 $Filename =~ s!.*/!! if $Filename;
138 # MIME::Head doesn't support perl strings well and can return
139 # octets which later will be double encoded in low-level code
140 my $head = $Attachment->head->as_string;
141 utf8::decode( $head ) unless utf8::is_utf8( $head );
143 # If a message has no bodyhandle, that means that it has subparts (or appears to)
144 # and we should act accordingly.
145 unless ( defined $Attachment->bodyhandle ) {
146 my ($id) = $self->SUPER::Create(
147 TransactionId => $args{'TransactionId'},
148 Parent => $args{'Parent'},
149 ContentType => $Attachment->mime_type,
151 MessageId => $MessageId,
156 $RT::Logger->crit("Attachment insert failed - ". $RT::Handle->dbh->errstr);
159 foreach my $part ( $Attachment->parts ) {
160 my $SubAttachment = new RT::Attachment( $self->CurrentUser );
161 my ($id) = $SubAttachment->Create(
162 TransactionId => $args{'TransactionId'},
167 $RT::Logger->crit("Attachment insert failed: ". $RT::Handle->dbh->errstr);
173 #If it's not multipart
176 my ($ContentEncoding, $Body) = $self->_EncodeLOB(
177 $Attachment->bodyhandle->as_string,
178 $Attachment->mime_type
181 my $id = $self->SUPER::Create(
182 TransactionId => $args{'TransactionId'},
183 ContentType => $Attachment->mime_type,
184 ContentEncoding => $ContentEncoding,
185 Parent => $args{'Parent'},
189 Filename => $Filename,
190 MessageId => $MessageId,
194 $RT::Logger->crit("Attachment insert failed: ". $RT::Handle->dbh->errstr);
202 Create an attachment exactly as specified in the named parameters.
208 my %args = ( ContentEncoding => 'none', @_ );
210 ( $args{'ContentEncoding'}, $args{'Content'} ) =
211 $self->_EncodeLOB( $args{'Content'}, $args{'MimeType'} );
213 return ( $self->SUPER::Create(%args) );
216 =head2 TransactionObj
218 Returns the transaction object asscoiated with this attachment.
225 unless ( $self->{_TransactionObj} ) {
226 $self->{_TransactionObj} = RT::Transaction->new( $self->CurrentUser );
227 $self->{_TransactionObj}->Load( $self->TransactionId );
230 unless ($self->{_TransactionObj}->Id) {
231 $RT::Logger->crit( "Attachment ". $self->id
232 ." can't find transaction ". $self->TransactionId
233 ." which it is ostensibly part of. That's bad");
235 return $self->{_TransactionObj};
240 Returns a parent's L<RT::Attachment> object if this attachment
241 has a parent, otherwise returns undef.
247 return undef unless $self->Parent;
249 my $parent = RT::Attachment->new( $self->CurrentUser );
250 $parent->LoadById( $self->Parent );
256 Returns an L<RT::Attachments> object which is preloaded with
257 all attachments objects with this attachment\'s Id as their
265 my $kids = RT::Attachments->new( $self->CurrentUser );
266 $kids->ChildrenOf( $self->Id );
272 Returns the attachment's content. if it's base64 encoded, decode it
279 return $self->_DecodeLOB(
281 $self->ContentEncoding,
282 $self->_Value('Content', decode_utf8 => 0),
286 =head2 OriginalContent
288 Returns the attachment's content as octets before RT's mangling.
289 Currently, this just means restoring text content back to its
294 sub OriginalContent {
297 return $self->Content unless RT::I18N::IsTextualContentType($self->ContentType);
298 my $enc = $self->OriginalEncoding;
301 if ( !$self->ContentEncoding || $self->ContentEncoding eq 'none' ) {
302 $content = $self->_Value('Content', decode_utf8 => 0);
303 } elsif ( $self->ContentEncoding eq 'base64' ) {
304 $content = MIME::Base64::decode_base64($self->_Value('Content', decode_utf8 => 0));
305 } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
306 $content = MIME::QuotedPrint::decode($self->_Value('Content', decode_utf8 => 0));
308 return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
311 # Turn *off* the SvUTF8 bits here so decode_utf8 and from_to below can work.
313 Encode::_utf8_off($content);
315 if (!$enc || $enc eq '' || $enc eq 'utf8' || $enc eq 'utf-8') {
316 # If we somehow fail to do the decode, at least push out the raw bits
317 eval { return( Encode::decode_utf8($content)) } || return ($content);
320 eval { Encode::from_to($content, 'utf8' => $enc) } if $enc;
322 $RT::Logger->error("Could not convert attachment from assumed utf8 to '$enc' :".$@);
327 =head2 OriginalEncoding
329 Returns the attachment's original encoding.
333 sub OriginalEncoding {
335 return $self->GetHeader('X-RT-Original-Encoding');
340 Returns length of L</Content> in bytes.
347 return undef unless $self->TransactionObj->CurrentUserCanSee;
349 my $len = $self->GetHeader('Content-Length');
350 unless ( defined $len ) {
352 no warnings 'uninitialized';
353 $len = length($self->Content);
354 $self->SetHeader('Content-Length' => $len);
365 my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
368 my ($quoted_content, $body, $headers);
371 # TODO: Handle Multipart/Mixed (eventually fix the link in the
372 # ShowHistory web template?)
373 if (RT::I18N::IsTextualContentType($self->ContentType)) {
374 $body=$self->Content;
376 # Do we need any preformatting (wrapping, that is) of the message?
378 # Remove quoted signature.
379 $body =~ s/\n-- \n(.*)$//s;
381 # What's the longest line like?
382 foreach (split (/\n/,$body)) {
383 $max=length if ( length > $max);
387 require Text::Wrapper;
388 my $wrapper=new Text::Wrapper
391 body_start => ($max > 70*3 ? ' ' : ''),
394 $body=$wrapper->wrap($body);
399 $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
404 $body = "[Non-text message not quoted]\n\n";
411 return (\$body, $max);
416 Returns MIME entity built from this attachment.
423 my $entity = new MIME::Entity;
424 foreach my $header ($self->SplitHeaders) {
425 my ($h_key, $h_val) = split /:/, $header, 2;
426 $entity->head->add( $h_key, RT::Interface::Email::EncodeToMIME( String => $h_val ) );
429 # since we want to return original content, let's use original encoding
430 $entity->head->mime_attr(
431 "Content-Type.charset" => $self->OriginalEncoding )
432 if $self->OriginalEncoding;
436 MIME::Body::Scalar->new( $self->OriginalContent )
445 Returns a hashref of all addresses related to this attachment.
446 The keys of the hash are C<From>, C<To>, C<Cc>, C<Bcc>, C<RT-Send-Cc>
447 and C<RT-Send-Bcc>. The values are references to lists of
448 L<Email::Address> objects.
456 my $current_user_address = lc $self->CurrentUser->EmailAddress;
457 foreach my $hdr (qw(From To Cc Bcc RT-Send-Cc RT-Send-Bcc)) {
459 my $line = $self->GetHeader($hdr);
461 foreach my $AddrObj ( Email::Address->parse( $line )) {
462 my $address = $AddrObj->address;
463 $address = lc RT::User->CanonicalizeEmailAddress($address);
464 next if $current_user_address eq $address;
465 next if RT::EmailParser->IsRTAddress($address);
466 push @Addresses, $AddrObj ;
468 $data{$hdr} = \@Addresses;
475 Returns a multi-line string of the To, From, Cc, Date and Subject headers.
482 my @hdrs = $self->_SplitHeaders;
483 while (my $str = shift @hdrs) {
484 next unless $str =~ /^(To|From|RT-Send-Cc|Cc|Bcc|Date|Subject):/i;
485 $hdrs .= $str . "\n";
486 $hdrs .= shift( @hdrs ) . "\n" while ($hdrs[0] =~ /^[ \t]+/);
493 Returns this object's headers as a string. This method specifically
494 removes the RT-Send-Bcc: header, so as to never reveal to whom RT sent a Bcc.
495 We need to record the RT-Send-Cc and RT-Send-Bcc values so that we can actually send
496 out mail. The mailing rules are separated from the ticket update code by
497 an abstraction barrier that makes it impossible to pass this data directly.
502 return join("\n", $_[0]->SplitHeaders);
505 =head2 EncodedHeaders
507 Takes encoding as argument and returns the attachment's headers as octets in encoded
510 This is not protection using quoted printable or base64 encoding.
516 my $encoding = shift || 'utf8';
517 return Encode::encode( $encoding, $self->Headers );
520 =head2 GetHeader $TAG
522 Returns the value of the header Tag as a string. This bypasses the weeding out
523 done in Headers() above.
530 foreach my $line ($self->_SplitHeaders) {
531 next unless $line =~ /^\Q$tag\E:\s+(.*)$/si;
533 #if we find the header, return its value
537 # we found no header. return an empty string
541 =head2 DelHeader $TAG
543 Delete a field from the attachment's headers.
552 foreach my $line ($self->_SplitHeaders) {
553 next if $line =~ /^\Q$tag\E:\s+(.*)$/is;
554 $newheader .= "$line\n";
556 return $self->__Set( Field => 'Headers', Value => $newheader);
559 =head2 AddHeader $TAG, $VALUE, ...
561 Add one or many fields to the attachment's headers.
568 my $newheader = $self->__Value( 'Headers' );
569 while ( my ($tag, $value) = splice @_, 0, 2 ) {
570 $value = '' unless defined $value;
572 $value =~ s/\r+\n/\n /g;
573 $newheader .= "$tag: $value\n";
575 return $self->__Set( Field => 'Headers', Value => $newheader);
578 =head2 SetHeader ( 'Tag', 'Value' )
580 Replace or add a Header to the attachment's headers.
589 foreach my $line ($self->_SplitHeaders) {
590 if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
591 $newheader .= "$tag: $_[0]\n";
595 $newheader .= "$line\n";
599 $newheader .= "$tag: $_[0]\n" if defined $tag;
600 $self->__Set( Field => 'Headers', Value => $newheader);
605 Returns an array of this attachment object's headers, with one header
606 per array entry. Multiple lines are folded.
608 B<Never> returns C<RT-Send-Bcc> field.
614 return (grep !/^RT-Send-Bcc/i, $self->_SplitHeaders(@_) );
619 Returns an array of this attachment object's headers, with one header
620 per array entry. multiple lines are folded.
627 my $headers = (shift || $self->SUPER::Headers());
629 for (split(/\n(?=\w|\z)/,$headers)) {
640 my $txn = $self->TransactionObj;
641 return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
642 return (0, $self->loc('Permission Denied'))
643 unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
644 return (0, $self->loc('GnuPG integration is disabled'))
645 unless RT->Config->Get('GnuPG')->{'Enable'};
646 return (0, $self->loc('Attachments encryption is disabled'))
647 unless RT->Config->Get('GnuPG')->{'AllowEncryptDataInDB'};
649 require RT::Crypt::GnuPG;
651 my $type = $self->ContentType;
652 if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
653 return (1, $self->loc('Already encrypted'));
654 } elsif ( $type =~ /^multipart\//i ) {
655 return (1, $self->loc('No need to encrypt'));
657 $type = qq{x-application-rt\/gpg-encrypted; original-type="$type"};
660 my $queue = $txn->TicketObj->QueueObj;
662 foreach my $address ( grep $_,
663 $queue->CorrespondAddress,
664 $queue->CommentAddress,
665 RT->Config->Get('CorrespondAddress'),
666 RT->Config->Get('CommentAddress'),
668 my %res = RT::Crypt::GnuPG::GetKeysInfo( $address, 'private' );
669 next if $res{'exit_code'} || !$res{'info'};
670 %res = RT::Crypt::GnuPG::GetKeysForEncryption( $address );
671 next if $res{'exit_code'} || !$res{'info'};
672 $encrypt_for = $address;
674 unless ( $encrypt_for ) {
675 return (0, $self->loc('No key suitable for encryption'));
678 $self->__Set( Field => 'ContentType', Value => $type );
679 $self->SetHeader( 'Content-Type' => $type );
681 my $content = $self->Content;
682 my %res = RT::Crypt::GnuPG::SignEncryptContent(
683 Content => \$content,
686 Recipients => [ $encrypt_for ],
688 if ( $res{'exit_code'} ) {
689 return (0, $self->loc('GnuPG error. Contact with administrator'));
692 my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
694 return ($status, $self->loc("Couldn't replace content with encrypted data: [_1]", $msg));
696 return (1, $self->loc('Successfuly encrypted data'));
702 my $txn = $self->TransactionObj;
703 return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
704 return (0, $self->loc('Permission Denied'))
705 unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
706 return (0, $self->loc('GnuPG integration is disabled'))
707 unless RT->Config->Get('GnuPG')->{'Enable'};
709 require RT::Crypt::GnuPG;
711 my $type = $self->ContentType;
712 if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
713 ($type) = ($type =~ /original-type="(.*)"/i);
714 $type ||= 'application/octeat-stream';
716 return (1, $self->loc('Is not encrypted'));
718 $self->__Set( Field => 'ContentType', Value => $type );
719 $self->SetHeader( 'Content-Type' => $type );
721 my $content = $self->Content;
722 my %res = RT::Crypt::GnuPG::DecryptContent( Content => \$content, );
723 if ( $res{'exit_code'} ) {
724 return (0, $self->loc('GnuPG error. Contact with administrator'));
727 my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
729 return ($status, $self->loc("Couldn't replace content with decrypted data: [_1]", $msg));
731 return (1, $self->loc('Successfuly decrypted data'));
736 Takes the name of a table column.
737 Returns its value as a string, if the user passes an ACL check
745 #if the field is public, return it.
746 if ( $self->_Accessible( $field, 'public' ) ) {
747 return ( $self->__Value( $field, @_ ) );
750 return undef unless $self->TransactionObj->CurrentUserCanSee;
751 return $self->__Value( $field, @_ );
754 # Transactions don't change. by adding this cache congif directiove,
755 # we don't lose pathalogically on long tickets.
759 'fast_update_p' => 1,
760 'cache_for_sec' => 180,