#!/usr/bin/env python
# -*- mode: python -*-
#
# Check and decode PGP signed emails.
# This script implements a wrapper around another program. It takes a mail
# on stdin and processes off a PGP signature, verifying it and seperating 
# out the checked plaintext. It then invokes a sub process and feeds it
# the verified plain text and sets environment vairables indicating the 
# result of the PGP check. If PGP checking fails then the subprocess is
# is never run and a bounce message is generated. The wrapper can understand
# PGP-MIME and all signatures supported by GPG. It completely decodes 
# PGP-MIME before running the subprocess. It also can do optional
# anti-replay checking on the signatures.
#
# If enabled it can also do LDAP checking to determine the uniq UID owner
# of the key.
#
# Options:
#  -r  Replay cache file, if unset replay checking is disabled
#  -e  Bounce error message template file, if unset very ugly bounces are 
#      made
#  -k  Colon seperated list of keyrings to use
#  -a  Reply to address (mail daemon administrator)
#  -d  LDAP search base DN
#  -l  LDAP server
#  -m  Email address to use when prettying up LDAP_EMAIL
#
# It exports the following environment variables:
#  LDAP_EMAIL="Adam Di Carlo <aph@debian.org>"
#  LDAP_UID="aph"
#  PGP_FINGERPRINT="E21E5D13FAD42A54F1AA5A00D801CE55"
#  PGP_KEYID="8FFC405EFD5A67CD"
#  PGP_KEYNAME="Adam Di Carlo <aph@debian.org> "
#  SENDER (from mailer - envelope sender for bounces)
#  REPLYTO (generated from message headers)
#
# Typical Debian invokation may look like:
# ./gpgwrapper -k /usr/share/keyrings/debian-keyring.gpg:/usr/share/keyrings/debian-keyring.pgp \
#      -d ou=users,dc=debian,dc=org -l db.debian.org \
#      -m debian.org -a admin@db.debian.org \
#      -e /etc/userdir-ldap/templtes/error-reply -- test.sh
      
import sys, traceback, time, os;
import pwd, getopt;
from userdir_gpg import *;

EX_TEMPFAIL = 75;
EX_PERMFAIL = 65;      # EX_DATAERR
Error = 'Message Error';
ReplyTo = "admin@db";

# Configuration
ReplayCacheFile = None;
ErrorTemplate = None;
LDAPDn = None;
LDAPServer = None;
EmailAppend = "";

# Safely get an attribute from a tuple representing a dn and an attribute
# list. It returns the first attribute if there are multi.
def GetAttr(DnRecord,Attribute,Default = ""):
   try:
      return DnRecord[1][Attribute][0];
   except IndexError:
      return Default;
   except KeyError:
      return Default;
   return Default;

# Return a printable email address from the attributes.
def EmailAddress(DnRecord):
   cn = GetAttr(DnRecord,"cn");
   sn = GetAttr(DnRecord,"sn");
   uid = GetAttr(DnRecord,"uid");
   if cn == "" and sn == "":
      return "<" + uid + "@" + EmailAppend + ">";
   return cn + " " + sn + " <" + uid + "@" + EmailAppend + ">"

# Match the key fingerprint against an LDAP directory
def CheckLDAP(FingerPrint):
   import ldap;
   
   # Connect to the ldap server
   global ErrTyp, ErrMsg;
   ErrType = EX_TEMPFAIL;
   ErrMsg = "An error occured while performing the LDAP lookup";
   global l;
   l = connectLDAP(LDAPServer);
   l.simple_bind_s("","");

   # Search for the matching key fingerprint
   Attrs = l.search_s(LDAPDn,ldap.SCOPE_ONELEVEL,"keyfingerprint=" + FingerPrint);
   if len(Attrs) == 0:
      raise Error, "Key not found"
   if len(Attrs) != 1:
      raise Error, "Oddly your key fingerprint is assigned to more than one account.."

   os.environ["LDAP_UID"] = GetAttr(Attrs[0],"uid");
   os.environ["LDAP_EMAIL"] = EmailAddress(Attrs[0]);
   
# Start of main program
# Process options
(options, arguments) = getopt.getopt(sys.argv[1:], "r:e:k:a:d:l:m:");
for (switch, val) in options:
   if (switch == '-r'):
      ReplayCacheFile = val;
   elif (switch == '-e'):
      ErrorTemplate  = val;
   elif (switch == '-k'):
      SetKeyrings(val.split(":"));
   elif (switch == '-a'):
      ReplyTo = val;
   elif (switch == '-d'):
      LDAPDn = val;
   elif (switch == '-l'):
      LDAPServer = val;
   elif (switch == '-m'):
      EmailAppend = val;
      
# Drop messages from a mailer daemon. (empty sender)
if os.environ.has_key('SENDER') == 0 or len(os.environ['SENDER']) == 0:
   sys.exit(0);

ErrMsg = "Indeterminate Error";
ErrType = EX_TEMPFAIL;
try:
   # Startup the replay cache
   ErrType = EX_TEMPFAIL;
   if ReplayCacheFile != None:
      ErrMsg = "Failed to initialize the replay cache:";
      RC = ReplayCache(ReplayCacheFile);
      RC.Clean();
   
   # Get the email 
   ErrType = EX_PERMFAIL;
   ErrMsg = "Failed to understand the email or find a signature:";
   Email = mimetools.Message(sys.stdin,0);
   Msg = GetClearSig(Email);

   ErrMsg = "Message is not PGP signed:"
   if Msg[0].find("-----BEGIN PGP SIGNED MESSAGE-----") == -1:
      raise Error, "No PGP signature";
   
   # Check the signature
   ErrMsg = "Unable to check the signature or the signature was invalid:";
   Res = GPGCheckSig(Msg[0]);

   if Res[0] != None:
      raise Error, Res[0];
      
   if Res[3] == None:
      raise Error, "Null signature text";

   # Extract the plain message text in the event of mime encoding
   global PlainText;
   ErrMsg = "Problem stripping MIME headers from the decoded message"
   if Msg[1] == 1:
      try:
         Index = Res[3].index("\n\n") + 2;
      except ValueError:
         Index = Res[3].index("\n\r\n") + 3;
      PlainText = Res[3][Index:];
   else:
      PlainText = Res[3];   

   # Check the signature against the replay cache
   if ReplayCacheFile != None:
      ErrMsg = "The replay cache rejected your message. Check your clock!";
      Rply = RC.Check(Res[1]);
      if Rply != None:
         raise Error, Rply;
      RC.Add(Res[1]);

   # Do LDAP stuff
   if LDAPDn != None:
      CheckLDAP(Res[2][1]);
      
   # Determine the sender address
   ErrType = EX_PERMFAIL;
   ErrMsg = "A problem occured while trying to formulate the reply";
   Sender = Email.getheader("Reply-To");
   if Sender == None:
      Sender = Email.getheader("From");
   if Sender == None:
      raise Error, "Unable to determine the sender's address";
      
   # Setup the environment
   ErrType = EX_TEMPFAIL;
   ErrMsg = "Problem calling the child process"
   os.environ["PGP_KEYID"] = Res[2][0];
   os.environ["PGP_FINGERPRINT"] = Res[2][1];
   os.environ["PGP_KEYNAME"] = Res[2][2];
   os.environ["REPLYTO"] = Sender;
   
   # Invoke the child
   Child = os.popen(" ".join(arguments),"w");
   Child.write(PlainText);
   if Child.close() != None:
      raise Error, "Child gave a non-zero return code";
   
except:
   # Error Reply Header
   Date = time.strftime("%a, %d %b %Y %H:%M:%S +0000",time.gmtime(time.time()));
   ErrReplyHead = "To: %s\nReply-To: %s\nDate: %s\n" % (os.environ['SENDER'],ReplyTo,Date);

   # Error Body
   Subst = {};
   Subst["__ERROR__"] = ErrMsg;
   Subst["__ADMIN__"] = ReplyTo;

   Trace = "==> %s: %s\n" %(sys.exc_type,sys.exc_value);
   List = traceback.extract_tb(sys.exc_traceback);
   if len(List) >= 1:
      Trace = Trace + "Python Stack Trace:\n";
      for x in List:
         Trace = Trace +  "   %s %s:%u: %s\n" %(x[2],x[0],x[1],x[3]);
	 
   Subst["__TRACE__"] = Trace;

   # Try to send the bounce
   try:
      if ErrorTemplate != None:
         ErrReply = TemplateSubst(Subst,open(ErrorTemplate,"r").read());
      else:
         ErrReply = "\n"+str(Subst)+"\n";
	 
      Child = os.popen("/usr/sbin/sendmail -t","w");
      Child.write(ErrReplyHead);
      Child.write(ErrReply);
      if Child.close() != None:
         raise Error, "Sendmail gave a non-zero return code";
   except:
      sys.exit(EX_TEMPFAIL);
      
   if ErrType != EX_PERMFAIL:
      sys.exit(ErrType);
   sys.exit(0);
   
