# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Interface/Email.pm,v 1.1 2002-08-12 06:17:08 ivan Exp $ # RT is (c) 1996-2001 Jesse Vincent package RT::Interface::Email; use strict; use Mail::Address; use MIME::Entity; BEGIN { use Exporter (); use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); # set the version for version checking $VERSION = do { my @r = (q$Revision: 1.1 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker @ISA = qw(Exporter); # your exported package globals go here, # as well as any optionally exported functions @EXPORT_OK = qw(&CleanEnv &LoadConfig &DBConnect &GetCurrentUser &GetMessageContent &CheckForLoops &CheckForSuspiciousSender &CheckForAutoGenerated &ParseMIMEEntityFromSTDIN &ParseTicketId &MailError &ParseCcAddressesFromHead &ParseSenderAddressFromHead &ParseErrorsToAddressFromHead &ParseAddressFromHeader &debug); } =head1 NAME RT::Interface::CLI - helper functions for creating a commandline RT interface =head1 SYNOPSIS use lib "!!RT_LIB_PATH!!"; use lib "!!RT_ETC_PATH!!"; use RT::Interface::Email qw(CleanEnv LoadConfig DBConnect ); #Clean out all the nasties from the environment CleanEnv(); #Load etc/config.pm and drop privs LoadConfig(); #Connect to the database and get RT::SystemUser and RT::Nobody loaded DBConnect(); #Get the current user all loaded my $CurrentUser = GetCurrentUser(); =head1 DESCRIPTION =begin testing ok(require RT::TestHarness); ok(require RT::Interface::Email); =end testing =head1 METHODS =cut =head2 CleanEnv Removes some of the nastiest nasties from the user\'s environment. =cut sub CleanEnv { $ENV{'PATH'} = '/bin:/usr/bin'; # or whatever you need $ENV{'CDPATH'} = '' if defined $ENV{'CDPATH'}; $ENV{'SHELL'} = '/bin/sh' if defined $ENV{'SHELL'}; $ENV{'ENV'} = '' if defined $ENV{'ENV'}; $ENV{'IFS'} = '' if defined $ENV{'IFS'}; } =head2 LoadConfig Loads RT's config file and then drops setgid privileges. =cut sub LoadConfig { #This drags in RT's config.pm use config; } =head2 DBConnect Calls RT::Init, which creates a database connection and then creates $RT::Nobody and $RT::SystemUser =cut sub DBConnect { use RT; RT::Init(); } # {{{ sub debug sub debug { my $val = shift; my ($debug); if ($val) { $RT::Logger->debug($val."\n"); if ($debug) { print STDERR "$val\n"; } } if ($debug) { return(1); } } # }}} # {{{ sub CheckForLoops sub CheckForLoops { my $head = shift; #If this instance of RT sent it our, we don't want to take it in my $RTLoop = $head->get("X-RT-Loop-Prevention") || ""; chomp ($RTLoop); #remove that newline if ($RTLoop eq "$RT::rtname") { return (1); } # TODO: We might not trap the case where RT instance A sends a mail # to RT instance B which sends a mail to ... return (undef); } # }}} # {{{ sub CheckForSuspiciousSender sub CheckForSuspiciousSender { my $head = shift; #if it's from a postmaster or mailer daemon, it's likely a bounce. #TODO: better algorithms needed here - there is no standards for #bounces, so it's very difficult to separate them from anything #else. At the other hand, the Return-To address is only ment to be #used as an error channel, we might want to put up a separate #Return-To address which is treated differently. #TODO: search through the whole email and find the right Ticket ID. my ($From, $junk) = ParseSenderAddressFromHead($head); if (($From =~ /^mailer-daemon/i) or ($From =~ /^postmaster/i)){ return (1); } return (undef); } # }}} # {{{ sub CheckForAutoGenerated sub CheckForAutoGenerated { my $head = shift; my $Precedence = $head->get("Precedence") || "" ; if ($Precedence =~ /^(bulk|junk)/i) { return (1); } else { return (0); } } # }}} # {{{ sub ParseMIMEEntityFromSTDIN sub ParseMIMEEntityFromSTDIN { # Create a new parser object: my $parser = new MIME::Parser; # {{{ Config $parser to store large attacments in temp dir ## TODO: Does it make sense storing to disk at all? After all, we ## need to put each msg as an in-core scalar before saving it to ## the database, don't we? ## At the same time, we should make sure that we nuke attachments ## Over max size and return them ## TODO: Remove the temp dir when we don't need it any more. my $AttachmentDir = File::Temp::tempdir (TMPDIR => 1, CLEANUP => 1); # Set up output directory for files: $parser->output_dir("$AttachmentDir"); #If someone includes a message, don't extract it $parser->extract_nested_messages(0); # Set up the prefix for files with auto-generated names: $parser->output_prefix("part"); # If content length is <= 20000 bytes, store each msg as in-core scalar; # Else, write to a disk file (the default action): $parser->output_to_core(20000); # }}} (temporary directory) #Ok. now that we're set up, let's get the stdin. my $entity; unless ($entity = $parser->read(\*STDIN)) { die "couldn't parse MIME stream"; } #Now we've got a parsed mime object. # Get the head, a MIME::Head: my $head = $entity->head; # Unfold headers that are have embedded newlines $head->unfold; # TODO - information about the charset is lost here! $head->decode; return ($entity, $head); } # }}} # {{{ sub ParseTicketId sub ParseTicketId { my $Subject = shift; my ($Id); if ($Subject =~ s/\[$RT::rtname \#(\d+)\]//i) { $Id = $1; $RT::Logger->debug("Found a ticket ID. It's $Id"); return($Id); } else { return(undef); } } # }}} # {{{ sub MailError sub MailError { my %args = (To => $RT::OwnerEmail, Bcc => undef, From => $RT::CorrespondAddress, Subject => 'There has been an error', Explanation => 'Unexplained error', MIMEObj => undef, LogLevel => 'crit', @_); $RT::Logger->log(level => $args{'LogLevel'}, message => $args{'Explanation'} ); my $entity = MIME::Entity->build( Type =>"multipart/mixed", From => $args{'From'}, Bcc => $args{'Bcc'}, To => $args{'To'}, Subject => $args{'Subject'}, 'X-RT-Loop-Prevention' => $RT::rtname, ); $entity->attach( Data => $args{'Explanation'}."\n"); my $mimeobj = $args{'MIMEObj'}; if ($mimeobj) { $mimeobj->sync_headers(); $entity->add_part($mimeobj); } if ($RT::MailCommand eq 'sendmailpipe') { open (MAIL, "|$RT::SendmailPath $RT::SendmailArguments") || return(0); print MAIL $entity->as_string; close(MAIL); } else { $entity->send($RT::MailCommand, $RT::MailParams); } } # }}} # {{{ sub GetCurrentUser sub GetCurrentUser { my $head = shift; my $entity = shift; my $ErrorsTo = shift; my %UserInfo = (); #Suck the address of the sender out of the header my ($Address, $Name) = ParseSenderAddressFromHead($head); #This will apply local address canonicalization rules $Address = RT::CanonicalizeAddress($Address); #If desired, synchronize with an external database my $UserFoundInExternalDatabase = 0; # Username is the 'Name' attribute of the user that RT uses for things # like authentication my $Username = undef; if ($RT::LookupSenderInExternalDatabase) { ($UserFoundInExternalDatabase, %UserInfo) = RT::LookupExternalUserInfo($Address, $Name); $Address = $UserInfo{'EmailAddress'}; $Username = $UserInfo{'Name'}; } my $CurrentUser = RT::CurrentUser->new(); # First try looking up by a username, if we got one from the external # db lookup. Next, try looking up by email address. Failing that, # try looking up by users who have this user's email address as their # username. if ($Username) { $CurrentUser->LoadByName($Username); } unless ($CurrentUser->Id) { $CurrentUser->LoadByEmail($Address); } #If we can't get it by email address, try by name. unless ($CurrentUser->Id) { $CurrentUser->LoadByName($Address); } unless ($CurrentUser->Id) { #If we couldn't load a user, determine whether to create a user # {{{ If we require an incoming address to be found in the external # user database, reject the incoming message appropriately if ( $RT::LookupSenderInExternalDatabase && $RT::SenderMustExistInExternalDatabase && !$UserFoundInExternalDatabase) { my $Message = "Sender's email address was not found in the user database."; # {{{ This code useful only if you've defined an AutoRejectRequest template require RT::Template; my $template = new RT::Template($RT::Nobody); $template->Load('AutoRejectRequest'); $Message = $template->Content || $Message; # }}} MailError( To => $ErrorsTo, Subject => "Ticket Creation failed: user could not be created", Explanation => $Message, MIMEObj => $entity, LogLevel => 'notice' ); return($CurrentUser); } # }}} else { my $NewUser = RT::User->new($RT::SystemUser); my ($Val, $Message) = $NewUser->Create(Name => ($Username || $Address), EmailAddress => $Address, RealName => "$Name", Password => undef, Privileged => 0, Comments => 'Autocreated on ticket submission' ); unless ($Val) { # Deal with the race condition of two account creations at once # if ($Username) { $NewUser->LoadByName($Username); } unless ($NewUser->Id) { $NewUser->LoadByEmail($Address); } unless ($NewUser->Id) { MailError( To => $ErrorsTo, Subject => "User could not be created", Explanation => "User creation failed in mailgateway: $Message", MIMEObj => $entity, LogLevel => 'crit' ); } } } #Load the new user object $CurrentUser->LoadByEmail($Address); unless ($CurrentUser->id) { $RT::Logger->warning("Couldn't load user '$Address'.". "giving up"); MailError( To => $ErrorsTo, Subject => "User could not be loaded", Explanation => "User '$Address' could not be loaded in the mail gateway", MIMEObj => $entity, LogLevel => 'crit' ); } } return ($CurrentUser); } # }}} # {{{ ParseCcAddressesFromHead =head2 ParseCcAddressesFromHead HASHREF Takes a hashref object containing QueueObj, Head and CurrentUser objects. Returns a list of all email addresses in the To and Cc headers b the current Queue\'s email addresses, the CurrentUser\'s email address and anything that the configuration sub RT::IsRTAddress matches. =cut sub ParseCcAddressesFromHead { my %args = ( Head => undef, QueueObj => undef, CurrentUser => undef, @_ ); my (@Addresses); my @ToObjs = Mail::Address->parse($args{'Head'}->get('To')); my @CcObjs = Mail::Address->parse($args{'Head'}->get('Cc')); foreach my $AddrObj (@ToObjs, @CcObjs) { my $Address = $AddrObj->address; $Address = RT::CanonicalizeAddress($Address); next if ($args{'CurrentUser'}->EmailAddress =~ /^$Address$/i); next if ($args{'QueueObj'}->CorrespondAddress =~ /^$Address$/i); next if ($args{'QueueObj'}->CommentAddress =~ /^$Address$/i); next if (RT::IsRTAddress($Address)); push (@Addresses, $Address); } return (@Addresses); } # }}} # {{{ ParseSenderAdddressFromHead =head2 ParseSenderAddressFromHead Takes a MIME::Header object. Returns a tuple: (user@host, friendly name) of the From (evaluated in order of Reply-To:, From:, Sender) =cut sub ParseSenderAddressFromHead { my $head = shift; #Figure out who's sending this message. my $From = $head->get('Reply-To') || $head->get('From') || $head->get('Sender'); return (ParseAddressFromHeader($From)); } # }}} # {{{ ParseErrorsToAdddressFromHead =head2 ParseErrorsToAddressFromHead Takes a MIME::Header object. Return a single value : user@host of the From (evaluated in order of Errors-To:,Reply-To:, From:, Sender) =cut sub ParseErrorsToAddressFromHead { my $head = shift; #Figure out who's sending this message. foreach my $header ('Errors-To' , 'Reply-To', 'From', 'Sender' ) { # If there's a header of that name my $headerobj = $head->get($header); if ($headerobj) { my ($addr, $name ) = ParseAddressFromHeader($headerobj); # If it's got actual useful content... return ($addr) if ($addr); } } } # }}} # {{{ ParseAddressFromHeader =head2 ParseAddressFromHeader ADDRESS Takes an address from $head->get('Line') and returns a tuple: user@host, friendly name =cut sub ParseAddressFromHeader{ my $Addr = shift; my @Addresses = Mail::Address->parse($Addr); my $AddrObj = $Addresses[0]; unless (ref($AddrObj)) { return(undef,undef); } my $Name = ($AddrObj->phrase || $AddrObj->comment || $AddrObj->address); #Lets take the from and load a user object. my $Address = $AddrObj->address; return ($Address, $Name); } # }}} 1;