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 RT::I18N::IsTextualContentType($self->ContentType);
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 $content = MIME::QuotedPrint::decode($self->_Value('Content', decode_utf8 => 0));
291 return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
294 # Turn *off* the SvUTF8 bits here so decode_utf8 and from_to below can work.
296 Encode::_utf8_off($content);
298 if (!$enc || $enc eq '' || $enc eq 'utf8' || $enc eq 'utf-8') {
299 # If we somehow fail to do the decode, at least push out the raw bits
300 eval {return( Encode::decode_utf8($content))} || return ($content);
303 eval { Encode::from_to($content, 'utf8' => $enc) } if $enc;
305 $RT::Logger->error("Could not convert attachment from assumed utf8 to '$enc' :".$@);
313 # {{{ sub OriginalEncoding
315 =head2 OriginalEncoding
317 Returns the attachment's original encoding.
321 sub OriginalEncoding {
323 return $self->GetHeader('X-RT-Original-Encoding');
332 Returns an RT::Attachments object which is preloaded with all Attachments objects with this Attachment\'s Id as their 'Parent'
339 my $kids = new RT::Attachments($self->CurrentUser);
340 $kids->ChildrenOf($self->Id);
354 my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
357 my ($quoted_content, $body, $headers);
360 # TODO: Handle Multipart/Mixed (eventually fix the link in the
361 # ShowHistory web template?)
362 if (RT::I18N::IsTextualContentType($self->ContentType)) {
363 $body=$self->Content;
365 # Do we need any preformatting (wrapping, that is) of the message?
367 # Remove quoted signature.
368 $body =~ s/\n-- \n(.*)$//s;
370 # What's the longest line like?
371 foreach (split (/\n/,$body)) {
372 $max=length if ( length > $max);
376 require Text::Wrapper;
377 my $wrapper=new Text::Wrapper
380 body_start => ($max > 70*3 ? ' ' : ''),
383 $body=$wrapper->wrap($body);
388 $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
393 $body = "[Non-text message not quoted]\n\n";
400 return (\$body, $max);
404 # {{{ sub NiceHeaders - pulls out only the most relevant headers
408 Returns a multi-line string of the To, From, Cc, Date and Subject headers.
415 my @hdrs = $self->_SplitHeaders;
416 while (my $str = shift @hdrs) {
417 next unless $str =~ /^(To|From|RT-Send-Cc|Cc|Bcc|Date|Subject):/i;
418 $hdrs .= $str . "\n";
419 $hdrs .= shift( @hdrs ) . "\n" while ($hdrs[0] =~ /^[ \t]+/);
429 Returns this object's headers as a string. This method specifically
430 removes the RT-Send-Bcc: header, so as to never reveal to whom RT sent a Bcc.
431 We need to record the RT-Send-Cc and RT-Send-Bcc values so that we can actually send
432 out mail. (The mailing rules are separated from the ticket update code by
433 an abstraction barrier that makes it impossible to pass this data directly
440 my @headers = grep { !/^RT-Send-Bcc/i } $self->_SplitHeaders;
441 return join("\n",@headers);
450 =head2 GetHeader ( 'Tag')
452 Returns the value of the header Tag as a string. This bypasses the weeding out
453 done in Headers() above.
460 foreach my $line ($self->_SplitHeaders) {
461 if ($line =~ /^\Q$tag\E:\s+(.*)$/si) { #if we find the header, return its value
466 # we found no header. return an empty string
473 =head2 SetHeader ( 'Tag', 'Value' )
475 Replace or add a Header to the attachment's headers.
484 foreach my $line ($self->_SplitHeaders) {
485 if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
486 $newheader .= "$tag: $_[0]\n";
490 $newheader .= "$line\n";
494 $newheader .= "$tag: $_[0]\n" if defined $tag;
495 $self->__Set( Field => 'Headers', Value => $newheader);
503 Takes the name of a table column.
504 Returns its value as a string, if the user passes an ACL check
513 #if the field is public, return it.
514 if ( $self->_Accessible( $field, 'public' ) ) {
515 return ( $self->__Value( $field, @_ ) );
518 #If it's a comment, we need to be extra special careful
519 elsif ( $self->TransactionObj->Type =~ /^Comment/ ) {
520 if ( $self->TransactionObj->CurrentUserHasRight('ShowTicketComments') )
522 return ( $self->__Value( $field, @_ ) );
525 elsif ( $self->TransactionObj->CurrentUserHasRight('ShowTicket') ) {
526 return ( $self->__Value( $field, @_ ) );
529 #if they ain't got rights to see, don't let em
540 Returns an array of this attachment object's headers, with one header
541 per array entry. multiple lines are folded.
545 my $test1 = "From: jesse";
546 my @headers = RT::Attachment->_SplitHeaders($test1);
547 is ($#headers, 0, $test1 );
549 my $test2 = qq{From: jesse
554 @headers = RT::Attachment->_SplitHeaders($test2);
555 is ($#headers, 2, "testing a bunch of singline multiple headers" );
558 my $test3 = qq{From: jesse
566 @headers = RT::Attachment->_SplitHeaders($test3);
567 is ($#headers, 2, "testing a bunch of singline multiple headers" );
576 my $headers = (shift || $self->SUPER::Headers());
578 for (split(/\n(?=\w|\z)/,$headers)) {
589 unless ( (($self->TransactionObj->CurrentUserHasRight('ShowTicketComments')) and
590 ($self->TransactionObj->Type eq 'Comment') ) or
591 ($self->TransactionObj->CurrentUserHasRight('ShowTicket'))) {
595 if (my $len = $self->GetHeader('Content-Length')) {
601 my $len = length($self->Content);
602 $self->SetHeader('Content-Length' => $len);
609 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
613 'fast_update_p' => 1,
614 'cache_for_sec' => 180,