1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2007 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/copyleft/gpl.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
65 ok (require RT::Attachment);
72 package RT::Attachment;
75 no warnings qw(redefine);
78 use MIME::QuotedPrint;
81 # {{{ sub _OverlayAccessible
82 sub _OverlayAccessible {
84 TransactionId => { 'read'=>1, 'public'=>1, 'write' => 0 },
85 MessageId => { 'read'=>1, 'write' => 0 },
86 Parent => { 'read'=>1, 'write' => 0 },
87 ContentType => { 'read'=>1, 'write' => 0 },
88 Subject => { 'read'=>1, 'write' => 0 },
89 Content => { 'read'=>1, 'write' => 0 },
90 ContentEncoding => { 'read'=>1, 'write' => 0 },
91 Headers => { 'read'=>1, 'write' => 0 },
92 Filename => { 'read'=>1, 'write' => 0 },
93 Creator => { 'read'=>1, 'auto'=>1, },
94 Created => { 'read'=>1, 'auto'=>1, },
99 # {{{ sub TransactionObj
101 =head2 TransactionObj
103 Returns the transaction object asscoiated with this attachment.
108 require RT::Transaction;
110 unless (exists $self->{_TransactionObj}) {
111 $self->{_TransactionObj}=RT::Transaction->new($self->CurrentUser);
112 $self->{_TransactionObj}->Load($self->TransactionId);
114 unless ($self->{_TransactionObj}->Id) {
115 $RT::Logger->crit("Attachment ".$self->id." can't find transaction ".$self->TransactionId." which it is ostensibly part of. That's bad");
117 return $self->{_TransactionObj};
126 Create a new attachment. Takes a paramhash:
128 'Attachment' Should be a single MIME body with optional subparts
129 'Parent' is an optional id of the parent attachment
130 'TransactionId' is the mandatory id of the transaction this attachment is associated with.;
136 my %args = ( id => 0,
142 #For ease of reference
143 my $Attachment = $args{'Attachment'};
145 #if we didn't specify a ticket, we need to bail
146 if ( $args{'TransactionId'} == 0 ) {
147 $RT::Logger->crit( "RT::Attachment->Create couldn't, as you didn't specify a transaction\n" );
152 #If we possibly can, collapse it to a singlepart
153 $Attachment->make_singlepart;
156 my $Subject = $Attachment->head->get( 'subject', 0 );
157 defined($Subject) or $Subject = '';
161 my $MessageId = $Attachment->head->get( 'Message-ID', 0 );
162 defined($MessageId) or $MessageId = '';
164 $MessageId =~ s/^<(.*)>$/$1/go;
168 my $Filename = $Attachment->head->recommended_filename;
170 # If a message has no bodyhandle, that means that it has subparts (or appears to)
171 # and we should act accordingly.
172 unless ( defined $Attachment->bodyhandle ) {
174 my $id = $self->SUPER::Create(
175 TransactionId => $args{'TransactionId'},
177 ContentType => $Attachment->mime_type,
178 Headers => $Attachment->head->as_string,
179 MessageId => $MessageId,
184 $RT::Logger->crit("Attachment insert failed - ".$RT::Handle->dbh->errstr);
187 foreach my $part ( $Attachment->parts ) {
188 my $SubAttachment = new RT::Attachment( $self->CurrentUser );
189 my ($id) = $SubAttachment->Create(
190 TransactionId => $args{'TransactionId'},
195 $RT::Logger->crit("Attachment insert failed - ".$RT::Handle->dbh->errstr);
201 #If it's not multipart
204 my ($ContentEncoding, $Body) = $self->_EncodeLOB( $Attachment->bodyhandle->as_string,
205 $Attachment->mime_type
207 my $id = $self->SUPER::Create(
208 TransactionId => $args{'TransactionId'},
209 ContentType => $Attachment->mime_type,
210 ContentEncoding => $ContentEncoding,
211 Parent => $args{'Parent'},
212 Headers => $Attachment->head->as_string,
215 Filename => $Filename,
216 MessageId => $MessageId,
219 $RT::Logger->crit("Attachment insert failed - ".$RT::Handle->dbh->errstr);
231 Create an attachment exactly as specified in the named parameters.
238 my %args = ( ContentEncoding => 'none',
243 ($args{'ContentEncoding'}, $args{'Content'}) = $self->_EncodeLOB($args{'Content'}, $args{'MimeType'});
245 return($self->SUPER::Create(%args));
252 Returns the attachment's content. if it's base64 encoded, decode it
259 $self->_DecodeLOB($self->ContentType, $self->ContentEncoding, $self->_Value('Content', decode_utf8 => 0));
266 # {{{ sub OriginalContent
268 =head2 OriginalContent
270 Returns the attachment's content as octets before RT's mangling.
271 Currently, this just means restoring text content back to its
276 sub OriginalContent {
279 return $self->Content unless (
280 $self->ContentType =~ qr{^(text/plain|message/rfc822)$}i) ;
281 my $enc = $self->OriginalEncoding;
284 if ( $self->ContentEncoding eq 'none' || ! $self->ContentEncoding ) {
285 $content = $self->_Value('Content', decode_utf8 => 0);
286 } elsif ( $self->ContentEncoding eq 'base64' ) {
287 $content = MIME::Base64::decode_base64($self->_Value('Content', decode_utf8 => 0));
288 } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
289 return MIME::QuotedPrint::decode($self->_Value('Content', decode_utf8 => 0));
291 return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
294 Encode::_utf8_on($content);
295 if (!$enc || $enc eq '' || $enc eq 'utf8' || $enc eq 'utf-8') {
296 # If we somehow fail to do the decode, at least push out the raw bits
297 eval {return( Encode::decode_utf8($content))} || return ($content);
300 eval { Encode::from_to($content, 'utf8' => $enc) } if $enc;
302 $RT::Logger->error("Could not convert attachment from assumed utf8 to '$enc' :".$@);
310 # {{{ sub OriginalEncoding
312 =head2 OriginalEncoding
314 Returns the attachment's original encoding.
318 sub OriginalEncoding {
320 return $self->GetHeader('X-RT-Original-Encoding');
329 Returns an RT::Attachments object which is preloaded with all Attachments objects with this Attachment\'s Id as their 'Parent'
336 my $kids = new RT::Attachments($self->CurrentUser);
337 $kids->ChildrenOf($self->Id);
351 my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
354 my ($quoted_content, $body, $headers);
357 # TODO: Handle Multipart/Mixed (eventually fix the link in the
358 # ShowHistory web template?)
359 if ($self->ContentType =~ m{^(text/plain|message)}i) {
360 $body=$self->Content;
362 # Do we need any preformatting (wrapping, that is) of the message?
364 # Remove quoted signature.
365 $body =~ s/\n-- \n(.*)$//s;
367 # What's the longest line like?
368 foreach (split (/\n/,$body)) {
369 $max=length if ( length > $max);
373 require Text::Wrapper;
374 my $wrapper=new Text::Wrapper
377 body_start => ($max > 70*3 ? ' ' : ''),
380 $body=$wrapper->wrap($body);
385 $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
390 $body = "[Non-text message not quoted]\n\n";
397 return (\$body, $max);
401 # {{{ sub NiceHeaders - pulls out only the most relevant headers
405 Returns a multi-line string of the To, From, Cc, Date and Subject headers.
412 my @hdrs = $self->_SplitHeaders;
413 while (my $str = shift @hdrs) {
414 next unless $str =~ /^(To|From|RT-Send-Cc|Cc|Bcc|Date|Subject):/i;
415 $hdrs .= $str . "\n";
416 $hdrs .= shift( @hdrs ) . "\n" while ($hdrs[0] =~ /^[ \t]+/);
426 Returns this object's headers as a string. This method specifically
427 removes the RT-Send-Bcc: header, so as to never reveal to whom RT sent a Bcc.
428 We need to record the RT-Send-Cc and RT-Send-Bcc values so that we can actually send
429 out mail. (The mailing rules are separated from the ticket update code by
430 an abstraction barrier that makes it impossible to pass this data directly
437 my @headers = grep { !/^RT-Send-Bcc/i } $self->_SplitHeaders;
438 return join("\n",@headers);
447 =head2 GetHeader ( 'Tag')
449 Returns the value of the header Tag as a string. This bypasses the weeding out
450 done in Headers() above.
457 foreach my $line ($self->_SplitHeaders) {
458 if ($line =~ /^\Q$tag\E:\s+(.*)$/si) { #if we find the header, return its value
463 # we found no header. return an empty string
470 =head2 SetHeader ( 'Tag', 'Value' )
472 Replace or add a Header to the attachment's headers.
481 foreach my $line ($self->_SplitHeaders) {
482 if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
483 $newheader .= "$tag: $_[0]\n";
487 $newheader .= "$line\n";
491 $newheader .= "$tag: $_[0]\n" if defined $tag;
492 $self->__Set( Field => 'Headers', Value => $newheader);
500 Takes the name of a table column.
501 Returns its value as a string, if the user passes an ACL check
510 #if the field is public, return it.
511 if ( $self->_Accessible( $field, 'public' ) ) {
512 return ( $self->__Value( $field, @_ ) );
515 #If it's a comment, we need to be extra special careful
516 elsif ( $self->TransactionObj->Type =~ /^Comment/ ) {
517 if ( $self->TransactionObj->CurrentUserHasRight('ShowTicketComments') )
519 return ( $self->__Value( $field, @_ ) );
522 elsif ( $self->TransactionObj->CurrentUserHasRight('ShowTicket') ) {
523 return ( $self->__Value( $field, @_ ) );
526 #if they ain't got rights to see, don't let em
537 Returns an array of this attachment object's headers, with one header
538 per array entry. multiple lines are folded.
542 my $test1 = "From: jesse";
543 my @headers = RT::Attachment->_SplitHeaders($test1);
544 is ($#headers, 0, $test1 );
546 my $test2 = qq{From: jesse
551 @headers = RT::Attachment->_SplitHeaders($test2);
552 is ($#headers, 2, "testing a bunch of singline multiple headers" );
555 my $test3 = qq{From: jesse
563 @headers = RT::Attachment->_SplitHeaders($test3);
564 is ($#headers, 2, "testing a bunch of singline multiple headers" );
573 my $headers = (shift || $self->SUPER::Headers());
575 for (split(/\n(?=\w|\z)/,$headers)) {
586 unless ( (($self->TransactionObj->CurrentUserHasRight('ShowTicketComments')) and
587 ($self->TransactionObj->Type eq 'Comment') ) or
588 ($self->TransactionObj->CurrentUserHasRight('ShowTicket'))) {
592 if (my $len = $self->GetHeader('Content-Length')) {
598 my $len = length($self->Content);
599 $self->SetHeader('Content-Length' => $len);
606 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
610 'fast_update_p' => 1,
611 'cache_for_sec' => 180,