# -*- tcl -*- # pop3d_dbox.tcl -- # # Implementation of a simple mailbox database for the pop3 server # Each mailbox is a a directory in a base directory, with each mail # a file in that directory. The mail file contains both headers and # body of the mail. # # Copyright (c) 2002 by Andreas Kupries # # See the file "license.terms" for information on usage and redistribution # of this file, and for a DISCLAIMER OF ALL WARRANTIES. # # RCS: @(#) $Id: pop3d_dbox.tcl,v 1.12 2005/09/28 04:51:23 andreas_kupries Exp $ package require mime ; # tcllib | mime token is result of "get". package require log ; # tcllib | Logging package namespace eval ::pop3d::dbox { # Data storage in the pop3d::dbox module # ------------------------------------- # One array per object containing the db contents. Keyed by user name. # And the information about the last file data was read from. # counter is used to give a unique name for unnamed databases variable counter 0 # commands is the list of subcommands recognized by the server variable commands [list \ "add" \ "base" \ "dele" \ "destroy" \ "exists" \ "get" \ "list" \ "lock" \ "locked" \ "move" \ "remove" \ "size" \ "stat" \ "unlock" \ ] variable version ; set version 1.0.2 } # ::pop3d::dbox::new -- # # Create a new mailbox database with a given name; # if no name is given, use # p3dboxX, where X is a number. # # Arguments: # name name of the mailbox database; if null, generate one. # # Results: # name name of the mailbox database created proc ::pop3d::dbox::new {{name ""}} { variable counter if { [llength [info level 0]] == 1 } { incr counter set name "p3dbox${counter}" } if { ![string equal [info commands ::$name] ""] } { return -code error \ "command \"$name\" already exists,\ unable to create mailbox database" } # Set up the namespace namespace eval ::pop3d::dbox::dbox::$name { variable dir "" variable state ; array set state {} variable locked ; array set locked {} variable transfer ; array set transfer {} } # Create the command to manipulate the mailbox database interp alias {} ::$name {} ::pop3d::dbox::DboxProc $name return $name } ########################## # Private functions follow # ::pop3d::dbox::DboxProc -- # # Command that processes all mailbox database object commands. # # Arguments: # name name of the mailbox database object to manipulate. # args command name and args for the command # # Results: # Varies based on command to perform proc ::pop3d::dbox::DboxProc {name {cmd ""} args} { # Do minimal args checks here if { [llength [info level 0]] == 2 } { return -code error \ "wrong # args: should be \"$name option ?arg arg ...?\"" } # Split the args into command and args components if { [llength [info commands ::pop3d::dbox::_$cmd]] == 0 } { variable commands set optlist [join $commands ", "] set optlist [linsert $optlist "end-1" "or"] return -code error "bad option \"$cmd\": must be $optlist" } eval [list ::pop3d::dbox::_$cmd $name] $args } proc ::pop3d::dbox::_base {name base} { # @c Constructor. Does some more checks on the given base directory. # sanity checks if {$base == {}} { return -code error "directory not specified" } if {! [file exists $base]} { return -code error "base: \"$base\" does not exist" } if {! [file isdirectory $base]} { return -code error "base: \"$base\" not a directory" } if {! [file readable $base]} { return -code error "base: \"$base\" not readable" } if {! [file writable $base]} { return -code error "base: \"$base\" not writable" } upvar ::pop3d::dbox::dbox::${name}::dir dir set dir $base return } # ::pop3d::dbox::_destroy -- # # Destroy a mail database, including its associated command and # data storage. # # Arguments: # name Name of the database to destroy. # # Results: # None. proc ::pop3d::dbox::_destroy {name} { namespace delete ::pop3d::dbox::dbox::$name interp alias {} ::$name {} return } proc ::pop3d::dbox::_add {name mbox} { # @c Create a mailbox with handle . The handle is used as the # @c name of the directory to contain the mails too. # # @a mbox: Reference to the mailbox to be operated on. set dir [CheckDir $name] set mboxpath [file join $dir $mbox] if {[file exists $mboxpath]} { return -code error "cannot add \"$mbox\", mailbox already in existence" } file mkdir $mboxpath return } proc ::pop3d::dbox::_remove {name mbox} { # @c Remove mailbox with handle . This will destroy all mails # @c contained in it too. # # @a mbox: Reference to the mailbox to be operated on. set dir [CheckDir $name] set mboxpath [file join $dir $mbox] if {![file exists $mboxpath]} { return -code error "cannot remove \"$mbox\", mailbox does not exist" } if {[_locked $name $mbox]} { return -code error "cannot remove \"$mbox\", mailbox is locked" } file delete -force $mboxpath return } proc ::pop3d::dbox::_move {name old new} { # @c Change the handle of mailbox to . # # @a old: Reference to the mailbox to be operated on. # @a new: New reference to the mailbox set dir [CheckDir $name] set oldpath [file join $dir $old] set newpath [file join $dir $new] if {![file exists $oldpath]} { return -code error "cannot move \"$old\", mailbox does not exist" } if {[file exists $newpath]} { return -code error \ "cannot move \"$old\", destination \"$new\" already exists" } file rename -force $oldpath $newpath return } proc ::pop3d::dbox::_list {name} { # @c Lists known mailboxes in object. # @r List of mailbox names. set dir [CheckDir $name] set here [pwd] cd $dir set files [glob -nocomplain *] cd $here set res [list] foreach f $files { set mboxpath [file join $dir $f] if {! [file isdirectory $mboxpath]} {continue} if {! [file readable $mboxpath]} {continue} if {! [file writable $mboxpath]} {continue} lappend res $f } return $res } proc ::pop3d::dbox::_exists {name mbox} { # @c Determines existence of mailbox . # @a mbox: Reference to the mailbox to check for. # @r 1 if the mailbox exists, 0 else. set dir [CheckDir $name] set mbox [file join $dir $mbox] return [file exists $mbox] } proc ::pop3d::dbox::_locked {name mbox} { # @c Checks wether the specified mailbox is locked or not. # @a mbox: Reference to the mailbox to check. # @r 1 if the mailbox is locked, 0 else. set dir [CheckDir $name] set mbox [file join $dir $mbox] upvar ::pop3d::dbox::dbox::${name}::locked locked return [::info exists locked($mbox)] } # -- interface to the pop server (storage callback) -- proc ::pop3d::dbox::_lock {name mbox} { # @c Locks the given mailbox, additionally stores a list of the # @c available files in the manager state. All files (= messages) # @c added to the mailbox after this operation will be ignored # @c during the session. # # @a mbox: Reference to the mailbox to be locked. # @r 1 if mailbox was locked sucessfully, 0 else. # locked already ? if {[_locked $name $mbox]} { return 0 } set dir [Check $name $mbox] # Compute a list of message files residing in the mailbox directory upvar ::pop3d::dbox::dbox::${name}::state state upvar ::pop3d::dbox::dbox::${name}::locked locked set state($dir) [lsort [glob -nocomplain [file join $dir *]]] set locked($dir) 1 return 1 } proc ::pop3d::dbox::_unlock {name mbox} { # @c A locked mailbox is unlocked, thereby made available # @c to other sessions. # # @a mbox: Reference to the mailbox to be locked. # not locked ? if {![_locked $name $mbox]} {return} set dir [Check $name $mbox] upvar ::pop3d::dbox::dbox::${name}::state state upvar ::pop3d::dbox::dbox::${name}::locked locked unset state($dir) unset locked($dir) return } proc ::pop3d::dbox::_stat {name mbox} { # @c Determines the number of messages picked up by . # @c Will fail if the mailbox was not locked. # # @a mbox: Reference to the mailbox queried. # @r The number of messages in the mailbox set dir [Check $name $mbox] if {![_locked $name $mbox]} { return -code error "mailbox \"$mbox\" is not locked" } upvar ::pop3d::dbox::dbox::${name}::state state return [llength $state($dir)] } proc ::pop3d::dbox::_size {name mbox {msgId {}}} { # @c Determines the size of the specified message, in bytes. # # @a mbox: Reference to the mailbox to be operated on. # @a msgId: Numerical index of the message to look at. # @r size of the message in bytes. log::log debug "$name size $mbox ($msgId)" set dir [Check $name $mbox] log::log debug "$name mbox dir = $dir" upvar ::pop3d::dbox::dbox::${name}::state state if {$msgId == {}} { log::log debug "$name size /full" # Full size of the maildrop requested. if {![info exists state($dir)]} { # No stat before size, assume that there are no messages # in the maildrop, which implies that the maildrop is # empty, i.e. of size 0. return 0 } set n 0 set k [llength $state($dir)] for {set id 0} {$id < $k} {incr id} { incr n [file size [lindex $state($dir) $id]] } return $n } if { ($msgId < 1) || (![info exists state($dir)]) || ([llength $state($dir)] < $msgId) } { return -code error "id \"$msgId\" out of range" } incr msgId -1 ## log::log debug "$name msg mails = $state($dir)" log::log debug "$name msg file = [lindex $state($dir) $msgId]" return [file size [lindex $state($dir) $msgId]] } proc ::pop3d::dbox::_dele {name mbox msgList} { # @c Deletes the specified messages from the mailbox. This should # @c be followed by a as the state is not updated # @c accordingly. # # @a mbox: Reference to the mailbox to be operated on. # @a msgList: List of message ids. set dir [Check $name $mbox] if {[llength $msgList] == 0} { return -code error "nothing to delete" } # @d The code assumes that the id's in the list were already # @d checked against the maximal number of messages. upvar ::pop3d::dbox::dbox::${name}::state state foreach msgId $msgList { if { ($msgId < 1) || (![info exists state($dir)]) || ([llength $state($dir)] < $msgId) } { return -code error "id \"$msgId\" out of range" } } foreach msgId $msgList { file delete [lindex $state($dir) [incr msgId -1]] } # the mailbox state is unusable now. return } proc ::pop3d::dbox::_get {name mbox msgId} { set dir [Check $name $mbox] upvar ::pop3d::dbox::dbox::${name}::state state if { ($msgId < 1) || (![info exists state($dir)]) || ([llength $state($dir)] < $msgId) } { return -code error "id \"$msgId\" out of range" } incr msgId -1 set mailfile [lindex $state($dir) $msgId] set token [::mime::initialize -file $mailfile] return $token } ########################### ########################### # Internal helper commands. proc ::pop3d::dbox::Check {name mbox} { # @c Internal procedure. Used to map a mailbox handle # @c to the directory containing the messages. # @a mbox: Reference to the mailbox to be operated on. # @r Path of directory holding the message files of the # @r specified mailbox. set dir [CheckDir $name] set mboxpath [file join $dir $mbox] if {! [file exists $mboxpath]} { return -code error "\"$mbox\" does not exist" } if {! [file isdirectory $mboxpath]} { return -code error "\"$mbox\" is not a directory" } if {! [file readable $mboxpath]} { return -code error "\"$mbox\" is not readable" } if {! [file writable $mboxpath]} { return -code error "\"$mbox\" is not writable" } return $mboxpath } proc ::pop3d::dbox::CheckDir {name} { upvar ::pop3d::dbox::dbox::${name}::dir dir if {$dir == {}} { return -code error "base directory not specified" } return $dir } ########################## # Module initialization package provide pop3d::dbox $::pop3d::dbox::version