Has anyone here ever dealt with PayPal development,
e.g., scripts called by their IPN (Instant Payment
Notification)?

With the help of a couple of people, I ported the
Perl script to Ruby... but I haven't got it to
work.

Code below, if you're curious.

Hal


require 'cgi'
require 'net/http'

###################### Change as needed ######################
PASSWORD_FILE = ''      # path of .htpassword file
TRANSACTION_FILE = ''   # Path of processed_txns file
PERIOD1 = nil           # Initial trial period; ex "1 M" = 1 month
PERIOD2 = nil           # Second trial period
PERIOD3 = nil           # Recurring/normal period
AMOUNT1 = nil           # Dollar amts for each period (free = 0.00)
AMOUNT2 = nil
AMOUNT3 = nil
SENDMAIL_PATH = '/usr/sbin/sendmail'   # nil if none
ADMIN_EMAIL = nil       # For error notifications; nil if none
PAYMENT_EMAILS = ['']   # Primary Paypal email addr

# change if running with https
PAYPAL_URL0 = 'www.paypal.com'
PAYPAL_URL1 = '/cgi-bin/webscr'
##############################################################

NONFATAL = NO_ACTION = false
FATAL    = ACTION    = true

###

def warn(str)
  $stderr.puts str
end

def die(str, code=1)
  $stderr.puts str
  exit(code)
end

def param(key)
  rep = $cgi.params[key] and rep[0]
end

###

class File
  def self.safe_operation(filename, open_str)
    file = File.open(filename, open_str)
    file.flock(File::LOCK_SH)
    file.rewind
    yield file
  rescue
    notify("Unable to access: #{filename} - #{$!}\n", "open file")
  ensure
    file.close if file
  end

  def find(regexp)
    each {|line| return true if regexp =~ line }
    false
  end

  def seek_end
    seek(0, IO::SEEK_END)
  end
end

###

def respond(success=true)   # Handle http reponse
  no_content = {"status" => "204 No Content", "type" => "text/plain"}
  server_error = {"status" => "SERVER_ERROR", "type" => "text/plain"}
  if success
    puts CGI.new.header(no_content)
  else
    CGI.new.out(server_error) { "Internal server error\n" }
  end
end

def ack_ipn   # ack the ipn
  begin
    h = Net::HTTP.new(PAYPAL_URL0, 80)
    resp, = h.post(PAYPAL_URL1, ENV["QUERY_STRING"] +
"&cmd=_notify-validate")
   rescue => err
     notify("Could not acknowledge because of a network " +
            "(or Paypal-related) problem. \nWill retry until success.\n" +
            "Error was: #{err}", "acknowledge IPN", FATAL, NO_ACTION)
   end
   case resp.body
     when /VERIFIED/
       return true
     when /INVALID/
       notify("Notification not from PayPal; message ignored.",
              "acknowledge IPN", NONFATAL, NO_ACTION)
   end
   false
end

def validate_signup     # validate the terms and amounts
  if param("period1") != PERIOD1 || param("period2") != PERIOD2 ||
      param("period3") != PERIOD3 || param("amount1") != AMOUNT1 ||
       param("amount2") != AMOUNT2 || param("amount3") != AMOUNT3
    notify("Customer did not sign up according to your payment terms. " +
           "Although payment was accepted, the account was not activated.",
           "validate subscription terms", NONFATAL)
    return false
  end
  # validate the receiver email
  if ! PAYMENT_EMAILS.include? param("receiver_email")
    notify("The IPN received did not match your primary email.\n" +
           "Message ignored.", "validate receiver email ", NONFATAL,
NO_ACTION)
    return false
  end

  File.safe_operation(TRANSACTION_FILE, "r+") do |file|
    if file.find(/\A#{param("subscr_id")}/)
      notify("The IPN received was previously processed.\n" +
             "Message ignored.", "validate subscription id",
              NONFATAL, NO_ACTION)
      return false
    end
  end
  true
end

###

def notify_no_user(action)
  notify("No username found. Check subscription button or link.",
         action, NONFATAL)
end

def handle_ipn
  case param("txn_type")
    when "subscr_signup"
      param("username") ? add_user : notify_no_user("add user")
    when "subscr_eot"
      param("username") ? remove_user : notify_no_user("remove user")
    else  # do nothing
  end
end

###

def sendmail(mail)   # send email using sendmail
  begin
    IO.popen("#{SENDMAIL_PATH} -t", "w") do |io|
      io.puts <<-EOF
To: #{mail['To']}
From: #{mail['From']}
Subject: #{mail['Subject']}
Content-type: text/plain

#{mail['Message']}
      EOF
      end
   rescue => err
      warn "sendmail error: #{err}"
   end
end

### Send email notification of error

def notify(err,action,fatal=true,attention=true)
  message = <<-EOS
    Error occurred while trying to #{action}:
      #{err}
    User Information
      Subscriber username: #{param("username")}
      Subscriber email:    #{param("payer_email")}
      Subscription number: #{param("subscr_id")}
      Transaction type:    #{param("txn_type")}
  EOS

  subject = "Subscription Error - "
  subject += attention ? "Requires action" : "No action required"

  # If no email, log only
  if ADMIN_EMAIL and SENDMAIL_PATH
    mail = {"To"=>ADMIN_EMAIL, "From"=>ADMIN_EMAIL,
            "Subject"=>subject, "Message"=>message}
    sendmail(mail)
  end

  # Log error
  if fatal
    respond(false)    # IPN will retry
    die message
  else
    warn message
  end
end

###

def add_user
  login  = param("username")
  password = param("password")
  txn = param("subscr_id")
  File.safe_operation(PASSWORD_FILE, "r+") do |file|
    if !file.find(/\A#{login}:/)
      file.seek_end
      file.puts "#{login}:#{password}"
    end
  end

  File.safe_operation(TRANSACTION_FILE, "r+") do |file|
    file.seek_end
    file.puts txn
  end
end

###

def remove_user
  login  = param("username")
  File.safe_operation(PASSWORD_FILE, "r+") do |file|
    others = []
    file.each {|line| others.push(line) if ! /\A#{login}:/ }
    unless others.empty?
      file.rewind
      file.puts others
      file.truncate(file.tell)
    end
  end
end

###


# "main"

$cgi = CGI.new
handle_ipn if ack_ipn     # Acknowlege IPN from PayPal
respond                   # Success