Repository created from code used for N4C Summer Tests 2009.
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 +