1 # {{{ BEGIN BPS TAGGED BLOCK
5 # This software is Copyright (c) 1996-2004 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
53 This module should never be instantiated directly by client code. it's an internal
54 module which should only be instantiated through exported APIs in Ticket, Queue and other
63 ok (require RT::Attachment);
70 no warnings qw(redefine);
73 use MIME::QuotedPrint;
76 # {{{ sub _OverlayAccessible
77 sub _OverlayAccessible {
79 TransactionId => { 'read'=>1, 'public'=>1, 'write' => 0 },
80 MessageId => { 'read'=>1, 'write' => 0 },
81 Parent => { 'read'=>1, 'write' => 0 },
82 ContentType => { 'read'=>1, 'write' => 0 },
83 Subject => { 'read'=>1, 'write' => 0 },
84 Content => { 'read'=>1, 'write' => 0 },
85 ContentEncoding => { 'read'=>1, 'write' => 0 },
86 Headers => { 'read'=>1, 'write' => 0 },
87 Filename => { 'read'=>1, 'write' => 0 },
88 Creator => { 'read'=>1, 'auto'=>1, },
89 Created => { 'read'=>1, 'auto'=>1, },
94 # {{{ sub TransactionObj
98 Returns the transaction object asscoiated with this attachment.
103 require RT::Transaction;
105 unless (exists $self->{_TransactionObj}) {
106 $self->{_TransactionObj}=RT::Transaction->new($self->CurrentUser);
107 $self->{_TransactionObj}->Load($self->TransactionId);
109 unless ($self->{_TransactionObj}->Id) {
110 $RT::Logger->crit("Attachment ".$self->id." can't find transaction ".$self->TransactionId." which it is ostensibly part of. That's bad");
112 return $self->{_TransactionObj};
121 Create a new attachment. Takes a paramhash:
123 'Attachment' Should be a single MIME body with optional subparts
124 'Parent' is an optional Parent RT::Attachment object
125 'TransactionId' is the mandatory id of the Transaction this attachment is associated with.;
132 my %args = ( id => 0,
138 #For ease of reference
139 my $Attachment = $args{'Attachment'};
141 #if we didn't specify a ticket, we need to bail
142 if ( $args{'TransactionId'} == 0 ) {
143 $RT::Logger->crit( "RT::Attachment->Create couldn't, as you didn't specify a transaction\n" );
148 #If we possibly can, collapse it to a singlepart
149 $Attachment->make_singlepart;
152 my $Subject = $Attachment->head->get( 'subject', 0 );
153 defined($Subject) or $Subject = '';
157 my $Filename = $Attachment->head->recommended_filename || eval {
158 ${ $Attachment->head->{mail_hdr_hash}{'Content-Disposition'}[0] }
159 =~ /^.*\bfilename="(.*)"$/ ? $1 : ''
162 # If a message has no bodyhandle, that means that it has subparts (or appears to)
163 # and we should act accordingly.
164 unless ( defined $Attachment->bodyhandle ) {
165 $id = $self->SUPER::Create(
166 TransactionId => $args{'TransactionId'},
168 ContentType => $Attachment->mime_type,
169 Headers => $Attachment->head->as_string,
170 Subject => $Subject);
172 foreach my $part ( $Attachment->parts ) {
173 my $SubAttachment = new RT::Attachment( $self->CurrentUser );
174 $SubAttachment->Create(
175 TransactionId => $args{'TransactionId'},
178 ContentType => $Attachment->mime_type,
179 Headers => $Attachment->head->as_string(),
186 #If it's not multipart
190 my $Body = $Attachment->bodyhandle->as_string;
193 my ($ContentEncoding, $Body) = $self->_EncodeLOB($Attachment->bodyhandle->as_string, $Attachment->mime_type);
196 my $id = $self->SUPER::Create( TransactionId => $args{'TransactionId'},
197 ContentType => $Attachment->mime_type,
198 ContentEncoding => $ContentEncoding,
199 Parent => $args{'Parent'},
200 Headers => $Attachment->head->as_string,
203 Filename => $Filename, );
213 Create an attachment exactly as specified in the named parameters.
220 my %args = ( ContentEncoding => 'none',
223 return($self->SUPER::Create(@_));
230 Returns the attachment's content. if it's base64 encoded, decode it
237 my $decode_utf8 = (($self->ContentType =~ qr{^text/plain}i) ? 1 : 0);
239 if ( $self->ContentEncoding eq 'none' || ! $self->ContentEncoding ) {
240 return $self->_Value(
242 decode_utf8 => $decode_utf8,
244 } elsif ( $self->ContentEncoding eq 'base64' ) {
245 return ( $decode_utf8
246 ? Encode::decode_utf8(MIME::Base64::decode_base64($self->_Value('Content')))
247 : MIME::Base64::decode_base64($self->_Value('Content'))
249 } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
250 return ( $decode_utf8
251 ? Encode::decode_utf8(MIME::QuotedPrint::decode($self->_Value('Content')))
252 : MIME::QuotedPrint::decode($self->_Value('Content'))
255 return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
263 # {{{ sub OriginalContent
265 =head2 OriginalContent
267 Returns the attachment's content as octets before RT's mangling.
268 Currently, this just means restoring text/plain content back to its
273 sub OriginalContent {
276 return $self->Content unless $self->ContentType eq 'text/plain';
277 my $enc = $self->OriginalEncoding;
280 if ( $self->ContentEncoding eq 'none' || ! $self->ContentEncoding ) {
281 $content = $self->_Value('Content', decode_utf8 => 0);
282 } elsif ( $self->ContentEncoding eq 'base64' ) {
283 $content = MIME::Base64::decode_base64($self->_Value('Content', decode_utf8 => 0));
284 } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
285 return MIME::QuotedPrint::decode($self->_Value('Content', decode_utf8 => 0));
287 return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
290 # Encode::_utf8_on($content);
291 if (!$enc || $enc eq '' || $enc eq 'utf8' || $enc eq 'utf-8') {
292 # If we somehow fail to do the decode, at least push out the raw bits
293 eval {return( Encode::decode_utf8($content))} || return ($content);
296 eval { Encode::from_to($content, 'utf8' => $enc);};
298 $RT::Logger->error("Could not convert attachment from assumed utf8 to '$enc' :".$@);
306 # {{{ sub OriginalEncoding
308 =head2 OriginalEncoding
310 Returns the attachment's original encoding.
314 sub OriginalEncoding {
316 return $self->GetHeader('X-RT-Original-Encoding');
325 Returns an RT::Attachments object which is preloaded with all Attachments objects with this Attachment\'s Id as their 'Parent'
332 my $kids = new RT::Attachments($self->CurrentUser);
333 $kids->ChildrenOf($self->Id);
347 my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
350 my ($quoted_content, $body, $headers);
353 # TODO: Handle Multipart/Mixed (eventually fix the link in the
354 # ShowHistory web template?)
355 if ($self->ContentType =~ m{^(text/plain|message)}i) {
356 $body=$self->Content;
358 # Do we need any preformatting (wrapping, that is) of the message?
360 # Remove quoted signature.
361 $body =~ s/\n-- \n(.*)$//s;
363 # What's the longest line like?
364 foreach (split (/\n/,$body)) {
365 $max=length if ( length > $max);
369 require Text::Wrapper;
370 my $wrapper=new Text::Wrapper
373 body_start => ($max > 70*3 ? ' ' : ''),
376 $body=$wrapper->wrap($body);
381 $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
386 $body = "[Non-text message not quoted]\n\n";
393 return (\$body, $max);
397 # {{{ sub NiceHeaders - pulls out only the most relevant headers
401 Returns the To, From, Cc, Date and Subject headers.
403 It is a known issue that this breaks if any of these headers are not
411 my @hdrs = split(/\n/,$self->Headers);
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 for ($self->_SplitHeaders) {
437 $hdrs.="$_\n" unless /^(RT-Send-Bcc):/i
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,