This isn''t even close to ready for release, but in case anybody is
interested, has feedback, finds it useful as is, or even wants to
collaborate...
I''ve been working on a "personal information server",
something of a
protocol multiplexer.  Right now it serves calendar feeds for:
- a local calendar folder (iCal 1.x''s Library/Calendars)
-	rss for local todos
-	icalendar for bdays in a vcard file
I run it as a LoginHook on OS X, haven''t tried on Linux.
Its a longterm project, with more possibilities than I have time, but I
conceive of it taking information from various sources, and presenting
them back using rss, ical, jabber, LDAP, webdav/caldave, etc.. The idea
is people have favorite tools and browsers. Some people like to see
everything as RSS, others like to see everything in a calendar, etc.,
and much information can be reperesented in many ways, but often the
provider of the info doesn''t think of that.
The reason I wrote net-mdns is because I wanted  this server to be able
to advertise services using Rendezvous/Bonjour/DNS-SD.
As an example, I can''t import .mp3 files of the Great Eastern as an
iTunes podcast, because they aren''t a podcast, just a php site. But, I
could build a web scraper that represented all the .mp3s found on a site
as an RSS/podcast, and then import them with iTunes, and have iTunes
notify me when new ones get added.
I think the possibiliites are endless, here''s the ideas I''ve
collected
from my TODO:
WARNING -- If you run vpimd, note that it basically has no security,
authorization, etc. It might be a huge privacy hole!
Note:
  - WebDAV client-side:
    - http://raa.ruby-lang.org/project/libneon-ruby/
    - http://www.webdav.org/neon/
  - WebDAV server-side:
    - implement a WEBRick servlet to handle server-side?
todo: growl-to-*
todo: *-to-growl
Q: should growls go to all servers, or just specific ones?
  - http://raa.ruby-lang.org/project/ruby-growl/
todo: soap/weather-to-ics
  Query weather reports using SOAP, represent as iCalendars.
todo: ics-alarm
  A really, really fast way of adding an alarm to iCalendar, i.e., 3 min, check
the kettle.
  Input:
    - a time offset, and an action
  Output:
    - .ics
    - ical
todo: local-to-rss
  Represent any collection of local files as a podcast, so it can be imported
  into iTunes as a podcast.
todo: http-to-rss
  Also, any web location, crawl it for audio, and represent as a podcast.
todo: ics-to-rss
  Input:
  - ~/Library/Calendars/
  - .ics, ../*.ics
  Output:
  - CGI
  - stdout
  - http
  Options:
  - rss versions
  - rss versions in the URL
  - implement autodiscovery with http? what does this mean...
todo: rss-to-jabber
todo: ics-to-ics
  split calendars by date
todo: ics-to-ics
  Input:
  x iCal 1
  - iCal 2
  - .ics
  Output:
  - http (subscribable)
  - CAP
  - WebDAV
  Options:
  - particular calendars
  x Rendezvous
  - StartupItem
  - all users, if they have a config file, like web pages
  - combine different calendars, and publish to .mac
todo: rss-to-rss
  See: feedblender - http://feedblender.rubyforge.org
todo: vcf-to-address-book
  Input:
  - .vcf
  - AB
  Output:
  - LDAP?
  AB can subscribe to, at least, ldap sources. Can I use this? Can the latest
  AB have plugins?
done: mutt-aliases-to-vcf
  Input:
  - mutt aliases
  Output
  - .vcf
todo: rss-to-ics   convert rss feed, text or URI, to ics file of todos
  Input:
  - .xml
  - http
  - http with autodiscovery?
  Output:
  - http: a subscribable calendar
  - .ics
  Options:
  - RSS -> VTODO
  - RSS -> VEVENT
  - RSS -> VJOURNAL
  
  Notes:
  - will need a decent ics encoding api
todo: vcf-bday-to-ics.rb
  Input:
  - .vcf
  - AB
  Output:
  - .ics
  - http
  - RSS??! maybe only see bdays within the next month, as a reminder?
  - cvt vcf file to ics file of birthdays
  - pull from Address Book, or from .vcf file
  Options:
  - bday -> VTODO
  - bday -> VEVENT
  - bday +  VALARM
todo:
 - an application server (with plugins?) that can do ALL of the above
-------------- next part --------------
#!/opt/local/bin/ruby -w
require ''etc''
require ''webrick''
require ''rss/maker''
require ''vpim/icalendar''
require ''vpim/vcard''
# Can we redirect IO to syslog?
STDOUT.reopen("/Users/sam/vpimd.err", "w")
STDERR.reopen(STDOUT)
puts "here 0"
# OS X Login Windows will run Login Hooks as root, with first argument being
# the user''s login name. Drop root-privilege.
#
# Notes:
# - LoginHook must be set system-wide (with sudo), or it won''t run.
From OS 10.? on.
# - LoginHook must return to the Login Window for login to resume.
if ARGV[0] && Process.uid == 0
  # Set environment up based on specified user
  pw = Etc.getpwnam(ARGV[0])
  ENV[''HOME''] = pw.dir
  ENV[''USER''] = pw.name
  # ENV[''SHELL''] = pw.shell?
  # ENV[''PATH''] = ?
  Dir.chdir pw.dir
  Process::Sys.setgid pw.gid
  Process::Sys.setuid pw.uid
  # or perhaps:
  # Process.initgroups
  # Process.gid  # Process.uid
  if pid = Kernel.fork
    puts "Child #{pid}"
    exit 0
  end
end
$LOG = Dir.getwd + "/vpimd.log"
puts "Open log: #{$LOG}"
# Reopen io to somewhere useful.
unless STDOUT.isatty
  STDOUT.reopen($LOG, "w")
  STDERR.reopen(STDOUT)
end
system "/usr/bin/id"
system "/usr/bin/env"
# Notes on debugging with dnssd API: check system.log, it should give info.
#require ''pp''
#module Kernel
#  def to_pp
#    s = PP.pp(self, '''')
#    s.chomp!
#    s
#  end
#end
#--------------------------------------------------------------------------------
# Load DNSSD support, if possible.
# TODO - should be in net/dns/dnssd
# FIXME - doesn''t work with dnssd
@avoid_native = true
begin
  if @avoid_native
    raise LoadError
  else
    require ''dnssd''
  end
rescue LoadError
  begin
    require ''net/dns/mdns-sd''
    DNSSD = Net::DNS::MDNSSD
  rescue LoadError
    DNSSD = nil
  end
end
#--------------------------------------------------------------------------------
# Set up server environment.
# Find the user''s name and host.
require ''etc''
$user = Etc.getpwuid(Process.uid).gecos
$host = Socket.gethostname
$services = []
$stuff = []
def register(name, path, protocol = ''http'')
  # $services << DNSSD.register(name, ''_http._tcp'',
''local'', $port, ''path'' => path )
  puts "register #{name.inspect} on path #{path.inspect} with
#{protocol}"
  $stuff << [name, path, protocol]
end
# Create a HTTP server, possibly on a dynamic port.
#   - Dynamic ports don''t actually work well. While it is true we can
advertise them,
#     non-DNSSD aware clients will store the hostname and port when they
subscribe to
#     a calendar, but when the server restarts the port will be different. Not
good.
$port = 8191
server = WEBrick::HTTPServer.new( :Port => $port )
if $port == 0
  # Server may have created multiple listeners, all on a different dynamically
  # assigned port.
  families = Socket.getaddrinfo(nil, 1, Socket::AF_UNSPEC, Socket::SOCK_STREAM,
0, Socket::AI_PASSIVE)
  listeners = []
  families.each do |af, one, dns, addr|
    listeners << TCPServer.new(addr, $port)
    $port = listeners.first.addr[1] unless $port != 0
  end
  listeners.each do |s|
    puts "listen on #{s.addr.inspect}"
  end
  # So we replace them with our TCPServer sockets which are all on the same
  # (dynamically assigned) port.
  server.listeners.each do |s| s.close end
  server.listeners.replace listeners
  server.config[:Port] = $port
end
server.config[:MimeTypes][''ics''] =
''text/calendar''
server.config[:MimeTypes][''vcf''] =
''text/directory''
#--------------------------------------------------------------------------------
# Mount services
##### Vcard Birthdays as iCalendar
$vcf_bday_file = ''vpim-bday.vcf''
$vcf_bday_path = ''/vcf/bday.ics''
class VcfBdayIcsServlet < WEBrick::HTTPServlet::AbstractServlet
  def do_GET(req, resp)
    cal = Vpim::Icalendar.create
    open($vcf_bday_file) do |vcf|
      Vpim::Vcard.decode(vcf).each do |card|
        begin
          bday = card.birthday
          if bday
            cal.push Vpim::Icalendar::Vevent.create_yearly(
              card.birthday,
              "Birthday for #{card[''fn''].strip}"
              )
            $stderr.puts "#{card[''fn'']} -> bday
#{cal.events.last.dtstart}"
          end
        rescue
          $stderr.puts $!
          $stderr.puts $!.backtrace.join("\n")
        end
      end
    end
    resp.body = cal.encode
    resp[''content-type''] = ''text/calendar''
    # Is this necessary, or is it default?
    raise WEBrick::HTTPStatus::OK
  end
end
server.mount( $vcf_bday_path, VcfBdayIcsServlet )
register( "Calendar for all the Birthdays in my vCards",
$vcf_bday_path, ''webcal'' )
##### iCalendar as calendars
# Export local calendars two different ways
$ical_folder = File.expand_path( "~/Library/Calendars" )
#
# Here we write a servlet to display all the allowed calendars with
# webcal links, so they open in iCal.
#
$ical_include = /^[A-Z]/
$ical_title = "My Calendars"
class IcalIcsServlet < WEBrick::HTTPServlet::AbstractServlet
  def do_GET(req, resp)
    body = ''''
#   body << @options.inspect
    folder, include, exclude = *@options
    path = req.path_info
#   body << "\n"
#   body << "path=#{path.inspect}\n"
    all = Dir.entries(folder).select do |f| f =~ /\.ics$/ end
    if include
      all.reject! do |f| !(f =~ include) end
    end
    if exclude
      all.reject! do |f| f =~ exclude end
    end
#   body << "#{all.inspect}\n"
    if(path == '''')
      body << "<ul>\n"
      all.each do |f|
        n = f.sub(''.ics'', '''')
        body << "<li><a
href=\"webcal://#{$host}:#{$port}/calfile/#{f}\">#{n}</a>\n"
      end
      body << "</ul>\n"
    end
    resp.body = body
    resp[''content-type''] = ''text/html''
    raise WEBrick::HTTPStatus::OK
  end
end
server.mount( ''/calweb'', IcalIcsServlet, $ical_folder,
$ical_include)
register( "My Calendars as webcal:// Feeds",
''/calweb'' )
#
# We use the WEBrick file servlet to actually serve calendar files.
# FIXME - this means that if you guess someone''s calendar name, you can
# download it, despite the rudimentary security.
#
server.mount( ''/calfile'', WEBrick::HTTPServlet::FileHandler,
$ical_folder, :FancyIndexing=>true )
# For debugging...
register( ''My Calendar Folder'', ''/calfile'' )
##### iCalendar/todo as RSS
$ics_todo_title = ''My Todo Items as an RSS Feed''
$ics_todo_path  = "/ics/todo.rss"
class IcalTodoRssServlet < WEBrick::HTTPServlet::AbstractServlet
  def do_GET(req, resp)   
    rss = RSS::Maker.make("0.9") do |maker|
      title = $ics_todo_title
      link = ''http:///''
      maker.channel.title = title
      maker.channel.link = link
      maker.channel.description = title
      maker.channel.language = ''en-us''
      # These are required, or RSS::Maker silently returns nil!
      maker.image.url = "maker.image.url"
      maker.image.title = "maker.image.title"
      Dir[ $ical_folder + "/*.ics" ].each do |file|
        # todo: use the open with a block variant
        Vpim::Icalendar.decode(File.open(file)).each do |cal|
          cal.todos.each do |todo|
            if !todo.status || todo.status.upcase != "COMPLETED"
              item = maker.items.new_item
              item.title = todo.summary
              item.link =  todo.properties[''url''] || link
              item.description = todo.description || todo.summary
            end
          end
        end
      end
    end
    resp.body = rss.to_s
    resp[''content-type''] = ''text/xml''
    raise WEBrick::HTTPStatus::OK
  end
end
server.mount( $ics_todo_path, IcalTodoRssServlet )
register( $ics_todo_title, $ics_todo_path )
#--------------------------------------------------------------------------------
## Top-level page.
$vpim_title = "vPim for #{$user}"
class VpimServlet < WEBrick::HTTPServlet::AbstractServlet
  def do_GET(req, resp)
    body = <<"EOF"
<h1>#{$vpim_title}</h1>
You can access:
<ul>
EOF
    $stuff.each do |name,path,protocol|
      body << "<li><a
href=\"#{protocol}://#{$host}:#{$port}#{path}\">#{name}</a>\n"
    end
    body << "</ul>\n"
    resp.body = body
    resp[''content-type''] = ''text/html''
    raise WEBrick::HTTPStatus::OK
  end
end
server.mount( ''/'', VpimServlet )
#--------------------------------------------------------------------------------
# Run server
$services << DNSSD.register($vpim_title, ''_http._tcp'',
''local'', $port, ''path'' =>
''/'' )
[''INT'', ''TERM''].each do |signal| 
  trap(signal) do
    server.shutdown
    $services.each do |s|
      s.stop
    end
  end
end
server.start
# TODO - cleanup err.log on success