Repository created from code used for N4C Summer Tests 2009. default tip
authormercurial@rosebud.folly.org.uk
Tue, 16 Mar 2010 13:36:42 +0000
changeset 00765c38ab38a
Repository created from code used for N4C Summer Tests 2009.
README
gateway/dtn/dtn/dtn.conf
gateway/dtn/dtn/dtn_pymail_log.conf
gateway/dtn/python/SocketServer2_6.py
gateway/dtn/python/dp_dtn.py
gateway/dtn/python/dp_main.py
gateway/dtn/python/dp_msg_send_evt.py
gateway/dtn/python/dp_pfmailin.py
gateway/dtn/python/dp_pfmailout.py
gateway/dtn/python/pypop_maildir.py
gateway/dtn/python/pypop_smtpout.py
gateway/dtn/start_dtnd.sh
gateway/dtn/start_mail_link.sh
gateway/dtn/start_nomadic_mail.sh
gateway/dtn/stop_nomadic_mail.sh
outstation/home/user/dtn/dp_out.awk
outstation/home/user/dtn/dtn/dtn_outstation_static.conf
outstation/home/user/dtn/dtn/dtn_prophet.conf
outstation/home/user/dtn/python/SocketServer2_6.py
outstation/home/user/dtn/python/dp_dtn.py
outstation/home/user/dtn/python/dp_msg_send_evt.py
outstation/home/user/dtn/python/dp_outstation.py
outstation/home/user/dtn/python/dp_pop3.py
outstation/home/user/dtn/python/dp_smtpserv.py
outstation/home/user/dtn/python/dtn_pymail_log.conf
outstation/home/user/dtn/python/pypop_maildir.py
outstation/home/user/dtn/start_pymail.sh
outstation/home/user/dtn/stop_pymail.sh
outstation/usr/share/applications/hildon/start_pymail.desktop
outstation/usr/share/applications/hildon/stop_pymail.desktop
outstation/usr/share/pixmaps/n4c.png
relay/dtnrun/dtn/dtn_relay_adhoc_static.conf
relay/dtnrun/dtn/dtn_relay_prophet.conf
relay/dtnrun/dtn/dtn_relay_static.conf
relay/dtnrun/dtn/start_dtn_N810_side.sh
relay/dtnrun/dtn/start_dtn_village_side.sh
relay/dtnrun/python/dp_relay_in.py
relay/dtnrun/python/dp_relay_out.py
relay/dtnrun/python/dtn_pymail_log.conf
relay/dtnrun/start_pymail_relay.sh
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/README	Tue Mar 16 13:36:42 2010 +0000
     1.3 @@ -0,0 +1,41 @@
     1.4 +# PyMail DTN Nomadic Mail System
     1.5 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
     1.6 +#
     1.7 +#    Licensed under the Apache License, Version 2.0 (the "License");
     1.8 +#    you may not use this file except in compliance with the License.
     1.9 +#    You may obtain a copy of the License at
    1.10 +# 
    1.11 +#        http://www.apache.org/licenses/LICENSE-2.0
    1.12 +# 
    1.13 +#    Unless required by applicable law or agreed to in writing, software
    1.14 +#    distributed under the License is distributed on an "AS IS" BASIS,
    1.15 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    1.16 +#    See the License for the specific language governing permissions and
    1.17 +#    limitations under the License.
    1.18 +#
    1.19 +
    1.20 +This directory tree contains the code and configuration files for running
    1.21 +the PyMail Postfix-DTN2 Nomadic email system built originally for use in the
    1.22 +N4C project by Elwyn Davies of Folly Consulting Ltd.  It was initaially 
    1.23 +deployed during the Summer Testing period in 2009 in arctic Sweden.
    1.24 +
    1.25 +The license above applies to the whole system.  It is reproduced in the various 
    1.26 +code files.
    1.27 +
    1.28 +The three top level directories contain the files for respectively
    1.29 +- The Internet gateway machine (gateway).
    1.30 +- A relay machine if the DTN NAT mechanism is needed to convert between static
    1.31 +  routing and dynamic routing (due to DTN2 only supporting one routing 
    1.32 +  mechanism per daemon) - (relay).
    1.33 +- An outstation specifically a Nokia N810 (outstation).
    1.34 +
    1.35 +The directory layout below each of these top level directories mirrors the 
    1.36 +layour used in the machine.
    1.37 +
    1.38 +See the document n4c-wp2-022-pymail-xx.doc for much more information.
    1.39 +
    1.40 +The code is all Python - there ought to be Python installers but they
    1.41 +haven't been written yet.  No makefile or configure script is used.
    1.42 +
    1.43 +Elwyn Davies
    1.44 +16 March 2010
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/gateway/dtn/dtn/dtn.conf	Tue Mar 16 13:36:42 2010 +0000
     2.3 @@ -0,0 +1,248 @@
     2.4 +#
     2.5 +# dtn.conf
     2.6 +#
     2.7 +# Default configuration file for Internet-connected DTN nodes. The
     2.8 +# daemon uses a tcl interpreter to parse this file, thus any standard
     2.9 +# tcl commands are valid, and all settings are get/set using a single
    2.10 +# 'set' functions as: <module> set <var> <val?>
    2.11 +#
    2.12 +
    2.13 +log /dtnd info "dtnd parsing configuration..."
    2.14 +
    2.15 +########################################
    2.16 +#
    2.17 +# Daemon Console Configuration
    2.18 +#
    2.19 +########################################
    2.20 +
    2.21 +#
    2.22 +# console set stdio [ true | false ]
    2.23 +#
    2.24 +# If set to false, disable the interactive console on stdin/stdout.
    2.25 +# The default is set to true (unless the dtnd process is run as a
    2.26 +# daemon).
    2.27 +#
    2.28 +# console set stdio false
    2.29 +
    2.30 +#
    2.31 +# console set addr <port>
    2.32 +# console set port <port>
    2.33 +#
    2.34 +# Settings for the socket based console protocol.
    2.35 +# (this interprets user commands)
    2.36 +#
    2.37 +console set addr 127.0.0.1
    2.38 +console set port 5050
    2.39 +
    2.40 +#
    2.41 +# console set prompt <prompt>
    2.42 +#
    2.43 +# Set the prompt string.  Helps if running multiple dtnd's
    2.44 +#
    2.45 +set shorthostname [lindex [split [info hostname] .] 0]
    2.46 +console set prompt "$shorthostname dtn% "
    2.47 +
    2.48 +########################################
    2.49 +#
    2.50 +# Storage Configuration
    2.51 +#
    2.52 +########################################
    2.53 +
    2.54 +#
    2.55 +# storage set type [ berkeleydb | postgres | mysql  | external ]
    2.56 +#
    2.57 +# Set the storage system to be used
    2.58 +#
    2.59 +storage set type berkeleydb
    2.60 +
    2.61 +# the following are for use with external data stores
    2.62 +#
    2.63 +# The server port to connect to (on localhost)
    2.64 +# Note that 62345 has no special significance -- chosen randomly
    2.65 +storage set server_port 62345
    2.66 +
    2.67 +# The external data store schema location, which can be
    2.68 +# found in dtn2/oasys/storage/DS.xsd.
    2.69 +storage set schema /etc/DS.xsd
    2.70 +
    2.71 +
    2.72 +#
    2.73 +# Do a runtime check for the standard locations for the persistent
    2.74 +# storage directory
    2.75 +#
    2.76 +set dbdir "/home/dtn/bundlestore"
    2.77 +if {$dbdir == ""} {
    2.78 +    foreach dir {/var/dtn /var/tmp/dtn} {
    2.79 +	if {[file isdirectory $dir]} {
    2.80 +            set dbdir $dir
    2.81 +            break
    2.82 +	}
    2.83 +    }
    2.84 +}
    2.85 +
    2.86 +if {$dbdir == ""} {
    2.87 +    puts stderr "Must create /var/dtn or /var/tmp/dtn storage directory"
    2.88 +    exit 1
    2.89 +}
    2.90 +
    2.91 +#
    2.92 +# storage set payloaddir <dir>
    2.93 +#
    2.94 +# Set the directory to be used for bundle payload files
    2.95 +#
    2.96 +storage set payloaddir $dbdir/bundles
    2.97 +
    2.98 +#
    2.99 +# storage set dbname <db>
   2.100 +#
   2.101 +# Set the database name (appended with .db as the filename in berkeley
   2.102 +# db, used as-is for SQL variants
   2.103 +#
   2.104 +storage set dbname     DTN
   2.105 +
   2.106 +#
   2.107 +# storage set dbdir    <dir>
   2.108 +#
   2.109 +#
   2.110 +# When using berkeley db, set the directory to be used for the
   2.111 +# database files and the name of the files and error log.
   2.112 +#
   2.113 +storage set dbdir      $dbdir/db
   2.114 +
   2.115 +########################################
   2.116 +#
   2.117 +# Routing configuration
   2.118 +#
   2.119 +########################################
   2.120 +
   2.121 +#
   2.122 +# Set the algorithm used for dtn routing.
   2.123 +#
   2.124 +# route set type [static | flood | neighborhood | linkstate | external]
   2.125 +#
   2.126 +route set type static
   2.127 +
   2.128 +#
   2.129 +# route local_eid <eid>
   2.130 +#
   2.131 +# Set the local administrative id of this node. The default just uses
   2.132 +# the internet hostname plus the appended string ".dtn" to make it
   2.133 +# clear that the hostname isn't really used for DNS lookups.
   2.134 +#
   2.135 +route local_eid "dtn://gateway.nomadic.n4c.eu"
   2.136 +
   2.137 +#
   2.138 +# External router specific options
   2.139 +#
   2.140 +# route set server_port 8001
   2.141 +# route set hello_interval 30
   2.142 +# route set schema "/etc/router.xsd"
   2.143 +
   2.144 +########################################
   2.145 +#
   2.146 +# TCP convergence layer configuration
   2.147 +#
   2.148 +########################################
   2.149 +
   2.150 +#
   2.151 +# interface add [name] [CL]
   2.152 +#
   2.153 +# Add an input interface to listen on addr:port for incoming bundles
   2.154 +# from other tcp / udp convergence layers
   2.155 +#
   2.156 +# For IP-based interfaces, interfaces listen on INADDR_ANY port 4556
   2.157 +# by default. These can be overridden by using the local_addr and/or
   2.158 +# local_port arguments.
   2.159 +interface add tcp0 tcp
   2.160 +interface add udp0 udp
   2.161 +
   2.162 +#
   2.163 +# link add <name> <nexthop> <type> <clayer> <args...>
   2.164 +#
   2.165 +# Add a link to a peer node.
   2.166 +# 
   2.167 +# For IP-based links (tcp or udp), the nexthop should contain a DNS
   2.168 +# hostname or IP address, followed optionally by a : and a port. If
   2.169 +# the port is not specified, the default of 4556 is used.
   2.170 +#
   2.171 +# e.g.  link add link1 dtn.dtnrg.org ONDEMAND tcp
   2.172 +#       link add link2 dtn2.dtnrg.org:10000 ONDEMAND tcp
   2.173 +
   2.174 +#
   2.175 +# route add <dest> <link|peer>
   2.176 +#
   2.177 +# Add a route to the given bundle endpoint id pattern <dest> using the
   2.178 +# specified link name or peer endpoint.
   2.179 +#
   2.180 +# e.g. route add dtn://host.domain/* tcp0
   2.181 +link add linkToN4C 130.240.97.204 ONDEMAND tcp
   2.182 +link add linkToWeeePc 127.0.0.1:4251 ONDEMAND tcp
   2.183 +link add linkToMightyAtom 127.0.0.1:4250 ONDEMAND tcp
   2.184 +link add link_tcp_od_basil basil.dsg.cs.tcd.ie:4556 ONDEMAND tcp
   2.185 +
   2.186 +#route add dtn://dtn.n4c.eu/* linkToN4C
   2.187 +route add dtn://Weee-Pc1.folly.org.uk/* linkToWeeePc
   2.188 +route add dtn://elwynd.nomadic.n4c.eu/* linkToWeeePc
   2.189 +route add dtn://relay.nomadic.n4c.eu/* link_tcp_od_basil
   2.190 +route add dtn://user1.nomadic.n4c.eu/* link_tcp_od_basil
   2.191 +route add dtn://elwynd.nomadic.folly.org.uk/* linkToMightyAtom
   2.192 +route add dtn://gateway.nomadic.folly.org.uk/* linkToMightyAtom
   2.193 +
   2.194 +route add dtn://basil.dsg.cs.tcd.ie.dtn/* link_tcp_od_basil
   2.195 +route add dtn://dtnrouter-11-10.village.n4c.eu.dtn/* link_tcp_od_basil
   2.196 +route add dtn://dtnrouter-11-20.village.n4c.eu.dtn/* link_tcp_od_basil
   2.197 +route add dtn://dtngateway-2-200.village.n4c.eu.dtn/* link_tcp_od_basil
   2.198 +route add dtn://dtnmule-2-10.village.n4c.eu.dtn/* link_tcp_od_basil
   2.199 +route add dtn://dtnmule-2-31.village.n4c.eu.dtn/* link_tcp_od_basil
   2.200 +
   2.201 +########################################
   2.202 +#
   2.203 +# Service discovery
   2.204 +#
   2.205 +########################################
   2.206 +
   2.207 +#
   2.208 +# discovery add <name> <af> <opts...>
   2.209 +# discovery announce <cl_name> <discovery_name> <cl_type> <opts...>
   2.210 +#
   2.211 +# Add a local neighborhood discovery module
   2.212 +#
   2.213 +# e.g. discovery add discovery_bonjour bonjour
   2.214 +
   2.215 +########################################
   2.216 +#
   2.217 +# Parameter Tuning
   2.218 +#
   2.219 +########################################
   2.220 +
   2.221 +#
   2.222 +# Set the size threshold for the daemon so any bundles smaller than this
   2.223 +# size maintain a shadow copy in memory to minimize disk accesses. 
   2.224 +#
   2.225 +# param set payload_mem_threshold 16384
   2.226 +
   2.227 +#
   2.228 +# Test option to keep all bundle files in the filesystem, even after the
   2.229 +# bundle is no longer present at the daemon.
   2.230 +#
   2.231 +# param set payload_test_no_remove true
   2.232 +
   2.233 +#
   2.234 +# Set the size for which the tcp convergence layer sends partial reception
   2.235 +# acknowledgements. Used with reactive fragmentation
   2.236 +#
   2.237 +# param set tcpcl_partial_ack_len 4096
   2.238 +
   2.239 +#
   2.240 +# Set if bundles are automatically deleted after transmission
   2.241 +#
   2.242 +# param set early_deletion true
   2.243 +
   2.244 +# (others exist but are not fully represented here)
   2.245 +
   2.246 +log /dtnd info "dtnd configuration parsing complete"
   2.247 +
   2.248 +## emacs settings to use tcl-mode by default
   2.249 +## Local Variables: ***
   2.250 +## mode:tcl ***
   2.251 +## End: ***
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/gateway/dtn/dtn/dtn_pymail_log.conf	Tue Mar 16 13:36:42 2010 +0000
     3.3 @@ -0,0 +1,42 @@
     3.4 +[loggers]
     3.5 +keys=root,dtn_pymail,dtn_postfix_pipe
     3.6 +
     3.7 +[handlers]
     3.8 +keys=dtn_pymail_handler,dtn_postfix_pipe_handler
     3.9 +
    3.10 +[formatters]
    3.11 +keys=dtn_pymail_formatter
    3.12 +
    3.13 +[logger_root]
    3.14 +level=NOTSET
    3.15 +handlers=dtn_pymail_handler
    3.16 +
    3.17 +[logger_dtn_pymail]
    3.18 +level=INFO
    3.19 +handlers=dtn_pymail_handler
    3.20 +propagate=0
    3.21 +qualname=dtn_pymail
    3.22 +
    3.23 +[logger_dtn_postfix_pipe]
    3.24 +level=INFO
    3.25 +handlers=dtn_postfix_pipe_handler
    3.26 +propagate=0
    3.27 +qualname=dtn_postfix_pipe
    3.28 +
    3.29 +[handler_dtn_pymail_handler]
    3.30 +class=handlers.TimedRotatingFileHandler
    3.31 +level=NOTSET
    3.32 +formatter=dtn_pymail_formatter
    3.33 +args=("/home/dtn/log/dtn_pymail.log", "midnight", 1, 10)
    3.34 +
    3.35 +[handler_dtn_postfix_pipe_handler]
    3.36 +class=handlers.TimedRotatingFileHandler
    3.37 +level=NOTSET
    3.38 +formatter=dtn_pymail_formatter
    3.39 +args=("/home/dtn/log/dtn_postfix_pipe.log", "midnight", 1, 10)
    3.40 +
    3.41 +[formatter_dtn_pymail_formatter]
    3.42 +format=%(asctime)s %(threadName)-18s %(levelname)-8s %(message)s
    3.43 +datefmt=
    3.44 +class=logging.Formatter
    3.45 +
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/gateway/dtn/python/SocketServer2_6.py	Tue Mar 16 13:36:42 2010 +0000
     4.3 @@ -0,0 +1,681 @@
     4.4 +"""Generic socket server classes.
     4.5 +
     4.6 +This module tries to capture the various aspects of defining a server:
     4.7 +
     4.8 +For socket-based servers:
     4.9 +
    4.10 +- address family:
    4.11 +        - AF_INET{,6}: IP (Internet Protocol) sockets (default)
    4.12 +        - AF_UNIX: Unix domain sockets
    4.13 +        - others, e.g. AF_DECNET are conceivable (see <socket.h>
    4.14 +- socket type:
    4.15 +        - SOCK_STREAM (reliable stream, e.g. TCP)
    4.16 +        - SOCK_DGRAM (datagrams, e.g. UDP)
    4.17 +
    4.18 +For request-based servers (including socket-based):
    4.19 +
    4.20 +- client address verification before further looking at the request
    4.21 +        (This is actually a hook for any processing that needs to look
    4.22 +         at the request before anything else, e.g. logging)
    4.23 +- how to handle multiple requests:
    4.24 +        - synchronous (one request is handled at a time)
    4.25 +        - forking (each request is handled by a new process)
    4.26 +        - threading (each request is handled by a new thread)
    4.27 +
    4.28 +The classes in this module favor the server type that is simplest to
    4.29 +write: a synchronous TCP/IP server.  This is bad class design, but
    4.30 +save some typing.  (There's also the issue that a deep class hierarchy
    4.31 +slows down method lookups.)
    4.32 +
    4.33 +There are five classes in an inheritance diagram, four of which represent
    4.34 +synchronous servers of four types:
    4.35 +
    4.36 +        +------------+
    4.37 +        | BaseServer |
    4.38 +        +------------+
    4.39 +              |
    4.40 +              v
    4.41 +        +-----------+        +------------------+
    4.42 +        | TCPServer |------->| UnixStreamServer |
    4.43 +        +-----------+        +------------------+
    4.44 +              |
    4.45 +              v
    4.46 +        +-----------+        +--------------------+
    4.47 +        | UDPServer |------->| UnixDatagramServer |
    4.48 +        +-----------+        +--------------------+
    4.49 +
    4.50 +Note that UnixDatagramServer derives from UDPServer, not from
    4.51 +UnixStreamServer -- the only difference between an IP and a Unix
    4.52 +stream server is the address family, which is simply repeated in both
    4.53 +unix server classes.
    4.54 +
    4.55 +Forking and threading versions of each type of server can be created
    4.56 +using the ForkingMixIn and ThreadingMixIn mix-in classes.  For
    4.57 +instance, a threading UDP server class is created as follows:
    4.58 +
    4.59 +        class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
    4.60 +
    4.61 +The Mix-in class must come first, since it overrides a method defined
    4.62 +in UDPServer! Setting the various member variables also changes
    4.63 +the behavior of the underlying server mechanism.
    4.64 +
    4.65 +To implement a service, you must derive a class from
    4.66 +BaseRequestHandler and redefine its handle() method.  You can then run
    4.67 +various versions of the service by combining one of the server classes
    4.68 +with your request handler class.
    4.69 +
    4.70 +The request handler class must be different for datagram or stream
    4.71 +services.  This can be hidden by using the request handler
    4.72 +subclasses StreamRequestHandler or DatagramRequestHandler.
    4.73 +
    4.74 +Of course, you still have to use your head!
    4.75 +
    4.76 +For instance, it makes no sense to use a forking server if the service
    4.77 +contains state in memory that can be modified by requests (since the
    4.78 +modifications in the child process would never reach the initial state
    4.79 +kept in the parent process and passed to each child).  In this case,
    4.80 +you can use a threading server, but you will probably have to use
    4.81 +locks to avoid two requests that come in nearly simultaneous to apply
    4.82 +conflicting changes to the server state.
    4.83 +
    4.84 +On the other hand, if you are building e.g. an HTTP server, where all
    4.85 +data is stored externally (e.g. in the file system), a synchronous
    4.86 +class will essentially render the service "deaf" while one request is
    4.87 +being handled -- which may be for a very long time if a client is slow
    4.88 +to reqd all the data it has requested.  Here a threading or forking
    4.89 +server is appropriate.
    4.90 +
    4.91 +In some cases, it may be appropriate to process part of a request
    4.92 +synchronously, but to finish processing in a forked child depending on
    4.93 +the request data.  This can be implemented by using a synchronous
    4.94 +server and doing an explicit fork in the request handler class
    4.95 +handle() method.
    4.96 +
    4.97 +Another approach to handling multiple simultaneous requests in an
    4.98 +environment that supports neither threads nor fork (or where these are
    4.99 +too expensive or inappropriate for the service) is to maintain an
   4.100 +explicit table of partially finished requests and to use select() to
   4.101 +decide which request to work on next (or whether to handle a new
   4.102 +incoming request).  This is particularly important for stream services
   4.103 +where each client can potentially be connected for a long time (if
   4.104 +threads or subprocesses cannot be used).
   4.105 +
   4.106 +Future work:
   4.107 +- Standard classes for Sun RPC (which uses either UDP or TCP)
   4.108 +- Standard mix-in classes to implement various authentication
   4.109 +  and encryption schemes
   4.110 +- Standard framework for select-based multiplexing
   4.111 +
   4.112 +XXX Open problems:
   4.113 +- What to do with out-of-band data?
   4.114 +
   4.115 +BaseServer:
   4.116 +- split generic "request" functionality out into BaseServer class.
   4.117 +  Copyright (C) 2000  Luke Kenneth Casson Leighton <lkcl@samba.org>
   4.118 +
   4.119 +  example: read entries from a SQL database (requires overriding
   4.120 +  get_request() to return a table entry from the database).
   4.121 +  entry is processed by a RequestHandlerClass.
   4.122 +
   4.123 +"""
   4.124 +
   4.125 +# Author of the BaseServer patch: Luke Kenneth Casson Leighton
   4.126 +
   4.127 +# XXX Warning!
   4.128 +# There is a test suite for this module, but it cannot be run by the
   4.129 +# standard regression test.
   4.130 +# To run it manually, run Lib/test/test_socketserver.py.
   4.131 +
   4.132 +__version__ = "0.4"
   4.133 +
   4.134 +
   4.135 +import socket
   4.136 +import select
   4.137 +import sys
   4.138 +import os
   4.139 +try:
   4.140 +    import threading
   4.141 +except ImportError:
   4.142 +    import dummy_threading as threading
   4.143 +
   4.144 +__all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer",
   4.145 +           "ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler",
   4.146 +           "StreamRequestHandler","DatagramRequestHandler",
   4.147 +           "ThreadingMixIn", "ForkingMixIn"]
   4.148 +if hasattr(socket, "AF_UNIX"):
   4.149 +    __all__.extend(["UnixStreamServer","UnixDatagramServer",
   4.150 +                    "ThreadingUnixStreamServer",
   4.151 +                    "ThreadingUnixDatagramServer"])
   4.152 +
   4.153 +class BaseServer:
   4.154 +
   4.155 +    """Base class for server classes.
   4.156 +
   4.157 +    Methods for the caller:
   4.158 +
   4.159 +    - __init__(server_address, RequestHandlerClass)
   4.160 +    - serve_forever(poll_interval=0.5)
   4.161 +    - shutdown()
   4.162 +    - handle_request()  # if you do not use serve_forever()
   4.163 +    - fileno() -> int   # for select()
   4.164 +
   4.165 +    Methods that may be overridden:
   4.166 +
   4.167 +    - server_bind()
   4.168 +    - server_activate()
   4.169 +    - get_request() -> request, client_address
   4.170 +    - handle_timeout()
   4.171 +    - verify_request(request, client_address)
   4.172 +    - server_close()
   4.173 +    - process_request(request, client_address)
   4.174 +    - close_request(request)
   4.175 +    - handle_error()
   4.176 +
   4.177 +    Methods for derived classes:
   4.178 +
   4.179 +    - finish_request(request, client_address)
   4.180 +
   4.181 +    Class variables that may be overridden by derived classes or
   4.182 +    instances:
   4.183 +
   4.184 +    - timeout
   4.185 +    - address_family
   4.186 +    - socket_type
   4.187 +    - allow_reuse_address
   4.188 +
   4.189 +    Instance variables:
   4.190 +
   4.191 +    - RequestHandlerClass
   4.192 +    - socket
   4.193 +
   4.194 +    """
   4.195 +
   4.196 +    timeout = None
   4.197 +
   4.198 +    def __init__(self, server_address, RequestHandlerClass):
   4.199 +        """Constructor.  May be extended, do not override."""
   4.200 +        self.server_address = server_address
   4.201 +        self.RequestHandlerClass = RequestHandlerClass
   4.202 +        self.__is_shut_down = threading.Event()
   4.203 +        self.__serving = False
   4.204 +
   4.205 +    def server_activate(self):
   4.206 +        """Called by constructor to activate the server.
   4.207 +
   4.208 +        May be overridden.
   4.209 +
   4.210 +        """
   4.211 +        pass
   4.212 +
   4.213 +    def serve_forever(self, poll_interval=0.5):
   4.214 +        """Handle one request at a time until shutdown.
   4.215 +
   4.216 +        Polls for shutdown every poll_interval seconds. Ignores
   4.217 +        self.timeout. If you need to do periodic tasks, do them in
   4.218 +        another thread.
   4.219 +        """
   4.220 +        self.__serving = True
   4.221 +        self.__is_shut_down.clear()
   4.222 +        while self.__serving:
   4.223 +            # XXX: Consider using another file descriptor or
   4.224 +            # connecting to the socket to wake this up instead of
   4.225 +            # polling. Polling reduces our responsiveness to a
   4.226 +            # shutdown request and wastes cpu at all other times.
   4.227 +            r, w, e = select.select([self], [], [], poll_interval)
   4.228 +            if r:
   4.229 +                self._handle_request_noblock()
   4.230 +        self.__is_shut_down.set()
   4.231 +
   4.232 +    def shutdown(self):
   4.233 +        """Stops the serve_forever loop.
   4.234 +
   4.235 +        Blocks until the loop has finished. This must be called while
   4.236 +        serve_forever() is running in another thread, or it will
   4.237 +        deadlock.
   4.238 +        """
   4.239 +        self.__serving = False
   4.240 +        self.__is_shut_down.wait()
   4.241 +
   4.242 +    # The distinction between handling, getting, processing and
   4.243 +    # finishing a request is fairly arbitrary.  Remember:
   4.244 +    #
   4.245 +    # - handle_request() is the top-level call.  It calls
   4.246 +    #   select, get_request(), verify_request() and process_request()
   4.247 +    # - get_request() is different for stream or datagram sockets
   4.248 +    # - process_request() is the place that may fork a new process
   4.249 +    #   or create a new thread to finish the request
   4.250 +    # - finish_request() instantiates the request handler class;
   4.251 +    #   this constructor will handle the request all by itself
   4.252 +
   4.253 +    def handle_request(self):
   4.254 +        """Handle one request, possibly blocking.
   4.255 +
   4.256 +        Respects self.timeout.
   4.257 +        """
   4.258 +        # Support people who used socket.settimeout() to escape
   4.259 +        # handle_request before self.timeout was available.
   4.260 +        timeout = self.socket.gettimeout()
   4.261 +        if timeout is None:
   4.262 +            timeout = self.timeout
   4.263 +        elif self.timeout is not None:
   4.264 +            timeout = min(timeout, self.timeout)
   4.265 +        fd_sets = select.select([self], [], [], timeout)
   4.266 +        if not fd_sets[0]:
   4.267 +            self.handle_timeout()
   4.268 +            return
   4.269 +        self._handle_request_noblock()
   4.270 +
   4.271 +    def _handle_request_noblock(self):
   4.272 +        """Handle one request, without blocking.
   4.273 +
   4.274 +        I assume that select.select has returned that the socket is
   4.275 +        readable before this function was called, so there should be
   4.276 +        no risk of blocking in get_request().
   4.277 +        """
   4.278 +        try:
   4.279 +            request, client_address = self.get_request()
   4.280 +        except socket.error:
   4.281 +            return
   4.282 +        if self.verify_request(request, client_address):
   4.283 +            try:
   4.284 +                self.process_request(request, client_address)
   4.285 +            except:
   4.286 +                self.handle_error(request, client_address)
   4.287 +                self.close_request(request)
   4.288 +
   4.289 +    def handle_timeout(self):
   4.290 +        """Called if no new request arrives within self.timeout.
   4.291 +
   4.292 +        Overridden by ForkingMixIn.
   4.293 +        """
   4.294 +        pass
   4.295 +
   4.296 +    def verify_request(self, request, client_address):
   4.297 +        """Verify the request.  May be overridden.
   4.298 +
   4.299 +        Return True if we should proceed with this request.
   4.300 +
   4.301 +        """
   4.302 +        return True
   4.303 +
   4.304 +    def process_request(self, request, client_address):
   4.305 +        """Call finish_request.
   4.306 +
   4.307 +        Overridden by ForkingMixIn and ThreadingMixIn.
   4.308 +
   4.309 +        """
   4.310 +        self.finish_request(request, client_address)
   4.311 +        self.close_request(request)
   4.312 +
   4.313 +    def server_close(self):
   4.314 +        """Called to clean-up the server.
   4.315 +
   4.316 +        May be overridden.
   4.317 +
   4.318 +        """
   4.319 +        pass
   4.320 +
   4.321 +    def finish_request(self, request, client_address):
   4.322 +        """Finish one request by instantiating RequestHandlerClass."""
   4.323 +        self.RequestHandlerClass(request, client_address, self)
   4.324 +
   4.325 +    def close_request(self, request):
   4.326 +        """Called to clean up an individual request."""
   4.327 +        pass
   4.328 +
   4.329 +    def handle_error(self, request, client_address):
   4.330 +        """Handle an error gracefully.  May be overridden.
   4.331 +
   4.332 +        The default is to print a traceback and continue.
   4.333 +
   4.334 +        """
   4.335 +        print '-'*40
   4.336 +        print 'Exception happened during processing of request from',
   4.337 +        print client_address
   4.338 +        import traceback
   4.339 +        traceback.print_exc() # XXX But this goes to stderr!
   4.340 +        print '-'*40
   4.341 +
   4.342 +
   4.343 +class TCPServer(BaseServer):
   4.344 +
   4.345 +    """Base class for various socket-based server classes.
   4.346 +
   4.347 +    Defaults to synchronous IP stream (i.e., TCP).
   4.348 +
   4.349 +    Methods for the caller:
   4.350 +
   4.351 +    - __init__(server_address, RequestHandlerClass, bind_and_activate=True)
   4.352 +    - serve_forever(poll_interval=0.5)
   4.353 +    - shutdown()
   4.354 +    - handle_request()  # if you don't use serve_forever()
   4.355 +    - fileno() -> int   # for select()
   4.356 +
   4.357 +    Methods that may be overridden:
   4.358 +
   4.359 +    - server_bind()
   4.360 +    - server_activate()
   4.361 +    - get_request() -> request, client_address
   4.362 +    - handle_timeout()
   4.363 +    - verify_request(request, client_address)
   4.364 +    - process_request(request, client_address)
   4.365 +    - close_request(request)
   4.366 +    - handle_error()
   4.367 +
   4.368 +    Methods for derived classes:
   4.369 +
   4.370 +    - finish_request(request, client_address)
   4.371 +
   4.372 +    Class variables that may be overridden by derived classes or
   4.373 +    instances:
   4.374 +
   4.375 +    - timeout
   4.376 +    - address_family
   4.377 +    - socket_type
   4.378 +    - request_queue_size (only for stream sockets)
   4.379 +    - allow_reuse_address
   4.380 +
   4.381 +    Instance variables:
   4.382 +
   4.383 +    - server_address
   4.384 +    - RequestHandlerClass
   4.385 +    - socket
   4.386 +
   4.387 +    """
   4.388 +
   4.389 +    address_family = socket.AF_INET
   4.390 +
   4.391 +    socket_type = socket.SOCK_STREAM
   4.392 +
   4.393 +    request_queue_size = 5
   4.394 +
   4.395 +    allow_reuse_address = False
   4.396 +
   4.397 +    def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
   4.398 +        """Constructor.  May be extended, do not override."""
   4.399 +        BaseServer.__init__(self, server_address, RequestHandlerClass)
   4.400 +        self.socket = socket.socket(self.address_family,
   4.401 +                                    self.socket_type)
   4.402 +        if bind_and_activate:
   4.403 +            self.server_bind()
   4.404 +            self.server_activate()
   4.405 +
   4.406 +    def server_bind(self):
   4.407 +        """Called by constructor to bind the socket.
   4.408 +
   4.409 +        May be overridden.
   4.410 +
   4.411 +        """
   4.412 +        if self.allow_reuse_address:
   4.413 +            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
   4.414 +        self.socket.bind(self.server_address)
   4.415 +        self.server_address = self.socket.getsockname()
   4.416 +
   4.417 +    def server_activate(self):
   4.418 +        """Called by constructor to activate the server.
   4.419 +
   4.420 +        May be overridden.
   4.421 +
   4.422 +        """
   4.423 +        self.socket.listen(self.request_queue_size)
   4.424 +
   4.425 +    def server_close(self):
   4.426 +        """Called to clean-up the server.
   4.427 +
   4.428 +        May be overridden.
   4.429 +
   4.430 +        """
   4.431 +        self.socket.close()
   4.432 +
   4.433 +    def fileno(self):
   4.434 +        """Return socket file number.
   4.435 +
   4.436 +        Interface required by select().
   4.437 +
   4.438 +        """
   4.439 +        return self.socket.fileno()
   4.440 +
   4.441 +    def get_request(self):
   4.442 +        """Get the request and client address from the socket.
   4.443 +
   4.444 +        May be overridden.
   4.445 +
   4.446 +        """
   4.447 +        return self.socket.accept()
   4.448 +
   4.449 +    def close_request(self, request):
   4.450 +        """Called to clean up an individual request."""
   4.451 +        request.close()
   4.452 +
   4.453 +
   4.454 +class UDPServer(TCPServer):
   4.455 +
   4.456 +    """UDP server class."""
   4.457 +
   4.458 +    allow_reuse_address = False
   4.459 +
   4.460 +    socket_type = socket.SOCK_DGRAM
   4.461 +
   4.462 +    max_packet_size = 8192
   4.463 +
   4.464 +    def get_request(self):
   4.465 +        data, client_addr = self.socket.recvfrom(self.max_packet_size)
   4.466 +        return (data, self.socket), client_addr
   4.467 +
   4.468 +    def server_activate(self):
   4.469 +        # No need to call listen() for UDP.
   4.470 +        pass
   4.471 +
   4.472 +    def close_request(self, request):
   4.473 +        # No need to close anything.
   4.474 +        pass
   4.475 +
   4.476 +class ForkingMixIn:
   4.477 +
   4.478 +    """Mix-in class to handle each request in a new process."""
   4.479 +
   4.480 +    timeout = 300
   4.481 +    active_children = None
   4.482 +    max_children = 40
   4.483 +
   4.484 +    def collect_children(self):
   4.485 +        """Internal routine to wait for children that have exited."""
   4.486 +        if self.active_children is None: return
   4.487 +        while len(self.active_children) >= self.max_children:
   4.488 +            # XXX: This will wait for any child process, not just ones
   4.489 +            # spawned by this library. This could confuse other
   4.490 +            # libraries that expect to be able to wait for their own
   4.491 +            # children.
   4.492 +            try:
   4.493 +                pid, status = os.waitpid(0, options=0)
   4.494 +            except os.error:
   4.495 +                pid = None
   4.496 +            if pid not in self.active_children: continue
   4.497 +            self.active_children.remove(pid)
   4.498 +
   4.499 +        # XXX: This loop runs more system calls than it ought
   4.500 +        # to. There should be a way to put the active_children into a
   4.501 +        # process group and then use os.waitpid(-pgid) to wait for any
   4.502 +        # of that set, but I couldn't find a way to allocate pgids
   4.503 +        # that couldn't collide.
   4.504 +        for child in self.active_children:
   4.505 +            try:
   4.506 +                pid, status = os.waitpid(child, os.WNOHANG)
   4.507 +            except os.error:
   4.508 +                pid = None
   4.509 +            if not pid: continue
   4.510 +            try:
   4.511 +                self.active_children.remove(pid)
   4.512 +            except ValueError, e:
   4.513 +                raise ValueError('%s. x=%d and list=%r' % (e.message, pid,
   4.514 +                                                           self.active_children))
   4.515 +
   4.516 +    def handle_timeout(self):
   4.517 +        """Wait for zombies after self.timeout seconds of inactivity.
   4.518 +
   4.519 +        May be extended, do not override.
   4.520 +        """
   4.521 +        self.collect_children()
   4.522 +
   4.523 +    def process_request(self, request, client_address):
   4.524 +        """Fork a new subprocess to process the request."""
   4.525 +        self.collect_children()
   4.526 +        pid = os.fork()
   4.527 +        if pid:
   4.528 +            # Parent process
   4.529 +            if self.active_children is None:
   4.530 +                self.active_children = []
   4.531 +            self.active_children.append(pid)
   4.532 +            self.close_request(request)
   4.533 +            return
   4.534 +        else:
   4.535 +            # Child process.
   4.536 +            # This must never return, hence os._exit()!
   4.537 +            try:
   4.538 +                self.finish_request(request, client_address)
   4.539 +                os._exit(0)
   4.540 +            except:
   4.541 +                try:
   4.542 +                    self.handle_error(request, client_address)
   4.543 +                finally:
   4.544 +                    os._exit(1)
   4.545 +
   4.546 +
   4.547 +class ThreadingMixIn:
   4.548 +    """Mix-in class to handle each request in a new thread."""
   4.549 +
   4.550 +    # Decides how threads will act upon termination of the
   4.551 +    # main process
   4.552 +    daemon_threads = False
   4.553 +
   4.554 +    def process_request_thread(self, request, client_address):
   4.555 +        """Same as in BaseServer but as a thread.
   4.556 +
   4.557 +        In addition, exception handling is done here.
   4.558 +
   4.559 +        """
   4.560 +        try:
   4.561 +            self.finish_request(request, client_address)
   4.562 +            self.close_request(request)
   4.563 +        except:
   4.564 +            self.handle_error(request, client_address)
   4.565 +            self.close_request(request)
   4.566 +
   4.567 +    def process_request(self, request, client_address):
   4.568 +        """Start a new thread to process the request."""
   4.569 +        t = threading.Thread(target = self.process_request_thread,
   4.570 +                             args = (request, client_address))
   4.571 +        if self.daemon_threads:
   4.572 +            t.setDaemon (1)
   4.573 +        t.start()
   4.574 +
   4.575 +
   4.576 +class ForkingUDPServer(ForkingMixIn, UDPServer): pass
   4.577 +class ForkingTCPServer(ForkingMixIn, TCPServer): pass
   4.578 +
   4.579 +class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
   4.580 +class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
   4.581 +
   4.582 +if hasattr(socket, 'AF_UNIX'):
   4.583 +
   4.584 +    class UnixStreamServer(TCPServer):
   4.585 +        address_family = socket.AF_UNIX
   4.586 +
   4.587 +    class UnixDatagramServer(UDPServer):
   4.588 +        address_family = socket.AF_UNIX
   4.589 +
   4.590 +    class ThreadingUnixStreamServer(ThreadingMixIn, UnixStreamServer): pass
   4.591 +
   4.592 +    class ThreadingUnixDatagramServer(ThreadingMixIn, UnixDatagramServer): pass
   4.593 +
   4.594 +class BaseRequestHandler:
   4.595 +
   4.596 +    """Base class for request handler classes.
   4.597 +
   4.598 +    This class is instantiated for each request to be handled.  The
   4.599 +    constructor sets the instance variables request, client_address
   4.600 +    and server, and then calls the handle() method.  To implement a
   4.601 +    specific service, all you need to do is to derive a class which
   4.602 +    defines a handle() method.
   4.603 +
   4.604 +    The handle() method can find the request as self.request, the
   4.605 +    client address as self.client_address, and the server (in case it
   4.606 +    needs access to per-server information) as self.server.  Since a
   4.607 +    separate instance is created for each request, the handle() method
   4.608 +    can define arbitrary other instance variariables.
   4.609 +
   4.610 +    """
   4.611 +
   4.612 +    def __init__(self, request, client_address, server):
   4.613 +        self.request = request
   4.614 +        self.client_address = client_address
   4.615 +        self.server = server
   4.616 +        try:
   4.617 +            self.setup()
   4.618 +            self.handle()
   4.619 +            self.finish()
   4.620 +        finally:
   4.621 +            sys.exc_traceback = None    # Help garbage collection
   4.622 +
   4.623 +    def setup(self):
   4.624 +        pass
   4.625 +
   4.626 +    def handle(self):
   4.627 +        pass
   4.628 +
   4.629 +    def finish(self):
   4.630 +        pass
   4.631 +
   4.632 +
   4.633 +# The following two classes make it possible to use the same service
   4.634 +# class for stream or datagram servers.
   4.635 +# Each class sets up these instance variables:
   4.636 +# - rfile: a file object from which receives the request is read
   4.637 +# - wfile: a file object to which the reply is written
   4.638 +# When the handle() method returns, wfile is flushed properly
   4.639 +
   4.640 +
   4.641 +class StreamRequestHandler(BaseRequestHandler):
   4.642 +
   4.643 +    """Define self.rfile and self.wfile for stream sockets."""
   4.644 +
   4.645 +    # Default buffer sizes for rfile, wfile.
   4.646 +    # We default rfile to buffered because otherwise it could be
   4.647 +    # really slow for large data (a getc() call per byte); we make
   4.648 +    # wfile unbuffered because (a) often after a write() we want to
   4.649 +    # read and we need to flush the line; (b) big writes to unbuffered
   4.650 +    # files are typically optimized by stdio even when big reads
   4.651 +    # aren't.
   4.652 +    rbufsize = -1
   4.653 +    wbufsize = 0
   4.654 +
   4.655 +    def setup(self):
   4.656 +        self.connection = self.request
   4.657 +        self.rfile = self.connection.makefile('rb', self.rbufsize)
   4.658 +        self.wfile = self.connection.makefile('wb', self.wbufsize)
   4.659 +
   4.660 +    def finish(self):
   4.661 +        if not self.wfile.closed:
   4.662 +            self.wfile.flush()
   4.663 +        self.wfile.close()
   4.664 +        self.rfile.close()
   4.665 +
   4.666 +
   4.667 +class DatagramRequestHandler(BaseRequestHandler):
   4.668 +
   4.669 +    # XXX Regrettably, I cannot get this working on Linux;
   4.670 +    # s.recvfrom() doesn't return a meaningful client address.
   4.671 +
   4.672 +    """Define self.rfile and self.wfile for datagram sockets."""
   4.673 +
   4.674 +    def setup(self):
   4.675 +        try:
   4.676 +            from cStringIO import StringIO
   4.677 +        except ImportError:
   4.678 +            from StringIO import StringIO
   4.679 +        self.packet, self.socket = self.request
   4.680 +        self.rfile = StringIO(self.packet)
   4.681 +        self.wfile = StringIO()
   4.682 +
   4.683 +    def finish(self):
   4.684 +        self.socket.sendto(self.wfile.getvalue(), self.client_address)
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/gateway/dtn/python/dp_dtn.py	Tue Mar 16 13:36:42 2010 +0000
     5.3 @@ -0,0 +1,591 @@
     5.4 +#! /usr/bin/python
     5.5 +# PyMail DTN Nomadic Mail System
     5.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
     5.7 +#
     5.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
     5.9 +#    you may not use this file except in compliance with the License.
    5.10 +#    You may obtain a copy of the License at
    5.11 +# 
    5.12 +#        http://www.apache.org/licenses/LICENSE-2.0
    5.13 +# 
    5.14 +#    Unless required by applicable law or agreed to in writing, software
    5.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
    5.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    5.17 +#    See the License for the specific language governing permissions and
    5.18 +#    limitations under the License.
    5.19 +#
    5.20 +
    5.21 +
    5.22 +"""
    5.23 +DTN-Postfix interface
    5.24 +
    5.25 +A single instance of each of the dtn_send_interface dtn_recv_interface classes
    5.26 +are instantiated and run in threads to manage sending and receiving bundles on
    5.27 +the DTN daemon interface. A third thread is used to handle incoming transmission
    5.28 +reports.
    5.29 +
    5.30 +The send side handles updating the database that records information about
    5.31 +sent messages.  Bundles are sent as files. Files to be sent are queued by
    5.32 +the Postfix outgoing message handler and the 'retry' timer process.
    5.33 +
    5.34 +The report receiver also updates the database as necessary.
    5.35 +
    5.36 +The receive side  dumps the bundle payload as a file in the dtn maildir (this is just
    5.37 +a matter of renaming as we get bundles as files).
    5.38 +
    5.39 +The send and receive threads use separate connections to the DTN daemon because
    5.40 +of the arcane pseudo-half-duplex nature of the DTN daemon.
    5.41 +
    5.42 +At present (until the naming system is improved):
    5.43 +- We expect the local  EID for the gateway to be dtn://gateway.nomadic.n4c.eu
    5.44 +- Oustations will have local  EIDs matching dtn://<user>.nomadic.n4c.eu
    5.45 +- The source EID for this system is <Local EID>/email/out
    5.46 +- Incoming mail bundles should be sent to <Local EID>/email/in
    5.47 +- The reports will be sent back to <Local EID>/email/reports
    5.48 +
    5.49 +Permanent registrations will be created for these EIDs with the 'failure action'
    5.50 +set to DEFER so that bundles are retained in case the mail interface is not
    5.51 +active at the time the bundles arrive. We should do this in the dtnd
    5.52 +configuration file.
    5.53 +
    5.54 +This piece ofthe program is symmetric between the gateway and the outstation
    5.55 +client implementations except for the destination EID for outgoing bundles.
    5.56 +The local EID name is retrieved from the dtnd to determine the source EID etc.
    5.57 +
    5.58 +The sending side receives notications of new emails to send in a Queue.
    5.59 +
    5.60 +The receive side informs the email handlers in one of two ways:
    5.61 +- It pushes an 'event' onto a queue (not used in outstations - there may
    5.62 +  not be a POP3 client active when the message is received).
    5.63 +- It keeps a 'state number' which is incremented each time a new email
    5.64 +  is received.  This is prtected by a Lock so that it can be read securely
    5.65 +  by a POP3 server which can then rebuild its message list - except that
    5.66 +  this does not work.  Clients delet mail as they retrieve it, so
    5.67 +  the list contains empty slots and will not be rebuilt easily.  In practice
    5.68 +  we can forget this wholebusiness.  Just wait until the next retrieval cycle.
    5.69 +  I'll leave the code in for the time being in case I think of another way..
    5.70 +
    5.71 +***************************************
    5.72 +This version can be configured to deal with either direct to Nomadic stations
    5.73 +or using the NAT relay that is reached with static routing before transitioning
    5.74 +to dynamic routing for the final stage.
    5.75 +
    5.76 +The initialisation code for the sending thread has a flag which chooses relay
    5.77 +or non-relay mode.  This just changes the pattern used for destination addresses.
    5.78 +***************************************
    5.79 +
    5.80 +Revision History
    5.81 +================
    5.82 +Version   Date       Author         Notes
    5.83 +0.1       14/03/2010 Elwyn Davies   License statement added.
    5.84 +                                    Relay mode switch added in dtn_send.
    5.85 +0.0	  04/08/2009 Elwyn Davies   Created for N4C Summer tests 2009
    5.86 +  """
    5.87 +
    5.88 +import os
    5.89 +import time
    5.90 +import threading
    5.91 +import dtnapi
    5.92 +import socket
    5.93 +import Queue
    5.94 +import logging
    5.95 +from select import select
    5.96 +from pysqlite2 import dbapi2 as sqlite3
    5.97 +
    5.98 +from dp_msg_send_evt import *
    5.99 +import pypop_maildir
   5.100 +
   5.101 +
   5.102 +# Exception resulting from DTN problems
   5.103 +class DtnError(Exception):
   5.104 +    def __init__(self, reason):
   5.105 +        self.reason = reason
   5.106 +    def __str__(self):
   5.107 +        return "Error communication with DTN daemon: %s" % (repr(self.reason),)
   5.108 +
   5.109 +# Local part of source/destination EID
   5.110 +EMAIL_OUT = "email/out"
   5.111 +EMAIL_IN = "email/in"
   5.112 +
   5.113 +# Local part of report EID
   5.114 +EMAIL_REPORTS = "email/reports"
   5.115 +
   5.116 +# Expiration period for mail bundles
   5.117 +# Try 3 days for now - value in seconds
   5.118 +MAIL_EXPIRY = (3 * 24 * 60 * 60)
   5.119 +
   5.120 +# Status report flag values
   5.121 +# See 
   5.122 +REPORT_STATUS_DELIVERED = 8
   5.123 +REPORT_STATUS_DELETED = 16
   5.124 +
   5.125 +
   5.126 +# Function to make the EIDs (has to be done in each thread)
   5.127 +def build_eids(dtn_handle):
   5.128 +    if dtn_handle == -1:
   5.129 +        raise DtnError("bad DTN handle")
   5.130 +    email_out_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_OUT)
   5.131 +    email_in_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_IN)
   5.132 +    report_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_REPORTS)
   5.133 +    if email_in_addr == None or email_out_addr == None or report_addr == None:
   5.134 +        raise DtnError("failure in dtn_build_local_eid")
   5.135 +    return (email_out_addr, email_in_addr, report_addr)
   5.136 +
   5.137 +# Class for dealing with sending email over DTN
   5.138 +# Single instance runs in a thread - only deals with
   5.139 +# sending so is not bound to any address.
   5.140 +class dtn_send(threading.Thread):
   5.141 +    def __init__(self, domain, send_q, logger, relay_mode):
   5.142 +        threading.Thread.__init__(self, name="dtn-send")
   5.143 +        self.domain = domain
   5.144 +        self.send_q = send_q
   5.145 +
   5.146 +        # Set up logging functions
   5.147 +        self.logger = logger
   5.148 +        self.loginfo = logger.info
   5.149 +        self.logdebug = logger.debug
   5.150 +        self.logerror = logger.error
   5.151 +        self.logwarn = logger.warn
   5.152 +        
   5.153 +        # Flag to keep the loop running
   5.154 +        self.send_run = True
   5.155 +
   5.156 +	# Record whether relay mode or not destination addresses
   5.157 +	self.relay_mode = relay_mode
   5.158 +
   5.159 +    def end_run(self):
   5.160 +        self.send_run = False
   5.161 +        
   5.162 +    def run(self):
   5.163 +        # This function loops forever (well more or less) waiting for
   5.164 +        # queued send requests.  So all the variables can be local.
   5.165 +        # Create a connection to the DTN daemon
   5.166 +        dtn_handle = dtnapi.dtn_open()
   5.167 +        if dtn_handle == -1:
   5.168 +            raise DtnError("unable to open connection with daemon")
   5.169 +
   5.170 +        # Generate the EIDs
   5.171 +        (email_out_eid, email_in_eid, report_eid) = build_eids(dtn_handle)
   5.172 +
   5.173 +        # Open the database connection
   5.174 +        dbconn = None
   5.175 +
   5.176 +        # Specify a basic bundle spec
   5.177 +        # - We always send the payload as a permanent file
   5.178 +        pt = dtnapi.DTN_PAYLOAD_FILE
   5.179 +        # - The source address is always the email_addr
   5.180 +        # - The report address is always the report_addr
   5.181 +        # - The registration id needed in dtn_send is the place
   5.182 +        #   where reports come back to.. we always want reports
   5.183 +        #   but it is unclear why we need to subscribe to the 'session'
   5.184 +        #   just to do the send.The reports will come back through
   5.185 +        #   another connection. Lets try with no regid
   5.186 +        regid = dtnapi.DTN_REGID_NONE
   5.187 +        # - The destination address has to be synthesized (later)
   5.188 +        # - We want delivery reports (and maybe deletion reports?)
   5.189 +        dopts = dtnapi.DOPTS_DELIVERY_RCPT
   5.190 +        # - Send with normal priority.
   5.191 +        pri = dtnapi.COS_NORMAL
   5.192 +        # Mail bundlesshould last a while..
   5.193 +        exp = MAIL_EXPIRY
   5.194 +
   5.195 +        self.logdebug("Entering DTN send Queue loop")
   5.196 +
   5.197 +        # Process mail
   5.198 +        # The thread sits waiting for queued events forever.
   5.199 +        # The thread is terminated by sending a special MSG_END event
   5.200 +        while (self.send_run):
   5.201 +            evt = self.send_q.get()
   5.202 +            if evt.is_last_msg():
   5.203 +                break
   5.204 +
   5.205 +            # Build the destination EID
   5.206 +            # The destination may be a list of email addresses if it
   5.207 +            # is going to the gateway but will only be one
   5.208 +            # address if going to another node directly by DTN
   5.209 +            destn = evt.destination()
   5.210 +            self.logdebug("Message for destination(s) %s" % destn)
   5.211 +            if len(destn) == 0:
   5.212 +                self.logerror("No destinations specified: message ignored")
   5.213 +                continue
   5.214 +            if '@' in destn[0]:
   5.215 +                [user, dest_domain] = destn[0].split("@")
   5.216 +            else:
   5.217 +                self.logwarn("Destination %s does not contain a domain name" %
   5.218 +                             destn[0])
   5.219 +                user = destn[0]
   5.220 +                dest_domain = ""
   5.221 +            if dest_domain != self.domain:
   5.222 +                destn_eid = "dtn://gateway.%s/%s" % (self.domain, EMAIL_IN)
   5.223 +	    elif self.relay_mode:
   5.224 +                destn_eid = "dtn://relay.%s/%s/%s" % (dest_domain, user, EMAIL_IN)
   5.225 +            else:
   5.226 +                destn_eid = "dtn://%s.%s/%s" % (user, dest_domain, EMAIL_IN)
   5.227 +
   5.228 +            self.loginfo("Sending message to %s" % destn_eid)
   5.229 +
   5.230 +            # Send the bundle
   5.231 +            bundle_id = dtnapi.dtn_send(dtn_handle, regid, email_out_eid, destn_eid,
   5.232 +                                        report_eid, pri, dopts, exp, pt,
   5.233 +                                        evt.filename(), "", "")
   5.234 +            if bundle_id == None:
   5.235 +                self.logwarn("Sending of message to %s failed" % destn_eid)
   5.236 +            else:
   5.237 +                # Store the details of the sent bundle
   5.238 +                self.loginfo("%s sent at %d, seq no %d" %(bundle_id.source,
   5.239 +                                                          bundle_id.creation_secs,
   5.240 +                                                          bundle_id.creation_seqno))
   5.241 +        dtnapi.dtn_close(dtn_handle)
   5.242 +        self.loginfo("dtn_send exiting")
   5.243 +
   5.244 +# Class for handling incoming email.
   5.245 +# This only deals with incoming email bundles and is bound to
   5.246 +# <local_eid>/email/in
   5.247 +class dtn_receive(threading.Thread):
   5.248 +    def __init__(self, domain, mailbox, recv_q, logger):
   5.249 +        threading.Thread.__init__(self, name="dtn-receive")
   5.250 +        self.domain = domain
   5.251 +        self.mailbox = mailbox
   5.252 +        # recv_q may be None in an outstation
   5.253 +        self.recv_q = recv_q
   5.254 +
   5.255 +        # New mail state counter and lock
   5.256 +        self.mail_state = 0
   5.257 +        self.mail_state_lock = threading.Lock()
   5.258 +
   5.259 +        # Set up logging functions
   5.260 +        self.logger = logger
   5.261 +        self.loginfo = logger.info
   5.262 +        self.logdebug = logger.debug
   5.263 +        self.logerror = logger.error
   5.264 +        self.logwarn = logger.warn
   5.265 +
   5.266 +        # Flag to keep the loop running
   5.267 +        self.receive_run = True
   5.268 +
   5.269 +    def end_run(self):
   5.270 +        self.receive_run = False
   5.271 +
   5.272 +    def get_mail_state(self):
   5.273 +        self.mail_state_lock.acquire()
   5.274 +        res = self.mail_state
   5.275 +        self.mail_state_lock.release()
   5.276 +        return res
   5.277 +        
   5.278 +    def run(self):
   5.279 +        # This function loops forever (well more or less) waiting for
   5.280 +        # incoming report bundles.
   5.281 +        # This requires it to poll the DTN daemon (yawn).
   5.282 +        # The poll periodically drops out in order to check if the thread has
   5.283 +        # been terminated (this may not be necessary - check if threads hung
   5.284 +        # up in select terminate naturally if the thread is terminated because
   5.285 +        # it is a daemon (but the check is cleaner really).
   5.286 +        
   5.287 +        # Create a connection to the DTN daemon
   5.288 +        dtn_handle = dtnapi.dtn_open()
   5.289 +        if dtn_handle == -1:
   5.290 +            raise DtnError("unable to open connection with daemon")
   5.291 +
   5.292 +        # Generate the EIDs
   5.293 +        (email_out_eid, email_in_eid, report_eid) = build_eids(dtn_handle)
   5.294 +
   5.295 +        # Open the database connection
   5.296 +        dbconn = None
   5.297 +
   5.298 +        # Check if email/in registration exists and register if not
   5.299 +        # Otherwise bind to the existing registration
   5.300 +        regid = dtnapi.dtn_find_registration(dtn_handle, email_in_eid)
   5.301 +        if (regid == -1):
   5.302 +            # Need to register the EID.. make it permanent with 'DEFER'
   5.303 +            # characteristics so that bundles are saved if theye arrive
   5.304 +            # while the handler is inactive
   5.305 +            # Expire the registration a long time in the future
   5.306 +            exp = 365 * 24 * 60 * 60
   5.307 +            # The registration is immediately active
   5.308 +            passive = False
   5.309 +            # We don't want to execute a script
   5.310 +            script = ""
   5.311 +            
   5.312 +            regid = dtnapi.dtn_register(dtn_handle, email_in_eid, dtnapi.DTN_REG_DEFER,
   5.313 +                                        exp, passive, script)
   5.314 +        else:
   5.315 +            dtnapi.dtn_bind(dtn_handle, regid)
   5.316 +
   5.317 +        # Wait for 30 seconds before looping
   5.318 +        recv_timeout = 30
   5.319 +
   5.320 +        self.loginfo("Entering email receive loop")
   5.321 +
   5.322 +        # Now sit and wait for incoming email
   5.323 +        # Note that just using dtn_recv with a timeout doesn't work.
   5.324 +        # The blocking I/O upsets the threading seemingly.
   5.325 +        receive_fd = dtnapi.dtn_poll_fd(dtn_handle)
   5.326 +        while (self.receive_run):
   5.327 +            # Poll currently sets timeout in ms - this is a bug
   5.328 +            dtnapi.dtn_begin_poll(dtn_handle, 5000)
   5.329 +            # The timeout on select is much longer than the dtn_begin_poll
   5.330 +            # timeout, so there is a problem if there is nothing to read
   5.331 +            rd_fd, wr_fd, err_fd = select([receive_fd], [], [], 10)
   5.332 +            if (len(rd_fd) != 1) or (rd_fd[0] != receive_fd):
   5.333 +                # Cancel the poll anyway
   5.334 +                dtnapi.dtn_cancel_poll(dtn_handle)
   5.335 +                raise DtnError("Report select call timed out")
   5.336 +            
   5.337 +            """
   5.338 +            There should always be something to read
   5.339 +            Put in a timeout just in case
   5.340 +            The call to dtn_recv terminates the poll
   5.341 +            We accept the email as a (temporary) file.
   5.342 +            There is a nasty snag here. There is small window
   5.343 +            of opportunity where the bundle has been written
   5.344 +            to a temporary file delivered to this application
   5.345 +            but before file has been renamed and registered in the maildir
   5.346 +            and database. If the application exits during this period
   5.347 +            the email will be lost.  This is not a happy situation.
   5.348 +            At present DTN2 does not have a means for dtn_recv to offer
   5.349 +            a filename that could be used.
   5.350 +            
   5.351 +            Uing DTN_PAYLOAD_MEM would present a similar problem
   5.352 +            During the period the memory image is being written to file
   5.353 +            in the application there is no permanent  copy of the file
   5.354 +            but the DTN daemon believes it has delivered and is busy sending
   5.355 +            a delivery notification.
   5.356 +
   5.357 +            NOTE: On receiving the file, the file name is in the bundle
   5.358 +            payload as a NULL terminated string.  Python leaves the terminating
   5.359 +            byte in place.
   5.360 +            """
   5.361 +            email = dtnapi.dtn_recv(dtn_handle, dtnapi.DTN_PAYLOAD_FILE, 1)
   5.362 +            if email != None:
   5.363 +                fn = email.payload
   5.364 +                l = len(fn)
   5.365 +                if fn[l-1] == "\x00":
   5.366 +                    fn = fn[:-1]
   5.367 +                self.logdebug("Got incoming bundle in file %s" % fn)
   5.368 +
   5.369 +                # Insert new message file  into the inbox
   5.370 +                final_fn = self.mailbox.newMessageFile(fn)
   5.371 +                if final_fn == None:
   5.372 +                    raise DtnError("Cannot store new mail in inbox")
   5.373 +
   5.374 +                self.loginfo("Received email bundle from %s sent %d seq %d" %
   5.375 +                             (email.source,
   5.376 +                              email.creation_secs,
   5.377 +                              email.creation_seqno))
   5.378 +                self.loginfo("New email stored as %s" % final_fn)
   5.379 +                
   5.380 +                # Update database
   5.381 +
   5.382 +                # When this is used in a gateway there will be a recv_q
   5.383 +                # where this can be sent off to be fed to Postfix
   5.384 +                # In an outstation, all that needs to be done is flag
   5.385 +                # that a new message has been added to the maildir
   5.386 +                # In practice this may be redundant. Not sure how POP3
   5.387 +                # deals with new mail arriving during a session.
   5.388 +                if self.recv_q != None:                             
   5.389 +                    # Put message on the queue to send it on to Postfix
   5.390 +                    evt = msg_send_evt(MSG_SMTP, final_fn, MSG_NEW, "smtp server")
   5.391 +                    self.recv_q.put_nowait(evt)
   5.392 +
   5.393 +                # Increment the state variable - locking while we do
   5.394 +                self.mail_state_lock.acquire()
   5.395 +                self.mail_state += 1
   5.396 +                self.mail_state_lock.release()
   5.397 +                    
   5.398 +            elif dtnapi.dtn_errno(dtn_handle) != dtnapi.DTN_ETIMEOUT:
   5.399 +                raise DtnError("Report: dtn_recv failed with error code %d" %
   5.400 +                               dtnapi.dtn_errno(dtn_handle))
   5.401 +            else:
   5.402 +                pass
   5.403 +                               
   5.404 +        dtnapi.dtn_close(dtn_handle)
   5.405 +        self.loginfo("dtn_receive exiting")
   5.406 +        
   5.407 +# Class for handling incoming reports.
   5.408 +# This only deals with incoming report bundles and is bound to
   5.409 +# <local_eid>/email/reports
   5.410 +class dtn_report(threading.Thread):
   5.411 +    def __init__(self, domain, logger):
   5.412 +        threading.Thread.__init__(self, name="dtn-report")
   5.413 +        self.domain = domain
   5.414 +
   5.415 +        # Set up logging functions
   5.416 +        self.logger = logger
   5.417 +        self.loginfo = logger.info
   5.418 +        self.logdebug = logger.debug
   5.419 +        self.logerror = logger.error
   5.420 +        self.logwarn = logger.warn
   5.421 +
   5.422 +        # Flag to keep the loop running
   5.423 +        self.report_run = True
   5.424 +
   5.425 +    def end_run(self):
   5.426 +        self.report_run = False
   5.427 +        
   5.428 +    def run(self):
   5.429 +        # This function loops forever (well more or less) waiting for
   5.430 +        # incoming report bundles.
   5.431 +        # This requires it to poll the DTN daemon (yawn).
   5.432 +        # The poll periodically drops out in order to check if the thread has
   5.433 +        # been terminated (this may not be necessary - check if threads hung
   5.434 +        # up in select terminate naturally if the thread is terminated because
   5.435 +        # it is a daemon (but the check is cleaner really).
   5.436 +        
   5.437 +        # Create a connection to the DTN daemon
   5.438 +        dtn_handle = dtnapi.dtn_open()
   5.439 +        if dtn_handle == -1:
   5.440 +            raise DtnError("unable to open connection with daemon")
   5.441 +
   5.442 +        # Generate the EIDs
   5.443 +        (email_out_eid, email_in_eid, report_eid) = build_eids(dtn_handle)
   5.444 +        
   5.445 +        # Open the database connection
   5.446 +        dbconn = None
   5.447 +
   5.448 +        # Check if email/report registration exists and register if not
   5.449 +        # Otherwise bind to the existing registration
   5.450 +        regid = dtnapi.dtn_find_registration(dtn_handle, report_eid)
   5.451 +        if (regid == -1):
   5.452 +            # Need to register the EID.. make it permanent with 'DEFER'
   5.453 +            # characteristics so that bundles are saved if theye arrive
   5.454 +            # while the handler is inactive
   5.455 +            # Expire the registration a long time in the future
   5.456 +            exp = 365 * 24 * 60 * 60
   5.457 +            # The registration is immediately active
   5.458 +            passive = False
   5.459 +            # We don't want to execute a script
   5.460 +            script = ""
   5.461 +            
   5.462 +            regid = dtnapi.dtn_register(dtn_handle, report_eid, dtnapi.DTN_REG_DEFER,
   5.463 +                                        exp, passive, script)
   5.464 +        else:
   5.465 +            dtnapi.dtn_bind(dtn_handle, regid)
   5.466 +
   5.467 +        # Wait for 30 seconds before looping
   5.468 +        recv_timeout = 30
   5.469 +
   5.470 +        self.loginfo("Entering DTN report recv loop")
   5.471 +
   5.472 +        # Now sit and wait for incoming reports
   5.473 +        # Note that just using dtn_recv with a timeout doesn't work.
   5.474 +        # The blocking I/O upsets the threading seemingly.
   5.475 +        report_fd = dtnapi.dtn_poll_fd(dtn_handle)
   5.476 +        while (self.report_run):
   5.477 +            # Poll currently sets timeout in ms - this is a bug
   5.478 +            dtnapi.dtn_begin_poll(dtn_handle, 5000)
   5.479 +            # The timeout on select is much longer than the dtn_begin_poll
   5.480 +            # timeout, so there is a problem if there is nothing to read
   5.481 +            rd_fd, wr_fd, err_fd = select([report_fd], [], [], 10)
   5.482 +            if (len(rd_fd) != 1) or (rd_fd[0] != report_fd):
   5.483 +                # Cancel the poll anyway
   5.484 +                dtnapi.dtn_cancel_poll(dtn_handle)
   5.485 +                raise DtnError("Report select call timed out")
   5.486 +            
   5.487 +            # There should always be something to read
   5.488 +            # Put in a timeout just in case
   5.489 +            # The call to dtn_recv terminates the poll
   5.490 +            report = dtnapi.dtn_recv(dtn_handle, dtnapi.DTN_PAYLOAD_MEM, 1)
   5.491 +            if report != None:
   5.492 +                self.logdebug("Received bundle")
   5.493 +                if report.status_report != None:
   5.494 +                    self.logdebug("Received status report")
   5.495 +                    if report.status_report.flags == REPORT_STATUS_DELIVERED:
   5.496 +                        self.loginfo( "Received delivery report re from %s sent %d seq %d" %
   5.497 +                                      (report.status_report.bundle_id.source,
   5.498 +                                       report.status_report.bundle_id.creation_secs,
   5.499 +                                       report.status_report.bundle_id.creation_seqno))
   5.500 +                        # Update the database
   5.501 +      
   5.502 +                    elif report.status_report.flags == REPORT_STATUS_DELETED:
   5.503 +                        self.loginfo("Received deletion report re from %s sent %d seq %d" %
   5.504 +                                     (report.status_report.bundle_id.source,
   5.505 +                                      report.status_report.bundle_id.creation_secs,
   5.506 +                                      report.status_report.bundle_id.creation_seqno))
   5.507 +                        # Update the database
   5.508 +
   5.509 +                    else:
   5.510 +                        self.logwarn("Received unexpected report: Flags: %d" % report.status_report.flags)
   5.511 +
   5.512 +            elif dtnapi.dtn_errno(dtn_handle) != dtnapi.DTN_ETIMEOUT:
   5.513 +                self.logerror("dtn_recv failed with error code %d" %
   5.514 +                              dtnapi.dtn_errno(dtn_handle))
   5.515 +                raise DtnError("Report: dtn_recv failed with error code %d" %
   5.516 +                               dtnapi.dtn_errno(dtn_handle))
   5.517 +            else:
   5.518 +                pass
   5.519 +                               
   5.520 +        dtnapi.dtn_close(dtn_handle)
   5.521 +        self.loginfo("dtn_report exiting")
   5.522 +            
   5.523 +# Test code
   5.524 +if __name__ == "__main__":
   5.525 +    logger = logging.getLogger("test")
   5.526 +    logger.setLevel(logging.DEBUG)
   5.527 +    ch = logging.StreamHandler()
   5.528 +    fmt = logging.Formatter("%(levelname)s %(threadName)s %(message)s")
   5.529 +    ch.setFormatter(fmt)
   5.530 +    logger.addHandler(ch)
   5.531 +    
   5.532 +    dtn_send_q = Queue.Queue()
   5.533 +    dtn_recv_q = Queue.Queue()
   5.534 +    nomadic_domain = "nomadic.n4c.eu"
   5.535 +    tmp_domain = pypop_maildir.MaildirDirdbmDomain("/tmp")
   5.536 +    if not tmp_domain.userDirectory("dtnin"):
   5.537 +        tmp_domain.addUser("dtnin", "password")
   5.538 +    dtnin_mailbox = pypop_maildir.MaildirMailbox(tmp_domain.userDirectory("dtnin"),
   5.539 +                                                 "dtnin")
   5.540 +    
   5.541 +    # Sending thread
   5.542 +    dtn_send_handler = dtn_send(nomadic_domain, dtn_send_q, logger)
   5.543 +    dtn_send_handler.setDaemon(True)
   5.544 +    dtn_send_handler.start()
   5.545 +
   5.546 +    # Receiving thread
   5.547 +    dtn_recv_handler = dtn_receive(nomadic_domain, dtnin_mailbox,
   5.548 +                                   dtn_recv_q, logger)
   5.549 +    dtn_recv_handler.setDaemon(True)
   5.550 +    dtn_recv_handler.start()
   5.551 +
   5.552 +    # Report handler thread
   5.553 +    dtn_report_handler = dtn_report(nomadic_domain, logger)
   5.554 +    dtn_report_handler.setDaemon(True)
   5.555 +    dtn_report_handler.start()
   5.556 +
   5.557 +    time.sleep(0.1)
   5.558 +    logger.info("Send handler running: %s" % dtn_send_handler.getName())
   5.559 +    logger.info("Receive handler running: %s" % dtn_recv_handler.getName())
   5.560 +    logger.info("Report handler running: %s" % dtn_report_handler.getName())
   5.561 +
   5.562 +    evt = msg_send_evt(MSG_DTN,
   5.563 +                       "/etc/group",
   5.564 +                       MSG_NEW,
   5.565 +                       ["elwynd@nomadic.n4c.eu"])
   5.566 +    dtn_send_q.put_nowait(evt)
   5.567 +
   5.568 +    logger.info("Waiting for incoming mail")
   5.569 +    evt = dtn_recv_q.get()
   5.570 +    logger.info("Mail received: ", evt.filename())
   5.571 +
   5.572 +    try:
   5.573 +        while True:
   5.574 +            pass
   5.575 +    except:
   5.576 +        evt = msg_send_evt(MSG_END,
   5.577 +                           "",
   5.578 +                           MSG_NEW,
   5.579 +                           "")
   5.580 +        dtn_send_q.put_nowait(evt)
   5.581 +        dtn_recv_handler.end_run()
   5.582 +        dtn_report_handler.end_run()
   5.583 +        time.sleep(1)
   5.584 +        
   5.585 +
   5.586 +        
   5.587 +    
   5.588 +
   5.589 +                                 
   5.590 +        
   5.591 +
   5.592 +         
   5.593 +
   5.594 +        
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/gateway/dtn/python/dp_main.py	Tue Mar 16 13:36:42 2010 +0000
     6.3 @@ -0,0 +1,229 @@
     6.4 +#! /usr/bin/python
     6.5 +# PyMail DTN Nomadic Mail System
     6.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
     6.7 +#
     6.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
     6.9 +#    you may not use this file except in compliance with the License.
    6.10 +#    You may obtain a copy of the License at
    6.11 +# 
    6.12 +#        http://www.apache.org/licenses/LICENSE-2.0
    6.13 +# 
    6.14 +#    Unless required by applicable law or agreed to in writing, software
    6.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
    6.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    6.17 +#    See the License for the specific language governing permissions and
    6.18 +#    limitations under the License.
    6.19 +#
    6.20 +
    6.21 +
    6.22 +"""
    6.23 +Postfix to DTN Interface - main routine
    6.24 +
    6.25 +This code sets up the set of threads that manage the interface:
    6.26 +
    6.27 +The interface manages five things:
    6.28 +- Managing outgoing messages coming from Postfix and sending them to dtnd
    6.29 +- Managing incoming messages coming from dtnd and sending them to Postfix
    6.30 +- Maintaining a maildir that contains copies of all mail messages sent out
    6.31 +  to dtnd but for which no delivery confirmation has yet been received.
    6.32 +- Maintaining a maildir that contains copies of  messages received from dtnd
    6.33 +  that it has not yet been possible to relay to Postfix
    6.34 +- Managing timers that determine when a message needs to be resent because it
    6.35 +  will have expired in the network subject to limit on the number of retries.
    6.36 +  Messages that have not been delivered after the retry limit are deleted
    6.37 +  and an 'undeliverable' message sent to the sender via Postfix.
    6.38 +- Managing a list of recently received messages that allows duplicate messages
    6.39 +  received from dtnd to be suppressed. The list is purged at  the expiry time
    6.40 +  of the bundle that brought the first copy plus a grace period.
    6.41 +
    6.42 +
    6.43 +There are four permanent threads created:
    6.44 +- Thread to accept connection requests from Postfix outbound pipe shim
    6.45 +    This thread opens a listening socket and waits for (local) connections
    6.46 +    from the Postfix outbound pipe shim.  Whenever a new connection is made
    6.47 +    Postfix will be relaying a (single) message.  This is expected to be in
    6.48 +    RFC 2822 format as with BSMTP (i.e.lines terminated with \r\n, lines
    6.49 +    containing only a . being turned into ..) and with a Delibvered To:
    6.50 +    header prepended.  The listener thread spawns a new thread that reads
    6.51 +    the message and dumps it into a file with a unique name in the maildir.
    6.52 +    When the message has been completly received a send request is queued
    6.53 +    for the the dtn manager thread.
    6.54 +- Thread to handle the dtnd connection.  The dtnd connection can both send
    6.55 +  and receive bundles in a sort of half duplex arrangement.  The thread
    6.56 +  manages the connection, waiting for incoming bundles when there is
    6.57 +  nothing else to do, periodically checking the send request queue and sending
    6.58 +  any messages in the request queue as bundles.  Incoming bundles with messages
    6.59 +  are dumped into files in the maildir (or if received as files, moved there).
    6.60 +  The file format is checked and the Delivered To: header checked. The database
    6.61 +  of recently received messages is checked to detect duplicates and duplicates
    6.62 +  discarded. If the header is for the DTN association being managed, the message
    6.63 +  is queued for resending. Otherwise a request to send to Postfix is queued.
    6.64 +  Bundles may also contain delivery reports. The mail message associated with
    6.65 +  the report is deleted from the outgoing maildir and records of the message
    6.66 +  are discarded.
    6.67 +- Thread to manage sending incoming messages (and delivery failure reports).
    6.68 +  This can be done either by calling sendmail or making an SMTP connection.
    6.69 +  Optimum solution TBD.  Probably SMTP.  Requests are received from a queue.
    6.70 +  Successfully sent mails are deleted from the maildir but the message is
    6.71 +  recorded in a database to manage duplicate suppression.
    6.72 +- Thread to handle manage message storage.  Deals with resends and sending
    6.73 +  delivery failure reports.  Queues requests on the dtnd and Postfix dending
    6.74 +  threads as required. On startup checks for unsent messages (either way)
    6.75 +  and requeues them for sending.
    6.76 +
    6.77 +Two maildirs are used: one each for incoming and outgoing mail.
    6.78 +
    6.79 +The records of timing and duplicate testing are (will be) held in a SQLite
    6.80 +database
    6.81 +
    6.82 +NOTE:
    6.83 +The Postfix outging mail server threads make use of the Python SocketServer
    6.84 +framework. Frankly, the code in the Python 2.5 library is complete rubbish.
    6.85 +It is esentially impossible to set the reuse address flag making (at least) testing
    6.86 +very difficult.  Further it is extremely difficult to shutdown the servers
    6.87 +cleanly.  Most of this is fixed in the Python 2.6 release. Since this module
    6.88 +is pure Python and doesn't do anything v2.6 specific itself, I have extracted
    6.89 +the module and am using for this program.
    6.90 +"
    6.91 +***************************************
    6.92 +This version can be configured to deal with either direct to Nomadic stations
    6.93 +or using the NAT relay that is reached with static routing before transitioning
    6.94 +to dynamic routing for the final stage. This just involves setting the boolean
    6.95 +flag relay_mode when creating the dtn_send thread.
    6.96 +***************************************
    6.97 +
    6.98 +Revision History
    6.99 +================
   6.100 +Version   Date       Author         Notes
   6.101 +0.1       14/03/2010 Elwyn Davies   License statement added.
   6.102 +                                    Relay mode switch incorporated.
   6.103 +0.0	  25/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
   6.104 +"""
   6.105 +
   6.106 +import os
   6.107 +import time
   6.108 +import threading
   6.109 +import Queue
   6.110 +import socket
   6.111 +from SocketServer2_6 import *
   6.112 +import logging
   6.113 +import logging.handlers
   6.114 +import logging.config
   6.115 +
   6.116 +import pypop_maildir
   6.117 +import dp_pfmailout
   6.118 +import dp_pfmailin
   6.119 +import dp_dtn
   6.120 +import dtnapi
   6.121 +from dp_msg_send_evt import *
   6.122 +
   6.123 +def main(mailbox_root, log_file, log_config_file):
   6.124 +    # Setup logging
   6.125 +    logging.config.fileConfig(log_config_file)
   6.126 +    pymail_logger = logging.getLogger("dtn_pymail")
   6.127 +    loginfo = pymail_logger.info
   6.128 +    logdebug = pymail_logger.debug
   6.129 +
   6.130 +    # Set domain for which we are handling mail
   6.131 +    mail_domain = "nomadic.n4c.eu"
   6.132 +    
   6.133 +    # Check mailboxes dtnin and dtnout  exist and create if not ***TBD***
   6.134 +    nomadic_domain = pypop_maildir.MaildirDirdbmDomain(mailbox_root)
   6.135 +    if not nomadic_domain.userDirectory("dtnin"):
   6.136 +        nomadic_domain.addUser("dtnin", "password")
   6.137 +    if not nomadic_domain.userDirectory("dtnout"):
   6.138 +        nomadic_domain.addUser("dtnout", "password")
   6.139 +    dtnin_mailbox  = pypop_maildir.MaildirMailbox(nomadic_domain.userDirectory("dtnin"),
   6.140 +                                                  "dtnin")
   6.141 +    dtnout_mailbox = pypop_maildir.MaildirMailbox(nomadic_domain.userDirectory("dtnout"),
   6.142 +                                                  "dtnout")
   6.143 +
   6.144 +    # Create queues to send requests for messages to be written out
   6.145 +    # FIFO queues with no depth limitation at present
   6.146 +    dtn_send_q = Queue.Queue()
   6.147 +    postfix_send_q = Queue.Queue()
   6.148 +
   6.149 +    #====================================================================#
   6.150 +    # Create server to handle mails from Postfix
   6.151 +    pfmailout = dp_pfmailout.postfix_mailout_server(dtnout_mailbox, dtn_send_q,
   6.152 +                                                    pymail_logger)
   6.153 +
   6.154 +    # Start a thread with the server -- that thread will then start one
   6.155 +    # more thread for each request
   6.156 +    mailout_listener = threading.Thread(target=pfmailout.serve_forever)
   6.157 +    # Exit the server thread when the main thread terminates
   6.158 +    mailout_listener.setDaemon(True)
   6.159 +    mailout_listener.start()
   6.160 +    
   6.161 +    #====================================================================#
   6.162 +    # Create threads to handle DTN interface
   6.163 +    # There are three threads that desl with:
   6.164 +    # - sending emails
   6.165 +    # - receiving emails
   6.166 +    # - receiving reports
   6.167 +
   6.168 +    # Sending thread
   6.169 +    dtn_send_handler = dp_dtn.dtn_send(mail_domain,
   6.170 +                                       dtn_send_q, pymail_logger, 
   6.171 +				       relay_mode=True)
   6.172 +    dtn_send_handler.setDaemon(True)
   6.173 +    dtn_send_handler.start()
   6.174 +
   6.175 +     # Receiving thread
   6.176 +    dtn_receive_handler = dp_dtn.dtn_receive(mail_domain, dtnin_mailbox,
   6.177 +                                             postfix_send_q, pymail_logger)
   6.178 +    dtn_receive_handler.setDaemon(True)
   6.179 +    dtn_receive_handler.start()
   6.180 +    
   6.181 +    # Report handler thread
   6.182 +    dtn_report_handler = dp_dtn.dtn_report(mail_domain, pymail_logger)
   6.183 +    dtn_report_handler.setDaemon(True)
   6.184 +    dtn_report_handler.start()
   6.185 +    
   6.186 +    #====================================================================#
   6.187 +    # Sender for incoming mails to  be deleivered by Postfix
   6.188 +    postfix_sender = dp_pfmailin.PostfixMailinHandler(mail_domain, dtnin_mailbox,
   6.189 +                                                    postfix_send_q, pymail_logger)
   6.190 +    postfix_sender.setDaemon(True)
   6.191 +    postfix_sender.start()
   6.192 + 
   6.193 +
   6.194 +    # Let everything have a chance to get going (old trick)
   6.195 +    time.sleep(0.1)
   6.196 +    loginfo("Postfix outgoing mail listener running in thread: %s" % mailout_listener.getName())
   6.197 +    loginfo("Postfix incoming mail sender running in thread: %s" % postfix_sender.getName())
   6.198 +    loginfo("DTN Send handler running in thread: %s" % dtn_send_handler.getName())
   6.199 +    loginfo("DTN Receive handler running in thread: %s" % dtn_receive_handler.getName())
   6.200 +
   6.201 +    loginfo("DTN Report handler running in thread: %s" % dtn_report_handler.getName())   
   6.202 +      
   6.203 +    try:
   6.204 +        while True:
   6.205 +            pass
   6.206 +    except:
   6.207 +        # Shutdown
   6.208 +        # Think a bit about ordering here...
   6.209 +        # Shutdown the producer threads first
   6.210 +        dtn_receive_handler.end_run()
   6.211 +        pfmailout.end_run()
   6.212 +
   6.213 +        # Give them a few seconds to die...
   6.214 +        time.sleep(6)
   6.215 +
   6.216 +        # Then shutdown the consumer threads
   6.217 +        dtn_report_handler.end_run()
   6.218 +        evt = msg_send_evt(MSG_END,
   6.219 +                           "",
   6.220 +                           MSG_NEW,
   6.221 +                           "")
   6.222 +        dtn_send_q.put_nowait(evt)
   6.223 +        postfix_send_q.put_nowait(evt)
   6.224 +
   6.225 +        # ..and wait for them to die...
   6.226 +        time.sleep(2)                                    
   6.227 +        
   6.228 +
   6.229 +if __name__ == "__main__":
   6.230 +    main("/home/dtn/maildir", "/home/dtn/log/dtn_pymail.log", "/home/dtn/dtn/dtn_pymail_log.conf")
   6.231 +    # Test code
   6.232 +
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/gateway/dtn/python/dp_msg_send_evt.py	Tue Mar 16 13:36:42 2010 +0000
     7.3 @@ -0,0 +1,99 @@
     7.4 +#! /usr/bin/python
     7.5 +# PyMail DTN Nomadic Mail System
     7.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
     7.7 +#
     7.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
     7.9 +#    you may not use this file except in compliance with the License.
    7.10 +#    You may obtain a copy of the License at
    7.11 +# 
    7.12 +#        http://www.apache.org/licenses/LICENSE-2.0
    7.13 +# 
    7.14 +#    Unless required by applicable law or agreed to in writing, software
    7.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
    7.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    7.17 +#    See the License for the specific language governing permissions and
    7.18 +#    limitations under the License.
    7.19 +#
    7.20 +
    7.21 +
    7.22 +"""
    7.23 +DTN-Postfix interface
    7.24 +
    7.25 +Event object class
    7.26 +
    7.27 +Instances of this object are passed between the various threads of the
    7.28 +system in the send queues to tell the sending threads what work they have
    7.29 +to do.
    7.30 +
    7.31 +Each contains
    7.32 +- the filename of a file that contains the email to send
    7.33 +- a flag indicating if the item is new or a resend
    7.34 +- (for debugging purposes) an indicator of whether DTN or Postfix send is needed
    7.35 +
    7.36 +Revision History
    7.37 +================
    7.38 +Version   Date       Author         Notes
    7.39 +0.1       14/03/2010 Elwyn Davies   License statement added.
    7.40 +0.0	  24/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
    7.41 +"""
    7.42 +import os
    7.43 +
    7.44 +# Whether the message is new or a retry (affects what we do to the database)
    7.45 +MSG_NEW = 0
    7.46 +MSG_RETRY = 1
    7.47 +
    7.48 +# Type send needed - or terminate sending process
    7.49 +MSG_DTN = "dtn_send"
    7.50 +MSG_SMTP = "smtp_send"
    7.51 +MSG_END = "end_send"
    7.52 +
    7.53 +
    7.54 +class msg_send_evt:
    7.55 +    def __init__(self, send_type, filename, new_or_retry, destination):
    7.56 +
    7.57 +        # Check the parameters are valid
    7.58 +        # (note the file *might* change under our feet - no guarantees later)
    7.59 +        if send_type not in (MSG_DTN, MSG_SMTP, MSG_END):
    7.60 +            raise ValueError
    7.61 +        if (send_type != MSG_END):
    7.62 +            if new_or_retry not in (MSG_NEW, MSG_RETRY):
    7.63 +                raise ValueError
    7.64 +            if not os.access(filename, os.R_OK):
    7.65 +                raise ValueError
    7.66 +        else:
    7.67 +            filename = ""
    7.68 +            new_or_retry = MSG_NEW
    7.69 +            destination = ""
    7.70 +        
    7.71 +        self._filename =  filename
    7.72 +        self.new_or_retry = new_or_retry
    7.73 +        self.send_type = send_type
    7.74 +        self._destination = destination
    7.75 +
    7.76 +    def is_new(self):
    7.77 +        return self.new_or_retry == MSG_NEW
    7.78 +
    7.79 +    def is_last_msg(self):
    7.80 +        return self.send_type == MSG_END
    7.81 +
    7.82 +    def filename(self):
    7.83 +        return self._filename
    7.84 +
    7.85 +    def destination(self):
    7.86 +        return self._destination
    7.87 +
    7.88 +    def __repr__(self):
    7.89 +        if self.send_type == MSG_END:
    7.90 +            return "Ending sending."
    7.91 +        else:
    7.92 +            return "%s message in file %s to be sent by %s to %s" % ({ MSG_NEW: "New", MSG_RETRY: "Resending"}[self.new_or_retry],
    7.93 +                                                                     self._filename,
    7.94 +                                                                     self.send_type,
    7.95 +                                                                     self._destination)
    7.96 +
    7.97 +if __name__ == "__main__":
    7.98 +    evt = msg_send_evt(MSG_DTN, "/etc/passwd", MSG_RETRY, "abc@def.ghi")
    7.99 +    print evt
   7.100 +    evt = msg_send_evt(MSG_END, "", MSG_NEW, "")
   7.101 +    print evt
   7.102 +    evt = msg_send_evt(MSG_SMTP, "/etc/passwd", 3, "abd@def.ghi")
     8.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     8.2 +++ b/gateway/dtn/python/dp_pfmailin.py	Tue Mar 16 13:36:42 2010 +0000
     8.3 @@ -0,0 +1,211 @@
     8.4 +#! /usr/bin/python
     8.5 +# PyMail DTN Nomadic Mail System
     8.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
     8.7 +#
     8.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
     8.9 +#    you may not use this file except in compliance with the License.
    8.10 +#    You may obtain a copy of the License at
    8.11 +# 
    8.12 +#        http://www.apache.org/licenses/LICENSE-2.0
    8.13 +# 
    8.14 +#    Unless required by applicable law or agreed to in writing, software
    8.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
    8.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    8.17 +#    See the License for the specific language governing permissions and
    8.18 +#    limitations under the License.
    8.19 +#
    8.20 +
    8.21 +"""
    8.22 +DTN-Postfix interface
    8.23 +
    8.24 +Handlers for messages being sent to Postfix after being relayed through DTN
    8.25 +
    8.26 +The thread makes a connection to Postfix over local SMTP and sends messages
    8.27 +using the smtplib.  The messages are not significantly processed, but
    8.28 +it is necessary to get the To and From addresses in order to build a wrapper.
    8.29 +
    8.30 +The message is stored in a maildir file  - the lines are terminated with \n
    8.31 +
    8.32 +Revision History
    8.33 +================
    8.34 +Version   Date       Author         Notes
    8.35 +0.1       14/03/2010 Elwyn Davies   License statement added.
    8.36 +0.0	  24/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
    8.37 +"""
    8.38 +
    8.39 +import os
    8.40 +import socket
    8.41 +import threading
    8.42 +import smtplib
    8.43 +import email
    8.44 +import logging
    8.45 +
    8.46 +import Queue
    8.47 +
    8.48 +import pypop_maildir
    8.49 +from dp_msg_send_evt import *
    8.50 +
    8.51 +# Special email header carrying recipient addresses across DTN
    8.52 +DTN_RCPTS_HDR = "X-DTN-rcpts"
    8.53 +
    8.54 +class PostfixMailinHandler(threading.Thread):
    8.55 +
    8.56 +    def __init__(self, domain, mailbox, send_q, logger):
    8.57 +        threading.Thread.__init__(self, name="postfix-send")
    8.58 +        self.domain = domain
    8.59 +        self.mailbox = mailbox
    8.60 +        self.send_q = send_q
    8.61 +        self.logger = logger
    8.62 +
    8.63 +        # Flag to keep the loop running
    8.64 +        self.mailin_run = True
    8.65 +
    8.66 +    def end_run(self):
    8.67 +        self.mailin_run = False
    8.68 +        
    8.69 +    def run(self):
    8.70 +        self.mailin_thread = threading.currentThread()
    8.71 +
    8.72 +        # Logging functions
    8.73 +        self.loginfo = self.logger.info
    8.74 +        self.logdebug = self.logger.debug
    8.75 +        self.logwarn = self.logger.warn
    8.76 +        self.logerror = self.logger.error
    8.77 +
    8.78 +        # Create an SMTP connection manager for the mailserver on localhost
    8.79 +        smtp_link = smtplib.SMTP()
    8.80 +
    8.81 +        # Loop waiting for messages to come from DTN (or end message)
    8.82 +        self.loginfo("Entering SMTP send Queue loop")
    8.83 +        
    8.84 +        # The thread sits waiting for queued events forever.
    8.85 +        # The thread is terminated by sending a special MSG_END event
    8.86 +        while (self.mailin_run):
    8.87 +            evt = self.send_q.get()
    8.88 +            if evt.is_last_msg():
    8.89 +                self.loginfo("Mailin sender finishing")
    8.90 +                break
    8.91 +
    8.92 +            # The received evt points to a file containing the email
    8.93 +            # message to be sent
    8.94 +            # Open it and parse the message to make sure it is real
    8.95 +            self.logdebug("new mail send event received")
    8.96 +            try:
    8.97 +                fp = None
    8.98 +                fp = open(evt.filename(), "r")
    8.99 +                email_msg = email.message_from_file(fp)
   8.100 +                fp.close()
   8.101 +
   8.102 +            except:
   8.103 +                print "Unable to extract mail message from %s." % evt.filename()
   8.104 +                if fp != None and not fp.closed:
   8.105 +                    fp.close()
   8.106 +
   8.107 +                # We should trash this mail message and possibly send a non-delivery
   8.108 +                # message.. but if the message is seriously garbled there won't be
   8.109 +                # anything we can do.  Don't do anything for the time being
   8.110 +                # We should move this out of the new folder at least.
   8.111 +                continue
   8.112 +
   8.113 +            # The message ought to contain the special X- header with the reciepients
   8.114 +            # that were given to the SMTP daemon on the other side of the link
   8.115 +            if email_msg.has_key(DTN_RCPTS_HDR):
   8.116 +                rcpts_str = email_msg.get(DTN_RCPTS_HDR)
   8.117 +            else:
   8.118 +                self.logerror("Email message in %s from DTN doesn't have %s header" % 
   8.119 +                              (evt.filename(), DTN_RCPTS_HDR))
   8.120 +
   8.121 +                # Again should get rid of these message
   8.122 +                continue
   8.123 +
   8.124 +            # Extract the addresses from the value string
   8.125 +            self.logdebug("Sending to %s" % rcpts_str)
   8.126 +            smtp_rcpts = rcpts_str.split()
   8.127 +            rcpts = []
   8.128 +            for r in smtp_rcpts:
   8.129 +                if r[0] == "<" and r[-1] == ">" and r != "<>":
   8.130 +                    rcpts.append(r[1:-1])
   8.131 +
   8.132 +            # The message should have a Unix From envelope header
   8.133 +            unixfrom = email_msg.get_unixfrom()
   8.134 +            if unixfrom == None:
   8.135 +                self.logerror("Email message in %s from DTN doesn't have From envelope header" %
   8.136 +                              evt.filename())
   8.137 +
   8.138 +                # Again get rid of the message
   8.139 +                continue
   8.140 +            else:
   8.141 +                from_cmpts = unixfrom.split()
   8.142 +                if len(from_cmpts) < 2:
   8.143 +                    self.logerror("Email message in %s from DTN has malformed From envelope header" %
   8.144 +                                  evt.filename())
   8.145 +
   8.146 +                    # Again get rid of the message
   8.147 +                    continue
   8.148 +                else:
   8.149 +                    from_addr = from_cmpts[1]
   8.150 +
   8.151 +            # We can now send the email to Postfix (on localhost - defaults
   8.152 +            # for connect are port 25 on localhost)
   8.153 +            try:
   8.154 +                self.logdebug("Opening link to Postfix")
   8.155 +                smtp_link.connect()
   8.156 +                smtp_link.sendmail(from_addr, smtp_rcpts, email_msg.as_string(True))
   8.157 +                self.loginfo("Email in %s passed to Postfix for sending to %s" %
   8.158 +                             (evt.filename(), " ".join(smtp_rcpts)))
   8.159 +            except:
   8.160 +                self.logerror("Unable to send message in %s to Postfix." %
   8.161 +                              evt.filename())
   8.162 +            finally:
   8.163 +                self.logdebug("Closing link to Postfix")
   8.164 +                smtp_link.quit()
   8.165 +if __name__ == "__main__":
   8.166 +    import time
   8.167 +    logger = logging.getLogger("test")
   8.168 +    logger.setLevel(logging.DEBUG)
   8.169 +    ch = logging.StreamHandler()
   8.170 +    fmt = logging.Formatter("%(levelname)s %(threadName)s %(message)s")
   8.171 +    ch.setFormatter(fmt)
   8.172 +    logger.addHandler(ch)
   8.173 +
   8.174 +    postfix_send_q = Queue.Queue()
   8.175 +
   8.176 +    # Set domain for which we are handling mail
   8.177 +    mail_domain = "nomadic.n4c.eu"
   8.178 +    
   8.179 +    # Check mailboxes dtnin and dtnout  exist and create if not ***TBD***
   8.180 +    nomadic_domain = pypop_maildir.MaildirDirdbmDomain("/tmp")
   8.181 +    if not nomadic_domain.userDirectory("dtnin"):
   8.182 +        nomadic_domain.addUser("dtnin", "password")
   8.183 +    dtnin_mailbox  = pypop_maildir.MaildirMailbox(nomadic_domain.userDirectory("dtnin"),
   8.184 +                                                  "dtnin")
   8.185 +    server = PostfixMailinHandler(mail_domain, dtnin_mailbox,
   8.186 +                                  postfix_send_q, logger)
   8.187 +    server.setDaemon(True)
   8.188 +    server.start()
   8.189 +    time.sleep(0.1)
   8.190 +    logger.info("Postfix mail sender running in thread: %s" % server.getName())
   8.191 +
   8.192 +    evt = msg_send_evt(MSG_SMTP,
   8.193 +                       "/home/elwynd/Python/pypop/msg3",
   8.194 +                       MSG_NEW,
   8.195 +                       ["elwynd@mightyatom.foly.org.uk"])
   8.196 +    postfix_send_q.put_nowait(evt)
   8.197 +
   8.198 +    time.sleep(1)
   8.199 +
   8.200 +    evt = msg_send_evt(MSG_END,
   8.201 +                       "",
   8.202 +                       MSG_NEW,
   8.203 +                       "")
   8.204 +    postfix_send_q.put_nowait(evt)
   8.205 +
   8.206 +    # ..and wait for them to die...
   8.207 +    time.sleep(2)                                    
   8.208 +
   8.209 +    
   8.210 +
   8.211 +    
   8.212 +    
   8.213 +
   8.214 +    
     9.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     9.2 +++ b/gateway/dtn/python/dp_pfmailout.py	Tue Mar 16 13:36:42 2010 +0000
     9.3 @@ -0,0 +1,224 @@
     9.4 +#! /usr/bin/python
     9.5 +# PyMail DTN Nomadic Mail System
     9.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
     9.7 +#
     9.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
     9.9 +#    you may not use this file except in compliance with the License.
    9.10 +#    You may obtain a copy of the License at
    9.11 +# 
    9.12 +#        http://www.apache.org/licenses/LICENSE-2.0
    9.13 +# 
    9.14 +#    Unless required by applicable law or agreed to in writing, software
    9.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
    9.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    9.17 +#    See the License for the specific language governing permissions and
    9.18 +#    limitations under the License.
    9.19 +#
    9.20 +
    9.21 +
    9.22 +"""
    9.23 +DTN-Postfix interface
    9.24 +
    9.25 +Handlers for messages sent from Postfix being relayed through DTN
    9.26 +
    9.27 +The script run by the pipe daemon makes a local  TCP connection to the
    9.28 +PostfixMailoutServer (subclassed from SoCketServer's threaded TCP Server)
    9.29 +Each new mail from Postfix creates a new thread to receive the mail and
    9.30 +dump it into a file in the dtnout maildir.
    9.31 +
    9.32 +The 'protocol' expects a single RFC822 message per PostfixMailoutHandler
    9.33 +invocation with a Delivered-To header on the front. This will be preserved
    9.34 +in the file. The address is used to create the detination EID later.
    9.35 +
    9.36 +The message is stored in a maildir file  - the lines are terminated with \n
    9.37 +
    9.38 +Revision History
    9.39 +================
    9.40 +Version   Date       Author         Notes
    9.41 +0.1       14/03/2010 Elwyn Davies   License statement added.
    9.42 +0.0	  24/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
    9.43 +"""
    9.44 +
    9.45 +import os
    9.46 +import socket
    9.47 +from SocketServer2_6 import *
    9.48 +import threading
    9.49 +import logging
    9.50 +import Queue
    9.51 +
    9.52 +import pypop_maildir
    9.53 +import dp_msg_send_evt
    9.54 +
    9.55 +class PostfixMailoutHandler(StreamRequestHandler):
    9.56 +
    9.57 +    lineseps = "\r\n"
    9.58 +    del_to = "Delivered-To: "
    9.59 +    del_to_len = len(del_to)
    9.60 +
    9.61 +    def end_run(self):
    9.62 +        self.request.close()
    9.63 +
    9.64 +    def handle(self):
    9.65 +        self.mailout_thread = threading.currentThread()
    9.66 +        self.mailout_thread.setName("Mailout receiver - %d" %
    9.67 +                                    self.server.next_server_num)
    9.68 +        self.server.next_server_num += 1
    9.69 +
    9.70 +        # Tell listener we are running
    9.71 +        self.server.add_thread(self)
    9.72 +        
    9.73 +        # Logging functions
    9.74 +        self.loginfo = self.server.logger.info
    9.75 +        self.logdebug = self.server.logger.debug
    9.76 +        self.logwarn = self.server.logger.warn
    9.77 +        self.logerror = self.server.logger.error
    9.78 +
    9.79 +        self.loginfo("New mailout connection from %s" % self.client_address[0])
    9.80 +
    9.81 +        # The first line should start "Delivered-To: "
    9.82 +        # The rest of the line is the target destination address
    9.83 +        # We use this to determine the EID that is used to send
    9.84 +        # this email across the DTN
    9.85 +        data = self.rfile.readline().rstrip(self.lineseps)
    9.86 +        if not data.startswith(self.del_to):
    9.87 +            # Protocol error
    9.88 +            self.logerror("Message does not start with 'Delivered-To:'")
    9.89 +            return
    9.90 +        destn = data[self.del_to_len:].strip()
    9.91 +        self.loginfo("Receiving message for delivery to %s" % destn )
    9.92 +
    9.93 +        # Initialize message destined for 'dest' in given mailbox
    9.94 +        msg = self.server.mailbox.newMessage(destn)
    9.95 +        self.logdebug("ready to receive message")
    9.96 +
    9.97 +        try:
    9.98 +            while True:
    9.99 +                data = self.rfile.readline()
   9.100 +                if data == "":
   9.101 +                    break
   9.102 +		data = data.rstrip(self.lineseps)
   9.103 +                self.logdebug(data)
   9.104 +                msg.lineReceived(data)
   9.105 +        except:
   9.106 +            msg.connectionLost()
   9.107 +            self.request.close()
   9.108 +            return
   9.109 +        
   9.110 +        # Finalize message and put it where it belongs
   9.111 +        self.logdebug("Finishing message")
   9.112 +        if msg.eomReceived():
   9.113 +            fn = msg.getFinalName()
   9.114 +
   9.115 +            # Queue the message for sending by DTN
   9.116 +            self.loginfo("Message to be sent by DTN stored as %s" % fn )
   9.117 +            try:
   9.118 +                evt = dp_msg_send_evt.msg_send_evt(dp_msg_send_evt.MSG_DTN,
   9.119 +                                                   fn,
   9.120 +                                                   dp_msg_send_evt.MSG_NEW,
   9.121 +                                                   [destn])
   9.122 +                self.server.send_queue.put_nowait(evt)
   9.123 +            except Queue.Full:
   9.124 +                # The periodic thread will pick up the new message later
   9.125 +                self.logdebug("Send queue was full")
   9.126 +                pass
   9.127 +        self.loginfo("Mailout receiver finishing")
   9.128 +        return
   9.129 +        
   9.130 +
   9.131 +class PostfixMailoutServer(ThreadingMixIn, TCPServer):
   9.132 +    def __init__(self, addr, handler, mailbox, send_queue, logger):
   9.133 +        # These are used  by individual requests
   9.134 +        # accessed via self.server in the handle function
   9.135 +        self.mailbox = mailbox
   9.136 +        self.send_queue = send_queue
   9.137 +        self.logger = logger
   9.138 +        
   9.139 +        self.running_threads = set()
   9.140 +        self.next_server_num = 1
   9.141 +
   9.142 +        # Setup to produce a daemon thread for each incoming request
   9.143 +        # and be able to reuse address
   9.144 +        TCPServer.__init__(self, addr, handler,
   9.145 +                           bind_and_activate=False)
   9.146 +        self.allow_reuse_address = True
   9.147 +        self.server_bind()
   9.148 +        self.server_activate()
   9.149 +                         
   9.150 +        self.daemon_threads = True
   9.151 +
   9.152 +    def add_thread(self, thread):
   9.153 +        self.logger.debug("New thread added")
   9.154 +        self.running_threads.add(thread)
   9.155 +
   9.156 +    def remove_thread(self, thread):
   9.157 +        if thread in self.running_threads:
   9.158 +            self.running_threads.remove(thread)
   9.159 +
   9.160 +    def end_run(self):
   9.161 +        for thread in self.running_threads:
   9.162 +            if thread.mailout_thread.isAlive():
   9.163 +                thread.request.close()
   9.164 +        del self.running_threads
   9.165 +        self.shutdown()
   9.166 +
   9.167 +def postfix_mailout_server(mailbox, send_queue, logger):
   9.168 +    HOST, PORT = "localhost", 2111
   9.169 +    return PostfixMailoutServer((HOST, PORT), PostfixMailoutHandler,
   9.170 +                                mailbox, send_queue, logger)
   9.171 +
   9.172 +
   9.173 +
   9.174 +if __name__ == "__main__":
   9.175 +    import time
   9.176 +    logger = logging.getLogger("test")
   9.177 +    logger.setLevel(logging.DEBUG)
   9.178 +    ch = logging.StreamHandler()
   9.179 +    fmt = logging.Formatter("%(levelname)s %(threadName)s %(message)s")
   9.180 +    ch.setFormatter(fmt)
   9.181 +    logger.addHandler(ch)
   9.182 +
   9.183 +    nomadic_domain = "nomadic.n4c.eu"
   9.184 +    tmp_domain = pypop_maildir.MaildirDirdbmDomain("/tmp")
   9.185 +    if not tmp_domain.userDirectory("dtnout"):
   9.186 +        tmp_domain.addUser("dtnout", "password")
   9.187 +    dtnout_mailbox = pypop_maildir.MaildirMailbox(tmp_domain.userDirectory("dtnout"),
   9.188 +                                                 "dtnout")
   9.189 +    
   9.190 +    send_q = Queue.Queue()
   9.191 +                          
   9.192 +    def client(ip, port, message):
   9.193 +        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   9.194 +        sock.connect((ip, port))
   9.195 +        sock.send(message)
   9.196 +        #response = sock.recv(1024)
   9.197 +        #print "Received: %s" % response
   9.198 +        sock.close()
   9.199 +
   9.200 +    # Port 0 means to select an arbitrary unused port
   9.201 +    HOST, PORT = "localhost", 0
   9.202 +
   9.203 +    server = PostfixMailoutServer((HOST, PORT), PostfixMailoutHandler,
   9.204 +                                  dtnout_mailbox, send_q, logger)
   9.205 +    ip, port = server.server_address
   9.206 +
   9.207 +    # Start a thread with the server -- that thread will then start one
   9.208 +    # more thread for each request
   9.209 +    server_thread = threading.Thread(target=server.serve_forever,
   9.210 +                                     name="Mailout Listener")
   9.211 +    # Exit the server thread when the main thread terminates
   9.212 +    server_thread.setDaemon(True)
   9.213 +    server_thread.start()
   9.214 +    logger.info("Server loop running in thread:%s" % server_thread.getName())
   9.215 +
   9.216 +    client(ip, port, "Delivered-To: a@b.c\r\nmessage\r\n")
   9.217 +    #client(ip, port, "Delivered-To: d@b.c\r\nmessage\r\nHello World 2\r\n")
   9.218 +    #client(ip, port, "Delivered-To: ae@b.c\r\nmessage\r\nHello World 3\r\n")
   9.219 +                
   9.220 +    time.sleep(1)
   9.221 +    while not send_q.empty():
   9.222 +        evt = send_q.get()
   9.223 +        logger.info("Msg to %s" % evt.destination())
   9.224 +
   9.225 +    time.sleep(1)
   9.226 +
   9.227 +    server.end_run()
    10.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    10.2 +++ b/gateway/dtn/python/pypop_maildir.py	Tue Mar 16 13:36:42 2010 +0000
    10.3 @@ -0,0 +1,764 @@
    10.4 +#! /usr/bin/python
    10.5 +# PyMail DTN Nomadic Mail System
    10.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    10.7 +#
    10.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    10.9 +#    you may not use this file except in compliance with the License.
   10.10 +#    You may obtain a copy of the License at
   10.11 +# 
   10.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   10.13 +# 
   10.14 +#    Unless required by applicable law or agreed to in writing, software
   10.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   10.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   10.17 +#    See the License for the specific language governing permissions and
   10.18 +#    limitations under the License.
   10.19 +#
   10.20 +
   10.21 +
   10.22 +"""
   10.23 +Maildir-style mailbox support
   10.24 +
   10.25 +Intended ultimately for use on Symbian-based mobile pnones in conjunction
   10.26 +with DTN package.
   10.27 +
   10.28 +This is intended to be a very lightweight system primaruily accessed only
   10.29 +on the local machine.   We need a system for storing mail messages to be received
   10.30 +from DTN so that they can be accessed by the standard phone messaging system.
   10.31 +
   10.32 +Standard email on a mobile phone doesn't readily handle bulk storage of emails
   10.33 +(as opposed to text messages etc).  The intention is that these mailboxes are
   10.34 +accessed by a lightweight POP3 protocol and is adapted as such.
   10.35 +
   10.36 +Mailboxes are associated with:
   10.37 +- a (mail) domain
   10.38 +- a directory structure
   10.39 +A domain has a root directory which contains:
   10.40 +- a file called 'passwd' which hold the passwords for all the users
   10.41 +- a directory for each user named by their username (avatar id)
   10.42 +Each user directory contains the usual set of maildir subdirectories
   10.43 +tmp, new, cur and .trash.  The maildir operates by means of the 'standard'
   10.44 +maildir semantics.
   10.45 +
   10.46 +When the system is active, a user's mailbox in the domain is represented by
   10.47 +an 'avatar' which is a MaildirMailbox object instance.  The domain maintains
   10.48 +one instance for each active user.  It is intended that there should only be
   10.49 +one client at a time for each user, but the system should work OK with multiple
   10.50 +clients subject to some thought about thread safeness.  However, the nature
   10.51 +of a mobile phone makes this an unlikely situation and we won't worry too hard
   10.52 +about it at the moment.
   10.53 +
   10.54 +The domain supports authentication via username/password (plain text) and APOP
   10.55 +style hashed password.  No great effort (actually no effort at all)is made to
   10.56 +keep the passwords securely since the intention is that the maildir will live
   10.57 +on the phone where it is accessed and the files can be read by anybody who is
   10.58 +able to access the phone memory.  Usernames are desirable so that a phone can
   10.59 +be used for multiple incoming mail addresses, but a phone is inherently a single
   10.60 +user device but normally the communication between mail client and mailbox will
   10.61 +be over a channel within the phone so that the user name and password are never
   10.62 +visible outside the phone.  On the other hand, one might have to give a bit more
   10.63 +thought to this if the system was used to copy mail off the phone via a POP3
   10.64 +mail client on another device.
   10.65 +
   10.66 +
   10.67 +Revision History
   10.68 +================
   10.69 +Version   Date       Author         Notes
   10.70 +0.1       14/03/2010 Elwyn Davies   License statement added.
   10.71 +0.0       01/06/2009 Elwyn Davies   Initial version based on ideas from the
   10.72 +                                    Twisted Python implementation.
   10.73 +"""
   10.74 +
   10.75 +import os
   10.76 +import stat
   10.77 +import socket
   10.78 +import time
   10.79 +import hashlib
   10.80 +
   10.81 +try:
   10.82 +    import cStringIO as StringIO
   10.83 +except ImportError:
   10.84 +    import StringIO
   10.85 +
   10.86 +try:
   10.87 +    import e32dbm as anydbm
   10.88 +except ImportError:
   10.89 +    import anydbm
   10.90 +
   10.91 +class NotImplementedError(Exception):
   10.92 +    pass
   10.93 +
   10.94 +class UnauthorizedLogin(Exception):
   10.95 +    pass
   10.96 +
   10.97 +class _MaildirNameGenerator:
   10.98 +    """
   10.99 +    Utility class to generate a unique maildir name
  10.100 +
  10.101 +    """
  10.102 +    n = 0
  10.103 +    p = os.getpid()
  10.104 +    hn = socket.getfqdn()
  10.105 +
  10.106 +    def __init__(self):
  10.107 +        return
  10.108 +
  10.109 +    def generate(self):
  10.110 +        """
  10.111 +        Return a string which is intended to unique across all calls to this
  10.112 +        function (across all processes, reboots, etc).
  10.113 +
  10.114 +        Strings returned by earlier calls to this method will compare less
  10.115 +        than strings returned by later calls as long as the clock provided
  10.116 +        doesn't go backwards.
  10.117 +        """
  10.118 +        self.n = self.n + 1
  10.119 +        t = time.time()
  10.120 +        seconds = int(t)
  10.121 +        microseconds = '%07d' % (int((t - seconds) * 10e6),)
  10.122 +        return '%s.M%sP%sQ%s.%s' % (str(seconds), microseconds,
  10.123 +                                    self.p, self.n, self.hn)
  10.124 +
  10.125 +_generateMaildirName = _MaildirNameGenerator().generate
  10.126 +
  10.127 +def initializeMaildir(dir):
  10.128 +    if not os.path.isdir(dir):
  10.129 +        os.mkdir(dir, 0700)
  10.130 +        for subdir in ['new', 'cur', 'tmp', '.Trash']:
  10.131 +            os.mkdir(os.path.join(dir, subdir), 0700)
  10.132 +        for subdir in ['new', 'cur', 'tmp']:
  10.133 +            os.mkdir(os.path.join(dir, '.Trash', subdir), 0700)
  10.134 +        # touch
  10.135 +        open(os.path.join(dir, '.Trash', 'maildirfolder'), 'w').close()
  10.136 +
  10.137 +class MaildirMessage:
  10.138 +    size = None
  10.139 +    UNIXFROM = "Fron "
  10.140 +    UNIXFROM_LEN = len(UNIXFROM)
  10.141 +    X_DTN_HDR_NAME = "X-DTN-rcpts"
  10.142 +
  10.143 +    def __init__(self, address, fp, name, finalName):
  10.144 +        self.fp = fp
  10.145 +        self.name = name
  10.146 +        self.address = address
  10.147 +        self.finalName = finalName
  10.148 +        self.first_line = True
  10.149 +        self.size = 0
  10.150 +
  10.151 +    def lineReceived(self, line):
  10.152 +        # Add a X-DTN-rcpts header after a possible From envelope header
  10.153 +        # which must be the first line received
  10.154 +        if self.first_line:
  10.155 +            dtn_header = "%s: %s\n" % (self.X_DTN_HDR_NAME, self.address)
  10.156 +            if line[:self.UNIXFROM_LEN]:
  10.157 +                self.fp.write(line + '\n')
  10.158 +                self.fp.write(dtn_header)
  10.159 +            else:
  10.160 +                self.fp.write(dtn_header)
  10.161 +                self.fp.write(line + '\n')
  10.162 +            self.size += len(dtn_header)
  10.163 +            self.first_line = False
  10.164 +        else:
  10.165 +            self.fp.write(line + '\n')
  10.166 +        self.size += len(line)+1
  10.167 +
  10.168 +    def eomReceived(self):
  10.169 +        self.finalName = self.finalName+',S=%d' % self.size
  10.170 +        self.fp.close()
  10.171 +        try:
  10.172 +            os.rename(self.name, self.finalName)
  10.173 +            return True
  10.174 +        except:
  10.175 +            os.remove(self.name)
  10.176 +            return False
  10.177 +        
  10.178 +    def connectionLost(self):
  10.179 +        self.fp.close()
  10.180 +        os.remove(self.name)
  10.181 +
  10.182 +    def getFinalName(self):
  10.183 +        return self.finalName
  10.184 +        
  10.185 +class AbstractMaildirDomain:
  10.186 +    """Abstract maildir-backed domain.
  10.187 +    """
  10.188 +    root = None
  10.189 +    _credcheckers = None
  10.190 +
  10.191 +    def __init__(self, root):
  10.192 +        """Initialize.
  10.193 +        """
  10.194 +        self.root = root
  10.195 +
  10.196 +    def userDirectory(self, user):
  10.197 +        """Get the maildir directory for a given user
  10.198 +
  10.199 +        Override to specify where to save mails for users.
  10.200 +        Return None for non-existing users.
  10.201 +        """
  10.202 +        return None
  10.203 +
  10.204 +    ##
  10.205 +    ## IDomain
  10.206 +    ##
  10.207 +    def exists(self, user):
  10.208 +        """Check for existence of user in the domain
  10.209 +        """
  10.210 +        if self.userDirectory(user.dest.local) is not None:
  10.211 +            return lambda: self.startMessage(user)
  10.212 +        else:
  10.213 +            raise pypop_smtp.SMTPBadRcpt(user)
  10.214 +
  10.215 +    def startMessage(self, destn):
  10.216 +        """
  10.217 +        Save a message for a given user.
  10.218 +        """
  10.219 +        if isinstance(destn, str):
  10.220 +            if destn.find("@") == (-1):
  10.221 +                name = destn
  10.222 +                domain = "localhost"
  10.223 +            else:
  10.224 +                name, domain = destn.split('@', 1)
  10.225 +        else:
  10.226 +            name, domain = destn.dest.local, destn.dest.domain
  10.227 +
  10.228 +        dirname = self.userDirectory(name)
  10.229 +        fname = _generateMaildirName()
  10.230 +        filename = os.path.join(dirname, 'tmp', fname)
  10.231 +        fp = open(filename, 'w')
  10.232 +        return MaildirMessage('%s@%s' % (name, domain), fp, filename,
  10.233 +                              os.path.join(dirname, 'new', fname))
  10.234 +
  10.235 +    def willRelay(self, user, protocol):
  10.236 +        return False
  10.237 +
  10.238 +    def addUser(self, user, password):
  10.239 +        raise NotImplementedError
  10.240 +
  10.241 +    def getCredentialsCheckers(self):
  10.242 +        raise NotImplementedError
  10.243 +    ##
  10.244 +    ## end of IDomain
  10.245 +    ##
  10.246 +
  10.247 +
  10.248 +class MaildirMailbox:
  10.249 +    """Implement the POP3 mailbox semantics for a Maildir mailbox
  10.250 +    
  10.251 +    @type loginDelay: C{int}
  10.252 +    @ivar loginDelay: The number of seconds between allowed logins for the
  10.253 +    user associated with this mailbox.  None
  10.254 +
  10.255 +    @type messageExpiration: C{int}
  10.256 +    @ivar messageExpiration: The number of days messages in this mailbox will
  10.257 +    remain on the server before being deleted.
  10.258 +    """
  10.259 +    def __init__(self, path, owner):
  10.260 +        """Initialize with name of the Maildir mailbox
  10.261 +        """
  10.262 +        self.path = path
  10.263 +        self.owner = owner
  10.264 +        self.deleted = {}
  10.265 +        self.list = []
  10.266 +        initializeMaildir(path)
  10.267 +        self.makeMessageList()
  10.268 +
  10.269 +    def makeMessageList(self):
  10.270 +        self.list = []
  10.271 +        for name in ('cur', 'new'):
  10.272 +            for file in os.listdir(os.path.join(self.path, name)):
  10.273 +                self.list.append((file, os.path.join(self.path, name, file)))
  10.274 +        self.list.sort()
  10.275 +        self.list = [e[1] for e in self.list]
  10.276 +
  10.277 +    def listMessages(self, i=None):
  10.278 +        """Retrieve the size of one or more messages.
  10.279 +
  10.280 +        @type index: C{int} or C{None}
  10.281 +        @param index: The number of the message for which to retrieve the
  10.282 +        size (starting at 0), or None to retrieve the size of all messages.
  10.283 +
  10.284 +        @rtype: C{int} or any iterable of C{int} or a L{Deferred} which fires
  10.285 +        with one of these.
  10.286 +
  10.287 +        @return: The number of octets in the specified message, or an iterable
  10.288 +        of integers representing the number of octets in all the messages.  Any
  10.289 +        value which would have referred to a deleted message should be set to 0.
  10.290 +
  10.291 +        @raise ValueError: if C{index} is greater than the index of any message
  10.292 +        in the mailbox.
  10.293 +        
  10.294 +        Return a list of lengths of all files in new/ and cur/
  10.295 +        """
  10.296 +        if i is None:
  10.297 +            ret = []
  10.298 +            for mess in self.list:
  10.299 +                if mess:
  10.300 +                    ret.append(os.stat(mess)[stat.ST_SIZE])
  10.301 +                else:
  10.302 +                    ret.append(0)
  10.303 +            return ret
  10.304 +        else:
  10.305 +            if (i < 0) or (i >= len(self.list)):
  10.306 +                raise ValueError
  10.307 +            else:
  10.308 +                return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0
  10.309 +
  10.310 +    def getMessage(self, i):
  10.311 +        """Retrieve a file-like object for a particular message.
  10.312 +
  10.313 +        @type index: C{int}
  10.314 +        @param index: The number of the message to retrieve
  10.315 +
  10.316 +        @rtype: A file-like object
  10.317 +        @return: A file containing the message data with lines delimited by
  10.318 +        C{\\n}.
  10.319 +        
  10.320 +        Return an open file-pointer to a message
  10.321 +        """
  10.322 +        if (i < 0) or (i >= len(self.list)) or (self.list[i] == 0):
  10.323 +            raise ValueError
  10.324 +        return open(self.list[i])
  10.325 +
  10.326 +    def getUidl(self, i):
  10.327 +        """Get a unique identifier for a particular message.
  10.328 +
  10.329 +        @type index: C{int}
  10.330 +        @param index: The number of the message for which to retrieve a UIDL
  10.331 +
  10.332 +        @rtype: C{str}
  10.333 +        @return: A string of printable characters uniquely identifying for all
  10.334 +        time the specified message.
  10.335 +
  10.336 +        @raise ValueError: if C{index} is greater than the index of any message
  10.337 +        in the mailbox.
  10.338 +        
  10.339 +        Return a unique identifier for a message
  10.340 +
  10.341 +        This is done using the basename of the filename.
  10.342 +        It is globally unique because this is how Maildirs are designed.
  10.343 +        """
  10.344 +        # Returning the actual filename is a mistake.  Hash it.
  10.345 +        if (i < 0) or (i >= len(self.list)) or (self.list[i] == 0):
  10.346 +            raise ValueError
  10.347 +        base = os.path.basename(self.list[i])
  10.348 +        m = hashlib.md5()
  10.349 +        m.update(base)
  10.350 +        return m.hexdigest()
  10.351 +
  10.352 +    def deleteMessage(self, i):
  10.353 +        """Delete a particular message.
  10.354 +
  10.355 +        This must not change the number of messages in this mailbox.  Further
  10.356 +        requests for the size of deleted messages should return 0.  Further
  10.357 +        requests for the message itself may raise an exception.
  10.358 +
  10.359 +        @type index: C{int}
  10.360 +        @param index: The number of the message to delete.
  10.361 +        
  10.362 +        Delete a message
  10.363 +
  10.364 +        This only moves a message to the .Trash/ subfolder,
  10.365 +        so it can be undeleted by an administrator.
  10.366 +        """
  10.367 +        if (i < 0) or (i >= len(self.list)) or (self.list[i] == 0):
  10.368 +            raise ValueError
  10.369 +        
  10.370 +        trashFile = os.path.join(
  10.371 +            self.path, '.Trash', 'cur', os.path.basename(self.list[i])
  10.372 +        )
  10.373 +        os.rename(self.list[i], trashFile)
  10.374 +        self.deleted[self.list[i]] = (i, trashFile)
  10.375 +        self.list[i] = 0
  10.376 +
  10.377 +    def undeleteMessages(self):
  10.378 +        """
  10.379 +        Undelete any messages which have been marked for deletion since the
  10.380 +        most recent L{sync} call.
  10.381 +
  10.382 +        Any message which can be undeleted should be returned to its
  10.383 +        original position in the message sequence and retain its original
  10.384 +        UID.
  10.385 +        
  10.386 +        Undelete any deleted messages it is possible to undelete
  10.387 +
  10.388 +        This moves any messages from .Trash/ subfolder back to their
  10.389 +        original position, and empties out the deleted dictionary.
  10.390 +        """
  10.391 +        for (real, trash) in self.deleted.items():
  10.392 +            try:
  10.393 +                os.rename(trash[1], real)
  10.394 +            except OSError, (err, estr):
  10.395 +                import errno
  10.396 +                # If the file has been deleted from disk, oh well!
  10.397 +                if err != errno.ENOENT:
  10.398 +                    raise
  10.399 +                # This is a pass
  10.400 +            else:
  10.401 +                try:
  10.402 +                    self.list[trash[0]] = real
  10.403 +                except ValueError:
  10.404 +                    self.list.append(real)
  10.405 +        self.deleted.clear()
  10.406 +
  10.407 +    def moveNewCur(self, i=None):
  10.408 +        """
  10.409 +        Move one or all messages in new to cur (not including any that
  10.410 +        might have been deleted).
  10.411 +        """
  10.412 +        newdir = os.path.join(self.path, "new")
  10.413 +        if i == None:
  10.414 +            i = 0
  10.415 +            for m in self.list:
  10.416 +                if m != 0:
  10.417 +                    (pathname, base) = os.path.split(m)
  10.418 +                    if pathname == newdir:
  10.419 +                        n = os.path.join(self.path, "cur", base)
  10.420 +                        os.rename(m, n)
  10.421 +                        self.list[i] = n
  10.422 +                i +=1
  10.423 +        else:
  10.424 +            if (i < 0) or (i >= len(self.list)) or (self.list[i] == 0):
  10.425 +                raise ValueError
  10.426 +            
  10.427 +            m = self.list[i]
  10.428 +            if m != 0:
  10.429 +                (pathname, base) = os.path.split(m)
  10.430 +                if pathname == newdir:
  10.431 +                    n = os.path.join(self.path, "cur", base)
  10.432 +                    os.rename(m, n)
  10.433 +                    self.list[i] = n
  10.434 +    def cleanTrash(self):
  10.435 +        """
  10.436 +        Remove any messages in deleted and compress the list.
  10.437 +        When using POP3 to access the mailbox this will be done
  10.438 +        in the UPDATE state when QUIT is called.  The list is kept in step
  10.439 +        so that the instance can be used with a new session.
  10.440 +        """
  10.441 +        for (real, trash) in self.deleted.items():
  10.442 +            try:
  10.443 +                os.remove(trash[1])
  10.444 +            except:
  10.445 +                pass
  10.446 +            else:
  10.447 +                del self.list[trash[0]]
  10.448 +
  10.449 +    def newMessage(self, destn):
  10.450 +        fname = _generateMaildirName()
  10.451 +        filename = os.path.join(self.path, 'tmp', fname)
  10.452 +        fp = open(filename, 'w')
  10.453 +        return MaildirMessage(destn, fp, filename,
  10.454 +                              os.path.join(self.path, 'new', fname))
  10.455 +
  10.456 +    def newMessageFile(self, temp_fname):
  10.457 +        fname = _generateMaildirName()
  10.458 +        try:
  10.459 +            size = os.stat(temp_fname)[stat.ST_SIZE]
  10.460 +            new_fn = os.path.join(self.path, 'new', "%s,S=%d" % (fname, size) )
  10.461 +            os.rename(temp_fname, new_fn)
  10.462 +        except:
  10.463 +            return None
  10.464 +        self.list.append(new_fn)
  10.465 +        return new_fn
  10.466 +        
  10.467 +        
  10.468 +    def appendMessage(self, msg):
  10.469 +        """
  10.470 +        Appends a message into the mailbox.
  10.471 +        called when a new message has been installed dynamically
  10.472 +        to add to the list.
  10.473 +
  10.474 +        This is an optimization to avoid rereading the files. 
  10.475 +        """
  10.476 +        self.list.append(msg.getFinalName())
  10.477 +
  10.478 +    def sync(self):
  10.479 +        """
  10.480 +        No specific requirements.
  10.481 +        """
  10.482 +        pass
  10.483 +
  10.484 +##
  10.485 +## Authentication
  10.486 +##
  10.487 +class CredentialsChecker:
  10.488 +    def checkPassword(self, password):
  10.489 +        raise NotImplementedError
  10.490 +
  10.491 +class UsernameHashedPassword(CredentialsChecker):
  10.492 +
  10.493 +    def __init__(self, username, hashed):
  10.494 +        self.username = username
  10.495 +        self.hashed = hashed
  10.496 +
  10.497 +    def checkPassword(self, password):
  10.498 +        return self.hashed == password
  10.499 +
  10.500 +class UsernamePassword(CredentialsChecker):
  10.501 +
  10.502 +    def __init__(self, username, password):
  10.503 +        self.username = username
  10.504 +        self.password = password
  10.505 +
  10.506 +    def checkPassword(self, password):
  10.507 +        return self.password == password
  10.508 +
  10.509 +class APOPCredentials(CredentialsChecker):
  10.510 +
  10.511 +    def __init__(self, magic, username, digest):
  10.512 +        self.magic = magic
  10.513 +        self.username = username
  10.514 +        self.digest = digest
  10.515 +
  10.516 +    def checkPassword(self, password):
  10.517 +        seed = self.magic + password
  10.518 +        m = hashlib.md5()
  10.519 +        m.update(seed)
  10.520 +        myDigest = m.hexdigest()
  10.521 +        return myDigest == self.digest
  10.522 +
  10.523 +class MaildirDirdbmDomain(AbstractMaildirDomain):
  10.524 +    """A Maildir Domain where membership is checked by a dirdbm file
  10.525 +    """
  10.526 +
  10.527 +    def __init__(self, root=None, postmaster=0):
  10.528 +        """Initialize
  10.529 +
  10.530 +        The first argument is where the Domain directory is rooted.
  10.531 +        The second is whether non-existing addresses are simply
  10.532 +        forwarded to postmaster instead of outright bounce
  10.533 +
  10.534 +        The directory structure of a MailddirDirdbmDomain is:
  10.535 +
  10.536 +        /passwd <-- a dbm file
  10.537 +        /USER/{cur,new,del} <-- each user has these three directories
  10.538 +
  10.539 +        A maximum of one 'avatar' (MaildirMailbox) object is maintained
  10.540 +        for each user of this domain.  The avatars are created when the
  10.541 +        first avatarRequest is made.  At present this probably not thread safe.
  10.542 +        """
  10.543 +        AbstractMaildirDomain.__init__(self, root)
  10.544 +        dbm = os.path.join(root, 'passwd')
  10.545 +        if not os.path.exists(root):
  10.546 +            os.makedirs(root)
  10.547 +        self.dbm = anydbm.open(dbm, "c")
  10.548 +        self.postmaster = postmaster
  10.549 +        self.avatarDict = {}
  10.550 +
  10.551 +    def userDirectory(self, name):
  10.552 +        """Get the directory for a user
  10.553 +
  10.554 +        If the user exists in the dirdbm file, return the directory
  10.555 +        os.path.join(root, name), creating it if necessary.
  10.556 +        Otherwise, returns postmaster's mailbox instead if bounces
  10.557 +        go to postmaster, otherwise return None
  10.558 +        """
  10.559 +        if not self.dbm.has_key(name):
  10.560 +            if not self.postmaster:
  10.561 +                return None
  10.562 +            name = 'postmaster'
  10.563 +        dir = os.path.join(self.root, name)
  10.564 +        if not os.path.exists(dir):
  10.565 +            initializeMaildir(dir)
  10.566 +        return dir
  10.567 +    def close(self):
  10.568 +        self.dbm.close()
  10.569 +
  10.570 +    ##
  10.571 +    ## IDomain
  10.572 +    ##
  10.573 +    def addUser(self, user, password):
  10.574 +        self.dbm[user] = password
  10.575 +        # Ensure it is initialized
  10.576 +        self.userDirectory(user)
  10.577 +
  10.578 +    def getCredentialsCheckers(self):
  10.579 +        if self._credcheckers is None:
  10.580 +            self._credcheckers = [DirdbmDatabase(self.dbm)]
  10.581 +        return self._credcheckers
  10.582 +
  10.583 +    def buildNewMessage(self, user, feeder):
  10.584 +        """Builds a new message into the mailbox for a given user."""
  10.585 +        # Call start message and then loop on feeder until finished calling
  10.586 +        # add line and then eom_message.  Update the list when finished.
  10.587 +        avatar = self.requestAvatar(user)
  10.588 +        if avatar == None:
  10.589 +            return False
  10.590 +        msg = self.startMessage(user)
  10.591 +        while True:
  10.592 +            try:
  10.593 +                msg.lineReceived(feeder.next())
  10.594 +            except:
  10.595 +                break
  10.596 +        result = msg.eomReceived()
  10.597 +        if result:
  10.598 +            avatar.appendMessage(msg)
  10.599 +        return result
  10.600 +        
  10.601 +        
  10.602 +    ##
  10.603 +    ## IRealm
  10.604 +    ##
  10.605 +    def verifyCredentials(self, credentials):
  10.606 +        for checker in self.getCredentialsCheckers():
  10.607 +            try:
  10.608 +                return checker.requestAvatarId(credentials)
  10.609 +            except UnauthorizedLogin:
  10.610 +                continue
  10.611 +        raise UnauthorizedLogin
  10.612 +    
  10.613 +    def requestAvatar(self, avatarId):
  10.614 +        dirname = self.userDirectory(avatarId)
  10.615 +        if dirname == None:
  10.616 +            return None
  10.617 +        if not self.avatarDict.has_key(avatarId):
  10.618 +            self.avatarDict[avatarId] = MaildirMailbox(dirname, avatarId)
  10.619 +        return self.avatarDict[avatarId]
  10.620 +
  10.621 +class DirdbmDatabase:
  10.622 +
  10.623 +    def __init__(self, dbm):
  10.624 +        self.dirdbm = dbm
  10.625 +
  10.626 +    """
  10.627 +    requestAvatarId expects an instance of an object that inherits from
  10.628 +    CredentialsChecker as its paramter.
  10.629 +    """
  10.630 +
  10.631 +    def requestAvatarId(self, c):
  10.632 +        if c.username in self.dirdbm:
  10.633 +            if c.checkPassword(self.dirdbm[c.username]):
  10.634 +                return c.username
  10.635 +        raise UnauthorizedLogin()
  10.636 +
  10.637 +if __name__ == "__main__":
  10.638 +    # Test code
  10.639 +    class TestFeeder:
  10.640 +        content = ["From: elwynd@example.com",
  10.641 +                   "To: godzilla@mumble.com",
  10.642 +                   "Subject: Test %d",
  10.643 +                   "",
  10.644 +                   "A test message",
  10.645 +                   "Some more text",
  10.646 +                   "Yet more text",
  10.647 +                   "The quick brown fox jumped"]
  10.648 +        def __init__(self):
  10.649 +            self.msg_no = 0
  10.650 +
  10.651 +        def get_line(self):
  10.652 +            self.msg_no += 1
  10.653 +            for l in self.content:
  10.654 +                if l[0:7] == "Subject":
  10.655 +                    yield l % (self.msg_no, )
  10.656 +                else:
  10.657 +                    yield l
  10.658 +            
  10.659 +    #testdomain = MaildirDirdbmDomain("/home/elwynd/maildir")
  10.660 +    testdomain = MaildirDirdbmDomain("/tmp")
  10.661 +    testdomain.addUser("postmaster", "postie_pat")
  10.662 +    testdomain.addUser("elwynd", "password")
  10.663 +
  10.664 +    magic = "abcdefgh1234567"
  10.665 +    m = hashlib.md5()
  10.666 +
  10.667 +    m.update(magic + "password")
  10.668 +    elwynd_digest = m.hexdigest()
  10.669 +    
  10.670 +    cred1 = UsernamePassword("postmaster", "postie_pat")
  10.671 +    cred2 = APOPCredentials(magic, "elwynd", elwynd_digest)
  10.672 +    cred3 = UsernamePassword("postmaster", "not_my_pas")
  10.673 +    cred4 = UsernamePassword("godzilla", "not_my_pas")
  10.674 +    cred5 = APOPCredentials(magic, "elwynd", "0afec56")
  10.675 +    cred6 = APOPCredentials(magic, "mummy", elwynd_digest)
  10.676 +
  10.677 +    n = 0
  10.678 +    for i in [cred1, cred2, cred3, cred4, cred5, cred6]:
  10.679 +        n += 1
  10.680 +        try:
  10.681 +            avatarId = testdomain.verifyCredentials(i)
  10.682 +            print "Successful credentials verification: %d %s\n" % (n, avatarId,)
  10.683 +        except:
  10.684 +            print "Verification failed: %d %s\n" %(n, i.username)
  10.685 +
  10.686 +    f = TestFeeder()
  10.687 +    g = f.get_line()
  10.688 +    while True:
  10.689 +        try:
  10.690 +            print g.next()
  10.691 +        except:
  10.692 +            break
  10.693 +    # see what is in elwynd's mailbox
  10.694 +    avatar = testdomain.requestAvatar("elwynd")
  10.695 +    orig_list = avatar.listMessages()
  10.696 +    l1 = len(orig_list)
  10.697 +    print "List of messages (%d): " % (l1, ), orig_list
  10.698 +
  10.699 +    # Create some more messages for elwynd
  10.700 +    g = f.get_line()
  10.701 +    result = testdomain.buildNewMessage("elwynd", g)
  10.702 +    print "Msg stored: ", result
  10.703 +    g = f.get_line()
  10.704 +    result = testdomain.buildNewMessage("elwynd", g)
  10.705 +    print "Msg stored: ", result
  10.706 +    g = f.get_line()
  10.707 +    result = testdomain.buildNewMessage("elwynd", g)
  10.708 +    print "Msg stored: ", result
  10.709 +    g = f.get_line()
  10.710 +    result = testdomain.buildNewMessage("elwynd", g)
  10.711 +    print "Msg stored: ", result
  10.712 +    new_list = avatar.listMessages()
  10.713 +    l2 = len(new_list)
  10.714 +    print "List of messages (%d): " % (l2, ), new_list
  10.715 +    if l2 == (l1 + 4):
  10.716 +        print "Correct number of messages added"
  10.717 +    else:
  10.718 +        print "New list does not contain expected number of messages"
  10.719 +
  10.720 +    # Test getUidl
  10.721 +    print "Uidl: ", avatar.getUidl(3)
  10.722 +    try:
  10.723 +        res = avatar.getUidl(l2+1)
  10.724 +        print "Error: out of range message id did not raise ValueError"
  10.725 +    except ValueError:
  10.726 +        print "Out of range message ifd raised ValueError correctly"
  10.727 +
  10.728 +    # Test deletion
  10.729 +    avatar.deleteMessage(4)
  10.730 +    avatar.deleteMessage(3)
  10.731 +    new_list = avatar.listMessages()
  10.732 +    l2 = len(new_list)
  10.733 +    print "List of messages after deletion (%d): " % (l2, ), new_list
  10.734 +    avatar.undeleteMessages()
  10.735 +    new_list = avatar.listMessages()
  10.736 +    l2 = len(new_list)
  10.737 +    print "List of messages after undeletion (%d): " % (l2, ), new_list
  10.738 +
  10.739 +    # Test moving new to current
  10.740 +    avatar.moveNewCur(2)
  10.741 +    new_list = avatar.listMessages()
  10.742 +    l2 = len(new_list)
  10.743 +    print "List of messages after moving one message from new to cur (%d): " % (l2, ), new_list
  10.744 +    avatar.moveNewCur()
  10.745 +    new_list = avatar.listMessages()
  10.746 +    l2 = len(new_list)
  10.747 +    print "List of messages after moving all messages from new to cur (%d): " % (l2, ), new_list
  10.748 +
  10.749 +    # Test clean trash
  10.750 +    avatar.deleteMessage(2)
  10.751 +    new_list = avatar.listMessages()
  10.752 +    l2 = len(new_list)
  10.753 +    print "List of messages after deleteing one message (%d): " % (l2, ), new_list
  10.754 +    avatar.cleanTrash()
  10.755 +    new_list = avatar.listMessages()
  10.756 +    l2 = len(new_list)
  10.757 +    print "List of messages after cleaning trash (%d): " % (l2, ), new_list
  10.758 +
  10.759 +    testdomain.close()
  10.760 +    
  10.761 +    
  10.762 +    
  10.763 +    
  10.764 +
  10.765 +    
  10.766 +
  10.767 +
    11.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    11.2 +++ b/gateway/dtn/python/pypop_smtpout.py	Tue Mar 16 13:36:42 2010 +0000
    11.3 @@ -0,0 +1,97 @@
    11.4 +#! /usr/bin/python
    11.5 +# PyMail DTN Nomadic Mail System
    11.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    11.7 +#
    11.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    11.9 +#    you may not use this file except in compliance with the License.
   11.10 +#    You may obtain a copy of the License at
   11.11 +# 
   11.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   11.13 +# 
   11.14 +#    Unless required by applicable law or agreed to in writing, software
   11.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   11.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11.17 +#    See the License for the specific language governing permissions and
   11.18 +#    limitations under the License.
   11.19 +#
   11.20 +
   11.21 +
   11.22 +"""
   11.23 +Postfix pipe client designed to squirrel mail messages in a temporary maildir
   11.24 +and send them out as dtn bundles.
   11.25 +
   11.26 +Intended to take messages for a specific DTN association (at present).
   11.27 +
   11.28 +The messages are passed on across a TCP connection to the main handler.
   11.29 +
   11.30 +The reason for this small complexity is that it makes it simpler to interface
   11.31 +with Postfix and use the capabilities of the pipe daemon.  This has lots of
   11.32 +options that can be usefully used here, such as one mail message per invocation
   11.33 +and adding Delivered To headers and simulating bsmtp capabilities.
   11.34 +
   11.35 +In the event of problems we can return error statuses from sysexits to tell
   11.36 +Postfix what happened.
   11.37 +
   11.38 +Revision History
   11.39 +================
   11.40 +Version   Date       Author         Notes
   11.41 +0.1       14/03/2010 Elwyn Davies   License statement added.
   11.42 +0.0	  25/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
   11.43 +"""
   11.44 +
   11.45 +import sys
   11.46 +import socket
   11.47 +import logging
   11.48 +import logging.config
   11.49 +import logging.handlers
   11.50 +
   11.51 +# Selected values from sysexits.h
   11.52 +# Postfix pipe command understands these and will do the right thing allegedly.
   11.53 +EX_OK = 0
   11.54 +EX_OSERR = 71
   11.55 +EX_IOERR = 74
   11.56 +EX_TEMPFAIL = 75
   11.57 +
   11.58 +# Setup logging
   11.59 +logging.config.fileConfig("/home/dtn/dtn/dtn_pymail_log.conf")
   11.60 +pymail_logger = logging.getLogger("dtn_postfix_pipe")
   11.61 +loginfo = pymail_logger.info
   11.62 +logdebug = pymail_logger.debug
   11.63 +logerror = pymail_logger.error
   11.64 +
   11.65 +loginfo("DTN mail Postfix transport shim running")
   11.66 +
   11.67 +# Open a socket to the outgoing mail handler
   11.68 +HOST, SERVER_PORT, ANY_PORT = "localhost", 2111, 0
   11.69 +try:
   11.70 +    skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   11.71 +except socket.error, msg:
   11.72 +    logerror("Socket creation failed")
   11.73 +    sys.exit(EX_OSERR)
   11.74 +
   11.75 +try:
   11.76 +    skt.bind((HOST, ANY_PORT,))
   11.77 +    skt.connect((HOST, SERVER_PORT))
   11.78 +except socket.error, msg:
   11.79 +    logerror("Transport shim: Couldn't connect")
   11.80 +    skt.close()
   11.81 +    # Indicate we should try again later if can't connect to server process
   11.82 +    sys.exit(EX_TEMPFAIL)
   11.83 +
   11.84 +
   11.85 +# Copy the message to the server
   11.86 +try:
   11.87 +    while (True):
   11.88 +        next_line = sys.stdin.readline()
   11.89 +        logdebug(next_line)
   11.90 +        if next_line == "":
   11.91 +            break
   11.92 +        skt.send(next_line)
   11.93 +except:
   11.94 +    skt.close()
   11.95 +    sys.exit(EX_IOERR)
   11.96 +loginfo("Transport shim: finished")
   11.97 +skt.close()
   11.98 +sys.exit(EX_OK)
   11.99 +
  11.100 +
    12.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    12.2 +++ b/gateway/dtn/start_dtnd.sh	Tue Mar 16 13:36:42 2010 +0000
    12.3 @@ -0,0 +1,19 @@
    12.4 +#! /bin/sh
    12.5 +# PyMail DTN Nomadic Mail System
    12.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    12.7 +#
    12.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    12.9 +#    you may not use this file except in compliance with the License.
   12.10 +#    You may obtain a copy of the License at
   12.11 +# 
   12.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   12.13 +# 
   12.14 +#    Unless required by applicable law or agreed to in writing, software
   12.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   12.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   12.17 +#    See the License for the specific language governing permissions and
   12.18 +#    limitations under the License.
   12.19 +#
   12.20 +
   12.21 +# Script to start DTN2 Bundle daemon for Postfic to DTN Nomadic mail system
   12.22 +dtnd -c /home/dtn/dtn/dtn.conf -l info -d -o /home/dtn/log/dtnd.log
    13.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    13.2 +++ b/gateway/dtn/start_mail_link.sh	Tue Mar 16 13:36:42 2010 +0000
    13.3 @@ -0,0 +1,20 @@
    13.4 +#!/bin/sh
    13.5 +# PyMail DTN Nomadic Mail System
    13.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    13.7 +#
    13.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    13.9 +#    you may not use this file except in compliance with the License.
   13.10 +#    You may obtain a copy of the License at
   13.11 +# 
   13.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   13.13 +# 
   13.14 +#    Unless required by applicable law or agreed to in writing, software
   13.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   13.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   13.17 +#    See the License for the specific language governing permissions and
   13.18 +#    limitations under the License.
   13.19 +#
   13.20 +
   13.21 +# Script to start Postfix to DTN interface program
   13.22 +python /home/dtn/python/dp_main.py &
   13.23 +ps --no-headers -C python -o pid=  >/home/dtn/dp_main.pid
    14.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    14.2 +++ b/gateway/dtn/start_nomadic_mail.sh	Tue Mar 16 13:36:42 2010 +0000
    14.3 @@ -0,0 +1,22 @@
    14.4 +#! /bin/sh
    14.5 +# PyMail DTN Nomadic Mail System
    14.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    14.7 +#
    14.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    14.9 +#    you may not use this file except in compliance with the License.
   14.10 +#    You may obtain a copy of the License at
   14.11 +# 
   14.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   14.13 +# 
   14.14 +#    Unless required by applicable law or agreed to in writing, software
   14.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   14.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   14.17 +#    See the License for the specific language governing permissions and
   14.18 +#    limitations under the License.
   14.19 +#
   14.20 +
   14.21 +# Overall script to start Postfix to DTN Nomadic email system
   14.22 +echo "Starting DTN2 daemon"
   14.23 +/home/dtn/start_dtnd.sh
   14.24 +echo "Starting Python mail redirector"
   14.25 +/home/dtn/start_mail_link.sh
    15.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    15.2 +++ b/gateway/dtn/stop_nomadic_mail.sh	Tue Mar 16 13:36:42 2010 +0000
    15.3 @@ -0,0 +1,26 @@
    15.4 +#! /bin/sh
    15.5 +# PyMail DTN Nomadic Mail System
    15.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    15.7 +#
    15.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    15.9 +#    you may not use this file except in compliance with the License.
   15.10 +#    You may obtain a copy of the License at
   15.11 +# 
   15.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   15.13 +# 
   15.14 +#    Unless required by applicable law or agreed to in writing, software
   15.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   15.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   15.17 +#    See the License for the specific language governing permissions and
   15.18 +#    limitations under the License.
   15.19 +#
   15.20 +
   15.21 +# Script to stop Postfix to DTN Nomadic email system
   15.22 +if [ -f /home/dtn/dp_main.pid ];
   15.23 +then
   15.24 +	echo "Stopping Python mail director in pid " `cat /home/dtn/dp_main.pid`
   15.25 +	kill `cat /home/dtn/dp_main.pid`
   15.26 +	rm /home/dtn/dp_main.pid
   15.27 +fi
   15.28 +echo "Stopping DTN2 daemon"
   15.29 +dtnd-control stop
    16.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    16.2 +++ b/outstation/home/user/dtn/dp_out.awk	Tue Mar 16 13:36:42 2010 +0000
    16.3 @@ -0,0 +1,1 @@
    16.4 +/\/home\/user\/dtn\/python\/dp_outstation.py/{ print $1; }
    17.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    17.2 +++ b/outstation/home/user/dtn/dtn/dtn_outstation_static.conf	Tue Mar 16 13:36:42 2010 +0000
    17.3 @@ -0,0 +1,284 @@
    17.4 +#
    17.5 +# dtn.conf
    17.6 +#
    17.7 +# Configuration file for the nomadic.n4c.eu relay on the PRoPHET
    17.8 +# routed N4C nomadic network. The machine with the relay runs two daemons
    17.9 +# one talking to the statically routed network and the other talking
   17.10 +# to the proxy gateway on the PRoPHET network.
   17.11 +
   17.12 +# The static side uses the standard set of port numbers whereas the
   17.13 +# the PRoPHET side uses a set of non standard ports so that in can 
   17.14 +# coexist in the same machine.  Note that the bundle storage database
   17.15 +# needs to be kept separate between the two daemons.
   17.16 +
   17.17 +# Default configuration file for Internet-connected DTN nodes. The
   17.18 +# daemon uses a tcl interpreter to parse this file, thus any standard
   17.19 +# tcl commands are valid, and all settings are get/set using a single
   17.20 +# 'set' functions as: <module> set <var> <val?>
   17.21 +#
   17.22 +
   17.23 +log /dtnd info "dtnd parsing configuration..."
   17.24 +
   17.25 +########################################
   17.26 +#
   17.27 +# Daemon Console Configuration
   17.28 +#
   17.29 +########################################
   17.30 +
   17.31 +#
   17.32 +# console set stdio [ true | false ]
   17.33 +#
   17.34 +# If set to false, disable the interactive console on stdin/stdout.
   17.35 +# The default is set to true (unless the dtnd process is run as a
   17.36 +# daemon).
   17.37 +#
   17.38 +# console set stdio false
   17.39 +
   17.40 +#
   17.41 +# console set addr <port>
   17.42 +# console set port <port>
   17.43 +#
   17.44 +# Settings for the socket based console protocol.
   17.45 +# (this interprets user commands)
   17.46 +# Default is 5050
   17.47 +#
   17.48 +console set addr 127.0.0.1
   17.49 +console set port 5050
   17.50 +
   17.51 +#
   17.52 +# console set prompt <prompt>
   17.53 +#
   17.54 +# Set the prompt string.  Helps if running multiple dtnd's
   17.55 +#
   17.56 +set shorthostname [lindex [split [info hostname] .] 0]
   17.57 +console set prompt "$shorthostname dtn-prophet% "
   17.58 +
   17.59 +########################################
   17.60 +#
   17.61 +# API Server Configuration
   17.62 +# Default port is 5010
   17.63 +#
   17.64 +########################################
   17.65 +api set local_port 5010
   17.66 +
   17.67 +########################################
   17.68 +#
   17.69 +# Storage Configuration
   17.70 +#
   17.71 +########################################
   17.72 +
   17.73 +#
   17.74 +# storage set type [ berkeleydb | postgres | mysql  | external ]
   17.75 +#
   17.76 +# Set the storage system to be used
   17.77 +#
   17.78 +storage set type berkeleydb
   17.79 +
   17.80 +# the following are for use with external data stores
   17.81 +#
   17.82 +# The server port to connect to (on localhost)
   17.83 +# Note that 62345 has no special significance -- chosen randomly
   17.84 +storage set server_port 62345
   17.85 +
   17.86 +# The external data store schema location, which can be
   17.87 +# found in dtn2/oasys/storage/DS.xsd.
   17.88 +storage set schema /etc/DS.xsd
   17.89 +
   17.90 +
   17.91 +#
   17.92 +# Do a runtime check for the standard locations for the persistent
   17.93 +# storage directory
   17.94 +#
   17.95 +#set dbdir "/media/mmc2/dtn/bundlestore/static"
   17.96 +set dbdir "/home/user/dtn/bundlestore"
   17.97 +if {$dbdir == ""} {
   17.98 +    foreach dir {/var/dtn /var/tmp/dtn} {
   17.99 +        if {[file isdirectory $dir]} {
  17.100 +            set dbdir $dir
  17.101 +            break
  17.102 +        }
  17.103 +    }
  17.104 +}
  17.105 +
  17.106 +if {$dbdir == ""} {
  17.107 +    puts stderr "Must create /var/dtn or /var/tmp/dtn storage directory"
  17.108 +    exit 1
  17.109 +}
  17.110 +
  17.111 +#
  17.112 +# storage set payloaddir <dir>
  17.113 +#
  17.114 +# Set the directory to be used for bundle payload files
  17.115 +#
  17.116 +storage set payloaddir $dbdir/bundles
  17.117 +
  17.118 +#
  17.119 +# storage set dbname <db>
  17.120 +#
  17.121 +# Set the database name (appended with .db as the filename in berkeley
  17.122 +# db, used as-is for SQL variants
  17.123 +#
  17.124 +storage set dbname     DTN
  17.125 +
  17.126 +#
  17.127 +# storage set dbdir    <dir>
  17.128 +#
  17.129 +#
  17.130 +# When using berkeley db, set the directory to be used for the
  17.131 +# database files and the name of the files and error log.
  17.132 +#
  17.133 +storage set dbdir      $dbdir/db
  17.134 +
  17.135 +########################################
  17.136 +#
  17.137 +# Routing configuration
  17.138 +#
  17.139 +########################################
  17.140 +
  17.141 +#
  17.142 +# Set the algorithm used for dtn routing.
  17.143 +#
  17.144 +# route set type [static | flood | neighborhood | linkstate | external]
  17.145 +#
  17.146 +route set type static
  17.147 +
  17.148 +# Set up some routing parameters
  17.149 +# Age out node parameters every 12 hours - 12 * 60 * 60 seconds
  17.150 +#prophet set age_period 43200
  17.151 +
  17.152 +# Decay probabilities by units of a day - kappa is set in milliseconds - 24 * 60 * 60 * 1000
  17.153 +#prophet set kappa 86400000
  17.154 +
  17.155 +#
  17.156 +# route local_eid <eid>
  17.157 +#
  17.158 +# Set the local administrative id of this node. The default just uses
  17.159 +# the internet hostname plus the appended string ".dtn" to make it
  17.160 +# clear that the hostname isn't really used for DNS lookups.
  17.161 +#
  17.162 +# This is *proxy* gateway machine!  The outstations route to here and 
  17.163 +# we relay on to the real gateway on rosebud.
  17.164 +route local_eid "dtn://user3.nomadic.n4c.eu"
  17.165 +
  17.166 +#
  17.167 +# External router specific options
  17.168 +#
  17.169 +# route set server_port 8001
  17.170 +# route set hello_interval 30
  17.171 +# route set schema "/etc/router.xsd"
  17.172 +
  17.173 +########################################
  17.174 +#
  17.175 +# TCP convergence layer configuration
  17.176 +#
  17.177 +########################################
  17.178 +
  17.179 +#
  17.180 +# interface add [name] [CL]
  17.181 +#
  17.182 +# Add an input interface to listen on addr:port for incoming bundles
  17.183 +# from other tcp / udp convergence layers
  17.184 +#
  17.185 +# For IP-based interfaces, interfaces listen on INADDR_ANY port 4556
  17.186 +# by default. These can be overridden by using the local_addr and/or
  17.187 +# local_port arguments.
  17.188 +interface add tcp0 tcp local_port=4556
  17.189 +#interface add udp0 udp
  17.190 +
  17.191 +#
  17.192 +# link add <name> <nexthop> <type> <clayer> <args...>
  17.193 +#
  17.194 +# Add a link to a peer node.
  17.195 +# 
  17.196 +# For IP-based links (tcp or udp), the nexthop should contain a DNS
  17.197 +# hostname or IP address, followed optionally by a : and a port. If
  17.198 +# the port is not specified, the default of 4556 is used.
  17.199 +#
  17.200 +# e.g.  link add link1 dtn.dtnrg.org ONDEMAND tcp
  17.201 +#       link add link2 dtn2.dtnrg.org:10000 ONDEMAND tcp
  17.202 +
  17.203 +link add linkToUser1  192.168.2.51 ONDEMAND tcp
  17.204 +link add linkToUser2  192.168.2.52 ONDEMAND tcp
  17.205 +#link add linkToUser3  192.168.2.53 ONDEMAND tcp
  17.206 +link add linkToUser4  192.168.2.54 ONDEMAND tcp
  17.207 +link add linkToUser5  192.168.2.55 ONDEMAND tcp
  17.208 +link add linkToUser6  192.168.2.56 ONDEMAND tcp
  17.209 +link add linkToUser7  192.168.2.57 ONDEMAND tcp
  17.210 +link add linkToUser8  192.168.2.58 ONDEMAND tcp
  17.211 +link add linkToUser9  192.168.2.59 ONDEMAND tcp
  17.212 +link add linkToUser10 192.168.2.60 ONDEMAND tcp
  17.213 +link add linkToProxyGw 192.168.2.70:4557 ONDEMAND tcp
  17.214 +
  17.215 +#
  17.216 +# route add <dest> <link|peer>
  17.217 +#
  17.218 +# Add a route to the given bundle endpoint id pattern <dest> using the
  17.219 +# specified link name or peer endpoint.
  17.220 +#
  17.221 +# e.g. route add dtn://host.domain/* tcp0
  17.222 +
  17.223 +route add dtn://user1.nomadic.n4c.eu/* linkToUser1
  17.224 +route add dtn://user2.nomadic.n4c.eu/* linkToUser2
  17.225 +#route add dtn://user3.nomadic.n4c.eu/* linkToUser3
  17.226 +route add dtn://user4.nomadic.n4c.eu/* linkToUser4
  17.227 +route add dtn://user5.nomadic.n4c.eu/* linkToUser5
  17.228 +route add dtn://user6.nomadic.n4c.eu/* linkToUser6
  17.229 +route add dtn://user7.nomadic.n4c.eu/* linkToUser7
  17.230 +route add dtn://user8.nomadic.n4c.eu/* linkToUser8
  17.231 +route add dtn://user9.nomadic.n4c.eu/* linkToUser9
  17.232 +route add dtn://user10.nomadic.n4c.eu/* linkToUser10
  17.233 +route add dtn://gateway.nomadic.n4c.eu/* linkToProxyGw
  17.234 +
  17.235 +########################################
  17.236 +#
  17.237 +# Service discovery
  17.238 +#
  17.239 +########################################
  17.240 +
  17.241 +#
  17.242 +# discovery add <name> <af> <opts...>
  17.243 +# discovery announce <cl_name> <discovery_name> <cl_type> <opts...>
  17.244 +#
  17.245 +# Add a local neighborhood discovery module
  17.246 +#
  17.247 +# e.g. discovery add discovery_bonjour bonjour
  17.248 +discovery add ip_disc ip port=4301
  17.249 +discovery announce tcp0 ip_disc tcp cl_port=4556 interval=10
  17.250 +
  17.251 +########################################
  17.252 +#
  17.253 +# Parameter Tuning
  17.254 +#
  17.255 +########################################
  17.256 +
  17.257 +#
  17.258 +# Set the size threshold for the daemon so any bundles smaller than this
  17.259 +# size maintain a shadow copy in memory to minimize disk accesses. 
  17.260 +#
  17.261 +# param set payload_mem_threshold 16384
  17.262 +
  17.263 +#
  17.264 +# Test option to keep all bundle files in the filesystem, even after the
  17.265 +# bundle is no longer present at the daemon.
  17.266 +#
  17.267 +# param set payload_test_no_remove true
  17.268 +
  17.269 +#
  17.270 +# Set the size for which the tcp convergence layer sends partial reception
  17.271 +# acknowledgements. Used with reactive fragmentation
  17.272 +#
  17.273 +# param set tcpcl_partial_ack_len 4096
  17.274 +
  17.275 +#
  17.276 +# Set if bundles are automatically deleted after transmission
  17.277 +#
  17.278 +# param set early_deletion true
  17.279 +
  17.280 +# (others exist but are not fully represented here)
  17.281 +
  17.282 +log /dtnd info "dtnd configuration parsing complete"
  17.283 +
  17.284 +## emacs settings to use tcl-mode by default
  17.285 +## Local Variables: ***
  17.286 +## mode:tcl ***
  17.287 +## End: ***
    18.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    18.2 +++ b/outstation/home/user/dtn/dtn/dtn_prophet.conf	Tue Mar 16 13:36:42 2010 +0000
    18.3 @@ -0,0 +1,231 @@
    18.4 +#
    18.5 +# dtn.conf
    18.6 +#
    18.7 +# Default configuration file for Internet-connected DTN nodes. The
    18.8 +# daemon uses a tcl interpreter to parse this file, thus any standard
    18.9 +# tcl commands are valid, and all settings are get/set using a single
   18.10 +# 'set' functions as: <module> set <var> <val?>
   18.11 +#
   18.12 +
   18.13 +log /dtnd info "dtnd parsing configuration..."
   18.14 +
   18.15 +########################################
   18.16 +#
   18.17 +# Daemon Console Configuration
   18.18 +#
   18.19 +########################################
   18.20 +
   18.21 +#
   18.22 +# console set stdio [ true | false ]
   18.23 +#
   18.24 +# If set to false, disable the interactive console on stdin/stdout.
   18.25 +# The default is set to true (unless the dtnd process is run as a
   18.26 +# daemon).
   18.27 +#
   18.28 +# console set stdio false
   18.29 +
   18.30 +#
   18.31 +# console set addr <port>
   18.32 +# console set port <port>
   18.33 +#
   18.34 +# Settings for the socket based console protocol.
   18.35 +# (this interprets user commands)
   18.36 +#
   18.37 +console set addr 127.0.0.1
   18.38 +console set port 5050
   18.39 +
   18.40 +#
   18.41 +# console set prompt <prompt>
   18.42 +#
   18.43 +# Set the prompt string.  Helps if running multiple dtnd's
   18.44 +#
   18.45 +set shorthostname [lindex [split [info hostname] .] 0]
   18.46 +console set prompt "$shorthostname dtn% "
   18.47 +
   18.48 +########################################
   18.49 +#
   18.50 +# Storage Configuration
   18.51 +#
   18.52 +########################################
   18.53 +
   18.54 +#
   18.55 +# storage set type [ berkeleydb | postgres | mysql  | external ]
   18.56 +#
   18.57 +# Set the storage system to be used
   18.58 +#
   18.59 +storage set type berkeleydb
   18.60 +
   18.61 +# the following are for use with external data stores
   18.62 +#
   18.63 +# The server port to connect to (on localhost)
   18.64 +# Note that 62345 has no special significance -- chosen randomly
   18.65 +storage set server_port 62345
   18.66 +
   18.67 +# The external data store schema location, which can be
   18.68 +# found in dtn2/oasys/storage/DS.xsd.
   18.69 +storage set schema /etc/DS.xsd
   18.70 +
   18.71 +
   18.72 +#
   18.73 +# Do a runtime check for the standard locations for the persistent
   18.74 +# storage directory
   18.75 +#
   18.76 +set dbdir ""
   18.77 +foreach dir {/var/dtn /var/tmp/dtn} {
   18.78 +    if {[file isdirectory $dir]} {
   18.79 +        set dbdir $dir
   18.80 +        break
   18.81 +    }
   18.82 +}
   18.83 +
   18.84 +if {$dbdir == ""} {
   18.85 +    puts stderr "Must create /var/dtn or /var/tmp/dtn storage directory"
   18.86 +    exit 1
   18.87 +}
   18.88 +
   18.89 +#
   18.90 +# storage set payloaddir <dir>
   18.91 +#
   18.92 +# Set the directory to be used for bundle payload files
   18.93 +#
   18.94 +storage set payloaddir $dbdir/bundles
   18.95 +
   18.96 +#
   18.97 +# storage set dbname <db>
   18.98 +#
   18.99 +# Set the database name (appended with .db as the filename in berkeley
  18.100 +# db, used as-is for SQL variants
  18.101 +#
  18.102 +storage set dbname     DTN
  18.103 +
  18.104 +#
  18.105 +# storage set dbdir    <dir>
  18.106 +#
  18.107 +#
  18.108 +# When using berkeley db, set the directory to be used for the
  18.109 +# database files and the name of the files and error log.
  18.110 +#
  18.111 +storage set dbdir      $dbdir/db
  18.112 +
  18.113 +########################################
  18.114 +#
  18.115 +# Routing configuration
  18.116 +#
  18.117 +########################################
  18.118 +
  18.119 +#
  18.120 +# Set the algorithm used for dtn routing.
  18.121 +#
  18.122 +# route set type [static | flood | neighborhood | linkstate | external]
  18.123 +#
  18.124 +route set type prophet
  18.125 +
  18.126 +#
  18.127 +# route local_eid <eid>
  18.128 +#
  18.129 +# Set the local administrative id of this node. The default just uses
  18.130 +# the internet hostname plus the appended string ".dtn" to make it
  18.131 +# clear that the hostname isn't really used for DNS lookups.
  18.132 +#
  18.133 +route local_eid "dtn://mightyatom.folly.org.uk"
  18.134 +
  18.135 +#
  18.136 +# External router specific options
  18.137 +#
  18.138 +# route set server_port 8001
  18.139 +# route set hello_interval 30
  18.140 +# route set schema "/etc/router.xsd"
  18.141 +
  18.142 +########################################
  18.143 +#
  18.144 +# TCP convergence layer configuration
  18.145 +#
  18.146 +########################################
  18.147 +
  18.148 +#
  18.149 +# interface add [name] [CL]
  18.150 +#
  18.151 +# Add an input interface to listen on addr:port for incoming bundles
  18.152 +# from other tcp / udp convergence layers
  18.153 +#
  18.154 +# For IP-based interfaces, interfaces listen on INADDR_ANY port 4556
  18.155 +# by default. These can be overridden by using the local_addr and/or
  18.156 +# local_port arguments.
  18.157 +interface add tcp0 tcp
  18.158 +interface add udp0 udp
  18.159 +
  18.160 +#
  18.161 +# link add <name> <nexthop> <type> <clayer> <args...>
  18.162 +#
  18.163 +# Add a link to a peer node.
  18.164 +# 
  18.165 +# For IP-based links (tcp or udp), the nexthop should contain a DNS
  18.166 +# hostname or IP address, followed optionally by a : and a port. If
  18.167 +# the port is not specified, the default of 4556 is used.
  18.168 +#
  18.169 +# e.g.  link add link1 dtn.dtnrg.org ONDEMAND tcp
  18.170 +#       link add link2 dtn2.dtnrg.org:10000 ONDEMAND tcp
  18.171 +
  18.172 +#
  18.173 +# route add <dest> <link|peer>
  18.174 +#
  18.175 +# Add a route to the given bundle endpoint id pattern <dest> using the
  18.176 +# specified link name or peer endpoint.
  18.177 +#
  18.178 +# e.g. route add dtn://host.domain/* tcp0
  18.179 +link add linkToN4C 130.240.97.204 ONDEMAND tcp
  18.180 +link add linkToWeeePc 81.187.254.251 ONDEMAND tcp
  18.181 +route add dtn://dtn.n4c.eu/* linkToN4C
  18.182 +route add dtn://Weee-Pc1.folly.org.uk/* linkToWeeePc
  18.183 +
  18.184 +########################################
  18.185 +#
  18.186 +# Service discovery
  18.187 +#
  18.188 +########################################
  18.189 +
  18.190 +#
  18.191 +# discovery add <name> <af> <opts...>
  18.192 +# discovery announce <cl_name> <discovery_name> <cl_type> <opts...>
  18.193 +#
  18.194 +# Add a local neighborhood discovery module
  18.195 +#
  18.196 +# e.g. discovery add discovery_bonjour bonjour
  18.197 +
  18.198 +########################################
  18.199 +#
  18.200 +# Parameter Tuning
  18.201 +#
  18.202 +########################################
  18.203 +
  18.204 +#
  18.205 +# Set the size threshold for the daemon so any bundles smaller than this
  18.206 +# size maintain a shadow copy in memory to minimize disk accesses. 
  18.207 +#
  18.208 +# param set payload_mem_threshold 16384
  18.209 +
  18.210 +#
  18.211 +# Test option to keep all bundle files in the filesystem, even after the
  18.212 +# bundle is no longer present at the daemon.
  18.213 +#
  18.214 +# param set payload_test_no_remove true
  18.215 +
  18.216 +#
  18.217 +# Set the size for which the tcp convergence layer sends partial reception
  18.218 +# acknowledgements. Used with reactive fragmentation
  18.219 +#
  18.220 +# param set tcpcl_partial_ack_len 4096
  18.221 +
  18.222 +#
  18.223 +# Set if bundles are automatically deleted after transmission
  18.224 +#
  18.225 +# param set early_deletion true
  18.226 +
  18.227 +# (others exist but are not fully represented here)
  18.228 +
  18.229 +log /dtnd info "dtnd configuration parsing complete"
  18.230 +
  18.231 +## emacs settings to use tcl-mode by default
  18.232 +## Local Variables: ***
  18.233 +## mode:tcl ***
  18.234 +## End: ***
    19.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    19.2 +++ b/outstation/home/user/dtn/python/SocketServer2_6.py	Tue Mar 16 13:36:42 2010 +0000
    19.3 @@ -0,0 +1,681 @@
    19.4 +"""Generic socket server classes.
    19.5 +
    19.6 +This module tries to capture the various aspects of defining a server:
    19.7 +
    19.8 +For socket-based servers:
    19.9 +
   19.10 +- address family:
   19.11 +        - AF_INET{,6}: IP (Internet Protocol) sockets (default)
   19.12 +        - AF_UNIX: Unix domain sockets
   19.13 +        - others, e.g. AF_DECNET are conceivable (see <socket.h>
   19.14 +- socket type:
   19.15 +        - SOCK_STREAM (reliable stream, e.g. TCP)
   19.16 +        - SOCK_DGRAM (datagrams, e.g. UDP)
   19.17 +
   19.18 +For request-based servers (including socket-based):
   19.19 +
   19.20 +- client address verification before further looking at the request
   19.21 +        (This is actually a hook for any processing that needs to look
   19.22 +         at the request before anything else, e.g. logging)
   19.23 +- how to handle multiple requests:
   19.24 +        - synchronous (one request is handled at a time)
   19.25 +        - forking (each request is handled by a new process)
   19.26 +        - threading (each request is handled by a new thread)
   19.27 +
   19.28 +The classes in this module favor the server type that is simplest to
   19.29 +write: a synchronous TCP/IP server.  This is bad class design, but
   19.30 +save some typing.  (There's also the issue that a deep class hierarchy
   19.31 +slows down method lookups.)
   19.32 +
   19.33 +There are five classes in an inheritance diagram, four of which represent
   19.34 +synchronous servers of four types:
   19.35 +
   19.36 +        +------------+
   19.37 +        | BaseServer |
   19.38 +        +------------+
   19.39 +              |
   19.40 +              v
   19.41 +        +-----------+        +------------------+
   19.42 +        | TCPServer |------->| UnixStreamServer |
   19.43 +        +-----------+        +------------------+
   19.44 +              |
   19.45 +              v
   19.46 +        +-----------+        +--------------------+
   19.47 +        | UDPServer |------->| UnixDatagramServer |
   19.48 +        +-----------+        +--------------------+
   19.49 +
   19.50 +Note that UnixDatagramServer derives from UDPServer, not from
   19.51 +UnixStreamServer -- the only difference between an IP and a Unix
   19.52 +stream server is the address family, which is simply repeated in both
   19.53 +unix server classes.
   19.54 +
   19.55 +Forking and threading versions of each type of server can be created
   19.56 +using the ForkingMixIn and ThreadingMixIn mix-in classes.  For
   19.57 +instance, a threading UDP server class is created as follows:
   19.58 +
   19.59 +        class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
   19.60 +
   19.61 +The Mix-in class must come first, since it overrides a method defined
   19.62 +in UDPServer! Setting the various member variables also changes
   19.63 +the behavior of the underlying server mechanism.
   19.64 +
   19.65 +To implement a service, you must derive a class from
   19.66 +BaseRequestHandler and redefine its handle() method.  You can then run
   19.67 +various versions of the service by combining one of the server classes
   19.68 +with your request handler class.
   19.69 +
   19.70 +The request handler class must be different for datagram or stream
   19.71 +services.  This can be hidden by using the request handler
   19.72 +subclasses StreamRequestHandler or DatagramRequestHandler.
   19.73 +
   19.74 +Of course, you still have to use your head!
   19.75 +
   19.76 +For instance, it makes no sense to use a forking server if the service
   19.77 +contains state in memory that can be modified by requests (since the
   19.78 +modifications in the child process would never reach the initial state
   19.79 +kept in the parent process and passed to each child).  In this case,
   19.80 +you can use a threading server, but you will probably have to use
   19.81 +locks to avoid two requests that come in nearly simultaneous to apply
   19.82 +conflicting changes to the server state.
   19.83 +
   19.84 +On the other hand, if you are building e.g. an HTTP server, where all
   19.85 +data is stored externally (e.g. in the file system), a synchronous
   19.86 +class will essentially render the service "deaf" while one request is
   19.87 +being handled -- which may be for a very long time if a client is slow
   19.88 +to reqd all the data it has requested.  Here a threading or forking
   19.89 +server is appropriate.
   19.90 +
   19.91 +In some cases, it may be appropriate to process part of a request
   19.92 +synchronously, but to finish processing in a forked child depending on
   19.93 +the request data.  This can be implemented by using a synchronous
   19.94 +server and doing an explicit fork in the request handler class
   19.95 +handle() method.
   19.96 +
   19.97 +Another approach to handling multiple simultaneous requests in an
   19.98 +environment that supports neither threads nor fork (or where these are
   19.99 +too expensive or inappropriate for the service) is to maintain an
  19.100 +explicit table of partially finished requests and to use select() to
  19.101 +decide which request to work on next (or whether to handle a new
  19.102 +incoming request).  This is particularly important for stream services
  19.103 +where each client can potentially be connected for a long time (if
  19.104 +threads or subprocesses cannot be used).
  19.105 +
  19.106 +Future work:
  19.107 +- Standard classes for Sun RPC (which uses either UDP or TCP)
  19.108 +- Standard mix-in classes to implement various authentication
  19.109 +  and encryption schemes
  19.110 +- Standard framework for select-based multiplexing
  19.111 +
  19.112 +XXX Open problems:
  19.113 +- What to do with out-of-band data?
  19.114 +
  19.115 +BaseServer:
  19.116 +- split generic "request" functionality out into BaseServer class.
  19.117 +  Copyright (C) 2000  Luke Kenneth Casson Leighton <lkcl@samba.org>
  19.118 +
  19.119 +  example: read entries from a SQL database (requires overriding
  19.120 +  get_request() to return a table entry from the database).
  19.121 +  entry is processed by a RequestHandlerClass.
  19.122 +
  19.123 +"""
  19.124 +
  19.125 +# Author of the BaseServer patch: Luke Kenneth Casson Leighton
  19.126 +
  19.127 +# XXX Warning!
  19.128 +# There is a test suite for this module, but it cannot be run by the
  19.129 +# standard regression test.
  19.130 +# To run it manually, run Lib/test/test_socketserver.py.
  19.131 +
  19.132 +__version__ = "0.4"
  19.133 +
  19.134 +
  19.135 +import socket
  19.136 +import select
  19.137 +import sys
  19.138 +import os
  19.139 +try:
  19.140 +    import threading
  19.141 +except ImportError:
  19.142 +    import dummy_threading as threading
  19.143 +
  19.144 +__all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer",
  19.145 +           "ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler",
  19.146 +           "StreamRequestHandler","DatagramRequestHandler",
  19.147 +           "ThreadingMixIn", "ForkingMixIn"]
  19.148 +if hasattr(socket, "AF_UNIX"):
  19.149 +    __all__.extend(["UnixStreamServer","UnixDatagramServer",
  19.150 +                    "ThreadingUnixStreamServer",
  19.151 +                    "ThreadingUnixDatagramServer"])
  19.152 +
  19.153 +class BaseServer:
  19.154 +
  19.155 +    """Base class for server classes.
  19.156 +
  19.157 +    Methods for the caller:
  19.158 +
  19.159 +    - __init__(server_address, RequestHandlerClass)
  19.160 +    - serve_forever(poll_interval=0.5)
  19.161 +    - shutdown()
  19.162 +    - handle_request()  # if you do not use serve_forever()
  19.163 +    - fileno() -> int   # for select()
  19.164 +
  19.165 +    Methods that may be overridden:
  19.166 +
  19.167 +    - server_bind()
  19.168 +    - server_activate()
  19.169 +    - get_request() -> request, client_address
  19.170 +    - handle_timeout()
  19.171 +    - verify_request(request, client_address)
  19.172 +    - server_close()
  19.173 +    - process_request(request, client_address)
  19.174 +    - close_request(request)
  19.175 +    - handle_error()
  19.176 +
  19.177 +    Methods for derived classes:
  19.178 +
  19.179 +    - finish_request(request, client_address)
  19.180 +
  19.181 +    Class variables that may be overridden by derived classes or
  19.182 +    instances:
  19.183 +
  19.184 +    - timeout
  19.185 +    - address_family
  19.186 +    - socket_type
  19.187 +    - allow_reuse_address
  19.188 +
  19.189 +    Instance variables:
  19.190 +
  19.191 +    - RequestHandlerClass
  19.192 +    - socket
  19.193 +
  19.194 +    """
  19.195 +
  19.196 +    timeout = None
  19.197 +
  19.198 +    def __init__(self, server_address, RequestHandlerClass):
  19.199 +        """Constructor.  May be extended, do not override."""
  19.200 +        self.server_address = server_address
  19.201 +        self.RequestHandlerClass = RequestHandlerClass
  19.202 +        self.__is_shut_down = threading.Event()
  19.203 +        self.__serving = False
  19.204 +
  19.205 +    def server_activate(self):
  19.206 +        """Called by constructor to activate the server.
  19.207 +
  19.208 +        May be overridden.
  19.209 +
  19.210 +        """
  19.211 +        pass
  19.212 +
  19.213 +    def serve_forever(self, poll_interval=0.5):
  19.214 +        """Handle one request at a time until shutdown.
  19.215 +
  19.216 +        Polls for shutdown every poll_interval seconds. Ignores
  19.217 +        self.timeout. If you need to do periodic tasks, do them in
  19.218 +        another thread.
  19.219 +        """
  19.220 +        self.__serving = True
  19.221 +        self.__is_shut_down.clear()
  19.222 +        while self.__serving:
  19.223 +            # XXX: Consider using another file descriptor or
  19.224 +            # connecting to the socket to wake this up instead of
  19.225 +            # polling. Polling reduces our responsiveness to a
  19.226 +            # shutdown request and wastes cpu at all other times.
  19.227 +            r, w, e = select.select([self], [], [], poll_interval)
  19.228 +            if r:
  19.229 +                self._handle_request_noblock()
  19.230 +        self.__is_shut_down.set()
  19.231 +
  19.232 +    def shutdown(self):
  19.233 +        """Stops the serve_forever loop.
  19.234 +
  19.235 +        Blocks until the loop has finished. This must be called while
  19.236 +        serve_forever() is running in another thread, or it will
  19.237 +        deadlock.
  19.238 +        """
  19.239 +        self.__serving = False
  19.240 +        self.__is_shut_down.wait()
  19.241 +
  19.242 +    # The distinction between handling, getting, processing and
  19.243 +    # finishing a request is fairly arbitrary.  Remember:
  19.244 +    #
  19.245 +    # - handle_request() is the top-level call.  It calls
  19.246 +    #   select, get_request(), verify_request() and process_request()
  19.247 +    # - get_request() is different for stream or datagram sockets
  19.248 +    # - process_request() is the place that may fork a new process
  19.249 +    #   or create a new thread to finish the request
  19.250 +    # - finish_request() instantiates the request handler class;
  19.251 +    #   this constructor will handle the request all by itself
  19.252 +
  19.253 +    def handle_request(self):
  19.254 +        """Handle one request, possibly blocking.
  19.255 +
  19.256 +        Respects self.timeout.
  19.257 +        """
  19.258 +        # Support people who used socket.settimeout() to escape
  19.259 +        # handle_request before self.timeout was available.
  19.260 +        timeout = self.socket.gettimeout()
  19.261 +        if timeout is None:
  19.262 +            timeout = self.timeout
  19.263 +        elif self.timeout is not None:
  19.264 +            timeout = min(timeout, self.timeout)
  19.265 +        fd_sets = select.select([self], [], [], timeout)
  19.266 +        if not fd_sets[0]:
  19.267 +            self.handle_timeout()
  19.268 +            return
  19.269 +        self._handle_request_noblock()
  19.270 +
  19.271 +    def _handle_request_noblock(self):
  19.272 +        """Handle one request, without blocking.
  19.273 +
  19.274 +        I assume that select.select has returned that the socket is
  19.275 +        readable before this function was called, so there should be
  19.276 +        no risk of blocking in get_request().
  19.277 +        """
  19.278 +        try:
  19.279 +            request, client_address = self.get_request()
  19.280 +        except socket.error:
  19.281 +            return
  19.282 +        if self.verify_request(request, client_address):
  19.283 +            try:
  19.284 +                self.process_request(request, client_address)
  19.285 +            except:
  19.286 +                self.handle_error(request, client_address)
  19.287 +                self.close_request(request)
  19.288 +
  19.289 +    def handle_timeout(self):
  19.290 +        """Called if no new request arrives within self.timeout.
  19.291 +
  19.292 +        Overridden by ForkingMixIn.
  19.293 +        """
  19.294 +        pass
  19.295 +
  19.296 +    def verify_request(self, request, client_address):
  19.297 +        """Verify the request.  May be overridden.
  19.298 +
  19.299 +        Return True if we should proceed with this request.
  19.300 +
  19.301 +        """
  19.302 +        return True
  19.303 +
  19.304 +    def process_request(self, request, client_address):
  19.305 +        """Call finish_request.
  19.306 +
  19.307 +        Overridden by ForkingMixIn and ThreadingMixIn.
  19.308 +
  19.309 +        """
  19.310 +        self.finish_request(request, client_address)
  19.311 +        self.close_request(request)
  19.312 +
  19.313 +    def server_close(self):
  19.314 +        """Called to clean-up the server.
  19.315 +
  19.316 +        May be overridden.
  19.317 +
  19.318 +        """
  19.319 +        pass
  19.320 +
  19.321 +    def finish_request(self, request, client_address):
  19.322 +        """Finish one request by instantiating RequestHandlerClass."""
  19.323 +        self.RequestHandlerClass(request, client_address, self)
  19.324 +
  19.325 +    def close_request(self, request):
  19.326 +        """Called to clean up an individual request."""
  19.327 +        pass
  19.328 +
  19.329 +    def handle_error(self, request, client_address):
  19.330 +        """Handle an error gracefully.  May be overridden.
  19.331 +
  19.332 +        The default is to print a traceback and continue.
  19.333 +
  19.334 +        """
  19.335 +        print '-'*40
  19.336 +        print 'Exception happened during processing of request from',
  19.337 +        print client_address
  19.338 +        import traceback
  19.339 +        traceback.print_exc() # XXX But this goes to stderr!
  19.340 +        print '-'*40
  19.341 +
  19.342 +
  19.343 +class TCPServer(BaseServer):
  19.344 +
  19.345 +    """Base class for various socket-based server classes.
  19.346 +
  19.347 +    Defaults to synchronous IP stream (i.e., TCP).
  19.348 +
  19.349 +    Methods for the caller:
  19.350 +
  19.351 +    - __init__(server_address, RequestHandlerClass, bind_and_activate=True)
  19.352 +    - serve_forever(poll_interval=0.5)
  19.353 +    - shutdown()
  19.354 +    - handle_request()  # if you don't use serve_forever()
  19.355 +    - fileno() -> int   # for select()
  19.356 +
  19.357 +    Methods that may be overridden:
  19.358 +
  19.359 +    - server_bind()
  19.360 +    - server_activate()
  19.361 +    - get_request() -> request, client_address
  19.362 +    - handle_timeout()
  19.363 +    - verify_request(request, client_address)
  19.364 +    - process_request(request, client_address)
  19.365 +    - close_request(request)
  19.366 +    - handle_error()
  19.367 +
  19.368 +    Methods for derived classes:
  19.369 +
  19.370 +    - finish_request(request, client_address)
  19.371 +
  19.372 +    Class variables that may be overridden by derived classes or
  19.373 +    instances:
  19.374 +
  19.375 +    - timeout
  19.376 +    - address_family
  19.377 +    - socket_type
  19.378 +    - request_queue_size (only for stream sockets)
  19.379 +    - allow_reuse_address
  19.380 +
  19.381 +    Instance variables:
  19.382 +
  19.383 +    - server_address
  19.384 +    - RequestHandlerClass
  19.385 +    - socket
  19.386 +
  19.387 +    """
  19.388 +
  19.389 +    address_family = socket.AF_INET
  19.390 +
  19.391 +    socket_type = socket.SOCK_STREAM
  19.392 +
  19.393 +    request_queue_size = 5
  19.394 +
  19.395 +    allow_reuse_address = False
  19.396 +
  19.397 +    def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
  19.398 +        """Constructor.  May be extended, do not override."""
  19.399 +        BaseServer.__init__(self, server_address, RequestHandlerClass)
  19.400 +        self.socket = socket.socket(self.address_family,
  19.401 +                                    self.socket_type)
  19.402 +        if bind_and_activate:
  19.403 +            self.server_bind()
  19.404 +            self.server_activate()
  19.405 +
  19.406 +    def server_bind(self):
  19.407 +        """Called by constructor to bind the socket.
  19.408 +
  19.409 +        May be overridden.
  19.410 +
  19.411 +        """
  19.412 +        if self.allow_reuse_address:
  19.413 +            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  19.414 +        self.socket.bind(self.server_address)
  19.415 +        self.server_address = self.socket.getsockname()
  19.416 +
  19.417 +    def server_activate(self):
  19.418 +        """Called by constructor to activate the server.
  19.419 +
  19.420 +        May be overridden.
  19.421 +
  19.422 +        """
  19.423 +        self.socket.listen(self.request_queue_size)
  19.424 +
  19.425 +    def server_close(self):
  19.426 +        """Called to clean-up the server.
  19.427 +
  19.428 +        May be overridden.
  19.429 +
  19.430 +        """
  19.431 +        self.socket.close()
  19.432 +
  19.433 +    def fileno(self):
  19.434 +        """Return socket file number.
  19.435 +
  19.436 +        Interface required by select().
  19.437 +
  19.438 +        """
  19.439 +        return self.socket.fileno()
  19.440 +
  19.441 +    def get_request(self):
  19.442 +        """Get the request and client address from the socket.
  19.443 +
  19.444 +        May be overridden.
  19.445 +
  19.446 +        """
  19.447 +        return self.socket.accept()
  19.448 +
  19.449 +    def close_request(self, request):
  19.450 +        """Called to clean up an individual request."""
  19.451 +        request.close()
  19.452 +
  19.453 +
  19.454 +class UDPServer(TCPServer):
  19.455 +
  19.456 +    """UDP server class."""
  19.457 +
  19.458 +    allow_reuse_address = False
  19.459 +
  19.460 +    socket_type = socket.SOCK_DGRAM
  19.461 +
  19.462 +    max_packet_size = 8192
  19.463 +
  19.464 +    def get_request(self):
  19.465 +        data, client_addr = self.socket.recvfrom(self.max_packet_size)
  19.466 +        return (data, self.socket), client_addr
  19.467 +
  19.468 +    def server_activate(self):
  19.469 +        # No need to call listen() for UDP.
  19.470 +        pass
  19.471 +
  19.472 +    def close_request(self, request):
  19.473 +        # No need to close anything.
  19.474 +        pass
  19.475 +
  19.476 +class ForkingMixIn:
  19.477 +
  19.478 +    """Mix-in class to handle each request in a new process."""
  19.479 +
  19.480 +    timeout = 300
  19.481 +    active_children = None
  19.482 +    max_children = 40
  19.483 +
  19.484 +    def collect_children(self):
  19.485 +        """Internal routine to wait for children that have exited."""
  19.486 +        if self.active_children is None: return
  19.487 +        while len(self.active_children) >= self.max_children:
  19.488 +            # XXX: This will wait for any child process, not just ones
  19.489 +            # spawned by this library. This could confuse other
  19.490 +            # libraries that expect to be able to wait for their own
  19.491 +            # children.
  19.492 +            try:
  19.493 +                pid, status = os.waitpid(0, options=0)
  19.494 +            except os.error:
  19.495 +                pid = None
  19.496 +            if pid not in self.active_children: continue
  19.497 +            self.active_children.remove(pid)
  19.498 +
  19.499 +        # XXX: This loop runs more system calls than it ought
  19.500 +        # to. There should be a way to put the active_children into a
  19.501 +        # process group and then use os.waitpid(-pgid) to wait for any
  19.502 +        # of that set, but I couldn't find a way to allocate pgids
  19.503 +        # that couldn't collide.
  19.504 +        for child in self.active_children:
  19.505 +            try:
  19.506 +                pid, status = os.waitpid(child, os.WNOHANG)
  19.507 +            except os.error:
  19.508 +                pid = None
  19.509 +            if not pid: continue
  19.510 +            try:
  19.511 +                self.active_children.remove(pid)
  19.512 +            except ValueError, e:
  19.513 +                raise ValueError('%s. x=%d and list=%r' % (e.message, pid,
  19.514 +                                                           self.active_children))
  19.515 +
  19.516 +    def handle_timeout(self):
  19.517 +        """Wait for zombies after self.timeout seconds of inactivity.
  19.518 +
  19.519 +        May be extended, do not override.
  19.520 +        """
  19.521 +        self.collect_children()
  19.522 +
  19.523 +    def process_request(self, request, client_address):
  19.524 +        """Fork a new subprocess to process the request."""
  19.525 +        self.collect_children()
  19.526 +        pid = os.fork()
  19.527 +        if pid:
  19.528 +            # Parent process
  19.529 +            if self.active_children is None:
  19.530 +                self.active_children = []
  19.531 +            self.active_children.append(pid)
  19.532 +            self.close_request(request)
  19.533 +            return
  19.534 +        else:
  19.535 +            # Child process.
  19.536 +            # This must never return, hence os._exit()!
  19.537 +            try:
  19.538 +                self.finish_request(request, client_address)
  19.539 +                os._exit(0)
  19.540 +            except:
  19.541 +                try:
  19.542 +                    self.handle_error(request, client_address)
  19.543 +                finally:
  19.544 +                    os._exit(1)
  19.545 +
  19.546 +
  19.547 +class ThreadingMixIn:
  19.548 +    """Mix-in class to handle each request in a new thread."""
  19.549 +
  19.550 +    # Decides how threads will act upon termination of the
  19.551 +    # main process
  19.552 +    daemon_threads = False
  19.553 +
  19.554 +    def process_request_thread(self, request, client_address):
  19.555 +        """Same as in BaseServer but as a thread.
  19.556 +
  19.557 +        In addition, exception handling is done here.
  19.558 +
  19.559 +        """
  19.560 +        try:
  19.561 +            self.finish_request(request, client_address)
  19.562 +            self.close_request(request)
  19.563 +        except:
  19.564 +            self.handle_error(request, client_address)
  19.565 +            self.close_request(request)
  19.566 +
  19.567 +    def process_request(self, request, client_address):
  19.568 +        """Start a new thread to process the request."""
  19.569 +        t = threading.Thread(target = self.process_request_thread,
  19.570 +                             args = (request, client_address))
  19.571 +        if self.daemon_threads:
  19.572 +            t.setDaemon (1)
  19.573 +        t.start()
  19.574 +
  19.575 +
  19.576 +class ForkingUDPServer(ForkingMixIn, UDPServer): pass
  19.577 +class ForkingTCPServer(ForkingMixIn, TCPServer): pass
  19.578 +
  19.579 +class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass
  19.580 +class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass
  19.581 +
  19.582 +if hasattr(socket, 'AF_UNIX'):
  19.583 +
  19.584 +    class UnixStreamServer(TCPServer):
  19.585 +        address_family = socket.AF_UNIX
  19.586 +
  19.587 +    class UnixDatagramServer(UDPServer):
  19.588 +        address_family = socket.AF_UNIX
  19.589 +
  19.590 +    class ThreadingUnixStreamServer(ThreadingMixIn, UnixStreamServer): pass
  19.591 +
  19.592 +    class ThreadingUnixDatagramServer(ThreadingMixIn, UnixDatagramServer): pass
  19.593 +
  19.594 +class BaseRequestHandler:
  19.595 +
  19.596 +    """Base class for request handler classes.
  19.597 +
  19.598 +    This class is instantiated for each request to be handled.  The
  19.599 +    constructor sets the instance variables request, client_address
  19.600 +    and server, and then calls the handle() method.  To implement a
  19.601 +    specific service, all you need to do is to derive a class which
  19.602 +    defines a handle() method.
  19.603 +
  19.604 +    The handle() method can find the request as self.request, the
  19.605 +    client address as self.client_address, and the server (in case it
  19.606 +    needs access to per-server information) as self.server.  Since a
  19.607 +    separate instance is created for each request, the handle() method
  19.608 +    can define arbitrary other instance variariables.
  19.609 +
  19.610 +    """
  19.611 +
  19.612 +    def __init__(self, request, client_address, server):
  19.613 +        self.request = request
  19.614 +        self.client_address = client_address
  19.615 +        self.server = server
  19.616 +        try:
  19.617 +            self.setup()
  19.618 +            self.handle()
  19.619 +            self.finish()
  19.620 +        finally:
  19.621 +            sys.exc_traceback = None    # Help garbage collection
  19.622 +
  19.623 +    def setup(self):
  19.624 +        pass
  19.625 +
  19.626 +    def handle(self):
  19.627 +        pass
  19.628 +
  19.629 +    def finish(self):
  19.630 +        pass
  19.631 +
  19.632 +
  19.633 +# The following two classes make it possible to use the same service
  19.634 +# class for stream or datagram servers.
  19.635 +# Each class sets up these instance variables:
  19.636 +# - rfile: a file object from which receives the request is read
  19.637 +# - wfile: a file object to which the reply is written
  19.638 +# When the handle() method returns, wfile is flushed properly
  19.639 +
  19.640 +
  19.641 +class StreamRequestHandler(BaseRequestHandler):
  19.642 +
  19.643 +    """Define self.rfile and self.wfile for stream sockets."""
  19.644 +
  19.645 +    # Default buffer sizes for rfile, wfile.
  19.646 +    # We default rfile to buffered because otherwise it could be
  19.647 +    # really slow for large data (a getc() call per byte); we make
  19.648 +    # wfile unbuffered because (a) often after a write() we want to
  19.649 +    # read and we need to flush the line; (b) big writes to unbuffered
  19.650 +    # files are typically optimized by stdio even when big reads
  19.651 +    # aren't.
  19.652 +    rbufsize = -1
  19.653 +    wbufsize = 0
  19.654 +
  19.655 +    def setup(self):
  19.656 +        self.connection = self.request
  19.657 +        self.rfile = self.connection.makefile('rb', self.rbufsize)
  19.658 +        self.wfile = self.connection.makefile('wb', self.wbufsize)
  19.659 +
  19.660 +    def finish(self):
  19.661 +        if not self.wfile.closed:
  19.662 +            self.wfile.flush()
  19.663 +        self.wfile.close()
  19.664 +        self.rfile.close()
  19.665 +
  19.666 +
  19.667 +class DatagramRequestHandler(BaseRequestHandler):
  19.668 +
  19.669 +    # XXX Regrettably, I cannot get this working on Linux;
  19.670 +    # s.recvfrom() doesn't return a meaningful client address.
  19.671 +
  19.672 +    """Define self.rfile and self.wfile for datagram sockets."""
  19.673 +
  19.674 +    def setup(self):
  19.675 +        try:
  19.676 +            from cStringIO import StringIO
  19.677 +        except ImportError:
  19.678 +            from StringIO import StringIO
  19.679 +        self.packet, self.socket = self.request
  19.680 +        self.rfile = StringIO(self.packet)
  19.681 +        self.wfile = StringIO()
  19.682 +
  19.683 +    def finish(self):
  19.684 +        self.socket.sendto(self.wfile.getvalue(), self.client_address)
    20.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    20.2 +++ b/outstation/home/user/dtn/python/dp_dtn.py	Tue Mar 16 13:36:42 2010 +0000
    20.3 @@ -0,0 +1,576 @@
    20.4 +#! /usr/bin/python
    20.5 +# PyMail DTN Nomadic Mail System
    20.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    20.7 +#
    20.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    20.9 +#    you may not use this file except in compliance with the License.
   20.10 +#    You may obtain a copy of the License at
   20.11 +# 
   20.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   20.13 +# 
   20.14 +#    Unless required by applicable law or agreed to in writing, software
   20.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   20.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   20.17 +#    See the License for the specific language governing permissions and
   20.18 +#    limitations under the License.
   20.19 +#
   20.20 +
   20.21 +
   20.22 +"""
   20.23 +DTN-Postfix interface
   20.24 +
   20.25 +A single instance of each of the dtn_send_interface dtn_recv_interface classes
   20.26 +are instantiated and run in threads to manage sending and receiving bundles on
   20.27 +the DTN daemon interface. A third thread is used to handle incoming transmission
   20.28 +reports.
   20.29 +
   20.30 +The send side handles updating the database that records information about
   20.31 +sent messages.  Bundles are sent as files. Files to be sent are queued by
   20.32 +the Postfix outgoing message handler and the 'retry' timer process.
   20.33 +
   20.34 +The report receiver also updates the database as necessary.
   20.35 +
   20.36 +The receive side  dumps the bundle payload as a file in the dtn maildir (this is just
   20.37 +a matter of renaming as we get bundles as files).
   20.38 +
   20.39 +The send and receive threads use separate connections to the DTN daemon because
   20.40 +of the arcane pseudo-half-duplex nature of the DTN daemon.
   20.41 +
   20.42 +At present (until the naming system is improved):
   20.43 +- We expect the local  EID for the gateway to be dtn://gateway.nomadic.n4c.eu
   20.44 +- Oustations will have local  EIDs matching dtn://<user>.nomadic.n4c.eu
   20.45 +- The source EID for this system is <Local EID>/email/out
   20.46 +- Incoming mail bundles should be sent to <Local EID>/email/in
   20.47 +- The reports will be sent back to <Local EID>/email/reports
   20.48 +
   20.49 +Permanent registrations will be created for these EIDs with the 'failure action'
   20.50 +set to DEFER so that bundles are retained in case the mail interface is not
   20.51 +active at the time the bundles arrive. We should do this in the dtnd
   20.52 +configuration file.
   20.53 +
   20.54 +This piece ofthe program is symmetric between the gateway and the outstation
   20.55 +client implementations except for the destination EID for outgoing bundles.
   20.56 +The local EID name is retrieved from the dtnd to determine the source EID etc.
   20.57 +
   20.58 +The sending side receives notications of new emails to send in a Queue.
   20.59 +
   20.60 +The receive side informs the email handlers in one of two ways:
   20.61 +- It pushes an 'event' onto a queue (not used in outstations - there may
   20.62 +  not be a POP3 client active when the message is received).
   20.63 +- It keeps a 'state number' which is incremented each time a new email
   20.64 +  is received.  This is prtected by a Lock so that it can be read securely
   20.65 +  by a POP3 server which can then rebuild its message list - except that
   20.66 +  this does not work.  Clients delet mail as they retrieve it, so
   20.67 +  the list contains empty slots and will not be rebuilt easily.  In practice
   20.68 +  we can forget this wholebusiness.  Just wait until the next retrieval cycle.
   20.69 +  I'll leave the code in for the time being in case I think of another way..
   20.70 +
   20.71 +Revision History
   20.72 +================
   20.73 +Version   Date       Author         Notes
   20.74 +0.1       14/03/2010 Elwyn Davies   License statement added.
   20.75 +0.0	  25/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
   20.76 +  """
   20.77 +
   20.78 +import os
   20.79 +import time
   20.80 +import threading
   20.81 +import dtnapi
   20.82 +import socket
   20.83 +import Queue
   20.84 +import logging
   20.85 +from select import select
   20.86 +from pysqlite2 import dbapi2 as sqlite3
   20.87 +
   20.88 +from dp_msg_send_evt import *
   20.89 +import pypop_maildir
   20.90 +
   20.91 +
   20.92 +# Exception resulting from DTN problems
   20.93 +class DtnError(Exception):
   20.94 +    def __init__(self, reason):
   20.95 +        self.reason = reason
   20.96 +    def __str__(self):
   20.97 +        return "Error communication with DTN daemon: %s" % (repr(self.reason),)
   20.98 +
   20.99 +# Local part of source/destination EID
  20.100 +EMAIL_OUT = "email/out"
  20.101 +EMAIL_IN = "email/in"
  20.102 +
  20.103 +# Local part of report EID
  20.104 +EMAIL_REPORTS = "email/reports"
  20.105 +
  20.106 +# Expiration period for mail bundles
  20.107 +# Try 3 days for now - value in seconds
  20.108 +MAIL_EXPIRY = (3 * 24 * 60 * 60)
  20.109 +
  20.110 +# Status report flag values
  20.111 +# See 
  20.112 +REPORT_STATUS_DELIVERED = 8
  20.113 +REPORT_STATUS_DELETED = 16
  20.114 +
  20.115 +
  20.116 +# Function to make the EIDs (has to be done in each thread)
  20.117 +def build_eids(dtn_handle):
  20.118 +    if dtn_handle == -1:
  20.119 +        raise DtnError("bad DTN handle")
  20.120 +    email_out_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_OUT)
  20.121 +    email_in_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_IN)
  20.122 +    report_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_REPORTS)
  20.123 +    if email_in_addr == None or email_out_addr == None or report_addr == None:
  20.124 +        raise DtnError("failure in dtn_build_local_eid")
  20.125 +    return (email_out_addr, email_in_addr, report_addr)
  20.126 +
  20.127 +# Class for dealing with sending email over DTN
  20.128 +# Single instance runs in a thread - only deals with
  20.129 +# sending so is not bound to any address.
  20.130 +class dtn_send(threading.Thread):
  20.131 +    def __init__(self, domain, send_q, logger):
  20.132 +        threading.Thread.__init__(self, name="dtn-send")
  20.133 +        self.domain = domain
  20.134 +        self.send_q = send_q
  20.135 +
  20.136 +        # Set up logging functions
  20.137 +        self.logger = logger
  20.138 +        self.loginfo = logger.info
  20.139 +        self.logdebug = logger.debug
  20.140 +        self.logerror = logger.error
  20.141 +        self.logwarn = logger.warn
  20.142 +        
  20.143 +        # Flag to keep the loop running
  20.144 +        self.send_run = True
  20.145 +
  20.146 +    def end_run(self):
  20.147 +        self.send_run = False
  20.148 +        
  20.149 +    def run(self):
  20.150 +        # This function loops forever (well more or less) waiting for
  20.151 +        # queued send requests.  So all the variables can be local.
  20.152 +        # Create a connection to the DTN daemon
  20.153 +        dtn_handle = dtnapi.dtn_open()
  20.154 +        if dtn_handle == -1:
  20.155 +            raise DtnError("unable to open connection with daemon")
  20.156 +
  20.157 +        # Generate the EIDs
  20.158 +        (email_out_eid, email_in_eid, report_eid) = build_eids(dtn_handle)
  20.159 +
  20.160 +        # Open the database connection
  20.161 +        dbconn = None
  20.162 +
  20.163 +        # Specify a basic bundle spec
  20.164 +        # - We always send the payload as a permanent file
  20.165 +        pt = dtnapi.DTN_PAYLOAD_FILE
  20.166 +        # - The source address is always the email_addr
  20.167 +        # - The report address is always the report_addr
  20.168 +        # - The registration id needed in dtn_send is the place
  20.169 +        #   where reports come back to.. we always want reports
  20.170 +        #   but it is unclear why we need to subscribe to the 'session'
  20.171 +        #   just to do the send.The reports will come back through
  20.172 +        #   another connection. Lets try with no regid
  20.173 +        regid = dtnapi.DTN_REGID_NONE
  20.174 +        # - The destination address has to be synthesized (later)
  20.175 +        # - We want delivery reports (and maybe deletion reports?)
  20.176 +        dopts = dtnapi.DOPTS_DELIVERY_RCPT
  20.177 +        # - Send with normal priority.
  20.178 +        pri = dtnapi.COS_NORMAL
  20.179 +        # Mail bundlesshould last a while..
  20.180 +        exp = MAIL_EXPIRY
  20.181 +
  20.182 +        self.logdebug("Entering DTN send Queue loop")
  20.183 +
  20.184 +        # Process mail
  20.185 +        # The thread sits waiting for queued events forever.
  20.186 +        # The thread is terminated by sending a special MSG_END event
  20.187 +        while (self.send_run):
  20.188 +            evt = self.send_q.get()
  20.189 +            if evt.is_last_msg():
  20.190 +                break
  20.191 +
  20.192 +            # Build the destination EID
  20.193 +            # The destination may be a list of email addresses if it
  20.194 +            # is going to the gateway but will only be one
  20.195 +            # address if going to another node directly by DTN
  20.196 +            destn = evt.destination()
  20.197 +            self.logdebug("Message for destination(s) %s" % destn)
  20.198 +            if len(destn) == 0:
  20.199 +                self.logerror("No destinations specified: message ignored")
  20.200 +                continue
  20.201 +            if '@' in destn[0]:
  20.202 +                [user, dest_domain] = destn[0].split("@")
  20.203 +            else:
  20.204 +                self.logwarn("Destination %s does not contain a domain name" %
  20.205 +                             destn[0])
  20.206 +                user = destn[0]
  20.207 +                dest_domain = ""
  20.208 +            if dest_domain != self.domain:
  20.209 +                destn_eid = "dtn://gateway.%s/%s" % (self.domain, EMAIL_IN)
  20.210 +            else:
  20.211 +                destn_eid = "dtn://%s.%s/%s" % (user, dest_domain, EMAIL_IN)
  20.212 +
  20.213 +            self.loginfo("Sending message to %s" % destn_eid)
  20.214 +
  20.215 +            # Send the bundle
  20.216 +            bundle_id = dtnapi.dtn_send(dtn_handle, regid, email_out_eid, destn_eid,
  20.217 +                                        report_eid, pri, dopts, exp, pt,
  20.218 +                                        evt.filename(), "", "")
  20.219 +            if bundle_id == None:
  20.220 +                self.logwarn("Sending of message to %s failed" % destn_eid)
  20.221 +            else:
  20.222 +                # Store the details of the sent bundle
  20.223 +                self.loginfo("%s sent at %d, seq no %d" %(bundle_id.source,
  20.224 +                                                          bundle_id.creation_secs,
  20.225 +                                                          bundle_id.creation_seqno))
  20.226 +        dtnapi.dtn_close(dtn_handle)
  20.227 +        self.loginfo("dtn_send exiting")
  20.228 +
  20.229 +# Class for handling incoming email.
  20.230 +# This only deals with incoming email bundles and is bound to
  20.231 +# <local_eid>/email/in
  20.232 +class dtn_receive(threading.Thread):
  20.233 +    def __init__(self, domain, mailbox, recv_q, logger):
  20.234 +        threading.Thread.__init__(self, name="dtn-receive")
  20.235 +        self.domain = domain
  20.236 +        self.mailbox = mailbox
  20.237 +        # recv_q may be None in an outstation
  20.238 +        self.recv_q = recv_q
  20.239 +
  20.240 +        # New mail state counter and lock
  20.241 +        self.mail_state = 0
  20.242 +        self.mail_state_lock = threading.Lock()
  20.243 +
  20.244 +        # Set up logging functions
  20.245 +        self.logger = logger
  20.246 +        self.loginfo = logger.info
  20.247 +        self.logdebug = logger.debug
  20.248 +        self.logerror = logger.error
  20.249 +        self.logwarn = logger.warn
  20.250 +
  20.251 +        # Flag to keep the loop running
  20.252 +        self.receive_run = True
  20.253 +
  20.254 +    def end_run(self):
  20.255 +        self.receive_run = False
  20.256 +
  20.257 +    def get_mail_state(self):
  20.258 +        self.mail_state_lock.acquire()
  20.259 +        res = self.mail_state
  20.260 +        self.mail_state_lock.release()
  20.261 +        return res
  20.262 +        
  20.263 +    def run(self):
  20.264 +        # This function loops forever (well more or less) waiting for
  20.265 +        # incoming report bundles.
  20.266 +        # This requires it to poll the DTN daemon (yawn).
  20.267 +        # The poll periodically drops out in order to check if the thread has
  20.268 +        # been terminated (this may not be necessary - check if threads hung
  20.269 +        # up in select terminate naturally if the thread is terminated because
  20.270 +        # it is a daemon (but the check is cleaner really).
  20.271 +        
  20.272 +        # Create a connection to the DTN daemon
  20.273 +        dtn_handle = dtnapi.dtn_open()
  20.274 +        if dtn_handle == -1:
  20.275 +            raise DtnError("unable to open connection with daemon")
  20.276 +
  20.277 +        # Generate the EIDs
  20.278 +        (email_out_eid, email_in_eid, report_eid) = build_eids(dtn_handle)
  20.279 +
  20.280 +        # Open the database connection
  20.281 +        dbconn = None
  20.282 +
  20.283 +        # Check if email/in registration exists and register if not
  20.284 +        # Otherwise bind to the existing registration
  20.285 +        regid = dtnapi.dtn_find_registration(dtn_handle, email_in_eid)
  20.286 +        if (regid == -1):
  20.287 +            # Need to register the EID.. make it permanent with 'DEFER'
  20.288 +            # characteristics so that bundles are saved if theye arrive
  20.289 +            # while the handler is inactive
  20.290 +            # Expire the registration a long time in the future
  20.291 +            exp = 365 * 24 * 60 * 60
  20.292 +            # The registration is immediately active
  20.293 +            passive = False
  20.294 +            # We don't want to execute a script
  20.295 +            script = ""
  20.296 +            
  20.297 +            regid = dtnapi.dtn_register(dtn_handle, email_in_eid, dtnapi.DTN_REG_DEFER,
  20.298 +                                        exp, passive, script)
  20.299 +        else:
  20.300 +            dtnapi.dtn_bind(dtn_handle, regid)
  20.301 +
  20.302 +        # Wait for 30 seconds before looping
  20.303 +        recv_timeout = 30
  20.304 +
  20.305 +        self.loginfo("Entering email receive loop")
  20.306 +
  20.307 +        # Now sit and wait for incoming email
  20.308 +        # Note that just using dtn_recv with a timeout doesn't work.
  20.309 +        # The blocking I/O upsets the threading seemingly.
  20.310 +        receive_fd = dtnapi.dtn_poll_fd(dtn_handle)
  20.311 +        while (self.receive_run):
  20.312 +            # Poll currently sets timeout in ms - this is a bug
  20.313 +            dtnapi.dtn_begin_poll(dtn_handle, 5000)
  20.314 +            # The timeout on select is much longer than the dtn_begin_poll
  20.315 +            # timeout, so there is a problem if there is nothing to read
  20.316 +            rd_fd, wr_fd, err_fd = select([receive_fd], [], [], 20)
  20.317 +            if (len(rd_fd) != 1) or (rd_fd[0] != receive_fd):
  20.318 +                # Cancel the poll anyway
  20.319 +                dtnapi.dtn_cancel_poll(dtn_handle)
  20.320 +                raise DtnError("Report select call timed out")
  20.321 +            
  20.322 +            """
  20.323 +            There should always be something to read
  20.324 +            Put in a timeout just in case
  20.325 +            The call to dtn_recv terminates the poll
  20.326 +            We accept the email as a (temporary) file.
  20.327 +            There is a nasty snag here. There is small window
  20.328 +            of opportunity where the bundle has been written
  20.329 +            to a temporary file delivered to this application
  20.330 +            but before file has been renamed and registered in the maildir
  20.331 +            and database. If the application exits during this period
  20.332 +            the email will be lost.  This is not a happy situation.
  20.333 +            At present DTN2 does not have a means for dtn_recv to offer
  20.334 +            a filename that could be used.
  20.335 +            
  20.336 +            Uing DTN_PAYLOAD_MEM would present a similar problem
  20.337 +            During the period the memory image is being written to file
  20.338 +            in the application there is no permanent  copy of the file
  20.339 +            but the DTN daemon believes it has delivered and is busy sending
  20.340 +            a delivery notification.
  20.341 +
  20.342 +            NOTE: On receiving the file, the file name is in the bundle
  20.343 +            payload as a NULL terminated string.  Python leaves the terminating
  20.344 +            byte in place.
  20.345 +            """
  20.346 +            email = dtnapi.dtn_recv(dtn_handle, dtnapi.DTN_PAYLOAD_FILE, 1)
  20.347 +            if email != None:
  20.348 +                fn = email.payload
  20.349 +                l = len(fn)
  20.350 +                if fn[l-1] == "\x00":
  20.351 +                    fn = fn[:-1]
  20.352 +                self.logdebug("Got incoming bundle in file %s" % fn)
  20.353 +
  20.354 +                # Insert new message file  into the inbox
  20.355 +                final_fn = self.mailbox.newMessageFile(fn)
  20.356 +                if final_fn == None:
  20.357 +                    raise DtnError("Cannot store new mail in inbox")
  20.358 +
  20.359 +                self.loginfo("Received email bundle from %s sent %d seq %d" %
  20.360 +                             (email.source,
  20.361 +                              email.creation_secs,
  20.362 +                              email.creation_seqno))
  20.363 +                self.loginfo("New email stored as %s" % final_fn)
  20.364 +                
  20.365 +                # Update database
  20.366 +
  20.367 +                # When this is used in a gateway there will be a recv_q
  20.368 +                # where this can be sent off to be fed to Postfix
  20.369 +                # In an outstation, all that needs to be done is flag
  20.370 +                # that a new message has been added to the maildir
  20.371 +                # In practice this may be redundant. Not sure how POP3
  20.372 +                # deals with new mail arriving during a session.
  20.373 +                if self.recv_q != None:                             
  20.374 +                    # Put message on the queue to send it on to Postfix
  20.375 +                    evt = msg_send_evt(MSG_SMTP, final_fn, MSG_NEW, "smtp server")
  20.376 +                    self.recv_q.put_nowait(evt)
  20.377 +
  20.378 +                # Increment the state variable - locking while we do
  20.379 +                self.mail_state_lock.acquire()
  20.380 +                self.mail_state += 1
  20.381 +                self.mail_state_lock.release()
  20.382 +                    
  20.383 +            elif dtnapi.dtn_errno(dtn_handle) != dtnapi.DTN_ETIMEOUT:
  20.384 +                raise DtnError("Report: dtn_recv failed with error code %d" %
  20.385 +                               dtnapi.dtn_errno(dtn_handle))
  20.386 +            else:
  20.387 +                pass
  20.388 +                               
  20.389 +        dtnapi.dtn_close(dtn_handle)
  20.390 +        self.loginfo("dtn_receive exiting")
  20.391 +        
  20.392 +# Class for handling incoming reports.
  20.393 +# This only deals with incoming report bundles and is bound to
  20.394 +# <local_eid>/email/reports
  20.395 +class dtn_report(threading.Thread):
  20.396 +    def __init__(self, domain, logger):
  20.397 +        threading.Thread.__init__(self, name="dtn-report")
  20.398 +        self.domain = domain
  20.399 +
  20.400 +        # Set up logging functions
  20.401 +        self.logger = logger
  20.402 +        self.loginfo = logger.info
  20.403 +        self.logdebug = logger.debug
  20.404 +        self.logerror = logger.error
  20.405 +        self.logwarn = logger.warn
  20.406 +
  20.407 +        # Flag to keep the loop running
  20.408 +        self.report_run = True
  20.409 +
  20.410 +    def end_run(self):
  20.411 +        self.report_run = False
  20.412 +        
  20.413 +    def run(self):
  20.414 +        # This function loops forever (well more or less) waiting for
  20.415 +        # incoming report bundles.
  20.416 +        # This requires it to poll the DTN daemon (yawn).
  20.417 +        # The poll periodically drops out in order to check if the thread has
  20.418 +        # been terminated (this may not be necessary - check if threads hung
  20.419 +        # up in select terminate naturally if the thread is terminated because
  20.420 +        # it is a daemon (but the check is cleaner really).
  20.421 +        
  20.422 +        # Create a connection to the DTN daemon
  20.423 +        dtn_handle = dtnapi.dtn_open()
  20.424 +        if dtn_handle == -1:
  20.425 +            raise DtnError("unable to open connection with daemon")
  20.426 +
  20.427 +        # Generate the EIDs
  20.428 +        (email_out_eid, email_in_eid, report_eid) = build_eids(dtn_handle)
  20.429 +        
  20.430 +        # Open the database connection
  20.431 +        dbconn = None
  20.432 +
  20.433 +        # Check if email/report registration exists and register if not
  20.434 +        # Otherwise bind to the existing registration
  20.435 +        regid = dtnapi.dtn_find_registration(dtn_handle, report_eid)
  20.436 +        if (regid == -1):
  20.437 +            # Need to register the EID.. make it permanent with 'DEFER'
  20.438 +            # characteristics so that bundles are saved if theye arrive
  20.439 +            # while the handler is inactive
  20.440 +            # Expire the registration a long time in the future
  20.441 +            exp = 365 * 24 * 60 * 60
  20.442 +            # The registration is immediately active
  20.443 +            passive = False
  20.444 +            # We don't want to execute a script
  20.445 +            script = ""
  20.446 +            
  20.447 +            regid = dtnapi.dtn_register(dtn_handle, report_eid, dtnapi.DTN_REG_DEFER,
  20.448 +                                        exp, passive, script)
  20.449 +        else:
  20.450 +            dtnapi.dtn_bind(dtn_handle, regid)
  20.451 +
  20.452 +        # Wait for 30 seconds before looping
  20.453 +        recv_timeout = 30
  20.454 +
  20.455 +        self.loginfo("Entering DTN report recv loop")
  20.456 +
  20.457 +        # Now sit and wait for incoming reports
  20.458 +        # Note that just using dtn_recv with a timeout doesn't work.
  20.459 +        # The blocking I/O upsets the threading seemingly.
  20.460 +        report_fd = dtnapi.dtn_poll_fd(dtn_handle)
  20.461 +        while (self.report_run):
  20.462 +            # Poll currently sets timeout in ms - this is a bug
  20.463 +            dtnapi.dtn_begin_poll(dtn_handle, 5000)
  20.464 +            # The timeout on select is much longer than the dtn_begin_poll
  20.465 +            # timeout, so there is a problem if there is nothing to read
  20.466 +            rd_fd, wr_fd, err_fd = select([report_fd], [], [], 20)
  20.467 +            if (len(rd_fd) != 1) or (rd_fd[0] != report_fd):
  20.468 +                # Cancel the poll anyway
  20.469 +                dtnapi.dtn_cancel_poll(dtn_handle)
  20.470 +                raise DtnError("Report select call timed out")
  20.471 +            
  20.472 +            # There should always be something to read
  20.473 +            # Put in a timeout just in case
  20.474 +            # The call to dtn_recv terminates the poll
  20.475 +            report = dtnapi.dtn_recv(dtn_handle, dtnapi.DTN_PAYLOAD_MEM, 1)
  20.476 +            if report != None:
  20.477 +                self.logdebug("Received bundle")
  20.478 +                if report.status_report != None:
  20.479 +                    self.logdebug("Received status report")
  20.480 +                    if report.status_report.flags == REPORT_STATUS_DELIVERED:
  20.481 +                        self.loginfo( "Received delivery report re from %s sent %d seq %d" %
  20.482 +                                      (report.status_report.bundle_id.source,
  20.483 +                                       report.status_report.bundle_id.creation_secs,
  20.484 +                                       report.status_report.bundle_id.creation_seqno))
  20.485 +                        # Update the database
  20.486 +      
  20.487 +                    elif report.status_report.flags == REPORT_STATUS_DELETED:
  20.488 +                        self.loginfo("Received deletion report re from %s sent %d seq %d" %
  20.489 +                                     (report.status_report.bundle_id.source,
  20.490 +                                      report.status_report.bundle_id.creation_secs,
  20.491 +                                      report.status_report.bundle_id.creation_seqno))
  20.492 +                        # Update the database
  20.493 +
  20.494 +                    else:
  20.495 +                        self.logwarn("Received unexpected report: Flags: %d" % report.status_report.flags)
  20.496 +
  20.497 +            elif dtnapi.dtn_errno(dtn_handle) != dtnapi.DTN_ETIMEOUT:
  20.498 +                self.logerror("dtn_recv failed with error code %d" %
  20.499 +                              dtnapi.dtn_errno(dtn_handle))
  20.500 +                raise DtnError("Report: dtn_recv failed with error code %d" %
  20.501 +                               dtnapi.dtn_errno(dtn_handle))
  20.502 +            else:
  20.503 +                pass
  20.504 +                               
  20.505 +        dtnapi.dtn_close(dtn_handle)
  20.506 +        self.loginfo("dtn_report exiting")
  20.507 +            
  20.508 +# Test code
  20.509 +if __name__ == "__main__":
  20.510 +    logger = logging.getLogger("test")
  20.511 +    logger.setLevel(logging.DEBUG)
  20.512 +    ch = logging.StreamHandler()
  20.513 +    fmt = logging.Formatter("%(levelname)s %(threadName)s %(message)s")
  20.514 +    ch.setFormatter(fmt)
  20.515 +    logger.addHandler(ch)
  20.516 +    
  20.517 +    dtn_send_q = Queue.Queue()
  20.518 +    dtn_recv_q = Queue.Queue()
  20.519 +    nomadic_domain = "nomadic.n4c.eu"
  20.520 +    tmp_domain = pypop_maildir.MaildirDirdbmDomain("/tmp")
  20.521 +    if not tmp_domain.userDirectory("dtnin"):
  20.522 +        tmp_domain.addUser("dtnin", "password")
  20.523 +    dtnin_mailbox = pypop_maildir.MaildirMailbox(tmp_domain.userDirectory("dtnin"),
  20.524 +                                                 "dtnin")
  20.525 +    
  20.526 +    # Sending thread
  20.527 +    dtn_send_handler = dtn_send(nomadic_domain, dtn_send_q, logger)
  20.528 +    dtn_send_handler.setDaemon(True)
  20.529 +    dtn_send_handler.start()
  20.530 +
  20.531 +    # Receiving thread
  20.532 +    dtn_recv_handler = dtn_receive(nomadic_domain, dtnin_mailbox,
  20.533 +                                   dtn_recv_q, logger)
  20.534 +    dtn_recv_handler.setDaemon(True)
  20.535 +    dtn_recv_handler.start()
  20.536 +
  20.537 +    # Report handler thread
  20.538 +    dtn_report_handler = dtn_report(nomadic_domain, logger)
  20.539 +    dtn_report_handler.setDaemon(True)
  20.540 +    dtn_report_handler.start()
  20.541 +
  20.542 +    time.sleep(0.1)
  20.543 +    logger.info("Send handler running: %s" % dtn_send_handler.getName())
  20.544 +    logger.info("Receive handler running: %s" % dtn_recv_handler.getName())
  20.545 +    logger.info("Report handler running: %s" % dtn_report_handler.getName())
  20.546 +
  20.547 +    evt = msg_send_evt(MSG_DTN,
  20.548 +                       "/etc/group",
  20.549 +                       MSG_NEW,
  20.550 +                       ["elwynd@nomadic.n4c.eu"])
  20.551 +    dtn_send_q.put_nowait(evt)
  20.552 +
  20.553 +    logger.info("Waiting for incoming mail")
  20.554 +    evt = dtn_recv_q.get()
  20.555 +    logger.info("Mail received: ", evt.filename())
  20.556 +
  20.557 +    try:
  20.558 +        while True:
  20.559 +            pass
  20.560 +    except:
  20.561 +        evt = msg_send_evt(MSG_END,
  20.562 +                           "",
  20.563 +                           MSG_NEW,
  20.564 +                           "")
  20.565 +        dtn_send_q.put_nowait(evt)
  20.566 +        dtn_recv_handler.end_run()
  20.567 +        dtn_report_handler.end_run()
  20.568 +        time.sleep(1)
  20.569 +        
  20.570 +
  20.571 +        
  20.572 +    
  20.573 +
  20.574 +                                 
  20.575 +        
  20.576 +
  20.577 +         
  20.578 +
  20.579 +        
    21.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    21.2 +++ b/outstation/home/user/dtn/python/dp_msg_send_evt.py	Tue Mar 16 13:36:42 2010 +0000
    21.3 @@ -0,0 +1,99 @@
    21.4 +#! /usr/bin/python
    21.5 +# PyMail DTN Nomadic Mail System
    21.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    21.7 +#
    21.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    21.9 +#    you may not use this file except in compliance with the License.
   21.10 +#    You may obtain a copy of the License at
   21.11 +# 
   21.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   21.13 +# 
   21.14 +#    Unless required by applicable law or agreed to in writing, software
   21.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   21.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   21.17 +#    See the License for the specific language governing permissions and
   21.18 +#    limitations under the License.
   21.19 +#
   21.20 +
   21.21 +
   21.22 +"""
   21.23 +DTN-Postfix interface
   21.24 +
   21.25 +Event object class
   21.26 +
   21.27 +Instances of this object are passed between the various threads of the
   21.28 +system in the send queues to tell the sending threads what work they have
   21.29 +to do.
   21.30 +
   21.31 +Each contains
   21.32 +- the filename of a file that contains the email to send
   21.33 +- a flag indicating if the item is new or a resend
   21.34 +- (for debugging purposes) an indicator of whether DTN or Postfix send is needed
   21.35 +
   21.36 +Revision History
   21.37 +================
   21.38 +Version   Date       Author         Notes
   21.39 +0.1       14/03/2010 Elwyn Davies   License statement added.
   21.40 +0.0	  25/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
   21.41 +"""
   21.42 +import os
   21.43 +
   21.44 +# Whether the message is new or a retry (affects what we do to the database)
   21.45 +MSG_NEW = 0
   21.46 +MSG_RETRY = 1
   21.47 +
   21.48 +# Type send needed - or terminate sending process
   21.49 +MSG_DTN = "dtn_send"
   21.50 +MSG_SMTP = "smtp_send"
   21.51 +MSG_END = "end_send"
   21.52 +
   21.53 +
   21.54 +class msg_send_evt:
   21.55 +    def __init__(self, send_type, filename, new_or_retry, destination):
   21.56 +
   21.57 +        # Check the parameters are valid
   21.58 +        # (note the file *might* change under our feet - no guarantees later)
   21.59 +        if send_type not in (MSG_DTN, MSG_SMTP, MSG_END):
   21.60 +            raise ValueError
   21.61 +        if (send_type != MSG_END):
   21.62 +            if new_or_retry not in (MSG_NEW, MSG_RETRY):
   21.63 +                raise ValueError
   21.64 +            if not os.access(filename, os.R_OK):
   21.65 +                raise ValueError
   21.66 +        else:
   21.67 +            filename = ""
   21.68 +            new_or_retry = MSG_NEW
   21.69 +            destination = ""
   21.70 +        
   21.71 +        self._filename =  filename
   21.72 +        self.new_or_retry = new_or_retry
   21.73 +        self.send_type = send_type
   21.74 +        self._destination = destination
   21.75 +
   21.76 +    def is_new(self):
   21.77 +        return self.new_or_retry == MSG_NEW
   21.78 +
   21.79 +    def is_last_msg(self):
   21.80 +        return self.send_type == MSG_END
   21.81 +
   21.82 +    def filename(self):
   21.83 +        return self._filename
   21.84 +
   21.85 +    def destination(self):
   21.86 +        return self._destination
   21.87 +
   21.88 +    def __repr__(self):
   21.89 +        if self.send_type == MSG_END:
   21.90 +            return "Ending sending."
   21.91 +        else:
   21.92 +            return "%s message in file %s to be sent by %s to %s" % ({ MSG_NEW: "New", MSG_RETRY: "Resending"}[self.new_or_retry],
   21.93 +                                                                     self._filename,
   21.94 +                                                                     self.send_type,
   21.95 +                                                                     self._destination)
   21.96 +
   21.97 +if __name__ == "__main__":
   21.98 +    evt = msg_send_evt(MSG_DTN, "/etc/passwd", MSG_RETRY, "abc@def.ghi")
   21.99 +    print evt
  21.100 +    evt = msg_send_evt(MSG_END, "", MSG_NEW, "")
  21.101 +    print evt
  21.102 +    evt = msg_send_evt(MSG_SMTP, "/etc/passwd", 3, "abd@def.ghi")
    22.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    22.2 +++ b/outstation/home/user/dtn/python/dp_outstation.py	Tue Mar 16 13:36:42 2010 +0000
    22.3 @@ -0,0 +1,227 @@
    22.4 +#! /usr/bin/python
    22.5 +# PyMail DTN Nomadic Mail System
    22.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    22.7 +#
    22.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    22.9 +#    you may not use this file except in compliance with the License.
   22.10 +#    You may obtain a copy of the License at
   22.11 +# 
   22.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   22.13 +# 
   22.14 +#    Unless required by applicable law or agreed to in writing, software
   22.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   22.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   22.17 +#    See the License for the specific language governing permissions and
   22.18 +#    limitations under the License.
   22.19 +#
   22.20 +
   22.21 +"""
   22.22 +This is the controller for the DTN nomadic email system that is loaded into
   22.23 +'outstations' i.e. 'small' systems where there is generallyno connection to
   22.24 +the Internet and there is no way of  making an SMTP connection to the rest of the world
   22.25 +for most of the time.
   22.26 +
   22.27 +It links between an email clinet (user agent) such as Thunderbird or Evolution
   22.28 +and the DTN daemon.
   22.29 +
   22.30 +It provides
   22.31 +- a POP server for incoming email
   22.32 +- an SMTP server for outgoing email
   22.33 +- interfaces to convert between emails and DTN bundles
   22.34 +
   22.35 +It is intended at present to work for a single DTN domain or Communications
   22.36 +Association. An outstation deals with only a single email address at present
   22.37 +due to limitations of the DTN naming system in DTN2 (as of version 2.6.0+), and
   22.38 +the email address is closely mapped to the EID of the node where the outstation
   22.39 +is installed: user@nomadic.n4c.eu <-> dtn://user.nomadic.n4c.eu.
   22.40 +
   22.41 +It stores emails in two maildir structures - one for incoming and one for 
   22.42 +outgoing.Outgoing emails are maintained until either a delivery report is sent
   22.43 +back or a delivery timeout occurs.  If the bundle sent for an email willhave expired
   22.44 +without a delivery report being returned, the bundle is retransmitted
   22.45 +[TBD Does custody do this for us?  Yes.. but only if every node will take custody.]
   22.46 +
   22.47 +We also need to cope with possible incoming duplicates as the same bundle may be
   22.48 +delivered by multiple paths.
   22.49 +
   22.50 +The application is heavily multi-threaded:
   22.51 +- Main thread.. this controller
   22.52 +- One master SMTP server listener thread plus a thread for each outgoing email connection.
   22.53 +- One master POP3 server listener thread plus a thread for each POP3 connection
   22.54 +- Three DTN handler threads dealing with:
   22.55 +  - Sending outgoing emails as bundles
   22.56 +  - Receiving incoming emails as bundles
   22.57 +  - Receiving delivery reports.
   22.58 +- Retransmit and junk disposal management thread
   22.59 +
   22.60 +Communication between threads in the outstation (different from the gateway)
   22.61 +uses a Queue to notify the DTN sender thread of new mails to send out as bundles
   22.62 +(note that in general the bundle daemon will accept custody of the bundle and
   22.63 +send it as and when there is a connection - give or take lack of storage, the
   22.64 +DTN sender will not need to retry sending.  On the incoming side, new messages
   22.65 +are dumped into the dtnin (or user name) maildir mailbox.  This process is
   22.66 +thread safe as the files all get unique names.  The POP3 server thread is
   22.67 +informed that there has been a change and will remake its list next time a
   22.68 +command is processed.  All that is needed is an incrementing counter.  We won't
   22.69 +worry about wrap around at the moment - even the most enthusiastic emailer and
   22.70 +spammers are unlikely to cause a problem  here. The counter is only incremented
   22.71 +by the DTN receive thread and read by the POP3 threads.  It is protected by a
   22.72 +simple Lock. The critical sections will be very short so no need to worry about
   22.73 +deadlock or threads being held off for a long time. The Queue used in the gateway
   22.74 +on the DTN receiving side is not used here - all the communication is effectively
   22.75 +through instances of the maildir mailbox. The counter and the lock are hidden
   22.76 +in the DTN receiver class.
   22.77 +
   22.78 +NOTE:
   22.79 +The SMTP and POP3 server threads make use of the Python SocketServer framework.
   22.80 +Frankly, the code in the Python 2.5 library is complete rubbish.  It is
   22.81 +esentially impossible to set the reuse address flag making (at least) testing
   22.82 +very difficult.  Further it is extremely difficult to shutdown the servers
   22.83 +cleanly.  Most of this is fixed in the Python 2.6 release. Since this module
   22.84 +is pure Python and doesn't do anything v2.6 specific itself, I have extracted
   22.85 +the module and am using for this program.
   22.86 +
   22.87 +Revision History
   22.88 +================
   22.89 +Version   Date       Author         Notes
   22.90 +0.1       14/03/2010 Elwyn Davies   License statement added.
   22.91 +0.0	  25/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
   22.92 +"""
   22.93 +import os
   22.94 +import time
   22.95 +import threading
   22.96 +import Queue
   22.97 +import socket
   22.98 +import SocketServer
   22.99 +import logging
  22.100 +import logging.handlers
  22.101 +import logging.config
  22.102 +
  22.103 +import pypop_maildir
  22.104 +import dp_pop3
  22.105 +import dp_smtpserv
  22.106 +import dp_dtn
  22.107 +from dp_msg_send_evt import *
  22.108 +import dtnapi
  22.109 +
  22.110 +def main(mailbox_root, log_file, log_config_file):
  22.111 +    # Setup logging
  22.112 +    logging.config.fileConfig(log_config_file)
  22.113 +    pymail_logger = logging.getLogger("dtn_pymail")
  22.114 +    loginfo = pymail_logger.info
  22.115 +    logdebug = pymail_logger.debug
  22.116 +
  22.117 +    # Set domain for which we are handling mail
  22.118 +    mail_domain = "nomadic.n4c.eu"
  22.119 +    
  22.120 +    # Check mailboxes dtnin and dtnout  exist and create if not ***TBD***
  22.121 +    nomadic_domain = pypop_maildir.MaildirDirdbmDomain(mailbox_root)
  22.122 +    if not nomadic_domain.userDirectory("dtnin"):
  22.123 +        nomadic_domain.addUser("dtnin", "password")
  22.124 +    if not nomadic_domain.userDirectory("dtnout"):
  22.125 +        nomadic_domain.addUser("dtnout", "password")
  22.126 +    dtnin_mailbox  = pypop_maildir.MaildirMailbox(nomadic_domain.userDirectory("dtnin"),
  22.127 +                                                  "dtnin")
  22.128 +    dtnout_mailbox = pypop_maildir.MaildirMailbox(nomadic_domain.userDirectory("dtnout"),
  22.129 +                                                  "dtnout")
  22.130 +
  22.131 +    # Create queue to send requests for messages to be written out by DTN
  22.132 +    # FIFO queue with no depth limitation at present
  22.133 +    dtn_send_q = Queue.Queue()
  22.134 +
  22.135 +    #====================================================================#
  22.136 +    # Create threads to handle DTN interface
  22.137 +    # There are three threads that desl with:
  22.138 +    # - sending emails
  22.139 +    # - receiving emails
  22.140 +    # - receiving reports
  22.141 +
  22.142 +    # Sending thread
  22.143 +    dtn_send_handler = dp_dtn.dtn_send(mail_domain, dtn_send_q, pymail_logger)
  22.144 +    dtn_send_handler.setDaemon(True)
  22.145 +    dtn_send_handler.start()
  22.146 +
  22.147 +     # Receiving thread
  22.148 +    dtn_receive_handler = dp_dtn.dtn_receive(mail_domain, dtnin_mailbox,
  22.149 +                                             None, pymail_logger)
  22.150 +    dtn_receive_handler.setDaemon(True)
  22.151 +    dtn_receive_handler.start()
  22.152 +    
  22.153 +    # Report handler thread
  22.154 +    dtn_report_handler = dp_dtn.dtn_report(mail_domain, pymail_logger)
  22.155 +    dtn_report_handler.setDaemon(True)
  22.156 +    dtn_report_handler.start()
  22.157 +
  22.158 +    #====================================================================#
  22.159 +    # Create listener for server(s) to handle outgoing mails via SMTP
  22.160 +    smtp_listener = dp_smtpserv.dtn_smtp_server(mail_domain, dtnout_mailbox,
  22.161 +                                                dtn_send_q, pymail_logger)
  22.162 +
  22.163 +    # Start a thread with the listener -- that thread will then start one
  22.164 +    # more thread for each SMTP connection request
  22.165 +    # This is usually one per email, but not necessarily.
  22.166 +    smtp_listener_thread = threading.Thread(target=smtp_listener.serve_forever,
  22.167 +                                            name="SMTP Listener")
  22.168 +
  22.169 +    # Exit the listener thread when the main thread terminates
  22.170 +    smtp_listener_thread.setDaemon(True)
  22.171 +    smtp_listener_thread.start()
  22.172 +
  22.173 +    #====================================================================#
  22.174 +    # Create server to provide POP3 service for mailbox on the incoming
  22.175 +    # side.
  22.176 +    pop3_listener = dp_pop3.dtn_pop3_server(nomadic_domain,
  22.177 +                                            "dtnin",
  22.178 +                                            pymail_logger)
  22.179 +
  22.180 +    # Start a thread with the listener -- that thread will then start one
  22.181 +    # more thread for each POP3 connection request
  22.182 +    pop3_listener_thread = threading.Thread(target=pop3_listener.serve_forever,
  22.183 +                                            name="POP3 Listener")
  22.184 +
  22.185 +    # Exit the listener thread when the main thread terminates
  22.186 +    pop3_listener_thread.setDaemon(True)
  22.187 +    pop3_listener_thread.start()
  22.188 +
  22.189 +    
  22.190 +    #====================================================================#
  22.191 +
  22.192 +    # Let everything have a chance to get going (old trick)
  22.193 +    time.sleep(0.1)
  22.194 +    loginfo("DTN Send handler running in thread: %s" % dtn_send_handler.getName())
  22.195 +    loginfo("DTN Receive handler running in thread: %s" % dtn_receive_handler.getName())
  22.196 +    loginfo("DTN Report handler running in thread: %s" % dtn_report_handler.getName())   
  22.197 +    loginfo("SMTP Listener loop running in thread:%s" % smtp_listener_thread.getName())
  22.198 +    loginfo("POP3 Listener loop running in thread:%s" % pop3_listener_thread.getName())
  22.199 +
  22.200 +    try:
  22.201 +        while True:
  22.202 +            pass
  22.203 +    except:
  22.204 +        # Shutdown
  22.205 +        # Think a bit about ordering here...
  22.206 +        # Shutdown the producer threads first
  22.207 +        dtn_receive_handler.end_run()
  22.208 +        smtp_listener.end_run()
  22.209 +
  22.210 +        # Give them a few seconds to die...
  22.211 +        time.sleep(6)
  22.212 +
  22.213 +        # Then shutdown the consumer threads
  22.214 +        pop3_listener.end_run()
  22.215 +        dtn_report_handler.end_run()
  22.216 +        evt = msg_send_evt(MSG_END,
  22.217 +                           "",
  22.218 +                           MSG_NEW,
  22.219 +                           "")
  22.220 +        dtn_send_q.put_nowait(evt)
  22.221 +
  22.222 +        # ..and wait for them to die...
  22.223 +        time.sleep(2)                                    
  22.224 +        
  22.225 +
  22.226 +if __name__ == "__main__":
  22.227 +    main("/home/user/dtn/maildir", "/media/mmc2/dtn/log", "/home/user/dtn/python/dtn_pymail_log.conf")
  22.228 +    # Test code
  22.229 +
  22.230 +
    23.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    23.2 +++ b/outstation/home/user/dtn/python/dp_pop3.py	Tue Mar 16 13:36:42 2010 +0000
    23.3 @@ -0,0 +1,883 @@
    23.4 +#! /usr/bin/python
    23.5 +# PyMail DTN Nomadic Mail System
    23.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    23.7 +#
    23.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    23.9 +#    you may not use this file except in compliance with the License.
   23.10 +#    You may obtain a copy of the License at
   23.11 +# 
   23.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   23.13 +# 
   23.14 +#    Unless required by applicable law or agreed to in writing, software
   23.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   23.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   23.17 +#    See the License for the specific language governing permissions and
   23.18 +#    limitations under the License.
   23.19 +#
   23.20 +
   23.21 +
   23.22 +# Simple POP3 server
   23.23 +
   23.24 +"""
   23.25 +Post-office Protocol version 3 Server (Threading version) 
   23.26 +
   23.27 +@author: Elwyn Davies
   23.28 +
   23.29 +This is a pretty complete but straightforward implementation of RFC 1939 plus
   23.30 +a few of the options from RFC 2449.
   23.31 +
   23.32 +It implements USER/PASS and APOP 'authentication'.
   23.33 +It implements the basic commands (STAT, LIST, RETR, DELE, NOOP and RSET) plus
   23.34 +the a subset of the optional commands (TOP, UIDL, CAPA, IMPLEMENTATION).
   23.35 +It does not provide SASL, PIPELINING, RESP-CODES, LOGIN-DELAY or EXPIRE.
   23.36 +
   23.37 +In particular the original purpose for this code was to provide an interface
   23.38 +to a local mailbox on a Symbian mobile phone.  The connection would be internal
   23.39 +to the phone which makes the use of SSL and complex authentication superfluous.
   23.40 +I has now moved on in this version to supply a POP server on a general purpose
   23.41 +Linux box connected in a multithreaded environment where other threads will provide
   23.42 +DTN connectivity and an SMTP server.
   23.43 +
   23.44 +It is intended to connect to a maildir mailbox implemented as local files on the
   23.45 +phone so that additional security beyond the access controls on the overall
   23.46 +phone make little sense.
   23.47 +
   23.48 +Because of the usual way that POP3 is used (open a session, see what emails
   23.49 +are listed, retrieve them and delete them in turn and then quit the session)
   23.50 +there is little point (and some difficulty) in trying to inform the POP3
   23.51 +server if new files are added to the maildir during a session.  They are just
   23.52 +picked up on the next session.
   23.53 +
   23.54 +It was written to support implementation of the DTN bundle protocol on the
   23.55 +mobile phone, which would deliver mail messages in bundles and store them in the
   23.56 +maildir.  The POP3 server would allow the files to be viewed through the standard
   23.57 +email client on the phone just like any other mailbox, but it would be local to
   23.58 +the phone (accessed using TCP on localhost). The limitations of the mobile phone
   23.59 +email client make pipelining irrelevant, and because normally there will be only
   23.60 +one user who might have more then one mailbox active but can only single task on
   23.61 +them, LOGIN-DELAY is not useful. 
   23.62 +
   23.63 +The code can support access to multiple mailboxes in parallel if required.  In
   23.64 +principle the mailbox can be accessed from clients off the phone, but it is probably
   23.65 +sensible to limit accees to localhost (127.0.0.1) only.
   23.66 +
   23.67 +The overall structure is based on the SocketServer module in Python with a threading
   23.68 +Listener. There is a listener (normally on port 110) that spawns a server object
   23.69 +in a spearate thread when a connections is made.  There is a single maildomain
   23.70 +with potentially multiple mailboxes owned by different names.  The mailbox is
   23.71 +connected up to the server when there has been an authenticated login.
   23.72 +
   23.73 +TO-DO:
   23.74 +Login timeout.
   23.75 +Restrict login to localhost
   23.76 +
   23.77 +Acknowledgement:
   23.78 +The structure of this software was inspired by the Twisted Python code for the
   23.79 +POP3 server and maildir structure.  It has, however, been hacked so that there
   23.80 +is very little of the original Twisted code left on account of not running with
   23.81 +the Twisted event framework and requiring a leaner solution. 
   23.82 +
   23.83 +Revision History
   23.84 +================
   23.85 +Version   Date       Author         Notes
   23.86 +0.1       14/03/2010 Elwyn Davies   License statement added.
   23.87 +0.0	  25/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
   23.88 +"""
   23.89 +
   23.90 +import string
   23.91 +#import warnings
   23.92 +import socket
   23.93 +import hashlib
   23.94 +import time
   23.95 +import os
   23.96 +import random
   23.97 +from SocketServer2_6 import *
   23.98 +import threading
   23.99 +import logging
  23.100 +import sys
  23.101 +from  select import select
  23.102 +
  23.103 +on_symbian = (sys.platform == "symbian_s60")
  23.104 +
  23.105 +if on_symbian:
  23.106 +    import globalui
  23.107 +
  23.108 +import pypop_maildir
  23.109 +
  23.110 +
  23.111 +class _POP3MessageDeleted(Exception):
  23.112 +    """
  23.113 +    Internal control-flow exception.  Indicates the file of a deleted message
  23.114 +    was requested.
  23.115 +    """
  23.116 +
  23.117 +class POP3Error(Exception):
  23.118 +    pass
  23.119 +
  23.120 +class _IteratorBuffer(object):
  23.121 +    bufSize = 0
  23.122 +
  23.123 +    def __init__(self, write, iterable, memoryBufferSize=None):
  23.124 +        """
  23.125 +        Create a _IteratorBuffer.
  23.126 +
  23.127 +        @param write: A one-argument callable which will be invoked with a list
  23.128 +        of strings which have been buffered.
  23.129 +
  23.130 +        @param iterable: The source of input strings as any iterable.
  23.131 +
  23.132 +        @param memoryBufferSize: The upper limit on buffered string length,
  23.133 +        beyond which the buffer will be flushed to the writer.
  23.134 +        """
  23.135 +        self.lines = []
  23.136 +        self.write = write
  23.137 +        self.iterator = iter(iterable)
  23.138 +        if memoryBufferSize is None:
  23.139 +            memoryBufferSize = 2 ** 16
  23.140 +        self.memoryBufferSize = memoryBufferSize
  23.141 +
  23.142 +
  23.143 +    def __iter__(self):
  23.144 +        return self
  23.145 +
  23.146 +
  23.147 +    def next(self):
  23.148 +        try:
  23.149 +            v = self.iterator.next()
  23.150 +        except StopIteration:
  23.151 +            if self.lines:
  23.152 +                self.write(self.lines)
  23.153 +            # Drop some references, in case they're edges in a cycle.
  23.154 +            del self.iterator, self.lines, self.write
  23.155 +            raise
  23.156 +        else:
  23.157 +            if v is not None:
  23.158 +                self.lines.append(v)
  23.159 +                self.bufSize += len(v)
  23.160 +                if self.bufSize > self.memoryBufferSize:
  23.161 +                    self.write(self.lines)
  23.162 +                    self.lines = []
  23.163 +                    self.bufSize = 0
  23.164 +
  23.165 +
  23.166 +# Cache the hostname (XXX Yes - this is broken)
  23.167 +if False: #platform.isMacOSX():
  23.168 +    # On OS X, getfqdn() is ridiculously slow - use the
  23.169 +    # probably-identical-but-sometimes-not gethostname() there.
  23.170 +    DNSNAME = socket.gethostname()
  23.171 +else:
  23.172 +    DNSNAME = socket.getfqdn()
  23.173 +
  23.174 +def idGenerator():
  23.175 +    i = 0
  23.176 +    while True:
  23.177 +        yield i
  23.178 +        i += 1
  23.179 +
  23.180 +def messageid(uniq=None, N=idGenerator().next):
  23.181 +    """Return a globally unique random string in RFC 2822 Message-ID format
  23.182 +
  23.183 +    <datetime.pid.random@host.dom.ain>
  23.184 +
  23.185 +    Optional uniq string will be added to strenghten uniqueness if given.
  23.186 +    """
  23.187 +    datetime = time.strftime('%Y%m%d%H%M%S', time.gmtime())
  23.188 +    pid = os.getpid()
  23.189 +    rand = random.randrange(2**31L-1)
  23.190 +    if uniq is None:
  23.191 +        uniq = ''
  23.192 +    else:
  23.193 +        uniq = '.' + uniq
  23.194 +
  23.195 +    return '<%s.%s.%s%s.%s@%s>' % (datetime, pid, rand, uniq, N(), DNSNAME)
  23.196 +
  23.197 +
  23.198 +
  23.199 +class FileIterator:
  23.200 +    """
  23.201 +    Takes an open file object and provides a generator which iterates over
  23.202 +    the lines terminating them with "\r\n" plus the terminator line when
  23.203 +    EOF is reached. Move the file out of 'new' into 'cur' after sending.
  23.204 +    """
  23.205 +    def __init__(self, mbox, msg_no, fp):
  23.206 +        self.fp = fp
  23.207 +        self.mbox = mbox
  23.208 +        self.msg_no = msg_no
  23.209 +        self.eof_seen = False
  23.210 +
  23.211 +    def feedLine(self):
  23.212 +        if not self.eof_seen:
  23.213 +            while True:
  23.214 +                buffer = self.fp.readline(1024)
  23.215 +                if buffer != "":
  23.216 +                    if buffer[0] == ".":
  23.217 +                        byte_stuff = "."
  23.218 +                    else:
  23.219 +                        byte_stuff = ""
  23.220 +                    if buffer.endswith("\r\n"):
  23.221 +                        yield byte_stuff + buffer
  23.222 +                    elif buffer.endswith("\n"):
  23.223 +                        yield byte_stuff + buffer[:-1] + "\r\n"
  23.224 +                    else:
  23.225 +                        yield byte_stuff + buffer + "\r\n"
  23.226 +                else:
  23.227 +                    self.eof_seen = True
  23.228 +                    yield ".\r\n"
  23.229 +                    break
  23.230 +        else:
  23.231 +            raise StopIteration
  23.232 +
  23.233 +    def closeFile(self):
  23.234 +        self.fp.close()
  23.235 +        self.mbox.moveNewCur(self.msg_no)
  23.236 +
  23.237 +
  23.238 +class Pop3Handler(BaseRequestHandler):
  23.239 +    """
  23.240 +    POP3 server protocol implementation.
  23.241 +
  23.242 +    @cvar delimiter: The line-ending delimiter to use. By default this is
  23.243 +                     '\\r\\n'.
  23.244 +    @cvar MAX_LENGTH: The maximum length of a line to allow (If a
  23.245 +                      sent line is longer than this, the connection is dropped).
  23.246 +                      Default is 16384.
  23.247 +
  23.248 +    @ivar timeOut: An integer which defines the minimum amount of time which
  23.249 +    may elapse without receiving any traffic after which the client will be
  23.250 +    disconnected.
  23.251 +
  23.252 +    """
  23.253 +
  23.254 +    delimiter = '\r\n'
  23.255 +    listEnder = '.'
  23.256 +    MAX_LENGTH = 16384
  23.257 +
  23.258 +    AUTH_CMDS = ['CAPA', 'USER', 'PASS', 'APOP', 'AUTH', 'RPOP', 'QUIT']
  23.259 +    TRANS_CMDS = ['CAPA', 'STAT', 'LIST', 'RETR', 'DELE', 'NOOP', 'RSET',
  23.260 +                  'TOP', 'LAST', 'UIDL', 'QUIT', 'STOP']
  23.261 +
  23.262 +    def setup(self):
  23.263 +        self.connection = self.request
  23.264 +        self.wfile = self.connection.makefile('wb', 0)
  23.265 +
  23.266 +    def finish(self):
  23.267 +        if not self.wfile.closed:
  23.268 +            self.wfile.flush()
  23.269 +        self.wfile.close()
  23.270 +
  23.271 +    def handle(self):
  23.272 +        self.pop3_thread = threading.currentThread()
  23.273 +        self.pop3_thread.setName("POP3 server - %d" %
  23.274 +                                 self.server.next_server_num)
  23.275 +        self.server.next_server_num += 1
  23.276 +
  23.277 +        # Tell listener we are running
  23.278 +        self.server.add_thread(self)
  23.279 +        
  23.280 +        # Logging functions
  23.281 +        self.loginfo = self.server.logger.info
  23.282 +        self.logdebug = self.server.logger.debug
  23.283 +        self.logwarn = self.server.logger.warn
  23.284 +        self.logerror = self.server.logger.error
  23.285 +
  23.286 +        self.loginfo("New POP3 connection from %s" % self.client_address[0])
  23.287 +
  23.288 +        # Magic number used as nonce for APOP hashing
  23.289 +        self.magic = self.generateMagic()
  23.290 +        self.successResponse(self.magic)
  23.291 +
  23.292 +        # The Domain
  23.293 +        self.maildomain = self.server.domain
  23.294 +
  23.295 +        # The current user
  23.296 +        self.userIs = None
  23.297 +
  23.298 +        #Clear up routine
  23.299 +        self._onLogout = None
  23.300 +
  23.301 +        # Set this pretty low -- POP3 clients are expected to log in, download
  23.302 +        # everything, and log out.
  23.303 +        self.timeOut = 300
  23.304 +
  23.305 +        # Current protocol state
  23.306 +        self.state = "AUTH"
  23.307 +
  23.308 +        # Message index of the highest retrieved message.
  23.309 +        self._highest = 0
  23.310 +
  23.311 +        # Terminates loop when there is no to come on there has been an error
  23.312 +        self.pop3_run = True
  23.313 +
  23.314 +        # Mailbox used
  23.315 +        self.mbox = None
  23.316 +        self.mail_state = 0
  23.317 +
  23.318 +        # Reading buffer
  23.319 +        buf = ""
  23.320 +
  23.321 +        # File descriptor for basuc  socket
  23.322 +        receive_fd = self.request.fileno()
  23.323 +
  23.324 +        # Loop reading lines until EOF
  23.325 +        while self.pop3_run:
  23.326 +            # Check if we have a line buffered up (This is very unlikely in POP3)
  23.327 +            i = buf.find('\n')
  23.328 +            if i == -1:
  23.329 +                # Read some more
  23.330 +                # Drop out periodically to check we haven't been stopped
  23.331 +                rd_fd, wr_fd, err_fd = select([receive_fd], [], [receive_fd], 5)
  23.332 +                if (len(err_fd) > 0) and (err_fd[0] == receive_fd):
  23.333 +                    self.logdebug("Socket closed")
  23.334 +                    self.pop3_run = False
  23.335 +                    continue
  23.336 +                if (len(rd_fd) != 1) or (rd_fd[0] != receive_fd):
  23.337 +                    if len(rd_fd) != 0:
  23.338 +                        self.logerror("Spurious output from select")
  23.339 +                    # Loop round again
  23.340 +                    continue
  23.341 +                more = self.request.recv(1024)
  23.342 +                if more == "":
  23.343 +                    self.logdebug("EOF occurred")
  23.344 +                    self.pop3_run = False
  23.345 +                    continue
  23.346 +                buf += more
  23.347 +                i = buf.find('\n')
  23.348 +                if i == -1:
  23.349 +                    self.logdebug("Stuck with partial line")
  23.350 +                    continue
  23.351 +            if i == len(buf) - 1:
  23.352 +                line = buf
  23.353 +                buf = ""
  23.354 +            else:
  23.355 +                line = buf[:i+1]
  23.356 +                buf = buf[i+1:]
  23.357 +                        
  23.358 +            # Analyse incoming message
  23.359 +            line = line.rstrip(self.delimiter)
  23.360 +            self.logdebug("In state %s Line received: %s" % (self.state, line))
  23.361 +            if len(line) >self.MAX_LENGTH:
  23.362 +                self.connectionLost("Line length exceeded")
  23.363 +                break
  23.364 +
  23.365 +            # Process line
  23.366 +            self.lineReceived(line)
  23.367 +
  23.368 +        # Finished
  23.369 +        self.server.remove_thread(self)
  23.370 +        self.finish()
  23.371 +
  23.372 +    def connectionLost(self, reason):
  23.373 +        if self._onLogout is not None:
  23.374 +            self._onLogout()
  23.375 +            self._onLogout = None
  23.376 +        # self.setTimeout(None)
  23.377 +        self.loginfo("Ending connection: %s" % reason)
  23.378 +        self.pop3_run = False
  23.379 +
  23.380 +    def sendLine(self, line):
  23.381 +        """Sends a line to the other end of the connection.
  23.382 +        """
  23.383 +        self.logdebug("POP3 S: %s" % repr(line))
  23.384 +        if line[0] == self.listEnder[0]:
  23.385 +            byteStuff = self.listEnder
  23.386 +        else:
  23.387 +            byteStuff = ""
  23.388 +        return self.wfile.write(byteStuff + line + self.delimiter)
  23.389 +
  23.390 +    def sendTerminator(self):
  23.391 +        self.logdebug("POP3 S: .")
  23.392 +        return self.wfile.write(self.listEnder + self.delimiter)
  23.393 +
  23.394 +    def lineReceived(self, line):
  23.395 +        self.logdebug("POP3 C: %s" % repr(line))
  23.396 +        # self.resetTimeout()
  23.397 +        getattr(self, 'state_' + self.state)(line)
  23.398 +
  23.399 +    def generateMagic(self):
  23.400 +        return messageid()
  23.401 +
  23.402 +    def successResponse(self, message=''):
  23.403 +        self.sendLine("+OK " + str(message))
  23.404 +
  23.405 +    def failResponse(self, message=''):
  23.406 +        self.sendLine('-ERR ' + str(message))
  23.407 +
  23.408 +    def state_AUTH(self, line):
  23.409 +        try:
  23.410 +            return self.processCommand(self.AUTH_CMDS, *line.split(' '))
  23.411 +        except (ValueError, AttributeError, POP3Error, TypeError), e:
  23.412 +            self.logerror("Bad protocol or server: %s %s" %
  23.413 +                          (e.__class__.__name__, e))
  23.414 +            self.failResponse('bad protocol or server: %s: %s' %
  23.415 +                              (e.__class__.__name__, e))
  23.416 +            return None
  23.417 +
  23.418 +    def state_TRANSACTION(self, line):
  23.419 +        try:
  23.420 +            return self.processCommand(self.TRANS_CMDS, *line.split(' '))
  23.421 +        except (ValueError, AttributeError, POP3Error, TypeError), e:
  23.422 +            if isinstance(e, POP3Error):
  23.423 +                self.failResponse(e)
  23.424 +            else:
  23.425 +                self.logerror("Bad protocol or server: %s %s" %
  23.426 +                              (e.__class__.__name__, e))
  23.427 +                self.failResponse('bad protocol or server: %s: %s' %
  23.428 +                                  (e.__class__.__name__, e))
  23.429 +                return None
  23.430 +
  23.431 +    def processCommand(self, valid_cmds, command, *args):
  23.432 +        command = string.upper(command)
  23.433 +        validCmd = command in valid_cmds
  23.434 +        if not validCmd:
  23.435 +            if self.mbox:
  23.436 +                raise POP3Error("cannot do " + command + " while logged in")
  23.437 +            else:
  23.438 +                raise POP3Error("not authenticated yet: cannot do " + command)
  23.439 +        self.logdebug("Processing command %s" % command)
  23.440 +        f = getattr(self, 'do_' + command, None)
  23.441 +        if f:
  23.442 +            return f(*args)
  23.443 +        raise POP3Error("Unknown protocol command: " + command)
  23.444 +
  23.445 +
  23.446 +    def listCapabilities(self):
  23.447 +        baseCaps = [
  23.448 +            "TOP",
  23.449 +            "USER",
  23.450 +            "UIDL",
  23.451 +            "LAST",
  23.452 +            "IMPLEMENTATION Folly Consulting Python POP3 v0.0"
  23.453 +        ]
  23.454 +
  23.455 +        return baseCaps
  23.456 +
  23.457 +    def do_CAPA(self):
  23.458 +        self.successResponse("I can do the following:")
  23.459 +        for cap in self.listCapabilities():
  23.460 +            self.sendLine(cap)
  23.461 +        self.sendTerminator()
  23.462 +
  23.463 +    def do_AUTH(self, args=None):
  23.464 +        self.failResponse("AUTH extension unsupported")
  23.465 +        return
  23.466 +
  23.467 +    def do_APOP(self, user, digest):
  23.468 +
  23.469 +        self.cred = pypop_maildir.APOPCredentials(self.magic, user, digest)
  23.470 +        try:
  23.471 +            # Get the mailbox associated with this user
  23.472 +            # The mailboxes are cached per user
  23.473 +            avatarId = self.maildomain.verifyCredentials(self.cred)
  23.474 +            self.mbox = self.maildomain.requestAvatar(avatarId)
  23.475 +            # Update the message list in case anything new has arrived
  23.476 +            #This has to be done only after a new login
  23.477 +            # Otherwise POP gets confused.
  23.478 +            self.mbox.makeMessageList()
  23.479 +            self._onLogout = self.logout
  23.480 +            self.state = "TRANSACTION"
  23.481 +            self.successResponse('Authentication succeeded')
  23.482 +            self.loginfo("Authenticated login for " + user)
  23.483 +        except pypop_maildir.UnauthorizedLogin:
  23.484 +            self.logwarn("Denied login attempt from " + user)
  23.485 +            self.failResponse("Login failed")
  23.486 +
  23.487 +    def do_USER(self, user):
  23.488 +        # Only dealing with user matched to DTN EID
  23.489 +        if user != self.server.user:
  23.490 +            self.failResponse("User not acceptable")
  23.491 +            return
  23.492 +
  23.493 +        self.userIs = user
  23.494 +        self.successResponse('USER accepted, send PASS')
  23.495 +
  23.496 +    def do_PASS(self, password):
  23.497 +        if self.userIs is None:
  23.498 +            self.failResponse("USER required before PASS")
  23.499 +            return
  23.500 +        user = self.userIs
  23.501 +        self.userIs = None
  23.502 +        self.cred = pypop_maildir.UsernamePassword(user, password)
  23.503 +        try:
  23.504 +            # Get the mailbox associated with this user
  23.505 +            # The mailboxes are cached per user
  23.506 +            avatarId = self.maildomain.verifyCredentials(self.cred)
  23.507 +            self.mbox = self.maildomain.requestAvatar(avatarId)
  23.508 +            # Update the message list in case anything new has arrived
  23.509 +            # This has to be done only after a new login
  23.510 +            # Otherwise POP gets confused.
  23.511 +            self.mbox.makeMessageList()
  23.512 +            self._onLogout = self.logout
  23.513 +            self.state = "TRANSACTION"
  23.514 +            self.successResponse('Authentication succeeded')
  23.515 +            self.loginfo("Authenticated login for " + user)
  23.516 +        except pypop_maildir.UnauthorizedLogin:
  23.517 +            self.logwarn("Denied login attempt from " + user)
  23.518 +            self.failResponse("Login failed")
  23.519 +
  23.520 +
  23.521 +    def do_STAT(self):
  23.522 +        msg_list = self.mbox.listMessages()
  23.523 +        n = 0
  23.524 +        s = 0
  23.525 +        for m in msg_list:
  23.526 +            if m > 0:
  23.527 +                n += 1
  23.528 +                s += m
  23.529 +        self.successResponse("%d %d" %(n, s))
  23.530 +
  23.531 +    def do_LIST(self, i=None):
  23.532 +        if i is None:
  23.533 +            msg_list = self.mbox.listMessages()
  23.534 +            self.successResponse("%d messages" % len(msg_list))
  23.535 +            n = 0
  23.536 +            s = 0
  23.537 +            for m in msg_list:
  23.538 +                n += 1
  23.539 +                if m > 0:
  23.540 +                    self.sendLine("%d %d" %(n, m))
  23.541 +            self.sendTerminator()
  23.542 +        else:
  23.543 +            try:
  23.544 +                i = int(i)
  23.545 +                if i < 1:
  23.546 +                    raise ValueError()
  23.547 +                else:
  23.548 +                    sz = self.mbox.listMessages(i-1)
  23.549 +                    if sz == 0:
  23.550 +                        raise ValueError
  23.551 +                    else:
  23.552 +                        self.successResponse("%d %d" % (i, sz))
  23.553 +            except ValueError:
  23.554 +                self.failResponse("Invalid message-number: %r" % (i,))
  23.555 +
  23.556 +    def do_UIDL(self, i=None):
  23.557 +        if i is None:
  23.558 +            self.successResponse("unique-id listing follows")
  23.559 +            msg_list = self.mbox.listMessages()
  23.560 +            n = 0
  23.561 +            s = 0
  23.562 +            for m in msg_list:
  23.563 +                n += 1
  23.564 +                uidl = self.mbox.getUidl(n - 1)
  23.565 +                self.sendLine("%d %s" % (n, uidl))
  23.566 +            self.sendTerminator()
  23.567 +        else:
  23.568 +            try:
  23.569 +                i = int(i)
  23.570 +                uidl = self.mbox.getUidl(i-1)
  23.571 +                self.successResponse("%d %s" % (i, uidl))
  23.572 +            except ValueError:
  23.573 +                self.failResponse("Invalid message-number: %d" % (i,))
  23.574 +
  23.575 +    def do_TOP(self, i=None, size=None):
  23.576 +        if i == None or size == None:
  23.577 +            self.failResponse("message-id and line count are mandatory for TOP")
  23.578 +            return
  23.579 +        try:
  23.580 +            size = int(size)
  23.581 +        except:
  23.582 +            self.failResponse("TOP - line count must be an integer")
  23.583 +        else:
  23.584 +            if size < 0:
  23.585 +                self.failResponse("TOP - line count must be positive")
  23.586 +                return
  23.587 +            try:
  23.588 +                i = int(i)
  23.589 +                f = self.mbox.getMessage(i-1)
  23.590 +            except:
  23.591 +                self.failResponse("TOP - bad message number")
  23.592 +            else:
  23.593 +                self.successResponse("Top %d lines of message %d follow" % (size, i))
  23.594 +                done = False
  23.595 +                amount = 1024
  23.596 +                headers = True
  23.597 +                linecount = 0
  23.598 +                buf = ''
  23.599 +                while not done:
  23.600 +                    data = f.read(amount)
  23.601 +                    if data == "":
  23.602 +                        done = True
  23.603 +                        break
  23.604 +                    buf += data
  23.605 +                    if headers:
  23.606 +                        # Look for the blank line between headers and body
  23.607 +                        df, sz = buf.find('\r\n\r\n'), 4
  23.608 +                        if df == -1:
  23.609 +                            df, sz = buf.find('\n\n'), 2
  23.610 +                        if df != -1:
  23.611 +                            linecount = 1
  23.612 +                            headers = False
  23.613 +                        else:
  23.614 +                            df, sz = buf.rfind('\n'), 1
  23.615 +                        if df != -1:
  23.616 +                            # Send any complete header lines
  23.617 +                            for s in buf[:df].splitlines():
  23.618 +                                self.sendLine(s)
  23.619 +                            df += sz
  23.620 +                            buf = buf[df:]
  23.621 +                    if linecount > 0:
  23.622 +                        self.logdebug("%s" % buf)
  23.623 +                        dsplit = buf.splitlines()
  23.624 +                        if len(dsplit) > 0:
  23.625 +                            self.buf = dsplit[-1]
  23.626 +                        else:
  23.627 +                            self.buf = ""
  23.628 +                        for ln in dsplit[:-1]:
  23.629 +                            if linecount > size:
  23.630 +                                self.done = True
  23.631 +                                break
  23.632 +                            self.sendLine(ln)
  23.633 +                            linecount += 1
  23.634 +                f.close()
  23.635 +                self.sendTerminator()
  23.636 +
  23.637 +    def do_RETR(self, i=None):
  23.638 +        if i == None:
  23.639 +            self.failResponse("message number mandatory for RETR")
  23.640 +        else:
  23.641 +            try:
  23.642 +                i = int(i)
  23.643 +                f = self.mbox.getMessage(i-1)
  23.644 +                self.successResponse("Message follows")
  23.645 +                self._highest = max(self._highest, i)
  23.646 +                eof_seen = False
  23.647 +                while not eof_seen:
  23.648 +                    buffer = f.readline(1024)
  23.649 +                    if buffer != "":
  23.650 +                        if buffer[0] == ".":
  23.651 +                            byte_stuff = "."
  23.652 +                        else:
  23.653 +                            byte_stuff = ""
  23.654 +                        if buffer.endswith("\r\n"):
  23.655 +                            self.wfile.write(byte_stuff + buffer)
  23.656 +                        elif buffer.endswith("\n"):
  23.657 +                            self.wfile.write(byte_stuff + buffer[:-1] + "\r\n")
  23.658 +                        else:
  23.659 +                            self.wfile.write(byte_stuff + buffer + "\r\n")
  23.660 +                    else:
  23.661 +                        eof_seen = True
  23.662 +                        self.wfile.write(".\r\n")
  23.663 +                        f.close()
  23.664 +                        self.mbox.moveNewCur(i-1)
  23.665 +            except:
  23.666 +                self.failResponse("Bad message number")
  23.667 +
  23.668 +    def do_DELE(self, i=None):
  23.669 +        if i == None:
  23.670 +            self.failResponse("message-id mandatory for DELE")
  23.671 +            return
  23.672 +        try:
  23.673 +            i = int(i)-1
  23.674 +            self.mbox.deleteMessage(i)
  23.675 +            self.successResponse()
  23.676 +        except:
  23.677 +            self.failResponse("Delete failed")
  23.678 +
  23.679 +
  23.680 +    def do_NOOP(self):
  23.681 +        """Perform no operation.  Return a success code"""
  23.682 +        self.successResponse("NOOP did not do anything")
  23.683 +
  23.684 +
  23.685 +    def do_RSET(self):
  23.686 +        """Unset all deleted message flags"""
  23.687 +        try:
  23.688 +            self.mbox.undeleteMessages()
  23.689 +        except:
  23.690 +            log.err()
  23.691 +            self.failResponse("unable to restore messages")
  23.692 +        else:
  23.693 +            self._highest = 0
  23.694 +            self.successResponse("messages restored")
  23.695 +
  23.696 +
  23.697 +    def do_LAST(self):
  23.698 +        """
  23.699 +        Return the index of the highest message yet downloaded.
  23.700 +        """
  23.701 +        self.successResponse(self._highest)
  23.702 +
  23.703 +
  23.704 +    def do_RPOP(self, user):
  23.705 +        self.failResponse('permission denied, sucker')
  23.706 +
  23.707 +    def do_QUIT(self):
  23.708 +        if self.mbox:
  23.709 +            try:
  23.710 +                self.mbox.sync()
  23.711 +                self.mbox.moveNewCur()
  23.712 +                self.mbox.cleanTrash()
  23.713 +                self.successResponse("mailbox tidied on QUIT")
  23.714 +                self.mbox=None
  23.715 +                self._onLogout = None
  23.716 +            except:
  23.717 +                self.failResponse("problem encountered during QUIT")
  23.718 +            finally:
  23.719 +                self.mbox = None
  23.720 +        else:
  23.721 +            self.successResponse("connection dropped when not logged in")
  23.722 +        self.connectionLost("Finished")
  23.723 +
  23.724 +    def do_STOP(self):
  23.725 +        # Non-standard command.  This causes the whole server to shutdown.
  23.726 +        # It is needed as a way of terminating the server in the mobile phone
  23.727 +        # environment.  We use the coarse debugging function, although it will
  23.728 +        # be a reasonably graceful shutdown.
  23.729 +        if on_symbian:
  23.730 +            globalui.global_note(u"POP3 server terminating")
  23.731 +        else:
  23.732 +            self.loginfo("POP3 server terminating")        
  23.733 +
  23.734 +    def logout(self):
  23.735 +        if self.mbox:
  23.736 +            self.mbox.sync()
  23.737 +
  23.738 +class Pop3Listener(ThreadingMixIn, TCPServer):
  23.739 +
  23.740 +    def __init__(self, addr, handler, 
  23.741 +                 domain, user, logger):        
  23.742 +        # These are used  by individual requests
  23.743 +        # accessed via self.server in the handle function
  23.744 +        self.domain = domain
  23.745 +        self.user = user
  23.746 +        self.logger = logger
  23.747 +        self.running_threads = set()
  23.748 +        self.next_server_num = 1
  23.749 +
  23.750 +        # Setup to produce a daemon thread for each incoming request
  23.751 +        TCPServer.__init__(self, addr, handler,
  23.752 +                           bind_and_activate=False)
  23.753 +        self.allow_reuse_address = True
  23.754 +        self.server_bind()
  23.755 +        self.server_activate()
  23.756 +                         
  23.757 +        self.daemon_threads = True
  23.758 +
  23.759 +    def add_thread(self, thread):
  23.760 +        self.logger.debug("New thread added")
  23.761 +        self.running_threads.add(thread)
  23.762 +
  23.763 +    def remove_thread(self, thread):
  23.764 +        if thread in self.running_threads:
  23.765 +            self.running_threads.remove(thread)
  23.766 +
  23.767 +    def end_run(self):
  23.768 +        for thread in self.running_threads:
  23.769 +            if thread.pop3_thread.isAlive():
  23.770 +                thread.connectionLost("Listener shutting down")
  23.771 +        del self.running_threads
  23.772 +        self.shutdown()
  23.773 +
  23.774 +def dtn_pop3_server(domain, user, logger):
  23.775 +    HOST, PORT = "localhost", 2110
  23.776 +    return Pop3Listener((HOST, PORT), Pop3Handler,
  23.777 +                        domain, user, logger)
  23.778 +# Test code
  23.779 +if __name__ == "__main__":
  23.780 +    import sys
  23.781 +    
  23.782 +    logger = logging.getLogger("test")
  23.783 +    logger.setLevel(logging.DEBUG)
  23.784 +    ch = logging.StreamHandler()
  23.785 +    fmt = logging.Formatter("%(levelname)s %(threadName)s %(message)s")
  23.786 +    ch.setFormatter(fmt)
  23.787 +    logger.addHandler(ch)
  23.788 +    
  23.789 +    pop3Domain = pypop_maildir.MaildirDirdbmDomain("/tmp",
  23.790 +                                                   postmaster=0)
  23.791 +    if not pop3Domain.userDirectory("dtnin"):
  23.792 +        pop3Domain.addUser("dtnin", "password")
  23.793 +        
  23.794 +    server = Pop3Listener(("localhost", 2110), Pop3Handler,
  23.795 +                          pop3Domain, "dtnin", logger)
  23.796 +
  23.797 +    server_thread = threading.Thread(target=server.serve_forever)
  23.798 +    server_thread.setDaemon(True)
  23.799 +    server_thread.start()
  23.800 +    logger.info("Server running in thread: %s" %server_thread.getName())
  23.801 +
  23.802 +    # Change this to True to just create a standalone POP3 server for testing
  23.803 +    if False:
  23.804 +        try:
  23.805 +            while True:
  23.806 +                pass
  23.807 +        except:
  23.808 +            pass
  23.809 +
  23.810 +        server.end_run()
  23.811 +
  23.812 +    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  23.813 +    sock.connect(("localhost", 2100))
  23.814 +    rfile = sock.makefile("r")
  23.815 +    wfile = sock.makefile("w")
  23.816 +
  23.817 +    def r(ss):
  23.818 +        logger.info("Send %s" % ss)
  23.819 +        wfile.write(ss+"\r\n")
  23.820 +        wfile.flush()
  23.821 +        response = rfile.readline(1024).rstrip("\r\n")
  23.822 +        logger.debug("Receive: %s" % response)
  23.823 +            
  23.824 +    def rm(ss):
  23.825 +        logger.info("Send %s" % ss)
  23.826 +        wfile.write(ss+"\r\n")
  23.827 +        wfile.flush()
  23.828 +        response = rfile.readline(1024).rstrip("\r\n")
  23.829 +        logger.debug("Receive: %s" % response)
  23.830 +        if response[:4]  == "+OK ":
  23.831 +            logger.debug("Receiving multi-line response:")
  23.832 +            while True:
  23.833 +                response = rfile.readline(1024).rstrip("\r\n")
  23.834 +                logger.debug("    %s" % response)
  23.835 +                if response == ".":
  23.836 +                    break
  23.837 +        else:
  23.838 +            logger.debug("Received: %s", resp)
  23.839 +            
  23.840 +    
  23.841 +    resp = rfile.readline(1024).rstrip("\r\n")
  23.842 +    logger.info("Startup string: %s" % resp)
  23.843 +    magic = resp[4:]
  23.844 +    logger.debug(magic)
  23.845 +    m = hashlib.md5()
  23.846 +
  23.847 +    m.update(magic + "password")
  23.848 +    dtnin_digest = m.hexdigest()
  23.849 +
  23.850 +    rm("CAPA")
  23.851 +    r("APOP elwynd " + dtnin_digest)
  23.852 +    r("USER elwynd")
  23.853 +    r("PASS password")
  23.854 +    r("STAT")
  23.855 +    rm("LIST")
  23.856 +    r("LIST 2")
  23.857 +    r("LIST 245")
  23.858 +    r("LAST")
  23.859 +    r("RETR")
  23.860 +    rm("RETR 2")
  23.861 +    rm("RETR 345")
  23.862 +    r("LAST")
  23.863 +    rm("UIDL")
  23.864 +    r("UIDL 5")
  23.865 +    r("UIDL -2")
  23.866 +    rm("TOP 3 2")
  23.867 +    rm("TOP")
  23.868 +    rm("TOP 34")
  23.869 +    rm("TOP 3 a")
  23.870 +    rm("TOP b 5")
  23.871 +    rm("TOP 3 -1")
  23.872 +    r("DELE")
  23.873 +    r("DELE 675")
  23.874 +    r("STAT")
  23.875 +    r("DELE 2")
  23.876 +    r("STAT")
  23.877 +    r("RSET")
  23.878 +    r("DELE 2")
  23.879 +    r("STAT")
  23.880 +    r("LAST")
  23.881 +    r("QUIT")
  23.882 +
  23.883 +    server.end_run()
  23.884 +        
  23.885 +
  23.886 +        
    24.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    24.2 +++ b/outstation/home/user/dtn/python/dp_smtpserv.py	Tue Mar 16 13:36:42 2010 +0000
    24.3 @@ -0,0 +1,512 @@
    24.4 +#! /usr/bin/python
    24.5 +# PyMail DTN Nomadic Mail System
    24.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    24.7 +#
    24.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    24.9 +#    you may not use this file except in compliance with the License.
   24.10 +#    You may obtain a copy of the License at
   24.11 +# 
   24.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   24.13 +# 
   24.14 +#    Unless required by applicable law or agreed to in writing, software
   24.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   24.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   24.17 +#    See the License for the specific language governing permissions and
   24.18 +#    limitations under the License.
   24.19 +#
   24.20 +
   24.21 +
   24.22 +"""
   24.23 +SMTP server intended for use in mail client nodes that will be passing
   24.24 +messages to DTN for conveyance to the gateway or another nomadic node.
   24.25 +
   24.26 +The server implements a minimal SMTP (not ESMTP - consider use of DSN later)
   24.27 +protocol server
   24.28 +
   24.29 +Received messages have an X-DTN-rcpts header added containing the set of
   24.30 +recipients to which the message is being sent.
   24.31 +
   24.32 +If any of the recipients is in the DTN domain, the message is forked.
   24.33 +One copy is sent to each DTN domain dcipient over DTN,and any other
   24.34 +recipients are handled by sending the message to the gateway.
   24.35 +
   24.36 +Each copy of the message is stored in a maildir file  - the lines are terminated with \n
   24.37 +
   24.38 +Revision History
   24.39 +================
   24.40 +Version   Date       Author         Notes
   24.41 +0.1       14/03/2010 Elwyn Davies   License statement added.
   24.42 +0.0	  25/07/2009 Elwyn Davies   Created for N4C Summer tests 2009
   24.43 +"""
   24.44 +
   24.45 +import os
   24.46 +import sys
   24.47 +import time
   24.48 +import socket
   24.49 +from SocketServer2_6 import *
   24.50 +import threading
   24.51 +import logging
   24.52 +import Queue
   24.53 +from select import select
   24.54 +
   24.55 +import pypop_maildir
   24.56 +from dp_msg_send_evt import *
   24.57 +
   24.58 +__version__ = "SMTP to DTN server - Python - v0.1"
   24.59 +
   24.60 +class SmtpBundleFile:
   24.61 +    # Container for mailbox mail object and destination
   24.62 +    def __init__(self, mail_msg, destn):
   24.63 +        # Maildir message instance
   24.64 +        self.msg = mail_msg
   24.65 +        # List of destinations
   24.66 +        self.destn = destn
   24.67 +
   24.68 +
   24.69 +class SMTPMessageHandler(BaseRequestHandler):
   24.70 +    """.
   24.71 +    client_address is a tuple containing (ipaddr, port) of the client that made the
   24.72 +    socket connection to our smtp port.
   24.73 +
   24.74 +    mailfrom is the raw address the client claims the message is coming
   24.75 +    from.
   24.76 +
   24.77 +    rcpttos is a list of raw addresses the client wishes to deliver the
   24.78 +    message to.
   24.79 +
   24.80 +    data is a string containing the entire full text of the message,
   24.81 +    headers (if supplied) and all.  It has been `de-transparencied'
   24.82 +    according to RFC 821, Section 4.5.2.  In other words, a line
   24.83 +    containing a `.' followed by other text has had the leading dot
   24.84 +    removed.
   24.85 +
   24.86 +    This function should return None, for a normal `250 Ok' response;
   24.87 +    otherwise it returns the desired response string in RFC 821 format.
   24.88 +
   24.89 +    """
   24.90 + 
   24.91 +    lineseps = "\r\n"
   24.92 +    UNIXFROM = "From "
   24.93 +    UNIXFROM_LEN = len(UNIXFROM)
   24.94 +
   24.95 +
   24.96 +    # SMTP states
   24.97 +    COMMAND = 0
   24.98 +    DATA = 1
   24.99 +
  24.100 +    def setup(self):
  24.101 +        self.connection = self.request
  24.102 +        self.wfile = self.connection.makefile('wb', 0)
  24.103 +
  24.104 +    def finish(self):
  24.105 +        if not self.wfile.closed:
  24.106 +            self.wfile.flush()
  24.107 +        self.wfile.close()
  24.108 +            
  24.109 +
  24.110 +    def close_smtp_handler(self):
  24.111 +        self.smtp_run = False
  24.112 +
  24.113 +    def handle(self):
  24.114 +        self.smtp_thread = threading.currentThread()
  24.115 +        self.smtp_thread.setName("SMTP server - %d" %
  24.116 +                                 self.server.next_server_num)
  24.117 +        self.server.next_server_num += 1
  24.118 +
  24.119 +        # Record this thread with the server
  24.120 +        self.server.add_thread(self)
  24.121 +
  24.122 +        self.smtp_run= True
  24.123 +
  24.124 +        self.loginfo = self.server.logger.info
  24.125 +        self.logdebug = self.server.logger.debug
  24.126 +        self.logwarn = self.server.logger.warn
  24.127 +        self.logerror = self.server.logger.error
  24.128 +
  24.129 +        self.fqdn = socket.getfqdn()
  24.130 +        conn_peer = self.request.getpeername()
  24.131 +
  24.132 +        self.logdebug("New SMTP connection from %s" % self.client_address[0])
  24.133 +
  24.134 +        # Only accept connection from localhost
  24.135 +        if self.client_address[0] == "127.0.0.1":
  24.136 +            # Write greeting
  24.137 +            self.logdebug("Greeting sent %s %s" % (self.fqdn, __version__))
  24.138 +            self.wfile.write('220 %s %s\r\n' % (self.fqdn, __version__))
  24.139 +        else:
  24.140 +            # Repulse overtures
  24.141 +            self.loginfo("Connection from %s refused" % self.client_address[0])
  24.142 +            self.wfile.write('554 Error: Connection Refused\r\n')
  24.143 +            self.request.close()
  24.144 +            return
  24.145 +            
  24.146 +        # Loop processing incoming messages
  24.147 +        while (self.smtp_run):
  24.148 +
  24.149 +            # Setup handler state
  24.150 +            self.state = self.COMMAND
  24.151 +            self.greeting = 0
  24.152 +            self.mailfrom = None
  24.153 +            self.rcpttos = []
  24.154 +            msg_data = ''
  24.155 +            self.msgs = []
  24.156 +            first_data = True
  24.157 +            receive_fd = self.request.fileno()
  24.158 +            buf =""
  24.159 +
  24.160 +            # Loop processing lines within a message
  24.161 +            try:
  24.162 +                while(self.smtp_run):
  24.163 +                    i = buf.find('\n')
  24.164 +                    if i == -1:
  24.165 +                       # Read some more
  24.166 +                        # Drop out periodically to check we haven't been stopped
  24.167 +                        rd_fd, wr_fd, err_fd = select([receive_fd], [], [receive_fd], 5)
  24.168 +                        if (len(err_fd) > 0) and (err_fd[0] == receive_fd):
  24.169 +                            self.logdebug("Socket closed")
  24.170 +                            self.smtp_run = False
  24.171 +                            continue
  24.172 +                        if (len(rd_fd) != 1) or (rd_fd[0] != receive_fd):
  24.173 +                            if len(rd_fd) != 0:
  24.174 +                                self.logerror("Spurious output from select")
  24.175 +                            # Loop round again
  24.176 +                            continue
  24.177 +                        more = self.request.recv(1024)
  24.178 +                        if more == "":
  24.179 +                            self.logdebug("EOF occurred")
  24.180 +                            self.smtp_run = False
  24.181 +                            continue
  24.182 +                        buf += more
  24.183 +                        i = buf.find('\n')
  24.184 +                        if i == -1:
  24.185 +                            self.logdebug("Stuck with partial line")
  24.186 +                            continue
  24.187 +                    if i == len(buf) - 1:
  24.188 +                        msg_data = buf
  24.189 +                        buf = ""
  24.190 +                    else:
  24.191 +                        msg_data = buf[:i+1]
  24.192 +                        buf = buf[i+1:]
  24.193 +                        
  24.194 +                    # Analyse incoming message
  24.195 +                    msg_data = msg_data.rstrip(self.lineseps)
  24.196 +                    self.logdebug("In state %d Line received: %s" % (self.state, msg_data))
  24.197 +                    if self.state == self.COMMAND:
  24.198 +                        if msg_data == "":
  24.199 +                            self.wfile.write("500 Error: bad syntax\r\n")
  24.200 +                            break
  24.201 +                        i = msg_data.find(" ")
  24.202 +                        if i < 0:
  24.203 +                            command = msg_data.upper()
  24.204 +                            arg = None
  24.205 +                        else:
  24.206 +                            command = msg_data[:i].upper()
  24.207 +                            arg = msg_data[i+1:].strip()
  24.208 +                        method = getattr(self, 'smtp_' + command, None)
  24.209 +                        if not method:
  24.210 +                            self.wfile.write("502 Error: command '%s' not implemented\r\n" % command)
  24.211 +                            break
  24.212 +                        self.logdebug("Executing command: %s %s" % (command, arg))
  24.213 +                        method(arg)
  24.214 +                    elif self.state == self.DATA:
  24.215 +                        # We will have a list of messages under construction
  24.216 +                        # Write each line to each mess until there is a line with just a "." 
  24.217 +                        if msg_data != ".":
  24.218 +                            # Remove 'quoting' for lines starting with '.'
  24.219 +                            if msg_data[:2] == "..":
  24.220 +                                msg_data = msg_data[1:]
  24.221 +                            for m in self.msgs:
  24.222 +                                # self.logdebug("Writing to %s %s" % (m.msg, " ".join(m.destn)))
  24.223 +                                if first_data:
  24.224 +                                    # self.logdebug("0")
  24.225 +                                    if msg_data[:self.UNIXFROM_LEN] != self.UNIXFROM:
  24.226 +                                        # Synthesize FROM envelope header
  24.227 +                                        # self.logdebug("1")
  24.228 +                                        # self.logdebug("%s%s %s" % (self.UNIXFROM, self.mailfrom, time.asctime()))
  24.229 +                                        m.msg.lineReceived("%s%s %s" % (self.UNIXFROM, self.mailfrom, time.asctime()))
  24.230 +                                        # Output the received data
  24.231 +                                        # self.logdebug("2")
  24.232 +                                        m.msg.lineReceived(msg_data)
  24.233 +                                    else:
  24.234 +                                        # Reuse existing envelope header
  24.235 +                                        # self.logdebug("3")
  24.236 +                                        m.msg.lineReceived(msg_data)
  24.237 +                                else:
  24.238 +                                    # self.logdebug("4")
  24.239 +                                    m.msg.lineReceived(msg_data)
  24.240 +                            first_data = False
  24.241 +                            # self.logdebug("Next line")
  24.242 +                        else:
  24.243 +                            # Finalize all the messages
  24.244 +                            # self.logdebug("5")
  24.245 +                            for m in self.msgs:
  24.246 +                                if m.msg.eomReceived():
  24.247 +                                    fn = m.msg.getFinalName()
  24.248 +                                    self.logdebug("Finishing message for %s in %s" % (" ".join(m.destn), fn))
  24.249 +                                    try:
  24.250 +                                        evt = msg_send_evt(MSG_DTN, fn, MSG_NEW, m.destn)
  24.251 +                                        self.server.send_queue.put_nowait(evt)
  24.252 +                                    except QueueFull:
  24.253 +                                        # The periodic thread will pick up new messages later
  24.254 +                                        pass
  24.255 +                                    finally:
  24.256 +                                        pass
  24.257 +                            # Finished this message - get ready for next one
  24.258 +                            self.msgs = []
  24.259 +                            self.wfile.write('250 Ok\r\n')
  24.260 +                            self.logdebug("Finished message")
  24.261 +                            break
  24.262 +                    else:
  24.263 +                        self.logerror("SMTP state has illegal value: %d" % self.state)
  24.264 +                        self.wfile.write("451 Internal confusion - bad state value\r\n")
  24.265 +                        self.smtp_run = False
  24.266 +                        self.request.close()
  24.267 +                        break
  24.268 +                # End of while loop - ready for next command or connection close
  24.269 +            except:
  24.270 +                self.logdebug("Exception %s" % sys.exc_info()[0])
  24.271 +                print sys.exc_info()
  24.272 +                if not self.smtp_run:
  24.273 +                    self.request.close()
  24.274 +            finally:
  24.275 +                if len(self.msgs) > 0:
  24.276 +                    for m in self.msgs:
  24.277 +                        m.msg.connectionLost()
  24.278 +            self.logdebug("Next iteration")
  24.279 +        self.server.remove_thread(self)
  24.280 +        self.finish()
  24.281 +        return
  24.282 +
  24.283 +    # SMTP and ESMTP commands
  24.284 +    def smtp_HELO(self, arg):
  24.285 +        if not arg:
  24.286 +            self.wfile.write('501 Syntax: HELO hostname\r\n')
  24.287 +            return
  24.288 +        if self.greeting:
  24.289 +            self.wfile.write('503 Duplicate HELO/EHLO\r\n')
  24.290 +        else:
  24.291 +            self.greeting = arg
  24.292 +            self.wfile.write('250 %s\r\n' % self.fqdn)
  24.293 +
  24.294 +    def smtp_NOOP(self, arg):
  24.295 +        if arg:
  24.296 +            self.wfile.write('501 Syntax: NOOP\r\n')
  24.297 +        else:
  24.298 +            self.wfile.write('250 Ok\r\n')
  24.299 +
  24.300 +    def smtp_QUIT(self, arg):
  24.301 +        # args is ignored
  24.302 +        self.wfile.write('221 Bye\r\n')
  24.303 +        self.wfile.flush()
  24.304 +        self.request.close()
  24.305 +        self.smtp_run = False
  24.306 +
  24.307 +    # used for both MAIL and RCPT
  24.308 +    def __getaddr(self, keyword, arg):
  24.309 +        address = None
  24.310 +        keylen = len(keyword)
  24.311 +        if arg[:keylen].upper() == keyword:
  24.312 +            address = arg[keylen:].strip()
  24.313 +            if not address:
  24.314 +                pass
  24.315 +            elif address[0] == '<' and address[-1] == '>' and address != '<>':
  24.316 +                # Addresses can be in the form <person@dom.com> but watch out
  24.317 +                # for null address, e.g. <>
  24.318 +                address = address[1:-1]
  24.319 +        self.logdebug("__getaddr returning %s" % address)
  24.320 +        return address
  24.321 +
  24.322 +    def smtp_MAIL(self, arg):
  24.323 +        self.logdebug('===> MAIL %s' % arg)
  24.324 +        address = self.__getaddr('FROM:', arg)
  24.325 +        if not address:
  24.326 +            self.wfile.write('501 Syntax: MAIL FROM:<address>\r\n')
  24.327 +            return
  24.328 +        if self.mailfrom:
  24.329 +            self.wfile.write('503 Error: nested MAIL command\r\n')
  24.330 +            return
  24.331 +        self.mailfrom = address
  24.332 +        self.logdebug('sender: %s' % self.mailfrom)
  24.333 +        self.wfile.write('250 Ok\r\n')
  24.334 +
  24.335 +    def smtp_RCPT(self, arg):
  24.336 +        self.logdebug('===> RCPT %s ' % arg)
  24.337 +        if not self.mailfrom:
  24.338 +            self.wfile.write('503 Error: need MAIL command\r\n')
  24.339 +            return
  24.340 +        address = self.__getaddr('TO:', arg) if arg else None
  24.341 +        if not address:
  24.342 +            self.wfile.write('501 Syntax: RCPT TO: <address>\r\n')
  24.343 +            return
  24.344 +        self.rcpttos.append(address)
  24.345 +        self.logdebug('recips: %s', " ".join(self.rcpttos))
  24.346 +        self.wfile.write('250 Ok\r\n')
  24.347 +
  24.348 +    def smtp_RSET(self, arg):
  24.349 +        if arg:
  24.350 +            self.wfile.write('501 Syntax: RSET\r\n')
  24.351 +            return
  24.352 +        # Resets the sender, recipients, and data, but not the greeting
  24.353 +        self.mailfrom = None
  24.354 +        self.rcpttos = []
  24.355 +        self.state = self.COMMAND
  24.356 +        self.wfile.write('250 Ok\r\n')
  24.357 +
  24.358 +    def smtp_DATA(self, arg):
  24.359 +        if not self.rcpttos:
  24.360 +            self.wfile.write('503 Error: need RCPT command\r\n')
  24.361 +            return
  24.362 +        if arg:
  24.363 +            self.wfile.write('501 Syntax: DATA\r\n')
  24.364 +            return
  24.365 +        self.state = self.DATA
  24.366 +        self.wfile.write('354 End data with <CR><LF>.<CR><LF>\r\n')
  24.367 +
  24.368 +        # Create message object(s) to write data into
  24.369 +        gateway_rcpts = []
  24.370 +        for r in self.rcpttos:
  24.371 +            if r.endswith("@"+self.server.domain):
  24.372 +                # This should be split off and sent in a separate bundle
  24.373 +                self.loginfo("Bundle to %s" % r)
  24.374 +                msg = self.server.mailbox.newMessage(r)
  24.375 +                self.msgs.append(SmtpBundleFile(msg, [r]))
  24.376 +            else:
  24.377 +                gateway_rcpts.append(r)
  24.378 +        if gateway_rcpts != []:
  24.379 +            self.loginfo("Common bundle to %s" % " ".join(gateway_rcpts))
  24.380 +            msg = self.server.mailbox.newMessage( " ".join(gateway_rcpts))
  24.381 +            self.msgs.append(SmtpBundleFile(msg, gateway_rcpts))           
  24.382 +        
  24.383 +
  24.384 +class DtnSmtpServer(ThreadingMixIn, TCPServer):
  24.385 +
  24.386 +    def __init__(self, addr, handler, domain, mailbox, send_queue, logger):
  24.387 +        # These are used  by individual requests
  24.388 +        # accessed via self.server in the handle function
  24.389 +        self.domain = domain
  24.390 +        self.mailbox = mailbox
  24.391 +        self.send_queue = send_queue
  24.392 +        self.logger = logger
  24.393 +        self.running_threads = set()
  24.394 +        self.next_server_num = 1
  24.395 +
  24.396 +        TCPServer.__init__(self, addr, handler,
  24.397 +                           bind_and_activate = False)
  24.398 +        self.allow_reuse_address = True
  24.399 +        self.server_bind()
  24.400 +        self.server_activate()
  24.401 +
  24.402 +        self.daemon_threads = True
  24.403 +        
  24.404 +    def add_thread(self, thread):
  24.405 +        self.logger.debug("New thread added")
  24.406 +        self.running_threads.add(thread)
  24.407 +
  24.408 +    def remove_thread(self, thread):
  24.409 +        if thread in self.running_threads:
  24.410 +            self.running_threads.remove(thread)
  24.411 +
  24.412 +    def end_run(self):
  24.413 +        for thread in self.running_threads:
  24.414 +            if thread.smtp_thread.isAlive():
  24.415 +                thread.close_smtp_handler()
  24.416 +        del self.running_threads
  24.417 +        self.shutdown()
  24.418 +
  24.419 +def dtn_smtp_server(domain, mailbox, send_queue, logger):
  24.420 +    HOST, PORT = "localhost", 2125
  24.421 +    return DtnSmtpServer((HOST, PORT), SMTPMessageHandler,
  24.422 +                          domain, mailbox, send_queue, logger)
  24.423 +
  24.424 +
  24.425 +if __name__ == "__main__":
  24.426 +
  24.427 +    import smtplib
  24.428 +    import email
  24.429 +
  24.430 +    # Basic test client
  24.431 +    def client(ip, port, message):
  24.432 +        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  24.433 +        sock.connect((ip, port))
  24.434 +        sock.send(message)
  24.435 +        response = sock.recv(1024)
  24.436 +        print "Received: %s" % response
  24.437 +        sock.close()
  24.438 +
  24.439 +    # Port 0 means to select an arbitrary unused port
  24.440 +    HOST, PORT = "localhost", 0
  24.441 +
  24.442 +    logger = logging.getLogger("test")
  24.443 +    logger.setLevel(logging.DEBUG)
  24.444 +    ch = logging.StreamHandler()
  24.445 +    fmt = logging.Formatter("%(levelname)s %(threadName)s %(message)s")
  24.446 +    ch.setFormatter(fmt)
  24.447 +    logger.addHandler(ch)
  24.448 +
  24.449 +    # Create an SMTP client
  24.450 +    smtp_client = smtplib.SMTP()
  24.451 +
  24.452 +    # Create the send queue
  24.453 +    send_q = Queue.Queue()
  24.454 +
  24.455 +    nomadic_domain = "nomadic.n4c.eu"
  24.456 +    tmp_domain = pypop_maildir.MaildirDirdbmDomain("/tmp")
  24.457 +    if not tmp_domain.userDirectory("dtnout"):
  24.458 +        tmp_domain.addUser("dtnout", "password")
  24.459 +    dtnout_mailbox = pypop_maildir.MaildirMailbox(tmp_domain.userDirectory("dtnout"),
  24.460 +                                                 "dtnout")
  24.461 +    
  24.462 +    server = DtnSmtpServer((HOST, PORT), SMTPMessageHandler,
  24.463 +                           nomadic_domain, dtnout_mailbox, send_q, logger)
  24.464 +    ip, port = server.server_address
  24.465 +
  24.466 +    # Start a thread with the server -- that thread will then start one
  24.467 +    # more thread for each request
  24.468 +    server_thread = threading.Thread(target=server.serve_forever)
  24.469 +    # Exit the server thread when the main thread terminates
  24.470 +    server_thread.setDaemon(True)
  24.471 +    server_thread.start()
  24.472 +    logger.info( "Server loop running in thread:%s" % server_thread.getName())
  24.473 +
  24.474 +    # Basic startup tests
  24.475 +    client(ip, port, "Hello World 1")
  24.476 +    client(ip, port, "Hello World 2")
  24.477 +
  24.478 +    # Read in test messages
  24.479 +    fn = open("msg3")
  24.480 +    msg3 = email.message_from_file(fn)
  24.481 +
  24.482 +    # SMTP tests
  24.483 +    resp = smtp_client.connect(ip, port)
  24.484 +    print resp
  24.485 +    logger.info(("SMTP connect replied: %s %s" % resp))
  24.486 +    smtp_client.helo("Tester on localhost")
  24.487 +    logger.info("SMTP greeted with: %s" %smtp_client.helo_resp )
  24.488 +    logger.info("sending msg3 with 1 recipient")
  24.489 +    smtp_client.sendmail("test@nomadic.n4c.eu", "mumble@example.com", msg3.as_string())
  24.490 +    logger.info("sending msg3 with 3 recipients")
  24.491 +    smtp_client.sendmail("test@nomadic.n4c.eu",
  24.492 +                         ["mumble@example.com", "jackass@nomadic.n4c.eu", "a@b.c"],
  24.493 +                         msg3.as_string(True))
  24.494 +
  24.495 +    logger.info("Testing RSET after MAIL")
  24.496 +    resp = smtp_client.mail("a@b.c")
  24.497 +    resp = smtp_client.rset()
  24.498 +    logger.info("Expecting '250 Ok' response from RSET: got %s %s" % resp)
  24.499 +    logger.info("Testing RSET after MAIL and RCPT")
  24.500 +    smtp_client.mail("a@b.c")
  24.501 +    smtp_client.rcpt("d@e.f")
  24.502 +    resp = smtp_client.rset()
  24.503 +    logger.info("Expecting '250 Ok' response from RSET: got %s %s" % resp)
  24.504 +    logger.info("Testing RCPT without preceding MAIL")
  24.505 +    resp = smtp_client.rcpt("d@e.f")
  24.506 +    logger.info("Expecting '503 Error: need MAIL command' response from RCPT: got %s %s" % resp)
  24.507 +    smtp_client.rset()
  24.508 +    logger.info("Closing connection with QUIT")
  24.509 +    resp = smtp_client.quit()
  24.510 +    logger.info("SMTP quit replied (nothing if running in Python 2.5): %s" %resp)
  24.511 +
  24.512 +    while not send_q.empty():
  24.513 +        evt = send_q.get()
  24.514 +        logger.info("Msg to %s" % evt.destination())
  24.515 +    server.end_run()
    25.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    25.2 +++ b/outstation/home/user/dtn/python/dtn_pymail_log.conf	Tue Mar 16 13:36:42 2010 +0000
    25.3 @@ -0,0 +1,44 @@
    25.4 +[loggers]
    25.5 +keys=root,dtn_pymail,dtn_postfix_pipe
    25.6 +
    25.7 +[handlers]
    25.8 +keys=dtn_pymail_handler,dtn_postfix_pipe_handler
    25.9 +
   25.10 +[formatters]
   25.11 +keys=dtn_pymail_formatter
   25.12 +
   25.13 +[logger_root]
   25.14 +level=NOTSET
   25.15 +handlers=dtn_pymail_handler
   25.16 +
   25.17 +[logger_dtn_pymail]
   25.18 +level=NOTSET
   25.19 +handlers=dtn_pymail_handler
   25.20 +propagate=0
   25.21 +qualname=dtn_pymail
   25.22 +
   25.23 +[logger_dtn_postfix_pipe]
   25.24 +level=NOTSET
   25.25 +handlers=dtn_postfix_pipe_handler
   25.26 +propagate=0
   25.27 +qualname=dtn_postfix_pipe
   25.28 +
   25.29 +[handler_dtn_pymail_handler]
   25.30 +class=handlers.TimedRotatingFileHandler
   25.31 +level=NOTSET
   25.32 +formatter=dtn_pymail_formatter
   25.33 +args=("/media/mmc2/dtn/log/dtn_pymail.log", "midnight", 1, 10)
   25.34 +#args=("/home/user/dtn/log/dtn_pymail.log", "midnight", 1, 10)
   25.35 +
   25.36 +[handler_dtn_postfix_pipe_handler]
   25.37 +class=handlers.TimedRotatingFileHandler
   25.38 +level=NOTSET
   25.39 +formatter=dtn_pymail_formatter
   25.40 +args=("/media/mmc2/dtn/log/dtn_postfix_pipe.log", "midnight", 1, 10)
   25.41 +#args=("/home/user/dtn/log/dtn_postfix_pipe.log", "midnight", 1, 10)
   25.42 +
   25.43 +[formatter_dtn_pymail_formatter]
   25.44 +format=%(asctime)s %(threadName)-16s %(levelname)-8s %(message)s
   25.45 +datefmt=
   25.46 +class=logging.Formatter
   25.47 +
    26.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    26.2 +++ b/outstation/home/user/dtn/python/pypop_maildir.py	Tue Mar 16 13:36:42 2010 +0000
    26.3 @@ -0,0 +1,767 @@
    26.4 +#! /usr/bin/python
    26.5 +# PyMail DTN Nomadic Mail System
    26.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    26.7 +#
    26.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    26.9 +#    you may not use this file except in compliance with the License.
   26.10 +#    You may obtain a copy of the License at
   26.11 +# 
   26.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   26.13 +# 
   26.14 +#    Unless required by applicable law or agreed to in writing, software
   26.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   26.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   26.17 +#    See the License for the specific language governing permissions and
   26.18 +#    limitations under the License.
   26.19 +#
   26.20 +
   26.21 +
   26.22 +"""
   26.23 +Maildir-style mailbox support
   26.24 +
   26.25 +Intended ultimately for use on Symbian-based mobile pnones in conjunction
   26.26 +with DTN package.
   26.27 +
   26.28 +This is intended to be a very lightweight system primaruily accessed only
   26.29 +on the local machine.   We need a system for storing mail messages to be received
   26.30 +from DTN so that they can be accessed by the standard phone messaging system.
   26.31 +
   26.32 +Standard email on a mobile phone doesn't readily handle bulk storage of emails
   26.33 +(as opposed to text messages etc).  The intention is that these mailboxes are
   26.34 +accessed by a lightweight POP3 protocol and is adapted as such.
   26.35 +
   26.36 +Mailboxes are associated with:
   26.37 +- a (mail) domain
   26.38 +- a directory structure
   26.39 +A domain has a root directory which contains:
   26.40 +- a file called 'passwd' which hold the passwords for all the users
   26.41 +- a directory for each user named by their username (avatar id)
   26.42 +Each user directory contains the usual set of maildir subdirectories
   26.43 +tmp, new, cur and .trash.  The maildir operates by means of the 'standard'
   26.44 +maildir semantics.
   26.45 +
   26.46 +When the system is active, a user's mailbox in the domain is represented by
   26.47 +an 'avatar' whic is a MaildirMailbox object instance.  The domain maintains
   26.48 +one instance for each active user.  It is intended that there should only be
   26.49 +one client at a time for each user, but the system should work OK with multiple
   26.50 +clients subject to some thought about thread safeness.  However, the nature
   26.51 +of a mobile phone makes this an unlikely situation and we won't worry too hard
   26.52 +about it at the moment.
   26.53 +
   26.54 +The domain supports authentication via username/password (plain text) and APOP
   26.55 +style hashed password.  No great effort (actually no effort at all)is made to
   26.56 +keep the passwords securely since the intention is that the maildir will live
   26.57 +on the phone where it is accessed and the files can be read by anybody who is
   26.58 +able to access the phone memory.  Usernames are desirable so that a phone can
   26.59 +be used for multiple incoming mail addresses, but a phone is inherently a single
   26.60 +user device but normally the communication between mail client and mailbox will
   26.61 +be over a channel within the phone so that the user name and password are never
   26.62 +visible outside the phone.  On the other hand, one might have to give a bit more
   26.63 +thought to this if the system was used to copy mail off the phone via a POP3
   26.64 +mail client on another device.
   26.65 +
   26.66 +
   26.67 +Revision History
   26.68 +================
   26.69 +Version   Date       Author         Notes
   26.70 +0.1       14/03/2010 Elwyn Davies   License statement added.
   26.71 +0.0       01/06/2009 Elwyn Davies   Initial version based on ideas from the
   26.72 +"""
   26.73 +
   26.74 +import os
   26.75 +import stat
   26.76 +import socket
   26.77 +import time
   26.78 +import hashlib
   26.79 +
   26.80 +try:
   26.81 +    import cStringIO as StringIO
   26.82 +except ImportError:
   26.83 +    import StringIO
   26.84 +
   26.85 +# On N810's anydbm pulls in gdbm which is unmaintaned and breaks
   26.86 +# in dtn mail we only have a couple of permanent users so dumbdbm is fine
   26.87 +try:
   26.88 +    import e32dbm as anydbm
   26.89 +except ImportError:
   26.90 +    import dumbdbm
   26.91 +    anydbm = dumbdbm
   26.92 +#   import anydbm
   26.93 +
   26.94 +class NotImplementedError(Exception):
   26.95 +    pass
   26.96 +
   26.97 +class UnauthorizedLogin(Exception):
   26.98 +    pass
   26.99 +
  26.100 +class _MaildirNameGenerator:
  26.101 +    """
  26.102 +    Utility class to generate a unique maildir name
  26.103 +
  26.104 +    """
  26.105 +    n = 0
  26.106 +    p = os.getpid()
  26.107 +    hn = socket.getfqdn()
  26.108 +
  26.109 +    def __init__(self):
  26.110 +        return
  26.111 +
  26.112 +    def generate(self):
  26.113 +        """
  26.114 +        Return a string which is intended to unique across all calls to this
  26.115 +        function (across all processes, reboots, etc).
  26.116 +
  26.117 +        Strings returned by earlier calls to this method will compare less
  26.118 +        than strings returned by later calls as long as the clock provided
  26.119 +        doesn't go backwards.
  26.120 +        """
  26.121 +        self.n = self.n + 1
  26.122 +        t = time.time()
  26.123 +        seconds = int(t)
  26.124 +        microseconds = '%07d' % (int((t - seconds) * 10e6),)
  26.125 +        return '%s.M%sP%sQ%s.%s' % (str(seconds), microseconds,
  26.126 +                                    self.p, self.n, self.hn)
  26.127 +
  26.128 +_generateMaildirName = _MaildirNameGenerator().generate
  26.129 +
  26.130 +def initializeMaildir(dir):
  26.131 +    if not os.path.isdir(dir):
  26.132 +        os.mkdir(dir, 0700)
  26.133 +        for subdir in ['new', 'cur', 'tmp', '.Trash']:
  26.134 +            os.mkdir(os.path.join(dir, subdir), 0700)
  26.135 +        for subdir in ['new', 'cur', 'tmp']:
  26.136 +            os.mkdir(os.path.join(dir, '.Trash', subdir), 0700)
  26.137 +        # touch
  26.138 +        open(os.path.join(dir, '.Trash', 'maildirfolder'), 'w').close()
  26.139 +
  26.140 +class MaildirMessage:
  26.141 +    size = None
  26.142 +    UNIXFROM = "Fron "
  26.143 +    UNIXFROM_LEN = len(UNIXFROM)
  26.144 +    X_DTN_HDR_NAME = "X-DTN-rcpts"
  26.145 +
  26.146 +    def __init__(self, address, fp, name, finalName):
  26.147 +        self.fp = fp
  26.148 +        self.name = name
  26.149 +        self.address = address
  26.150 +        self.finalName = finalName
  26.151 +        self.first_line = True
  26.152 +        self.size = 0
  26.153 +
  26.154 +    def lineReceived(self, line):
  26.155 +        # Add a X-DTN-rcpts header after a possible From envelope header
  26.156 +        # which must be the first line received
  26.157 +        if self.first_line:
  26.158 +            dtn_header = "%s: %s\n" % (self.X_DTN_HDR_NAME, self.address)
  26.159 +            if line[:self.UNIXFROM_LEN]:
  26.160 +                self.fp.write(line + '\n')
  26.161 +                self.fp.write(dtn_header)
  26.162 +            else:
  26.163 +                self.fp.write(dtn_header)
  26.164 +                self.fp.write(line + '\n')
  26.165 +            self.size += len(dtn_header)
  26.166 +            self.first_line = False
  26.167 +        else:
  26.168 +            self.fp.write(line + '\n')
  26.169 +        self.size += len(line)+1
  26.170 +
  26.171 +    def eomReceived(self):
  26.172 +        self.finalName = self.finalName+',S=%d' % self.size
  26.173 +        self.fp.close()
  26.174 +        try:
  26.175 +            os.rename(self.name, self.finalName)
  26.176 +            return True
  26.177 +        except:
  26.178 +            os.remove(self.name)
  26.179 +            return False
  26.180 +        
  26.181 +    def connectionLost(self):
  26.182 +        self.fp.close()
  26.183 +        os.remove(self.name)
  26.184 +
  26.185 +    def getFinalName(self):
  26.186 +        return self.finalName
  26.187 +        
  26.188 +class AbstractMaildirDomain:
  26.189 +    """Abstract maildir-backed domain.
  26.190 +    """
  26.191 +    root = None
  26.192 +    _credcheckers = None
  26.193 +
  26.194 +    def __init__(self, root):
  26.195 +        """Initialize.
  26.196 +        """
  26.197 +        self.root = root
  26.198 +
  26.199 +    def userDirectory(self, user):
  26.200 +        """Get the maildir directory for a given user
  26.201 +
  26.202 +        Override to specify where to save mails for users.
  26.203 +        Return None for non-existing users.
  26.204 +        """
  26.205 +        return None
  26.206 +
  26.207 +    ##
  26.208 +    ## IDomain
  26.209 +    ##
  26.210 +    def exists(self, user):
  26.211 +        """Check for existence of user in the domain
  26.212 +        """
  26.213 +        if self.userDirectory(user.dest.local) is not None:
  26.214 +            return lambda: self.startMessage(user)
  26.215 +        else:
  26.216 +            raise pypop_smtp.SMTPBadRcpt(user)
  26.217 +
  26.218 +    def startMessage(self, destn):
  26.219 +        """
  26.220 +        Save a message for a given user.
  26.221 +        """
  26.222 +        if isinstance(destn, str):
  26.223 +            if destn.find("@") == (-1):
  26.224 +                name = destn
  26.225 +                domain = "localhost"
  26.226 +            else:
  26.227 +                name, domain = destn.split('@', 1)
  26.228 +        else:
  26.229 +            name, domain = destn.dest.local, destn.dest.domain
  26.230 +
  26.231 +        dirname = self.userDirectory(name)
  26.232 +        fname = _generateMaildirName()
  26.233 +        filename = os.path.join(dirname, 'tmp', fname)
  26.234 +        fp = open(filename, 'w')
  26.235 +        return MaildirMessage('%s@%s' % (name, domain), fp, filename,
  26.236 +                              os.path.join(dirname, 'new', fname))
  26.237 +
  26.238 +    def willRelay(self, user, protocol):
  26.239 +        return False
  26.240 +
  26.241 +    def addUser(self, user, password):
  26.242 +        raise NotImplementedError
  26.243 +
  26.244 +    def getCredentialsCheckers(self):
  26.245 +        raise NotImplementedError
  26.246 +    ##
  26.247 +    ## end of IDomain
  26.248 +    ##
  26.249 +
  26.250 +
  26.251 +class MaildirMailbox:
  26.252 +    """Implement the POP3 mailbox semantics for a Maildir mailbox
  26.253 +    
  26.254 +    @type loginDelay: C{int}
  26.255 +    @ivar loginDelay: The number of seconds between allowed logins for the
  26.256 +    user associated with this mailbox.  None
  26.257 +
  26.258 +    @type messageExpiration: C{int}
  26.259 +    @ivar messageExpiration: The number of days messages in this mailbox will
  26.260 +    remain on the server before being deleted.
  26.261 +    """
  26.262 +    def __init__(self, path, owner):
  26.263 +        """Initialize with name of the Maildir mailbox
  26.264 +        """
  26.265 +        self.path = path
  26.266 +        self.owner = owner
  26.267 +        self.deleted = {}
  26.268 +        self.list = []
  26.269 +        initializeMaildir(path)
  26.270 +        self.makeMessageList()
  26.271 +
  26.272 +    def makeMessageList(self):
  26.273 +        self.list = []
  26.274 +        for name in ('cur', 'new'):
  26.275 +            for file in os.listdir(os.path.join(self.path, name)):
  26.276 +                self.list.append((file, os.path.join(self.path, name, file)))
  26.277 +        self.list.sort()
  26.278 +        self.list = [e[1] for e in self.list]
  26.279 +
  26.280 +    def listMessages(self, i=None):
  26.281 +        """Retrieve the size of one or more messages.
  26.282 +
  26.283 +        @type index: C{int} or C{None}
  26.284 +        @param index: The number of the message for which to retrieve the
  26.285 +        size (starting at 0), or None to retrieve the size of all messages.
  26.286 +
  26.287 +        @rtype: C{int} or any iterable of C{int} or a L{Deferred} which fires
  26.288 +        with one of these.
  26.289 +
  26.290 +        @return: The number of octets in the specified message, or an iterable
  26.291 +        of integers representing the number of octets in all the messages.  Any
  26.292 +        value which would have referred to a deleted message should be set to 0.
  26.293 +
  26.294 +        @raise ValueError: if C{index} is greater than the index of any message
  26.295 +        in the mailbox.
  26.296 +        
  26.297 +        Return a list of lengths of all files in new/ and cur/
  26.298 +        """
  26.299 +        if i is None:
  26.300 +            ret = []
  26.301 +            for mess in self.list:
  26.302 +                if mess:
  26.303 +                    ret.append(os.stat(mess)[stat.ST_SIZE])
  26.304 +                else:
  26.305 +                    ret.append(0)
  26.306 +            return ret
  26.307 +        else:
  26.308 +            if (i < 0) or (i >= len(self.list)):
  26.309 +                raise ValueError
  26.310 +            else:
  26.311 +                return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0
  26.312 +
  26.313 +    def getMessage(self, i):
  26.314 +        """Retrieve a file-like object for a particular message.
  26.315 +
  26.316 +        @type index: C{int}
  26.317 +        @param index: The number of the message to retrieve
  26.318 +
  26.319 +        @rtype: A file-like object
  26.320 +        @return: A file containing the message data with lines delimited by
  26.321 +        C{\\n}.
  26.322 +        
  26.323 +        Return an open file-pointer to a message
  26.324 +        """
  26.325 +        if (i < 0) or (i >= len(self.list)) or (self.list[i] == 0):
  26.326 +            raise ValueError
  26.327 +        return open(self.list[i])
  26.328 +
  26.329 +    def getUidl(self, i):
  26.330 +        """Get a unique identifier for a particular message.
  26.331 +
  26.332 +        @type index: C{int}
  26.333 +        @param index: The number of the message for which to retrieve a UIDL
  26.334 +
  26.335 +        @rtype: C{str}
  26.336 +        @return: A string of printable characters uniquely identifying for all
  26.337 +        time the specified message.
  26.338 +
  26.339 +        @raise ValueError: if C{index} is greater than the index of any message
  26.340 +        in the mailbox.
  26.341 +        
  26.342 +        Return a unique identifier for a message
  26.343 +
  26.344 +        This is done using the basename of the filename.
  26.345 +        It is globally unique because this is how Maildirs are designed.
  26.346 +        """
  26.347 +        # Returning the actual filename is a mistake.  Hash it.
  26.348 +        if (i < 0) or (i >= len(self.list)) or (self.list[i] == 0):
  26.349 +            raise ValueError
  26.350 +        base = os.path.basename(self.list[i])
  26.351 +        m = hashlib.md5()
  26.352 +        m.update(base)
  26.353 +        return m.hexdigest()
  26.354 +
  26.355 +    def deleteMessage(self, i):
  26.356 +        """Delete a particular message.
  26.357 +
  26.358 +        This must not change the number of messages in this mailbox.  Further
  26.359 +        requests for the size of deleted messages should return 0.  Further
  26.360 +        requests for the message itself may raise an exception.
  26.361 +
  26.362 +        @type index: C{int}
  26.363 +        @param index: The number of the message to delete.
  26.364 +        
  26.365 +        Delete a message
  26.366 +
  26.367 +        This only moves a message to the .Trash/ subfolder,
  26.368 +        so it can be undeleted by an administrator.
  26.369 +        """
  26.370 +        if (i < 0) or (i >= len(self.list)) or (self.list[i] == 0):
  26.371 +            raise ValueError
  26.372 +        
  26.373 +        trashFile = os.path.join(
  26.374 +            self.path, '.Trash', 'cur', os.path.basename(self.list[i])
  26.375 +        )
  26.376 +        os.rename(self.list[i], trashFile)
  26.377 +        self.deleted[self.list[i]] = (i, trashFile)
  26.378 +        self.list[i] = 0
  26.379 +
  26.380 +    def undeleteMessages(self):
  26.381 +        """
  26.382 +        Undelete any messages which have been marked for deletion since the
  26.383 +        most recent L{sync} call.
  26.384 +
  26.385 +        Any message which can be undeleted should be returned to its
  26.386 +        original position in the message sequence and retain its original
  26.387 +        UID.
  26.388 +        
  26.389 +        Undelete any deleted messages it is possible to undelete
  26.390 +
  26.391 +        This moves any messages from .Trash/ subfolder back to their
  26.392 +        original position, and empties out the deleted dictionary.
  26.393 +        """
  26.394 +        for (real, trash) in self.deleted.items():
  26.395 +            try:
  26.396 +                os.rename(trash[1], real)
  26.397 +            except OSError, (err, estr):
  26.398 +                import errno
  26.399 +                # If the file has been deleted from disk, oh well!
  26.400 +                if err != errno.ENOENT:
  26.401 +                    raise
  26.402 +                # This is a pass
  26.403 +            else:
  26.404 +                try:
  26.405 +                    self.list[trash[0]] = real
  26.406 +                except ValueError:
  26.407 +                    self.list.append(real)
  26.408 +        self.deleted.clear()
  26.409 +
  26.410 +    def moveNewCur(self, i=None):
  26.411 +        """
  26.412 +        Move one or all messages in new to cur (not including any that
  26.413 +        might have been deleted).
  26.414 +        """
  26.415 +        newdir = os.path.join(self.path, "new")
  26.416 +        if i == None:
  26.417 +            i = 0
  26.418 +            for m in self.list:
  26.419 +                if m != 0:
  26.420 +                    (pathname, base) = os.path.split(m)
  26.421 +                    if pathname == newdir:
  26.422 +                        n = os.path.join(self.path, "cur", base)
  26.423 +                        os.rename(m, n)
  26.424 +                        self.list[i] = n
  26.425 +                i +=1
  26.426 +        else:
  26.427 +            if (i < 0) or (i >= len(self.list)) or (self.list[i] == 0):
  26.428 +                raise ValueError
  26.429 +            
  26.430 +            m = self.list[i]
  26.431 +            if m != 0:
  26.432 +                (pathname, base) = os.path.split(m)
  26.433 +                if pathname == newdir:
  26.434 +                    n = os.path.join(self.path, "cur", base)
  26.435 +                    os.rename(m, n)
  26.436 +                    self.list[i] = n
  26.437 +    def cleanTrash(self):
  26.438 +        """
  26.439 +        Remove any messages in deleted and compress the list.
  26.440 +        When using POP3 to access the mailbox this will be done
  26.441 +        in the UPDATE state when QUIT is called.  The list is kept in step
  26.442 +        so that the instance can be used with a new session.
  26.443 +        """
  26.444 +        for (real, trash) in self.deleted.items():
  26.445 +            try:
  26.446 +                os.remove(trash[1])
  26.447 +            except:
  26.448 +                pass
  26.449 +            else:
  26.450 +                del self.list[trash[0]]
  26.451 +
  26.452 +    def newMessage(self, destn):
  26.453 +        fname = _generateMaildirName()
  26.454 +        filename = os.path.join(self.path, 'tmp', fname)
  26.455 +        fp = open(filename, 'w')
  26.456 +        return MaildirMessage(destn, fp, filename,
  26.457 +                              os.path.join(self.path, 'new', fname))
  26.458 +
  26.459 +    def newMessageFile(self, temp_fname):
  26.460 +        fname = _generateMaildirName()
  26.461 +        try:
  26.462 +            size = os.stat(temp_fname)[stat.ST_SIZE]
  26.463 +            new_fn = os.path.join(self.path, 'new', "%s,S=%d" % (fname, size) )
  26.464 +            os.rename(temp_fname, new_fn)
  26.465 +        except:
  26.466 +            return None
  26.467 +        self.list.append(new_fn)
  26.468 +        return new_fn
  26.469 +        
  26.470 +        
  26.471 +    def appendMessage(self, msg):
  26.472 +        """
  26.473 +        Appends a message into the mailbox.
  26.474 +        called when a new message has been installed dynamically
  26.475 +        to add to the list.
  26.476 +
  26.477 +        This is an optimization to avoid rereading the files. 
  26.478 +        """
  26.479 +        self.list.append(msg.getFinalName())
  26.480 +
  26.481 +    def sync(self):
  26.482 +        """
  26.483 +        No specific requirements.
  26.484 +        """
  26.485 +        pass
  26.486 +
  26.487 +##
  26.488 +## Authentication
  26.489 +##
  26.490 +class CredentialsChecker:
  26.491 +    def checkPassword(self, password):
  26.492 +        raise NotImplementedError
  26.493 +
  26.494 +class UsernameHashedPassword(CredentialsChecker):
  26.495 +
  26.496 +    def __init__(self, username, hashed):
  26.497 +        self.username = username
  26.498 +        self.hashed = hashed
  26.499 +
  26.500 +    def checkPassword(self, password):
  26.501 +        return self.hashed == password
  26.502 +
  26.503 +class UsernamePassword(CredentialsChecker):
  26.504 +
  26.505 +    def __init__(self, username, password):
  26.506 +        self.username = username
  26.507 +        self.password = password
  26.508 +
  26.509 +    def checkPassword(self, password):
  26.510 +        return self.password == password
  26.511 +
  26.512 +class APOPCredentials(CredentialsChecker):
  26.513 +
  26.514 +    def __init__(self, magic, username, digest):
  26.515 +        self.magic = magic
  26.516 +        self.username = username
  26.517 +        self.digest = digest
  26.518 +
  26.519 +    def checkPassword(self, password):
  26.520 +        seed = self.magic + password
  26.521 +        m = hashlib.md5()
  26.522 +        m.update(seed)
  26.523 +        myDigest = m.hexdigest()
  26.524 +        return myDigest == self.digest
  26.525 +
  26.526 +class MaildirDirdbmDomain(AbstractMaildirDomain):
  26.527 +    """A Maildir Domain where membership is checked by a dirdbm file
  26.528 +    """
  26.529 +
  26.530 +    def __init__(self, root=None, postmaster=0):
  26.531 +        """Initialize
  26.532 +
  26.533 +        The first argument is where the Domain directory is rooted.
  26.534 +        The second is whether non-existing addresses are simply
  26.535 +        forwarded to postmaster instead of outright bounce
  26.536 +
  26.537 +        The directory structure of a MailddirDirdbmDomain is:
  26.538 +
  26.539 +        /passwd <-- a dbm file
  26.540 +        /USER/{cur,new,del} <-- each user has these three directories
  26.541 +
  26.542 +        A maximum of one 'avatar' (MaildirMailbox) object is maintained
  26.543 +        for each user of this domain.  The avatars are created when the
  26.544 +        first avatarRequest is made.  At present this probably not thread safe.
  26.545 +        """
  26.546 +        AbstractMaildirDomain.__init__(self, root)
  26.547 +        dbm = os.path.join(root, 'passwd')
  26.548 +        if not os.path.exists(root):
  26.549 +            os.makedirs(root)
  26.550 +        self.dbm = anydbm.open(dbm, "c")
  26.551 +        self.postmaster = postmaster
  26.552 +        self.avatarDict = {}
  26.553 +
  26.554 +    def userDirectory(self, name):
  26.555 +        """Get the directory for a user
  26.556 +
  26.557 +        If the user exists in the dirdbm file, return the directory
  26.558 +        os.path.join(root, name), creating it if necessary.
  26.559 +        Otherwise, returns postmaster's mailbox instead if bounces
  26.560 +        go to postmaster, otherwise return None
  26.561 +        """
  26.562 +        if not self.dbm.has_key(name):
  26.563 +            if not self.postmaster:
  26.564 +                return None
  26.565 +            name = 'postmaster'
  26.566 +        dir = os.path.join(self.root, name)
  26.567 +        if not os.path.exists(dir):
  26.568 +            initializeMaildir(dir)
  26.569 +        return dir
  26.570 +    def close(self):
  26.571 +        self.dbm.close()
  26.572 +
  26.573 +    ##
  26.574 +    ## IDomain
  26.575 +    ##
  26.576 +    def addUser(self, user, password):
  26.577 +        self.dbm[user] = password
  26.578 +        # Ensure it is initialized
  26.579 +        self.userDirectory(user)
  26.580 +
  26.581 +    def getCredentialsCheckers(self):
  26.582 +        if self._credcheckers is None:
  26.583 +            self._credcheckers = [DirdbmDatabase(self.dbm)]
  26.584 +        return self._credcheckers
  26.585 +
  26.586 +    def buildNewMessage(self, user, feeder):
  26.587 +        """Builds a new message into the mailbox for a given user."""
  26.588 +        # Call start message and then loop on feeder until finished calling
  26.589 +        # add line and then eom_message.  Update the list when finished.
  26.590 +        avatar = self.requestAvatar(user)
  26.591 +        if avatar == None:
  26.592 +            return False
  26.593 +        msg = self.startMessage(user)
  26.594 +        while True:
  26.595 +            try:
  26.596 +                msg.lineReceived(feeder.next())
  26.597 +            except:
  26.598 +                break
  26.599 +        result = msg.eomReceived()
  26.600 +        if result:
  26.601 +            avatar.appendMessage(msg)
  26.602 +        return result
  26.603 +        
  26.604 +        
  26.605 +    ##
  26.606 +    ## IRealm
  26.607 +    ##
  26.608 +    def verifyCredentials(self, credentials):
  26.609 +        for checker in self.getCredentialsCheckers():
  26.610 +            try:
  26.611 +                return checker.requestAvatarId(credentials)
  26.612 +            except UnauthorizedLogin:
  26.613 +                continue
  26.614 +        raise UnauthorizedLogin
  26.615 +    
  26.616 +    def requestAvatar(self, avatarId):
  26.617 +        dirname = self.userDirectory(avatarId)
  26.618 +        if dirname == None:
  26.619 +            return None
  26.620 +        if not self.avatarDict.has_key(avatarId):
  26.621 +            self.avatarDict[avatarId] = MaildirMailbox(dirname, avatarId)
  26.622 +        return self.avatarDict[avatarId]
  26.623 +
  26.624 +class DirdbmDatabase:
  26.625 +
  26.626 +    def __init__(self, dbm):
  26.627 +        self.dirdbm = dbm
  26.628 +
  26.629 +    """
  26.630 +    requestAvatarId expects an instance of an object that inherits from
  26.631 +    CredentialsChecker as its paramter.
  26.632 +    """
  26.633 +
  26.634 +    def requestAvatarId(self, c):
  26.635 +        if c.username in self.dirdbm:
  26.636 +            if c.checkPassword(self.dirdbm[c.username]):
  26.637 +                return c.username
  26.638 +        raise UnauthorizedLogin()
  26.639 +
  26.640 +if __name__ == "__main__":
  26.641 +    # Test code
  26.642 +    class TestFeeder:
  26.643 +        content = ["From: elwynd@example.com",
  26.644 +                   "To: godzilla@mumble.com",
  26.645 +                   "Subject: Test %d",
  26.646 +                   "",
  26.647 +                   "A test message",
  26.648 +                   "Some more text",
  26.649 +                   "Yet more text",
  26.650 +                   "The quick brown fox jumped"]
  26.651 +        def __init__(self):
  26.652 +            self.msg_no = 0
  26.653 +
  26.654 +        def get_line(self):
  26.655 +            self.msg_no += 1
  26.656 +            for l in self.content:
  26.657 +                if l[0:7] == "Subject":
  26.658 +                    yield l % (self.msg_no, )
  26.659 +                else:
  26.660 +                    yield l
  26.661 +            
  26.662 +    #testdomain = MaildirDirdbmDomain("/home/elwynd/maildir")
  26.663 +    testdomain = MaildirDirdbmDomain("/tmp")
  26.664 +    testdomain.addUser("postmaster", "postie_pat")
  26.665 +    testdomain.addUser("elwynd", "password")
  26.666 +
  26.667 +    magic = "abcdefgh1234567"
  26.668 +    m = hashlib.md5()
  26.669 +
  26.670 +    m.update(magic + "password")
  26.671 +    elwynd_digest = m.hexdigest()
  26.672 +    
  26.673 +    cred1 = UsernamePassword("postmaster", "postie_pat")
  26.674 +    cred2 = APOPCredentials(magic, "elwynd", elwynd_digest)
  26.675 +    cred3 = UsernamePassword("postmaster", "not_my_pas")
  26.676 +    cred4 = UsernamePassword("godzilla", "not_my_pas")
  26.677 +    cred5 = APOPCredentials(magic, "elwynd", "0afec56")
  26.678 +    cred6 = APOPCredentials(magic, "mummy", elwynd_digest)
  26.679 +
  26.680 +    n = 0
  26.681 +    for i in [cred1, cred2, cred3, cred4, cred5, cred6]:
  26.682 +        n += 1
  26.683 +        try:
  26.684 +            avatarId = testdomain.verifyCredentials(i)
  26.685 +            print "Successful credentials verification: %d %s\n" % (n, avatarId,)
  26.686 +        except:
  26.687 +            print "Verification failed: %d %s\n" %(n, i.username)
  26.688 +
  26.689 +    f = TestFeeder()
  26.690 +    g = f.get_line()
  26.691 +    while True:
  26.692 +        try:
  26.693 +            print g.next()
  26.694 +        except:
  26.695 +            break
  26.696 +    # see what is in elwynd's mailbox
  26.697 +    avatar = testdomain.requestAvatar("elwynd")
  26.698 +    orig_list = avatar.listMessages()
  26.699 +    l1 = len(orig_list)
  26.700 +    print "List of messages (%d): " % (l1, ), orig_list
  26.701 +
  26.702 +    # Create some more messages for elwynd
  26.703 +    g = f.get_line()
  26.704 +    result = testdomain.buildNewMessage("elwynd", g)
  26.705 +    print "Msg stored: ", result
  26.706 +    g = f.get_line()
  26.707 +    result = testdomain.buildNewMessage("elwynd", g)
  26.708 +    print "Msg stored: ", result
  26.709 +    g = f.get_line()
  26.710 +    result = testdomain.buildNewMessage("elwynd", g)
  26.711 +    print "Msg stored: ", result
  26.712 +    g = f.get_line()
  26.713 +    result = testdomain.buildNewMessage("elwynd", g)
  26.714 +    print "Msg stored: ", result
  26.715 +    new_list = avatar.listMessages()
  26.716 +    l2 = len(new_list)
  26.717 +    print "List of messages (%d): " % (l2, ), new_list
  26.718 +    if l2 == (l1 + 4):
  26.719 +        print "Correct number of messages added"
  26.720 +    else:
  26.721 +        print "New list does not contain expected number of messages"
  26.722 +
  26.723 +    # Test getUidl
  26.724 +    print "Uidl: ", avatar.getUidl(3)
  26.725 +    try:
  26.726 +        res = avatar.getUidl(l2+1)
  26.727 +        print "Error: out of range message id did not raise ValueError"
  26.728 +    except ValueError:
  26.729 +        print "Out of range message ifd raised ValueError correctly"
  26.730 +
  26.731 +    # Test deletion
  26.732 +    avatar.deleteMessage(4)
  26.733 +    avatar.deleteMessage(3)
  26.734 +    new_list = avatar.listMessages()
  26.735 +    l2 = len(new_list)
  26.736 +    print "List of messages after deletion (%d): " % (l2, ), new_list
  26.737 +    avatar.undeleteMessages()
  26.738 +    new_list = avatar.listMessages()
  26.739 +    l2 = len(new_list)
  26.740 +    print "List of messages after undeletion (%d): " % (l2, ), new_list
  26.741 +
  26.742 +    # Test moving new to current
  26.743 +    avatar.moveNewCur(2)
  26.744 +    new_list = avatar.listMessages()
  26.745 +    l2 = len(new_list)
  26.746 +    print "List of messages after moving one message from new to cur (%d): " % (l2, ), new_list
  26.747 +    avatar.moveNewCur()
  26.748 +    new_list = avatar.listMessages()
  26.749 +    l2 = len(new_list)
  26.750 +    print "List of messages after moving all messages from new to cur (%d): " % (l2, ), new_list
  26.751 +
  26.752 +    # Test clean trash
  26.753 +    avatar.deleteMessage(2)
  26.754 +    new_list = avatar.listMessages()
  26.755 +    l2 = len(new_list)
  26.756 +    print "List of messages after deleteing one message (%d): " % (l2, ), new_list
  26.757 +    avatar.cleanTrash()
  26.758 +    new_list = avatar.listMessages()
  26.759 +    l2 = len(new_list)
  26.760 +    print "List of messages after cleaning trash (%d): " % (l2, ), new_list
  26.761 +
  26.762 +    testdomain.close()
  26.763 +    
  26.764 +    
  26.765 +    
  26.766 +    
  26.767 +
  26.768 +    
  26.769 +
  26.770 +
    27.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    27.2 +++ b/outstation/home/user/dtn/start_pymail.sh	Tue Mar 16 13:36:42 2010 +0000
    27.3 @@ -0,0 +1,22 @@
    27.4 +#!/bin/sh
    27.5 +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/bin/X11
    27.6 +export PATH
    27.7 +
    27.8 +TEMP=/home/user/dtn/tmp
    27.9 +export TEMP
   27.10 +
   27.11 +PYTHONROOT=/usr/lib/python2.5
   27.12 +PYTHONPATH=${PYTHONROOT}:${PYTHONROOT}/site-packages:${PYTHONROOT}/lib-dynload
   27.13 +export PYTHONROOT
   27.14 +export PYTHONPATH
   27.15 +
   27.16 +# Start the DTN daemon
   27.17 +echo Starting DTN daemon
   27.18 +/usr/local/bin/dtnd -c /home/user/dtn/dtn/dtn_outstation_static.conf -d -o /media/mmc2/dtn/log/dtnd.log -l info
   27.19 +
   27.20 +# Wait for the daemon to start up
   27.21 +sleep 10
   27.22 +
   27.23 +# Start the Python DTN mail interface
   27.24 +echo starting DTN mail interface
   27.25 +python /home/user/dtn/python/dp_outstation.py &
    28.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    28.2 +++ b/outstation/home/user/dtn/stop_pymail.sh	Tue Mar 16 13:36:42 2010 +0000
    28.3 @@ -0,0 +1,10 @@
    28.4 +#! /bin/sh
    28.5 +# kill off the Pythoin redirector
    28.6 +# Find its process
    28.7 +pid=`ps | awk -f /home/user/dtn/dp_out.awk`
    28.8 +if [ x"$pid" !=  "x" ]; then 
    28.9 +kill $pid
   28.10 +fi
   28.11 +
   28.12 +# Kill of the dtnd daemon
   28.13 +dtnd-control stop
    29.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    29.2 +++ b/outstation/usr/share/applications/hildon/start_pymail.desktop	Tue Mar 16 13:36:42 2010 +0000
    29.3 @@ -0,0 +1,8 @@
    29.4 +[Desktop Entry]
    29.5 +Version=1.0.0
    29.6 +Encoding=UTF-8
    29.7 +Name=Start pymail
    29.8 +Exec=/home/user/dtn/start_pymail.sh
    29.9 +Icon=n4c
   29.10 +Type=Application
   29.11 +Categories=N4C
    30.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    30.2 +++ b/outstation/usr/share/applications/hildon/stop_pymail.desktop	Tue Mar 16 13:36:42 2010 +0000
    30.3 @@ -0,0 +1,8 @@
    30.4 +[Desktop Entry]
    30.5 +Version=1.0.0
    30.6 +Encoding=UTF-8
    30.7 +Name=Stop pymail
    30.8 +Exec=/home/user/dtn/stop_pymail.sh
    30.9 +Icon=n4c
   30.10 +Type=Application
   30.11 +Categories=N4C
    31.1 Binary file outstation/usr/share/pixmaps/n4c.png has changed
    32.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    32.2 +++ b/relay/dtnrun/dtn/dtn_relay_adhoc_static.conf	Tue Mar 16 13:36:42 2010 +0000
    32.3 @@ -0,0 +1,281 @@
    32.4 +#
    32.5 +# dtn.conf
    32.6 +#
    32.7 +# Configuration file for the nomadic.n4c.eu relay on the PRoPHET
    32.8 +# routed N4C nomadic network. The machine with the relay runs two daemons
    32.9 +# one talking to the statically routed network and the other talking
   32.10 +# to the proxy gateway on the PRoPHET network.
   32.11 +
   32.12 +# The static side uses the standard set of port numbers whereas the
   32.13 +# the PRoPHET side uses a set of non standard ports so that in can 
   32.14 +# coexist in the same machine.  Note that the bundle storage database
   32.15 +# needs to be kept separate between the two daemons.
   32.16 +
   32.17 +# Default configuration file for Internet-connected DTN nodes. The
   32.18 +# daemon uses a tcl interpreter to parse this file, thus any standard
   32.19 +# tcl commands are valid, and all settings are get/set using a single
   32.20 +# 'set' functions as: <module> set <var> <val?>
   32.21 +#
   32.22 +
   32.23 +log /dtnd info "dtnd parsing configuration..."
   32.24 +
   32.25 +########################################
   32.26 +#
   32.27 +# Daemon Console Configuration
   32.28 +#
   32.29 +########################################
   32.30 +
   32.31 +#
   32.32 +# console set stdio [ true | false ]
   32.33 +#
   32.34 +# If set to false, disable the interactive console on stdin/stdout.
   32.35 +# The default is set to true (unless the dtnd process is run as a
   32.36 +# daemon).
   32.37 +#
   32.38 +# console set stdio false
   32.39 +
   32.40 +#
   32.41 +# console set addr <port>
   32.42 +# console set port <port>
   32.43 +#
   32.44 +# Settings for the socket based console protocol.
   32.45 +# (this interprets user commands)
   32.46 +#
   32.47 +console set addr 127.0.0.1
   32.48 +console set port 5051
   32.49 +
   32.50 +#
   32.51 +# console set prompt <prompt>
   32.52 +#
   32.53 +# Set the prompt string.  Helps if running multiple dtnd's
   32.54 +#
   32.55 +set shorthostname [lindex [split [info hostname] .] 0]
   32.56 +console set prompt "$shorthostname dtn-prophet% "
   32.57 +
   32.58 +########################################
   32.59 +#
   32.60 +# API Server Configuration
   32.61 +#
   32.62 +########################################
   32.63 +api set local_port 5011
   32.64 +
   32.65 +########################################
   32.66 +#
   32.67 +# Storage Configuration
   32.68 +#
   32.69 +########################################
   32.70 +
   32.71 +#
   32.72 +# storage set type [ berkeleydb | postgres | mysql  | external ]
   32.73 +#
   32.74 +# Set the storage system to be used
   32.75 +#
   32.76 +storage set type berkeleydb
   32.77 +
   32.78 +# the following are for use with external data stores
   32.79 +#
   32.80 +# The server port to connect to (on localhost)
   32.81 +# Note that 62345 has no special significance -- chosen randomly
   32.82 +storage set server_port 62345
   32.83 +
   32.84 +# The external data store schema location, which can be
   32.85 +# found in dtn2/oasys/storage/DS.xsd.
   32.86 +storage set schema /etc/DS.xsd
   32.87 +
   32.88 +
   32.89 +#
   32.90 +# Do a runtime check for the standard locations for the persistent
   32.91 +# storage directory
   32.92 +#
   32.93 +set dbdir "/home/user/dtnrun/bundlestore/nomadic"
   32.94 +if {$dbdir == ""} {
   32.95 +    foreach dir {/var/dtn /var/tmp/dtn} {
   32.96 +        if {[file isdirectory $dir]} {
   32.97 +            set dbdir $dir
   32.98 +            break
   32.99 +        }
  32.100 +    }
  32.101 +}
  32.102 +
  32.103 +if {$dbdir == ""} {
  32.104 +    puts stderr "Must create /var/dtn or /var/tmp/dtn storage directory"
  32.105 +    exit 1
  32.106 +}
  32.107 +
  32.108 +#
  32.109 +# storage set payloaddir <dir>
  32.110 +#
  32.111 +# Set the directory to be used for bundle payload files
  32.112 +#
  32.113 +storage set payloaddir $dbdir/bundles
  32.114 +
  32.115 +#
  32.116 +# storage set dbname <db>
  32.117 +#
  32.118 +# Set the database name (appended with .db as the filename in berkeley
  32.119 +# db, used as-is for SQL variants
  32.120 +#
  32.121 +storage set dbname     DTN
  32.122 +
  32.123 +#
  32.124 +# storage set dbdir    <dir>
  32.125 +#
  32.126 +#
  32.127 +# When using berkeley db, set the directory to be used for the
  32.128 +# database files and the name of the files and error log.
  32.129 +#
  32.130 +storage set dbdir      $dbdir/db
  32.131 +
  32.132 +########################################
  32.133 +#
  32.134 +# Routing configuration
  32.135 +#
  32.136 +########################################
  32.137 +
  32.138 +#
  32.139 +# Set the algorithm used for dtn routing.
  32.140 +#
  32.141 +# route set type [static | flood | neighborhood | linkstate | external]
  32.142 +#
  32.143 +route set type static
  32.144 +
  32.145 +# Set up some routing parameters
  32.146 +# Age out node parameters every 12 hours - 12 * 60 * 60 seconds
  32.147 +#prophet set age_period 43200
  32.148 +
  32.149 +# Decay probabilities by units of a day - kappa is set in milliseconds - 24 * 60 * 60 * 1000
  32.150 +#prophet set kappa 86400000
  32.151 +
  32.152 +#
  32.153 +# route local_eid <eid>
  32.154 +#
  32.155 +# Set the local administrative id of this node. The default just uses
  32.156 +# the internet hostname plus the appended string ".dtn" to make it
  32.157 +# clear that the hostname isn't really used for DNS lookups.
  32.158 +#
  32.159 +# This is *proxy* gateway machine!  The outstations route to here and 
  32.160 +# we relay on to the real gateway on rosebud.
  32.161 +route local_eid "dtn://gateway.nomadic.n4c.eu"
  32.162 +
  32.163 +#
  32.164 +# External router specific options
  32.165 +#
  32.166 +# route set server_port 8001
  32.167 +# route set hello_interval 30
  32.168 +# route set schema "/etc/router.xsd"
  32.169 +
  32.170 +########################################
  32.171 +#
  32.172 +# TCP convergence layer configuration
  32.173 +#
  32.174 +########################################
  32.175 +
  32.176 +#
  32.177 +# interface add [name] [CL]
  32.178 +#
  32.179 +# Add an input interface to listen on addr:port for incoming bundles
  32.180 +# from other tcp / udp convergence layers
  32.181 +#
  32.182 +# For IP-based interfaces, interfaces listen on INADDR_ANY port 4556
  32.183 +# by default. These can be overridden by using the local_addr and/or
  32.184 +# local_port arguments.
  32.185 +interface add tcp0 tcp local_addr=192.168.2.70 local_port=4557
  32.186 +#interface add udp0 udp
  32.187 +
  32.188 +#
  32.189 +# link add <name> <nexthop> <type> <clayer> <args...>
  32.190 +#
  32.191 +# Add a link to a peer node.
  32.192 +# 
  32.193 +# For IP-based links (tcp or udp), the nexthop should contain a DNS
  32.194 +# hostname or IP address, followed optionally by a : and a port. If
  32.195 +# the port is not specified, the default of 4556 is used.
  32.196 +#
  32.197 +# e.g.  link add link1 dtn.dtnrg.org ONDEMAND tcp
  32.198 +#       link add link2 dtn2.dtnrg.org:10000 ONDEMAND tcp
  32.199 +
  32.200 +link add linkToUser1  192.168.2.51 ONDEMAND tcp
  32.201 +link add linkToUser2  192.168.2.52 ONDEMAND tcp
  32.202 +link add linkToUser3  192.168.2.53 ONDEMAND tcp
  32.203 +link add linkToUser4  192.168.2.54 ONDEMAND tcp
  32.204 +link add linkToUser5  192.168.2.55 ONDEMAND tcp
  32.205 +link add linkToUser6  192.168.2.56 ONDEMAND tcp
  32.206 +link add linkToUser7  192.168.2.57 ONDEMAND tcp
  32.207 +link add linkToUser8  192.168.2.58 ONDEMAND tcp
  32.208 +link add linkToUser9  192.168.2.59 ONDEMAND tcp
  32.209 +link add linkToUser10 192.168.2.60 ONDEMAND tcp
  32.210 +#link add linkToProxyGw 192.168.2.70 ONDEMAND tcp
  32.211 +
  32.212 +#
  32.213 +# route add <dest> <link|peer>
  32.214 +#
  32.215 +# Add a route to the given bundle endpoint id pattern <dest> using the
  32.216 +# specified link name or peer endpoint.
  32.217 +#
  32.218 +# e.g. route add dtn://host.domain/* tcp0
  32.219 +
  32.220 +route add dtn://user1.nomadic.n4c.eu/* linkToUser1
  32.221 +route add dtn://user2.nomadic.n4c.eu/* linkToUser2
  32.222 +route add dtn://user3.nomadic.n4c.eu/* linkToUser3
  32.223 +route add dtn://user4.nomadic.n4c.eu/* linkToUser4
  32.224 +route add dtn://user5.nomadic.n4c.eu/* linkToUser5
  32.225 +route add dtn://user6.nomadic.n4c.eu/* linkToUser6
  32.226 +route add dtn://user7.nomadic.n4c.eu/* linkToUser7
  32.227 +route add dtn://user8.nomadic.n4c.eu/* linkToUser8
  32.228 +route add dtn://user9.nomadic.n4c.eu/* linkToUser9
  32.229 +route add dtn://user10.nomadic.n4c.eu/* linkToUser10
  32.230 +#route add dtn://gateway.nomadic.n4c.eu/* linkToProxyGw
  32.231 +
  32.232 +########################################
  32.233 +#
  32.234 +# Service discovery
  32.235 +#
  32.236 +########################################
  32.237 +
  32.238 +#
  32.239 +# discovery add <name> <af> <opts...>
  32.240 +# discovery announce <cl_name> <discovery_name> <cl_type> <opts...>
  32.241 +#
  32.242 +# Add a local neighborhood discovery module
  32.243 +#
  32.244 +# e.g. discovery add discovery_bonjour bonjour
  32.245 +discovery add ip_disc ip port=4301
  32.246 +discovery announce tcp0 ip_disc tcp cl_addr=192.168.2.70 cl_port=4557 interval=10
  32.247 +
  32.248 +########################################
  32.249 +#
  32.250 +# Parameter Tuning
  32.251 +#
  32.252 +########################################
  32.253 +
  32.254 +#
  32.255 +# Set the size threshold for the daemon so any bundles smaller than this
  32.256 +# size maintain a shadow copy in memory to minimize disk accesses. 
  32.257 +#
  32.258 +# param set payload_mem_threshold 16384
  32.259 +
  32.260 +#
  32.261 +# Test option to keep all bundle files in the filesystem, even after the
  32.262 +# bundle is no longer present at the daemon.
  32.263 +#
  32.264 +# param set payload_test_no_remove true
  32.265 +
  32.266 +#
  32.267 +# Set the size for which the tcp convergence layer sends partial reception
  32.268 +# acknowledgements. Used with reactive fragmentation
  32.269 +#
  32.270 +# param set tcpcl_partial_ack_len 4096
  32.271 +
  32.272 +#
  32.273 +# Set if bundles are automatically deleted after transmission
  32.274 +#
  32.275 +# param set early_deletion true
  32.276 +
  32.277 +# (others exist but are not fully represented here)
  32.278 +
  32.279 +log /dtnd info "dtnd configuration parsing complete"
  32.280 +
  32.281 +## emacs settings to use tcl-mode by default
  32.282 +## Local Variables: ***
  32.283 +## mode:tcl ***
  32.284 +## End: ***
    33.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    33.2 +++ b/relay/dtnrun/dtn/dtn_relay_prophet.conf	Tue Mar 16 13:36:42 2010 +0000
    33.3 @@ -0,0 +1,261 @@
    33.4 +#
    33.5 +# dtn.conf
    33.6 +#
    33.7 +# Configuration file for the nomadic.n4c.eu relay on the PRoPHET
    33.8 +# routed N4C nomadic network. The machine with the relay runs two daemons
    33.9 +# one talking to the statically routed network and the other talking
   33.10 +# to the proxy gateway on the PRoPHET network.
   33.11 +
   33.12 +# The static side uses the standard set of port numbers whereas the
   33.13 +# the PRoPHET side uses a set of non standard ports so that in can 
   33.14 +# coexist in the same machine.  Note that the bundle storage database
   33.15 +# needs to be kept separate between the two daemons.
   33.16 +
   33.17 +# Default configuration file for Internet-connected DTN nodes. The
   33.18 +# daemon uses a tcl interpreter to parse this file, thus any standard
   33.19 +# tcl commands are valid, and all settings are get/set using a single
   33.20 +# 'set' functions as: <module> set <var> <val?>
   33.21 +#
   33.22 +
   33.23 +log /dtnd info "dtnd parsing configuration..."
   33.24 +
   33.25 +########################################
   33.26 +#
   33.27 +# Daemon Console Configuration
   33.28 +#
   33.29 +########################################
   33.30 +
   33.31 +#
   33.32 +# console set stdio [ true | false ]
   33.33 +#
   33.34 +# If set to false, disable the interactive console on stdin/stdout.
   33.35 +# The default is set to true (unless the dtnd process is run as a
   33.36 +# daemon).
   33.37 +#
   33.38 +# console set stdio false
   33.39 +
   33.40 +#
   33.41 +# console set addr <port>
   33.42 +# console set port <port>
   33.43 +#
   33.44 +# Settings for the socket based console protocol.
   33.45 +# (this interprets user commands)
   33.46 +#
   33.47 +console set addr 127.0.0.1
   33.48 +console set port 5051
   33.49 +
   33.50 +#
   33.51 +# console set prompt <prompt>
   33.52 +#
   33.53 +# Set the prompt string.  Helps if running multiple dtnd's
   33.54 +#
   33.55 +set shorthostname [lindex [split [info hostname] .] 0]
   33.56 +console set prompt "$shorthostname dtn-prophet% "
   33.57 +
   33.58 +########################################
   33.59 +#
   33.60 +# API Server Configuration
   33.61 +#
   33.62 +########################################
   33.63 +api set local_port 5011
   33.64 +
   33.65 +########################################
   33.66 +#
   33.67 +# Storage Configuration
   33.68 +#
   33.69 +########################################
   33.70 +
   33.71 +#
   33.72 +# storage set type [ berkeleydb | postgres | mysql  | external ]
   33.73 +#
   33.74 +# Set the storage system to be used
   33.75 +#
   33.76 +storage set type berkeleydb
   33.77 +
   33.78 +# the following are for use with external data stores
   33.79 +#
   33.80 +# The server port to connect to (on localhost)
   33.81 +# Note that 62345 has no special significance -- chosen randomly
   33.82 +storage set server_port 62345
   33.83 +
   33.84 +# The external data store schema location, which can be
   33.85 +# found in dtn2/oasys/storage/DS.xsd.
   33.86 +storage set schema /etc/DS.xsd
   33.87 +
   33.88 +
   33.89 +#
   33.90 +# Do a runtime check for the standard locations for the persistent
   33.91 +# storage directory
   33.92 +#
   33.93 +set dbdir "/home/user/dtnrun/bundlestore/nomadic"
   33.94 +if {$dbdir == ""} {
   33.95 +    foreach dir {/var/dtn /var/tmp/dtn} {
   33.96 +        if {[file isdirectory $dir]} {
   33.97 +            set dbdir $dir
   33.98 +            break
   33.99 +        }
  33.100 +    }
  33.101 +}
  33.102 +
  33.103 +if {$dbdir == ""} {
  33.104 +    puts stderr "Must create /var/dtn or /var/tmp/dtn storage directory"
  33.105 +    exit 1
  33.106 +}
  33.107 +
  33.108 +#
  33.109 +# storage set payloaddir <dir>
  33.110 +#
  33.111 +# Set the directory to be used for bundle payload files
  33.112 +#
  33.113 +storage set payloaddir $dbdir/bundles
  33.114 +
  33.115 +#
  33.116 +# storage set dbname <db>
  33.117 +#
  33.118 +# Set the database name (appended with .db as the filename in berkeley
  33.119 +# db, used as-is for SQL variants
  33.120 +#
  33.121 +storage set dbname     DTN
  33.122 +
  33.123 +#
  33.124 +# storage set dbdir    <dir>
  33.125 +#
  33.126 +#
  33.127 +# When using berkeley db, set the directory to be used for the
  33.128 +# database files and the name of the files and error log.
  33.129 +#
  33.130 +storage set dbdir      $dbdir/db
  33.131 +
  33.132 +########################################
  33.133 +#
  33.134 +# Routing configuration
  33.135 +#
  33.136 +########################################
  33.137 +
  33.138 +#
  33.139 +# Set the algorithm used for dtn routing.
  33.140 +#
  33.141 +# route set type [static | flood | neighborhood | linkstate | external]
  33.142 +#
  33.143 +route set type prophet
  33.144 +
  33.145 +# Set up some routing parameters
  33.146 +# Age out node parameters every 12 hours - 12 * 60 * 60 seconds
  33.147 +prophet set age_period 43200
  33.148 +
  33.149 +# Decay probabilities by units of a day - kappa is set in milliseconds - 24 * 60 * 60 * 1000
  33.150 +prophet set kappa 86400000
  33.151 +
  33.152 +#
  33.153 +# route local_eid <eid>
  33.154 +#
  33.155 +# Set the local administrative id of this node. The default just uses
  33.156 +# the internet hostname plus the appended string ".dtn" to make it
  33.157 +# clear that the hostname isn't really used for DNS lookups.
  33.158 +#
  33.159 +# This is *proxy* gateway machine!  The outstations route to here and 
  33.160 +# we relay on to the real gateway on rosebud.
  33.161 +route local_eid "dtn://gateway.nomadic.n4c.eu"
  33.162 +
  33.163 +#
  33.164 +# External router specific options
  33.165 +#
  33.166 +# route set server_port 8001
  33.167 +# route set hello_interval 30
  33.168 +# route set schema "/etc/router.xsd"
  33.169 +
  33.170 +########################################
  33.171 +#
  33.172 +# TCP convergence layer configuration
  33.173 +#
  33.174 +########################################
  33.175 +
  33.176 +#
  33.177 +# interface add [name] [CL]
  33.178 +#
  33.179 +# Add an input interface to listen on addr:port for incoming bundles
  33.180 +# from other tcp / udp convergence layers
  33.181 +#
  33.182 +# For IP-based interfaces, interfaces listen on INADDR_ANY port 4556
  33.183 +# by default. These can be overridden by using the local_addr and/or
  33.184 +# local_port arguments.
  33.185 +interface add tcp0 tcp local_port=4557
  33.186 +#interface add udp0 udp
  33.187 +
  33.188 +#
  33.189 +# link add <name> <nexthop> <type> <clayer> <args...>
  33.190 +#
  33.191 +# Add a link to a peer node.
  33.192 +# 
  33.193 +# For IP-based links (tcp or udp), the nexthop should contain a DNS
  33.194 +# hostname or IP address, followed optionally by a : and a port. If
  33.195 +# the port is not specified, the default of 4556 is used.
  33.196 +#
  33.197 +# e.g.  link add link1 dtn.dtnrg.org ONDEMAND tcp
  33.198 +#       link add link2 dtn2.dtnrg.org:10000 ONDEMAND tcp
  33.199 +
  33.200 +#
  33.201 +# route add <dest> <link|peer>
  33.202 +#
  33.203 +# Add a route to the given bundle endpoint id pattern <dest> using the
  33.204 +# specified link name or peer endpoint.
  33.205 +#
  33.206 +# e.g. route add dtn://host.domain/* tcp0
  33.207 +#link add linkToN4C 130.240.97.204 ONDEMAND tcp
  33.208 +#link add linkToWeeePc 81.187.254.251 ONDEMAND tcp
  33.209 +#route add dtn://dtn.n4c.eu/* linkToN4C
  33.210 +#route add dtn://Weee-Pc1.folly.org.uk/* linkToWeeePc
  33.211 +
  33.212 +########################################
  33.213 +#
  33.214 +# Service discovery
  33.215 +#
  33.216 +########################################
  33.217 +
  33.218 +#
  33.219 +# discovery add <name> <af> <opts...>
  33.220 +# discovery announce <cl_name> <discovery_name> <cl_type> <opts...>
  33.221 +#
  33.222 +# Add a local neighborhood discovery module
  33.223 +#
  33.224 +# e.g. discovery add discovery_bonjour bonjour
  33.225 +discovery add ip_disc ip port=4301
  33.226 +discovery announce tcp0 ip_disc tcp cl_port=4557 interval=10
  33.227 +
  33.228 +########################################
  33.229 +#
  33.230 +# Parameter Tuning
  33.231 +#
  33.232 +########################################
  33.233 +
  33.234 +#
  33.235 +# Set the size threshold for the daemon so any bundles smaller than this
  33.236 +# size maintain a shadow copy in memory to minimize disk accesses. 
  33.237 +#
  33.238 +# param set payload_mem_threshold 16384
  33.239 +
  33.240 +#
  33.241 +# Test option to keep all bundle files in the filesystem, even after the
  33.242 +# bundle is no longer present at the daemon.
  33.243 +#
  33.244 +# param set payload_test_no_remove true
  33.245 +
  33.246 +#
  33.247 +# Set the size for which the tcp convergence layer sends partial reception
  33.248 +# acknowledgements. Used with reactive fragmentation
  33.249 +#
  33.250 +# param set tcpcl_partial_ack_len 4096
  33.251 +
  33.252 +#
  33.253 +# Set if bundles are automatically deleted after transmission
  33.254 +#
  33.255 +# param set early_deletion true
  33.256 +
  33.257 +# (others exist but are not fully represented here)
  33.258 +
  33.259 +log /dtnd info "dtnd configuration parsing complete"
  33.260 +
  33.261 +## emacs settings to use tcl-mode by default
  33.262 +## Local Variables: ***
  33.263 +## mode:tcl ***
  33.264 +## End: ***
    34.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    34.2 +++ b/relay/dtnrun/dtn/dtn_relay_static.conf	Tue Mar 16 13:36:42 2010 +0000
    34.3 @@ -0,0 +1,250 @@
    34.4 +#
    34.5 +# dtn.conf
    34.6 +#
    34.7 +# Configuration file for the nomadic.n4c.eu relay on the statically
    34.8 +# routed TCD village network. The machine with the relay runs two daemons
    34.9 +# one talking to the statically routed network and the other talking
   34.10 +# to the proxy gateway on the PRoPHET network.
   34.11 +
   34.12 +# The static side uses the standard set of port numbers whereas the
   34.13 +# the PRoPHET side uses a set of non standard ports so that in can 
   34.14 +# coexist in the same machine.  Note that the bundle storage database
   34.15 +# needs to be kept separate between the two daemons.
   34.16 +
   34.17 +# The daemon uses a tcl interpreter to parse this file, thus any standard
   34.18 +# tcl commands are valid, and all settings are get/set using a single
   34.19 +# 'set' functions as: <module> set <var> <val?>
   34.20 +#
   34.21 +
   34.22 +log /dtnd info "dtnd parsing configuration..."
   34.23 +
   34.24 +# Set user for mail
   34.25 +set user "user1"
   34.26 +
   34.27 +########################################
   34.28 +#
   34.29 +# Daemon Console Configuration
   34.30 +#
   34.31 +########################################
   34.32 +
   34.33 +#
   34.34 +# console set stdio [ true | false ]
   34.35 +#
   34.36 +# If set to false, disable the interactive console on stdin/stdout.
   34.37 +# The default is set to true (unless the dtnd process is run as a
   34.38 +# daemon).
   34.39 +#
   34.40 +# console set stdio false
   34.41 +
   34.42 +#
   34.43 +# console set addr <port>
   34.44 +# console set port <port>
   34.45 +#
   34.46 +# Settings for the socket based console protocol.
   34.47 +# (this interprets user commands)
   34.48 +#
   34.49 +console set addr 127.0.0.1
   34.50 +console set port 5050
   34.51 +
   34.52 +#
   34.53 +# console set prompt <prompt>
   34.54 +#
   34.55 +# Set the prompt string.  Helps if running multiple dtnd's
   34.56 +#
   34.57 +set shorthostname [lindex [split [info hostname] .] 0]
   34.58 +console set prompt "$shorthostname dtn-static% "
   34.59 +
   34.60 +########################################
   34.61 +#
   34.62 +# Storage Configuration
   34.63 +#
   34.64 +########################################
   34.65 +
   34.66 +#
   34.67 +# storage set type [ berkeleydb | postgres | mysql  | external ]
   34.68 +#
   34.69 +# Set the storage system to be used
   34.70 +#
   34.71 +storage set type berkeleydb
   34.72 +
   34.73 +# the following are for use with external data stores
   34.74 +#
   34.75 +# The server port to connect to (on localhost)
   34.76 +# Note that 62345 has no special significance -- chosen randomly
   34.77 +storage set server_port 62345
   34.78 +
   34.79 +# The external data store schema location, which can be
   34.80 +# found in dtn2/oasys/storage/DS.xsd.
   34.81 +storage set schema /etc/DS.xsd
   34.82 +
   34.83 +
   34.84 +#
   34.85 +# Do a runtime check for the standard locations for the persistent
   34.86 +# storage directory
   34.87 +#
   34.88 +set dbdir "/home/user/dtnrun/bundlestore/village"
   34.89 +if {$dbdir == ""} {
   34.90 +    foreach dir {/var/dtn /var/tmp/dtn} {
   34.91 +        if {[file isdirectory $dir]} {
   34.92 +            set dbdir $dir
   34.93 +            break
   34.94 +        }
   34.95 +    }
   34.96 +}
   34.97 +
   34.98 +if {$dbdir == ""} {
   34.99 +    puts stderr "Must create /var/dtn or /var/tmp/dtn storage directory"
  34.100 +    exit 1
  34.101 +}
  34.102 +
  34.103 +#
  34.104 +# storage set payloaddir <dir>
  34.105 +#
  34.106 +# Set the directory to be used for bundle payload files
  34.107 +#
  34.108 +storage set payloaddir $dbdir/bundles
  34.109 +
  34.110 +#
  34.111 +# storage set dbname <db>
  34.112 +#
  34.113 +# Set the database name (appended with .db as the filename in berkeley
  34.114 +# db, used as-is for SQL variants
  34.115 +#
  34.116 +storage set dbname     DTN
  34.117 +
  34.118 +#
  34.119 +# storage set dbdir    <dir>
  34.120 +#
  34.121 +#
  34.122 +# When using berkeley db, set the directory to be used for the
  34.123 +# database files and the name of the files and error log.
  34.124 +#
  34.125 +storage set dbdir      $dbdir/db
  34.126 +
  34.127 +########################################
  34.128 +#
  34.129 +# Routing configuration
  34.130 +#
  34.131 +########################################
  34.132 +
  34.133 +#
  34.134 +# Set the algorithm used for dtn routing.
  34.135 +#
  34.136 +# route set type [static | flood | neighborhood | linkstate | external]
  34.137 +#
  34.138 +route set type static
  34.139 +
  34.140 +#
  34.141 +# route local_eid <eid>
  34.142 +#
  34.143 +# Set the local administrative id of this node. The default just uses
  34.144 +# the internet hostname plus the appended string ".dtn" to make it
  34.145 +# clear that the hostname isn't really used for DNS lookups.
  34.146 +#
  34.147 +route local_eid "dtn://relay.nomadic.n4c.eu"
  34.148 +
  34.149 +#
  34.150 +# External router specific options
  34.151 +#
  34.152 +# route set server_port 8001
  34.153 +# route set hello_interval 30
  34.154 +# route set schema "/etc/router.xsd"
  34.155 +
  34.156 +########################################
  34.157 +#
  34.158 +# TCP convergence layer configuration
  34.159 +#
  34.160 +########################################
  34.161 +
  34.162 +#
  34.163 +# interface add [name] [CL]
  34.164 +#
  34.165 +# Add an input interface to listen on addr:port for incoming bundles
  34.166 +# from other tcp / udp convergence layers
  34.167 +#
  34.168 +# For IP-based interfaces, interfaces listen on INADDR_ANY port 4556
  34.169 +# by default. These can be overridden by using the local_addr and/or
  34.170 +# local_port arguments.
  34.171 +interface add tcp0 tcp
  34.172 +#interface add udp0 udp
  34.173 +
  34.174 +#
  34.175 +# link add <name> <nexthop> <type> <clayer> <args...>
  34.176 +#
  34.177 +# Add a link to a peer node.
  34.178 +# 
  34.179 +# For IP-based links (tcp or udp), the nexthop should contain a DNS
  34.180 +# hostname or IP address, followed optionally by a : and a port. If
  34.181 +# the port is not specified, the default of 4556 is used.
  34.182 +#
  34.183 +# e.g.  link add link1 dtn.dtnrg.org ONDEMAND tcp
  34.184 +#       link add link2 dtn2.dtnrg.org:10000 ONDEMAND tcp
  34.185 +
  34.186 +#
  34.187 +# route add <dest> <link|peer>
  34.188 +#
  34.189 +# Add a route to the given bundle endpoint id pattern <dest> using the
  34.190 +# specified link name or peer endpoint.
  34.191 +#
  34.192 +# e.g. route add dtn://host.domain/* tcp0
  34.193 +link add linkToVillage 192.168.2.110 ONDEMAND tcp
  34.194 +
  34.195 +route add dtn://gateway.nomadic.n4c.eu/* linkToVillage
  34.196 +route add dtn://basil.dsg.cs.tcd.ie.dtn/* linkToVillage
  34.197 +route add dtn://dtnrouter-11-10.village.n4c.eu.dtn/* linkToVillage
  34.198 +route add dtn://dtnrouter-11-20.village.n4c.eu.dtn/* linkToVillage
  34.199 +route add dtn://dtngateway-2-200.village.n4c.eu.dtn/* linkToVillage
  34.200 +route add dtn://dtnmule-2-10.village.n4c.eu.dtn/* linkToVillage
  34.201 +route add dtn://dtnmule-2-31.village.n4c.eu.dtn/* linkToVillage
  34.202 +
  34.203 +########################################
  34.204 +#
  34.205 +# Service discovery
  34.206 +#
  34.207 +########################################
  34.208 +
  34.209 +#
  34.210 +# discovery add <name> <af> <opts...>
  34.211 +# discovery announce <cl_name> <discovery_name> <cl_type> <opts...>
  34.212 +#
  34.213 +# Add a local neighborhood discovery module
  34.214 +#
  34.215 +# e.g. discovery add discovery_bonjour bonjour
  34.216 +
  34.217 +########################################
  34.218 +#
  34.219 +# Parameter Tuning
  34.220 +#
  34.221 +########################################
  34.222 +
  34.223 +#
  34.224 +# Set the size threshold for the daemon so any bundles smaller than this
  34.225 +# size maintain a shadow copy in memory to minimize disk accesses. 
  34.226 +#
  34.227 +# param set payload_mem_threshold 16384
  34.228 +
  34.229 +#
  34.230 +# Test option to keep all bundle files in the filesystem, even after the
  34.231 +# bundle is no longer present at the daemon.
  34.232 +#
  34.233 +# param set payload_test_no_remove true
  34.234 +
  34.235 +#
  34.236 +# Set the size for which the tcp convergence layer sends partial reception
  34.237 +# acknowledgements. Used with reactive fragmentation
  34.238 +#
  34.239 +# param set tcpcl_partial_ack_len 4096
  34.240 +
  34.241 +#
  34.242 +# Set if bundles are automatically deleted after transmission
  34.243 +#
  34.244 +# param set early_deletion true
  34.245 +
  34.246 +# (others exist but are not fully represented here)
  34.247 +
  34.248 +log /dtnd info "dtnd configuration parsing complete"
  34.249 +
  34.250 +## emacs settings to use tcl-mode by default
  34.251 +## Local Variables: ***
  34.252 +## mode:tcl ***
  34.253 +## End: ***
    35.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    35.2 +++ b/relay/dtnrun/dtn/start_dtn_N810_side.sh	Tue Mar 16 13:36:42 2010 +0000
    35.3 @@ -0,0 +1,21 @@
    35.4 +#! /bin/sh
    35.5 +# PyMail DTN Nomadic Mail System
    35.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    35.7 +#
    35.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    35.9 +#    you may not use this file except in compliance with the License.
   35.10 +#    You may obtain a copy of the License at
   35.11 +# 
   35.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   35.13 +# 
   35.14 +#    Unless required by applicable law or agreed to in writing, software
   35.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   35.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   35.17 +#    See the License for the specific language governing permissions and
   35.18 +#    limitations under the License.
   35.19 +#
   35.20 +
   35.21 +# DTN NAT relay node
   35.22 +# Script to start DTN2 Bundle daemon for N810 (nomadic side)
   35.23 +# This version uses static routing as DTN2 PRoPHET is currently broken.
   35.24 +dtnd -c /home/user/dtnrun/dtn/dtn_relay_adhoc_static.conf -d -o /home/user/dtnrun/logs/dtnd-prophet.log -l info
    36.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    36.2 +++ b/relay/dtnrun/dtn/start_dtn_village_side.sh	Tue Mar 16 13:36:42 2010 +0000
    36.3 @@ -0,0 +1,20 @@
    36.4 +#! /bin/sh
    36.5 +# PyMail DTN Nomadic Mail System
    36.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    36.7 +#
    36.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    36.9 +#    you may not use this file except in compliance with the License.
   36.10 +#    You may obtain a copy of the License at
   36.11 +# 
   36.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   36.13 +# 
   36.14 +#    Unless required by applicable law or agreed to in writing, software
   36.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   36.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   36.17 +#    See the License for the specific language governing permissions and
   36.18 +#    limitations under the License.
   36.19 +#
   36.20 +
   36.21 +# DTN NAT relay node
   36.22 +# Script to start DTN2 Bundle daemon for village (enclave side)
   36.23 +dtnd -c /home/user/dtnrun/dtn/dtn_relay_static.conf -d -o /home/user/dtnrun/logs/dtnd-static.log -l info
    37.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    37.2 +++ b/relay/dtnrun/python/dp_relay_in.py	Tue Mar 16 13:36:42 2010 +0000
    37.3 @@ -0,0 +1,342 @@
    37.4 +#! /usr/bin/python
    37.5 +# PyMail DTN Nomadic Mail System
    37.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    37.7 +#
    37.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    37.9 +#    you may not use this file except in compliance with the License.
   37.10 +#    You may obtain a copy of the License at
   37.11 +# 
   37.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   37.13 +# 
   37.14 +#    Unless required by applicable law or agreed to in writing, software
   37.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   37.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   37.17 +#    See the License for the specific language governing permissions and
   37.18 +#    limitations under the License.
   37.19 +#
   37.20 +
   37.21 +
   37.22 +"""
   37.23 +DTN NAT relay linkage
   37.24 +
   37.25 +This program provides half the relay between the static routing domain used
   37.26 +by TCD's 'village' email relay and the nomadic routing domain that uses PRoPHET
   37.27 +routing.
   37.28 +
   37.29 +It registers to receive any of the emails or reports that might come in to the
   37.30 +relay in the static routing domain and sends them on to the PRoPHET domain.
   37.31 +
   37.32 +This requires destination address translation!
   37.33 +
   37.34 +To minimize the routing in the static domain we use a single eid and multiplex
   37.35 +different users onto it.  These are then sent on to to the different eids in the
   37.36 +PRoPHET domain.
   37.37 +Thus
   37.38 +dtn://relay.nomadic.n4c.eu/<user>/email/in
   37.39 +is mapped to
   37.40 +dtn://<user>.nomadic.n4c.eu/email/in
   37.41 +
   37.42 +We also receive the delivery reports from the gateway but we will just junk them
   37.43 +for the time being (or maybe not bother with delivery reports at all on the static
   37.44 +side for the time being.)
   37.45 +
   37.46 +  """
   37.47 +
   37.48 +import os
   37.49 +import time
   37.50 +import threading
   37.51 +import dtnapi
   37.52 +import socket
   37.53 +import Queue
   37.54 +import logging
   37.55 +import logging.handlers
   37.56 +import logging.config
   37.57 +import re
   37.58 +from select import select
   37.59 +
   37.60 +# Expiration period for mail bundles
   37.61 +# Try 3 days for now - value in seconds
   37.62 +MAIL_EXPIRY = (3 * 24 * 60 * 60)
   37.63 +
   37.64 +# Status report flag values
   37.65 +# See 
   37.66 +REPORT_STATUS_DELIVERED = 8
   37.67 +REPORT_STATUS_DELETED = 16
   37.68 +
   37.69 +# API Ports used for ProPHET and Static regions
   37.70 +# Declared as strings so they can be pushed into the env variable value...
   37.71 +PRoPHET_API_PORT_STR = "5011"
   37.72 +STATIC_API_PORT_STR = "5010"
   37.73 +
   37.74 +# To connect to the right daemon we have to set the DTNAPI_PORT envvironment
   37.75 +# variable before call dtn_open
   37.76 +API_PORT_VAR = "DTNAPI_PORT"
   37.77 +
   37.78 +# List of users we are looking after
   37.79 +users = ["elwynd", "user1", "user2", "user3", "user4", "user5", "user6", "user7", "user8", "user9", "user10"]
   37.80 +
   37.81 +# Local part of source/destination EID
   37.82 +EMAIL_OUT = "email/out"
   37.83 +EMAIL_IN = "email/in"
   37.84 +
   37.85 +# Local part of report EID
   37.86 +EMAIL_REPORTS = "email/reports"
   37.87 +
   37.88 +
   37.89 +# Function to make the EIDs
   37.90 +def build_eids(dtn_handle):
   37.91 +    if dtn_handle == -1:
   37.92 +        raise DtnError("bad DTN handle")
   37.93 +    email_out_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_OUT)
   37.94 +    email_in_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_IN)
   37.95 +    report_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_REPORTS)
   37.96 +    if email_in_addr == None or email_out_addr == None or report_addr == None:
   37.97 +        raise DtnError("failure in dtn_build_local_eid")
   37.98 +    return (email_out_addr, email_in_addr, report_addr)
   37.99 +
  37.100 +def main(log_file, log_config_file):
  37.101 +    # Keep the loop running
  37.102 +    relay_in_run = True
  37.103 +
  37.104 +    # Setup logging
  37.105 +    logging.config.fileConfig(log_config_file)
  37.106 +    pymail_logger = logging.getLogger("dtn_pymail")
  37.107 +    loginfo = pymail_logger.info
  37.108 +    logerror = pymail_logger.error
  37.109 +    logdebug = pymail_logger.debug
  37.110 +
  37.111 +    # Set domain for which we are handling mail
  37.112 +    mail_domain = "nomadic.n4c.eu"
  37.113 +
  37.114 +    # RE pattern for converting destination addresses
  37.115 +    nat_patt = re.compile(r"dtn://relay\.nomadic\.n4c\.eu/([^/]*)/email/in$")
  37.116 +
  37.117 +    # Create a connection to the PRoPHET side DTN daemon
  37.118 +    os.putenv(API_PORT_VAR, PRoPHET_API_PORT_STR)
  37.119 +    prophet_dtn_handle = dtnapi.dtn_open()
  37.120 +    if prophet_dtn_handle == -1:
  37.121 +        logerror("dp_relay_in: unable to open connection with PRoPHET daemon")
  37.122 +    prophet_eid = dtnapi.dtn_build_local_eid(prophet_dtn_handle, "")
  37.123 +    loginfo("dp_relay_in: Connected to PRoPHET daemon at %s", prophet_eid)
  37.124 +
  37.125 +    # Generate the (outgoing) EIDs
  37.126 +    (email_out_eid, email_in_eid, report_eid) = build_eids(prophet_dtn_handle)
  37.127 +
  37.128 +    # Create a connection to the Static side DTN daemon
  37.129 +    os.putenv(API_PORT_VAR, STATIC_API_PORT_STR)
  37.130 +    static_dtn_handle = dtnapi.dtn_open()
  37.131 +    if static_dtn_handle == -1:
  37.132 +        logerror("dp_relay_in: unable to open connection with Static daemon")
  37.133 +    static_eid = dtnapi.dtn_build_local_eid(static_dtn_handle, "")
  37.134 +    static_report_address = dtnapi.dtn_build_local_eid(static_dtn_handle,
  37.135 +                                                       EMAIL_REPORTS)
  37.136 +    loginfo("dp_relay_in: Connected to Static daemon at %s", static_eid)
  37.137 +
  37.138 +    # Destination address for 
  37.139 +
  37.140 +    # Prepare for sending bundles on.
  37.141 +    # Specify a basic bundle spec
  37.142 +    # - We always send the payload as a permanent file
  37.143 +    send_pt = dtnapi.DTN_PAYLOAD_FILE
  37.144 +    # - The source address is always the email_addr
  37.145 +    # - The report address is always the report_addr
  37.146 +    # - The registration id needed in dtn_send is the place
  37.147 +    #   where reports come back to.. we always want reports
  37.148 +    #   but it is unclear why we need to subscribe to the 'session'
  37.149 +    #   just to do the send.The reports will come back through
  37.150 +    #   another connection. Lets try with no regid
  37.151 +    send_regid = dtnapi.DTN_REGID_NONE
  37.152 +    # - The destination address has to be synthesized (later)
  37.153 +    # - We want delivery reports (and maybe deletion reports?)
  37.154 +    send_dopts = dtnapi.DOPTS_DELIVERY_RCPT
  37.155 +    # - Send with normal priority.
  37.156 +    send_pri = dtnapi.COS_NORMAL
  37.157 +    # Mail bundlesshould last a while..
  37.158 +    send_exp = MAIL_EXPIRY
  37.159 +
  37.160 +
  37.161 +    # Register to receive bundles on email in addresses for each user
  37.162 +    for user in users:
  37.163 +        # Address at which email will arrive on static relay
  37.164 +        user_email_in_addr = static_eid + user + "/" + EMAIL_IN
  37.165 +        
  37.166 +        # Check if email/in registration exists and register if not
  37.167 +        # Otherwise bind to the existing registration
  37.168 +        regid = dtnapi.dtn_find_registration(static_dtn_handle,
  37.169 +                                             user_email_in_addr)
  37.170 +        if (regid == -1):
  37.171 +            # Need to register the EID.. make it permanent with 'DEFER'
  37.172 +            # characteristics so that bundles are saved if theye arrive
  37.173 +            # while the handler is inactive
  37.174 +            # Expire the registration a long time in the future
  37.175 +            exp = 365 * 24 * 60 * 60
  37.176 +            # The registration is immediately active
  37.177 +            passive = False
  37.178 +            # We don't want to execute a script
  37.179 +            script = ""
  37.180 +            
  37.181 +            regid = dtnapi.dtn_register(static_dtn_handle, user_email_in_addr,
  37.182 +                                        dtnapi.DTN_REG_DEFER,
  37.183 +                                        exp, passive, script)
  37.184 +        else:
  37.185 +            dtnapi.dtn_bind(static_dtn_handle, regid)
  37.186 +        
  37.187 +    # Still on the Static side...
  37.188 +    # Check if email/report registration exists and register if not
  37.189 +    static_report_addr = dtnapi.dtn_build_local_eid(static_dtn_handle,
  37.190 +                                                    EMAIL_REPORTS)
  37.191 +    # Otherwise bind to the existing registration
  37.192 +    regid = dtnapi.dtn_find_registration(static_dtn_handle, static_report_addr)
  37.193 +    if (regid == -1):
  37.194 +        # Need to register the EID.. make it permanent with 'DEFER'
  37.195 +        # characteristics so that bundles are saved if theye arrive
  37.196 +        # while the handler is inactive
  37.197 +        # Expire the registration a long time in the future
  37.198 +        exp = 365 * 24 * 60 * 60
  37.199 +        # The registration is immediately active
  37.200 +        passive = False
  37.201 +        # We don't want to execute a script
  37.202 +        script = ""
  37.203 +        
  37.204 +        regid = dtnapi.dtn_register(static_dtn_handle, static_report_addr,
  37.205 +                                    dtnapi.DTN_REG_DEFER,
  37.206 +                                    exp, passive, script)
  37.207 +    else:
  37.208 +        dtnapi.dtn_bind(static_dtn_handle, regid)
  37.209 +
  37.210 +    logdebug("dp_relay_in: Entering DTN relay incoming loop")
  37.211 +
  37.212 +    # Process mail
  37.213 +    # Now sit and wait for incoming email
  37.214 +    # Note that just using dtn_recv with a timeout doesn't work.
  37.215 +    # The blocking I/O upsets the threading seemingly.
  37.216 +    receive_fd = dtnapi.dtn_poll_fd(static_dtn_handle)
  37.217 +    while (relay_in_run):
  37.218 +        # Poll currently sets timeout in ms - this is a bug
  37.219 +        # but -1 means infinity
  37.220 +        dtnapi.dtn_begin_poll(static_dtn_handle, -1)
  37.221 +        # Wait until some IO comes in.
  37.222 +        # This is either a received bundle or an Interrupt (SIGINT).
  37.223 +        try:
  37.224 +            rd_fd, wr_fd, err_fd = select([receive_fd], [], [])
  37.225 +        except:
  37.226 +            # The system call was interrupted
  37.227 +            dtnapi.dtn_cancel_poll(static_dtn_handle)
  37.228 +            relay_in_run = False
  37.229 +            break
  37.230 +
  37.231 +        # The fd is allegedly readable
  37.232 +        # Cancel the poll anyway
  37.233 +        dtnapi.dtn_cancel_poll(static_dtn_handle)
  37.234 +
  37.235 +        # If there are no readable file desciptors something went wrong
  37.236 +        if (len(rd_fd) == 0):
  37.237 +            logerror("dp_relay_in: No readable filedescriptor returned from select")
  37.238 +            relay_in_run = False
  37.239 +            break
  37.240 +
  37.241 +        # Check the dtn fd was actually readable
  37.242 +        if not (receive_fd in rd_fd):
  37.243 +            # this is an error
  37.244 +            self.logerror("dp_relay_in: Unknown fd has become readbale")
  37.245 +            self.receive_run = False
  37.246 +            break
  37.247 +        
  37.248 +        """
  37.249 +        There should always be something to read
  37.250 +        Put in a timeout just in case
  37.251 +        The call to dtn_recv terminates the poll
  37.252 +        We accept the email as a (temporary) file.
  37.253 +        There is a nasty snag here. There is small window
  37.254 +        of opportunity where the bundle has been written
  37.255 +        to a temporary file delivered to this application
  37.256 +        but before file has been renamed and registered in the maildir
  37.257 +        and database. If the application exits during this period
  37.258 +        the email will be lost.  This is not a happy situation.
  37.259 +        At present DTN2 does not have a means for dtn_recv to offer
  37.260 +        a filename that could be used.
  37.261 +        
  37.262 +        Uing DTN_PAYLOAD_MEM would present a similar problem
  37.263 +        During the period the memory image is being written to file
  37.264 +        in the application there is no permanent  copy of the file
  37.265 +        but the DTN daemon believes it has delivered and is busy sending
  37.266 +        a delivery notification.
  37.267 +
  37.268 +        NOTE: On receiving the file, the file name is in the bundle
  37.269 +        payload as a NULL terminated string.  Python leaves the terminating
  37.270 +        byte in place.
  37.271 +        """
  37.272 +        email = dtnapi.dtn_recv(static_dtn_handle, dtnapi.DTN_PAYLOAD_FILE, 1)
  37.273 +        if email == None:
  37.274 +            # just go back to listening (xxxxebd is this sensible?)
  37.275 +            logerror("dp_relay_in: No readable bundle received after select")
  37.276 +            continue
  37.277 +
  37.278 +        # Check for delivery reports
  37.279 +        if email.dest == static_report_address:
  37.280 +            # For the time being log the report and continue..
  37.281 +            # later we will redirect these as well
  37.282 +            if email.status_report != None:
  37.283 +                logdebug("dp_relay_in: Received status report")
  37.284 +                if email.status_report.flags == REPORT_STATUS_DELIVERED:
  37.285 +                    loginfo( "dp_relay_in: Received delivery report re from %s sent %d seq %d" %
  37.286 +                             (email.status_report.bundle_id.source,
  37.287 +                              email.status_report.bundle_id.creation_secs,
  37.288 +                              email.status_report.bundle_id.creation_seqno))
  37.289 +                    # Update the database
  37.290 +  
  37.291 +                elif email.status_report.flags == REPORT_STATUS_DELETED:
  37.292 +                    loginfo("dp_relay_in: Received deletion report re from %s sent %d seq %d" %
  37.293 +                            (email.status_report.bundle_id.source,
  37.294 +                             email.status_report.bundle_id.creation_secs,
  37.295 +                             email.status_report.bundle_id.creation_seqno))
  37.296 +                    # Update the database
  37.297 +
  37.298 +                else:
  37.299 +                    logwarn("dp_relay_in: Received unexpected report: Flags: %d" % email.status_report.flags)
  37.300 +
  37.301 +            continue
  37.302 +
  37.303 +        # Modify the destination address and resend into PRoPHET domain
  37.304 +        fn = email.payload
  37.305 +        l = len(fn)
  37.306 +        if fn[l-1] == "\x00":
  37.307 +            fn = fn[:-1]
  37.308 +        logdebug("dp_relay_in: Got incoming bundle in file %s" % fn)
  37.309 +        loginfo("dp_relay_in: Received email bundle from %s sent %d seq %d" %
  37.310 +                (email.source,
  37.311 +                 email.creation_secs,
  37.312 +                 email.creation_seqno))
  37.313 +        
  37.314 +        # Build the destination EID
  37.315 +        nat_match = nat_patt.match(email.dest)
  37.316 +        if nat_match == None:
  37.317 +            logerror("dp_relay_in: Non-standard bundle destination %s" % email.dest)
  37.318 +            continue
  37.319 +        
  37.320 +        destn_eid = "dtn://%s.%s/%s" % (nat_match.group(1), mail_domain, EMAIL_IN)
  37.321 +
  37.322 +        loginfo("dp_relay_in: Sending message to %s" % destn_eid)
  37.323 +
  37.324 +        # Send the bundle
  37.325 +        # The last two empty strings are to do with sessions which we don't use
  37.326 +        bundle_id = dtnapi.dtn_send(prophet_dtn_handle, send_regid,
  37.327 +                                    email_out_eid, destn_eid,
  37.328 +                                    report_eid, send_pri, send_dopts,
  37.329 +                                    send_exp, send_pt, fn, "", "")
  37.330 +        if bundle_id == None:
  37.331 +            logwarn("dp_relay_in: Sending of message to %s failed" % destn_eid)
  37.332 +        else:
  37.333 +            # Store the details of the sent bundle
  37.334 +            loginfo("dp_relay_in: %s sent at %d, seq no %d" %
  37.335 +                    (bundle_id.source,
  37.336 +                     bundle_id.creation_secs,
  37.337 +                     bundle_id.creation_seqno))
  37.338 +
  37.339 +    dtnapi.dtn_close(static_dtn_handle)
  37.340 +    dtnapi.dtn_close(prophet_dtn_handle)
  37.341 +    loginfo("dp_relay_in exiting")
  37.342 +
  37.343 +if __name__ == "__main__":
  37.344 +    main("/home/user/dtnrun/logs", "/home/user/dtnrun/python/dtn_pymail_log.conf")
  37.345 +
    38.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    38.2 +++ b/relay/dtnrun/python/dp_relay_out.py	Tue Mar 16 13:36:42 2010 +0000
    38.3 @@ -0,0 +1,336 @@
    38.4 +#! /usr/bin/python
    38.5 +# PyMail DTN Nomadic Mail System
    38.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    38.7 +#
    38.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    38.9 +#    you may not use this file except in compliance with the License.
   38.10 +#    You may obtain a copy of the License at
   38.11 +# 
   38.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   38.13 +# 
   38.14 +#    Unless required by applicable law or agreed to in writing, software
   38.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   38.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   38.17 +#    See the License for the specific language governing permissions and
   38.18 +#    limitations under the License.
   38.19 +#
   38.20 +
   38.21 +
   38.22 +"""
   38.23 +DTN NAT relay linkage
   38.24 +
   38.25 +This program provides half the relay between the static routing domain used
   38.26 +by TCD's 'village' email relay and the nomadic routing domain that uses PRoPHET
   38.27 +routing.
   38.28 +
   38.29 +It registers to receive any of the emails or reports that might come in to the
   38.30 +relay in the PRoPHET routing domain and sends them on to the Static domain.
   38.31 +
   38.32 +It is connected to the proxy gateway.nomadic.n4c.eu and so does not require any
   38.33 +address translation of the destination address.  However, the reports
   38.34 +address will be inappropriate and will be modified to be
   38.35 +dtn://relay.nomadic.n4c.eu/email/reports
   38.36 +and the source address has to be translated so that it is part of the
   38.37 +relay address:
   38.38 +dtn://<user>.nomadic.n4c.eu/email/in
   38.39 +is mapped to
   38.40 +dtn://relay.nomadic.n4c.eu/<user>/email/in
   38.41 +
   38.42 +The delivery reports be sent back to this address and the relay system needs to sort
   38.43 +out what needs to be sent onwards. 
   38.44 +
   38.45 +We also receive the delivery reports from the outstations but we will just junk them
   38.46 +for the time being (or maybe not bother with delivery reports at all on the static
   38.47 +side for the time being.)
   38.48 +
   38.49 +"""
   38.50 +
   38.51 +import os
   38.52 +import time
   38.53 +import threading
   38.54 +import dtnapi
   38.55 +import socket
   38.56 +import Queue
   38.57 +import logging
   38.58 +import logging.handlers
   38.59 +import logging.config
   38.60 +import re
   38.61 +from select import select
   38.62 +
   38.63 +# Expiration period for mail bundles
   38.64 +# Try 3 days for now - value in seconds
   38.65 +MAIL_EXPIRY = (3 * 24 * 60 * 60)
   38.66 +
   38.67 +# Status report flag values
   38.68 +# See 
   38.69 +REPORT_STATUS_DELIVERED = 8
   38.70 +REPORT_STATUS_DELETED = 16
   38.71 +
   38.72 +# API Ports used for ProPHET and Static regions
   38.73 +# Declared as strings so they can be pushed into the env variable value...
   38.74 +PRoPHET_API_PORT_STR = "5011"
   38.75 +STATIC_API_PORT_STR = "5010"
   38.76 +
   38.77 +# To connect to the right daemon we have to set the DTNAPI_PORT envvironment
   38.78 +# variable before call dtn_open
   38.79 +API_PORT_VAR = "DTNAPI_PORT"
   38.80 +
   38.81 +# List of users we are looking after
   38.82 +users = ["elwynd", "user1", "user2"]
   38.83 +
   38.84 +# Local part of source/destination EID
   38.85 +EMAIL_OUT = "email/out"
   38.86 +EMAIL_IN = "email/in"
   38.87 +
   38.88 +# Local part of report EID
   38.89 +EMAIL_REPORTS = "email/reports"
   38.90 +
   38.91 +
   38.92 +# Function to make the EIDs
   38.93 +def build_eids(dtn_handle):
   38.94 +    if dtn_handle == -1:
   38.95 +        raise DtnError("bad DTN handle")
   38.96 +    email_out_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_OUT)
   38.97 +    email_in_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_IN)
   38.98 +    report_addr = dtnapi.dtn_build_local_eid(dtn_handle, EMAIL_REPORTS)
   38.99 +    if email_in_addr == None or email_out_addr == None or report_addr == None:
  38.100 +        raise DtnError("failure in dtn_build_local_eid")
  38.101 +    return (email_out_addr, email_in_addr, report_addr)
  38.102 +
  38.103 +def main(log_file, log_config_file):
  38.104 +    # Keep the loop running
  38.105 +    relay_out_run = True
  38.106 +
  38.107 +    # Setup logging
  38.108 +    logging.config.fileConfig(log_config_file)
  38.109 +    pymail_logger = logging.getLogger("dtn_pymail")
  38.110 +    loginfo = pymail_logger.info
  38.111 +    logerror = pymail_logger.error
  38.112 +    logdebug = pymail_logger.debug
  38.113 +
  38.114 +    # Set domain for which we are handling mail
  38.115 +    mail_domain = "nomadic.n4c.eu"
  38.116 +
  38.117 +    # RE pattern for converting source addresses
  38.118 +    nat_patt = re.compile(r"dtn://([^\.]*)\.nomadic\.n4c\.eu/email/out$")
  38.119 +
  38.120 +    # Create a connection to the PRoPHET side DTN daemon
  38.121 +    os.putenv(API_PORT_VAR, PRoPHET_API_PORT_STR)
  38.122 +    prophet_dtn_handle = dtnapi.dtn_open()
  38.123 +    if prophet_dtn_handle == -1:
  38.124 +        logerror("dp_relay_out: unable to open connection with PRoPHET daemon")
  38.125 +    prophet_eid = dtnapi.dtn_build_local_eid(prophet_dtn_handle, "")
  38.126 +    loginfo("dp_relay_out: Connected to PRoPHET daemon at %s", prophet_eid)
  38.127 +
  38.128 +    # Generate the (incoming) EIDs
  38.129 +    (pr_email_out_eid, pr_email_in_eid, pr_report_eid) = build_eids(prophet_dtn_handle)
  38.130 +
  38.131 +    # Create a connection to the Static side DTN daemon
  38.132 +    os.putenv(API_PORT_VAR, STATIC_API_PORT_STR)
  38.133 +    static_dtn_handle = dtnapi.dtn_open()
  38.134 +    if static_dtn_handle == -1:
  38.135 +        logerror("dp_relay_out: unable to open connection with Static daemon")
  38.136 +    static_eid = dtnapi.dtn_build_local_eid(static_dtn_handle, "")
  38.137 +    loginfo("dp_relay_out: Connected to Static daemon at %s", static_eid)
  38.138 +
  38.139 +    # Generate the (outgoing) EIDs
  38.140 +    (st_email_out_eid, st_email_in_eid, st_report_eid) = build_eids(static_dtn_handle)
  38.141 +
  38.142 +    # Prepare for sending bundles on.
  38.143 +    # Specify a basic bundle spec
  38.144 +    # - We always send the payload as a permanent file
  38.145 +    send_pt = dtnapi.DTN_PAYLOAD_FILE
  38.146 +    # - The source address is always the email_addr
  38.147 +    # - The report address is always the report_addr
  38.148 +    # - The registration id needed in dtn_send is the place
  38.149 +    #   where reports come back to.. we always want reports
  38.150 +    #   but it is unclear why we need to subscribe to the 'session'
  38.151 +    #   just to do the send.The reports will come back through
  38.152 +    #   another connection. Lets try with no regid
  38.153 +    send_regid = dtnapi.DTN_REGID_NONE
  38.154 +    # - The destination address has to be synthesized (later)
  38.155 +    # - We want delivery reports (and maybe deletion reports?)
  38.156 +    send_dopts = dtnapi.DOPTS_DELIVERY_RCPT
  38.157 +    # - Send with normal priority.
  38.158 +    send_pri = dtnapi.COS_NORMAL
  38.159 +    # Mail bundlesshould last a while..
  38.160 +    send_exp = MAIL_EXPIRY
  38.161 +
  38.162 +    # Register to receive bundles intended for gateway.nomadic.n4c.eu
  38.163 +    # and reports to this address on the PRoPHET side
  38.164 +        
  38.165 +    # Check if email/report registrations exist and register if not
  38.166 +    # Otherwise bind to the existing registration
  38.167 +    # Email address:
  38.168 +    regid = dtnapi.dtn_find_registration(prophet_dtn_handle, pr_email_in_eid)
  38.169 +    if (regid == -1):
  38.170 +        # Need to register the EID.. make it permanent with 'DEFER'
  38.171 +        # characteristics so that bundles are saved if theye arrive
  38.172 +        # while the handler is inactive
  38.173 +        # Expire the registration a long time in the future
  38.174 +        exp = 365 * 24 * 60 * 60
  38.175 +        # The registration is immediately active
  38.176 +        passive = False
  38.177 +        # We don't want to execute a script
  38.178 +        script = ""
  38.179 +        
  38.180 +        regid = dtnapi.dtn_register(prophet_dtn_handle, pr_email_in_eid,
  38.181 +                                    dtnapi.DTN_REG_DEFER,
  38.182 +                                    exp, passive, script)
  38.183 +    else:
  38.184 +        dtnapi.dtn_bind(prophet_dtn_handle, regid)
  38.185 +
  38.186 +    # Report address:
  38.187 +    regid = dtnapi.dtn_find_registration(prophet_dtn_handle, pr_report_eid)
  38.188 +    if (regid == -1):
  38.189 +        # Need to register the EID.. make it permanent with 'DEFER'
  38.190 +        # characteristics so that bundles are saved if theye arrive
  38.191 +        # while the handler is inactive
  38.192 +        # Expire the registration a long time in the future
  38.193 +        exp = 365 * 24 * 60 * 60
  38.194 +        # The registration is immediately active
  38.195 +        passive = False
  38.196 +        # We don't want to execute a script
  38.197 +        script = ""
  38.198 +        
  38.199 +        regid = dtnapi.dtn_register(prophet_dtn_handle, pr_report_eid,
  38.200 +                                    dtnapi.DTN_REG_DEFER,
  38.201 +                                    exp, passive, script)
  38.202 +    else:
  38.203 +        dtnapi.dtn_bind(prophet_dtn_handle, regid)
  38.204 +
  38.205 +    logdebug("dp_relay_out: Entering DTN relay outing loop")
  38.206 +
  38.207 +    # Process mail
  38.208 +    # Now sit and wait for incoming email
  38.209 +    # Note that just using dtn_recv with a timeout doesn't work.
  38.210 +    # The blocking I/O upsets the threading seemingly.
  38.211 +    receive_fd = dtnapi.dtn_poll_fd(prophet_dtn_handle)
  38.212 +    while (relay_out_run):
  38.213 +        # Poll currently sets timeout in ms - this is a bug
  38.214 +        # but -1 means infinity
  38.215 +        dtnapi.dtn_begin_poll(prophet_dtn_handle, -1)
  38.216 +        # Wait until some IO comes in.
  38.217 +        # This is either a received bundle or an Interrupt (SIGINT).
  38.218 +        try:
  38.219 +            rd_fd, wr_fd, err_fd = select([receive_fd], [], [])
  38.220 +        except:
  38.221 +            # The system call was interrupted
  38.222 +            dtnapi.dtn_cancel_poll(prophet_dtn_handle)
  38.223 +            relay_out_run = False
  38.224 +            break
  38.225 +
  38.226 +        # The fd is allegedly readable
  38.227 +        # Cancel the poll anyway
  38.228 +        dtnapi.dtn_cancel_poll(prophet_dtn_handle)
  38.229 +
  38.230 +        # If there are no readable file desciptors something went wrong
  38.231 +        if (len(rd_fd) == 0):
  38.232 +            logerror("dp_relay_out: No readable filedescriptor returned from select")
  38.233 +            relay_out_run = False
  38.234 +            break
  38.235 +
  38.236 +        # Check the dtn fd was actually readable
  38.237 +        if not (receive_fd in rd_fd):
  38.238 +            # this is an error
  38.239 +            self.logerror("dp_relay_out: Unknown fd has become readbale")
  38.240 +            self.receive_run = False
  38.241 +            break
  38.242 +        
  38.243 +        """
  38.244 +        There should always be something to read
  38.245 +        Put in a timeout just in case
  38.246 +        The call to dtn_recv terminates the poll
  38.247 +        We accept the email as a (temporary) file.
  38.248 +        There is a nasty snag here. There is small window
  38.249 +        of opportunity where the bundle has been written
  38.250 +        to a temporary file delivered to this application
  38.251 +        but before file has been renamed and registered in the maildir
  38.252 +        and database. If the application exits during this period
  38.253 +        the email will be lost.  This is not a happy situation.
  38.254 +        At present DTN2 does not have a means for dtn_recv to offer
  38.255 +        a filename that could be used.
  38.256 +        
  38.257 +        Uing DTN_PAYLOAD_MEM would present a similar problem
  38.258 +        During the period the memory image is being written to file
  38.259 +        in the application there is no permanent  copy of the file
  38.260 +        but the DTN daemon believes it has delivered and is busy sending
  38.261 +        a delivery notification.
  38.262 +
  38.263 +        NOTE: On receiving the file, the file name is in the bundle
  38.264 +        payload as a NULL terminated string.  Python leaves the terminating
  38.265 +        byte in place.
  38.266 +        """
  38.267 +        email = dtnapi.dtn_recv(prophet_dtn_handle, dtnapi.DTN_PAYLOAD_FILE, 1)
  38.268 +        if email == None:
  38.269 +            # just go back to listening (xxxxebd is this sensible?)
  38.270 +            logerror("dp_relay_out: No readable bundle received after select")
  38.271 +            continue
  38.272 +
  38.273 +        # Check the destination address and resend into Static domain
  38.274 +        fn = email.payload
  38.275 +        # Check for delivery reports
  38.276 +        if email.dest == pr_report_eid:
  38.277 +            # For the time being log the report and continue..
  38.278 +            # later we will redirect these as well
  38.279 +            if email.status_report != None:
  38.280 +                logdebug("dp_relay_out: Received status report")
  38.281 +                if email.status_report.flags == REPORT_STATUS_DELIVERED:
  38.282 +                    loginfo( "dp_relay_out: Received delivery report re from %s sent %d seq %d" %
  38.283 +                             (email.status_report.bundle_id.source,
  38.284 +                              email.status_report.bundle_id.creation_secs,
  38.285 +                              email.status_report.bundle_id.creation_seqno))
  38.286 +                    # Update the database
  38.287 +  
  38.288 +                elif email.status_report.flags == REPORT_STATUS_DELETED:
  38.289 +                    loginfo("dp_relay_out: Received deletion report re from %s sent %d seq %d" %
  38.290 +                            (email.status_report.bundle_id.source,
  38.291 +                             email.status_report.bundle_id.creation_secs,
  38.292 +                             email.status_report.bundle_id.creation_seqno))
  38.293 +                    # Update the database
  38.294 +
  38.295 +                else:
  38.296 +                    logwarn("dp_relay_out: Received unexpected report: Flags: %d" % email.status_report.flags)
  38.297 +
  38.298 +            continue
  38.299 +
  38.300 +        loginfo("dp_relay_out: Received email bundle from %s sent %d seq %d" %
  38.301 +                (email.source,
  38.302 +                 email.creation_secs,
  38.303 +                 email.creation_seqno))
  38.304 +        l = len(fn)
  38.305 +        if fn[l-1] == "\x00":
  38.306 +            fn = fn[:-1]
  38.307 +        logdebug("dp_relay_out: Got outgoing bundle in file %s" % fn)
  38.308 +
  38.309 +
  38.310 +        # translate the source address
  38.311 +        nat_match = nat_patt.match(email.source)
  38.312 +        if nat_match == None:
  38.313 +            logerror("dp_relay_out: Non-standard bundle source %s" % email.source)
  38.314 +            continue
  38.315 +        
  38.316 +        source_eid = "dtn://relay.%s/%s/%s" % (mail_domain, nat_match.group(1), EMAIL_OUT)
  38.317 +  
  38.318 +        loginfo("dp_relay_out: Sending message from %s" % source_eid)
  38.319 +
  38.320 +        # Send the bundle
  38.321 +        # The last two empty strings are to do with sessions which we don't use
  38.322 +        bundle_id = dtnapi.dtn_send(static_dtn_handle, send_regid,
  38.323 +                                    source_eid, email.dest,
  38.324 +                                    st_report_eid, send_pri, send_dopts,
  38.325 +                                    send_exp, send_pt, fn, "", "")
  38.326 +        if bundle_id == None:
  38.327 +            logwarn("Sending of message to %s failed" % email.dest)
  38.328 +        else:
  38.329 +            # Store the details of the sent bundle
  38.330 +            loginfo("%s sent at %d, seq no %d" %(bundle_id.source,
  38.331 +                                                 bundle_id.creation_secs,
  38.332 +                                                 bundle_id.creation_seqno))
  38.333 +    dtnapi.dtn_close(static_dtn_handle)
  38.334 +    dtnapi.dtn_close(prophet_dtn_handle)
  38.335 +    loginfo("dp_relay_out exiting")
  38.336 +
  38.337 +if __name__ == "__main__":
  38.338 +    main("/home/user/dtnrun/logs", "/home/user/dtnrun/python/dtn_pymail_log.conf")
  38.339 +
    39.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    39.2 +++ b/relay/dtnrun/python/dtn_pymail_log.conf	Tue Mar 16 13:36:42 2010 +0000
    39.3 @@ -0,0 +1,42 @@
    39.4 +[loggers]
    39.5 +keys=root,dtn_pymail,dtn_postfix_pipe
    39.6 +
    39.7 +[handlers]
    39.8 +keys=dtn_pymail_handler,dtn_postfix_pipe_handler
    39.9 +
   39.10 +[formatters]
   39.11 +keys=dtn_pymail_formatter
   39.12 +
   39.13 +[logger_root]
   39.14 +level=NOTSET
   39.15 +handlers=dtn_pymail_handler
   39.16 +
   39.17 +[logger_dtn_pymail]
   39.18 +level=NOTSET
   39.19 +handlers=dtn_pymail_handler
   39.20 +propagate=0
   39.21 +qualname=dtn_pymail
   39.22 +
   39.23 +[logger_dtn_postfix_pipe]
   39.24 +level=NOTSET
   39.25 +handlers=dtn_postfix_pipe_handler
   39.26 +propagate=0
   39.27 +qualname=dtn_postfix_pipe
   39.28 +
   39.29 +[handler_dtn_pymail_handler]
   39.30 +class=handlers.TimedRotatingFileHandler
   39.31 +level=NOTSET
   39.32 +formatter=dtn_pymail_formatter
   39.33 +args=("/home/user/dtnrun/logs/dtn_pymail.log", "midnight", 1, 10)
   39.34 +
   39.35 +[handler_dtn_postfix_pipe_handler]
   39.36 +class=handlers.TimedRotatingFileHandler
   39.37 +level=NOTSET
   39.38 +formatter=dtn_pymail_formatter
   39.39 +args=("/home/user/dtnrun/logs/dtn_postfix_pipe.log", "midnight", 1, 10)
   39.40 +
   39.41 +[formatter_dtn_pymail_formatter]
   39.42 +format=%(asctime)s %(threadName)-16s %(levelname)-8s %(message)s
   39.43 +datefmt=
   39.44 +class=logging.Formatter
   39.45 +
    40.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    40.2 +++ b/relay/dtnrun/start_pymail_relay.sh	Tue Mar 16 13:36:42 2010 +0000
    40.3 @@ -0,0 +1,37 @@
    40.4 +#! /bin/sh
    40.5 +# PyMail DTN Nomadic Mail System
    40.6 +# Copyright (C) Folly Consulting Ltd, 2009, 2010
    40.7 +#
    40.8 +#    Licensed under the Apache License, Version 2.0 (the "License");
    40.9 +#    you may not use this file except in compliance with the License.
   40.10 +#    You may obtain a copy of the License at
   40.11 +# 
   40.12 +#        http://www.apache.org/licenses/LICENSE-2.0
   40.13 +# 
   40.14 +#    Unless required by applicable law or agreed to in writing, software
   40.15 +#    distributed under the License is distributed on an "AS IS" BASIS,
   40.16 +#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   40.17 +#    See the License for the specific language governing permissions and
   40.18 +#    limitations under the License.
   40.19 +#
   40.20 +
   40.21 +
   40.22 +# Script to start DTN NAT relay node part of system
   40.23 +# Check path covers location of DTN2 executables and Python executable
   40.24 +PATH=${PATH}:/usr/local/bin
   40.25 +export PATH
   40.26 +
   40.27 +# Location of this file
   40.28 +PYMAIL_HOME=/home/user/dtnrun
   40.29 +
   40.30 +# Starts two DTN2 Bundle daemons
   40.31 +$PYMAIL_HOME/dtn/start_dtn_N810_side.sh
   40.32 +$PYMAIL_HOME/dtn/start_dtn_village_side.sh
   40.33 +
   40.34 +# Wait a while while the daemons get going
   40.35 +sleep 10
   40.36 +
   40.37 +# Start the Python relay programs
   40.38 +python $PYMAIL_HOME/python/dp_relay_in.py
   40.39 +python $PYMAIL_HOME/python/dp_relay_out.py
   40.40 +