1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
6 # <jesse@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 defined($Subject) or $Subject = '';
127 my $MessageId = $Attachment->head->get( 'Message-ID', 0 );
128 defined($MessageId) or $MessageId = '';
130 $MessageId =~ s/^<(.*?)>$/$1/o;
133 my $Filename = $Attachment->head->recommended_filename;
135 # MIME::Head doesn't support perl strings well and can return
136 # octets which later will be double encoded in low-level code
137 my $head = $Attachment->head->as_string;
138 utf8::decode( $head );
140 # If a message has no bodyhandle, that means that it has subparts (or appears to)
141 # and we should act accordingly.
142 unless ( defined $Attachment->bodyhandle ) {
143 my ($id) = $self->SUPER::Create(
144 TransactionId => $args{'TransactionId'},
145 Parent => $args{'Parent'},
146 ContentType => $Attachment->mime_type,
148 MessageId => $MessageId,
153 $RT::Logger->crit("Attachment insert failed - ". $RT::Handle->dbh->errstr);
156 foreach my $part ( $Attachment->parts ) {
157 my $SubAttachment = new RT::Attachment( $self->CurrentUser );
158 my ($id) = $SubAttachment->Create(
159 TransactionId => $args{'TransactionId'},
164 $RT::Logger->crit("Attachment insert failed: ". $RT::Handle->dbh->errstr);
170 #If it's not multipart
173 my ($ContentEncoding, $Body) = $self->_EncodeLOB(
174 $Attachment->bodyhandle->as_string,
175 $Attachment->mime_type
178 my $id = $self->SUPER::Create(
179 TransactionId => $args{'TransactionId'},
180 ContentType => $Attachment->mime_type,
181 ContentEncoding => $ContentEncoding,
182 Parent => $args{'Parent'},
186 Filename => $Filename,
187 MessageId => $MessageId,
191 $RT::Logger->crit("Attachment insert failed: ". $RT::Handle->dbh->errstr);
199 Create an attachment exactly as specified in the named parameters.
205 my %args = ( ContentEncoding => 'none', @_ );
207 ( $args{'ContentEncoding'}, $args{'Content'} ) =
208 $self->_EncodeLOB( $args{'Content'}, $args{'MimeType'} );
210 return ( $self->SUPER::Create(%args) );
213 =head2 TransactionObj
215 Returns the transaction object asscoiated with this attachment.
222 unless ( $self->{_TransactionObj} ) {
223 $self->{_TransactionObj} = RT::Transaction->new( $self->CurrentUser );
224 $self->{_TransactionObj}->Load( $self->TransactionId );
227 unless ($self->{_TransactionObj}->Id) {
228 $RT::Logger->crit( "Attachment ". $self->id
229 ." can't find transaction ". $self->TransactionId
230 ." which it is ostensibly part of. That's bad");
232 return $self->{_TransactionObj};
237 Returns a parent's L<RT::Attachment> object if this attachment
238 has a parent, otherwise returns undef.
244 return undef unless $self->Parent;
246 my $parent = RT::Attachment->new( $self->CurrentUser );
247 $parent->LoadById( $self->Parent );
253 Returns an L<RT::Attachments> object which is preloaded with
254 all attachments objects with this attachment\'s Id as their
262 my $kids = RT::Attachments->new( $self->CurrentUser );
263 $kids->ChildrenOf( $self->Id );
269 Returns the attachment's content. if it's base64 encoded, decode it
276 return $self->_DecodeLOB(
278 $self->ContentEncoding,
279 $self->_Value('Content', decode_utf8 => 0),
283 =head2 OriginalContent
285 Returns the attachment's content as octets before RT's mangling.
286 Currently, this just means restoring text content back to its
291 sub OriginalContent {
294 return $self->Content unless RT::I18N::IsTextualContentType($self->ContentType);
295 my $enc = $self->OriginalEncoding;
298 if ( !$self->ContentEncoding || $self->ContentEncoding eq 'none' ) {
299 $content = $self->_Value('Content', decode_utf8 => 0);
300 } elsif ( $self->ContentEncoding eq 'base64' ) {
301 $content = MIME::Base64::decode_base64($self->_Value('Content', decode_utf8 => 0));
302 } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
303 $content = MIME::QuotedPrint::decode($self->_Value('Content', decode_utf8 => 0));
305 return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
308 # Turn *off* the SvUTF8 bits here so decode_utf8 and from_to below can work.
310 Encode::_utf8_off($content);
312 if (!$enc || $enc eq '' || $enc eq 'utf8' || $enc eq 'utf-8') {
313 # If we somehow fail to do the decode, at least push out the raw bits
314 eval { return( Encode::decode_utf8($content)) } || return ($content);
317 eval { Encode::from_to($content, 'utf8' => $enc) } if $enc;
319 $RT::Logger->error("Could not convert attachment from assumed utf8 to '$enc' :".$@);
324 =head2 OriginalEncoding
326 Returns the attachment's original encoding.
330 sub OriginalEncoding {
332 return $self->GetHeader('X-RT-Original-Encoding');
337 Returns length of L</Content> in bytes.
344 return undef unless $self->TransactionObj->CurrentUserCanSee;
346 my $len = $self->GetHeader('Content-Length');
347 unless ( defined $len ) {
349 no warnings 'uninitialized';
350 $len = length($self->Content);
351 $self->SetHeader('Content-Length' => $len);
362 my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
365 my ($quoted_content, $body, $headers);
368 # TODO: Handle Multipart/Mixed (eventually fix the link in the
369 # ShowHistory web template?)
370 if (RT::I18N::IsTextualContentType($self->ContentType)) {
371 $body=$self->Content;
373 # Do we need any preformatting (wrapping, that is) of the message?
375 # Remove quoted signature.
376 $body =~ s/\n-- \n(.*)$//s;
378 # What's the longest line like?
379 foreach (split (/\n/,$body)) {
380 $max=length if ( length > $max);
384 require Text::Wrapper;
385 my $wrapper=new Text::Wrapper
388 body_start => ($max > 70*3 ? ' ' : ''),
391 $body=$wrapper->wrap($body);
396 $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
401 $body = "[Non-text message not quoted]\n\n";
408 return (\$body, $max);
413 Returns MIME entity built from this attachment.
420 my $entity = new MIME::Entity;
421 foreach my $header ($self->SplitHeaders) {
422 my ($h_key, $h_val) = split /:/, $header, 2;
423 $entity->head->add( $h_key, RT::Interface::Email::EncodeToMIME( String => $h_val ) );
428 MIME::Body::Scalar->new( $self->OriginalContent )
437 Returns a hashref of all addresses related to this attachment.
438 The keys of the hash are C<From>, C<To>, C<Cc>, C<Bcc>, C<RT-Send-Cc>
439 and C<RT-Send-Bcc>. The values are references to lists of
440 L<Email::Address> objects.
448 my $current_user_address = lc $self->CurrentUser->EmailAddress;
449 my $correspond = lc $self->TransactionObj->TicketObj->QueueObj->CorrespondAddress;
450 my $comment = lc $self->TransactionObj->TicketObj->QueueObj->CommentAddress;
451 foreach my $hdr (qw(From To Cc Bcc RT-Send-Cc RT-Send-Bcc)) {
453 my $line = $self->GetHeader($hdr);
455 foreach my $AddrObj ( Email::Address->parse( $line )) {
456 my $address = $AddrObj->address;
457 $address = lc RT::User->CanonicalizeEmailAddress($address);
458 next if ( $current_user_address eq $address );
459 next if ( $comment eq $address );
460 next if ( $correspond eq $address );
461 next if ( RT::EmailParser->IsRTAddress($address) );
462 push @Addresses, $AddrObj ;
464 $data{$hdr} = \@Addresses;
471 Returns a multi-line string of the To, From, Cc, Date and Subject headers.
478 my @hdrs = $self->_SplitHeaders;
479 while (my $str = shift @hdrs) {
480 next unless $str =~ /^(To|From|RT-Send-Cc|Cc|Bcc|Date|Subject):/i;
481 $hdrs .= $str . "\n";
482 $hdrs .= shift( @hdrs ) . "\n" while ($hdrs[0] =~ /^[ \t]+/);
489 Returns this object's headers as a string. This method specifically
490 removes the RT-Send-Bcc: header, so as to never reveal to whom RT sent a Bcc.
491 We need to record the RT-Send-Cc and RT-Send-Bcc values so that we can actually send
492 out mail. The mailing rules are separated from the ticket update code by
493 an abstraction barrier that makes it impossible to pass this data directly.
498 return join("\n", $_[0]->SplitHeaders);
501 =head2 EncodedHeaders
503 Takes encoding as argument and returns the attachment's headers as octets in encoded
506 This is not protection using quoted printable or base64 encoding.
512 my $encoding = shift || 'utf8';
513 return Encode::encode( $encoding, $self->Headers );
516 =head2 GetHeader $TAG
518 Returns the value of the header Tag as a string. This bypasses the weeding out
519 done in Headers() above.
526 foreach my $line ($self->_SplitHeaders) {
527 next unless $line =~ /^\Q$tag\E:\s+(.*)$/si;
529 #if we find the header, return its value
533 # we found no header. return an empty string
537 =head2 DelHeader $TAG
539 Delete a field from the attachment's headers.
548 foreach my $line ($self->_SplitHeaders) {
549 next if $line =~ /^\Q$tag\E:\s+(.*)$/is;
550 $newheader .= "$line\n";
552 return $self->__Set( Field => 'Headers', Value => $newheader);
555 =head2 AddHeader $TAG, $VALUE, ...
557 Add one or many fields to the attachment's headers.
564 my $newheader = $self->__Value( 'Headers' );
565 while ( my ($tag, $value) = splice @_, 0, 2 ) {
566 $value = '' unless defined $value;
568 $value =~ s/\r+\n/\n /g;
569 $newheader .= "$tag: $value\n";
571 return $self->__Set( Field => 'Headers', Value => $newheader);
574 =head2 SetHeader ( 'Tag', 'Value' )
576 Replace or add a Header to the attachment's headers.
585 foreach my $line ($self->_SplitHeaders) {
586 if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
587 $newheader .= "$tag: $_[0]\n";
591 $newheader .= "$line\n";
595 $newheader .= "$tag: $_[0]\n" if defined $tag;
596 $self->__Set( Field => 'Headers', Value => $newheader);
601 Returns an array of this attachment object's headers, with one header
602 per array entry. Multiple lines are folded.
604 B<Never> returns C<RT-Send-Bcc> field.
610 return (grep !/^RT-Send-Bcc/i, $self->_SplitHeaders(@_) );
615 Returns an array of this attachment object's headers, with one header
616 per array entry. multiple lines are folded.
623 my $headers = (shift || $self->SUPER::Headers());
625 for (split(/\n(?=\w|\z)/,$headers)) {
636 my $txn = $self->TransactionObj;
637 return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
638 return (0, $self->loc('Permission Denied'))
639 unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
640 return (0, $self->loc('GnuPG integration is disabled'))
641 unless RT->Config->Get('GnuPG')->{'Enable'};
642 return (0, $self->loc('Attachments encryption is disabled'))
643 unless RT->Config->Get('GnuPG')->{'AllowEncryptDataInDB'};
645 require RT::Crypt::GnuPG;
647 my $type = $self->ContentType;
648 if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
649 return (1, $self->loc('Already encrypted'));
650 } elsif ( $type =~ /^multipart\//i ) {
651 return (1, $self->loc('No need to encrypt'));
653 $type = qq{x-application-rt\/gpg-encrypted; original-type="$type"};
656 my $queue = $txn->TicketObj->QueueObj;
658 foreach my $address ( grep $_,
659 $queue->CorrespondAddress,
660 $queue->CommentAddress,
661 RT->Config->Get('CorrespondAddress'),
662 RT->Config->Get('CommentAddress'),
664 my %res = RT::Crypt::GnuPG::GetKeysInfo( $address, 'private' );
665 next if $res{'exit_code'} || !$res{'info'};
666 %res = RT::Crypt::GnuPG::GetKeysForEncryption( $address );
667 next if $res{'exit_code'} || !$res{'info'};
668 $encrypt_for = $address;
670 unless ( $encrypt_for ) {
671 return (0, $self->loc('No key suitable for encryption'));
674 $self->__Set( Field => 'ContentType', Value => $type );
675 $self->SetHeader( 'Content-Type' => $type );
677 my $content = $self->Content;
678 my %res = RT::Crypt::GnuPG::SignEncryptContent(
679 Content => \$content,
682 Recipients => [ $encrypt_for ],
684 if ( $res{'exit_code'} ) {
685 return (0, $self->loc('GnuPG error. Contact with administrator'));
688 my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
690 return ($status, $self->loc("Couldn't replace content with encrypted data: [_1]", $msg));
692 return (1, $self->loc('Successfuly encrypted data'));
698 my $txn = $self->TransactionObj;
699 return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
700 return (0, $self->loc('Permission Denied'))
701 unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
702 return (0, $self->loc('GnuPG integration is disabled'))
703 unless RT->Config->Get('GnuPG')->{'Enable'};
705 require RT::Crypt::GnuPG;
707 my $type = $self->ContentType;
708 if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
709 ($type) = ($type =~ /original-type="(.*)"/i);
710 $type ||= 'application/octeat-stream';
712 return (1, $self->loc('Is not encrypted'));
714 $self->__Set( Field => 'ContentType', Value => $type );
715 $self->SetHeader( 'Content-Type' => $type );
717 my $content = $self->Content;
718 my %res = RT::Crypt::GnuPG::DecryptContent( Content => \$content, );
719 if ( $res{'exit_code'} ) {
720 return (0, $self->loc('GnuPG error. Contact with administrator'));
723 my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
725 return ($status, $self->loc("Couldn't replace content with decrypted data: [_1]", $msg));
727 return (1, $self->loc('Successfuly decrypted data'));
732 Takes the name of a table column.
733 Returns its value as a string, if the user passes an ACL check
741 #if the field is public, return it.
742 if ( $self->_Accessible( $field, 'public' ) ) {
743 return ( $self->__Value( $field, @_ ) );
746 return undef unless $self->TransactionObj->CurrentUserCanSee;
747 return $self->__Value( $field, @_ );
750 # Transactions don't change. by adding this cache congif directiove,
751 # we don't lose pathalogically on long tickets.
755 'fast_update_p' => 1,
756 'cache_for_sec' => 180,