1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
54 This module should never be instantiated directly by client code. it's an internal
55 module which should only be instantiated through exported APIs in Ticket, Queue and other
64 ok (require RT::Attachment);
71 package RT::Attachment;
74 no warnings qw(redefine);
77 use MIME::QuotedPrint;
80 # {{{ sub _OverlayAccessible
81 sub _OverlayAccessible {
83 TransactionId => { 'read'=>1, 'public'=>1, 'write' => 0 },
84 MessageId => { 'read'=>1, 'write' => 0 },
85 Parent => { 'read'=>1, 'write' => 0 },
86 ContentType => { 'read'=>1, 'write' => 0 },
87 Subject => { 'read'=>1, 'write' => 0 },
88 Content => { 'read'=>1, 'write' => 0 },
89 ContentEncoding => { 'read'=>1, 'write' => 0 },
90 Headers => { 'read'=>1, 'write' => 0 },
91 Filename => { 'read'=>1, 'write' => 0 },
92 Creator => { 'read'=>1, 'auto'=>1, },
93 Created => { 'read'=>1, 'auto'=>1, },
98 # {{{ sub TransactionObj
100 =head2 TransactionObj
102 Returns the transaction object asscoiated with this attachment.
107 require RT::Transaction;
109 unless (exists $self->{_TransactionObj}) {
110 $self->{_TransactionObj}=RT::Transaction->new($self->CurrentUser);
111 $self->{_TransactionObj}->Load($self->TransactionId);
113 unless ($self->{_TransactionObj}->Id) {
114 $RT::Logger->crit("Attachment ".$self->id." can't find transaction ".$self->TransactionId." which it is ostensibly part of. That's bad");
116 return $self->{_TransactionObj};
125 Create a new attachment. Takes a paramhash:
127 'Attachment' Should be a single MIME body with optional subparts
128 'Parent' is an optional Parent RT::Attachment object
129 '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 || eval {
169 ${ $Attachment->head->{mail_hdr_hash}{'Content-Disposition'}[0] }
170 =~ /^.*\bfilename="(.*)"$/ ? $1 : ''
173 # If a message has no bodyhandle, that means that it has subparts (or appears to)
174 # and we should act accordingly.
175 unless ( defined $Attachment->bodyhandle ) {
177 $id = $self->SUPER::Create(
178 TransactionId => $args{'TransactionId'},
180 ContentType => $Attachment->mime_type,
181 Headers => $Attachment->head->as_string,
182 MessageId => $MessageId,
183 Subject => $Subject);
186 $RT::Logger->crit("Attachment insert failed - ".$RT::Handle->dbh->errstr);
190 foreach my $part ( $Attachment->parts ) {
191 my $SubAttachment = new RT::Attachment( $self->CurrentUser );
192 $SubAttachment->Create(
193 TransactionId => $args{'TransactionId'},
196 ContentType => $Attachment->mime_type,
203 #If it's not multipart
206 my ($ContentEncoding, $Body) = $self->_EncodeLOB($Attachment->bodyhandle->as_string, $Attachment->mime_type);
207 my $id = $self->SUPER::Create( TransactionId => $args{'TransactionId'},
208 ContentType => $Attachment->mime_type,
209 ContentEncoding => $ContentEncoding,
210 Parent => $args{'Parent'},
211 Headers => $Attachment->head->as_string,
214 Filename => $Filename,
215 MessageId => $MessageId
218 $RT::Logger->crit("Attachment insert failed - ".$RT::Handle->dbh->errstr);
230 Create an attachment exactly as specified in the named parameters.
237 my %args = ( ContentEncoding => 'none',
242 ($args{'ContentEncoding'}, $args{'Content'}) = $self->_EncodeLOB($args{'Content'}, $args{'MimeType'});
244 return($self->SUPER::Create(%args));
251 Returns the attachment's content. if it's base64 encoded, decode it
258 $self->_DecodeLOB($self->ContentType, $self->ContentEncoding, $self->_Value('Content', decode_utf8 => 0));
265 # {{{ sub OriginalContent
267 =head2 OriginalContent
269 Returns the attachment's content as octets before RT's mangling.
270 Currently, this just means restoring text content back to its
275 sub OriginalContent {
278 return $self->Content unless (
279 $self->ContentType =~ qr{^(text/plain|message/rfc822)$}i) ;
280 my $enc = $self->OriginalEncoding;
283 if ( $self->ContentEncoding eq 'none' || ! $self->ContentEncoding ) {
284 $content = $self->_Value('Content', decode_utf8 => 0);
285 } elsif ( $self->ContentEncoding eq 'base64' ) {
286 $content = MIME::Base64::decode_base64($self->_Value('Content', decode_utf8 => 0));
287 } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
288 return MIME::QuotedPrint::decode($self->_Value('Content', decode_utf8 => 0));
290 return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
293 # Encode::_utf8_on($content);
294 if (!$enc || $enc eq '' || $enc eq 'utf8' || $enc eq 'utf-8') {
295 # If we somehow fail to do the decode, at least push out the raw bits
296 eval {return( Encode::decode_utf8($content))} || return ($content);
299 eval { Encode::from_to($content, 'utf8' => $enc);};
301 $RT::Logger->error("Could not convert attachment from assumed utf8 to '$enc' :".$@);
309 # {{{ sub OriginalEncoding
311 =head2 OriginalEncoding
313 Returns the attachment's original encoding.
317 sub OriginalEncoding {
319 return $self->GetHeader('X-RT-Original-Encoding');
328 Returns an RT::Attachments object which is preloaded with all Attachments objects with this Attachment\'s Id as their 'Parent'
335 my $kids = new RT::Attachments($self->CurrentUser);
336 $kids->ChildrenOf($self->Id);
350 my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
353 my ($quoted_content, $body, $headers);
356 # TODO: Handle Multipart/Mixed (eventually fix the link in the
357 # ShowHistory web template?)
358 if ($self->ContentType =~ m{^(text/plain|message)}i) {
359 $body=$self->Content;
361 # Do we need any preformatting (wrapping, that is) of the message?
363 # Remove quoted signature.
364 $body =~ s/\n-- \n(.*)$//s;
366 # What's the longest line like?
367 foreach (split (/\n/,$body)) {
368 $max=length if ( length > $max);
372 require Text::Wrapper;
373 my $wrapper=new Text::Wrapper
376 body_start => ($max > 70*3 ? ' ' : ''),
379 $body=$wrapper->wrap($body);
384 $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
389 $body = "[Non-text message not quoted]\n\n";
396 return (\$body, $max);
400 # {{{ sub NiceHeaders - pulls out only the most relevant headers
404 Returns a multi-line string of the To, From, Cc, Date and Subject headers.
411 my @hdrs = $self->_SplitHeaders;
412 while (my $str = shift @hdrs) {
413 next unless $str =~ /^(To|From|RT-Send-Cc|Cc|Bcc|Date|Subject):/i;
414 $hdrs .= $str . "\n";
415 $hdrs .= shift( @hdrs ) . "\n" while ($hdrs[0] =~ /^[ \t]+/);
425 Returns this object's headers as a string. This method specifically
426 removes the RT-Send-Bcc: header, so as to never reveal to whom RT sent a Bcc.
427 We need to record the RT-Send-Cc and RT-Send-Bcc values so that we can actually send
428 out mail. (The mailing rules are separated from the ticket update code by
429 an abstraction barrier that makes it impossible to pass this data directly
436 my @headers = grep { !/^RT-Send-Bcc/i } $self->_SplitHeaders;
437 return join("\n",@headers);
446 =head2 GetHeader ( 'Tag')
448 Returns the value of the header Tag as a string. This bypasses the weeding out
449 done in Headers() above.
456 foreach my $line ($self->_SplitHeaders) {
457 if ($line =~ /^\Q$tag\E:\s+(.*)$/si) { #if we find the header, return its value
462 # we found no header. return an empty string
469 =head2 SetHeader ( 'Tag', 'Value' )
471 Replace or add a Header to the attachment's headers.
480 foreach my $line ($self->_SplitHeaders) {
481 if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
482 $newheader .= "$tag: $_[0]\n";
486 $newheader .= "$line\n";
490 $newheader .= "$tag: $_[0]\n" if defined $tag;
491 $self->__Set( Field => 'Headers', Value => $newheader);
499 Takes the name of a table column.
500 Returns its value as a string, if the user passes an ACL check
509 #if the field is public, return it.
510 if ( $self->_Accessible( $field, 'public' ) ) {
511 return ( $self->__Value( $field, @_ ) );
514 #If it's a comment, we need to be extra special careful
515 elsif ( $self->TransactionObj->Type =~ /^Comment/ ) {
516 if ( $self->TransactionObj->CurrentUserHasRight('ShowTicketComments') )
518 return ( $self->__Value( $field, @_ ) );
521 elsif ( $self->TransactionObj->CurrentUserHasRight('ShowTicket') ) {
522 return ( $self->__Value( $field, @_ ) );
525 #if they ain't got rights to see, don't let em
536 Returns an array of this attachment object's headers, with one header
537 per array entry. multiple lines are folded.
541 my $test1 = "From: jesse";
542 my @headers = RT::Attachment->_SplitHeaders($test1);
543 is ($#headers, 0, $test1 );
545 my $test2 = qq{From: jesse
550 @headers = RT::Attachment->_SplitHeaders($test2);
551 is ($#headers, 2, "testing a bunch of singline multiple headers" );
554 my $test3 = qq{From: jesse
562 @headers = RT::Attachment->_SplitHeaders($test3);
563 is ($#headers, 2, "testing a bunch of singline multiple headers" );
572 my $headers = (shift || $self->SUPER::Headers());
574 for (split(/\n(?=\w|\z)/,$headers)) {
585 unless ( (($self->TransactionObj->CurrentUserHasRight('ShowTicketComments')) and
586 ($self->TransactionObj->Type eq 'Comment') ) or
587 ($self->TransactionObj->CurrentUserHasRight('ShowTicket'))) {
591 if (my $len = $self->GetHeader('Content-Length')) {
597 my $len = length($self->Content);
598 $self->SetHeader('Content-Length' => $len);
605 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
609 'fast_update_p' => 1,
610 'cache_for_sec' => 180,