import rt 3.6.4
[freeside.git] / rt / lib / RT / Attachment_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC 
6 #                                          <jesse@bestpractical.com>
7
8 # (Except where explicitly superseded by other copyright notices)
9
10
11 # LICENSE:
12
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
16 # from www.gnu.org.
17
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.
22
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.
28
29
30 # CONTRIBUTION SUBMISSION POLICY:
31
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.)
37
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.
46
47 # END BPS TAGGED BLOCK }}}
48 =head1 SYNOPSIS
49
50   use RT::Attachment;
51
52
53 =head1 DESCRIPTION
54
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 
57 similar objects.
58
59
60 =head1 METHODS
61
62
63 =begin testing
64
65 ok (require RT::Attachment);
66
67 =end testing
68
69 =cut
70
71
72 package RT::Attachment;
73
74 use strict;
75 no warnings qw(redefine);
76
77 use MIME::Base64;
78 use MIME::QuotedPrint;
79
80
81 # {{{ sub _OverlayAccessible 
82 sub _OverlayAccessible {
83     {
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, },
95   };
96 }
97 # }}}
98
99 # {{{ sub TransactionObj 
100
101 =head2 TransactionObj
102
103 Returns the transaction object asscoiated with this attachment.
104
105 =cut
106
107 sub TransactionObj {
108     require RT::Transaction;
109     my $self=shift;
110     unless (exists $self->{_TransactionObj}) {
111         $self->{_TransactionObj}=RT::Transaction->new($self->CurrentUser);
112         $self->{_TransactionObj}->Load($self->TransactionId);
113     }
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");
116     }
117     return $self->{_TransactionObj};
118 }
119
120 # }}}
121
122 # {{{ sub Create 
123
124 =head2 Create
125
126 Create a new attachment. Takes a paramhash:
127     
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.;
131
132 =cut
133
134 sub Create {
135     my $self = shift;
136     my %args = ( id            => 0,
137                  TransactionId => 0,
138                  Parent        => 0,
139                  Attachment    => undef,
140                  @_ );
141
142     #For ease of reference
143     my $Attachment = $args{'Attachment'};
144
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" );
148         return (0);
149
150     }
151
152     #If we possibly can, collapse it to a singlepart
153     $Attachment->make_singlepart;
154
155     #Get the subject
156     my $Subject = $Attachment->head->get( 'subject', 0 );
157     defined($Subject) or $Subject = '';
158     chomp($Subject);
159
160     #Get the Message-ID
161     my $MessageId = $Attachment->head->get( 'Message-ID', 0 );
162     defined($MessageId) or $MessageId = '';
163     chomp ($MessageId);
164     $MessageId =~ s/^<(.*)>$/$1/go;
165
166
167     #Get the filename
168     my $Filename = $Attachment->head->recommended_filename;
169
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 ) {
173
174         my $id = $self->SUPER::Create(
175             TransactionId => $args{'TransactionId'},
176             Parent        => 0,
177             ContentType   => $Attachment->mime_type,
178             Headers       => $Attachment->head->as_string,
179             MessageId     => $MessageId,
180             Subject       => $Subject
181         );
182         
183         unless ($id) {
184             $RT::Logger->crit("Attachment insert failed - ".$RT::Handle->dbh->errstr);
185         }
186
187         foreach my $part ( $Attachment->parts ) {
188             my $SubAttachment = new RT::Attachment( $self->CurrentUser );
189             my ($id) = $SubAttachment->Create(
190                 TransactionId => $args{'TransactionId'},
191                 Parent        => $id,
192                 Attachment    => $part,
193             );
194             unless ($id) {
195                 $RT::Logger->crit("Attachment insert failed - ".$RT::Handle->dbh->errstr);
196             }
197         }
198         return ($id);
199     }
200
201     #If it's not multipart
202     else {
203
204         my ($ContentEncoding, $Body) = $self->_EncodeLOB( $Attachment->bodyhandle->as_string,
205                                                           $Attachment->mime_type 
206                                                         );
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,
213             Subject         => $Subject,
214             Content         => $Body,
215             Filename        => $Filename,
216             MessageId       => $MessageId,
217         );
218         unless ($id) {
219             $RT::Logger->crit("Attachment insert failed - ".$RT::Handle->dbh->errstr);
220         }
221
222         return ($id);
223     }
224 }
225
226 # }}}
227
228
229 =head2 Import
230
231 Create an attachment exactly as specified in the named parameters.
232
233 =cut
234
235
236 sub Import {
237     my $self = shift;
238     my %args = ( ContentEncoding => 'none',
239
240                  @_ );
241
242
243  ($args{'ContentEncoding'}, $args{'Content'}) = $self->_EncodeLOB($args{'Content'}, $args{'MimeType'});
244
245     return($self->SUPER::Create(%args));
246 }
247
248 # {{{ sub Content
249
250 =head2 Content
251
252 Returns the attachment's content. if it's base64 encoded, decode it 
253 before returning it.
254
255 =cut
256
257 sub Content {
258   my $self = shift;
259    $self->_DecodeLOB($self->ContentType, $self->ContentEncoding, $self->_Value('Content', decode_utf8 => 0));
260 }
261
262
263 # }}}
264
265
266 # {{{ sub OriginalContent
267
268 =head2 OriginalContent
269
270 Returns the attachment's content as octets before RT's mangling.
271 Currently, this just means restoring text content back to its
272 original encoding.
273
274 =cut
275
276 sub OriginalContent {
277   my $self = shift;
278
279   return $self->Content unless (
280      $self->ContentType =~ qr{^(text/plain|message/rfc822)$}i) ;
281   my $enc = $self->OriginalEncoding;
282
283   my $content;
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));
290   } else {
291       return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
292   }
293
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);
298   }
299   
300   eval { Encode::from_to($content, 'utf8' => $enc) } if $enc;
301   if ($@) {
302         $RT::Logger->error("Could not convert attachment from assumed utf8 to '$enc' :".$@);
303   }
304   return $content;
305 }
306
307 # }}}
308
309
310 # {{{ sub OriginalEncoding
311
312 =head2 OriginalEncoding
313
314 Returns the attachment's original encoding.
315
316 =cut
317
318 sub OriginalEncoding {
319   my $self = shift;
320   return $self->GetHeader('X-RT-Original-Encoding');
321 }
322
323 # }}}
324
325 # {{{ sub Children
326
327 =head2 Children
328
329   Returns an RT::Attachments object which is preloaded with all Attachments objects with this Attachment\'s Id as their 'Parent'
330
331 =cut
332
333 sub Children {
334     my $self = shift;
335     
336     my $kids = new RT::Attachments($self->CurrentUser);
337     $kids->ChildrenOf($self->Id);
338     return($kids);
339 }
340
341 # }}}
342
343 # {{{ UTILITIES
344
345 # {{{ sub Quote 
346
347
348
349 sub Quote {
350     my $self=shift;
351     my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
352               @_);
353
354     my ($quoted_content, $body, $headers);
355     my $max=0;
356
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;
361
362         # Do we need any preformatting (wrapping, that is) of the message?
363
364         # Remove quoted signature.
365         $body =~ s/\n-- \n(.*)$//s;
366
367         # What's the longest line like?
368         foreach (split (/\n/,$body)) {
369             $max=length if ( length > $max);
370         }
371
372         if ($max>76) {
373             require Text::Wrapper;
374             my $wrapper=new Text::Wrapper
375                 (
376                  columns => 70, 
377                  body_start => ($max > 70*3 ? '   ' : ''),
378                  par_start => ''
379                  );
380             $body=$wrapper->wrap($body);
381         }
382
383         $body =~ s/^/> /gm;
384
385         $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
386                     . "]:\n\n"
387                 . $body . "\n\n";
388
389     } else {
390         $body = "[Non-text message not quoted]\n\n";
391     }
392     
393     $max=60 if $max<60;
394     $max=70 if $max>78;
395     $max+=2;
396
397     return (\$body, $max);
398 }
399 # }}}
400
401 # {{{ sub NiceHeaders - pulls out only the most relevant headers
402
403 =head2 NiceHeaders
404
405 Returns a multi-line string of the To, From, Cc, Date and Subject headers.
406
407 =cut
408
409 sub NiceHeaders {
410     my $self = shift;
411     my $hdrs = "";
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]+/);
417     }
418     return $hdrs;
419 }
420 # }}}
421
422 # {{{ sub Headers
423
424 =head2 Headers
425
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
431
432 =cut
433
434 sub Headers {
435     my $self = shift;
436     my $hdrs="";
437     my @headers =  grep { !/^RT-Send-Bcc/i } $self->_SplitHeaders;
438     return join("\n",@headers);
439
440 }
441
442
443 # }}}
444
445 # {{{ sub GetHeader
446
447 =head2 GetHeader ( 'Tag')
448
449 Returns the value of the header Tag as a string. This bypasses the weeding out
450 done in Headers() above.
451
452 =cut
453
454 sub GetHeader {
455     my $self = shift;
456     my $tag = shift;
457     foreach my $line ($self->_SplitHeaders) {
458         if ($line =~ /^\Q$tag\E:\s+(.*)$/si) { #if we find the header, return its value
459             return ($1);
460         }
461     }
462     
463     # we found no header. return an empty string
464     return undef;
465 }
466 # }}}
467
468 # {{{ sub SetHeader
469
470 =head2 SetHeader ( 'Tag', 'Value' )
471
472 Replace or add a Header to the attachment's headers.
473
474 =cut
475
476 sub SetHeader {
477     my $self = shift;
478     my $tag = shift;
479     my $newheader = '';
480
481     foreach my $line ($self->_SplitHeaders) {
482         if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
483             $newheader .= "$tag: $_[0]\n";
484             undef $tag;
485         }
486         else {
487             $newheader .= "$line\n";
488         }
489     }
490
491     $newheader .= "$tag: $_[0]\n" if defined $tag;
492     $self->__Set( Field => 'Headers', Value => $newheader);
493 }
494 # }}}
495
496 # {{{ sub _Value 
497
498 =head2 _Value
499
500 Takes the name of a table column.
501 Returns its value as a string, if the user passes an ACL check
502
503 =cut
504
505 sub _Value {
506
507     my $self  = shift;
508     my $field = shift;
509
510     #if the field is public, return it.
511     if ( $self->_Accessible( $field, 'public' ) ) {
512         return ( $self->__Value( $field, @_ ) );
513     }
514
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') )
518         {
519             return ( $self->__Value( $field, @_ ) );
520         }
521     }
522     elsif ( $self->TransactionObj->CurrentUserHasRight('ShowTicket') ) {
523         return ( $self->__Value( $field, @_ ) );
524     }
525
526     #if they ain't got rights to see, don't let em
527     else {
528         return (undef);
529     }
530
531 }
532
533 # }}}
534
535 =head2 _SplitHeaders
536
537 Returns an array of this attachment object's headers, with one header 
538 per array entry. multiple lines are folded.
539
540 =begin testing
541
542 my $test1 = "From: jesse";
543 my @headers = RT::Attachment->_SplitHeaders($test1);
544 is ($#headers, 0, $test1 );
545
546 my $test2 = qq{From: jesse
547 To: bobby
548 Subject: foo
549 };
550
551 @headers = RT::Attachment->_SplitHeaders($test2);
552 is ($#headers, 2, "testing a bunch of singline multiple headers" );
553
554
555 my $test3 = qq{From: jesse
556 To: bobby,
557  Suzie,
558     Sally,
559     Joey: bizzy,
560 Subject: foo
561 };
562
563 @headers = RT::Attachment->_SplitHeaders($test3);
564 is ($#headers, 2, "testing a bunch of singline multiple headers" );
565
566
567 =end testing
568
569 =cut
570
571 sub _SplitHeaders {
572     my $self = shift;
573     my $headers = (shift || $self->SUPER::Headers());
574     my @headers;
575     for (split(/\n(?=\w|\z)/,$headers)) {
576         push @headers, $_;
577
578     }
579     return(@headers);
580 }
581
582
583 sub ContentLength {
584     my $self = shift;
585
586     unless ( (($self->TransactionObj->CurrentUserHasRight('ShowTicketComments')) and
587              ($self->TransactionObj->Type eq 'Comment') )  or
588             ($self->TransactionObj->CurrentUserHasRight('ShowTicket'))) {
589         return undef;
590     }
591
592     if (my $len = $self->GetHeader('Content-Length')) {
593         return $len;
594     }
595
596     {
597         use bytes;
598         my $len = length($self->Content);
599         $self->SetHeader('Content-Length' => $len);
600         return $len;
601     }
602 }
603
604 # }}}
605
606 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
607 sub _CacheConfig {
608     {
609         'cache_p'         => 1,
610           'fast_update_p' => 1,
611           'cache_for_sec' => 180,
612     }
613 }
614
615 1;