summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Interface/Email.pm
diff options
context:
space:
mode:
authorivan <ivan>2002-08-12 06:17:09 +0000
committerivan <ivan>2002-08-12 06:17:09 +0000
commit3ef62a0570055da710328937e7f65dbb2c027c62 (patch)
treed549158b172fd499b4f81a2981b62aabbde4f99b /rt/lib/RT/Interface/Email.pm
parent030438c9cb1c12ccb79130979ef0922097b4311a (diff)
import rt 2.0.14
Diffstat (limited to 'rt/lib/RT/Interface/Email.pm')
-rwxr-xr-xrt/lib/RT/Interface/Email.pm581
1 files changed, 581 insertions, 0 deletions
diff --git a/rt/lib/RT/Interface/Email.pm b/rt/lib/RT/Interface/Email.pm
new file mode 100755
index 000000000..e95436091
--- /dev/null
+++ b/rt/lib/RT/Interface/Email.pm
@@ -0,0 +1,581 @@
+# $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 <jesse@fsck.com>
+
+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<except> 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;