1 # $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Interface/Email.pm,v 1.1 2002-08-12 06:17:08 ivan Exp $
2 # RT is (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
4 package RT::Interface::Email;
12 use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
14 # set the version for version checking
15 $VERSION = do { my @r = (q$Revision: 1.1 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker
19 # your exported package globals go here,
20 # as well as any optionally exported functions
21 @EXPORT_OK = qw(&CleanEnv
27 &CheckForSuspiciousSender
28 &CheckForAutoGenerated
29 &ParseMIMEEntityFromSTDIN
32 &ParseCcAddressesFromHead
33 &ParseSenderAddressFromHead
34 &ParseErrorsToAddressFromHead
35 &ParseAddressFromHeader
43 RT::Interface::CLI - helper functions for creating a commandline RT interface
47 use lib "!!RT_LIB_PATH!!";
48 use lib "!!RT_ETC_PATH!!";
50 use RT::Interface::Email qw(CleanEnv LoadConfig DBConnect
53 #Clean out all the nasties from the environment
56 #Load etc/config.pm and drop privs
59 #Connect to the database and get RT::SystemUser and RT::Nobody loaded
63 #Get the current user all loaded
64 my $CurrentUser = GetCurrentUser();
71 ok(require RT::TestHarness);
72 ok(require RT::Interface::Email);
84 Removes some of the nastiest nasties from the user\'s environment.
89 $ENV{'PATH'} = '/bin:/usr/bin'; # or whatever you need
90 $ENV{'CDPATH'} = '' if defined $ENV{'CDPATH'};
91 $ENV{'SHELL'} = '/bin/sh' if defined $ENV{'SHELL'};
92 $ENV{'ENV'} = '' if defined $ENV{'ENV'};
93 $ENV{'IFS'} = '' if defined $ENV{'IFS'};
100 Loads RT's config file and then drops setgid privileges.
106 #This drags in RT's config.pm
115 Calls RT::Init, which creates a database connection and then creates $RT::Nobody
134 $RT::Logger->debug($val."\n");
136 print STDERR "$val\n";
147 # {{{ sub CheckForLoops
152 #If this instance of RT sent it our, we don't want to take it in
153 my $RTLoop = $head->get("X-RT-Loop-Prevention") || "";
154 chomp ($RTLoop); #remove that newline
155 if ($RTLoop eq "$RT::rtname") {
159 # TODO: We might not trap the case where RT instance A sends a mail
160 # to RT instance B which sends a mail to ...
166 # {{{ sub CheckForSuspiciousSender
168 sub CheckForSuspiciousSender {
171 #if it's from a postmaster or mailer daemon, it's likely a bounce.
173 #TODO: better algorithms needed here - there is no standards for
174 #bounces, so it's very difficult to separate them from anything
175 #else. At the other hand, the Return-To address is only ment to be
176 #used as an error channel, we might want to put up a separate
177 #Return-To address which is treated differently.
179 #TODO: search through the whole email and find the right Ticket ID.
181 my ($From, $junk) = ParseSenderAddressFromHead($head);
183 if (($From =~ /^mailer-daemon/i) or
184 ($From =~ /^postmaster/i)){
195 # {{{ sub CheckForAutoGenerated
196 sub CheckForAutoGenerated {
199 my $Precedence = $head->get("Precedence") || "" ;
200 if ($Precedence =~ /^(bulk|junk)/i) {
210 # {{{ sub ParseMIMEEntityFromSTDIN
212 sub ParseMIMEEntityFromSTDIN {
214 # Create a new parser object:
216 my $parser = new MIME::Parser;
218 # {{{ Config $parser to store large attacments in temp dir
220 ## TODO: Does it make sense storing to disk at all? After all, we
221 ## need to put each msg as an in-core scalar before saving it to
222 ## the database, don't we?
224 ## At the same time, we should make sure that we nuke attachments
225 ## Over max size and return them
227 ## TODO: Remove the temp dir when we don't need it any more.
229 my $AttachmentDir = File::Temp::tempdir (TMPDIR => 1, CLEANUP => 1);
231 # Set up output directory for files:
232 $parser->output_dir("$AttachmentDir");
234 #If someone includes a message, don't extract it
235 $parser->extract_nested_messages(0);
238 # Set up the prefix for files with auto-generated names:
239 $parser->output_prefix("part");
241 # If content length is <= 20000 bytes, store each msg as in-core scalar;
242 # Else, write to a disk file (the default action):
244 $parser->output_to_core(20000);
246 # }}} (temporary directory)
248 #Ok. now that we're set up, let's get the stdin.
250 unless ($entity = $parser->read(\*STDIN)) {
251 die "couldn't parse MIME stream";
253 #Now we've got a parsed mime object.
255 # Get the head, a MIME::Head:
256 my $head = $entity->head;
259 # Unfold headers that are have embedded newlines
262 # TODO - information about the charset is lost here!
265 return ($entity, $head);
270 # {{{ sub ParseTicketId
276 if ($Subject =~ s/\[$RT::rtname \#(\d+)\]//i) {
278 $RT::Logger->debug("Found a ticket ID. It's $Id");
289 my %args = (To => $RT::OwnerEmail,
291 From => $RT::CorrespondAddress,
292 Subject => 'There has been an error',
293 Explanation => 'Unexplained error',
299 $RT::Logger->log(level => $args{'LogLevel'},
300 message => $args{'Explanation'}
302 my $entity = MIME::Entity->build( Type =>"multipart/mixed",
303 From => $args{'From'},
306 Subject => $args{'Subject'},
307 'X-RT-Loop-Prevention' => $RT::rtname,
310 $entity->attach( Data => $args{'Explanation'}."\n");
312 my $mimeobj = $args{'MIMEObj'};
314 $mimeobj->sync_headers();
315 $entity->add_part($mimeobj);
318 if ($RT::MailCommand eq 'sendmailpipe') {
319 open (MAIL, "|$RT::SendmailPath $RT::SendmailArguments") || return(0);
320 print MAIL $entity->as_string;
324 $entity->send($RT::MailCommand, $RT::MailParams);
330 # {{{ sub GetCurrentUser
335 my $ErrorsTo = shift;
339 #Suck the address of the sender out of the header
340 my ($Address, $Name) = ParseSenderAddressFromHead($head);
342 #This will apply local address canonicalization rules
343 $Address = RT::CanonicalizeAddress($Address);
345 #If desired, synchronize with an external database
347 my $UserFoundInExternalDatabase = 0;
349 # Username is the 'Name' attribute of the user that RT uses for things
350 # like authentication
351 my $Username = undef;
352 if ($RT::LookupSenderInExternalDatabase) {
353 ($UserFoundInExternalDatabase, %UserInfo) =
354 RT::LookupExternalUserInfo($Address, $Name);
356 $Address = $UserInfo{'EmailAddress'};
357 $Username = $UserInfo{'Name'};
360 my $CurrentUser = RT::CurrentUser->new();
362 # First try looking up by a username, if we got one from the external
363 # db lookup. Next, try looking up by email address. Failing that,
364 # try looking up by users who have this user's email address as their
368 $CurrentUser->LoadByName($Username);
371 unless ($CurrentUser->Id) {
372 $CurrentUser->LoadByEmail($Address);
375 #If we can't get it by email address, try by name.
376 unless ($CurrentUser->Id) {
377 $CurrentUser->LoadByName($Address);
381 unless ($CurrentUser->Id) {
382 #If we couldn't load a user, determine whether to create a user
384 # {{{ If we require an incoming address to be found in the external
385 # user database, reject the incoming message appropriately
386 if ( $RT::LookupSenderInExternalDatabase &&
387 $RT::SenderMustExistInExternalDatabase &&
388 !$UserFoundInExternalDatabase) {
390 my $Message = "Sender's email address was not found in the user database.";
392 # {{{ This code useful only if you've defined an AutoRejectRequest template
394 require RT::Template;
395 my $template = new RT::Template($RT::Nobody);
396 $template->Load('AutoRejectRequest');
397 $Message = $template->Content || $Message;
401 MailError( To => $ErrorsTo,
402 Subject => "Ticket Creation failed: user could not be created",
403 Explanation => $Message,
408 return($CurrentUser);
414 my $NewUser = RT::User->new($RT::SystemUser);
416 my ($Val, $Message) =
417 $NewUser->Create(Name => ($Username || $Address),
418 EmailAddress => $Address,
422 Comments => 'Autocreated on ticket submission'
427 # Deal with the race condition of two account creations at once
430 $NewUser->LoadByName($Username);
433 unless ($NewUser->Id) {
434 $NewUser->LoadByEmail($Address);
437 unless ($NewUser->Id) {
438 MailError( To => $ErrorsTo,
439 Subject => "User could not be created",
440 Explanation => "User creation failed in mailgateway: $Message",
448 #Load the new user object
449 $CurrentUser->LoadByEmail($Address);
451 unless ($CurrentUser->id) {
452 $RT::Logger->warning("Couldn't load user '$Address'.". "giving up");
453 MailError( To => $ErrorsTo,
454 Subject => "User could not be loaded",
455 Explanation => "User '$Address' could not be loaded in the mail gateway",
463 return ($CurrentUser);
468 # {{{ ParseCcAddressesFromHead
470 =head2 ParseCcAddressesFromHead HASHREF
472 Takes a hashref object containing QueueObj, Head and CurrentUser objects.
473 Returns a list of all email addresses in the To and Cc
474 headers b<except> the current Queue\'s email addresses, the CurrentUser\'s
475 email address and anything that the configuration sub RT::IsRTAddress matches.
479 sub ParseCcAddressesFromHead {
480 my %args = ( Head => undef,
482 CurrentUser => undef,
487 my @ToObjs = Mail::Address->parse($args{'Head'}->get('To'));
488 my @CcObjs = Mail::Address->parse($args{'Head'}->get('Cc'));
490 foreach my $AddrObj (@ToObjs, @CcObjs) {
491 my $Address = $AddrObj->address;
492 $Address = RT::CanonicalizeAddress($Address);
493 next if ($args{'CurrentUser'}->EmailAddress =~ /^$Address$/i);
494 next if ($args{'QueueObj'}->CorrespondAddress =~ /^$Address$/i);
495 next if ($args{'QueueObj'}->CommentAddress =~ /^$Address$/i);
496 next if (RT::IsRTAddress($Address));
498 push (@Addresses, $Address);
506 # {{{ ParseSenderAdddressFromHead
508 =head2 ParseSenderAddressFromHead
510 Takes a MIME::Header object. Returns a tuple: (user@host, friendly name)
511 of the From (evaluated in order of Reply-To:, From:, Sender)
515 sub ParseSenderAddressFromHead {
517 #Figure out who's sending this message.
518 my $From = $head->get('Reply-To') ||
519 $head->get('From') ||
520 $head->get('Sender');
521 return (ParseAddressFromHeader($From));
525 # {{{ ParseErrorsToAdddressFromHead
527 =head2 ParseErrorsToAddressFromHead
529 Takes a MIME::Header object. Return a single value : user@host
530 of the From (evaluated in order of Errors-To:,Reply-To:, From:, Sender)
534 sub ParseErrorsToAddressFromHead {
536 #Figure out who's sending this message.
538 foreach my $header ('Errors-To' , 'Reply-To', 'From', 'Sender' ) {
539 # If there's a header of that name
540 my $headerobj = $head->get($header);
542 my ($addr, $name ) = ParseAddressFromHeader($headerobj);
543 # If it's got actual useful content...
544 return ($addr) if ($addr);
550 # {{{ ParseAddressFromHeader
552 =head2 ParseAddressFromHeader ADDRESS
554 Takes an address from $head->get('Line') and returns a tuple: user@host, friendly name
559 sub ParseAddressFromHeader{
562 my @Addresses = Mail::Address->parse($Addr);
564 my $AddrObj = $Addresses[0];
566 unless (ref($AddrObj)) {
570 my $Name = ($AddrObj->phrase || $AddrObj->comment || $AddrObj->address);
573 #Lets take the from and load a user object.
574 my $Address = $AddrObj->address;
576 return ($Address, $Name);