import rt 3.8.9
[freeside.git] / rt / lib / RT / Attachment_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 #                                          <sales@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/licenses/old-licenses/gpl-2.0.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
49 =head1 SYNOPSIS
50
51     use RT::Attachment;
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 =head1 METHODS
60
61
62
63 =cut
64
65
66 package RT::Attachment;
67
68 use strict;
69 no warnings qw(redefine);
70
71 use RT::Transaction;
72 use MIME::Base64;
73 use MIME::QuotedPrint;
74
75 sub _OverlayAccessible {
76   {
77     TransactionId   => { 'read'=>1, 'public'=>1, 'write' => 0 },
78     MessageId       => { 'read'=>1, 'write' => 0 },
79     Parent          => { 'read'=>1, 'write' => 0 },
80     ContentType     => { 'read'=>1, 'write' => 0 },
81     Subject         => { 'read'=>1, 'write' => 0 },
82     Content         => { 'read'=>1, 'write' => 0 },
83     ContentEncoding => { 'read'=>1, 'write' => 0 },
84     Headers         => { 'read'=>1, 'write' => 0 },
85     Filename        => { 'read'=>1, 'write' => 0 },
86     Creator         => { 'read'=>1, 'auto'=>1, },
87     Created         => { 'read'=>1, 'auto'=>1, },
88   };
89 }
90
91 =head2 Create
92
93 Create a new attachment. Takes a paramhash:
94     
95     'Attachment' Should be a single MIME body with optional subparts
96     'Parent' is an optional id of the parent attachment
97     'TransactionId' is the mandatory id of the transaction this attachment is associated with.;
98
99 =cut
100
101 sub Create {
102     my $self = shift;
103     my %args = ( id            => 0,
104                  TransactionId => 0,
105                  Parent        => 0,
106                  Attachment    => undef,
107                  @_ );
108
109     # For ease of reference
110     my $Attachment = $args{'Attachment'};
111
112     # if we didn't specify a ticket, we need to bail
113     unless ( $args{'TransactionId'} ) {
114         $RT::Logger->crit( "RT::Attachment->Create couldn't, as you didn't specify a transaction" );
115         return (0);
116     }
117
118     # If we possibly can, collapse it to a singlepart
119     $Attachment->make_singlepart;
120
121     # Get the subject
122     my $Subject = $Attachment->head->get( 'subject', 0 );
123     $Subject = '' unless defined $Subject;
124     chomp $Subject;
125     utf8::decode( $Subject ) unless utf8::is_utf8( $Subject );
126
127     #Get the Message-ID
128     my $MessageId = $Attachment->head->get( 'Message-ID', 0 );
129     defined($MessageId) or $MessageId = '';
130     chomp ($MessageId);
131     $MessageId =~ s/^<(.*?)>$/$1/o;
132
133     #Get the filename
134     my $Filename = $Attachment->head->recommended_filename;
135     # remove path part. 
136     $Filename =~ s!.*/!! if $Filename;
137
138     # MIME::Head doesn't support perl strings well and can return
139     # octets which later will be double encoded in low-level code
140     my $head = $Attachment->head->as_string;
141     utf8::decode( $head ) unless utf8::is_utf8( $head );
142
143     # If a message has no bodyhandle, that means that it has subparts (or appears to)
144     # and we should act accordingly.  
145     unless ( defined $Attachment->bodyhandle ) {
146         my ($id) = $self->SUPER::Create(
147             TransactionId => $args{'TransactionId'},
148             Parent        => $args{'Parent'},
149             ContentType   => $Attachment->mime_type,
150             Headers       => $head,
151             MessageId     => $MessageId,
152             Subject       => $Subject,
153         );
154
155         unless ($id) {
156             $RT::Logger->crit("Attachment insert failed - ". $RT::Handle->dbh->errstr);
157         }
158
159         foreach my $part ( $Attachment->parts ) {
160             my $SubAttachment = new RT::Attachment( $self->CurrentUser );
161             my ($id) = $SubAttachment->Create(
162                 TransactionId => $args{'TransactionId'},
163                 Parent        => $id,
164                 Attachment    => $part,
165             );
166             unless ($id) {
167                 $RT::Logger->crit("Attachment insert failed: ". $RT::Handle->dbh->errstr);
168             }
169         }
170         return ($id);
171     }
172
173     #If it's not multipart
174     else {
175
176         my ($ContentEncoding, $Body) = $self->_EncodeLOB(
177             $Attachment->bodyhandle->as_string,
178             $Attachment->mime_type
179         );
180
181         my $id = $self->SUPER::Create(
182             TransactionId   => $args{'TransactionId'},
183             ContentType     => $Attachment->mime_type,
184             ContentEncoding => $ContentEncoding,
185             Parent          => $args{'Parent'},
186             Headers         => $head,
187             Subject         => $Subject,
188             Content         => $Body,
189             Filename        => $Filename,
190             MessageId       => $MessageId,
191         );
192
193         unless ($id) {
194             $RT::Logger->crit("Attachment insert failed: ". $RT::Handle->dbh->errstr);
195         }
196         return $id;
197     }
198 }
199
200 =head2 Import
201
202 Create an attachment exactly as specified in the named parameters.
203
204 =cut
205
206 sub Import {
207     my $self = shift;
208     my %args = ( ContentEncoding => 'none', @_ );
209
210     ( $args{'ContentEncoding'}, $args{'Content'} ) =
211         $self->_EncodeLOB( $args{'Content'}, $args{'MimeType'} );
212
213     return ( $self->SUPER::Create(%args) );
214 }
215
216 =head2 TransactionObj
217
218 Returns the transaction object asscoiated with this attachment.
219
220 =cut
221
222 sub TransactionObj {
223     my $self = shift;
224
225     unless ( $self->{_TransactionObj} ) {
226         $self->{_TransactionObj} = RT::Transaction->new( $self->CurrentUser );
227         $self->{_TransactionObj}->Load( $self->TransactionId );
228     }
229
230     unless ($self->{_TransactionObj}->Id) {
231         $RT::Logger->crit(  "Attachment ". $self->id
232                            ." can't find transaction ". $self->TransactionId
233                            ." which it is ostensibly part of. That's bad");
234     }
235     return $self->{_TransactionObj};
236 }
237
238 =head2 ParentObj
239
240 Returns a parent's L<RT::Attachment> object if this attachment
241 has a parent, otherwise returns undef.
242
243 =cut
244
245 sub ParentObj {
246     my $self = shift;
247     return undef unless $self->Parent;
248
249     my $parent = RT::Attachment->new( $self->CurrentUser );
250     $parent->LoadById( $self->Parent );
251     return $parent;
252 }
253
254 =head2 Children
255
256 Returns an L<RT::Attachments> object which is preloaded with
257 all attachments objects with this attachment\'s Id as their
258 C<Parent>.
259
260 =cut
261
262 sub Children {
263     my $self = shift;
264     
265     my $kids = RT::Attachments->new( $self->CurrentUser );
266     $kids->ChildrenOf( $self->Id );
267     return($kids);
268 }
269
270 =head2 Content
271
272 Returns the attachment's content. if it's base64 encoded, decode it 
273 before returning it.
274
275 =cut
276
277 sub Content {
278     my $self = shift;
279     return $self->_DecodeLOB(
280         $self->ContentType,
281         $self->ContentEncoding,
282         $self->_Value('Content', decode_utf8 => 0),
283     );
284 }
285
286 =head2 OriginalContent
287
288 Returns the attachment's content as octets before RT's mangling.
289 Currently, this just means restoring text content back to its
290 original encoding.
291
292 =cut
293
294 sub OriginalContent {
295     my $self = shift;
296
297     return $self->Content unless RT::I18N::IsTextualContentType($self->ContentType);
298     my $enc = $self->OriginalEncoding;
299
300     my $content;
301     if ( !$self->ContentEncoding || $self->ContentEncoding eq 'none' ) {
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         $content = MIME::QuotedPrint::decode($self->_Value('Content', decode_utf8 => 0));
307     } else {
308         return( $self->loc("Unknown ContentEncoding [_1]", $self->ContentEncoding));
309     }
310
311     # Turn *off* the SvUTF8 bits here so decode_utf8 and from_to below can work.
312     local $@;
313     Encode::_utf8_off($content);
314
315     if (!$enc || $enc eq '' ||  $enc eq 'utf8' || $enc eq 'utf-8') {
316         # If we somehow fail to do the decode, at least push out the raw bits
317         eval { return( Encode::decode_utf8($content)) } || return ($content);
318     }
319
320     eval { Encode::from_to($content, 'utf8' => $enc) } if $enc;
321     if ($@) {
322         $RT::Logger->error("Could not convert attachment from assumed utf8 to '$enc' :".$@);
323     }
324     return $content;
325 }
326
327 =head2 OriginalEncoding
328
329 Returns the attachment's original encoding.
330
331 =cut
332
333 sub OriginalEncoding {
334     my $self = shift;
335     return $self->GetHeader('X-RT-Original-Encoding');
336 }
337
338 =head2 ContentLength
339
340 Returns length of L</Content> in bytes.
341
342 =cut
343
344 sub ContentLength {
345     my $self = shift;
346
347     return undef unless $self->TransactionObj->CurrentUserCanSee;
348
349     my $len = $self->GetHeader('Content-Length');
350     unless ( defined $len ) {
351         use bytes;
352         no warnings 'uninitialized';
353         $len = length($self->Content);
354         $self->SetHeader('Content-Length' => $len);
355     }
356     return $len;
357 }
358
359 =head2 Quote
360
361 =cut
362
363 sub Quote {
364     my $self=shift;
365     my %args=(Reply=>undef, # Prefilled reply (i.e. from the KB/FAQ system)
366               @_);
367
368     my ($quoted_content, $body, $headers);
369     my $max=0;
370
371     # TODO: Handle Multipart/Mixed (eventually fix the link in the
372     # ShowHistory web template?)
373     if (RT::I18N::IsTextualContentType($self->ContentType)) {
374         $body=$self->Content;
375
376         # Do we need any preformatting (wrapping, that is) of the message?
377
378         # Remove quoted signature.
379         $body =~ s/\n-- \n(.*)$//s;
380
381         # What's the longest line like?
382         foreach (split (/\n/,$body)) {
383             $max=length if ( length > $max);
384         }
385
386         if ($max>76) {
387             require Text::Wrapper;
388             my $wrapper=new Text::Wrapper
389                 (
390                  columns => 70, 
391                  body_start => ($max > 70*3 ? '   ' : ''),
392                  par_start => ''
393                  );
394             $body=$wrapper->wrap($body);
395         }
396
397         $body =~ s/^/> /gm;
398
399         $body = '[' . $self->TransactionObj->CreatorObj->Name() . ' - ' . $self->TransactionObj->CreatedAsString()
400                     . "]:\n\n"
401                 . $body . "\n\n";
402
403     } else {
404         $body = "[Non-text message not quoted]\n\n";
405     }
406     
407     $max=60 if $max<60;
408     $max=70 if $max>78;
409     $max+=2;
410
411     return (\$body, $max);
412 }
413
414 =head2 ContentAsMIME
415
416 Returns MIME entity built from this attachment.
417
418 =cut
419
420 sub ContentAsMIME {
421     my $self = shift;
422
423     my $entity = new MIME::Entity;
424     foreach my $header ($self->SplitHeaders) {
425         my ($h_key, $h_val) = split /:/, $header, 2;
426         $entity->head->add( $h_key, RT::Interface::Email::EncodeToMIME( String => $h_val ) );
427     }
428     
429     # since we want to return original content, let's use original encoding
430     $entity->head->mime_attr(
431         "Content-Type.charset" => $self->OriginalEncoding )
432       if $self->OriginalEncoding;
433
434     use MIME::Body;
435     $entity->bodyhandle(
436         MIME::Body::Scalar->new( $self->OriginalContent )
437     );
438
439     return $entity;
440 }
441
442
443 =head2 Addresses
444
445 Returns a hashref of all addresses related to this attachment.
446 The keys of the hash are C<From>, C<To>, C<Cc>, C<Bcc>, C<RT-Send-Cc>
447 and C<RT-Send-Bcc>. The values are references to lists of
448 L<Email::Address> objects.
449
450 =cut
451
452 sub Addresses {
453     my $self = shift;
454
455     my %data = ();
456     my $current_user_address = lc $self->CurrentUser->EmailAddress;
457     foreach my $hdr (qw(From To Cc Bcc RT-Send-Cc RT-Send-Bcc)) {
458         my @Addresses;
459         my $line = $self->GetHeader($hdr);
460         
461         foreach my $AddrObj ( Email::Address->parse( $line )) {
462             my $address = $AddrObj->address;
463             $address = lc RT::User->CanonicalizeEmailAddress($address);
464             next if $current_user_address eq $address;
465             next if RT::EmailParser->IsRTAddress($address);
466             push @Addresses, $AddrObj ;
467         }
468         $data{$hdr} = \@Addresses;
469     }
470     return \%data;
471 }
472
473 =head2 NiceHeaders
474
475 Returns a multi-line string of the To, From, Cc, Date and Subject headers.
476
477 =cut
478
479 sub NiceHeaders {
480     my $self = shift;
481     my $hdrs = "";
482     my @hdrs = $self->_SplitHeaders;
483     while (my $str = shift @hdrs) {
484             next unless $str =~ /^(To|From|RT-Send-Cc|Cc|Bcc|Date|Subject):/i;
485             $hdrs .= $str . "\n";
486             $hdrs .= shift( @hdrs ) . "\n" while ($hdrs[0] =~ /^[ \t]+/);
487     }
488     return $hdrs;
489 }
490
491 =head2 Headers
492
493 Returns this object's headers as a string.  This method specifically
494 removes the RT-Send-Bcc: header, so as to never reveal to whom RT sent a Bcc.
495 We need to record the RT-Send-Cc and RT-Send-Bcc values so that we can actually send
496 out mail. The mailing rules are separated from the ticket update code by
497 an abstraction barrier that makes it impossible to pass this data directly.
498
499 =cut
500
501 sub Headers {
502     return join("\n", $_[0]->SplitHeaders);
503 }
504
505 =head2 EncodedHeaders
506
507 Takes encoding as argument and returns the attachment's headers as octets in encoded
508 using the encoding.
509
510 This is not protection using quoted printable or base64 encoding.
511
512 =cut
513
514 sub EncodedHeaders {
515     my $self = shift;
516     my $encoding = shift || 'utf8';
517     return Encode::encode( $encoding, $self->Headers );
518 }
519
520 =head2 GetHeader $TAG
521
522 Returns the value of the header Tag as a string. This bypasses the weeding out
523 done in Headers() above.
524
525 =cut
526
527 sub GetHeader {
528     my $self = shift;
529     my $tag = shift;
530     foreach my $line ($self->_SplitHeaders) {
531         next unless $line =~ /^\Q$tag\E:\s+(.*)$/si;
532
533         #if we find the header, return its value
534         return ($1);
535     }
536     
537     # we found no header. return an empty string
538     return undef;
539 }
540
541 =head2 DelHeader $TAG
542
543 Delete a field from the attachment's headers.
544
545 =cut
546
547 sub DelHeader {
548     my $self = shift;
549     my $tag = shift;
550
551     my $newheader = '';
552     foreach my $line ($self->_SplitHeaders) {
553         next if $line =~ /^\Q$tag\E:\s+(.*)$/is;
554         $newheader .= "$line\n";
555     }
556     return $self->__Set( Field => 'Headers', Value => $newheader);
557 }
558
559 =head2 AddHeader $TAG, $VALUE, ...
560
561 Add one or many fields to the attachment's headers.
562
563 =cut
564
565 sub AddHeader {
566     my $self = shift;
567
568     my $newheader = $self->__Value( 'Headers' );
569     while ( my ($tag, $value) = splice @_, 0, 2 ) {
570         $value = '' unless defined $value;
571         $value =~ s/\s+$//s;
572         $value =~ s/\r+\n/\n /g;
573         $newheader .= "$tag: $value\n";
574     }
575     return $self->__Set( Field => 'Headers', Value => $newheader);
576 }
577
578 =head2 SetHeader ( 'Tag', 'Value' )
579
580 Replace or add a Header to the attachment's headers.
581
582 =cut
583
584 sub SetHeader {
585     my $self = shift;
586     my $tag = shift;
587
588     my $newheader = '';
589     foreach my $line ($self->_SplitHeaders) {
590         if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
591             $newheader .= "$tag: $_[0]\n";
592             undef $tag;
593         }
594         else {
595             $newheader .= "$line\n";
596         }
597     }
598
599     $newheader .= "$tag: $_[0]\n" if defined $tag;
600     $self->__Set( Field => 'Headers', Value => $newheader);
601 }
602
603 =head2 SplitHeaders
604
605 Returns an array of this attachment object's headers, with one header 
606 per array entry. Multiple lines are folded.
607
608 B<Never> returns C<RT-Send-Bcc> field.
609
610 =cut
611
612 sub SplitHeaders {
613     my $self = shift;
614     return (grep !/^RT-Send-Bcc/i, $self->_SplitHeaders(@_) );
615 }
616
617 =head2 _SplitHeaders
618
619 Returns an array of this attachment object's headers, with one header 
620 per array entry. multiple lines are folded.
621
622
623 =cut
624
625 sub _SplitHeaders {
626     my $self = shift;
627     my $headers = (shift || $self->SUPER::Headers());
628     my @headers;
629     for (split(/\n(?=\w|\z)/,$headers)) {
630         push @headers, $_;
631
632     }
633     return(@headers);
634 }
635
636
637 sub Encrypt {
638     my $self = shift;
639
640     my $txn = $self->TransactionObj;
641     return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
642     return (0, $self->loc('Permission Denied'))
643         unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
644     return (0, $self->loc('GnuPG integration is disabled'))
645         unless RT->Config->Get('GnuPG')->{'Enable'};
646     return (0, $self->loc('Attachments encryption is disabled'))
647         unless RT->Config->Get('GnuPG')->{'AllowEncryptDataInDB'};
648
649     require RT::Crypt::GnuPG;
650
651     my $type = $self->ContentType;
652     if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
653         return (1, $self->loc('Already encrypted'));
654     } elsif ( $type =~ /^multipart\//i ) {
655         return (1, $self->loc('No need to encrypt'));
656     } else {
657         $type = qq{x-application-rt\/gpg-encrypted; original-type="$type"};
658     }
659
660     my $queue = $txn->TicketObj->QueueObj;
661     my $encrypt_for;
662     foreach my $address ( grep $_,
663         $queue->CorrespondAddress,
664         $queue->CommentAddress,
665         RT->Config->Get('CorrespondAddress'),
666         RT->Config->Get('CommentAddress'),
667     ) {
668         my %res = RT::Crypt::GnuPG::GetKeysInfo( $address, 'private' );
669         next if $res{'exit_code'} || !$res{'info'};
670         %res = RT::Crypt::GnuPG::GetKeysForEncryption( $address );
671         next if $res{'exit_code'} || !$res{'info'};
672         $encrypt_for = $address;
673     }
674     unless ( $encrypt_for ) {
675         return (0, $self->loc('No key suitable for encryption'));
676     }
677
678     $self->__Set( Field => 'ContentType', Value => $type );
679     $self->SetHeader( 'Content-Type' => $type );
680
681     my $content = $self->Content;
682     my %res = RT::Crypt::GnuPG::SignEncryptContent(
683         Content => \$content,
684         Sign => 0,
685         Encrypt => 1,
686         Recipients => [ $encrypt_for ],
687     );
688     if ( $res{'exit_code'} ) {
689         return (0, $self->loc('GnuPG error. Contact with administrator'));
690     }
691
692     my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
693     unless ( $status ) {
694         return ($status, $self->loc("Couldn't replace content with encrypted data: [_1]", $msg));
695     }
696     return (1, $self->loc('Successfuly encrypted data'));
697 }
698
699 sub Decrypt {
700     my $self = shift;
701
702     my $txn = $self->TransactionObj;
703     return (0, $self->loc('Permission Denied')) unless $txn->CurrentUserCanSee;
704     return (0, $self->loc('Permission Denied'))
705         unless $txn->TicketObj->CurrentUserHasRight('ModifyTicket');
706     return (0, $self->loc('GnuPG integration is disabled'))
707         unless RT->Config->Get('GnuPG')->{'Enable'};
708
709     require RT::Crypt::GnuPG;
710
711     my $type = $self->ContentType;
712     if ( $type =~ /^x-application-rt\/gpg-encrypted/i ) {
713         ($type) = ($type =~ /original-type="(.*)"/i);
714         $type ||= 'application/octeat-stream';
715     } else {
716         return (1, $self->loc('Is not encrypted'));
717     }
718     $self->__Set( Field => 'ContentType', Value => $type );
719     $self->SetHeader( 'Content-Type' => $type );
720
721     my $content = $self->Content;
722     my %res = RT::Crypt::GnuPG::DecryptContent( Content => \$content, );
723     if ( $res{'exit_code'} ) {
724         return (0, $self->loc('GnuPG error. Contact with administrator'));
725     }
726
727     my ($status, $msg) = $self->__Set( Field => 'Content', Value => $content );
728     unless ( $status ) {
729         return ($status, $self->loc("Couldn't replace content with decrypted data: [_1]", $msg));
730     }
731     return (1, $self->loc('Successfuly decrypted data'));
732 }
733
734 =head2 _Value
735
736 Takes the name of a table column.
737 Returns its value as a string, if the user passes an ACL check
738
739 =cut
740
741 sub _Value {
742     my $self  = shift;
743     my $field = shift;
744
745     #if the field is public, return it.
746     if ( $self->_Accessible( $field, 'public' ) ) {
747         return ( $self->__Value( $field, @_ ) );
748     }
749
750     return undef unless $self->TransactionObj->CurrentUserCanSee;
751     return $self->__Value( $field, @_ );
752 }
753
754 # Transactions don't change. by adding this cache congif directiove,
755 # we don't lose pathalogically on long tickets.
756 sub _CacheConfig {
757     {
758         'cache_p'       => 1,
759         'fast_update_p' => 1,
760         'cache_for_sec' => 180,
761     }
762 }
763
764 1;