3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
5 # (Except where explictly superceded by other copyright notices)
7 # This work is made available to you under the terms of Version 2 of
8 # the GNU General Public License. A copy of that license should have
9 # been provided with this software, but in any event can be snarfed
12 # This work is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # Unless otherwise specified, all modifications, corrections or
18 # extensions to this work which alter its source code become the
19 # property of Best Practical Solutions, LLC when submitted for
20 # inclusion in the work.
31 This module should never be instantiated directly by client code. it's an internal
32 module which should only be instantiated through exported APIs in Ticket, Queue and other
41 ok (require RT::Attachment);
48 no warnings qw(redefine);
51 use MIME::QuotedPrint;
53 # {{{ sub _ClassAccessible
54 sub _ClassAccessible {
56 TransactionId => { 'read'=>1, 'public'=>1, },
57 MessageId => { 'read'=>1, },
58 Parent => { 'read'=>1, },
59 ContentType => { 'read'=>1, },
60 Subject => { 'read'=>1, },
61 Content => { 'read'=>1, },
62 ContentEncoding => { 'read'=>1, },
63 Headers => { 'read'=>1, },
64 Filename => { 'read'=>1, },
65 Creator => { 'read'=>1, 'auto'=>1, },
66 Created => { 'read'=>1, 'auto'=>1, },
71 # {{{ sub TransactionObj
75 Returns the transaction object asscoiated with this attachment.
80 require RT::Transaction;
82 unless (exists $self->{_TransactionObj}) {
83 $self->{_TransactionObj}=RT::Transaction->new($self->CurrentUser);
84 $self->{_TransactionObj}->Load($self->TransactionId);
86 return $self->{_TransactionObj};
95 Create a new attachment. Takes a paramhash:
97 'Attachment' Should be a single MIME body with optional subparts
98 'Parent' is an optional Parent RT::Attachment object
99 'TransactionId' is the mandatory id of the Transaction this attachment is associated with.;
106 my %args = ( id => 0,
112 #For ease of reference
113 my $Attachment = $args{'Attachment'};
115 #if we didn't specify a ticket, we need to bail
116 if ( $args{'TransactionId'} == 0 ) {
117 $RT::Logger->crit( "RT::Attachment->Create couldn't, as you didn't specify a transaction\n" );
122 #If we possibly can, collapse it to a singlepart
123 $Attachment->make_singlepart;
126 my $Subject = $Attachment->head->get( 'subject', 0 );
127 defined($Subject) or $Subject = '';
131 my $Filename = $Attachment->head->recommended_filename || eval {
132 ${ $Attachment->head->{mail_hdr_hash}{'Content-Disposition'}[0] }
133 =~ /^.*\bfilename="(.*)"$/ ? $1 : ''
136 if ( $Attachment->parts ) {
137 $id = $self->SUPER::Create(
138 TransactionId => $args{'TransactionId'},
140 ContentType => $Attachment->mime_type,
141 Headers => $Attachment->head->as_string,
142 Subject => $Subject);
144 foreach my $part ( $Attachment->parts ) {
145 my $SubAttachment = new RT::Attachment( $self->CurrentUser );
146 $SubAttachment->Create(
147 TransactionId => $args{'TransactionId'},
150 ContentType => $Attachment->mime_type,
151 Headers => $Attachment->head->as_string(),
158 #If it's not multipart
161 my $ContentEncoding = 'none';
163 my $Body = $Attachment->bodyhandle->as_string;
165 #get the max attachment length from RT
166 my $MaxSize = $RT::MaxAttachmentSize;
168 #if the current attachment contains nulls and the
169 #database doesn't support embedded nulls
171 if ( $RT::AlwaysUseBase64 or
172 ( !$RT::Handle->BinarySafeBLOBs ) && ( $Body =~ /\x00/ ) ) {
174 # set a flag telling us to mimencode the attachment
175 $ContentEncoding = 'base64';
177 #cut the max attchment size by 25% (for mime-encoding overhead.
178 $RT::Logger->debug("Max size is $MaxSize\n");
179 $MaxSize = $MaxSize * 3 / 4;
180 # Some databases (postgres) can't handle non-utf8 data
181 } elsif ( !$RT::Handle->BinarySafeBLOBs
182 && $Attachment->mime_type !~ /text\/plain/gi
183 && !Encode::is_utf8( $Body, 1 ) ) {
184 $ContentEncoding = 'quoted-printable';
187 #if the attachment is larger than the maximum size
188 if ( ($MaxSize) and ( $MaxSize < length($Body) ) ) {
190 # if we're supposed to truncate large attachments
191 if ($RT::TruncateLongAttachments) {
193 # truncate the attachment to that length.
194 $Body = substr( $Body, 0, $MaxSize );
198 # elsif we're supposed to drop large attachments on the floor,
199 elsif ($RT::DropLongAttachments) {
201 # drop the attachment on the floor
202 $RT::Logger->info( "$self: Dropped an attachment of size " . length($Body) . "\n" . "It started: " . substr( $Body, 0, 60 ) . "\n" );
207 # if we need to mimencode the attachment
208 if ( $ContentEncoding eq 'base64' ) {
210 # base64 encode the attachment
211 Encode::_utf8_off($Body);
212 $Body = MIME::Base64::encode_base64($Body);
214 } elsif ($ContentEncoding eq 'quoted-printable') {
215 Encode::_utf8_off($Body);
216 $Body = MIME::QuotedPrint::encode($Body);
220 my $id = $self->SUPER::Create( TransactionId => $args{'TransactionId'},
221 ContentType => $Attachment->mime_type,
222 ContentEncoding => $ContentEncoding,
223 Parent => $args{'Parent'},
224 Headers => $Attachment->head->as_string,
227 Filename => $Filename, );
237 Create an attachment exactly as specified in the named parameters.
244 return($self->SUPER::Create(@_));
251 Returns the attachment's content. if it's base64 encoded, decode it
258 my $decode_utf8 = (($self->ContentType eq 'text/plain') ? 1 : 0);
260 if ( $self->ContentEncoding eq 'none' || ! $self->ContentEncoding ) {
261 return $self->_Value(
263 decode_utf8 => $decode_utf8,
265 } elsif ( $self->ContentEncoding eq 'base64' ) {
266 return ( $decode_utf8
267 ? Encode::decode_utf8(MIME::Base64::decode_base64($self->_Value('Content')))
268 : MIME::Base64::decode_base64($self->_Value('Content'))
270 } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
271 return ( $decode_utf8
272 ? Encode::decode_utf8(MIME::QuotedPrint::decode($self->_Value('Content')))
273 : MIME::QuotedPrint::decode($self->_Value('Content'))
276 return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
284 # {{{ sub OriginalContent
286 =head2 OriginalContent
288 Returns the attachment's content as octets before RT's mangling.
289 Currently, this just means restoring text/plain content back to its
294 sub OriginalContent {
297 return $self->Content unless $self->ContentType eq 'text/plain';
298 my $enc = $self->OriginalEncoding;
301 if ( $self->ContentEncoding eq 'none' || ! $self->ContentEncoding ) {
302 $content = $self->_Value('Content', decode_utf8 => 0);
303 } elsif ( $self->ContentEncoding eq 'base64' ) {
304 $content = MIME::Base64::decode_base64($self->_Value('Content', decode_utf8 => 0));
305 } elsif ( $self->ContentEncoding eq 'quoted-printable' ) {
306 return MIME::QuotedPrint::decode($self->_Value('Content', decode_utf8 => 0));
308 return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
311 # Encode::_utf8_on($content);
312 if (!$enc or $enc eq 'utf8' or $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);
316 Encode::from_to($content, 'utf8' => $enc);
323 # {{{ sub OriginalEncoding
325 =head2 OriginalEncoding
327 Returns the attachment's original encoding.
331 sub OriginalEncoding {
333 return $self->GetHeader('X-RT-Original-Encoding');
342 Returns an RT::Attachments object which is preloaded with all Attachments objects with this Attachment\'s Id as their 'Parent'
349 my $kids = new RT::Attachments($self->CurrentUser);
350 $kids->ChildrenOf($self->Id);
364 my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
367 my ($quoted_content, $body, $headers);
370 # TODO: Handle Multipart/Mixed (eventually fix the link in the
371 # ShowHistory web template?)
372 if ($self->ContentType =~ m{^(text/plain|message)}i) {
373 $body=$self->Content;
375 # Do we need any preformatting (wrapping, that is) of the message?
377 # Remove quoted signature.
378 $body =~ s/\n-- \n(.*)$//s;
380 # What's the longest line like?
381 foreach (split (/\n/,$body)) {
382 $max=length if ( length > $max);
386 require Text::Wrapper;
387 my $wrapper=new Text::Wrapper
390 body_start => ($max > 70*3 ? ' ' : ''),
393 $body=$wrapper->wrap($body);
398 $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
403 $body = "[Non-text message not quoted]\n\n";
410 return (\$body, $max);
414 # {{{ sub NiceHeaders - pulls out only the most relevant headers
418 Returns the To, From, Cc, Date and Subject headers.
420 It is a known issue that this breaks if any of these headers are not
428 for (split(/\n/,$self->Headers)) {
429 $hdrs.="$_\n" if /^(To|From|RT-Send-Cc|Cc|Date|Subject): /i
439 Returns this object's headers as a string. This method specifically
440 removes the RT-Send-Bcc: header, so as to never reveal to whom RT sent a Bcc.
441 We need to record the RT-Send-Cc and RT-Send-Bcc values so that we can actually send
442 out mail. (The mailing rules are seperated from the ticket update code by
443 an abstraction barrier that makes it impossible to pass this data directly
450 for (split(/\n/,$self->SUPER::Headers)) {
451 $hdrs.="$_\n" unless /^(RT-Send-Bcc): /i
461 =head2 GetHeader ( 'Tag')
463 Returns the value of the header Tag as a string. This bypasses the weeding out
464 done in Headers() above.
471 foreach my $line (split(/\n/,$self->SUPER::Headers)) {
472 if ($line =~ /^\Q$tag\E:\s+(.*)$/i) { #if we find the header, return its value
477 # we found no header. return an empty string
484 =head2 SetHeader ( 'Tag', 'Value' )
486 Replace or add a Header to the attachment's headers.
495 foreach my $line (split(/\n/,$self->SUPER::Headers)) {
496 if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
497 $newheader .= "$tag: $_[0]\n";
501 $newheader .= "$line\n";
505 $newheader .= "$tag: $_[0]\n" if defined $tag;
506 $self->__Set( Field => 'Headers', Value => $newheader);
514 Takes the name of a table column.
515 Returns its value as a string, if the user passes an ACL check
525 #if the field is public, return it.
526 if ($self->_Accessible($field, 'public')) {
527 #$RT::Logger->debug("Skipping ACL check for $field\n");
528 return($self->__Value($field, @_));
532 #If it's a comment, we need to be extra special careful
533 elsif ( (($self->TransactionObj->CurrentUserHasRight('ShowTicketComments')) and
534 ($self->TransactionObj->Type eq 'Comment') ) or
535 ($self->TransactionObj->CurrentUserHasRight('ShowTicket'))) {
536 return($self->__Value($field, @_));
538 #if they ain't got rights to see, don't let em
551 unless ( (($self->TransactionObj->CurrentUserHasRight('ShowTicketComments')) and
552 ($self->TransactionObj->Type eq 'Comment') ) or
553 ($self->TransactionObj->CurrentUserHasRight('ShowTicket'))) {
557 if (my $len = $self->GetHeader('Content-Length')) {
563 my $len = length($self->Content);
564 $self->SetHeader('Content-Length' => $len);