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